fix(python): refactoring python client

refactoring several pieces of the client and the Python wrapper to make them more usable.
This commit is contained in:
Scott Lyons 2024-08-21 08:36:59 -07:00
commit f2ae459c22
11 changed files with 275 additions and 95 deletions

View file

@ -17,7 +17,7 @@ derive_builder = "0.20.0"
futures-util = "0.3.30" futures-util = "0.3.30"
hex = "0.4.3" hex = "0.4.3"
openssl = { version = "0.10.66", features = ["vendored"] } openssl = { version = "0.10.66", features = ["vendored"] }
pyo3 = { version="0.22.0", optional=true, features=["chrono-tz", "chrono", "extension-module", "serde", "experimental-async"] } pyo3 = { version="0.22.0", optional=true, features=["chrono-tz", "chrono", "serde", "experimental-async"] }
reqwest = { version = "0.12.5", features = ["json", "multipart", "stream"] } reqwest = { version = "0.12.5", features = ["json", "multipart", "stream"] }
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde-pyobject = { version = "0.4.0", optional = true } serde-pyobject = { version = "0.4.0", optional = true }
@ -36,9 +36,9 @@ mockito = "1.4.0"
tokio = { version = "1.39.2", features = ["full"] } tokio = { version = "1.39.2", features = ["full"] }
[features] [features]
python = ["dep:pyo3", "dep:tokio", "dep:serde-pyobject"] python = ["dep:pyo3", "dep:tokio", "dep:serde-pyobject", "pyo3/extension-module"]
serde-pyobject = ["dep:serde-pyobject"] extension-module = ["pyo3/extension-module"]
[lib] [lib]
name = "szurubooru_client" name = "szurubooru_client"
crate-type = ["cdylib"] crate-type = ["cdylib", "lib"]

View file

@ -3,7 +3,7 @@ requires = ["maturin>=1.7,<2.0"]
build-backend = "maturin" build-backend = "maturin"
[project] [project]
name = "szurubooru-client" name = "szurubooru_client"
requires-python = ">=3.8" requires-python = ">=3.8"
classifiers = [ classifiers = [
"Programming Language :: Rust", "Programming Language :: Rust",
@ -12,4 +12,4 @@ classifiers = [
] ]
dynamic = ["version"] dynamic = ["version"]
[tool.maturin] [tool.maturin]
features = ["pyo3/extension-module"] features = ["pyo3/extension-module", "python"]

View file

@ -420,12 +420,14 @@ impl<'a> SzurubooruRequest<'a> {
async fn handle_response(&self, response: Response) -> SzurubooruResult<Response> { async fn handle_response(&self, response: Response) -> SzurubooruResult<Response> {
if response.status().is_client_error() || response.status().is_server_error() { if response.status().is_client_error() || response.status().is_server_error() {
let status = response.status();
let resp_json = response let resp_json = response
.text() .text()
.await .await
.map_err(SzurubooruClientError::RequestError)?; .map_err(SzurubooruClientError::RequestError)?;
let server_error = serde_json::from_str::<SzurubooruServerError>(&resp_json) let server_error = serde_json::from_str::<SzurubooruServerError>(&resp_json)
.map_err(|e| SzurubooruClientError::ResponseParsingError(e, resp_json))?; .map_err(|e| SzurubooruClientError::ResponseError(status, resp_json))?;
Err(SzurubooruClientError::SzurubooruServerError(server_error)) Err(SzurubooruClientError::SzurubooruServerError(server_error))
} else { } else {
Ok(response) Ok(response)
@ -436,7 +438,7 @@ impl<'a> SzurubooruRequest<'a> {
&self, &self,
request: RequestBuilder, request: RequestBuilder,
) -> SzurubooruResult<T> { ) -> SzurubooruResult<T> {
let request = request let mut request = request
.build() .build()
.map_err(SzurubooruClientError::RequestBuilderError)?; .map_err(SzurubooruClientError::RequestBuilderError)?;
@ -450,6 +452,7 @@ impl<'a> SzurubooruRequest<'a> {
.text() .text()
.await .await
.map_err(SzurubooruClientError::RequestError)?; .map_err(SzurubooruClientError::RequestError)?;
serde_json::from_str::<SzuruEither<T, SzurubooruServerError>>(&response_text) serde_json::from_str::<SzuruEither<T, SzurubooruServerError>>(&response_text)
.map_err(|e| SzurubooruClientError::ResponseParsingError(e, response_text))? .map_err(|e| SzurubooruClientError::ResponseParsingError(e, response_text))?
.into_result() .into_result()
@ -599,7 +602,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Removes source tag and merges all of its usages, suggestions and implications to the /// Removes source tag and merges all of its usages, suggestions and implications to the
/// target tag. Other tag properties such as category and aliases do not get transferred /// target tag. Other tag properties such as category and aliases do not get transferred
/// and are discarded. /// and are discarded.
pub async fn merge_tag(&self, merge_opts: &MergeTags) -> SzurubooruResult<TagResource> { pub async fn merge_tags(&self, merge_opts: &MergeTags) -> SzurubooruResult<TagResource> {
self.do_request(Method::POST, "/api/tag-merge", None, Some(merge_opts)) self.do_request(Method::POST, "/api/tag-merge", None, Some(merge_opts))
.await .await
} }
@ -639,6 +642,11 @@ impl<'a> SzurubooruRequest<'a> {
method: Method, method: Method,
cupost: &CreateUpdatePost, cupost: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> { ) -> SzurubooruResult<PostResource> {
if method == Method::POST && cupost.safety.is_none() {
return Err(SzurubooruClientError::ValidationError(
"Safety must be set".to_string(),
));
}
self.do_request(method, path, None, Some(cupost)).await self.do_request(method, path, None, Some(cupost)).await
} }
@ -1009,7 +1017,12 @@ impl<'a> SzurubooruRequest<'a> {
path: impl AsRef<Path>, path: impl AsRef<Path>,
) -> SzurubooruResult<()> { ) -> SzurubooruResult<()> {
let mut stream = self.get_image_bytestream(post_id).await?; let mut stream = self.get_image_bytestream(post_id).await?;
let mut file = File::open(path.as_ref()).map_err(SzurubooruClientError::IOError)?; let mut file = File::options()
.write(true)
.truncate(true)
.create(true)
.open(path.as_ref())
.map_err(SzurubooruClientError::IOError)?;
self.write_content_to_file(&mut file, &mut stream).await self.write_content_to_file(&mut file, &mut stream).await
} }
@ -1067,7 +1080,7 @@ impl<'a> SzurubooruRequest<'a> {
// Need to add a reverse search for bytes // Need to add a reverse search for bytes
/// Searches for an exact match of a file based on the SHA1 checksum /// Searches for an exact match of a file based on the SHA1 checksum
pub async fn posts_for_file( pub async fn post_for_file(
&self, &self,
mut file: &mut File, mut file: &mut File,
) -> SzurubooruResult<Option<PostResource>> { ) -> SzurubooruResult<Option<PostResource>> {
@ -1081,21 +1094,17 @@ impl<'a> SzurubooruRequest<'a> {
.list_posts(Some(&vec![qt])) .list_posts(Some(&vec![qt]))
.await .await
.map(|psr| self.propagate_urls(psr))?; .map(|psr| self.propagate_urls(psr))?;
Ok(if psr.total > 1 { Ok(psr.results.first().map(|pr| pr.clone()))
Some(psr.results.swap_remove(0))
} else {
None
})
} }
/// Searches for an exact match of a file path based on the SHA1 checksum /// Searches for an exact match of a file path based on the SHA1 checksum
pub async fn posts_for_file_path( pub async fn post_for_file_path(
&self, &self,
file_path: impl AsRef<Path>, file_path: impl AsRef<Path>,
) -> SzurubooruResult<Option<PostResource>> { ) -> SzurubooruResult<Option<PostResource>> {
let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?; let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?;
self.posts_for_file(&mut file).await self.post_for_file(&mut file).await
} }
/// Retrieves information about an existing post. /// Retrieves information about an existing post.
@ -1137,6 +1146,11 @@ impl<'a> SzurubooruRequest<'a> {
/// Updates score of authenticated user for given post. Valid scores are -1, 0 and 1. /// Updates score of authenticated user for given post. Valid scores are -1, 0 and 1.
pub async fn rate_post(&self, post_id: u32, score: i8) -> SzurubooruResult<PostResource> { pub async fn rate_post(&self, post_id: u32, score: i8) -> SzurubooruResult<PostResource> {
if score < -1 || score > 1 {
return Err(SzurubooruClientError::ValidationError(
"Score must be -1, 0 or 1".to_string(),
));
}
let rating_obj = RateResource { score }; let rating_obj = RateResource { score };
let path = format!("/api/post/{post_id}/score"); let path = format!("/api/post/{post_id}/score");
self.do_request(Method::PUT, &path, None, Some(&rating_obj)) self.do_request(Method::PUT, &path, None, Some(&rating_obj))
@ -1380,6 +1394,11 @@ impl<'a> SzurubooruRequest<'a> {
comment_id: u32, comment_id: u32,
score: i8, score: i8,
) -> SzurubooruResult<CommentResource> { ) -> SzurubooruResult<CommentResource> {
if score < -1 || score > 1 {
return Err(SzurubooruClientError::ValidationError(
"Score must be -1, 0 or 1".to_string(),
));
}
let path = format!("/api/comment/{comment_id}/score"); let path = format!("/api/comment/{comment_id}/score");
let rating = RateResource { score }; let rating = RateResource { score };
self.do_request(Method::PUT, &path, None, Some(&rating)) self.do_request(Method::PUT, &path, None, Some(&rating))

View file

@ -5,8 +5,11 @@ use crate::models::SzuruEither;
use base64::EncodeSliceError; use base64::EncodeSliceError;
use derive_builder::UninitializedFieldError; use derive_builder::UninitializedFieldError;
#[cfg(feature = "python")] #[cfg(feature = "python")]
use pyo3::{exceptions::PyRuntimeError, prelude::*}; use pyo3::{create_exception, exceptions::PyException, prelude::*};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum_macros::AsRefStr;
use thiserror::Error; use thiserror::Error;
use url::ParseError as UParseError; use url::ParseError as UParseError;
@ -17,7 +20,7 @@ pub trait IntoClientResult<T> {
fn into_result(self) -> SzurubooruResult<T>; fn into_result(self) -> SzurubooruResult<T>;
} }
#[derive(Debug, Error)] #[derive(Debug, Error, AsRefStr)]
/// Type that represents the various error states that can occur when interacting with /// Type that represents the various error states that can occur when interacting with
/// Szurubooru /// Szurubooru
pub enum SzurubooruClientError { pub enum SzurubooruClientError {
@ -38,6 +41,9 @@ pub enum SzurubooruClientError {
/// Error occurred pas part of the request to the server /// Error occurred pas part of the request to the server
#[error("Request error {0}")] #[error("Request error {0}")]
RequestError(#[source] reqwest::Error), RequestError(#[source] reqwest::Error),
/// Error response with a text response from the server
#[error("Response error {0}: Server reply: {1}")]
ResponseError(StatusCode, String),
/// Error parsing the JSON response from the server /// Error parsing the JSON response from the server
#[error("Response Parsing error: {0}: {1}")] #[error("Response Parsing error: {0}: {1}")]
ResponseParsingError( ResponseParsingError(
@ -51,8 +57,8 @@ pub enum SzurubooruClientError {
#[error("JSON Serialization error: {0}")] #[error("JSON Serialization error: {0}")]
JSONSerializationError(#[source] serde_json::Error), JSONSerializationError(#[source] serde_json::Error),
/// Error when validation fails for one of the Builder types /// Error when validation fails for one of the Builder types
#[error("Builder validation error: {0}")] #[error("Vlidation error: {0}")]
BuilderValidationError(String), ValidationError(String),
/// Error occurred when reading a file /// Error occurred when reading a file
#[error("IO Error: {0}")] #[error("IO Error: {0}")]
IOError(#[source] std::io::Error), IOError(#[source] std::io::Error),
@ -69,14 +75,17 @@ impl From<SzurubooruServerError> for SzurubooruClientError {
impl From<UninitializedFieldError> for SzurubooruClientError { impl From<UninitializedFieldError> for SzurubooruClientError {
fn from(value: UninitializedFieldError) -> Self { fn from(value: UninitializedFieldError) -> Self {
SzurubooruClientError::BuilderValidationError(value.to_string()) SzurubooruClientError::ValidationError(value.to_string())
} }
} }
#[cfg(feature = "python")]
create_exception!(szurubooru_client, SzuruPyClientError, PyException);
#[cfg(feature = "python")] #[cfg(feature = "python")]
impl std::convert::From<SzurubooruClientError> for PyErr { impl std::convert::From<SzurubooruClientError> for PyErr {
fn from(value: SzurubooruClientError) -> Self { fn from(value: SzurubooruClientError) -> Self {
PyRuntimeError::new_err(value.to_string()) SzuruPyClientError::new_err((value.as_ref().to_string(), value.to_string()))
} }
} }

View file

@ -22,7 +22,7 @@
//! ``` //! ```
//! //!
//! For all other methods for making the requests, see the documentation. //! For all other methods for making the requests, see the documentation.
#![feature(cfg_eval)]
#![warn(missing_docs)] #![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)] #![warn(rustdoc::missing_crate_level_docs)]
@ -34,9 +34,6 @@ pub use client::SzurubooruRequest;
pub mod errors; pub mod errors;
pub use errors::SzurubooruResult; pub use errors::SzurubooruResult;
pub mod models; pub mod models;
#[cfg(feature = "python")]
pub mod pyclient;
pub mod tokens; pub mod tokens;
#[cfg(feature = "python")] #[cfg(feature = "python")]
@ -49,27 +46,26 @@ use pyo3::prelude::*;
#[cfg_attr(feature = "python", pymodule)] #[cfg_attr(feature = "python", pymodule)]
/// A Python wrapper around [SzurubooruClient] /// A Python wrapper around [SzurubooruClient]
mod szurubooru_client { mod szurubooru_client {
use pyo3::prelude::*;
#[pymodule_export] #[pymodule_export]
pub use crate::{ pub use crate::{
errors::SzuruPyClientError,
models::{ models::{
AroundPostResult, CommentResource, GlobalInfo, ImageSearchResult, AroundPostResult, CommentResource, GlobalInfo, ImageSearchResult,
ImageSearchSimilarPost, MergePool, MergePost, MergeTags, MicroPoolResource, ImageSearchSimilarPost, MicroPoolResource, MicroPostResource, MicroTagResource,
MicroPostResource, MicroTagResource, MicroUserResource, NoteResource, MicroUserResource, NoteResource, PoolCategoryResource, PoolResource, PostResource,
PoolCategoryResource, PoolResource, PostResource, PostSafety, PostType, PostSafety, PostType, SnapshotCreationDeletionData, SnapshotData,
SnapshotCreationDeletionData, SnapshotData, SnapshotModificationData, SnapshotModificationData, SnapshotOperationType, SnapshotResource,
SnapshotOperationType, SnapshotResource, SnapshotResourceType, TagCategoryResource, SnapshotResourceType, TagCategoryResource, TagResource, TagSibling,
TagResource, TagSibling, TemporaryFileUpload, UserAuthTokenResource, UserAvatarStyle, UserAuthTokenResource, UserAvatarStyle, UserRank, UserResource,
UserRank, UserResource,
}, },
py::asynchronous::PythonAsyncClient, py::asynchronous::PythonAsyncClient,
py::synchronous::PythonSyncClient, py::synchronous::PythonSyncClient,
tokens::{ tokens::{
anonymous_token, named_token, sort_token, special_token, CommentNamedToken, anonymous_token, named_token, sort_token, special_token, CommentNamedToken,
CommentSortToken, PoolNamedToken, PoolSortToken, PostNamedToken, PostSortToken, CommentSortToken, PoolNamedToken, PoolSortToken, PostNamedToken, PostSortToken,
PostSpecialToken, SnapshotNamedToken, TagNamedToken, TagSortToken, UserNamedToken, PostSpecialToken, QueryToken, SnapshotNamedToken, TagNamedToken, TagSortToken,
UserSortToken, UserNamedToken, UserSortToken,
}, },
}; };
} }

View file

@ -266,14 +266,12 @@ pub struct CreateUpdateTagCategory {
#[derive(Debug, Clone, Serialize, Deserialize, Builder)] #[derive(Debug, Clone, Serialize, Deserialize, Builder)]
#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))] #[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))]
#[cfg_attr(all(feature = "python"), pyclass(get_all))]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// Removes source tag and merges all of its usages, suggestions and implications to the target tag. /// Removes source tag and merges all of its usages, suggestions and implications to the target tag.
/// Other tag properties such as category and aliases do not get transferred and are discarded. /// Other tag properties such as category and aliases do not get transferred and are discarded.
pub struct MergeTags { pub struct MergeTags {
/// Version of the tag to remove /// Version of the tag to remove
#[serde(rename = "removeVersion")] #[serde(rename = "removeVersion")]
#[cfg(feature = "python")]
pub remove_tag_version: u32, pub remove_tag_version: u32,
/// The name of the tag to remove /// The name of the tag to remove
#[serde(rename = "remove")] #[serde(rename = "remove")]
@ -514,6 +512,7 @@ pub struct CreateUpdatePost {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>, pub tags: Option<Vec<String>>,
/// Required field, represents the SFW/NSFW state of a post /// Required field, represents the SFW/NSFW state of a post
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub safety: Option<PostSafety>, pub safety: Option<PostSafety>,
/// The origin of the post's content /// The origin of the post's content
@ -541,27 +540,21 @@ pub struct CreateUpdatePost {
/// [upload_temporary_file](crate::SzurubooruRequest::upload_temporary_file) /// [upload_temporary_file](crate::SzurubooruRequest::upload_temporary_file)
#[builder(default)] #[builder(default)]
pub content_token: Option<String>, pub content_token: Option<String>,
/// Upload the post anonymously
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub anonymous: Option<bool>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(all(feature = "python"), pyclass(get_all))]
/// A token representing a temporary file upload /// A token representing a temporary file upload
pub struct TemporaryFileUpload { pub struct TemporaryFileUpload {
/// Temporary upload token /// Temporary upload token
pub token: String, 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)] #[derive(Debug, Clone, Serialize, Deserialize, Builder)]
#[cfg_attr(all(feature = "python"), pyclass(get_all))]
#[builder(build_fn(error = "SzurubooruClientError"))] #[builder(build_fn(error = "SzurubooruClientError"))]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// Removes source post and merges all of its tags, relations, scores, favorites and comments to /// Removes source post and merges all of its tags, relations, scores, favorites and comments to
@ -647,61 +640,110 @@ pub enum UserAvatarStyle {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// A single user /// A single user
pub struct UserResource { pub struct UserResource {
#[cfg(feature = "python")]
#[pyo3(get)]
/// Resource version. See [versioning](ResourceVersion) /// Resource version. See [versioning](ResourceVersion)
pub version: Option<u32>,
#[cfg(feature = "python")] #[cfg(feature = "python")]
#[pyo3(get)] #[pyo3(get)]
pub version: Option<u32>,
/// Resource version. See [versioning](ResourceVersion)
#[cfg(not(feature = "python"))]
pub version: Option<u32>,
/// The user's username /// The user's username
#[cfg(feature = "python")]
#[pyo3(get)]
pub name: Option<String>, pub name: Option<String>,
/// The user's username
#[cfg(not(feature = "python"))]
pub name: Option<String>,
/// The user email. It is available only if the request is authenticated by the same user, /// The user email. It is available only if the request is authenticated by the same user,
/// or the authenticated user can change the email. If it's unavailable, the server returns /// or the authenticated user can change the email. If it's unavailable, the server returns
/// `false`. If the user hasn't specified an email, the server returns [None](Option::None) /// `false`. If the user hasn't specified an email, the server returns [None](Option::None)
pub email: Option<SzuruEither<String, bool>>, pub email: Option<SzuruEither<String, bool>>,
/// The user rank, which effectively affects their privileges
#[cfg(feature = "python")] #[cfg(feature = "python")]
#[pyo3(get)] #[pyo3(get)]
/// The user rank, which effectively affects their privileges
pub rank: Option<UserRank>, pub rank: Option<UserRank>,
/// The user rank, which effectively affects their privileges
#[cfg(not(feature = "python"))]
pub rank: Option<UserRank>,
/// The last login time
#[cfg(feature = "python")] #[cfg(feature = "python")]
#[pyo3(get)] #[pyo3(get)]
#[serde(rename = "last-login-time")] #[serde(rename = "last-login-time")]
/// The last login time
pub last_login_time: Option<DateTime<Utc>>, pub last_login_time: Option<DateTime<Utc>>,
#[cfg(feature = "python")]
#[pyo3(get)] /// The last login time
#[serde(rename = "creation-time")]
/// The user registration time
pub creation_time: Option<DateTime<Utc>>,
#[cfg(feature = "python")]
#[pyo3(get)]
/// How to render the user avatar
pub avatar_style: Option<UserAvatarStyle>,
#[cfg(feature = "python")]
#[pyo3(get)]
/// The URL to the avatar
pub avatar_url: Option<String>,
#[cfg(not(feature = "python"))] #[cfg(not(feature = "python"))]
/// The URL to the avatar #[serde(rename = "last-login-time")]
pub avatar_url: Option<String>, pub last_login_time: Option<DateTime<Utc>>,
/// The user registration time
#[serde(rename = "creation-time")]
#[cfg(feature = "python")] #[cfg(feature = "python")]
#[pyo3(get)] #[pyo3(get)]
pub creation_time: Option<DateTime<Utc>>,
/// The user registration time
#[serde(rename = "creation-time")]
#[cfg(not(feature = "python"))]
pub creation_time: Option<DateTime<Utc>>,
/// How to render the user avatar
#[cfg(feature = "python")]
#[pyo3(get)]
pub avatar_style: Option<UserAvatarStyle>,
/// How to render the user avatar
#[cfg(not(feature = "python"))]
pub avatar_style: Option<UserAvatarStyle>,
/// The URL to the avatar
#[cfg(feature = "python")]
#[pyo3(get)]
pub avatar_url: Option<String>,
/// The URL to the avatar
#[cfg(not(feature = "python"))]
pub avatar_url: Option<String>,
/// Number of comments /// Number of comments
#[cfg(feature = "python")]
#[pyo3(get)]
#[serde(rename = "comment-count")] #[serde(rename = "comment-count")]
pub comment_count: Option<u32>, pub comment_count: Option<u32>,
/// Number of comments
#[cfg(not(feature = "python"))]
#[serde(rename = "comment-count")]
pub comment_count: Option<u32>,
/// Number of uploaded posts
#[cfg(feature = "python")] #[cfg(feature = "python")]
#[pyo3(get)] #[pyo3(get)]
/// Number of uploaded posts
#[serde(rename = "uploaded-post-count")] #[serde(rename = "uploaded-post-count")]
pub uploaded_post_count: Option<u32>, pub uploaded_post_count: Option<u32>,
/// Number of uploaded posts
#[cfg(not(feature = "python"))]
#[serde(rename = "uploaded-post-count")]
pub uploaded_post_count: Option<u32>,
/// Number of liked posts. It is available only if the request is authenticated by the same /// Number of liked posts. It is available only if the request is authenticated by the same
/// user. If it's unavailable, the server returns `false` /// user. If it's unavailable, the server returns `false`
#[serde(rename = "liked-post-count")] #[serde(rename = "liked-post-count")]
pub liked_post_count: Option<SzuruEither<u32, bool>>, pub liked_post_count: Option<SzuruEither<u32, bool>>,
/// Number of disliked posts. It is available only if the request is authenticated by the same /// Number of disliked posts. It is available only if the request is authenticated by the same
/// user. If it's unavailable, the server returns `false`. /// user. If it's unavailable, the server returns `false`.
#[serde(rename = "disliked-post-count")] #[serde(rename = "disliked-post-count")]
pub disliked_post_count: Option<SzuruEither<u32, bool>>, pub disliked_post_count: Option<SzuruEither<u32, bool>>,
/// Number of favorited posts /// Number of favorited posts
#[serde(rename = "favorite-post-count")] #[serde(rename = "favorite-post-count")]
pub favorite_post_count: Option<SzuruEither<u32, bool>>, pub favorite_post_count: Option<SzuruEither<u32, bool>>,
@ -1108,7 +1150,6 @@ pub struct CreateUpdatePool {
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
#[builder(build_fn(error = "SzurubooruClientError"))] #[builder(build_fn(error = "SzurubooruClientError"))]
#[cfg_attr(all(feature = "python"), pyclass(get_all))]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// This type is used to specify which pools should be merged. Uses the builder pattern like so: /// This type is used to specify which pools should be merged. Uses the builder pattern like so:
/// ///
@ -1116,7 +1157,7 @@ pub struct CreateUpdatePool {
/// use szurubooru_client::models::MergePoolBuilder; /// use szurubooru_client::models::MergePoolBuilder;
/// // Merge pool ID 1 at version 1 to pool ID 3 at version 5 /// // Merge pool ID 1 at version 1 to pool ID 3 at version 5
/// let merge_pool = MergePoolBuilder::default() /// let merge_pool = MergePoolBuilder::default()
/// .remove_version(1) /// .remove_pool_version(1)
/// .remove(1) /// .remove(1)
/// .merge_to_version(5) /// .merge_to_version(5)
/// .merge_to(3) /// .merge_to(3)
@ -1125,7 +1166,7 @@ pub struct CreateUpdatePool {
/// ``` /// ```
pub struct MergePool { pub struct MergePool {
/// Version of the pool to remove. Must match the current Pool version /// Version of the pool to remove. Must match the current Pool version
#[serde(rename = "removePool")] #[serde(rename = "removeVersion")]
pub remove_pool_version: u32, pub remove_pool_version: u32,
/// Pool ID to remove /// Pool ID to remove
#[serde(rename = "remove")] #[serde(rename = "remove")]
@ -1304,11 +1345,17 @@ impl WithBaseURL for SnapshotCreationDeletionData {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// Data for a modified resource /// Data for a modified resource
pub struct SnapshotModificationData { pub struct SnapshotModificationData {
/// The type of snapshot
#[serde(rename = "type")]
#[cfg(feature = "python")] #[cfg(feature = "python")]
#[serde(rename = "type")]
#[pyo3(get)] #[pyo3(get)]
/// The type of snapshot
pub snapshot_type: String, pub snapshot_type: String,
#[cfg(not(feature = "python"))]
#[serde(rename = "type")]
/// The type of snapshot
pub snapshot_type: String,
/// The JSON value for the modified resource. A dictionary diff that depends on the resource /// The JSON value for the modified resource. A dictionary diff that depends on the resource
/// kind. /// kind.
/// ///

View file

@ -16,6 +16,8 @@ pub struct PythonAsyncClient {
impl PythonAsyncClient { impl PythonAsyncClient {
#[new] #[new]
#[pyo3(signature = (host, username=None, token=None, password=None, allow_insecure=None))] #[pyo3(signature = (host, username=None, token=None, password=None, allow_insecure=None))]
///
///
pub fn new( pub fn new(
host: String, host: String,
username: Option<String>, username: Option<String>,
@ -57,6 +59,30 @@ impl PythonAsyncClient {
.map_err(Into::into) .map_err(Into::into)
} }
#[pyo3(signature = (name, color=None, order=None, fields=None))]
pub async fn create_tag_category(
&self,
name: String,
color: Option<String>,
order: Option<u32>,
fields: Option<Vec<String>>,
) -> PyResult<TagCategoryResource> {
let mut cutagcat = CreateUpdateTagCategoryBuilder::default();
cutagcat.name(name);
if let Some(color) = color {
cutagcat.color(color);
}
if let Some(order) = order {
cutagcat.order(order);
}
let cutagcat = cutagcat.build()?;
self.client
.with_optional_fields(fields)
.create_tag_category(&cutagcat)
.await
.map_err(Into::into)
}
#[pyo3(signature = (name, version, color=None, order=None, fields=None))] #[pyo3(signature = (name, version, color=None, order=None, fields=None))]
pub async fn update_tag_category( pub async fn update_tag_category(
&self, &self,
@ -134,7 +160,8 @@ impl PythonAsyncClient {
#[pyo3(signature = (names, category=None, description=None, implications=None, suggestions=None, fields=None))] #[pyo3(signature = (names, category=None, description=None, implications=None, suggestions=None, fields=None))]
pub async fn create_tag( pub async fn create_tag(
&self, &self,
names: Vec<String>, //names: Vec<String>,
names: Py<PyAny>,
category: Option<String>, category: Option<String>,
description: Option<String>, description: Option<String>,
implications: Option<Vec<String>>, implications: Option<Vec<String>>,
@ -142,7 +169,19 @@ impl PythonAsyncClient {
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<TagResource> { ) -> PyResult<TagResource> {
let mut cubuild = CreateUpdateTagBuilder::default(); let mut cubuild = CreateUpdateTagBuilder::default();
cubuild.names(names); Python::with_gil(|py| {
if let Ok(name) = names.extract::<String>(py) {
Ok(cubuild.names(vec![name]))
} else {
let list_res = names.extract::<Vec<String>>(py);
if let Ok(names) = list_res {
Ok(cubuild.names(names))
} else {
Err(list_res.err().unwrap())
}
}
})?;
//cubuild.names(names);
if let Some(cat) = category { if let Some(cat) = category {
cubuild.category(cat); cubuild.category(cat);
} }
@ -163,10 +202,11 @@ impl PythonAsyncClient {
.map_err(Into::into) .map_err(Into::into)
} }
#[pyo3(signature = (name, names, category=None, description=None, implications=None, suggestions=None, fields=None))] #[pyo3(signature = (name, version, names=None, category=None, description=None, implications=None, suggestions=None, fields=None))]
pub async fn update_tag( pub async fn update_tag(
&self, &self,
name: String, name: String,
version: u32,
names: Option<Vec<String>>, names: Option<Vec<String>>,
category: Option<String>, category: Option<String>,
description: Option<String>, description: Option<String>,
@ -175,6 +215,7 @@ impl PythonAsyncClient {
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<TagResource> { ) -> PyResult<TagResource> {
let mut cubuild = CreateUpdateTagBuilder::default(); let mut cubuild = CreateUpdateTagBuilder::default();
cubuild.version(version);
if let Some(names) = names { if let Some(names) = names {
cubuild.names(names); cubuild.names(names);
} }
@ -220,7 +261,7 @@ impl PythonAsyncClient {
} }
#[pyo3(signature = (remove_tag, remove_tag_version, merge_to_tag, merge_to_version, fields=None))] #[pyo3(signature = (remove_tag, remove_tag_version, merge_to_tag, merge_to_version, fields=None))]
pub async fn merge_tag( pub async fn merge_tags(
&self, &self,
remove_tag: String, remove_tag: String,
remove_tag_version: u32, remove_tag_version: u32,
@ -236,7 +277,7 @@ impl PythonAsyncClient {
.build()?; .build()?;
self.client self.client
.with_optional_fields(fields) .with_optional_fields(fields)
.merge_tag(&mtags) .merge_tags(&mtags)
.await .await
.map_err(Into::into) .map_err(Into::into)
} }
@ -269,7 +310,7 @@ impl PythonAsyncClient {
} }
#[pyo3(signature = (url=None, token=None, file_path=None, thumbnail_path=None, tags=None, safety=None, source=None, #[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))] relations=None, notes=None, flags=None, anonymous=None, fields=None))]
pub async fn create_post( pub async fn create_post(
&self, &self,
url: Option<String>, url: Option<String>,
@ -282,6 +323,7 @@ impl PythonAsyncClient {
relations: Option<Vec<u32>>, relations: Option<Vec<u32>>,
notes: Option<Vec<NoteResource>>, notes: Option<Vec<NoteResource>>,
flags: Option<Vec<String>>, flags: Option<Vec<String>>,
anonymous: Option<bool>,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<PostResource> { ) -> PyResult<PostResource> {
let mut cupost = CreateUpdatePostBuilder::default(); let mut cupost = CreateUpdatePostBuilder::default();
@ -303,6 +345,9 @@ impl PythonAsyncClient {
if let Some(flags) = flags { if let Some(flags) = flags {
cupost.flags(flags); cupost.flags(flags);
} }
if let Some(anonymous) = anonymous {
cupost.anonymous(anonymous);
}
if let Some(token) = token { if let Some(token) = token {
cupost.content_token(token); cupost.content_token(token);
@ -447,7 +492,7 @@ impl PythonAsyncClient {
.map_err(Into::into) .map_err(Into::into)
} }
pub async fn reverse_search_image(&self, image_path: PathBuf) -> PyResult<ImageSearchResult> { pub async fn reverse_image_search(&self, image_path: PathBuf) -> PyResult<ImageSearchResult> {
self.client self.client
.request() .request()
.reverse_search_file_path(image_path) .reverse_search_file_path(image_path)
@ -458,7 +503,7 @@ impl PythonAsyncClient {
pub async fn post_for_image(&self, image_path: PathBuf) -> PyResult<Option<PostResource>> { pub async fn post_for_image(&self, image_path: PathBuf) -> PyResult<Option<PostResource>> {
self.client self.client
.request() .request()
.posts_for_file_path(image_path) .post_for_file_path(image_path)
.await .await
.map_err(Into::into) .map_err(Into::into)
} }
@ -493,13 +538,14 @@ impl PythonAsyncClient {
} }
#[pyo3(signature = (remove_post, remove_post_version, merge_to_post, #[pyo3(signature = (remove_post, remove_post_version, merge_to_post,
merge_to_version, fields=None))] merge_to_version, replace_post_content=false, fields=None))]
pub async fn merge_post( pub async fn merge_post(
&self, &self,
remove_post: u32, remove_post: u32,
remove_post_version: u32, remove_post_version: u32,
merge_to_post: u32, merge_to_post: u32,
merge_to_version: u32, merge_to_version: u32,
replace_post_content: bool,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<PostResource> { ) -> PyResult<PostResource> {
let mpost = MergePostBuilder::default() let mpost = MergePostBuilder::default()
@ -507,6 +553,7 @@ impl PythonAsyncClient {
.remove_post(remove_post) .remove_post(remove_post)
.merge_to_version(merge_to_version) .merge_to_version(merge_to_version)
.merge_to_post(merge_to_post) .merge_to_post(merge_to_post)
.replace_post_content(replace_post_content)
.build()?; .build()?;
self.client self.client
.with_optional_fields(fields) .with_optional_fields(fields)
@ -697,14 +744,26 @@ impl PythonAsyncClient {
#[pyo3(signature = (names, category=None, description=None, posts=None, fields=None))] #[pyo3(signature = (names, category=None, description=None, posts=None, fields=None))]
pub async fn create_pool<'py>( pub async fn create_pool<'py>(
&self, &self,
names: Vec<String>, names: Py<PyAny>,
category: Option<String>, category: Option<String>,
description: Option<String>, description: Option<String>,
posts: Option<Vec<u32>>, posts: Option<Vec<u32>>,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<PoolResource> { ) -> PyResult<PoolResource> {
let mut cupool = CreateUpdatePoolBuilder::default(); let mut cupool = CreateUpdatePoolBuilder::default();
cupool.names(names); Python::with_gil(|py| {
if let Ok(name) = names.extract::<String>(py) {
Ok(cupool.names(vec![name]))
} else {
let list_res = names.extract::<Vec<String>>(py);
if let Ok(names) = list_res {
Ok(cupool.names(names))
} else {
Err(list_res.err().unwrap())
}
}
})?;
//cupool.names(names);
if let Some(cat) = category { if let Some(cat) = category {
cupool.category(cat); cupool.category(cat);
} }
@ -885,15 +944,11 @@ impl PythonAsyncClient {
rating: i8, rating: i8,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<CommentResource> { ) -> PyResult<CommentResource> {
if rating < -1 || rating > 1 { self.client
Err(PyValueError::new_err("Rating must be -1, 0, or 1")) .with_optional_fields(fields)
} else { .rate_comment(comment_id, rating)
self.client .await
.with_optional_fields(fields) .map_err(Into::into)
.rate_comment(comment_id, rating)
.await
.map_err(Into::into)
}
} }
#[pyo3(signature = (query=None, fields=None, limit=None, offset=None))] #[pyo3(signature = (query=None, fields=None, limit=None, offset=None))]
@ -1024,11 +1079,12 @@ impl PythonAsyncClient {
.map(|ur| ur.results) .map(|ur| ur.results)
} }
#[pyo3(signature = (user_name, note=None, expiration_time=None, fields=None))] #[pyo3(signature = (user_name, note=None, enabled=None, expiration_time=None, fields=None))]
pub async fn create_user_token( pub async fn create_user_token(
&self, &self,
user_name: String, user_name: String,
note: Option<String>, note: Option<String>,
enabled: Option<bool>,
expiration_time: Option<DateTime<Utc>>, expiration_time: Option<DateTime<Utc>>,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<UserAuthTokenResource> { ) -> PyResult<UserAuthTokenResource> {
@ -1039,6 +1095,9 @@ impl PythonAsyncClient {
if let Some(etime) = expiration_time { if let Some(etime) = expiration_time {
cutoken.expiration_time(etime); cutoken.expiration_time(etime);
} }
if let Some(enabled) = enabled {
cutoken.enabled(enabled);
}
let cutoken = cutoken.build()?; let cutoken = cutoken.build()?;
self.client self.client
.with_optional_fields(fields) .with_optional_fields(fields)
@ -1137,11 +1196,12 @@ impl PythonAsyncClient {
.map_err(Into::into) .map_err(Into::into)
} }
pub async fn upload_temporary_file(&self, file_path: PathBuf) -> PyResult<TemporaryFileUpload> { pub async fn upload_temporary_file(&self, file_path: PathBuf) -> PyResult<String> {
self.client self.client
.request() .request()
.upload_temporary_file_from_path(file_path) .upload_temporary_file_from_path(file_path)
.await .await
.map_err(Into::into) .map_err(Into::into)
.map(|t| t.token)
} }
} }

View file

@ -1,6 +1,8 @@
use crate::models::PagedSearchResult; use crate::models::PagedSearchResult;
use pyo3::exceptions::PyException;
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::PyList; use pyo3::types::PyList;
use pyo3::types::PyListMethods;
pub mod asynchronous; pub mod asynchronous;
pub mod synchronous; pub mod synchronous;
@ -20,6 +22,12 @@ impl PyPagedSearchResult {
fn __repr__(&self) -> String { fn __repr__(&self) -> String {
format!("{:?}", self) format!("{:?}", self)
} }
/*fn __len__(&self) -> PyResult<usize> {
Python::with_gil(|py| {
Ok(self.results.bind_borrowed(py).len())
})
}*/
} }
impl<T: IntoPy<PyObject>> From<PagedSearchResult<T>> for PyPagedSearchResult { impl<T: IntoPy<PyObject>> From<PagedSearchResult<T>> for PyPagedSearchResult {

View file

@ -10,6 +10,20 @@ use std::path::{Path, PathBuf};
use tokio::runtime::{Builder, Runtime}; use tokio::runtime::{Builder, Runtime};
#[pyclass(name = "SzurubooruSyncClient")] #[pyclass(name = "SzurubooruSyncClient")]
/// Constructor for the SzurubooruSyncClient
/// This client is completely synchronous. For the `asyncio` compatible version,
/// see [szurubooru_client.PythonAsyncClient](SzurubooruAsyncClient)
///
/// ## Arguments
/// * `host`: Base host URL for the Szurubooru instance. Should be the protocol, hostname and any port
/// E.g `http://localhost:9801`
/// * `username`: The username used to authenticate against the Szurubooru instance. Leave blank for
/// anonymous authentication
/// * `password`: The password to use for `Basic` authentication. Token authentication should
/// be preferred
/// * `token`: The token to use for `Bearer` authentication.
/// * `allow_insecure`: Disable cert validation. Disables SSL authentication
///
pub struct PythonSyncClient { pub struct PythonSyncClient {
client: PythonAsyncClient, client: PythonAsyncClient,
runtime: Runtime, runtime: Runtime,
@ -19,6 +33,7 @@ pub struct PythonSyncClient {
impl PythonSyncClient { impl PythonSyncClient {
#[new] #[new]
#[pyo3(signature = (host, username=None, token=None, password=None, allow_insecure=None))] #[pyo3(signature = (host, username=None, token=None, password=None, allow_insecure=None))]
/// This method is for creating new instances of the SzurubooruSyncClient
pub fn new( pub fn new(
host: String, host: String,
username: Option<String>, username: Option<String>,
@ -32,6 +47,7 @@ impl PythonSyncClient {
} }
#[pyo3(signature = (fields=None))] #[pyo3(signature = (fields=None))]
/// List the available tag categories
pub fn list_tag_categories( pub fn list_tag_categories(
&self, &self,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
@ -40,6 +56,18 @@ impl PythonSyncClient {
.block_on(self.client.list_tag_categories(fields)) .block_on(self.client.list_tag_categories(fields))
} }
#[pyo3(signature = (name, color=None, order=None, fields=None))]
pub fn create_tag_category(
&self,
name: String,
color: Option<String>,
order: Option<u32>,
fields: Option<Vec<String>>,
) -> PyResult<TagCategoryResource> {
self.runtime
.block_on(self.client.create_tag_category(name, color, order, fields))
}
#[pyo3(signature = (name, version, color=None, order=None, fields=None))] #[pyo3(signature = (name, version, color=None, order=None, fields=None))]
pub fn update_tag_category( pub fn update_tag_category(
&self, &self,
@ -90,7 +118,8 @@ impl PythonSyncClient {
#[pyo3(signature = (names, category=None, description=None, implications=None, suggestions=None, fields=None))] #[pyo3(signature = (names, category=None, description=None, implications=None, suggestions=None, fields=None))]
pub fn create_tag( pub fn create_tag(
&self, &self,
names: Vec<String>, //names: Vec<String>,
names: Py<PyAny>,
category: Option<String>, category: Option<String>,
description: Option<String>, description: Option<String>,
implications: Option<Vec<String>>, implications: Option<Vec<String>>,
@ -107,10 +136,12 @@ impl PythonSyncClient {
)) ))
} }
#[pyo3(signature = (name, names, category=None, description=None, implications=None, suggestions=None, fields=None))] #[pyo3(signature = (name, version, names=None, category=None, description=None,
implications=None, suggestions=None, fields=None))]
pub fn update_tag( pub fn update_tag(
&self, &self,
name: String, name: String,
version: u32,
names: Option<Vec<String>>, names: Option<Vec<String>>,
category: Option<String>, category: Option<String>,
description: Option<String>, description: Option<String>,
@ -120,6 +151,7 @@ impl PythonSyncClient {
) -> PyResult<TagResource> { ) -> PyResult<TagResource> {
self.runtime.block_on(self.client.update_tag( self.runtime.block_on(self.client.update_tag(
name, name,
version,
names, names,
category, category,
description, description,
@ -139,7 +171,7 @@ impl PythonSyncClient {
} }
#[pyo3(signature = (remove_tag, remove_tag_version, merge_to_tag, merge_to_version, fields=None))] #[pyo3(signature = (remove_tag, remove_tag_version, merge_to_tag, merge_to_version, fields=None))]
pub fn merge_tag( pub fn merge_tags(
&self, &self,
remove_tag: String, remove_tag: String,
remove_tag_version: u32, remove_tag_version: u32,
@ -147,7 +179,7 @@ impl PythonSyncClient {
merge_to_version: u32, merge_to_version: u32,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<TagResource> { ) -> PyResult<TagResource> {
self.runtime.block_on(self.client.merge_tag( self.runtime.block_on(self.client.merge_tags(
remove_tag, remove_tag,
remove_tag_version, remove_tag_version,
merge_to_tag, merge_to_tag,
@ -173,7 +205,7 @@ impl PythonSyncClient {
} }
#[pyo3(signature = (url=None, token=None, file_path=None, thumbnail_path=None, tags=None, safety=None, source=None, #[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))] relations=None, notes=None, flags=None, anonymous=None, fields=None))]
pub fn create_post( pub fn create_post(
&self, &self,
url: Option<String>, url: Option<String>,
@ -186,6 +218,7 @@ impl PythonSyncClient {
relations: Option<Vec<u32>>, relations: Option<Vec<u32>>,
notes: Option<Vec<NoteResource>>, notes: Option<Vec<NoteResource>>,
flags: Option<Vec<String>>, flags: Option<Vec<String>>,
anonymous: Option<bool>,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<PostResource> { ) -> PyResult<PostResource> {
self.runtime.block_on(self.client.create_post( self.runtime.block_on(self.client.create_post(
@ -199,6 +232,7 @@ impl PythonSyncClient {
relations, relations,
notes, notes,
flags, flags,
anonymous,
fields, fields,
)) ))
} }
@ -258,9 +292,9 @@ impl PythonSyncClient {
.block_on(self.client.download_thumbnail_to_path(post_id, file_path)) .block_on(self.client.download_thumbnail_to_path(post_id, file_path))
} }
pub fn reverse_search_image(&self, image_path: PathBuf) -> PyResult<ImageSearchResult> { pub fn reverse_image_search(&self, image_path: PathBuf) -> PyResult<ImageSearchResult> {
self.runtime self.runtime
.block_on(self.client.reverse_search_image(image_path)) .block_on(self.client.reverse_image_search(image_path))
} }
pub fn post_for_image(&self, image_path: PathBuf) -> PyResult<Option<PostResource>> { pub fn post_for_image(&self, image_path: PathBuf) -> PyResult<Option<PostResource>> {
@ -283,13 +317,14 @@ impl PythonSyncClient {
} }
#[pyo3(signature = (remove_post, remove_post_version, merge_to_post, #[pyo3(signature = (remove_post, remove_post_version, merge_to_post,
merge_to_version, fields=None))] merge_to_version, replace_post_content=false, fields=None))]
pub fn merge_post( pub fn merge_post(
&self, &self,
remove_post: u32, remove_post: u32,
remove_post_version: u32, remove_post_version: u32,
merge_to_post: u32, merge_to_post: u32,
merge_to_version: u32, merge_to_version: u32,
replace_post_content: bool,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<PostResource> { ) -> PyResult<PostResource> {
self.runtime.block_on(self.client.merge_post( self.runtime.block_on(self.client.merge_post(
@ -297,6 +332,7 @@ impl PythonSyncClient {
remove_post_version, remove_post_version,
merge_to_post, merge_to_post,
merge_to_version, merge_to_version,
replace_post_content,
fields, fields,
)) ))
} }
@ -422,7 +458,7 @@ impl PythonSyncClient {
#[pyo3(signature = (names, category=None, description=None, posts=None, fields=None))] #[pyo3(signature = (names, category=None, description=None, posts=None, fields=None))]
pub fn create_pool( pub fn create_pool(
&self, &self,
names: Vec<String>, names: Py<PyAny>,
category: Option<String>, category: Option<String>,
description: Option<String>, description: Option<String>,
posts: Option<Vec<u32>>, posts: Option<Vec<u32>>,
@ -622,17 +658,19 @@ impl PythonSyncClient {
.block_on(self.client.list_user_tokens(user_name, fields)) .block_on(self.client.list_user_tokens(user_name, fields))
} }
#[pyo3(signature = (user_name, note=None, expiration_time=None, fields=None))] #[pyo3(signature = (user_name, note=None, enabled=None, expiration_time=None, fields=None))]
pub fn create_user_token( pub fn create_user_token(
&self, &self,
user_name: String, user_name: String,
note: Option<String>, note: Option<String>,
enabled: Option<bool>,
expiration_time: Option<DateTime<Utc>>, expiration_time: Option<DateTime<Utc>>,
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
) -> PyResult<UserAuthTokenResource> { ) -> PyResult<UserAuthTokenResource> {
self.runtime.block_on(self.client.create_user_token( self.runtime.block_on(self.client.create_user_token(
user_name, user_name,
note, note,
enabled,
expiration_time, expiration_time,
fields, fields,
)) ))
@ -702,7 +740,7 @@ impl PythonSyncClient {
self.runtime.block_on(self.client.global_info()) self.runtime.block_on(self.client.global_info())
} }
pub fn upload_temporary_file(&self, file_path: PathBuf) -> PyResult<TemporaryFileUpload> { pub fn upload_temporary_file(&self, file_path: PathBuf) -> PyResult<String> {
self.runtime self.runtime
.block_on(self.client.upload_temporary_file(file_path)) .block_on(self.client.upload_temporary_file(file_path))
} }

View file

@ -1 +0,0 @@

View file

@ -157,7 +157,7 @@ pub fn sort_token(key: &Bound<'_, PyAny>) -> PyResult<QueryToken> {
#[cfg(feature = "python")] #[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pyfunction)] #[cfg_attr(all(feature = "python"), pyfunction)]
pub fn anonymous_token(key: &Bound<'_, PyString>) -> PyResult<QueryToken> { pub fn anonymous_token(key: &Bound<'_, PyAny>) -> PyResult<QueryToken> {
QueryToken::anonymous_py(key) QueryToken::anonymous_py(key)
} }
@ -183,7 +183,11 @@ impl QueryToken {
#[pyo3(name = "token")] #[pyo3(name = "token")]
#[staticmethod] #[staticmethod]
pub fn token_py(key: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult<Self> { pub fn token_py(key: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult<Self> {
let value = value.extract::<String>()?; let value = if let Ok(value) = value.extract::<u32>() {
value.to_string()
} else {
value.extract::<String>()?
};
if let Ok(tnt) = key.extract::<TagNamedToken>() { if let Ok(tnt) = key.extract::<TagNamedToken>() {
Ok(QueryToken::token(tnt, value)) Ok(QueryToken::token(tnt, value))
@ -226,7 +230,7 @@ impl QueryToken {
#[pyo3(name = "anonymous")] #[pyo3(name = "anonymous")]
#[staticmethod] #[staticmethod]
pub fn anonymous_py(key: &Bound<'_, PyString>) -> PyResult<Self> { pub fn anonymous_py(key: &Bound<'_, PyAny>) -> PyResult<Self> {
let key = key.extract::<String>()?; let key = key.extract::<String>()?;
Ok(QueryToken::anonymous(key)) Ok(QueryToken::anonymous(key))
} }