feat(all): adding python library wrapper

Adding a Python library wrapper using pyo3

BREAKING CHANGE:
This commit is contained in:
Scott Lyons 2024-08-15 00:36:45 -07:00
commit f4ba4a9c39
12 changed files with 2566 additions and 703 deletions

View file

@ -165,10 +165,15 @@ impl SzurubooruClient {
/// # };
/// # ()
/// ```
pub fn with_fields<'a>(&'a self, fields: Vec<&'a str>) -> SzurubooruRequest {
pub fn with_fields(&self, fields: Vec<String>) -> SzurubooruRequest {
self.request().with_fields(fields)
}
/// The same as (with_fields)[SzurubooruClient::with_fields], but accepts an Option type instead
pub fn with_optional_fields(&self, fields: Option<Vec<String>>) -> SzurubooruRequest {
self.request().with_optional_fields(fields)
}
/// Construct a new request with the given limit
/// The Szurubooru API supports limiting the number of resources returned for Paginated
/// API endpoints.
@ -190,6 +195,11 @@ impl SzurubooruClient {
self.request().with_limit(limit)
}
/// The same as (with_limit)[SzurubooruClient::with_limit], but accepts an Option type instead
pub fn with_optional_limit(&self, limit: Option<u32>) -> SzurubooruRequest {
self.request().with_optional_limit(limit)
}
/// Construct a new request starting at the given offset
/// The Szurubooru API supports offsetting the results returned from Paginated API
/// endpoints. Use this offset in combination with the limit to page through
@ -211,12 +221,17 @@ impl SzurubooruClient {
pub fn with_offset(&self, offset: u32) -> SzurubooruRequest {
self.request().with_offset(offset)
}
/// The same as (with_offset)[SzurubooruClient::with_offset], but accepts an Option type instead
pub fn with_optional_offset(&self, offset: Option<u32>) -> SzurubooruRequest {
self.request().with_optional_offset(offset)
}
}
#[derive(Debug)]
/// A type that represents a single Szurubooru request.
pub struct SzurubooruRequest<'a> {
fields: Option<Vec<&'a str>>,
fields: Option<Vec<String>>,
limit: Option<u32>,
offset: Option<u32>,
client: &'a SzurubooruClient,
@ -250,11 +265,19 @@ impl<'a> SzurubooruRequest<'a> {
/// # };
/// # ()
/// ```
pub fn with_fields(mut self, fields: Vec<&'a str>) -> Self {
pub fn with_fields(mut self, fields: Vec<String>) -> Self {
self.fields = Some(fields);
self
}
/// The same as (with_fields)[SzurubooruRequest::with_fields], but accepts an Option type instead
pub fn with_optional_fields(mut self, val: Option<Vec<String>>) -> Self {
match val {
Some(f) => self.with_fields(f),
None => self,
}
}
/// Limit the number of returned results
/// The Szurubooru API supports limiting the number of resources returned for Paginated
/// API endpoints.
@ -277,6 +300,14 @@ impl<'a> SzurubooruRequest<'a> {
self
}
/// The same as (with_limit)[SzurubooruRequest::with_limit], but accepts an Option type instead
pub fn with_optional_limit(mut self, val: Option<u32>) -> Self {
match val {
Some(f) => self.with_limit(f),
None => self,
}
}
/// Skip a certain number of records
/// The Szurubooru API supports offsetting the results returned from Paginated API
/// endpoints. Use this offset in combination with the limit to page through
@ -300,6 +331,14 @@ impl<'a> SzurubooruRequest<'a> {
self
}
/// The same as (with_offset)[SzurubooruRequest::with_offset], but accepts an Option type instead
pub fn with_optional_offset(self, val: Option<u32>) -> Self {
match val {
Some(f) => self.with_offset(f),
None => self,
}
}
#[doc(hidden)]
fn prep_request<T>(
&self,
@ -406,8 +445,6 @@ impl<'a> SzurubooruRequest<'a> {
let response = self
.handle_response(response.map_err(SzurubooruClientError::RequestError)?)
.await?;
//.error_for_status()
//.map_err(SzurubooruClientError::RequestError)?;
let response_text = response
.text()
@ -639,6 +676,23 @@ impl<'a> SzurubooruRequest<'a> {
.map(|pr| self.propagate_urls(pr))
}
/// Update an existing post from a given URL
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
pub async fn update_post_from_url(
&self,
post_id: u32,
update_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
assert!(update_post.content_url.is_some());
let path = format!("/api/post/{post_id}");
self.create_update_post_from_url(&path, Method::PUT, update_post)
.await
.map(|pr| self.propagate_urls(pr))
}
// Create function to upload by byte array in the future
fn part_from_file(&self, file: &mut File) -> SzurubooruResult<Part> {
let mut bytes = vec![];
file.read_to_end(&mut bytes)
@ -825,6 +879,27 @@ impl<'a> SzurubooruRequest<'a> {
.map(|pr| self.propagate_urls(pr))
}
/// Update a post from a token previously generated by
/// (upload_temporary_file_from_path)[SzurubooruRequest::upload_temporary_file_from_path]
pub async fn update_post_from_token(
&self,
post_id: u32,
update_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
assert!(update_post.content_token.is_some());
let url = format!("/api/post/{post_id}");
self.create_update_post_from_file(
None,
None,
None::<String>,
&url,
Method::PUT,
update_post,
)
.await
.map(|pr| self.propagate_urls(pr))
}
async fn get_post_content(
&self,
post_id: u32,
@ -989,32 +1064,38 @@ impl<'a> SzurubooruRequest<'a> {
.map(|isr| self.propagate_urls(isr))
}
// Need to add a reverse search for bytes
/// Searches for an exact match of a file based on the SHA1 checksum
pub async fn posts_for_file(
&self,
mut file: &mut File,
) -> SzurubooruResult<PagedSearchResult<PostResource>> {
) -> SzurubooruResult<Option<PostResource>> {
let mut hasher = Sha1::new();
std::io::copy(&mut file, &mut hasher).map_err(SzurubooruClientError::IOError)?;
let hash = hasher.finalize();
let hex_string = hex::encode(hash);
let qt = QueryToken::token(PostNamedToken::ContentChecksum, hex_string);
self.list_posts(Some(&vec![qt]))
let mut psr = self
.list_posts(Some(&vec![qt]))
.await
.map(|psr| self.propagate_urls(psr))
.map(|psr| self.propagate_urls(psr))?;
Ok(if psr.total > 1 {
Some(psr.results.swap_remove(0))
} else {
None
})
}
/// Searches for an exact match of a file path based on the SHA1 checksum
pub async fn posts_for_file_path(
&self,
file_path: impl AsRef<Path>,
) -> SzurubooruResult<PagedSearchResult<PostResource>> {
) -> SzurubooruResult<Option<PostResource>> {
let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?;
self.posts_for_file(&mut file)
.await
.map(|psr| self.propagate_urls(psr))
self.posts_for_file(&mut file).await
}
/// Retrieves information about an existing post.
@ -1516,13 +1597,13 @@ impl<'a> SzurubooruRequest<'a> {
/// instead of a password.
pub async fn create_user_token<T>(
&self,
name: T,
user_name: T,
create_token: &CreateUpdateUserAuthToken,
) -> SzurubooruResult<UserAuthTokenResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user-token/{name}");
let path = format!("/api/user-token/{user_name}");
self.do_request(Method::POST, &path, None, Some(create_token))
.await
.map(|r| self.propagate_urls(r))

View file

@ -3,6 +3,9 @@
use crate::models::SzuruEither;
use base64::EncodeSliceError;
use derive_builder::UninitializedFieldError;
#[cfg(feature = "python")]
use pyo3::{exceptions::PyRuntimeError, prelude::*};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use url::ParseError as UParseError;
@ -47,6 +50,9 @@ pub enum SzurubooruClientError {
/// Error serializing an object as JSON
#[error("JSON Serialization error: {0}")]
JSONSerializationError(#[source] serde_json::Error),
/// Error when validation fails for one of the Builder types
#[error("Builder validation error: {0}")]
BuilderValidationError(String),
/// Error occurred when reading a file
#[error("IO Error: {0}")]
IOError(#[source] std::io::Error),
@ -61,6 +67,19 @@ impl From<SzurubooruServerError> for SzurubooruClientError {
}
}
impl From<UninitializedFieldError> for SzurubooruClientError {
fn from(value: UninitializedFieldError) -> Self {
SzurubooruClientError::BuilderValidationError(value.to_string())
}
}
#[cfg(feature = "python")]
impl std::convert::From<SzurubooruClientError> for PyErr {
fn from(value: SzurubooruClientError) -> Self {
PyRuntimeError::new_err(value.to_string())
}
}
/// Type used to represent success or a failure of some kind
pub type SzurubooruResult<T> = Result<T, SzurubooruClientError>;

View file

@ -35,4 +35,42 @@ pub mod errors;
pub use errors::SzurubooruResult;
pub mod models;
#[cfg(feature = "python")]
pub mod pyclient;
pub mod tokens;
#[cfg(feature = "python")]
pub mod py;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymodule)]
mod szurubooru_client {
use pyo3::prelude::*;
#[pymodule_export]
pub use crate::{
models::{
AroundPostResult, CommentResource, CreateUpdateComment, CreateUpdatePool,
CreateUpdatePoolCategory, CreateUpdatePost, CreateUpdateTag, CreateUpdateTagCategory,
CreateUpdateUser, CreateUpdateUserAuthToken, GlobalInfo, ImageSearchResult,
ImageSearchSimilarPost, MergePool, MergePost, MergeTags, MicroPoolResource,
MicroPostResource, MicroTagResource, MicroUserResource, NoteResource,
PoolCategoryResource, PoolResource, PostResource, PostSafety, PostType,
SnapshotCreationDeletionData, SnapshotData, SnapshotModificationData,
SnapshotOperationType, SnapshotResource, SnapshotResourceType, TagCategoryResource,
TagResource, TagSibling, TemporaryFileUpload, UserAuthTokenResource, UserAvatarStyle,
UserRank, UserResource,
},
py::asynchronous::PythonAsyncClient,
py::synchronous::PythonSyncClient,
tokens::{
anonymous_token, named_token, sort_token, special_token, CommentNamedToken,
CommentSortToken, PoolNamedToken, PoolSortToken, PostNamedToken, PostSortToken,
PostSpecialToken, SnapshotNamedToken, TagNamedToken, TagSortToken, UserNamedToken,
UserSortToken,
},
};
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,34 @@
use crate::models::PagedSearchResult;
use pyo3::prelude::*;
pub mod asynchronous;
pub mod synchronous;
#[derive(Debug)]
#[pyclass(name = "PagedSearchResult", get_all)]
pub struct PyPagedSearchResult {
pub query: String,
pub offset: u32,
pub limit: u32,
pub total: u32,
pub results: PyObject,
}
#[cfg_attr(all(feature = "python"), pymethods)]
impl PyPagedSearchResult {
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
impl<T: IntoPy<PyObject>> From<PagedSearchResult<T>> for PyPagedSearchResult {
fn from(value: PagedSearchResult<T>) -> Self {
Python::with_gil(|py| PyPagedSearchResult {
query: value.query,
offset: value.offset,
limit: value.limit,
total: value.total,
results: value.results.into_py(py),
})
}
}

View file

@ -0,0 +1,709 @@
use crate::models::*;
use crate::py::asynchronous::PythonAsyncClient;
use crate::py::PyPagedSearchResult;
use crate::tokens::QueryToken;
use chrono::{DateTime, Utc};
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyList};
use std::path::{Path, PathBuf};
use tokio::runtime::{Builder, Runtime};
#[pyclass(name = "SzurubooruSyncClient")]
pub struct PythonSyncClient {
client: PythonAsyncClient,
runtime: Runtime,
}
#[pymethods]
impl PythonSyncClient {
#[new]
#[pyo3(signature = (host, username=None, token=None, password=None, allow_insecure=None))]
pub fn new(
host: String,
username: Option<String>,
token: Option<String>,
password: Option<String>,
allow_insecure: Option<bool>,
) -> PyResult<Self> {
let runtime = Builder::new_current_thread().enable_all().build()?;
let client = PythonAsyncClient::new(host, username, token, password, allow_insecure)?;
Ok(Self { client, runtime })
}
#[pyo3(signature = (fields=None))]
pub fn list_tag_categories(
&self,
fields: Option<Vec<String>>,
) -> PyResult<Vec<TagCategoryResource>> {
self.runtime
.block_on(self.client.list_tag_categories(fields))
}
#[pyo3(signature = (name, version, color=None, order=None, fields=None))]
pub fn update_tag_category(
&self,
name: String,
version: u32,
color: Option<String>,
order: Option<u32>,
fields: Option<Vec<String>>,
) -> PyResult<TagCategoryResource> {
self.runtime.block_on(
self.client
.update_tag_category(name, version, color, order, fields),
)
}
#[pyo3(signature = (name, fields=None))]
pub fn get_tag_category(
&self,
name: String,
fields: Option<Vec<String>>,
) -> PyResult<TagCategoryResource> {
self.runtime
.block_on(self.client.get_tag_category(name, fields))
}
pub fn delete_tag_category(&self, name: String, version: u32) -> PyResult<()> {
self.runtime
.block_on(self.client.delete_tag_category(name, version))
}
pub fn set_default_tag_category(&self, name: String) -> PyResult<()> {
self.runtime
.block_on(self.client.set_default_tag_category(name))
}
#[pyo3(signature = (query=None, fields=None, limit=None, offset=None))]
pub fn list_tags(
&self,
query: Option<Vec<QueryToken>>,
fields: Option<Vec<String>>,
limit: Option<u32>,
offset: Option<u32>,
) -> PyResult<PyPagedSearchResult> {
self.runtime
.block_on(self.client.list_tags(query, fields, limit, offset))
}
#[pyo3(signature = (names, category=None, description=None, implications=None, suggestions=None, fields=None))]
pub fn create_tag(
&self,
names: Vec<String>,
category: Option<String>,
description: Option<String>,
implications: Option<Vec<String>>,
suggestions: Option<Vec<String>>,
fields: Option<Vec<String>>,
) -> PyResult<TagResource> {
self.runtime.block_on(self.client.create_tag(
names,
category,
description,
implications,
suggestions,
fields,
))
}
#[pyo3(signature = (name, names, category=None, description=None, implications=None, suggestions=None, fields=None))]
pub fn update_tag(
&self,
name: String,
names: Option<Vec<String>>,
category: Option<String>,
description: Option<String>,
implications: Option<Vec<String>>,
suggestions: Option<Vec<String>>,
fields: Option<Vec<String>>,
) -> PyResult<TagResource> {
self.runtime.block_on(self.client.update_tag(
name,
names,
category,
description,
implications,
suggestions,
fields,
))
}
#[pyo3(signature = (name, fields=None))]
pub fn get_tag(&self, name: String, fields: Option<Vec<String>>) -> PyResult<TagResource> {
self.runtime.block_on(self.client.get_tag(name, fields))
}
pub fn delete_tag(&self, name: String, version: u32) -> PyResult<()> {
self.runtime.block_on(self.client.delete_tag(name, version))
}
#[pyo3(signature = (remove_tag, remove_tag_version, merge_to_tag, merge_to_version, fields=None))]
pub fn merge_tag(
&self,
remove_tag: String,
remove_tag_version: u32,
merge_to_tag: String,
merge_to_version: u32,
fields: Option<Vec<String>>,
) -> PyResult<TagResource> {
self.runtime.block_on(self.client.merge_tag(
remove_tag,
remove_tag_version,
merge_to_tag,
merge_to_version,
fields,
))
}
pub fn get_tag_siblings(&self, name: String) -> PyResult<Vec<TagSibling>> {
self.runtime.block_on(self.client.get_tag_siblings(name))
}
#[pyo3(signature = (query=None, fields=None, limit=None, offset=None))]
pub fn list_posts(
&self,
query: Option<Vec<QueryToken>>,
fields: Option<Vec<String>>,
limit: Option<u32>,
offset: Option<u32>,
) -> PyResult<PyPagedSearchResult> {
self.runtime
.block_on(self.client.list_posts(query, fields, limit, offset))
}
#[pyo3(signature = (url=None, token=None, file_path=None, thumbnail_path=None, tags=None, safety=None, source=None,
relations=None, notes=None, flags=None, fields=None))]
pub fn create_post(
&self,
url: Option<String>,
token: Option<String>,
file_path: Option<PathBuf>,
thumbnail_path: Option<PathBuf>,
tags: Option<Vec<String>>,
safety: Option<PostSafety>,
source: Option<String>,
relations: Option<Vec<u32>>,
notes: Option<Vec<NoteResource>>,
flags: Option<Vec<String>>,
fields: Option<Vec<String>>,
) -> PyResult<PostResource> {
self.runtime.block_on(self.client.create_post(
url,
token,
file_path,
thumbnail_path,
tags,
safety,
source,
relations,
notes,
flags,
fields,
))
}
#[pyo3(signature = (post_id, post_version, url=None, token=None, file_path=None,
thumbnail_path=None, tags=None, safety=None, source=None, relations=None, notes=None,
flags=None, fields=None))]
pub fn update_post(
&self,
post_id: u32,
post_version: u32,
url: Option<String>,
token: Option<String>,
file_path: Option<PathBuf>,
thumbnail_path: Option<PathBuf>,
tags: Option<Vec<String>>,
safety: Option<PostSafety>,
source: Option<String>,
relations: Option<Vec<u32>>,
notes: Option<Vec<NoteResource>>,
flags: Option<Vec<String>>,
fields: Option<Vec<String>>,
) -> PyResult<PostResource> {
self.runtime.block_on(self.client.update_post(
post_id,
post_version,
url,
token,
file_path,
thumbnail_path,
tags,
safety,
source,
relations,
notes,
flags,
fields,
))
}
pub fn get_image_bytes<'py>(&self, post_id: u32) -> PyResult<Vec<u8>> {
self.runtime.block_on(self.client.get_image_bytes(post_id))
}
pub fn download_image_to_path(&self, post_id: u32, file_path: PathBuf) -> PyResult<()> {
self.runtime
.block_on(self.client.download_image_to_path(post_id, file_path))
}
pub fn get_thumbnail_bytes<'py>(&self, post_id: u32) -> PyResult<Vec<u8>> {
self.runtime
.block_on(self.client.get_thumbnail_bytes(post_id))
}
pub fn download_thumbnail_to_path(&self, post_id: u32, file_path: PathBuf) -> PyResult<()> {
self.runtime
.block_on(self.client.download_thumbnail_to_path(post_id, file_path))
}
pub fn reverse_search_image(&self, image_path: PathBuf) -> PyResult<ImageSearchResult> {
self.runtime
.block_on(self.client.reverse_search_image(image_path))
}
pub fn post_for_image(&self, image_path: PathBuf) -> PyResult<Option<PostResource>> {
self.runtime
.block_on(self.client.post_for_image(image_path))
}
#[pyo3(signature = (post_id, fields=None))]
pub fn get_post(&self, post_id: u32, fields: Option<Vec<String>>) -> PyResult<PostResource> {
self.runtime.block_on(self.client.get_post(post_id, fields))
}
pub fn get_around_post(&self, post_id: u32) -> PyResult<AroundPostResult> {
self.runtime.block_on(self.client.get_around_post(post_id))
}
pub fn delete_post(&self, post_id: u32, version: u32) -> PyResult<()> {
self.runtime
.block_on(self.client.delete_post(post_id, version))
}
#[pyo3(signature = (remove_post, remove_post_version, merge_to_post,
merge_to_version, fields=None))]
pub fn merge_post(
&self,
remove_post: u32,
remove_post_version: u32,
merge_to_post: u32,
merge_to_version: u32,
fields: Option<Vec<String>>,
) -> PyResult<PostResource> {
self.runtime.block_on(self.client.merge_post(
remove_post,
remove_post_version,
merge_to_post,
merge_to_version,
fields,
))
}
#[pyo3(signature = (post_id, rating, fields=None))]
pub fn rate_post(
&self,
post_id: u32,
rating: i8,
fields: Option<Vec<String>>,
) -> PyResult<PostResource> {
self.runtime
.block_on(self.client.rate_post(post_id, rating, fields))
}
#[pyo3(signature = (post_id, fields=None))]
pub fn favorite_post(
&self,
post_id: u32,
fields: Option<Vec<String>>,
) -> PyResult<PostResource> {
self.runtime
.block_on(self.client.favorite_post(post_id, fields))
}
#[pyo3(signature = (post_id, fields=None))]
pub fn unfavorite_post(
&self,
post_id: u32,
fields: Option<Vec<String>>,
) -> PyResult<PostResource> {
self.runtime
.block_on(self.client.unfavorite_post(post_id, fields))
}
#[pyo3(signature = (fields=None))]
pub fn get_featured_post(&self, fields: Option<Vec<String>>) -> PyResult<Option<PostResource>> {
self.runtime.block_on(self.client.get_featured_post(fields))
}
#[pyo3(signature = (post_id, fields=None))]
pub fn set_featured_post(
&self,
post_id: u32,
fields: Option<Vec<String>>,
) -> PyResult<PostResource> {
self.runtime
.block_on(self.client.set_featured_post(post_id, fields))
}
#[pyo3(signature = (fields=None))]
pub fn list_pool_categories(
&self,
fields: Option<Vec<String>>,
) -> PyResult<Vec<PoolCategoryResource>> {
self.runtime
.block_on(self.client.list_pool_categories(fields))
}
#[pyo3(signature = (name, color=None, fields=None))]
pub fn create_pool_category(
&self,
name: String,
color: Option<String>,
fields: Option<Vec<String>>,
) -> PyResult<PoolCategoryResource> {
self.runtime
.block_on(self.client.create_pool_category(name, color, fields))
}
#[pyo3(signature = (name, version, new_name=None, color=None, fields=None))]
pub fn update_pool_category(
&self,
name: String,
version: u32,
new_name: Option<String>,
color: Option<String>,
fields: Option<Vec<String>>,
) -> PyResult<PoolCategoryResource> {
self.runtime.block_on(
self.client
.update_pool_category(name, version, new_name, color, fields),
)
}
#[pyo3(signature = (name, fields=None))]
pub fn get_pool_category(
&self,
name: String,
fields: Option<Vec<String>>,
) -> PyResult<PoolCategoryResource> {
self.runtime
.block_on(self.client.get_pool_category(name, fields))
}
pub fn delete_pool_category(&self, name: String, version: u32) -> PyResult<()> {
self.runtime
.block_on(self.client.delete_pool_category(name, version))
}
#[pyo3(signature = (name, fields=None))]
pub fn set_default_pool_category(
&self,
name: String,
fields: Option<Vec<String>>,
) -> PyResult<PoolCategoryResource> {
self.runtime
.block_on(self.client.set_default_pool_category(name, fields))
}
#[pyo3(signature = (query=None, fields=None, limit=None, offset=None))]
pub fn list_pools(
&self,
query: Option<Vec<QueryToken>>,
fields: Option<Vec<String>>,
limit: Option<u32>,
offset: Option<u32>,
) -> PyResult<PyPagedSearchResult> {
self.runtime
.block_on(self.client.list_pools(query, fields, limit, offset))
}
#[pyo3(signature = (names, category=None, description=None, posts=None, fields=None))]
pub fn create_pool(
&self,
names: Vec<String>,
category: Option<String>,
description: Option<String>,
posts: Option<Vec<u32>>,
fields: Option<Vec<String>>,
) -> PyResult<PoolResource> {
self.runtime.block_on(
self.client
.create_pool(names, category, description, posts, fields),
)
}
#[pyo3(signature = (pool_id, version, names=None, category=None, description=None,
posts=None, fields=None))]
pub fn update_pool(
&self,
pool_id: u32,
version: u32,
names: Option<Vec<String>>,
category: Option<String>,
description: Option<String>,
posts: Option<Vec<u32>>,
fields: Option<Vec<String>>,
) -> PyResult<PoolResource> {
self.runtime.block_on(self.client.update_pool(
pool_id,
version,
names,
category,
description,
posts,
fields,
))
}
#[pyo3(signature = (pool_id, fields=None))]
pub fn get_pool(&self, pool_id: u32, fields: Option<Vec<String>>) -> PyResult<PoolResource> {
self.runtime.block_on(self.client.get_pool(pool_id, fields))
}
pub fn delete_pool(&self, pool_id: u32, version: u32) -> PyResult<()> {
self.runtime
.block_on(self.client.delete_pool(pool_id, version))
}
#[pyo3(signature = (remove_pool, remove_pool_version, merge_to_pool, merge_to_version, fields=None))]
pub fn merge_pools(
&self,
remove_pool: u32,
remove_pool_version: u32,
merge_to_pool: u32,
merge_to_version: u32,
fields: Option<Vec<String>>,
) -> PyResult<PoolResource> {
self.runtime.block_on(self.client.merge_pools(
remove_pool,
remove_pool_version,
merge_to_pool,
merge_to_version,
fields,
))
}
#[pyo3(signature = (query=None, fields=None, limit=None, offset=None))]
pub fn list_comments(
&self,
query: Option<Vec<QueryToken>>,
fields: Option<Vec<String>>,
limit: Option<u32>,
offset: Option<u32>,
) -> PyResult<PyPagedSearchResult> {
self.runtime
.block_on(self.client.list_comments(query, fields, limit, offset))
}
#[pyo3(signature = (text, post_id, fields=None))]
pub fn create_comment(
&self,
text: String,
post_id: u32,
fields: Option<Vec<String>>,
) -> PyResult<CommentResource> {
self.runtime
.block_on(self.client.create_comment(text, post_id, fields))
}
#[pyo3(signature = (comment_id, version, text, fields=None))]
pub fn update_comment(
&self,
comment_id: u32,
version: u32,
text: String,
fields: Option<Vec<String>>,
) -> PyResult<CommentResource> {
self.runtime.block_on(
self.client
.update_comment(comment_id, version, text, fields),
)
}
#[pyo3(signature = (comment_id, fields=None))]
pub fn get_comment(
&self,
comment_id: u32,
fields: Option<Vec<String>>,
) -> PyResult<CommentResource> {
self.runtime
.block_on(self.client.get_comment(comment_id, fields))
}
pub fn delete_comment(&self, comment_id: u32, version: u32) -> PyResult<()> {
self.runtime
.block_on(self.client.delete_comment(comment_id, version))
}
#[pyo3(signature = (comment_id, rating, fields=None))]
pub fn rate_comment(
&self,
comment_id: u32,
rating: i8,
fields: Option<Vec<String>>,
) -> PyResult<CommentResource> {
self.runtime
.block_on(self.client.rate_comment(comment_id, rating, fields))
}
#[pyo3(signature = (query=None, fields=None, limit=None, offset=None))]
pub fn list_users(
&self,
query: Option<Vec<QueryToken>>,
fields: Option<Vec<String>>,
limit: Option<u32>,
offset: Option<u32>,
) -> PyResult<PyPagedSearchResult> {
self.runtime
.block_on(self.client.list_users(query, fields, limit, offset))
}
#[pyo3(signature = (name, password, rank=None, avatar_path=None, fields=None))]
pub fn create_user(
&self,
name: String,
password: String,
rank: Option<UserRank>,
avatar_path: Option<PathBuf>,
fields: Option<Vec<String>>,
) -> PyResult<UserResource> {
self.runtime.block_on(
self.client
.create_user(name, password, rank, avatar_path, fields),
)
}
#[pyo3(signature = (name, version, new_name=None, password=None, rank=None, avatar_path=None, fields=None))]
pub fn update_user(
&self,
name: String,
version: u32,
new_name: Option<String>,
password: Option<String>,
rank: Option<UserRank>,
avatar_path: Option<PathBuf>,
fields: Option<Vec<String>>,
) -> PyResult<UserResource> {
self.runtime.block_on(self.client.update_user(
name,
version,
new_name,
password,
rank,
avatar_path,
fields,
))
}
#[pyo3(signature = (user_name, fields=None))]
pub fn get_user(
&self,
user_name: String,
fields: Option<Vec<String>>,
) -> PyResult<UserResource> {
self.runtime
.block_on(self.client.get_user(user_name, fields))
}
pub fn delete_user(&self, user_name: String, version: u32) -> PyResult<()> {
self.runtime
.block_on(self.client.delete_user(user_name, version))
}
#[pyo3(signature = (user_name, fields=None))]
pub fn list_user_tokens(
&self,
user_name: String,
fields: Option<Vec<String>>,
) -> PyResult<Vec<UserAuthTokenResource>> {
self.runtime
.block_on(self.client.list_user_tokens(user_name, fields))
}
#[pyo3(signature = (user_name, note=None, expiration_time=None, fields=None))]
pub fn create_user_token(
&self,
user_name: String,
note: Option<String>,
expiration_time: Option<DateTime<Utc>>,
fields: Option<Vec<String>>,
) -> PyResult<UserAuthTokenResource> {
self.runtime.block_on(self.client.create_user_token(
user_name,
note,
expiration_time,
fields,
))
}
#[pyo3(signature = (user_name, token, version, enabled=None, note=None, expiration_time=None, fields=None))]
pub fn update_user_token(
&self,
user_name: String,
token: String,
version: u32,
enabled: Option<bool>,
note: Option<String>,
expiration_time: Option<DateTime<Utc>>,
fields: Option<Vec<String>>,
) -> PyResult<UserAuthTokenResource> {
self.runtime.block_on(self.client.update_user_token(
user_name,
token,
version,
enabled,
note,
expiration_time,
fields,
))
}
pub fn delete_user_token(
&self,
user_name: String,
token: String,
version: u32,
) -> PyResult<()> {
self.runtime
.block_on(self.client.delete_user_token(user_name, token, version))
}
pub fn password_reset_request(&self, email_or_name: String) -> PyResult<()> {
self.runtime
.block_on(self.client.password_reset_request(email_or_name))
}
pub fn password_reset_confirm(
&self,
email_or_name: String,
reset_token: String,
) -> PyResult<String> {
self.runtime.block_on(
self.client
.password_reset_confirm(email_or_name, reset_token),
)
}
#[pyo3(signature = (query=None, fields=None, limit=None, offset=None))]
pub fn list_snapshots(
&self,
query: Option<Vec<QueryToken>>,
fields: Option<Vec<String>>,
limit: Option<u32>,
offset: Option<u32>,
) -> PyResult<PyPagedSearchResult> {
self.runtime
.block_on(self.client.list_snapshots(query, fields, limit, offset))
}
pub fn global_info(&self) -> PyResult<GlobalInfo> {
self.runtime.block_on(self.client.global_info())
}
pub fn upload_temporary_file(&self, file_path: PathBuf) -> PyResult<TemporaryFileUpload> {
self.runtime
.block_on(self.client.upload_temporary_file(file_path))
}
}

View file

@ -0,0 +1 @@

View file

@ -2,7 +2,10 @@
//! warned that the types here help with the Type safety for the Tag names only. It does
//! not guarantee that a given API endpoint will support the given tag.
#[cfg(feature = "python")]
use pyo3::{exceptions::PyValueError, prelude::*, types::*};
use std::fmt::Display;
use std::str::FromStr;
use strum_macros::AsRefStr;
/// A named token such as `foo:bar`
@ -22,7 +25,8 @@ pub trait ToQueryString {
}
/// A query token using for searching posts, tags and pools
#[derive(Debug)]
#[derive(Debug, Clone)]
#[cfg_attr(all(feature = "python"), pyclass)]
pub struct QueryToken {
/// The key for this token. For `foo:bar` this would be `foo`
pub key: String,
@ -116,7 +120,7 @@ impl QueryToken {
/// let liked_posts = QueryToken::special(PostSpecialToken::Liked);
/// client.request().list_posts(Some(&vec![liked_posts]));
/// ```
pub fn special(key: impl SpecialToken) -> Self {
pub fn special(key: impl AsRef<str>) -> Self {
QueryToken::anonymous(key)
}
@ -139,6 +143,112 @@ impl QueryToken {
}
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pyfunction)]
pub fn named_token(key: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult<QueryToken> {
QueryToken::token_py(key, value)
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pyfunction)]
pub fn sort_token(key: &Bound<'_, PyAny>) -> PyResult<QueryToken> {
QueryToken::sort_py(key)
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pyfunction)]
pub fn anonymous_token(key: &Bound<'_, PyString>) -> PyResult<QueryToken> {
QueryToken::anonymous_py(key)
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pyfunction)]
pub fn special_token(key: &Bound<'_, PyAny>) -> PyResult<QueryToken> {
QueryToken::special_py(key)
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
impl QueryToken {
#[pyo3(name = "__str__")]
pub fn to_python_string(&self) -> PyResult<String> {
Ok(format!("QueryToken(\"{}\", \"{}\")", self.key, self.value))
}
#[pyo3(name = "__repr__")]
pub fn to_python_repr(&self) -> PyResult<String> {
self.to_python_string()
}
#[pyo3(name = "token")]
#[staticmethod]
pub fn token_py(key: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult<Self> {
let value = value.extract::<String>()?;
if let Ok(tnt) = key.extract::<TagNamedToken>() {
Ok(QueryToken::token(tnt, value))
} else if let Ok(pnt) = key.extract::<PostNamedToken>() {
Ok(QueryToken::token(pnt, value))
} else if let Ok(pnt) = key.extract::<PoolNamedToken>() {
Ok(QueryToken::token(pnt, value))
} else if let Ok(comment) = key.extract::<CommentNamedToken>() {
Ok(QueryToken::token(comment, value))
} else if let Ok(user) = key.extract::<UserNamedToken>() {
Ok(QueryToken::token(user, value))
} else if let Ok(x) = key.extract::<SnapshotNamedToken>() {
Ok(QueryToken::token(x, value))
} else if let Ok(strvalue) = key.extract::<String>() {
Ok(QueryToken::token(strvalue, value))
} else {
Err(PyErr::new::<PyValueError, _>("Invalid value type for key"))
}
}
#[pyo3(name = "sort")]
#[staticmethod]
pub fn sort_py(key: &Bound<'_, PyAny>) -> PyResult<Self> {
if let Ok(tnt) = key.extract::<TagSortToken>() {
Ok(QueryToken::sort(tnt))
} else if let Ok(pnt) = key.extract::<PostSortToken>() {
Ok(QueryToken::sort(pnt))
} else if let Ok(pnt) = key.extract::<PoolSortToken>() {
Ok(QueryToken::sort(pnt))
} else if let Ok(comment) = key.extract::<CommentSortToken>() {
Ok(QueryToken::sort(comment))
} else if let Ok(user) = key.extract::<UserSortToken>() {
Ok(QueryToken::sort(user))
} else if let Ok(strvalue) = key.extract::<String>() {
Ok(QueryToken::sort(strvalue))
} else {
Err(PyErr::new::<PyValueError, _>("Invalid value type for key"))
}
}
#[pyo3(name = "anonymous")]
#[staticmethod]
pub fn anonymous_py(key: &Bound<'_, PyString>) -> PyResult<Self> {
let key = key.extract::<String>()?;
Ok(QueryToken::anonymous(key))
}
#[pyo3(name = "special")]
#[staticmethod]
pub fn special_py(key: &Bound<'_, PyAny>) -> PyResult<Self> {
if let Ok(special) = key.extract::<PostSpecialToken>() {
Ok(QueryToken::special(special))
} else if let Ok(strvalue) = key.extract::<String>() {
Ok(QueryToken::special(strvalue))
} else {
Err(PyErr::new::<PyValueError, _>("Invalid value type for key"))
}
}
#[pyo3(name = "negate")]
pub fn negate_py(&self) -> PyResult<Self> {
Ok(self.negate())
}
}
impl Display for QueryToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let suffix = if !self.value.is_empty() {
@ -157,8 +267,9 @@ impl ToQueryString for Vec<QueryToken> {
}
}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe named query tokens for use with [list_tags](crate::SzurubooruRequest::list_tags)
pub enum TagNamedToken {
/// having given name (accepts wildcards)
@ -188,8 +299,25 @@ pub enum TagNamedToken {
}
impl NamedToken for TagNamedToken {}
#[derive(Debug, AsRefStr)]
/*#[cfg(feature="python")]
impl<'py> FromPyObject<'py> for TagNamedToken {
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
/*use pyo3::exceptions::PyTypeError;
if ob.is_instance_of::<TagNamedToken>() {
Ok()
}
let strvalue = ob.extract::<String>()?;
match TagNamedToken::from_str(&strvalue) {
Ok(tnt) => Ok(tnt),
Err(_) => Err(PyTypeError::new_err("Invalid variant"))
}*/
Ok(ob.downcast_into_exact::<Self>()?.)
}
}*/
#[derive(Debug, AsRefStr, Eq, PartialEq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe sort query tokens for use with [list_tags](crate::SzurubooruRequest::list_tags)
pub enum TagSortToken {
/// as random as it can get
@ -223,8 +351,9 @@ pub enum TagSortToken {
}
impl SortableToken for TagSortToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe named query tokens for use with [list_posts](crate::SzurubooruRequest::list_posts)
pub enum PostNamedToken {
/// having given post number
@ -320,8 +449,9 @@ pub enum PostNamedToken {
}
impl NamedToken for PostNamedToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe sort query tokens for use with [list_posts](crate::SzurubooruRequest::list_posts)
pub enum PostSortToken {
/// as random as it can get
@ -387,8 +517,9 @@ pub enum PostSortToken {
}
impl SortableToken for PostSortToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe special query tokens for use with [list_posts](crate::SzurubooruRequest::list_posts)
pub enum PostSpecialToken {
/// posts liked by currently logged-in user
@ -402,8 +533,9 @@ pub enum PostSpecialToken {
}
impl SpecialToken for PostSpecialToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe named query tokens for use with [list_pools](crate::SzurubooruRequest::list_pools)
pub enum PoolNamedToken {
/// having given name (accepts wildcards)
@ -427,8 +559,9 @@ pub enum PoolNamedToken {
}
impl NamedToken for PoolNamedToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe sort query tokens for use with [list_pools](crate::SzurubooruRequest::list_pools)
pub enum PoolSortToken {
/// as random as it can get
@ -454,8 +587,9 @@ pub enum PoolSortToken {
}
impl SortableToken for PoolSortToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe named query tokens for use with
/// [list_comments](crate::SzurubooruRequest::list_comments)
pub enum CommentNamedToken {
@ -484,8 +618,9 @@ pub enum CommentNamedToken {
}
impl NamedToken for CommentNamedToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe sort query tokens for use with
/// [list_comments](crate::SzurubooruRequest::list_comments)
pub enum CommentSortToken {
@ -512,8 +647,9 @@ pub enum CommentSortToken {
}
impl SortableToken for CommentSortToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe named query tokens for use with [list_users](crate::SzurubooruRequest::list_users)
pub enum UserNamedToken {
/// having given name (accepts wildcards)
@ -533,8 +669,9 @@ pub enum UserNamedToken {
}
impl NamedToken for UserNamedToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe sort query tokens for use with [list_users](crate::SzurubooruRequest::list_users)
pub enum UserSortToken {
/// as random as it can get
@ -556,8 +693,9 @@ pub enum UserSortToken {
}
impl SortableToken for UserNamedToken {}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))]
/// Type-safe named query tokens for use with
/// [list_snapshots](crate::SzurubooruRequest::list_snapshots)
pub enum SnapshotNamedToken {