diff --git a/Cargo.lock b/Cargo.lock index acceafc..96487a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,6 +150,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "colored" version = "2.1.0" @@ -632,6 +654,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "ipnet" version = "2.9.0" @@ -702,6 +730,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -887,12 +924,59 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -931,6 +1015,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -949,6 +1039,72 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pyo3" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831e8e819a138c36e212f3af3fd9eeffed6bf1510a805af35b0edee5ffa59433" +dependencies = [ + "cfg-if", + "chrono", + "chrono-tz", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "serde", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8730e591b14492a8945cdff32f089250b05f5accecf74aeddf9e8272ce1fa8" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e97e919d2df92eb88ca80a037969f44e5e70356559654962cbb3316d00300c6" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb57983022ad41f9e683a599f2fd13c3664d7063a3ac5714cae4b7bee7d3f206" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec480c0c51ddec81019531705acac51bcdbeae563557c982aa8263bb96880372" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.36" @@ -1290,6 +1446,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -1404,6 +1566,7 @@ dependencies = [ "futures-util", "hex", "mockito", + "pyo3", "reqwest", "serde", "serde_json", @@ -1430,6 +1593,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.12.0" @@ -1682,6 +1851,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/szurubooru-client/Cargo.toml b/szurubooru-client/Cargo.toml index acf43ac..8f96369 100644 --- a/szurubooru-client/Cargo.toml +++ b/szurubooru-client/Cargo.toml @@ -16,6 +16,7 @@ chrono = { version = "0.4.38", features = ["serde"] } derive_builder = "0.20.0" futures-util = "0.3.30" hex = "0.4.3" +pyo3 = { version="0.22.0", optional=true, features=["chrono-tz", "chrono", "extension-module", "serde", "experimental-async"] } reqwest = { version = "0.12.5", features = ["json", "multipart", "stream"] } serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.120" @@ -23,6 +24,7 @@ sha1 = "0.10.6" strum = { version = "0.26.3", features = ["derive", "strum_macros"] } strum_macros = "0.26.4" thiserror = "1.0.63" +tokio = { version = "1.39.2", features = ["rt", "sync"], optional = true } tracing = "0.1.40" url = "2.5.2" urlencoding = "2.1.3" @@ -30,3 +32,10 @@ urlencoding = "2.1.3" [dev-dependencies] mockito = "1.4.0" tokio = { version = "1.39.2", features = ["full"] } + +[features] +python = ["dep:pyo3", "dep:tokio"] + +[lib] +name = "szurubooru_client" +crate-type = ["cdylib"] diff --git a/szurubooru-client/pyproject.toml b/szurubooru-client/pyproject.toml new file mode 100644 index 0000000..f0eacf9 --- /dev/null +++ b/szurubooru-client/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["maturin>=1.7,<2.0"] +build-backend = "maturin" + +[project] +name = "szurubooru-client" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = ["version"] +[tool.maturin] +features = ["pyo3/extension-module"] diff --git a/szurubooru-client/src/client.rs b/szurubooru-client/src/client.rs index 13130ee..eb3aec0 100644 --- a/szurubooru-client/src/client.rs +++ b/szurubooru-client/src/client.rs @@ -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) -> 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>) -> 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) -> 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) -> SzurubooruRequest { + self.request().with_optional_offset(offset) + } } #[derive(Debug)] /// A type that represents a single Szurubooru request. pub struct SzurubooruRequest<'a> { - fields: Option>, + fields: Option>, limit: Option, offset: Option, 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) -> 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>) -> 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) -> 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) -> Self { + match val { + Some(f) => self.with_offset(f), + None => self, + } + } + #[doc(hidden)] fn prep_request( &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 { + 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 { 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 { + assert!(update_post.content_token.is_some()); + let url = format!("/api/post/{post_id}"); + self.create_update_post_from_file( + None, + None, + None::, + &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> { + ) -> SzurubooruResult> { 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, - ) -> SzurubooruResult> { + ) -> SzurubooruResult> { 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( &self, - name: T, + user_name: T, create_token: &CreateUpdateUserAuthToken, ) -> SzurubooruResult where T: AsRef + 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)) diff --git a/szurubooru-client/src/errors.rs b/szurubooru-client/src/errors.rs index 2b01581..5de5323 100644 --- a/szurubooru-client/src/errors.rs +++ b/szurubooru-client/src/errors.rs @@ -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 for SzurubooruClientError { } } +impl From for SzurubooruClientError { + fn from(value: UninitializedFieldError) -> Self { + SzurubooruClientError::BuilderValidationError(value.to_string()) + } +} + +#[cfg(feature = "python")] +impl std::convert::From 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 = Result; diff --git a/szurubooru-client/src/lib.rs b/szurubooru-client/src/lib.rs index e6eddf9..5f931df 100644 --- a/szurubooru-client/src/lib.rs +++ b/szurubooru-client/src/lib.rs @@ -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, + }, + }; +} diff --git a/szurubooru-client/src/models.rs b/szurubooru-client/src/models.rs index c5183a0..94e7125 100644 --- a/szurubooru-client/src/models.rs +++ b/szurubooru-client/src/models.rs @@ -88,7 +88,7 @@ impl WithBaseURL for Vec { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(all(feature = "python"), pyclass(get_all))] +#[cfg_attr(all(feature = "python"), pyclass(get_all, eq))] /// A [tag resource](TagResource) stripped down to `names`, `category` and `usages` fields. pub struct MicroTagResource { /// The tag names and aliases @@ -99,6 +99,14 @@ pub struct MicroTagResource { pub usages: u32, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl MicroTagResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] /// To prevent problems with concurrent resource modification, Szurubooru implements optimistic /// locks using resource versions. Each modifiable resource has its version returned to the client @@ -152,6 +160,14 @@ pub struct TagResource { pub description: Option, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl TagResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + /// Creates or updates a tag using specified parameters. Names, suggestions and implications must /// match `tag_name_regex` from server's configuration. Category must exist and is the same as name /// field within resource. Suggestions and implications are optional. If specified @@ -170,7 +186,6 @@ pub struct TagResource { /// ``` #[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)] //#[builder(pattern="owned")] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))] pub struct CreateUpdateTag { @@ -200,81 +215,6 @@ pub struct CreateUpdateTag { pub suggestions: Option>, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateTag { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(CreateUpdateTagBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateTagBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - - #[pyo3(name = "with_version")] - pub fn version_py(mut slf: PyRefMut<'_, Self>, version: u32) -> PyResult> { - slf.version(version); - Ok(slf) - } - - #[pyo3(name = "with_category")] - pub fn category_py(mut slf: PyRefMut<'_, Self>, cat: String) -> PyResult> { - slf.category(cat); - Ok(slf) - } - - #[pyo3(name = "with_names")] - pub fn names_py<'py>( - mut slf: PyRefMut<'py, Self>, - names: Vec, - ) -> PyResult> { - slf.names(names); - Ok(slf) - } - - #[pyo3(name = "with_description")] - pub fn description_py( - mut slf: PyRefMut<'_, Self>, - desc: String, - ) -> PyResult> { - slf.description(desc); - Ok(slf) - } - - #[pyo3(name = "with_implications")] - pub fn implications_py( - mut slf: PyRefMut<'_, Self>, - implications: Vec, - ) -> PyResult> { - slf.implications(implications); - Ok(slf) - } - - #[pyo3(name = "with_suggestions")] - pub fn suggestions_py( - mut slf: PyRefMut<'_, Self>, - suggestions: Vec, - ) -> PyResult> { - slf.suggestions(suggestions); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[cfg_attr(all(feature = "python"), pyclass(get_all))] /// A single tag category. The primary purpose of tag categories is to distinguish certain tag @@ -294,8 +234,15 @@ pub struct TagCategoryResource { pub default: Option, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl TagCategoryResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Default, Builder)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] #[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] /// Used for creating or updating a Tag Category @@ -318,59 +265,8 @@ pub struct CreateUpdateTagCategory { pub order: Option, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateTagCategory { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(CreateUpdateTagCategoryBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateTagCategoryBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - - #[pyo3(name = "with_version")] - pub fn version_py(mut slf: PyRefMut<'_, Self>, version: u32) -> PyResult> { - slf.version(version); - Ok(slf) - } - - #[pyo3(name = "with_name")] - pub fn name_py(mut slf: PyRefMut<'_, Self>, name: String) -> PyResult> { - slf.name(name); - Ok(slf) - } - - #[pyo3(name = "with_color")] - pub fn color_py(mut slf: PyRefMut<'_, Self>, color: String) -> PyResult> { - slf.color(color); - Ok(slf) - } - - #[pyo3(name = "with_order")] - pub fn order_py(mut slf: PyRefMut<'_, Self>, order: u32) -> PyResult> { - slf.order(order); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Builder)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] +#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[serde(rename_all = "camelCase")] /// Removes source tag and merges all of its usages, suggestions and implications to the target tag. @@ -390,63 +286,6 @@ pub struct MergeTags { pub merge_to_tag: String, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl MergeTags { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(MergeTagsBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl MergeTagsBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - - #[pyo3(name = "with_remove_tag_version")] - pub fn remove_tag_version_py( - mut slf: PyRefMut<'_, Self>, - v: u32, - ) -> PyResult> { - slf.remove_tag_version(v); - Ok(slf) - } - - #[pyo3(name = "with_remove_tag")] - pub fn remove_tag_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.remove_tag(v); - Ok(slf) - } - - #[pyo3(name = "with_merge_tag_version")] - pub fn merge_to_version_py( - mut slf: PyRefMut<'_, Self>, - v: u32, - ) -> PyResult> { - slf.merge_to_version(v); - Ok(slf) - } - - #[pyo3(name = "with_merge_to_tag")] - pub fn merge_to_tag_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.merge_to_tag(v); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(all(feature = "python"), pyclass(get_all))] /// Lists siblings of given tag, e.g. tags that were used in the same posts as the given tag @@ -457,6 +296,14 @@ pub struct TagSibling { pub occurrences: u32, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl TagSibling { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)] #[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))] #[serde(rename_all = "camelCase")] @@ -506,6 +353,14 @@ pub struct MicroPostResource { pub thumbnail_url: String, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl MicroPostResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + impl WithBaseURL for MicroPostResource { fn with_base_url(self, url: &str) -> Self { if !self.thumbnail_url.contains(url) { @@ -601,6 +456,14 @@ pub struct PostResource { pub pools: Option>, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl PostResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + impl WithBaseURL for PostResource { fn with_base_url(self, url: &str) -> Self { let curl = self.content_url.map(|cu| { @@ -636,7 +499,6 @@ impl WithBaseURL for PostResource { } #[derive(Debug, Clone, Serialize, Deserialize, Builder)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))] #[serde(rename_all = "camelCase")] @@ -683,105 +545,6 @@ pub struct CreateUpdatePost { pub content_token: Option, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdatePost { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(CreateUpdatePostBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdatePostBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - - #[pyo3(name = "with_version")] - pub fn version_py(mut slf: PyRefMut<'_, Self>, version: u32) -> PyResult> { - slf.version(version); - Ok(slf) - } - - #[pyo3(name = "with_tags")] - pub fn tags_py(mut slf: PyRefMut<'_, Self>, tags: Vec) -> PyResult> { - slf.tags(tags); - Ok(slf) - } - - #[pyo3(name = "with_safety")] - pub fn safety_py( - mut slf: PyRefMut<'_, Self>, - safety: PostSafety, - ) -> PyResult> { - slf.safety(safety); - Ok(slf) - } - - #[pyo3(name = "with_source")] - pub fn source_py(mut slf: PyRefMut<'_, Self>, source: String) -> PyResult> { - slf.source(source); - Ok(slf) - } - - #[pyo3(name = "with_relations")] - pub fn relations_py( - mut slf: PyRefMut<'_, Self>, - relations: Vec, - ) -> PyResult> { - slf.relations(relations); - Ok(slf) - } - - #[pyo3(name = "with_notes")] - pub fn notes_py( - mut slf: PyRefMut<'_, Self>, - notes: Vec, - ) -> PyResult> { - slf.notes(notes); - Ok(slf) - } - - #[pyo3(name = "with_flags")] - pub fn flags_py( - mut slf: PyRefMut<'_, Self>, - flags: Vec, - ) -> PyResult> { - slf.flags(flags); - Ok(slf) - } - - #[pyo3(name = "with_content_url")] - pub fn content_url_py( - mut slf: PyRefMut<'_, Self>, - val: String, - ) -> PyResult> { - slf.content_url(val); - Ok(slf) - } - - #[pyo3(name = "with_content_token")] - pub fn content_token_py( - mut slf: PyRefMut<'_, Self>, - val: String, - ) -> PyResult> { - slf.content_token(val); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(all(feature = "python"), pyclass(get_all))] @@ -791,10 +554,17 @@ pub struct TemporaryFileUpload { pub token: String, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl TemporaryFileUpload { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Builder)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] -#[builder(setter(into))] +#[builder(build_fn(error = "SzurubooruClientError"))] #[serde(rename_all = "camelCase")] /// Removes source post and merges all of its tags, relations, scores, favorites and comments to /// the target post. If replaceContent is set to true, content of the target post is replaced using @@ -818,72 +588,6 @@ pub struct MergePost { pub replace_post_content: bool, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl MergePost { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(MergePostBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl MergePostBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - - #[pyo3(name = "with_remove_post_version")] - pub fn remove_post_version_py( - mut slf: PyRefMut<'_, Self>, - v: u32, - ) -> PyResult> { - slf.remove_post_version(v); - Ok(slf) - } - - #[pyo3(name = "with_remove_post")] - pub fn remove_post_py(mut slf: PyRefMut<'_, Self>, v: u32) -> PyResult> { - slf.remove_post(v); - Ok(slf) - } - - #[pyo3(name = "with_merge_to_version")] - pub fn merge_to_version_py( - mut slf: PyRefMut<'_, Self>, - v: u32, - ) -> PyResult> { - slf.merge_to_version(v); - Ok(slf) - } - - #[pyo3(name = "with_merge_to_post")] - pub fn merge_to_post_py(mut slf: PyRefMut<'_, Self>, v: u32) -> PyResult> { - slf.merge_to_post(v); - Ok(slf) - } - - #[pyo3(name = "with_replace_post_content")] - pub fn with_replace_post_content_py( - mut slf: PyRefMut<'_, Self>, - v: bool, - ) -> PyResult> { - slf.replace_post_content(v); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[doc(hidden)] pub struct RateResource { @@ -904,6 +608,14 @@ pub struct NoteResource { pub text: String, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl NoteResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))] #[serde(rename_all = "camelCase")] @@ -1000,6 +712,10 @@ pub struct UserResource { #[cfg(feature = "python")] #[cfg_attr(all(feature = "python"), pymethods)] impl UserResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } + #[getter] #[pyo3(name = "email")] pub fn email_py(&self) -> PyResult> { @@ -1058,7 +774,6 @@ impl WithBaseURL for UserResource { } #[derive(Debug, Clone, Serialize, Deserialize, Default, Builder)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))] #[serde(rename_all = "camelCase")] @@ -1088,66 +803,6 @@ pub struct CreateUpdateUser { pub avatar_style: Option, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateUser { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(CreateUpdateUserBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateUserBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - #[pyo3(name = "with_version")] - pub fn version_py(mut slf: PyRefMut<'_, Self>, version: u32) -> PyResult> { - slf.version(version); - Ok(slf) - } - - #[pyo3(name = "with_name")] - pub fn name_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.name(v); - Ok(slf) - } - - #[pyo3(name = "with_password")] - pub fn password_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.password(v); - Ok(slf) - } - - #[pyo3(name = "with_rank")] - pub fn rank_py(mut slf: PyRefMut<'_, Self>, v: UserRank) -> PyResult> { - slf.rank(v); - Ok(slf) - } - - #[pyo3(name = "with_avatar_style")] - pub fn avatar_style_py( - mut slf: PyRefMut<'_, Self>, - v: UserAvatarStyle, - ) -> PyResult> { - // Should we accept types other than UserAvatarStyle? - slf.avatar_style(v); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[serde(rename_all = "camelCase")] @@ -1159,6 +814,14 @@ pub struct MicroUserResource { pub avatar_url: String, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl MicroUserResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + impl WithBaseURL for MicroUserResource { fn with_base_url(self, url: &str) -> Self { if !self.avatar_url.contains(url) { @@ -1197,6 +860,14 @@ pub struct UserAuthTokenResource { pub last_usage_time: Option>, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl UserAuthTokenResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + impl WithBaseURL for UserAuthTokenResource { fn with_base_url(self, url: &str) -> Self { Self { @@ -1207,7 +878,6 @@ impl WithBaseURL for UserAuthTokenResource { } #[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[builder(setter(into, strip_option), build_fn(error = "SzurubooruClientError"))] #[serde(rename_all = "kebab-case")] @@ -1232,59 +902,6 @@ pub struct CreateUpdateUserAuthToken { pub expiration_time: Option>, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateUserAuthToken { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(CreateUpdateUserAuthTokenBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateUserAuthTokenBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - #[pyo3(name = "with_version")] - pub fn version_py(mut slf: PyRefMut<'_, Self>, version: u32) -> PyResult> { - slf.version(version); - Ok(slf) - } - - #[pyo3(name = "with_enabled")] - pub fn enabled_py(mut slf: PyRefMut<'_, Self>, v: bool) -> PyResult> { - slf.enabled(v); - Ok(slf) - } - - #[pyo3(name = "with_note")] - pub fn note_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.note(v); - Ok(slf) - } - - #[pyo3(name = "with_expiration_time")] - pub fn password_py( - mut slf: PyRefMut<'_, Self>, - v: DateTime, - ) -> PyResult> { - slf.expiration_time(v); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[doc(hidden)] @@ -1294,7 +911,6 @@ pub struct PasswordResetToken { } #[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(all(feature = "python"), pyclass(get_all))] #[serde(rename_all = "camelCase")] /// Type that represents a new temporary password pub struct TemporaryPassword { @@ -1348,6 +964,14 @@ pub struct GlobalInfo { pub config: GlobalInfoConfig, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl GlobalInfo { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[serde(rename_all = "camelCase")] @@ -1366,8 +990,15 @@ pub struct PoolCategoryResource { pub default: Option, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl PoolCategoryResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Builder)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))] /// `struct` used for creating or updating a pool category. This type uses a Builder pattern like @@ -1397,50 +1028,6 @@ pub struct CreateUpdatePoolCategory { pub color: Option, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdatePoolCategory { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(CreateUpdatePoolCategoryBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdatePoolCategoryBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - #[pyo3(name = "with_version")] - pub fn version_py(mut slf: PyRefMut<'_, Self>, version: u32) -> PyResult> { - slf.version(version); - Ok(slf) - } - - #[pyo3(name = "with_name")] - pub fn name_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.name(v); - Ok(slf) - } - - #[pyo3(name = "with_color")] - pub fn color_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.color(v); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[serde(rename_all = "camelCase")] @@ -1467,6 +1054,14 @@ pub struct PoolResource { pub description: Option, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl PoolResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + impl WithBaseURL for PoolResource { fn with_base_url(self, url: &str) -> Self { PoolResource { @@ -1477,7 +1072,6 @@ impl WithBaseURL for PoolResource { } #[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))] #[serde(rename_all = "camelCase")] @@ -1518,64 +1112,8 @@ pub struct CreateUpdatePool { pub posts: Option>, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdatePool { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(CreateUpdatePoolBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdatePoolBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - #[pyo3(name = "with_version")] - pub fn version_py(mut slf: PyRefMut<'_, Self>, version: u32) -> PyResult> { - slf.version(version); - Ok(slf) - } - - #[pyo3(name = "with_names")] - pub fn names_py(mut slf: PyRefMut<'_, Self>, v: Vec) -> PyResult> { - slf.names(v); - Ok(slf) - } - - #[pyo3(name = "with_category")] - pub fn category_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.category(v); - Ok(slf) - } - - #[pyo3(name = "with_description")] - pub fn description_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.description(v); - Ok(slf) - } - - #[pyo3(name = "with_posts")] - pub fn posts_py(mut slf: PyRefMut<'_, Self>, v: Vec) -> PyResult> { - slf.posts(v); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] +#[builder(build_fn(error = "SzurubooruClientError"))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[serde(rename_all = "camelCase")] /// This type is used to specify which pools should be merged. Uses the builder pattern like so: @@ -1605,62 +1143,6 @@ pub struct MergePool { pub merge_to_pool: u32, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl MergePool { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(MergePoolBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl MergePoolBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - #[pyo3(name = "with_remove_pool_version")] - pub fn remove_pool_version_py( - mut slf: PyRefMut<'_, Self>, - v: u32, - ) -> PyResult> { - slf.remove_pool_version(v); - Ok(slf) - } - - #[pyo3(name = "with_remove_pool")] - pub fn remove_pool_py(mut slf: PyRefMut<'_, Self>, v: u32) -> PyResult> { - slf.remove_pool(v); - Ok(slf) - } - - #[pyo3(name = "with_merge_to_version")] - pub fn merge_to_version_py( - mut slf: PyRefMut<'_, Self>, - v: u32, - ) -> PyResult> { - slf.merge_to_version(v); - Ok(slf) - } - - #[pyo3(name = "with_merge_to_pool")] - pub fn merge_to_pool_py(mut slf: PyRefMut<'_, Self>, v: u32) -> PyResult> { - slf.merge_to_pool(v); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[serde(rename_all = "camelCase")] @@ -1678,6 +1160,14 @@ pub struct MicroPoolResource { pub description: Option, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl MicroPoolResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[serde(rename_all = "camelCase")] @@ -1703,8 +1193,15 @@ pub struct CommentResource { pub own_score: Option, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl CommentResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)] -#[cfg_attr(all(feature = "python"), builder_struct_attr(pyclass))] #[cfg_attr(all(feature = "python"), pyclass(get_all))] #[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))] #[serde(rename_all = "camelCase")] @@ -1735,50 +1232,6 @@ pub struct CreateUpdateComment { pub post_id: Option, } -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateComment { - #[pyo3(name = "builder")] - #[staticmethod] - pub fn builder_py() -> PyResult { - Ok(CreateUpdateCommentBuilder::default()) - } -} - -#[cfg(feature = "python")] -#[cfg_attr(all(feature = "python"), pymethods)] -impl CreateUpdateCommentBuilder { - #[new] - pub fn new() -> PyResult { - Ok(Self::default()) - } - - #[pyo3(name = "build")] - pub fn build_py(&self) -> PyResult { - match self.build() { - Ok(cutag) => Ok(cutag), - Err(e) => Err(PyErr::new::(e.to_string())), - } - } - #[pyo3(name = "with_version")] - pub fn version_py(mut slf: PyRefMut<'_, Self>, version: u32) -> PyResult> { - slf.version(version); - Ok(slf) - } - - #[pyo3(name = "with_text")] - pub fn with_text_py(mut slf: PyRefMut<'_, Self>, v: String) -> PyResult> { - slf.text(v); - Ok(slf) - } - - #[pyo3(name = "with_post_id")] - pub fn post_id_py(mut slf: PyRefMut<'_, Self>, v: u32) -> PyResult> { - slf.post_id(v); - Ok(slf) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[cfg_attr(all(feature = "python"), pyclass(eq, eq_int))] #[serde(rename_all = "camelCase")] @@ -1831,6 +1284,14 @@ pub enum SnapshotCreationDeletionData { PoolCategory(PoolCategoryResource), } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl SnapshotCreationDeletionData { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + impl WithBaseURL for SnapshotCreationDeletionData { fn with_base_url(self, url: &str) -> Self { match self { @@ -1871,6 +1332,10 @@ impl SnapshotModificationData { let obj = to_pyobject(py, &self.value).unwrap().unbind(); Ok(obj) } + + fn __repr__(&self) -> String { + format!("{:?}", self) + } } #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] @@ -1918,6 +1383,14 @@ pub struct SnapshotResource { pub time: Option>, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl SnapshotResource { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + impl WithBaseURL for SnapshotResource { fn with_base_url(self, url: &str) -> Self { SnapshotResource { @@ -1939,6 +1412,14 @@ pub struct ImageSearchSimilarPost { pub post: PostResource, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl ImageSearchSimilarPost { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + impl WithBaseURL for ImageSearchSimilarPost { fn with_base_url(self, url: &str) -> Self { Self { @@ -1961,6 +1442,14 @@ pub struct ImageSearchResult { pub similar_posts: Vec, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl ImageSearchResult { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + impl WithBaseURL for ImageSearchResult { fn with_base_url(self, url: &str) -> Self { Self { @@ -1980,6 +1469,14 @@ pub struct AroundPostResult { next: Option, } +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pymethods)] +impl AroundPostResult { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + #[cfg(test)] mod tests { use crate::models::{GlobalInfo, GlobalInfoConfig, SnapshotResource, TagCategoryResource}; diff --git a/szurubooru-client/src/py/asynchronous.rs b/szurubooru-client/src/py/asynchronous.rs new file mode 100644 index 0000000..0322f0d --- /dev/null +++ b/szurubooru-client/src/py/asynchronous.rs @@ -0,0 +1,1147 @@ +use crate::models::*; +use crate::py::PyPagedSearchResult; +use crate::tokens::QueryToken; +use crate::SzurubooruClient; +use chrono::{DateTime, Utc}; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::prelude::*; +use std::path::PathBuf; + +#[pyclass(name = "SzurubooruAsyncClient")] +pub struct PythonAsyncClient { + client: SzurubooruClient, +} + +#[pymethods] +impl PythonAsyncClient { + #[new] + #[pyo3(signature = (host, username=None, token=None, password=None, allow_insecure=None))] + pub fn new( + host: String, + username: Option, + token: Option, + password: Option, + allow_insecure: Option, + ) -> PyResult { + let allow_insecure = allow_insecure.unwrap_or(false); + + match (username, token, password) { + (Some(u), Some(t), None) => { + let client = SzurubooruClient::new_with_token(&host, &u, &t, allow_insecure)?; + Ok(PythonAsyncClient { client }) + } + (Some(u), None, Some(p)) => { + let client = SzurubooruClient::new_with_basic_auth(&host, &u, &p, allow_insecure)?; + Ok(PythonAsyncClient { client }) + } + (None, None, None) => { + let client = SzurubooruClient::new_anonymous(&host, allow_insecure)?; + Ok(PythonAsyncClient { client }) + } + _ => Err(PyRuntimeError::new_err( + "(Username and Token) or (Username and Password) must be provided", + )), + } + } + + #[pyo3(signature = (fields=None))] + pub async fn list_tag_categories( + &self, + fields: Option>, + ) -> PyResult> { + let request = self.client.with_optional_fields(fields); + request + .list_tag_categories() + .await + .map(|ltc| ltc.results) + .map_err(Into::into) + } + + #[pyo3(signature = (name, version, color=None, order=None, fields=None))] + pub async fn update_tag_category( + &self, + name: String, + version: u32, + color: Option, + order: Option, + fields: Option>, + ) -> PyResult { + let mut cutag = CreateUpdateTagCategoryBuilder::default(); + let mut cutag = cutag.version(version); + + if let Some(color) = color { + cutag = cutag.color(color); + } + if let Some(order) = order { + cutag = cutag.order(order); + } + + let cutag = cutag.build()?; + let request = self.client.with_optional_fields(fields); + request + .update_tag_category(name, &cutag) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (name, fields=None))] + pub async fn get_tag_category( + &self, + name: String, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .get_tag_category(name) + .await + .map_err(Into::into) + } + + pub async fn delete_tag_category(&self, name: String, version: u32) -> PyResult<()> { + self.client + .request() + .delete_tag_category(name, version) + .await + .map_err(Into::into) + } + + pub async fn set_default_tag_category(&self, name: String) -> PyResult<()> { + self.client + .request() + .set_default_tag_category(name) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (query=None, fields=None, limit=None, offset=None))] + pub async fn list_tags( + &self, + query: Option>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .with_optional_limit(limit) + .with_optional_offset(offset) + .list_tags(query.as_ref()) + .await + .map_err(Into::into) + .map(Into::into) + } + + #[pyo3(signature = (names, category=None, description=None, implications=None, suggestions=None, fields=None))] + pub async fn create_tag( + &self, + names: Vec, + category: Option, + description: Option, + implications: Option>, + suggestions: Option>, + fields: Option>, + ) -> PyResult { + let mut cubuild = CreateUpdateTagBuilder::default(); + cubuild.names(names); + if let Some(cat) = category { + cubuild.category(cat); + } + if let Some(desc) = description { + cubuild.description(desc); + } + if let Some(imps) = implications { + cubuild.implications(imps); + } + if let Some(s) = suggestions { + cubuild.suggestions(s); + } + let tag_build = cubuild.build()?; + self.client + .with_optional_fields(fields) + .create_tag(&tag_build) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (name, names, category=None, description=None, implications=None, suggestions=None, fields=None))] + pub async fn update_tag( + &self, + name: String, + names: Option>, + category: Option, + description: Option, + implications: Option>, + suggestions: Option>, + fields: Option>, + ) -> PyResult { + let mut cubuild = CreateUpdateTagBuilder::default(); + if let Some(names) = names { + cubuild.names(names); + } + if let Some(cat) = category { + cubuild.category(cat); + } + if let Some(desc) = description { + cubuild.description(desc); + } + if let Some(imps) = implications { + cubuild.implications(imps); + } + if let Some(s) = suggestions { + cubuild.suggestions(s); + } + let tag_build = cubuild.build()?; + self.client + .with_optional_fields(fields) + .update_tag(name, &tag_build) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (name, fields=None))] + pub async fn get_tag( + &self, + name: String, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .get_tag(name) + .await + .map_err(Into::into) + } + + pub async fn delete_tag(&self, name: String, version: u32) -> PyResult<()> { + self.client + .request() + .delete_tag(name, version) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (remove_tag, remove_tag_version, merge_to_tag, merge_to_version, fields=None))] + pub async fn merge_tag( + &self, + remove_tag: String, + remove_tag_version: u32, + merge_to_tag: String, + merge_to_version: u32, + fields: Option>, + ) -> PyResult { + let mtags = MergeTagsBuilder::default() + .remove_tag_version(remove_tag_version) + .remove_tag(remove_tag) + .merge_to_version(merge_to_version) + .merge_to_tag(merge_to_tag) + .build()?; + self.client + .with_optional_fields(fields) + .merge_tag(&mtags) + .await + .map_err(Into::into) + } + + pub async fn get_tag_siblings(&self, name: String) -> PyResult> { + self.client + .request() + .get_tag_siblings(name) + .await + .map(|ts| ts.results) + .map_err(Into::into) + } + + #[pyo3(signature = (query=None, fields=None, limit=None, offset=None))] + pub async fn list_posts( + &self, + query: Option>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .with_optional_limit(limit) + .with_optional_offset(offset) + .list_posts(query.as_ref()) + .await + .map_err(Into::into) + .map(Into::into) + } + + #[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 async fn create_post( + &self, + url: Option, + token: Option, + file_path: Option, + thumbnail_path: Option, + tags: Option>, + safety: Option, + source: Option, + relations: Option>, + notes: Option>, + flags: Option>, + fields: Option>, + ) -> PyResult { + let mut cupost = CreateUpdatePostBuilder::default(); + if let Some(source) = source { + cupost.source(source); + } + if let Some(tags) = tags { + cupost.tags(tags); + } + if let Some(safety) = safety { + cupost.safety(safety); + } + if let Some(relations) = relations { + cupost.relations(relations); + } + if let Some(notes) = notes { + cupost.notes(notes); + } + if let Some(flags) = flags { + cupost.flags(flags); + } + + if let Some(token) = token { + cupost.content_token(token); + let cupost = cupost.build()?; + self.client + .with_optional_fields(fields) + .create_post_from_token(&cupost) + .await + .map_err(Into::into) + } else if let Some(url) = url { + cupost.content_url(url); + let cupost = cupost.build()?; + self.client + .with_optional_fields(fields) + .create_post_from_url(&cupost) + .await + .map_err(Into::into) + } else if let Some(file) = file_path { + let cupost = cupost.build()?; + self.client + .with_optional_fields(fields) + .create_post_from_file_path(file, thumbnail_path, &cupost) + .await + .map_err(Into::into) + } else { + Err(PyRuntimeError::new_err( + "One of url, token or file must be specified", + )) + } + } + + #[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 async fn update_post( + &self, + post_id: u32, + post_version: u32, + url: Option, + token: Option, + file_path: Option, + thumbnail_path: Option, + tags: Option>, + safety: Option, + source: Option, + relations: Option>, + notes: Option>, + flags: Option>, + fields: Option>, + ) -> PyResult { + let mut cupost = CreateUpdatePostBuilder::default(); + cupost.version(post_version); + if let Some(source) = source { + cupost.source(source); + } + if let Some(tags) = tags { + cupost.tags(tags); + } + if let Some(safety) = safety { + cupost.safety(safety); + } + if let Some(relations) = relations { + cupost.relations(relations); + } + if let Some(notes) = notes { + cupost.notes(notes); + } + if let Some(flags) = flags { + cupost.flags(flags); + } + + if let Some(token) = token { + cupost.content_token(token); + let cupost = cupost.build()?; + self.client + .with_optional_fields(fields) + .update_post_from_token(post_id, &cupost) + .await + .map_err(Into::into) + } else if let Some(url) = url { + cupost.content_url(url); + let cupost = cupost.build()?; + self.client + .with_optional_fields(fields) + .update_post_from_url(post_id, &cupost) + .await + .map_err(Into::into) + } else if file_path.is_some() || thumbnail_path.is_some() { + let cupost = cupost.build()?; + self.client + .with_optional_fields(fields) + .update_post_from_file_path(post_id, file_path, thumbnail_path, &cupost) + .await + .map_err(Into::into) + } else { + let cupost = cupost.build()?; + self.client + .with_optional_fields(fields) + .update_post(post_id, &cupost) + .await + .map_err(Into::into) + } + } + + pub async fn get_image_bytes(&self, post_id: u32) -> PyResult> { + let bytes = self + .client + .request() + .get_image_bytes(post_id) + .await? + .to_vec(); + Ok(bytes) + } + + pub async fn download_image_to_path(&self, post_id: u32, file_path: PathBuf) -> PyResult<()> { + self.client + .request() + .download_image_to_path(post_id, file_path) + .await + .map_err(Into::into) + } + + pub async fn get_thumbnail_bytes<'py>(&self, post_id: u32) -> PyResult> { + let bytes = self + .client + .request() + .get_thumbnail_bytes(post_id) + .await? + .to_vec(); + Ok(bytes) + } + + pub async fn download_thumbnail_to_path( + &self, + post_id: u32, + file_path: PathBuf, + ) -> PyResult<()> { + self.client + .request() + .download_thumbnail_to_path(post_id, file_path) + .await + .map_err(Into::into) + } + + pub async fn reverse_search_image(&self, image_path: PathBuf) -> PyResult { + self.client + .request() + .reverse_search_file_path(image_path) + .await + .map_err(Into::into) + } + + pub async fn post_for_image(&self, image_path: PathBuf) -> PyResult> { + self.client + .request() + .posts_for_file_path(image_path) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (post_id, fields=None))] + pub async fn get_post( + &self, + post_id: u32, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .get_post(post_id) + .await + .map_err(Into::into) + } + + pub async fn get_around_post(&self, post_id: u32) -> PyResult { + self.client + .request() + .get_around_post(post_id) + .await + .map_err(Into::into) + } + + pub async fn delete_post(&self, post_id: u32, version: u32) -> PyResult<()> { + self.client + .request() + .delete_post(post_id, version) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (remove_post, remove_post_version, merge_to_post, + merge_to_version, fields=None))] + pub async fn merge_post( + &self, + remove_post: u32, + remove_post_version: u32, + merge_to_post: u32, + merge_to_version: u32, + fields: Option>, + ) -> PyResult { + let mpost = MergePostBuilder::default() + .remove_post_version(remove_post_version) + .remove_post(remove_post) + .merge_to_version(merge_to_version) + .merge_to_post(merge_to_post) + .build()?; + self.client + .with_optional_fields(fields) + .merge_post(&mpost) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (post_id, rating, fields=None))] + pub async fn rate_post( + &self, + post_id: u32, + rating: i8, + fields: Option>, + ) -> PyResult { + if rating < -1 || rating > 1 { + Err(PyValueError::new_err("Rating must be -1, 0, or 1")) + } else { + self.client + .with_optional_fields(fields) + .rate_post(post_id, rating) + .await + .map_err(Into::into) + } + } + + #[pyo3(signature = (post_id, fields=None))] + pub async fn favorite_post( + &self, + post_id: u32, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .favorite_post(post_id) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (post_id, fields=None))] + pub async fn unfavorite_post( + &self, + post_id: u32, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .unfavorite_post(post_id) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (fields=None))] + pub async fn get_featured_post( + &self, + fields: Option>, + ) -> PyResult> { + self.client + .with_optional_fields(fields) + .get_featured_post() + .await + .map_err(Into::into) + } + + #[pyo3(signature = (post_id, fields=None))] + pub async fn set_featured_post( + &self, + post_id: u32, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .set_featured_post(post_id) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (fields=None))] + pub async fn list_pool_categories( + &self, + fields: Option>, + ) -> PyResult> { + self.client + .with_optional_fields(fields) + .list_pool_categories() + .await + .map_err(Into::into) + .map(|pc| pc.results) + } + + #[pyo3(signature = (name, color=None, fields=None))] + pub async fn create_pool_category( + &self, + name: String, + color: Option, + fields: Option>, + ) -> PyResult { + let mut pc = CreateUpdatePoolCategoryBuilder::default(); + pc.name(name); + if let Some(color) = color { + pc.color(color); + } + let pc = pc.build()?; + self.client + .with_optional_fields(fields) + .create_pool_category(&pc) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (name, version, new_name=None, color=None, fields=None))] + pub async fn update_pool_category( + &self, + name: String, + version: u32, + new_name: Option, + color: Option, + fields: Option>, + ) -> PyResult { + let mut pc = CreateUpdatePoolCategoryBuilder::default(); + pc.version(version); + if let Some(name) = new_name { + pc.name(name); + } + if let Some(color) = color { + pc.color(color); + } + let pc = pc.build()?; + self.client + .with_optional_fields(fields) + .update_pool_category(name, &pc) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (name, fields=None))] + pub async fn get_pool_category( + &self, + name: String, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .get_pool_category(name) + .await + .map_err(Into::into) + } + + pub async fn delete_pool_category(&self, name: String, version: u32) -> PyResult<()> { + self.client + .request() + .delete_pool_category(name, version) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (name, fields=None))] + pub async fn set_default_pool_category( + &self, + name: String, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .set_default_pool_category(name) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (query=None, fields=None, limit=None, offset=None))] + pub async fn list_pools( + &self, + query: Option>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .with_optional_limit(limit) + .with_optional_offset(offset) + .list_pools(query.as_ref()) + .await + .map_err(Into::into) + .map(Into::into) + } + + #[pyo3(signature = (names, category=None, description=None, posts=None, fields=None))] + pub async fn create_pool<'py>( + &self, + names: Vec, + category: Option, + description: Option, + posts: Option>, + fields: Option>, + ) -> PyResult { + let mut cupool = CreateUpdatePoolBuilder::default(); + cupool.names(names); + if let Some(cat) = category { + cupool.category(cat); + } + if let Some(desc) = description { + cupool.description(desc); + } + if let Some(posts) = posts { + cupool.posts(posts); + } + let cupool = cupool.build()?; + self.client + .with_optional_fields(fields) + .create_pool(&cupool) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (pool_id, version, names=None, category=None, description=None, + posts=None, fields=None))] + pub async fn update_pool( + &self, + pool_id: u32, + version: u32, + names: Option>, + category: Option, + description: Option, + posts: Option>, + fields: Option>, + ) -> PyResult { + let mut cupool = CreateUpdatePoolBuilder::default(); + cupool.version(version); + if let Some(names) = names { + cupool.names(names); + } + + if let Some(cat) = category { + cupool.category(cat); + } + if let Some(desc) = description { + cupool.description(desc); + } + if let Some(posts) = posts { + cupool.posts(posts); + } + let cupool = cupool.build()?; + self.client + .with_optional_fields(fields) + .update_pool(pool_id, &cupool) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (pool_id, fields=None))] + pub async fn get_pool( + &self, + pool_id: u32, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .get_pool(pool_id) + .await + .map_err(Into::into) + } + + pub async fn delete_pool(&self, pool_id: u32, version: u32) -> PyResult<()> { + self.client + .request() + .delete_pool(pool_id, version) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (remove_pool, remove_pool_version, merge_to_pool, merge_to_version, fields=None))] + pub async fn merge_pools( + &self, + remove_pool: u32, + remove_pool_version: u32, + merge_to_pool: u32, + merge_to_version: u32, + fields: Option>, + ) -> PyResult { + let mpool = MergePoolBuilder::default() + .remove_pool_version(remove_pool_version) + .remove_pool(remove_pool) + .merge_to_version(merge_to_version) + .merge_to_pool(merge_to_pool) + .build()?; + self.client + .with_optional_fields(fields) + .merge_pools(&mpool) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (query=None, fields=None, limit=None, offset=None))] + pub async fn list_comments( + &self, + query: Option>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .with_optional_limit(limit) + .with_optional_offset(offset) + .list_comments(query.as_ref()) + .await + .map_err(Into::into) + .map(Into::into) + } + + #[pyo3(signature = (text, post_id, fields=None))] + pub async fn create_comment( + &self, + text: String, + post_id: u32, + fields: Option>, + ) -> PyResult { + let mut cucomment = CreateUpdateCommentBuilder::default(); + cucomment.post_id(post_id); + cucomment.text(text); + + let cucomment = cucomment.build()?; + self.client + .with_optional_fields(fields) + .create_comment(&cucomment) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (comment_id, version, text, fields=None))] + pub async fn update_comment( + &self, + comment_id: u32, + version: u32, + text: String, + fields: Option>, + ) -> PyResult { + let mut cucomment = CreateUpdateCommentBuilder::default(); + cucomment.version(version); + cucomment.text(text); + + let cucomment = cucomment.build()?; + self.client + .with_optional_fields(fields) + .update_comment(comment_id, &cucomment) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (comment_id, fields=None))] + pub async fn get_comment( + &self, + comment_id: u32, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .get_comment(comment_id) + .await + .map_err(Into::into) + } + + pub async fn delete_comment(&self, comment_id: u32, version: u32) -> PyResult<()> { + self.client + .request() + .delete_comment(comment_id, version) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (comment_id, rating, fields=None))] + pub async fn rate_comment( + &self, + comment_id: u32, + rating: i8, + fields: Option>, + ) -> PyResult { + if rating < -1 || rating > 1 { + Err(PyValueError::new_err("Rating must be -1, 0, or 1")) + } else { + self.client + .with_optional_fields(fields) + .rate_comment(comment_id, rating) + .await + .map_err(Into::into) + } + } + + #[pyo3(signature = (query=None, fields=None, limit=None, offset=None))] + pub async fn list_users( + &self, + query: Option>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .with_optional_limit(limit) + .with_optional_offset(offset) + .list_users(query.as_ref()) + .await + .map_err(Into::into) + .map(Into::into) + } + + #[pyo3(signature = (name, password, rank=None, avatar_path=None, fields=None))] + pub async fn create_user( + &self, + name: String, + password: String, + rank: Option, + avatar_path: Option, + fields: Option>, + ) -> PyResult { + let mut cuser = CreateUpdateUserBuilder::default(); + cuser.name(name); + cuser.password(password); + if let Some(rank) = rank { + cuser.rank(rank); + } + if let Some(avatar_path) = avatar_path { + cuser.avatar_style(UserAvatarStyle::Manual); + let cuser = cuser.build()?; + self.client + .with_optional_fields(fields) + .create_user_with_avatar_path(avatar_path, &cuser) + .await + .map_err(Into::into) + } else { + cuser.avatar_style(UserAvatarStyle::Gravatar); + let cuser = cuser.build()?; + self.client + .with_optional_fields(fields) + .create_user(&cuser) + .await + .map_err(Into::into) + } + } + + #[pyo3(signature = (name, version, new_name=None, password=None, rank=None, avatar_path=None, fields=None))] + pub async fn update_user( + &self, + name: String, + version: u32, + new_name: Option, + password: Option, + rank: Option, + avatar_path: Option, + fields: Option>, + ) -> PyResult { + let mut cuser = CreateUpdateUserBuilder::default(); + cuser.version(version); + if let Some(new_name) = new_name { + cuser.name(new_name); + } + if let Some(password) = password { + cuser.password(password); + } + if let Some(rank) = rank { + cuser.rank(rank); + } + if let Some(avatar_path) = avatar_path { + cuser.avatar_style(UserAvatarStyle::Manual); + let cuser = cuser.build()?; + self.client + .with_optional_fields(fields) + .update_user_with_avatar_path(name, avatar_path, &cuser) + .await + .map_err(Into::into) + } else { + cuser.avatar_style(UserAvatarStyle::Gravatar); + let cuser = cuser.build()?; + self.client + .with_optional_fields(fields) + .update_user(name, &cuser) + .await + .map_err(Into::into) + } + } + + #[pyo3(signature = (user_name, fields=None))] + pub async fn get_user( + &self, + user_name: String, + fields: Option>, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .get_user(user_name) + .await + .map_err(Into::into) + } + + pub async fn delete_user(&self, user_name: String, version: u32) -> PyResult<()> { + self.client + .request() + .delete_user(user_name, version) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (user_name, fields=None))] + pub async fn list_user_tokens( + &self, + user_name: String, + fields: Option>, + ) -> PyResult> { + self.client + .with_optional_fields(fields) + .list_user_tokens(user_name) + .await + .map_err(Into::into) + .map(|ur| ur.results) + } + + #[pyo3(signature = (user_name, note=None, expiration_time=None, fields=None))] + pub async fn create_user_token( + &self, + user_name: String, + note: Option, + expiration_time: Option>, + fields: Option>, + ) -> PyResult { + let mut cutoken = CreateUpdateUserAuthTokenBuilder::default(); + if let Some(note) = note { + cutoken.note(note); + } + if let Some(etime) = expiration_time { + cutoken.expiration_time(etime); + } + let cutoken = cutoken.build()?; + self.client + .with_optional_fields(fields) + .create_user_token(user_name, &cutoken) + .await + .map_err(Into::into) + } + + #[pyo3(signature = (user_name, token, version, enabled=None, note=None, expiration_time=None, fields=None))] + pub async fn update_user_token( + &self, + user_name: String, + token: String, + version: u32, + enabled: Option, + note: Option, + expiration_time: Option>, + fields: Option>, + ) -> PyResult { + let mut cutoken = CreateUpdateUserAuthTokenBuilder::default(); + cutoken.version(version); + if let Some(enabled) = enabled { + cutoken.enabled(enabled); + } + if let Some(note) = note { + cutoken.note(note); + } + if let Some(etime) = expiration_time { + cutoken.expiration_time(etime); + } + let cutoken = cutoken.build()?; + self.client + .with_optional_fields(fields) + .update_user_token(user_name, token, &cutoken) + .await + .map_err(Into::into) + } + + pub async fn delete_user_token( + &self, + user_name: String, + token: String, + version: u32, + ) -> PyResult<()> { + self.client + .request() + .delete_user_token(user_name, token, version) + .await + .map_err(Into::into) + } + + pub async fn password_reset_request(&self, email_or_name: String) -> PyResult<()> { + self.client + .request() + .password_reset_request(email_or_name) + .await + .map_err(Into::into) + } + + pub async fn password_reset_confirm( + &self, + email_or_name: String, + reset_token: String, + ) -> PyResult { + self.client + .request() + .password_reset_confirm(email_or_name, reset_token) + .await + .map_err(Into::into) + .map(|tp| tp.password) + } + + #[pyo3(signature = (query=None, fields=None, limit=None, offset=None))] + pub async fn list_snapshots( + &self, + query: Option>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + self.client + .with_optional_fields(fields) + .with_optional_limit(limit) + .with_optional_offset(offset) + .list_snapshots(query.as_ref()) + .await + .map_err(Into::into) + .map(Into::into) + } + + pub async fn global_info(&self) -> PyResult { + self.client + .request() + .get_global_info() + .await + .map_err(Into::into) + } + + pub async fn upload_temporary_file(&self, file_path: PathBuf) -> PyResult { + self.client + .request() + .upload_temporary_file_from_path(file_path) + .await + .map_err(Into::into) + } +} diff --git a/szurubooru-client/src/py/mod.rs b/szurubooru-client/src/py/mod.rs new file mode 100644 index 0000000..f49ad0c --- /dev/null +++ b/szurubooru-client/src/py/mod.rs @@ -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> From> for PyPagedSearchResult { + fn from(value: PagedSearchResult) -> Self { + Python::with_gil(|py| PyPagedSearchResult { + query: value.query, + offset: value.offset, + limit: value.limit, + total: value.total, + results: value.results.into_py(py), + }) + } +} diff --git a/szurubooru-client/src/py/synchronous.rs b/szurubooru-client/src/py/synchronous.rs new file mode 100644 index 0000000..8ebca8d --- /dev/null +++ b/szurubooru-client/src/py/synchronous.rs @@ -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, + token: Option, + password: Option, + allow_insecure: Option, + ) -> PyResult { + 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>, + ) -> PyResult> { + 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, + order: Option, + fields: Option>, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + 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, + category: Option, + description: Option, + implications: Option>, + suggestions: Option>, + fields: Option>, + ) -> PyResult { + 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>, + category: Option, + description: Option, + implications: Option>, + suggestions: Option>, + fields: Option>, + ) -> PyResult { + 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>) -> PyResult { + 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>, + ) -> PyResult { + 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> { + 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>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + 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, + token: Option, + file_path: Option, + thumbnail_path: Option, + tags: Option>, + safety: Option, + source: Option, + relations: Option>, + notes: Option>, + flags: Option>, + fields: Option>, + ) -> PyResult { + 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, + token: Option, + file_path: Option, + thumbnail_path: Option, + tags: Option>, + safety: Option, + source: Option, + relations: Option>, + notes: Option>, + flags: Option>, + fields: Option>, + ) -> PyResult { + 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> { + 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> { + 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 { + self.runtime + .block_on(self.client.reverse_search_image(image_path)) + } + + pub fn post_for_image(&self, image_path: PathBuf) -> PyResult> { + 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>) -> PyResult { + self.runtime.block_on(self.client.get_post(post_id, fields)) + } + + pub fn get_around_post(&self, post_id: u32) -> PyResult { + 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>, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + ) -> PyResult { + self.runtime + .block_on(self.client.unfavorite_post(post_id, fields)) + } + + #[pyo3(signature = (fields=None))] + pub fn get_featured_post(&self, fields: Option>) -> PyResult> { + 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>, + ) -> PyResult { + self.runtime + .block_on(self.client.set_featured_post(post_id, fields)) + } + + #[pyo3(signature = (fields=None))] + pub fn list_pool_categories( + &self, + fields: Option>, + ) -> PyResult> { + 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, + fields: Option>, + ) -> PyResult { + 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, + color: Option, + fields: Option>, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + 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, + category: Option, + description: Option, + posts: Option>, + fields: Option>, + ) -> PyResult { + 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>, + category: Option, + description: Option, + posts: Option>, + fields: Option>, + ) -> PyResult { + 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>) -> PyResult { + 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>, + ) -> PyResult { + 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>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + 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, + avatar_path: Option, + fields: Option>, + ) -> PyResult { + 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, + password: Option, + rank: Option, + avatar_path: Option, + fields: Option>, + ) -> PyResult { + 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>, + ) -> PyResult { + 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>, + ) -> PyResult> { + 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, + expiration_time: Option>, + fields: Option>, + ) -> PyResult { + 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, + note: Option, + expiration_time: Option>, + fields: Option>, + ) -> PyResult { + 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 { + 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>, + fields: Option>, + limit: Option, + offset: Option, + ) -> PyResult { + self.runtime + .block_on(self.client.list_snapshots(query, fields, limit, offset)) + } + + pub fn global_info(&self) -> PyResult { + self.runtime.block_on(self.client.global_info()) + } + + pub fn upload_temporary_file(&self, file_path: PathBuf) -> PyResult { + self.runtime + .block_on(self.client.upload_temporary_file(file_path)) + } +} diff --git a/szurubooru-client/src/pyclient.rs b/szurubooru-client/src/pyclient.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/szurubooru-client/src/pyclient.rs @@ -0,0 +1 @@ + diff --git a/szurubooru-client/src/tokens.rs b/szurubooru-client/src/tokens.rs index a7474b0..921b365 100644 --- a/szurubooru-client/src/tokens.rs +++ b/szurubooru-client/src/tokens.rs @@ -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) -> 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::token_py(key, value) +} + +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pyfunction)] +pub fn sort_token(key: &Bound<'_, PyAny>) -> PyResult { + QueryToken::sort_py(key) +} + +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pyfunction)] +pub fn anonymous_token(key: &Bound<'_, PyString>) -> PyResult { + QueryToken::anonymous_py(key) +} + +#[cfg(feature = "python")] +#[cfg_attr(all(feature = "python"), pyfunction)] +pub fn special_token(key: &Bound<'_, PyAny>) -> PyResult { + 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 { + Ok(format!("QueryToken(\"{}\", \"{}\")", self.key, self.value)) + } + + #[pyo3(name = "__repr__")] + pub fn to_python_repr(&self) -> PyResult { + self.to_python_string() + } + + #[pyo3(name = "token")] + #[staticmethod] + pub fn token_py(key: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult { + let value = value.extract::()?; + + if let Ok(tnt) = key.extract::() { + Ok(QueryToken::token(tnt, value)) + } else if let Ok(pnt) = key.extract::() { + Ok(QueryToken::token(pnt, value)) + } else if let Ok(pnt) = key.extract::() { + Ok(QueryToken::token(pnt, value)) + } else if let Ok(comment) = key.extract::() { + Ok(QueryToken::token(comment, value)) + } else if let Ok(user) = key.extract::() { + Ok(QueryToken::token(user, value)) + } else if let Ok(x) = key.extract::() { + Ok(QueryToken::token(x, value)) + } else if let Ok(strvalue) = key.extract::() { + Ok(QueryToken::token(strvalue, value)) + } else { + Err(PyErr::new::("Invalid value type for key")) + } + } + + #[pyo3(name = "sort")] + #[staticmethod] + pub fn sort_py(key: &Bound<'_, PyAny>) -> PyResult { + if let Ok(tnt) = key.extract::() { + Ok(QueryToken::sort(tnt)) + } else if let Ok(pnt) = key.extract::() { + Ok(QueryToken::sort(pnt)) + } else if let Ok(pnt) = key.extract::() { + Ok(QueryToken::sort(pnt)) + } else if let Ok(comment) = key.extract::() { + Ok(QueryToken::sort(comment)) + } else if let Ok(user) = key.extract::() { + Ok(QueryToken::sort(user)) + } else if let Ok(strvalue) = key.extract::() { + Ok(QueryToken::sort(strvalue)) + } else { + Err(PyErr::new::("Invalid value type for key")) + } + } + + #[pyo3(name = "anonymous")] + #[staticmethod] + pub fn anonymous_py(key: &Bound<'_, PyString>) -> PyResult { + let key = key.extract::()?; + Ok(QueryToken::anonymous(key)) + } + + #[pyo3(name = "special")] + #[staticmethod] + pub fn special_py(key: &Bound<'_, PyAny>) -> PyResult { + if let Ok(special) = key.extract::() { + Ok(QueryToken::special(special)) + } else if let Ok(strvalue) = key.extract::() { + Ok(QueryToken::special(strvalue)) + } else { + Err(PyErr::new::("Invalid value type for key")) + } + } + + #[pyo3(name = "negate")] + pub fn negate_py(&self) -> PyResult { + 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 { } } -#[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 { + /*use pyo3::exceptions::PyTypeError; + if ob.is_instance_of::() { + Ok() + } + let strvalue = ob.extract::()?; + match TagNamedToken::from_str(&strvalue) { + Ok(tnt) => Ok(tnt), + Err(_) => Err(PyTypeError::new_err("Invalid variant")) + }*/ + Ok(ob.downcast_into_exact::()?.) + } +}*/ + +#[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 {