Merge pull request #4 from slyons/dev/named_token_typesafe_fix

Fixing typesafe token values for named tokens
This commit is contained in:
Scott Lyons 2024-09-06 11:23:17 -07:00 • committed by GitHub
commit f3e7e82be0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 381 additions and 174 deletions

227
.github/workflows/CD.yaml vendored Normal file
View file

@ -0,0 +1,227 @@
# This file is autogenerated by maturin v1.7.0
# To update, run
#
# maturin generate-ci github
#
name: CD
on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
unit_test:
name: Unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo test --all-features
rust_integration_test:
name: Rust Integration test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd tests/rust-test && ./test.sh
python_integration_test:
name: Python Integration test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: getsentry/action-setup-venv@v2.1.0
id: venv
with:
python-version: 3.10.7
- run: cd tests/python-sync && ./test.sh
linux:
runs-on: ${{ matrix.platform.runner }}
defaults:
run:
working-directory: ./szurubooru-client
strategy:
matrix:
platform:
- runner: ubuntu-latest
target: x86_64
- runner: ubuntu-latest
target: x86
- runner: ubuntu-latest
target: aarch64
- runner: ubuntu-latest
target: armv7
- runner: ubuntu-latest
target: ppc64le
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: -F python --release --out dist --find-interpreter -m szurubooru-client/Cargo.toml
sccache: 'true'
manylinux: auto
before-script-linux: |
# If we're running on rhel centos, install needed packages.
if command -v yum &> /dev/null; then
yum update -y && yum install -y perl-core openssl openssl-devel pkgconfig libatomic
# If we're running on i686 we need to symlink libatomic
# in order to build openssl with -latomic flag.
if [[ ! -d "/usr/lib64" ]]; then
ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so
fi
else
# If we're running on debian-based system.
apt update -y && apt-get install -y libssl-dev openssl pkg-config
fi
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-linux-${{ matrix.platform.target }}
path: dist
musllinux:
runs-on: ${{ matrix.platform.runner }}
defaults:
run:
working-directory: ./szurubooru-client
strategy:
matrix:
platform:
- runner: ubuntu-latest
target: x86_64
- runner: ubuntu-latest
target: x86
- runner: ubuntu-latest
target: aarch64
- runner: ubuntu-latest
target: armv7
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: -F python --release --out dist --find-interpreter -m szurubooru-client/Cargo.toml
sccache: 'true'
manylinux: musllinux_1_2
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-musllinux-${{ matrix.platform.target }}
path: dist
windows:
runs-on: ${{ matrix.platform.runner }}
defaults:
run:
working-directory: ./szurubooru-client
strategy:
matrix:
platform:
- runner: windows-latest
target: x64
- runner: windows-latest
target: x86
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
architecture: ${{ matrix.platform.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: -F python --release --out dist --find-interpreter -m szurubooru-client/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-windows-${{ matrix.platform.target }}
path: dist
macos:
runs-on: ${{ matrix.platform.runner }}
defaults:
run:
working-directory: ./szurubooru-client
strategy:
matrix:
platform:
- runner: macos-12
target: x86_64
- runner: macos-14
target: aarch64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: -F python --release --out dist --find-interpreter -m szurubooru-client/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-macos-${{ matrix.platform.target }}
path: dist
sdist:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./szurubooru-client
steps:
- uses: actions/checkout@v4
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist -m szurubooru-client/Cargo.toml
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: dist
python_release:
name: Python Release
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [linux, musllinux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --non-interactive --skip-existing wheels-*/*
crate_release:
name: Crates.io Release
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [linux, musllinux, windows, macos, sdist]
steps:
- uses: actions/checkout@v4
- name: Cargo publish
run: cargo publish -p szurubooru-client --token ${CRATES_TOKEN}
env:
CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}

View file

@ -6,14 +6,7 @@
name: CI name: CI
on: on:
#push:
# branches:
# - main
# - master
# tags:
# - '*'
pull_request: pull_request:
workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@ -47,23 +40,10 @@ jobs:
python-version: 3.10.7 python-version: 3.10.7
- run: cd tests/python-sync && ./test.sh - run: cd tests/python-sync && ./test.sh
linux: linux:
runs-on: ${{ matrix.platform.runner }} runs-on: ubuntu-latest
defaults: defaults:
run: run:
working-directory: ./szurubooru-client working-directory: ./szurubooru-client
strategy:
matrix:
platform:
- runner: ubuntu-latest
target: x86_64
- runner: ubuntu-latest
target: x86
- runner: ubuntu-latest
target: aarch64
- runner: ubuntu-latest
target: armv7
- runner: ubuntu-latest
target: ppc64le
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
@ -90,145 +70,3 @@ jobs:
# If we're running on debian-based system. # If we're running on debian-based system.
apt update -y && apt-get install -y libssl-dev openssl pkg-config apt update -y && apt-get install -y libssl-dev openssl pkg-config
fi fi
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-linux-${{ matrix.platform.target }}
path: dist
musllinux:
runs-on: ${{ matrix.platform.runner }}
defaults:
run:
working-directory: ./szurubooru-client
strategy:
matrix:
platform:
- runner: ubuntu-latest
target: x86_64
- runner: ubuntu-latest
target: x86
- runner: ubuntu-latest
target: aarch64
- runner: ubuntu-latest
target: armv7
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: -F python --release --out dist --find-interpreter -m szurubooru-client/Cargo.toml
sccache: 'true'
manylinux: musllinux_1_2
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-musllinux-${{ matrix.platform.target }}
path: dist
windows:
runs-on: ${{ matrix.platform.runner }}
defaults:
run:
working-directory: ./szurubooru-client
strategy:
matrix:
platform:
- runner: windows-latest
target: x64
- runner: windows-latest
target: x86
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
architecture: ${{ matrix.platform.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: -F python --release --out dist --find-interpreter -m szurubooru-client/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-windows-${{ matrix.platform.target }}
path: dist
macos:
runs-on: ${{ matrix.platform.runner }}
defaults:
run:
working-directory: ./szurubooru-client
strategy:
matrix:
platform:
- runner: macos-12
target: x86_64
- runner: macos-14
target: aarch64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: -F python --release --out dist --find-interpreter -m szurubooru-client/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-macos-${{ matrix.platform.target }}
path: dist
sdist:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./szurubooru-client
steps:
- uses: actions/checkout@v4
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist -m szurubooru-client/Cargo.toml
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: dist
python_release:
name: Python Release
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [linux, musllinux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --non-interactive --skip-existing wheels-*/*
crate_release:
name: Crates.io Release
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [linux, musllinux, windows, macos, sdist]
steps:
- uses: actions/checkout@v4
- name: Cargo publish
run: cargo publish -p szurubooru-client --token ${CRATES_TOKEN}
env:
CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}

View file

@ -1,3 +1,9 @@
## v0.6.1 (2024-09-06)
### Fix
- **tokens**: fixing typesafe named tokens
## v0.6.0 (2024-09-05) ## v0.6.0 (2024-09-05)
### Feat ### Feat

2
Cargo.lock generated
View file

@ -1590,7 +1590,7 @@ dependencies = [
[[package]] [[package]]
name = "szurubooru-client" name = "szurubooru-client"
version = "0.6.0" version = "0.6.1"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",

118
README.md
View file

@ -1,11 +1,13 @@
# szurubooru-client # szurubooru-client
SzurubooruClient is a wrapper around the excellently-documented Szurubooru API, SzurubooruClient is a wrapper around the excellently-documented Szurubooru API,
including type-safe (if not API-safe) Query and Sort tokens. including type-safe (if not API-safe) Query and Sort tokens. Includes a Python wrapper.
## Creating a new client ## From Rust
### Basic authentication ### Creating a new client
#### Basic authentication
Please keep in mind that this is not the preferred method of authentication. Tokens Please keep in mind that this is not the preferred method of authentication. Tokens
are far superior. are far superior.
@ -15,7 +17,7 @@ let client = SzurubooruClient::new_with_basic_auth("http://localhost:5001", "myu
"mypassword", true).unwrap(); "mypassword", true).unwrap();
``` ```
### Token authentication #### Token authentication
The far superior and more secure means of authentication The far superior and more secure means of authentication
```rust ```rust
@ -23,4 +25,110 @@ use szurubooru_client::SzurubooruClient;
let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap(); let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
``` ```
For all other methods for making the requests, see the documentation. For all other methods for making the requests, see the [documentation](https://docs.rs/szurubooru-client/latest).
## From Python
Install the client using `pip install szurubooru-client`
```python
from szurubooru_client import *
from szurubooru_client.tokens import *
from szurubooru_client.models import *
```
### Creating a client
```python
client = SzurubooruSyncClient("http://localhost:9802", username="integration_user",
password="integration_password", allow_insecure=True)
```
### Creating a new tag
```python
foo_tag = client.create_tag("foo", category="default", description="The foo tag")
assert foo_tag.names == ["foo"]
```
### Returning only a subset of fields
```python
# Omit the "description" field
tags = client.list_tags(fields=["version", "names", "category"])
assert tags.results[0].description is None
```
### Uploading from a file path
```python
folly1 = client.create_post(file_path="../folly1.jpg",
tags=["maine_coon", "cat", "folly1"],
safety=PostSafety.Safe)
```
### Searching for an existing post using an image
```python
folly1_search = client.post_for_image("../folly1.jpg")
assert folly1_search is not None
```
### Querying by an anonymous tag
```python
cat_posts = client.list_posts(query=[anonymous_token("cat")])
```
### Querying by a named tag
```python
mc_posts = client.list_posts(query=[named_token(PostNamedToken.Tag, "maine_coon")])
```
### Pagination
```python
posts = client.list_posts(limit=1)
assert posts.total == 4
assert len(posts.results) == 1
posts2 = client.list_posts(limit=1, offset=1)
assert posts.results != posts2.results
```
### Commenting on a post
```python
cat_results = client.list_posts([anonymous_token("cat")])
post_id = cat_results.results[0].id
comment = client.create_comment("Excellent cat!", post_id)
```
### Getting all comments for a post
```python
comment_list = client.list_comments([named_token(CommentNamedToken.Post, post_id)])
assert len(comment_list.results) != 0
```
### Downloading an image to a local path
```python
cat_results = client.list_posts([anonymous_token("cat")])
post_id = cat_results.results[0].id
client.download_image_to_path(post_id, "/tmp/cat.jpg")
```

View file

@ -1,3 +1,9 @@
## v0.6.1 (2024-09-06)
### Fix
- **tokens**: fixing typesafe named tokens
## v0.6.0 (2024-09-05) ## v0.6.0 (2024-09-05)
### Feat ### Feat

View file

@ -1,6 +1,6 @@
[package] [package]
name = "szurubooru-client" name = "szurubooru-client"
version = "0.6.0" version = "0.6.1"
edition = "2021" edition = "2021"
authors = ["Scott Lyons <scottalyons@gmail.com>"] authors = ["Scott Lyons <scottalyons@gmail.com>"]
description = "A wrapper around the Szurubooru API, including type-safe Query and Sort tokens" description = "A wrapper around the Szurubooru API, including type-safe Query and Sort tokens"

View file

@ -326,6 +326,7 @@ impl TagSibling {
all(feature = "python"), all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models") pyclass(eq, eq_int, module = "szurubooru_client.models")
)] )]
#[strum(serialize_all = "camelCase")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// The type of post /// The type of post
pub enum PostType { pub enum PostType {
@ -352,6 +353,7 @@ pub enum PostType {
all(feature = "python"), all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models") pyclass(eq, eq_int, module = "szurubooru_client.models")
)] )]
#[strum(serialize_all = "camelCase")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// How SFW/NSFW the post is /// How SFW/NSFW the post is
pub enum PostSafety { pub enum PostSafety {
@ -648,11 +650,12 @@ impl NoteResource {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)]
#[cfg_attr( #[cfg_attr(
all(feature = "python"), all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models") pyclass(eq, eq_int, module = "szurubooru_client.models")
)] )]
#[strum(serialize_all = "camelCase")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// The Rank of a given User /// The Rank of a given User
pub enum UserRank { pub enum UserRank {
@ -668,11 +671,12 @@ pub enum UserRank {
Administrator, Administrator,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)]
#[cfg_attr( #[cfg_attr(
all(feature = "python"), all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models") pyclass(eq, eq_int, module = "szurubooru_client.models")
)] )]
#[strum(serialize_all = "camelCase")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// The kind of User Avatar /// The kind of User Avatar
pub enum UserAvatarStyle { pub enum UserAvatarStyle {
@ -1358,11 +1362,12 @@ pub struct CreateUpdateComment {
pub post_id: Option<u32>, pub post_id: Option<u32>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)]
#[cfg_attr( #[cfg_attr(
all(feature = "python"), all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models") pyclass(eq, eq_int, module = "szurubooru_client.models")
)] )]
#[strum(serialize_all = "camelCase")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// The kind of snapshot that has been recorded /// The kind of snapshot that has been recorded
pub enum SnapshotOperationType { pub enum SnapshotOperationType {
@ -1376,11 +1381,12 @@ pub enum SnapshotOperationType {
Merged, Merged,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)]
#[cfg_attr( #[cfg_attr(
all(feature = "python"), all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models") pyclass(eq, eq_int, module = "szurubooru_client.models")
)] )]
#[strum(serialize_all = "camelCase")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
/// The kind of resource described by this snapshot /// The kind of resource described by this snapshot
pub enum SnapshotResourceType { pub enum SnapshotResourceType {

View file

@ -2,6 +2,8 @@
//! warned that the types here help with the Type safety for the Tag names only. It does //! 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. //! not guarantee that a given API endpoint will support the given tag.
#[cfg(feature = "python")]
use crate::models::{PostSafety, PostType, SnapshotOperationType, SnapshotResourceType, UserRank};
#[cfg(feature = "python")] #[cfg(feature = "python")]
use pyo3::{exceptions::PyValueError, prelude::*}; use pyo3::{exceptions::PyValueError, prelude::*};
use std::fmt::Display; use std::fmt::Display;
@ -269,6 +271,16 @@ impl QueryToken {
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 = if let Ok(value) = value.extract::<u32>() { let value = if let Ok(value) = value.extract::<u32>() {
value.to_string() value.to_string()
} else if let Ok(tv) = value.extract::<PostSafety>() {
tv.as_ref().to_string()
} else if let Ok(tv) = value.extract::<PostType>() {
tv.as_ref().to_string()
} else if let Ok(tv) = value.extract::<UserRank>() {
tv.as_ref().to_string()
} else if let Ok(tv) = value.extract::<SnapshotOperationType>() {
tv.as_ref().to_string()
} else if let Ok(tv) = value.extract::<SnapshotResourceType>() {
tv.as_ref().to_string()
} else { } else {
value.extract::<String>()? value.extract::<String>()?
}; };

View file

@ -157,6 +157,10 @@ def test_creating_posts(client):
cat_posts = client.list_posts(query=[named_token(PostNamedToken.Tag, "maine_coon")]) cat_posts = client.list_posts(query=[named_token(PostNamedToken.Tag, "maine_coon")])
assert len(cat_posts.results) == 4 assert len(cat_posts.results) == 4
logger.info("Querying using typesafe types")
cat_posts = client.list_posts(query=[named_token(PostNamedToken.Safety, PostSafety.Safe)])
assert len(cat_posts.results) == 4
logger.info("Testing pagination") logger.info("Testing pagination")
cat_posts = client.list_posts(limit=1) cat_posts = client.list_posts(limit=1)
assert cat_posts.total == 4 assert cat_posts.total == 4