Merge pull request #1 from slyons/dev/pylib

Adding Python library to client
This commit is contained in:
Scott Lyons 2024-09-05 15:53:03 -07:00 • committed by GitHub
commit f9363c3c38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 6552 additions and 212 deletions

234
.github/workflows/CI.yaml vendored Normal file
View file

@ -0,0 +1,234 @@
# This file is autogenerated by maturin v1.7.0
# To update, run
#
# maturin generate-ci github
#
name: CI
on:
push:
branches:
- main
- master
tags:
- '*'
pull_request:
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: "startsWith(github.ref, 'refs/tags/')"
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: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, musllinux, windows, macos, sdist]
steps:
- uses: actions/checkout@v4
- name: Cargo publish
run: cargo publish --token ${CRATES_TOKEN}
env:
CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}

4
.gitignore vendored
View file

@ -5,3 +5,7 @@ szurubooru-integration-test/szurubooru/data/*
szurubooru-integration-test/szurubooru/pgsql/*
szurubooru-integration-test/szurubooru/server/*
**/.DS_Store
**/target
*.so
*.pyc
/docs/_build/**

View file

@ -1,3 +1,21 @@
## v0.5.0 (2024-09-05)
### Feat
- **all**: adding python library wrapper
### Fix
- **python**: refactoring python client
- **cargo**: adding vendored openssl to help with build
- **python-client**: fixing paged search results to use a PyList instead
### Refactor
- **python**: adding custom python modules to possibly help with documentation
- **models**: removing unnecessary python classes
- **models**: saving some changes in prep of python library
## v0.4.0 (2024-08-12)
### Feat

235
Cargo.lock generated
View file

@ -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"
@ -846,6 +883,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.3.1+3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.103"
@ -854,6 +900,7 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
@ -887,12 +934,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 +1025,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 +1049,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"
@ -1102,6 +1268,19 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rust-test"
version = "0.1.0"
dependencies = [
"chrono",
"sha1",
"szurubooru-client",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -1213,18 +1392,28 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.205"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.205"
name = "serde-pyobject"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
checksum = "ca4b0aad8b225845739a0030a0d5cc2ae949c56a86a7daf9226c7df7c2016d16"
dependencies = [
"pyo3",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
@ -1290,6 +1479,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"
@ -1357,9 +1552,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.72"
version = "2.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
dependencies = [
"proc-macro2",
"quote",
@ -1395,7 +1590,7 @@ dependencies = [
[[package]]
name = "szurubooru-client"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"base64",
"bytes",
@ -1404,8 +1599,11 @@ dependencies = [
"futures-util",
"hex",
"mockito",
"openssl",
"pyo3",
"reqwest",
"serde",
"serde-pyobject",
"serde_json",
"sha1",
"strum",
@ -1418,17 +1616,10 @@ dependencies = [
]
[[package]]
name = "szurubooru-integration-test"
version = "0.1.0"
dependencies = [
"chrono",
"sha1",
"szurubooru-client",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
]
name = "target-lexicon"
version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tempfile"
@ -1682,6 +1873,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"

View file

@ -2,5 +2,7 @@
resolver = "2"
members = [
"szurubooru-client",
"szurubooru-integration-test"
#"szurubooru-integration-test",
"tests/rust-test"
]
default-members = ["szurubooru-client"]

20
LICENSE Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2024 Scott Lyons
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

28
docs/Makefile Normal file
View file

@ -0,0 +1,28 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
serve:
python -m http.server -d $(BUILDDIR)/html
clean:
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
pip uninstall -y szurubooru_client
maturin develop -F python -m ../szurubooru-client/Cargo.toml
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

63
docs/client.rst Normal file
View file

@ -0,0 +1,63 @@
Szurubooru Clients
==================
These are the clients available for interacting with Szurubooru. There are two clients, ``SzurubooruSyncClient`` and ``SzurubooruAsyncClient`` . The only difference
between the two of them is that ``SzurubooruAsyncClient`` is compatible with ``asyncio`` for use in ``async`` scripts.
.. contents:: Table of Contents
:depth: 3
:local:
Sync Client
-----------
.. autoclass:: szurubooru_client.SzurubooruSyncClient
Async Client
------------
.. autoclass:: szurubooru_client.SzurubooruAsyncClient
Exceptions
----------
All exceptions that are thrown by the client are instances of the ``SzuruClientError`` class. It's a tuple of two items: An exception type (as a string) and a string with more details
about the exception.
.. autoexception:: szurubooru_client.SzuruClientError
.. _rver:
Resource Versioning
-------------------
Many of the ``update`` and other methods on the client classes accept a ``version`` argument. This is part of Szurubooru's optimistic locking. Each resource has its ``version`` retuned
to the client through any of the ``get`` methods. This value must be provided to any of the ``update`` or ``delete`` methods. If the version doesn't match at the time
(due to a modified resource) then the call will fail.
Pagination
----------
Most of the ``list`` methods on the clients return a paged object that holds the results along with other details about the query:
.. autoclass:: szurubooru_client.PagedResult
Paging through the results of ``list`` methods are done using two parameters:
.. _limits:
Result Limits
^^^^^^^^^^^^^
Most of the ``list`` methods on the clients support a ``limit`` parameter. This parameter limits the number of resources returned by the method as part of
a :class:`~szurubooru_client.PagedResult` object
.. _offsets:
Result Offsets
^^^^^^^^^^^^^^
Most of the ``list`` methods on the clients support a ``offset`` parameter. This parameter skips through the results returned by the method as part of
a :class:`~szurubooru_client.PagedResult` object

41
docs/conf.py Normal file
View file

@ -0,0 +1,41 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Szurubooru Client'
copyright = '2024, Scott Lyons'
author = 'Scott Lyons'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'myst_parser',
'sphinx.ext.autodoc',
"sphinx_codefence"
]
autodoc_default_options = {
'members': True,
'show-inheritance': True,
}
autodoc_typehints = "description"
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
source_suffix = {
'.rst': 'restructuredtext',
'.txt': 'markdown',
'.md': 'markdown',
}
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'alabaster'
html_static_path = ['_static']

113
docs/examples.rst Normal file
View file

@ -0,0 +1,113 @@
========
Examples
========
.. contents:: Table of Contents
:depth: 3
:local:
All of these examples are taken from the ``python-sync`` script in the ``tests`` directory of the repo.
Before any of the examples will work, you need to import the right classes and functions:
```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")
```

15
docs/fields.rst Normal file
View file

@ -0,0 +1,15 @@
Field selection
===============
The Szurubooru API supports *Field Selection*. This allows you to select only a subset of fields to return from the API. By default all fields are selected and returned, but if you
pass in a list of field names to any of the client methods that support it, only those fields will be populated in the models.
Example
-------
```python
tags = client.list_tags(fields=["version", "names", "category"])
assert tags.results[0].description is None
```
Because the ``description`` field was not specified, it's set to ``None``

15
docs/index.rst Normal file
View file

@ -0,0 +1,15 @@
Szurubooru Client documentation
===============================
Welcome to the documentation for ``szurubooru_client`` . This library is a wrapper around a Rust library for interacting with the self-hosted
image site `Szurubooru <https://github.com/rr-/szurubooru/tree/master>`_
.. toctree::
:maxdepth: 1
:caption: Contents:
client
tokens
models
fields
examples

35
docs/make.bat Normal file
View file

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

10
docs/models.rst Normal file
View file

@ -0,0 +1,10 @@
Szurubooru Models
=================
.. contents:: Table of Contents
:depth: 3
:local:
.. automodule:: szurubooru_client.models
:members:
:undoc-members:

830
docs/tokens.rst Normal file
View file

@ -0,0 +1,830 @@
Szurubooru Query Tokens
=======================
.. contents:: Table of Contents
:depth: 3
:local:
Most of the ``list`` methods on the client support a ``query`` parameter. This parameter (a ``list`` of tokens) allows you to filter the results returned by the API.
There's four kinds of tokens: ``Named``, ``Sort``, ``Anonymous`` and ``Special``.
Because this library is a wrapper around a Rust library, there's a group of type-safe ``Enum`` objects that allow you to
make sure you get the right field names when constructing the tokens.
All tokens can be mixed and matched to filter the results how you'd prefer.
.. _named-tokens:
Named tokens
^^^^^^^^^^^^
.. autofunction:: szurubooru_client.tokens.named_token
.. _sort-tokens:
Sort tokens
^^^^^^^^^^^
.. autofunction:: szurubooru_client.tokens.sort_token
Anonymous tokens
^^^^^^^^^^^^^^^^
.. autofunction:: szurubooru_client.tokens.anonymous_token
Special tokens
^^^^^^^^^^^^^^
.. autofunction:: szurubooru_client.tokens.special_token
Negating tokens
^^^^^^^^^^^^^^^
All query tokens can be negated using either ``.negate`` or by prefixing with the ``-`` operator. Negated tokens mean to *not* match that particular token.
For example, to match the anonymous tag ``konosuba``, we would do the following:
```python
client.list_posts(query=[anonymous_token("konosuba")])
```
Now to match all posts that *don't* have the ``konosuba`` tag:
```python
client.list_posts(query=[-anonymous_token("konosuba")])
# or
client.list_posts(query=[anonymous_token("konosuba").negate()])
```
.. _named-tokens-enums:
Token Enums
^^^^^^^^^^^
-----------------
Named Token Enums
-----------------
.. py:class:: szurubooru_client.tokens.TagNamedToken
.. py:attribute:: Name
Having given name (accepts wildcards)
.. py:attribute:: Category
Having given category (accepts wildcards)
.. py:attribute:: CreationDate
Created at given date
.. py:attribute:: LastEditDate
Edited at given date
.. py:attribute:: LastEditTime
Alias of :attr:`LastEditTime <szurubooru_client.tokens.TagNamedToken.LastEditTime>`
.. py:attribute:: EditDate
Alias of :attr:`LastEditTime <szurubooru_client.tokens.TagNamedToken.LastEditTime>`
.. py:attribute:: EditTime
Alias of :attr:`LastEditTime <szurubooru_client.tokens.TagNamedToken.LastEditTime>`
.. py:attribute:: Usages
Used in given number of posts
.. py:attribute:: UsageCount
Alias of :attr:`Usages <szurubooru_client.tokens.TagNamedToken.Usages>`
.. py:attribute:: PostCount
Alias of :attr:`Usages <szurubooru_client.tokens.TagNamedToken.Usages>`
.. py:attribute:: SuggestionCount
With given number of suggestions
.. py:attribute:: ImplicationCount
With given number of implications
.. py:class:: szurubooru_client.tokens.PostNamedToken
.. py:attribute:: Id
Having given post number
.. py:attribute:: Tag
Having given tag (accepts wildcards)
.. py:attribute:: Score
Having given score
.. py:attribute:: Uploader
Uploaded by given user (accepts wildcards)
.. py:attribute:: Upload
Alias of :attr:`Uploader <szurubooru_client.tokens.PostNamedToken.Uploader>`
.. py:attribute:: Submit
Alias of :attr:`Uploader <szurubooru_client.tokens.PostNamedToken.Uploader>`
.. py:attribute:: Comment
Commented by given user (accepts wildcards)
.. py:attribute:: Fav
Favorited by given user (accepts wildcards)
.. py:attribute:: Pool
Belonging to the pool with the given id
.. py:attribute:: TagCount
Having given number of tags
.. py:attribute:: CommentCount
Having given number of comments
.. py:attribute:: FavCount
Favorited by given number of users
.. py:attribute:: NoteCount
Having given number of annotations
.. py:attribute:: NoteText
Having given note text (accepts wildcards)
.. py:attribute:: RelationCount
Having given number of relations
.. py:attribute:: FeatureCount
Having been featured given number of times
.. py:attribute:: Type
``flash`` (or ``swf``) or ``video`` (or ``webm``). Use :attr:`models.posttype` for type-safe values
.. py:attribute:: ContentChecksum
Having given sha1 checksum
.. py:attribute:: FileSize
Having given file size (in bytes)
.. py:attribute:: ImageWidth
Having given image width (where applicable)
.. py:attribute:: ImageHeight
Having given image height (where applicable)
.. py:attribute:: ImageArea
Having given number of pixels (image width * image height)
.. py:attribute:: ImageAspectRatio
Having given aspect ratio (image width / image height)
.. py:attribute:: ImageAr
Alias of :attr:`ImageAspectRatio <szurubooru_client.tokens.PostNamedToken.ImageAspectRatio>`
.. py:attribute:: Width
Alias of :attr:`ImageWidth <szurubooru_client.tokens.PostNamedToken.ImageWidth>`
.. py:attribute:: Height
Alias of :attr:`ImageHeight <szurubooru_client.tokens.PostNamedToken.ImageHeight>`
.. py:attribute:: Ar
Alias of :attr:`ImageAspectRatio <szurubooru_client.tokens.PostNamedToken.ImageAspectRatio>`
.. py:attribute:: AspectRatio
Alias of :attr:`ImageAspectRatio <szurubooru_client.tokens.PostNamedToken.ImageAspectRatio>`
.. py:attribute:: CreationDate
Posted at given date
.. py:attribute:: CreationTime
Alias of :attr:`CreationDate <szurubooru_client.tokens.PostNamedToken.CreationDate>`
.. py:attribute:: Date
Alias of :attr:`CreationDate <szurubooru_client.tokens.PostNamedToken.CreationDate>`
.. py:attribute:: Time
Alias of :attr:`CreationDate <szurubooru_client.tokens.PostNamedToken.CreationDate>`
.. py:attribute:: LastEditDate
Edited at given date
.. py:attribute:: LastEditTime
Alias of :attr:`LastEditDate <szurubooru_client.tokens.PostNamedToken.LastEditDate>`
.. py:attribute:: EditDate
Alias of :attr:`LastEditDate <szurubooru_client.tokens.PostNamedToken.LastEditDate>`
.. py:attribute:: EditTime
Alias of :attr:`LastEditDate <szurubooru_client.tokens.PostNamedToken.LastEditDate>`
.. py:attribute:: CommentDate
Commented at given date
.. py:attribute:: CommentTime
Alias of :attr:`CommentDate <szurubooru_client.tokens.PostNamedToken.CommentDate>`
.. py:attribute:: FavDate
Last favorited at given time
.. py:attribute:: FavTime
Alias of :attr:`FavDate <szurubooru_client.tokens.PostNamedToken.FavDate>`
.. py:attribute:: FeatureDate
Featured at given date
.. py:attribute:: FeatureTime
Alias of :attr:`FeatureDate <szurubooru_client.tokens.PostNamedToken.FeatureDate>`
.. py:attribute:: Safety
Use :class:`PostSafety <szurubooru_client.models.PostSafety>` for the type-safe version
.. py:attribute:: Rating
Alias of :attr:`Safety <szurubooru_client.tokens.PostNamedToken.Safety>`
.. py:class:: szurubooru_client.tokens.CommentNamedToken
.. py:attribute:: Id
Specific comment id
.. py:attribute:: Post
Specific post id
.. py:attribute:: User
Created by given user (accepts wildcards)
.. py:attribute:: Author
Alias of user
.. py:attribute:: Text
Containing given text (accepts wildcards)
.. py:attribute:: CreationDate
Created at given date
.. py:attribute:: CreationTime
Alias of :attr:`CreationDate <szurubooru_client.tokens.CommentNamedToken.CreationDate>`
.. py:attribute:: LastEditDate
Whose most recent edit date matches given date
.. py:attribute:: LastEditTime
Alias of :attr:`LastEditDate <szurubooru_client.tokens.CommentNamedToken.LastEditDate>`
.. py:attribute:: EditDate
Alias of :attr:`LastEditDate <szurubooru_client.tokens.CommentNamedToken.LastEditDate>`
.. py:attribute:: EditTime
Alias of :attr:`LastEditDate <szurubooru_client.tokens.CommentNamedToken.LastEditDate>`
.. py:class:: szurubooru_client.tokens.UserNamedToken
.. py:attribute:: Name
Having given name (accepts wildcards)
.. py:attribute:: CreationDate
Registered at given date
.. py:attribute:: CreationTime
Alias of :attr:`CreationDate <szurubooru_client.tokens.UserNamedToken.CreationDate>`
.. py:attribute:: LastLoginDate
Whose most recent login date matches given date
.. py:attribute:: LastLoginTime
Alias of :attr:`LastLoginDate <szurubooru_client.tokens.UserNamedToken.LastLoginDate>`
.. py:attribute:: LoginDate
Alias of :attr:`LastLoginDate <szurubooru_client.tokens.UserNamedToken.LastLoginDate>`
.. py:attribute:: LoginTime
Alias of :attr:`LastLoginDate <szurubooru_client.tokens.UserNamedToken.LastLoginDate>`
.. py:class:: szurubooru_client.tokens.SnapshotNamedToken
.. py:attribute:: Type
Involving given resource type
.. py:attribute:: Id
Involving given resource id
.. py:attribute:: Date
Created at given date
.. py:attribute:: Time
Alias of :attr:`Date <szurubooru_client.tokens.UserNamedToken.LastLoginDate>`
.. py:attribute:: Operation
``modified``, ``created``, ``deleted`` or ``merged``. Use :attr:`SnapshotType <szurubooru_client.models.SnapshotType>` for type-safe values
.. py:attribute:: User
Name of the user that created given snapshot (accepts wildcards)
.. _sort-tokens-enums:
----------------
Sort Token Enums
----------------
.. py:class:: szurubooru_client.tokens.PostSortToken
.. py:attribute:: Random
As random as it can get
.. py:attribute:: Id
Highest to lowest post number
.. py:attribute:: Score
Highest scored
.. py:attribute:: TagCount
With most tags
.. py:attribute:: CommentCount
Most commented first
.. py:attribute:: FavCount
Loved by most
.. py:attribute:: NoteCount
With most annotations
.. py:attribute:: RelationCount
With most relations
.. py:attribute:: FeatureCount
Most often featured
.. py:attribute:: FileSize
Largest files first
.. py:attribute:: ImageWidth
Widest images first
.. py:attribute:: ImageHeight
Tallest images first
.. py:attribute:: ImageArea
Largest images first
.. py:attribute:: Width
Alias of :attr:`ImageWidth <szurubooru_client.tokens.PostSortToken.imagewidth>`
.. py:attribute:: Height
Alias of :attr:`ImageHeight <szurubooru_client.tokens.PostSortToken.imageheight>`
.. py:attribute:: Area
Alias of :attr:`ImageArea <szurubooru_client.tokens.PostSortToken.imagearea>`
.. py:attribute:: CreationDate
Newest to oldest (pretty much same as id)
.. py:attribute:: CreationTime
Alias of :attr:`CreationDate <szurubooru_client.tokens.PostSortToken.creationdate>`
.. py:attribute:: Date
Alias of :attr:`CreationDate <szurubooru_client.tokens.PostSortToken.creationdate>`
.. py:attribute:: Time
Alias of :attr:`CreationDate <szurubooru_client.tokens.PostSortToken.creationdate>`
.. py:attribute:: LastEditDate
Like :attr:`CreationDate <szurubooru_client.tokens.PostSortToken.creationdate>`, only looks at last edit time instead
.. py:attribute:: LastEditTime
Alias of :attr:`LastEditDate <szurubooru_client.tokens.PostSortToken.lasteditdate>`
.. py:attribute:: EditDate
Alias of :attr:`LastEditDate <szurubooru_client.tokens.PostSortToken.lasteditdate>`
.. py:attribute:: EditTime
Alias of :attr:`LastEditDate <szurubooru_client.tokens.PostSortToken.lasteditdate>`
.. py:attribute:: CommentDate
Recently commented by anyone
.. py:attribute:: CommentTime
Alias of :attr:`CommentDate <szurubooru_client.tokens.PostSortToken.commentdate>`
.. py:attribute:: FavDate
Recently added to favorites by anyone
.. py:attribute:: FavTime
Alias of :attr:`FavDate <szurubooru_client.tokens.PostSortToken.favdate>`
.. py:attribute:: FeatureDate
Recently featured
.. py:attribute:: FeatureTime
Alias of :attr:`FeatureDate <szurubooru_client.tokens.PostSortToken.featuredate>`
.. py:class:: szurubooru_client.tokens.TagSortToken
.. py:attribute:: Random
As random as it can get
.. py:attribute:: Name
A to Z
.. py:attribute:: Category
Category (a to z)
.. py:attribute:: CreationDate
Recently created first
.. py:attribute:: CreationTime
Alias of :attr:`CreationDate <szurubooru_client.tokens.TagSortToken.CreationDate>`
.. py:attribute:: LastEditDate
Recently edited first
.. py:attribute:: LastEditTime
Alias of :attr:`CreationDate <szurubooru_client.tokens.TagSortToken.CreationDate>`
.. py:attribute:: EditDate
Alias of :attr:`CreationDate <szurubooru_client.tokens.TagSortToken.CreationDate>`
.. py:attribute:: EditTime
Alias of :attr:`CreationDate <szurubooru_client.tokens.TagSortToken.CreationDate>`
.. py:attribute:: Usages
Used in most posts first
.. py:attribute:: UsageCount
Alias of :attr:`Usages <szurubooru_client.tokens.TagSortToken.Usages>`
.. py:attribute:: PostCount
Alias of :attr:`Usages <szurubooru_client.tokens.TagSortToken.Usages>`
.. py:attribute:: SuggestionCount
With most suggestions first
.. py:attribute:: ImplicationCount
With most implications first
.. py:class:: szurubooru_client.tokens.CommentSortToken
.. py:attribute:: Random
As random as it can get
.. py:attribute:: User
Author name, a to z
.. py:attribute:: Author
Alias of user
.. py:attribute:: Post
Post id, newest to oldest
.. py:attribute:: CreationDate
Newest to oldest
.. py:attribute:: CreationTime
Alias of :attr:`CreationDate <szurubooru_client.tokens.CommentSortToken.CreationDate>`
.. py:attribute:: LastEditDate
Recently edited first
.. py:attribute:: LastEditTime
Alias of :attr:`LastEditDate <szurubooru_client.tokens.CommentSortToken.LastEditDate>`
.. py:attribute:: EditDate
Alias of :attr:`LastEditDate <szurubooru_client.tokens.CommentSortToken.LastEditDate>`
.. py:attribute:: EditTime
Alias of :attr:`LastEditDate <szurubooru_client.tokens.CommentSortToken.LastEditDate>`
.. py:class:: szurubooru_client.tokens.UserSortToken
.. py:attribute:: Random
As random as it can get
.. py:attribute:: Name
A to z
.. py:attribute:: CreationDate
Newest to oldest
.. py:attribute:: CreationTime
Alias of :attr:`CreationDate <szurubooru_client.tokens.UserSortToken.CreationDate>`
.. py:attribute:: LastLoginDate
Recently active first
.. py:attribute:: LastLoginTime
Alias of :attr:`LastLoginDate <szurubooru_client.tokens.UserSortToken.CreationDate>`
.. py:attribute:: LoginDate
Alias of :attr:`LastLoginDate <szurubooru_client.tokens.UserSortToken.CreationDate>`
.. py:attribute:: LoginTime
Alias of :attr:`LastLoginDate <szurubooru_client.tokens.UserSortToken.CreationDate>`
.. _special-tokens-enum:
-------------------
Special Token Enums
-------------------
.. py:class:: szurubooru_client.tokens.PostSpecialToken
.. py:attribute:: Liked
Posts liked by currently logged-in user
.. py:attribute:: Disliked
Posts disliked by currently logged in user
.. py:attribute:: Fav
Posts added to favorites by currently logged-in user
.. py:attribute:: Tumbleweed
Posts with score of 0, without comments and without favorites

View file

@ -1,3 +1,21 @@
## v0.5.0 (2024-09-05)
### Feat
- **all**: adding python library wrapper
### Fix
- **python**: refactoring python client
- **cargo**: adding vendored openssl to help with build
- **python-client**: fixing paged search results to use a PyList instead
### Refactor
- **python**: adding custom python modules to possibly help with documentation
- **models**: removing unnecessary python classes
- **models**: saving some changes in prep of python library
## v0.4.0 (2024-08-12)
### Feat

View file

@ -1,6 +1,6 @@
[package]
name = "szurubooru-client"
version = "0.4.0"
version = "0.5.0"
edition = "2021"
authors = ["Scott Lyons <scottalyons@gmail.com>"]
description = "A wrapper around the Szurubooru API, including type-safe Query and Sort tokens"
@ -16,13 +16,17 @@ chrono = { version = "0.4.38", features = ["serde"] }
derive_builder = "0.20.0"
futures-util = "0.3.30"
hex = "0.4.3"
openssl = { version = "0.10.66", features = ["vendored"] }
pyo3 = { version="0.22.0", optional=true, features=["chrono-tz", "chrono", "serde", "experimental-async"] }
reqwest = { version = "0.12.5", features = ["json", "multipart", "stream"] }
serde = { version = "1.0.204", features = ["derive"] }
serde-pyobject = { version = "0.4.0", optional = true }
serde_json = "1.0.120"
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 +34,11 @@ urlencoding = "2.1.3"
[dev-dependencies]
mockito = "1.4.0"
tokio = { version = "1.39.2", features = ["full"] }
[features]
python = ["dep:pyo3", "dep:tokio", "dep:serde-pyobject", "pyo3/extension-module"]
extension-module = ["pyo3/extension-module"]
[lib]
name = "szurubooru_client"
crate-type = ["cdylib", "lib"]

View file

@ -0,0 +1,24 @@
[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",
"Development Status :: 4 - Beta",
"Environment :: Console",
"Environment :: Web Environment",
"Intended Audience :: End Users/Desktop",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX",
"Topic :: Multimedia :: Graphics",
]
dynamic = ["version"]
[tool.maturin]
features = ["pyo3/extension-module", "python"]

View file

@ -128,10 +128,10 @@ impl SzurubooruClient {
/// Construct a new request using the existing client auth and base URL
/// All requests start with the [SzurubooruClient] struct.
/// The (request)[SzurubooruClient::request],
/// (with_fields)[SzurubooruClient::fields],
/// (limit)[SzurubooruClient::limit] and
/// (offset)[SzurubooruClient::offset] methods all return a [SzurubooruRequest] struct that will
/// The [request](crate::SzurubooruClient::request),
/// [with_fields](crate::SzurubooruClient::with_fields),
/// [with_limit](crate::SzurubooruClient::with_limit) and
/// [with_offset](crate::SzurubooruClient::with_offset) methods all return a [SzurubooruRequest] struct that will
/// enable you to actually make the requests.
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
@ -149,31 +149,36 @@ impl SzurubooruClient {
/// Construct a new request while selecting only the given fields
/// The Szurubooru API supports selecting a subset of fields for a given resource.
/// Most resource (models)[szurubooru_client::models] have [Option] fields because of that.
/// Most resource [models](crate::models) have [Option] fields because of that.
/// The default is to return all fields for a given resource.
/// See [here](https://github.com/rr-/szurubooru/blob/master/doc/API.md#field-selecting) for
/// more details
///
/// For example, to select only the `version`, `id` and `content_url` fields of a
/// (PostResource)[szurubooru_client::models::PostResource]
/// [PostResource]
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
/// # async {
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// let new_request = client.request().with_fields(vec!["version", "id", "content_url"]);
/// let new_request = client.request().with_fields(vec!["version".to_string(), "id".to_string(), "content_url".to_string()]);
/// # };
/// # ()
/// ```
pub fn with_fields<'a>(&'a self, fields: Vec<&'a str>) -> SzurubooruRequest {
pub fn with_fields(&self, fields: Vec<String>) -> SzurubooruRequest {
self.request().with_fields(fields)
}
/// The same as [with_fields](SzurubooruClient::with_fields), but accepts an Option type instead
pub fn with_optional_fields(&self, fields: Option<Vec<String>>) -> SzurubooruRequest {
self.request().with_optional_fields(fields)
}
/// Construct a new request with the given limit
/// The Szurubooru API supports limiting the number of resources returned for Paginated
/// API endpoints.
///
/// For example, to limit the number of pools returned by (list_pools)[SzurubooruRequest::list_pools]
/// For example, to limit the number of pools returned by [list_pools](SzurubooruRequest::list_pools)
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
@ -190,12 +195,17 @@ impl SzurubooruClient {
self.request().with_limit(limit)
}
/// The same as [with_limit](SzurubooruClient::with_limit), but accepts an Option type instead
pub fn with_optional_limit(&self, limit: Option<u32>) -> SzurubooruRequest {
self.request().with_optional_limit(limit)
}
/// Construct a new request starting at the given offset
/// The Szurubooru API supports offsetting the results returned from Paginated API
/// endpoints. Use this offset in combination with the limit to page through
/// large result sets.
///
/// For example, to offset the list of pools returned by (list_pools)[SzurubooruRequest::list_pools]
/// For example, to offset the list of pools returned by [list_pools](SzurubooruRequest::list_pools)
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
@ -211,14 +221,23 @@ impl SzurubooruClient {
pub fn with_offset(&self, offset: u32) -> SzurubooruRequest {
self.request().with_offset(offset)
}
/// The same as [with_offset](SzurubooruClient::with_offset), but accepts an Option type instead
pub fn with_optional_offset(&self, offset: Option<u32>) -> SzurubooruRequest {
self.request().with_optional_offset(offset)
}
}
#[derive(Debug)]
/// A type that represents a single Szurubooru request.
pub struct SzurubooruRequest<'a> {
fields: Option<Vec<&'a str>>,
limit: Option<u32>,
offset: Option<u32>,
/// The currently selected fields to return (if applicable)
pub fields: Option<Vec<String>>,
/// The maximum number of resources to return (if supported by the API endpoint)
pub limit: Option<u32>,
/// The number of resource to skip before returning any results
/// (if supported by the API endpoint)
pub offset: Option<u32>,
client: &'a SzurubooruClient,
}
@ -234,32 +253,40 @@ impl<'a> SzurubooruRequest<'a> {
/// Select which fields to return from the query.
/// The Szurubooru API supports selecting a subset of fields for a given resource.
/// Most resource (models)[szurubooru_client::models] have [Option] fields because of that.
/// Most resource [models](crate::models) have [Option] fields because of that.
/// The default is to return all fields for a given resource.
/// See [here](https://github.com/rr-/szurubooru/blob/master/doc/API.md#field-selecting) for
/// more details
///
/// For example, to select only the `version`, `id` and `content_url` fields of a
/// (PostResource)[szurubooru_client::models::PostResource]
/// [PostResource]
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
/// # async {
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// let new_request = client.request().with_fields(vec!["version", "id", "content_url"]);
/// let new_request = client.request().with_fields(vec!["version".to_string(), "id".to_string(), "content_url".to_string()]);
/// # };
/// # ()
/// ```
pub fn with_fields(mut self, fields: Vec<&'a str>) -> Self {
pub fn with_fields(mut self, fields: Vec<String>) -> Self {
self.fields = Some(fields);
self
}
/// The same as [with_fields](SzurubooruRequest::with_fields), but accepts an Option type instead
pub fn with_optional_fields(self, val: Option<Vec<String>>) -> Self {
match val {
Some(f) => self.with_fields(f),
None => self,
}
}
/// Limit the number of returned results
/// The Szurubooru API supports limiting the number of resources returned for Paginated
/// API endpoints.
///
/// For example, to limit the number of pools returned by (list_pools)[SzurubooruRequest::list_pools]
/// For example, to limit the number of pools returned by [list_pools](SzurubooruRequest::list_pools)
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
@ -277,12 +304,20 @@ impl<'a> SzurubooruRequest<'a> {
self
}
/// The same as [with_limit](SzurubooruRequest::with_limit), but accepts an Option type instead
pub fn with_optional_limit(self, val: Option<u32>) -> Self {
match val {
Some(f) => self.with_limit(f),
None => self,
}
}
/// Skip a certain number of records
/// The Szurubooru API supports offsetting the results returned from Paginated API
/// endpoints. Use this offset in combination with the limit to page through
/// large result sets.
///
/// For example, to offset the list of pools returned by (list_pools)[SzurubooruRequest::list_pools]
/// For example, to offset the list of pools returned by [list_pools](SzurubooruRequest::list_pools)
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
@ -300,6 +335,14 @@ impl<'a> SzurubooruRequest<'a> {
self
}
/// The same as [with_offset](SzurubooruRequest::with_offset), but accepts an Option type instead
pub fn with_optional_offset(self, val: Option<u32>) -> Self {
match val {
Some(f) => self.with_offset(f),
None => self,
}
}
#[doc(hidden)]
fn prep_request<T>(
&self,
@ -381,12 +424,14 @@ impl<'a> SzurubooruRequest<'a> {
async fn handle_response(&self, response: Response) -> SzurubooruResult<Response> {
if response.status().is_client_error() || response.status().is_server_error() {
let status = response.status();
let resp_json = response
.text()
.await
.map_err(SzurubooruClientError::RequestError)?;
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))
} else {
Ok(response)
@ -406,13 +451,12 @@ 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()
.await
.map_err(SzurubooruClientError::RequestError)?;
serde_json::from_str::<SzuruEither<T, SzurubooruServerError>>(&response_text)
.map_err(|e| SzurubooruClientError::ResponseParsingError(e, response_text))?
.into_result()
@ -447,7 +491,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Updates an existing tag category using specified parameters. Name must match
/// `tag_category_name_regex` from server's configuration. All fields except
/// [version](models::TagCategoryResource::version) are optional - update concerns only provided fields.
/// [version](crate::models::TagCategoryResource::version) are optional - update concerns only provided fields.
pub async fn update_tag_category<T>(
&self,
name: T,
@ -495,8 +539,9 @@ impl<'a> SzurubooruRequest<'a> {
}
/// Searches for tags.
/// See the (named tokens)[tokens::TagNamedToken] and (sort tokens)[tokens::TagSortToken] for
/// all possible query tokens, or use (QueryToken)[tokens::QueryToken] for a custom token
/// See the [named tokens](crate::tokens::TagNamedToken) and
/// [sort tokens](crate::tokens::TagSortToken) for all possible query tokens, or use
/// [QueryToken] for a custom token
pub async fn list_tags(
&self,
query: Option<&Vec<QueryToken>>,
@ -507,7 +552,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Creates a new 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 the `name` field within (TagCategoryResource)[models::TagCategoryResource] resource.
/// as the `name` field within [TagCategoryResource] resource.
/// Suggestions and implications are optional. If specified implied tags or suggested tags do
/// not exist yet, they will be automatically created. Tags created automatically have no
/// implications, no suggestions, one name and their category is set to the first tag category
@ -519,7 +564,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Updates an existing 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 the `name` field within (TagCategoryResource)[models::TagCategoryResource] resource.
/// as the `name` field within [TagCategoryResource] resource.
/// Suggestions and implications are optional. If specified implied tags or suggested tags do
/// not exist yet, they will be automatically created. Tags created automatically have no
/// implications, no suggestions, one name and their category is set to the first tag category
@ -562,13 +607,13 @@ impl<'a> SzurubooruRequest<'a> {
/// 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.
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))
.await
}
/// Lists siblings of given tag, e.g. tags that were used in the same posts as the given tag.
/// The (occurrences)[models::TagSibling::occurrences] field signifies how many times a given
/// The [occurrences](crate::models::TagSibling::occurrences) field signifies how many times a given
/// sibling appears with given tag. Results are sorted by occurrences count and the list is
/// truncated to the first 50 elements. Doesn't use paging.
pub async fn get_tag_siblings<T>(
@ -584,9 +629,8 @@ impl<'a> SzurubooruRequest<'a> {
}
/// Searches for posts.
/// See (PostNamedToken)[tokens::PostNamedToken], (PostSortToken)[tokens::PostSortToken] and
/// (PostSpecialToken)[tokens::PostSpecialToken] for valid tokens to use with this method, or
/// use (QueryToken)[tokens::QueryToken] to construct a custom token
/// See [PostNamedToken], [PostSortToken] and [PostSpecialToken] for valid tokens to use with
/// this method, or use [QueryToken] to construct a custom token
pub async fn list_posts(
&self,
query: Option<&Vec<QueryToken>>,
@ -602,6 +646,11 @@ impl<'a> SzurubooruRequest<'a> {
method: Method,
cupost: &CreateUpdatePost,
) -> 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
}
@ -609,7 +658,7 @@ impl<'a> SzurubooruRequest<'a> {
/// the image.
/// If specified tags do not exist yet, they will be automatically created. Tags created
/// automatically have no implications, no suggestions, one name and their category is set to
/// the first tag category found. (safety)[models::CreateUpdatePost::safety] must be any of
/// the first tag category found. [safety](crate::models::CreateUpdatePost::safety) must be any of
/// `safe`, `sketchy` or `unsafe`.
/// Relations must contain valid post IDs. If `flag` is omitted, they will be defined by
/// default (`"loop"` will be set for all video posts, and `"sound"` will be auto-detected).
@ -627,7 +676,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Update an existing post
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
/// [CreateUpdatePost]
pub async fn update_post(
&self,
post_id: u32,
@ -639,6 +688,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]
pub async fn update_post_from_url(
&self,
post_id: u32,
update_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
assert!(update_post.content_url.is_some());
let path = format!("/api/post/{post_id}");
self.create_update_post_from_url(&path, Method::PUT, update_post)
.await
.map(|pr| self.propagate_urls(pr))
}
// Create function to upload by byte array in the future
fn part_from_file(&self, file: &mut File) -> SzurubooruResult<Part> {
let mut bytes = vec![];
file.read_to_end(&mut bytes)
@ -686,7 +752,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Create a new post from a file handle
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
/// [CreateUpdatePost]
pub async fn create_post_from_file<T>(
&self,
file: &mut File,
@ -711,7 +777,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Create a new post from a file path
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
/// [CreateUpdatePost]
pub async fn create_post_from_file_path(
&self,
file_path: impl AsRef<Path>,
@ -731,7 +797,7 @@ impl<'a> SzurubooruRequest<'a> {
}
/// Create a post from a token previously generated by
/// (upload_temporary_file_from_path)[SzurubooruRequest::upload_temporary_file_from_path]
/// [upload_temporary_file_from_path](SzurubooruRequest::upload_temporary_file_from_path)
pub async fn create_post_from_token(
&self,
new_post: &CreateUpdatePost,
@ -752,7 +818,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Update an existing post from an open File handle
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
/// [CreateUpdatePost]
pub async fn update_post_from_file(
&self,
post_id: u32,
@ -776,7 +842,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Update an existing post from a file path
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
/// [CreateUpdatePost]
pub async fn update_post_from_file_path(
&self,
post_id: u32,
@ -825,6 +891,27 @@ impl<'a> SzurubooruRequest<'a> {
.map(|pr| self.propagate_urls(pr))
}
/// Update a post from a token previously generated by
/// [upload_temporary_file_from_path](SzurubooruRequest::upload_temporary_file_from_path)
pub async fn update_post_from_token(
&self,
post_id: u32,
update_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
assert!(update_post.content_token.is_some());
let url = format!("/api/post/{post_id}");
self.create_update_post_from_file(
None,
None,
None::<String>,
&url,
Method::PUT,
update_post,
)
.await
.map(|pr| self.propagate_urls(pr))
}
async fn get_post_content(
&self,
post_id: u32,
@ -874,7 +961,7 @@ impl<'a> SzurubooruRequest<'a> {
.map(|cr| cr.bytes_stream())
}
///Fetches the given post ID's image as a (Bytes)[bytes::Bytes] struct
///Fetches the given post ID's image as a [Bytes](bytes::Bytes) struct
pub async fn get_image_bytes(&self, post_id: u32) -> SzurubooruResult<bytes::Bytes> {
let content_response = self.get_post_content(post_id, false).await?;
@ -884,7 +971,7 @@ impl<'a> SzurubooruRequest<'a> {
.map_err(SzurubooruClientError::RequestError)
}
///Fetches the given post ID's thumbnail as a (Bytes)[bytes::Bytes] struct
///Fetches the given post ID's thumbnail as a [Bytes](bytes::Bytes) struct
pub async fn get_thumbnail_bytes(&self, post_id: u32) -> SzurubooruResult<bytes::Bytes> {
let content_response = self.get_post_content(post_id, true).await?;
@ -934,7 +1021,12 @@ impl<'a> SzurubooruRequest<'a> {
path: impl AsRef<Path>,
) -> SzurubooruResult<()> {
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
}
@ -989,32 +1081,34 @@ 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(
pub async fn post_for_file(
&self,
mut file: &mut File,
) -> SzurubooruResult<PagedSearchResult<PostResource>> {
) -> SzurubooruResult<Option<PostResource>> {
let mut hasher = Sha1::new();
std::io::copy(&mut file, &mut hasher).map_err(SzurubooruClientError::IOError)?;
let hash = hasher.finalize();
let hex_string = hex::encode(hash);
let qt = QueryToken::token(PostNamedToken::ContentChecksum, hex_string);
self.list_posts(Some(&vec![qt]))
let psr = self
.list_posts(Some(&vec![qt]))
.await
.map(|psr| self.propagate_urls(psr))
.map(|psr| self.propagate_urls(psr))?;
Ok(psr.results.first().cloned())
}
/// 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,
file_path: impl AsRef<Path>,
) -> SzurubooruResult<PagedSearchResult<PostResource>> {
) -> SzurubooruResult<Option<PostResource>> {
let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?;
self.posts_for_file(&mut file)
.await
.map(|psr| self.propagate_urls(psr))
self.post_for_file(&mut file).await
}
/// Retrieves information about an existing post.
@ -1043,7 +1137,7 @@ impl<'a> SzurubooruRequest<'a> {
///
/// Removes source post and merges all of its tags, relations, scores, favorites and comments to
/// the target post. If [MergePost::replace_content] is set to `true`, content of the target post
/// the target post. If [MergePost::replace_post_content] is set to `true`, content of the target post
/// is replaced using the content of the source post; otherwise it remains unchanged. Source
/// post properties such as its safety, source, whether to loop the video and other scalar
/// values do not get transferred and are discarded.
@ -1056,6 +1150,11 @@ impl<'a> SzurubooruRequest<'a> {
/// 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> {
if !(-1..=1).contains(&score) {
return Err(SzurubooruClientError::ValidationError(
"Score must be -1, 0 or 1".to_string(),
));
}
let rating_obj = RateResource { score };
let path = format!("/api/post/{post_id}/score");
self.do_request(Method::PUT, &path, None, Some(&rating_obj))
@ -1080,9 +1179,9 @@ impl<'a> SzurubooruRequest<'a> {
}
/// Retrieves the post that is currently featured on the main page in web client. If no post is
/// featured, <post> is [Option::None]. Note that this method exists mostly for compatibility
/// with setting featured post - most of the time, you'd want to use query global info which
/// contains more information.
/// featured, the result will be [Option::None]. Note that this method exists mostly for
/// compatibility with setting featured post - most of the time, you'd want to use query global
/// info which contains more information.
pub async fn get_featured_post(&self) -> SzurubooruResult<Option<PostResource>> {
self.do_request(Method::GET, "/api/featured-post", None, None::<&String>)
.await
@ -1118,7 +1217,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Updates an existing tag category using specified parameters. Name must match
/// `tag_category_name_regex` from server's configuration. All fields except the
/// [version](models::CreateUpdatePoolCategory::version) field are optional - update concerns
/// [version](crate::models::CreateUpdatePoolCategory::version) field are optional - update concerns
/// only the provided fields.
pub async fn update_pool_category<T>(
&self,
@ -1177,7 +1276,7 @@ impl<'a> SzurubooruRequest<'a> {
}
/// Searches for pools.
/// Anonymous tokens are the same as the [name](tokens::PoolNamedToken::Name) token
/// Anonymous tokens are the same as the [name](crate::tokens::PoolNamedToken::Name) token
pub async fn list_pools(
&self,
query: Option<&Vec<QueryToken>>,
@ -1189,8 +1288,8 @@ impl<'a> SzurubooruRequest<'a> {
/// Creates a new pool using specified parameters. Names, suggestions and implications must
/// match `pool_name_regex` from server's configuration. Category must exist and is the same as
/// [name](models::PoolCategoryResource::name) field.
/// [posts](models::CreateUpdatePool::posts) is an optional list of integer post IDs. If the
/// [name](crate::models::PoolCategoryResource::name) field.
/// [posts](crate::models::CreateUpdatePool::posts) is an optional list of integer post IDs. If the
/// specified posts do not exist, an error will be thrown.
pub async fn create_pool(
&self,
@ -1201,14 +1300,14 @@ impl<'a> SzurubooruRequest<'a> {
.map(|r| self.propagate_urls(r))
}
/// Updates an existing pool using specified parameters. [name](models::CreateUpdatePool::name),
/// Updates an existing pool using specified parameters. [names](crate::models::CreateUpdatePool::names),
/// must match `pool_name_regex` from server's configuration.
/// [category](models::CreateUpdatePool::category) must exist and is the same as
/// [name](models::PoolCategoryResource::name) field. [posts](models::CreateUpdatePool::posts)
/// [category](crate::models::CreateUpdatePool::category) must exist and is the same as
/// [name](crate::models::PoolCategoryResource::name) field. [posts](crate::models::CreateUpdatePool::posts)
/// is an optional list of integer post IDs. If the specified posts do not exist yet, an error
/// will be thrown. The full list of post IDs must be provided if they are being updated, and
/// the previous list of posts will be replaced with the new one. All fields except
/// [version](models::CreateUpdatePool::version) are optional - update concerns only provided
/// [version](crate::models::CreateUpdatePool::version) are optional - update concerns only provided
/// fields.
pub async fn update_pool(
&self,
@ -1248,7 +1347,7 @@ impl<'a> SzurubooruRequest<'a> {
}
/// Searches for comments.
/// Anonymous tokens are the same as the [text](tokens::CommentNamedToken::text) token
/// Anonymous tokens are the same as the [text](crate::tokens::CommentNamedToken::Text) token
pub async fn list_comments(
&self,
query: Option<&Vec<QueryToken>>,
@ -1299,6 +1398,11 @@ impl<'a> SzurubooruRequest<'a> {
comment_id: u32,
score: i8,
) -> SzurubooruResult<CommentResource> {
if !(-1..=1).contains(&score) {
return Err(SzurubooruClientError::ValidationError(
"Score must be -1, 0 or 1".to_string(),
));
}
let path = format!("/api/comment/{comment_id}/score");
let rating = RateResource { score };
self.do_request(Method::PUT, &path, None, Some(&rating))
@ -1306,9 +1410,8 @@ impl<'a> SzurubooruRequest<'a> {
}
/// Searches for users
/// Anonymous tokens are the same as the [name](tokens::UserNamedToken)
/// See [UserNamedToken](tokens::UserNamedToken) and [UserSortToken](tokens::UserSortToken)
/// for type-safe tokens
/// Anonymous tokens are the same as the [name](crate::tokens::UserNamedToken::Name) token
/// See [UserNamedToken] and [UserSortToken] for type-safe tokens
pub async fn list_users(
&self,
query: Option<&Vec<QueryToken>>,
@ -1351,7 +1454,7 @@ impl<'a> SzurubooruRequest<'a> {
/// Creates a new user using specified parameters. Names and passwords must match
/// `user_name_regex` and `password_regex` from server's configuration, respectively.
/// Email address, rank and avatar fields are optional. Avatar style can be either
/// [gravatar](models::UserAvatarStyle::Gravatar) or [manual](models::UserAvatarStyle::Manual).
/// [gravatar](crate::models::UserAvatarStyle::Gravatar) or [manual](crate::models::UserAvatarStyle::Manual).
/// `manual` avatar style requires client to pass also the `avatar` file.
/// If the rank is empty and the user happens to be the first user ever created,
/// become an administrator, whereas subsequent users will be given the rank indicated by
@ -1362,7 +1465,7 @@ impl<'a> SzurubooruRequest<'a> {
.map(|r| self.propagate_urls(r))
}
/// Create a [UserResource](models::UserResource) with the included Avatar file
/// Create a [UserResource] with the included Avatar file
/// See [create_user](SzurubooruRequest::create_user) for other applicable fields and
/// restrictions
pub async fn create_user_with_avatar_file(
@ -1382,7 +1485,7 @@ impl<'a> SzurubooruRequest<'a> {
.map(|r| self.propagate_urls(r))
}
/// Create a [UserResource](models::UserResource) with the included Avatar file path
/// Create a [UserResource] with the included Avatar file path
/// See [create_user](SzurubooruRequest::create_user) for other applicable fields and
/// restrictions
pub async fn create_user_with_avatar_path(
@ -1406,9 +1509,9 @@ impl<'a> SzurubooruRequest<'a> {
/// Updates user using specified parameters. Names and passwords must match
/// `user_name_regex` and `password_regex` from server's configuration, respectively.
/// Email address, rank and avatar fields are optional. Avatar style can be either
/// [gravatar](models::UserAvatarStyle::Gravatar) or [manual](models::UserAvatarStyle::Manual).
/// [gravatar](crate::models::UserAvatarStyle::Gravatar) or [manual](crate::models::UserAvatarStyle::Manual).
/// `manual` avatar style requires client to pass also the `avatar` file.
/// All fields except the [version](models::CreateUpdateUser::version) are optional
/// All fields except the [version](crate::models::CreateUpdateUser::version) are optional
/// - update concerns only provided fields.
pub async fn update_user<T>(
&self,
@ -1424,7 +1527,7 @@ impl<'a> SzurubooruRequest<'a> {
.map(|r| self.propagate_urls(r))
}
/// Update a [UserResource](models::UserResource) with the included Avatar file
/// Update a [UserResource] with the included Avatar file
/// See [update_user](SzurubooruRequest::update_user) for other applicable fields and
/// restrictions
pub async fn update_user_with_avatar_file<T>(
@ -1449,7 +1552,7 @@ impl<'a> SzurubooruRequest<'a> {
.map(|r| self.propagate_urls(r))
}
/// Update a [UserResource](models::UserResource) with the included Avatar file path
/// Update a [UserResource] with the included Avatar file path
/// See [update_user](SzurubooruRequest::update_user) for other applicable fields and
/// restrictions
pub async fn update_user_with_avatar_path<T>(
@ -1516,20 +1619,20 @@ impl<'a> SzurubooruRequest<'a> {
/// instead of a password.
pub async fn create_user_token<T>(
&self,
name: T,
user_name: T,
create_token: &CreateUpdateUserAuthToken,
) -> SzurubooruResult<UserAuthTokenResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user-token/{name}");
let path = format!("/api/user-token/{user_name}");
self.do_request(Method::POST, &path, None, Some(create_token))
.await
.map(|r| self.propagate_urls(r))
}
/// Updates an existing user token using specified parameters. All fields except the
/// [version](models::CreateUpdateUserAuthToken::version) are optional - update concerns only
/// [version](crate::models::CreateUpdateUserAuthToken::version) are optional - update concerns only
/// provided fields.
pub async fn update_user_token<T>(
&self,
@ -1547,7 +1650,7 @@ impl<'a> SzurubooruRequest<'a> {
}
/// Deletes an existing user token using specified parameters. All fields except the
/// [version](models::CreateUpdateUserAuthToken::version) are optional - update concerns only
/// [version](crate::models::CreateUpdateUserAuthToken::version) are optional - update concerns only
/// provided fields.
pub async fn delete_user_token<T>(
&self,
@ -1600,7 +1703,7 @@ impl<'a> SzurubooruRequest<'a> {
}
/// Lists recent resource snapshots.
/// See [SnapshotNamedToken](tokens::SnapshotNamedToken) for query tokens.
/// See [SnapshotNamedToken] for query tokens.
/// There are no sort tokens. The snapshots are always sorted by creation time.
pub async fn list_snapshots(
&self,
@ -1611,11 +1714,11 @@ impl<'a> SzurubooruRequest<'a> {
.map(|r| self.propagate_urls(r))
}
/// Retrieves simple statistics. [featured_post](models::GlobalInfo::featured_post) is
/// [None](Option::None) if there is no featured post yet.
/// [server_time](models::GlobalInfo::server_time) is pretty much the same as the Date HTTP
/// Retrieves simple statistics. [featured_post](crate::models::GlobalInfo::featured_post) is
/// [None] if there is no featured post yet.
/// [server_time](crate::models::GlobalInfo::server_time) is pretty much the same as the Date HTTP
/// field, only formatted in a manner consistent with other dates. Values in config key are
/// taken directly from the server config, with the exception of privilege array keys being
/// taken directly from the server config, except for the privilege array keys being
/// converted to lower camel case to match the API convention.
pub async fn get_global_info(&self) -> SzurubooruResult<GlobalInfo> {
self.do_request(Method::GET, "/api/info", None, None::<&String>)

View file

@ -3,7 +3,13 @@
use crate::models::SzuruEither;
use base64::EncodeSliceError;
use derive_builder::UninitializedFieldError;
#[cfg(feature = "python")]
use pyo3::{create_exception, exceptions::PyException, prelude::*};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use strum_macros::AsRefStr;
use thiserror::Error;
use url::ParseError as UParseError;
@ -14,7 +20,7 @@ pub trait IntoClientResult<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
/// Szurubooru
pub enum SzurubooruClientError {
@ -35,6 +41,9 @@ pub enum SzurubooruClientError {
/// Error occurred pas part of the request to the server
#[error("Request error {0}")]
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("Response Parsing error: {0}: {1}")]
ResponseParsingError(
@ -47,6 +56,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("Validation error: {0}")]
ValidationError(String),
/// Error occurred when reading a file
#[error("IO Error: {0}")]
IOError(#[source] std::io::Error),
@ -61,6 +73,27 @@ impl From<SzurubooruServerError> for SzurubooruClientError {
}
}
impl From<UninitializedFieldError> for SzurubooruClientError {
fn from(value: UninitializedFieldError) -> Self {
SzurubooruClientError::ValidationError(value.to_string())
}
}
#[cfg(feature = "python")]
create_exception!(
szurubooru_client,
SzuruClientError,
PyException,
"An exception that contains two pieces of information: The error kind and error details"
);
#[cfg(feature = "python")]
impl std::convert::From<SzurubooruClientError> for PyErr {
fn from(value: SzurubooruClientError) -> Self {
SzuruClientError::new_err((value.as_ref().to_string(), value.to_string()))
}
}
/// Type used to represent success or a failure of some kind
pub type SzurubooruResult<T> = Result<T, SzurubooruClientError>;

View file

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

View file

@ -5,12 +5,18 @@
//! See [here](https://github.com/rr-/szurubooru/blob/master/doc/API.md#field-selecting) for
//! more information.
use crate::errors::SzurubooruClientError;
use chrono::{DateTime, Utc};
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use strum_macros::AsRefStr;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use serde_pyobject::to_pyobject;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
/// Enum used to represent something that's either `Left` or `Right`
@ -39,16 +45,16 @@ impl<T: WithBaseURL> WithBaseURL for UnpagedSearchResult<T> {
#[derive(Debug, Serialize, Deserialize)]
/// A result of search operation that involves paging
///
/// Use (offset)[crate::SzurubooruRequest::offset] and (limit)[crate::SzurubooruRequest::limit]
/// Use [offset](crate::SzurubooruRequest::with_offset) and [limit](crate::SzurubooruRequest::with_limit)
/// to fetch the next page
pub struct PagedSearchResult<T> {
/// The original query for the request
pub query: String,
/// The number of [T] to skip forward
/// The number of `T` to skip forward
pub offset: u32,
/// The maximum number of [T] to return
/// The maximum number of `T` to return
pub limit: u32,
/// The total number of [T] that match the [query](PagedSearchResult::query)
/// The total number of `T` that match the [query](PagedSearchResult::query)
pub total: u32,
/// The results themselves
pub results: Vec<T>,
@ -82,6 +88,10 @@ impl<T: WithBaseURL> WithBaseURL for Vec<T> {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, eq, module = "szurubooru_client.models")
)]
/// A [tag resource](TagResource) stripped down to `names`, `category` and `usages` fields.
pub struct MicroTagResource {
/// The tag names and aliases
@ -92,6 +102,16 @@ pub struct MicroTagResource {
pub usages: u32,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl MicroTagResource {
/// Function that generates the representation string for this resource
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
@ -116,8 +136,12 @@ pub struct ResourceVersion {
pub version: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
/// A single tag. Tags are used to let users search for posts.
pub struct TagResource {
/// resource version. See [versioning](ResourceVersion)
@ -144,9 +168,19 @@ pub struct TagResource {
pub description: Option<String>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl TagResource {
/// Generates a representative string of this resource
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 <tag-category> resource. Suggestions and implications are optional. If specified
/// field within [TagCategoryResource] resource. Suggestions and implications are optional. If specified
/// implied tags or suggested tags do not exist yet, they will be automatically created. Tags
/// created automatically have no implications, no suggestions, one name and their category is set
/// to the first tag category found. If there are no tag categories established yet, an error
@ -161,7 +195,8 @@ pub struct TagResource {
/// .expect("A new tag");
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
#[builder(setter(strip_option))]
//#[builder(pattern="owned")]
#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))]
pub struct CreateUpdateTag {
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
@ -189,7 +224,11 @@ pub struct CreateUpdateTag {
pub suggestions: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
/// A single tag category. The primary purpose of tag categories is to distinguish certain tag
/// types (such as characters, media type etc.), which improves user experience.
pub struct TagCategoryResource {
@ -207,8 +246,19 @@ pub struct TagCategoryResource {
pub default: Option<bool>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl TagCategoryResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Builder)]
#[builder(setter(strip_option))]
#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))]
/// Used for creating or updating a Tag Category
pub struct CreateUpdateTagCategory {
/// Resource version. See [versioning](ResourceVersion)
@ -230,22 +280,29 @@ pub struct CreateUpdateTagCategory {
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))]
#[serde(rename_all = "camelCase")]
#[builder(setter(into))]
/// 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.
pub struct MergeTags {
/// Version of the tag to remove
pub remove_version: u32,
#[serde(rename = "removeVersion")]
pub remove_tag_version: u32,
/// The name of the tag to remove
pub remove: String,
#[serde(rename = "remove")]
pub remove_tag: String,
/// The version of the tag to merge TO
pub merge_to_version: u32,
/// The name of the tag to merge TO
pub merge_to: String,
#[serde(rename = "mergeTo")]
pub merge_to_tag: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
/// Lists siblings of given tag, e.g. tags that were used in the same posts as the given tag
pub struct TagSibling {
/// The related tag
@ -254,7 +311,21 @@ pub struct TagSibling {
pub occurrences: u32,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl TagSibling {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// The type of post
pub enum PostType {
@ -277,6 +348,10 @@ pub enum PostType {
}
#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// How SFW/NSFW the post is
pub enum PostSafety {
@ -284,13 +359,17 @@ pub enum PostSafety {
Safe,
/// Post is possibly NSFW
Sketchy,
/// Alias of (Sketchy)[PostSafety::Sketchy]
/// Alias of [Sketchy](PostSafety::Sketchy)
Questionable,
/// Post is NSFW
Unsafe,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// A post resource stripped down to `id` and `thumbnailUrl` fields.
pub struct MicroPostResource {
@ -300,6 +379,16 @@ pub struct MicroPostResource {
pub thumbnail_url: String,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl MicroPostResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
impl WithBaseURL for MicroPostResource {
fn with_base_url(self, url: &str) -> Self {
if !self.thumbnail_url.contains(url) {
@ -320,6 +409,10 @@ pub(crate) struct PostId {
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// A post resource
pub struct PostResource {
@ -394,6 +487,16 @@ pub struct PostResource {
pub pools: Option<Vec<PoolResource>>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl PostResource {
/// Generates a representative string of this resource
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| {
@ -429,7 +532,7 @@ impl WithBaseURL for PostResource {
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
#[builder(setter(strip_option))]
#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))]
#[serde(rename_all = "camelCase")]
/// A `struct` used to create or update a post. For updating purposes
/// the [version](CreateUpdatePost::version) field is required
@ -445,6 +548,7 @@ pub struct CreateUpdatePost {
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
/// Required field, represents the SFW/NSFW state of a post
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub safety: Option<PostSafety>,
/// The origin of the post's content
@ -472,6 +576,10 @@ pub struct CreateUpdatePost {
/// [upload_temporary_file](crate::SzurubooruRequest::upload_temporary_file)
#[builder(default)]
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)]
@ -483,7 +591,7 @@ pub struct TemporaryFileUpload {
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
#[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
@ -492,15 +600,19 @@ pub struct TemporaryFileUpload {
/// and are discarded.
pub struct MergePost {
/// The version of the post to remove
pub remove_version: u32,
#[serde(rename = "removeVersion")]
pub remove_post_version: u32,
/// The ID of the post to remove
pub remove: u32,
#[serde(rename = "remove")]
pub remove_post: u32,
/// The version of the post to merge TO
pub merge_to_version: u32,
/// The post ID of the post to merge TO
pub merge_to: u32,
#[serde(rename = "mergeTo")]
pub merge_to_post: u32,
/// Whether to replace the content
pub replace_content: bool,
#[serde(rename = "replaceContent")]
pub replace_post_content: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -510,6 +622,10 @@ pub struct RateResource {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// A text annotation rendered on top of the post
pub struct NoteResource {
@ -522,7 +638,21 @@ pub struct NoteResource {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl NoteResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// The Rank of a given User
pub enum UserRank {
@ -538,7 +668,11 @@ pub enum UserRank {
Administrator,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// The kind of User Avatar
pub enum UserAvatarStyle {
@ -548,49 +682,175 @@ pub enum UserAvatarStyle {
Manual,
}
// Because pyo3 get_all doesn't let you exclude fields we have to define the fields twice
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(all(feature = "python"), pyclass(module = "szurubooru_client.models"))]
#[serde(rename_all = "camelCase")]
/// A single user
pub struct UserResource {
/// Resource version. See [versioning](ResourceVersion)
#[cfg(feature = "python")]
#[pyo3(get)]
pub version: Option<u32>,
/// Resource version. See [versioning](ResourceVersion)
#[cfg(not(feature = "python"))]
pub version: Option<u32>,
/// The user's username
#[cfg(feature = "python")]
#[pyo3(get)]
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,
/// 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)
pub email: Option<SzuruEither<String, bool>>,
/// The user rank, which effectively affects their privileges
#[cfg(feature = "python")]
#[pyo3(get)]
pub rank: Option<UserRank>,
#[serde(rename = "last-login-time")]
/// The user rank, which effectively affects their privileges
#[cfg(not(feature = "python"))]
pub rank: Option<UserRank>,
/// The last login time
#[cfg(feature = "python")]
#[pyo3(get)]
#[serde(rename = "last-login-time")]
pub last_login_time: Option<DateTime<Utc>>,
#[serde(rename = "creation-time")]
/// The last login time
#[cfg(not(feature = "python"))]
#[serde(rename = "last-login-time")]
pub last_login_time: Option<DateTime<Utc>>,
/// The user registration time
#[serde(rename = "creation-time")]
#[cfg(feature = "python")]
#[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
#[cfg(feature = "python")]
#[pyo3(get)]
#[serde(rename = "comment-count")]
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")]
#[pyo3(get)]
#[serde(rename = "uploaded-post-count")]
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
/// user. If it's unavailable, the server returns `false`
#[serde(rename = "liked-post-count")]
pub liked_post_count: Option<SzuruEither<u32, bool>>,
/// 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`.
#[serde(rename = "disliked-post-count")]
pub disliked_post_count: Option<SzuruEither<u32, bool>>,
/// Number of favorited posts
#[serde(rename = "favorite-post-count")]
pub favorite_post_count: Option<SzuruEither<u32, bool>>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl UserResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
#[getter]
#[pyo3(name = "email")]
/// Returns this resource's email field, if the current user has permission to see it
pub fn email_py(&self) -> PyResult<Option<String>> {
match &self.email {
None => Ok(None),
Some(SzuruEither::Left(s)) => Ok(Some(s.to_string())),
Some(SzuruEither::Right(_)) => Ok(None),
}
}
#[getter]
#[pyo3(name = "liked_post_count")]
/// Returns this resource's liked_post_count, if the current user has permission to see it
pub fn liked_post_count_py(&self) -> PyResult<Option<u32>> {
match &self.liked_post_count {
None => Ok(None),
Some(SzuruEither::Left(s)) => Ok(Some(*s)),
Some(SzuruEither::Right(_)) => Ok(None),
}
}
#[getter]
#[pyo3(name = "disliked_post_count")]
/// Returns this resource's disliked_post_count, if the current user has permission to see it
pub fn disliked_post_count_py(&self) -> PyResult<Option<u32>> {
match &self.disliked_post_count {
None => Ok(None),
Some(SzuruEither::Left(s)) => Ok(Some(*s)),
Some(SzuruEither::Right(_)) => Ok(None),
}
}
#[getter]
#[pyo3(name = "favorite_post_count")]
/// Returns this resource's favorite_post_count, if the current user has permission to see it
pub fn favorite_post_count_py(&self) -> PyResult<Option<u32>> {
match &self.favorite_post_count {
None => Ok(None),
Some(SzuruEither::Left(s)) => Ok(Some(*s)),
Some(SzuruEither::Right(_)) => Ok(None),
}
}
}
impl WithBaseURL for UserResource {
fn with_base_url(self, url: &str) -> Self {
let av_url = self.avatar_url.map(|au| {
@ -608,7 +868,7 @@ impl WithBaseURL for UserResource {
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Builder)]
#[builder(setter(strip_option))]
#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))]
#[serde(rename_all = "camelCase")]
/// `struct` used to create or update a user resource. The version field is only used when
/// updating an existing resource
@ -637,6 +897,10 @@ pub struct CreateUpdateUser {
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// A user resource stripped down to `name` and `avatarUrl` fields
pub struct MicroUserResource {
@ -646,6 +910,16 @@ pub struct MicroUserResource {
pub avatar_url: String,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl MicroUserResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
impl WithBaseURL for MicroUserResource {
fn with_base_url(self, url: &str) -> Self {
if !self.avatar_url.contains(url) {
@ -660,6 +934,10 @@ impl WithBaseURL for MicroUserResource {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "kebab-case")]
/// A single user token
pub struct UserAuthTokenResource {
@ -683,6 +961,16 @@ pub struct UserAuthTokenResource {
pub last_usage_time: Option<DateTime<Utc>>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl UserAuthTokenResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
impl WithBaseURL for UserAuthTokenResource {
fn with_base_url(self, url: &str) -> Self {
Self {
@ -693,7 +981,7 @@ impl WithBaseURL for UserAuthTokenResource {
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
#[builder(setter(into, strip_option))]
#[builder(setter(into, strip_option), build_fn(error = "SzurubooruClientError"))]
#[serde(rename_all = "kebab-case")]
/// `struct` to create or update a UserAuthToken. `version` is only required when updating an
/// existing resource
@ -733,6 +1021,10 @@ pub struct TemporaryPassword {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// Simple server configuration
pub struct GlobalInfoConfig {
@ -757,6 +1049,10 @@ pub struct GlobalInfoConfig {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// Simple server statistics
pub struct GlobalInfo {
@ -776,7 +1072,21 @@ pub struct GlobalInfo {
pub config: GlobalInfoConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl GlobalInfo {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// A single pool category. The primary purpose of pool categories is to distinguish certain pool
/// types (such as series, relations etc.), which improves user experience.
@ -793,8 +1103,18 @@ pub struct PoolCategoryResource {
pub default: Option<bool>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl PoolCategoryResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
#[builder(setter(strip_option))]
#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))]
/// `struct` used for creating or updating a pool category. This type uses a Builder pattern like
/// so:
///
@ -823,6 +1143,10 @@ pub struct CreateUpdatePoolCategory {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// Type that represents a Pool resource
pub struct PoolResource {
@ -847,6 +1171,16 @@ pub struct PoolResource {
pub description: Option<String>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl PoolResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
impl WithBaseURL for PoolResource {
fn with_base_url(self, url: &str) -> Self {
PoolResource {
@ -857,7 +1191,7 @@ impl WithBaseURL for PoolResource {
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
#[builder(setter(strip_option))]
#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))]
#[serde(rename_all = "camelCase")]
/// This type is used when creating or updating a pool object. It uses the builder pattern like so:
///
@ -897,6 +1231,7 @@ pub struct CreateUpdatePool {
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
#[builder(build_fn(error = "SzurubooruClientError"))]
#[serde(rename_all = "camelCase")]
/// This type is used to specify which pools should be merged. Uses the builder pattern like so:
///
@ -904,25 +1239,32 @@ pub struct CreateUpdatePool {
/// use szurubooru_client::models::MergePoolBuilder;
/// // Merge pool ID 1 at version 1 to pool ID 3 at version 5
/// let merge_pool = MergePoolBuilder::default()
/// .remove_version(1)
/// .remove(1)
/// .remove_pool_version(1)
/// .remove_pool(1)
/// .merge_to_version(5)
/// .merge_to(3)
/// .merge_to_pool(3)
/// .build()
/// .unwrap();
/// ```
pub struct MergePool {
/// Version of the pool to remove. Must match the current Pool version
pub remove_version: u32,
#[serde(rename = "removeVersion")]
pub remove_pool_version: u32,
/// Pool ID to remove
pub remove: u32,
#[serde(rename = "remove")]
pub remove_pool: u32,
/// Version of the pool to merge TO
pub merge_to_version: u32,
/// Pool ID of the pool to merge TO
pub merge_to: u32,
#[serde(rename = "mergeTo")]
pub merge_to_pool: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// A micro resource representing a Pool. A subset of the fields of a [PoolResource].
pub struct MicroPoolResource {
@ -938,7 +1280,21 @@ pub struct MicroPoolResource {
pub description: Option<String>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl MicroPoolResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// A type representing a Comment on a post
pub struct CommentResource {
@ -962,8 +1318,18 @@ pub struct CommentResource {
pub own_score: Option<i32>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl CommentResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
#[builder(setter(strip_option))]
#[builder(setter(strip_option), build_fn(error = "SzurubooruClientError"))]
#[serde(rename_all = "camelCase")]
/// This type is used when creating or updating a comment. This type uses the builder pattern like
/// so:
@ -992,7 +1358,11 @@ pub struct CreateUpdateComment {
pub post_id: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// The kind of snapshot that has been recorded
pub enum SnapshotOperationType {
@ -1006,7 +1376,11 @@ pub enum SnapshotOperationType {
Merged,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// The kind of resource described by this snapshot
pub enum SnapshotResourceType {
@ -1024,7 +1398,11 @@ pub enum SnapshotResourceType {
PoolCategory,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase", untagged)]
/// Data for a resource that was created
#[allow(clippy::large_enum_variant)]
@ -1041,6 +1419,16 @@ pub enum SnapshotCreationDeletionData {
PoolCategory(PoolCategoryResource),
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl SnapshotCreationDeletionData {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
impl WithBaseURL for SnapshotCreationDeletionData {
fn with_base_url(self, url: &str) -> Self {
match self {
@ -1055,13 +1443,25 @@ impl WithBaseURL for SnapshotCreationDeletionData {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// Data for a modified resource
pub struct SnapshotModificationData {
/// The type of snapshot
#[cfg(feature = "python")]
#[serde(rename = "type")]
#[pyo3(get)]
/// The type of snapshot
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
/// kind.
///
@ -1070,7 +1470,28 @@ pub struct SnapshotModificationData {
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl SnapshotModificationData {
#[getter]
/// Get the value associated with this snapshot
pub fn get_value(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let obj = to_pyobject(py, &self.value).unwrap().unbind();
Ok(obj)
}
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, module = "szurubooru_client.models")
)]
#[serde(untagged)]
/// Type representing the data as part of a snapshot
#[allow(clippy::large_enum_variant)]
@ -1095,6 +1516,10 @@ impl WithBaseURL for SnapshotData {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// Overall type representing some sort of change to a resource
pub struct SnapshotResource {
@ -1113,6 +1538,16 @@ pub struct SnapshotResource {
pub time: Option<DateTime<Utc>>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl SnapshotResource {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
impl WithBaseURL for SnapshotResource {
fn with_base_url(self, url: &str) -> Self {
SnapshotResource {
@ -1124,6 +1559,10 @@ impl WithBaseURL for SnapshotResource {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// A result when searching for similar posts to a given image
pub struct ImageSearchSimilarPost {
@ -1133,6 +1572,16 @@ pub struct ImageSearchSimilarPost {
pub post: PostResource,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl ImageSearchSimilarPost {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
impl WithBaseURL for ImageSearchSimilarPost {
fn with_base_url(self, url: &str) -> Self {
Self {
@ -1143,6 +1592,10 @@ impl WithBaseURL for ImageSearchSimilarPost {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
#[serde(rename_all = "camelCase")]
/// A type to represent the result from an Image search request
pub struct ImageSearchResult {
@ -1154,6 +1607,16 @@ pub struct ImageSearchResult {
pub similar_posts: Vec<ImageSearchSimilarPost>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl ImageSearchResult {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
impl WithBaseURL for ImageSearchResult {
fn with_base_url(self, url: &str) -> Self {
Self {
@ -1164,6 +1627,10 @@ impl WithBaseURL for ImageSearchResult {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "python"),
pyclass(get_all, module = "szurubooru_client.models")
)]
/// A type that represents posts that are before or after an existing post
pub struct AroundPostResult {
/// A previous post, if it exists
@ -1172,6 +1639,16 @@ pub struct AroundPostResult {
next: Option<u32>,
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
#[doc(hidden)]
impl AroundPostResult {
/// Generates a representative string of this resource
fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
#[cfg(test)]
mod tests {
use crate::models::{GlobalInfo, GlobalInfoConfig, SnapshotResource, TagCategoryResource};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,54 @@
use crate::models::PagedSearchResult;
use pyo3::prelude::*;
use pyo3::types::PyList;
// rustfmt likes to break the Python docstrings
#[rustfmt::skip]
pub mod asynchronous;
#[rustfmt::skip]
pub mod synchronous;
#[derive(Debug)]
#[pyclass(name = "PagedResult", get_all, module = "szurubooru_client")]
/// A paged result generated by most of the ``list`` methods of the Szurubooru clients
pub struct PyPagedSearchResult {
/// The query string that was used to generate these results
pub query: String,
/// The offset for the request, how many resource to skip before returning the results
pub offset: u32,
/// The maximum number of results to return
pub limit: u32,
/// The total number of results generated by the query
pub total: u32,
/// The results themselves
pub results: Py<PyList>,
}
#[cfg_attr(all(feature = "python"), pymethods)]
impl PyPagedSearchResult {
fn __repr__(&self) -> String {
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 {
fn from(value: PagedSearchResult<T>) -> Self {
Python::with_gil(|py| {
let list =
PyList::new_bound(py, value.results.into_iter().map(|v| v.into_py(py))).unbind();
PyPagedSearchResult {
query: value.query,
offset: value.offset,
limit: value.limit,
total: value.total,
results: list,
}
})
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,8 @@
//! 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::*};
use std::fmt::Display;
use strum_macros::AsRefStr;
@ -22,7 +24,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(module = "szurubooru_client.tokens"))]
pub struct QueryToken {
/// The key for this token. For `foo:bar` this would be `foo`
pub key: String,
@ -116,7 +119,7 @@ impl QueryToken {
/// let liked_posts = QueryToken::special(PostSpecialToken::Liked);
/// client.request().list_posts(Some(&vec![liked_posts]));
/// ```
pub fn special(key: impl SpecialToken) -> Self {
pub fn special(key: impl AsRef<str>) -> Self {
QueryToken::anonymous(key)
}
@ -139,6 +142,210 @@ impl QueryToken {
}
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pyfunction)]
/// Generates a named token. Named tokens are used to filter resources returned by the API.
/// An example of this would be returning posts with a certain safety value.
///
/// This function will accept any string, but if you want to be more sure about your code you
/// can use any of the :ref:`Named token <named-tokens-enums>` types listed below.
/// See the example below.
///
/// :param key: String or Named field
/// :param value: The string or int value to use to filter by
/// :returns: The named query token
/// :rtype: QueryToken
///
/// -----
/// Usage
/// -----
/// This lists all posts that are marked as 'safe'.
///
/// ```python
/// client.list_posts(query=[named_token(PostNamedToken.Safety, 'safe')])
/// ```
///
/// Which is equivalent to the possibly more error-prone:
///
/// ```python
/// client.list_posts(query=[named_token("safety", 'safe')])
/// ```
pub fn named_token(key: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult<QueryToken> {
QueryToken::token_py(key, value)
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pyfunction)]
/// Generates a sorting token. Sorting tokens are used to sort resources returned by the API.
/// An example of this would be returning posts by score descending.
///
/// This function will accept any string, but if you want to be more sure about your code you
/// can use any of the :ref:`Sort token <sort-tokens-enums>` types listed below. See the example below.
///
/// :param key: String or Sort field name
/// :returns: The sort query token
/// :rtype: QueryToken
///
/// -----
/// Usage
/// -----
/// This lists posts by score descending:
///
/// ```python
/// client.list_posts(query=[-sort_token(PostSortToken.Score)])
/// ```
///
/// Which is equivalent to the possibly more error-prone:
///
/// ```python
/// client.list_posts(query=[-sort_token("score")])
/// ```
pub fn sort_token(key: &Bound<'_, PyAny>) -> PyResult<QueryToken> {
QueryToken::sort_py(key)
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pyfunction)]
/// Generates an anonymous token. Anonymous tokens are tokens that don't require some
/// sort of prefix to be used in a search. What the anonymous tag corresponds to depends
/// on the type of resource you're listing. For example, when listing posts the anonymous
/// tags correspond to post tags
///
///
/// :param str key: The anonymous token to create
/// :returns: The anonymous query token
/// :rtype: QueryToken
///
/// -----
/// Usage
/// -----
///
/// ```python
/// client.list_posts(fields=[anonymous_token("cat")])
/// ```
pub fn anonymous_token(key: &Bound<'_, PyAny>) -> PyResult<QueryToken> {
QueryToken::anonymous_py(key)
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pyfunction)]
/// Special tokens are a very limited set of tokens supported by the ``list_posts`` API.
/// They include being able to filter by posts that the current user has upvoted, or favorited.
/// See :class:`PostSpecialToken` for all the supported token names.
///
/// :param key: The special token name, string or ``PostSpecialToken``
///
/// -----
/// Usage
/// -----
///
/// Selects posts with score of 0, without comments and without favorites
///
/// ```python
/// client.list_post(fields=[special_token(PostSpecialToken.Tumbleweed)])
/// ```
pub fn special_token(key: &Bound<'_, PyAny>) -> PyResult<QueryToken> {
QueryToken::special_py(key)
}
#[cfg(feature = "python")]
#[cfg_attr(all(feature = "python"), pymethods)]
impl QueryToken {
#[pyo3(name = "__str__")]
/// Generates a string representation of this QueryToken
pub fn to_python_string(&self) -> PyResult<String> {
Ok(format!("QueryToken(\"{}\", \"{}\")", self.key, self.value))
}
#[pyo3(name = "__repr__")]
/// Generates a string representation of this QueryToken
pub fn to_python_repr(&self) -> PyResult<String> {
self.to_python_string()
}
#[pyo3(name = "token")]
#[staticmethod]
#[doc(hidden)]
pub fn token_py(key: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult<Self> {
let value = if let Ok(value) = value.extract::<u32>() {
value.to_string()
} else {
value.extract::<String>()?
};
if let Ok(tnt) = key.extract::<TagNamedToken>() {
Ok(QueryToken::token(tnt, value))
} else if let Ok(pnt) = key.extract::<PostNamedToken>() {
Ok(QueryToken::token(pnt, value))
} else if let Ok(pnt) = key.extract::<PoolNamedToken>() {
Ok(QueryToken::token(pnt, value))
} else if let Ok(comment) = key.extract::<CommentNamedToken>() {
Ok(QueryToken::token(comment, value))
} else if let Ok(user) = key.extract::<UserNamedToken>() {
Ok(QueryToken::token(user, value))
} else if let Ok(x) = key.extract::<SnapshotNamedToken>() {
Ok(QueryToken::token(x, value))
} else if let Ok(strvalue) = key.extract::<String>() {
Ok(QueryToken::token(strvalue, value))
} else {
Err(PyErr::new::<PyValueError, _>("Invalid value type for key"))
}
}
#[pyo3(name = "sort")]
#[staticmethod]
#[doc(hidden)]
pub fn sort_py(key: &Bound<'_, PyAny>) -> PyResult<Self> {
if let Ok(tnt) = key.extract::<TagSortToken>() {
Ok(QueryToken::sort(tnt))
} else if let Ok(pnt) = key.extract::<PostSortToken>() {
Ok(QueryToken::sort(pnt))
} else if let Ok(pnt) = key.extract::<PoolSortToken>() {
Ok(QueryToken::sort(pnt))
} else if let Ok(comment) = key.extract::<CommentSortToken>() {
Ok(QueryToken::sort(comment))
} else if let Ok(user) = key.extract::<UserSortToken>() {
Ok(QueryToken::sort(user))
} else if let Ok(strvalue) = key.extract::<String>() {
Ok(QueryToken::sort(strvalue))
} else {
Err(PyErr::new::<PyValueError, _>("Invalid value type for key"))
}
}
#[pyo3(name = "anonymous")]
#[staticmethod]
#[doc(hidden)]
pub fn anonymous_py(key: &Bound<'_, PyAny>) -> PyResult<Self> {
let key = key.extract::<String>()?;
Ok(QueryToken::anonymous(key))
}
#[pyo3(name = "special")]
#[staticmethod]
#[doc(hidden)]
pub fn special_py(key: &Bound<'_, PyAny>) -> PyResult<Self> {
if let Ok(special) = key.extract::<PostSpecialToken>() {
Ok(QueryToken::special(special))
} else if let Ok(strvalue) = key.extract::<String>() {
Ok(QueryToken::special(strvalue))
} else {
Err(PyErr::new::<PyValueError, _>("Invalid value type for key"))
}
}
#[pyo3(name = "negate")]
#[doc(hidden)]
pub fn negate_py(&self) -> PyResult<Self> {
Ok(self.negate())
}
/// Negates the query token. Would turn ``konosuba`` into ``-konosuba``
pub fn __neg__(&self) -> PyResult<Self> {
Ok(self.negate())
}
}
impl Display for QueryToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let suffix = if !self.value.is_empty() {
@ -157,8 +364,12 @@ impl ToQueryString for Vec<QueryToken> {
}
}
#[derive(Debug, AsRefStr)]
#[derive(Debug, AsRefStr, PartialEq, Eq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.tokens")
)]
/// Type-safe named query tokens for use with [list_tags](crate::SzurubooruRequest::list_tags)
pub enum TagNamedToken {
/// having given name (accepts wildcards)
@ -188,8 +399,28 @@ pub enum TagNamedToken {
}
impl NamedToken for TagNamedToken {}
#[derive(Debug, AsRefStr)]
/*#[cfg(feature="python")]
impl<'py> FromPyObject<'py> for TagNamedToken {
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
/*use pyo3::exceptions::PyTypeError;
if ob.is_instance_of::<TagNamedToken>() {
Ok()
}
let strvalue = ob.extract::<String>()?;
match TagNamedToken::from_str(&strvalue) {
Ok(tnt) => Ok(tnt),
Err(_) => Err(PyTypeError::new_err("Invalid variant"))
}*/
Ok(ob.downcast_into_exact::<Self>()?.)
}
}*/
#[derive(Debug, AsRefStr, Eq, PartialEq, Clone)]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(
all(feature = "python"),
pyclass(eq, eq_int, module = "szurubooru_client.tokens")
)]
/// 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 +454,12 @@ 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, module = "szurubooru_client.tokens")
)]
/// Type-safe named query tokens for use with [list_posts](crate::SzurubooruRequest::list_posts)
pub enum PostNamedToken {
/// having given post number
@ -259,8 +494,9 @@ pub enum PostNamedToken {
RelationCount,
/// having been featured given number of times
FeatureCount,
/// given type of posts. `value` can be either `image`, `animation` (or `animated` or `anim`),
/// `flash` (or `swf`) or `video` (or `webm`). Use [models::PostType] for type-safe values
/// given type of posts. The value can be either `image`, `animation` (or `animated` or `anim`),
/// `flash` (or `swf`) or `video` (or `webm`). Use [PostType](crate::models::PostType)
/// for type-safe values
Type,
/// having given SHA1 checksum
ContentChecksum,
@ -312,16 +548,20 @@ pub enum PostNamedToken {
FeatureDate,
/// alias of [PostNamedToken::FeatureDate]
FeatureTime,
/// having given safety. <value> can be either `safe`, `sketchy` (or `questionable`) or `unsafe`
/// Use [models::PostSafety] for the type-safe version
/// Post safety. Can be either `safe`, `sketchy` (or `questionable`) or `unsafe`
/// Use [PostSafety](crate::models::PostSafety) for the type-safe version
Safety,
/// alias of [PostNamedToken::Safety]
Rating,
}
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, module = "szurubooru_client.tokens")
)]
/// Type-safe sort query tokens for use with [list_posts](crate::SzurubooruRequest::list_posts)
pub enum PostSortToken {
/// as random as it can get
@ -364,7 +604,7 @@ pub enum PostSortToken {
Date,
/// alias of [PostSortToken::CreationDate]
Time,
/// like [PostSortToken::CreationDate], only looks at last edit time
/// like [PostSortToken::CreationDate], only looks at last edit time instead
LastEditDate,
/// alias of [PostSortToken::LastEditDate]
LastEditTime,
@ -387,8 +627,12 @@ 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, module = "szurubooru_client.tokens")
)]
/// 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 +646,12 @@ 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, module = "szurubooru_client.tokens")
)]
/// Type-safe named query tokens for use with [list_pools](crate::SzurubooruRequest::list_pools)
pub enum PoolNamedToken {
/// having given name (accepts wildcards)
@ -427,8 +675,12 @@ 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, module = "szurubooru_client.tokens")
)]
/// 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 +706,12 @@ 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, module = "szurubooru_client.tokens")
)]
/// Type-safe named query tokens for use with
/// [list_comments](crate::SzurubooruRequest::list_comments)
pub enum CommentNamedToken {
@ -484,8 +740,12 @@ 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, module = "szurubooru_client.tokens")
)]
/// Type-safe sort query tokens for use with
/// [list_comments](crate::SzurubooruRequest::list_comments)
pub enum CommentSortToken {
@ -512,8 +772,12 @@ 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, module = "szurubooru_client.tokens")
)]
/// Type-safe named query tokens for use with [list_users](crate::SzurubooruRequest::list_users)
pub enum UserNamedToken {
/// having given name (accepts wildcards)
@ -533,8 +797,12 @@ 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, module = "szurubooru_client.tokens")
)]
/// 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 +824,12 @@ 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, module = "szurubooru_client.tokens")
)]
/// Type-safe named query tokens for use with
/// [list_snapshots](crate::SzurubooruRequest::list_snapshots)
pub enum SnapshotNamedToken {

View file

@ -0,0 +1,4 @@
from .szurubooru_client import *
__doc__ = szurubooru_client.__doc__
__all__ = ["SzurubooruSyncClient", "SzurubooruAsyncClient", "SzuruClientError", "PagedResult"]

View file

@ -0,0 +1,34 @@
from .szurubooru_client import _models
AroundPostResult = _models.AroundPostResult
CommentResource = _models.CommentResource
GlobalInfo = _models.GlobalInfo
ImageSearchResult = _models.ImageSearchResult
ImageSearchSimilarPost = _models.ImageSearchSimilarPost
MicroPoolResource = _models.MicroPoolResource
MicroPostResource = _models.MicroPostResource
MicroTagResource = _models.MicroTagResource
MicroUserResource = _models.MicroUserResource
NoteResource = _models.NoteResource
PoolCategoryResource = _models.PoolCategoryResource
PoolResource = _models.PoolResource
PostResource = _models.PostResource
PostSafety = _models.PostSafety
PostType = _models.PostType
SnapshotCreationDeletionData = _models.SnapshotCreationDeletionData
SnapshotData = _models.SnapshotData
SnapshotModificationData = _models.SnapshotModificationData
SnapshotOperationType = _models.SnapshotOperationType
SnapshotResource = _models.SnapshotResource
SnapshotResourceType = _models.SnapshotResourceType
TagCategoryResource = _models.TagCategoryResource
TagResource = _models.TagResource
TagSibling = _models.TagSibling
UserAuthTokenResource = _models.UserAuthTokenResource
UserAvatarStyle = _models.UserAvatarStyle
UserRank = _models.UserRank
UserResource = _models.UserResource
__doc__ = _models.__doc__
#if hasattr(_models, "__all__"):
# __all__ = getattr(_models, "__all__")

View file

@ -0,0 +1,23 @@
from .szurubooru_client import _tokens
anonymous_token = _tokens.anonymous_token
named_token = _tokens.named_token
sort_token = _tokens.sort_token
special_token = _tokens.special_token
CommentNamedToken = _tokens.CommentNamedToken
CommentSortToken = _tokens.CommentSortToken
PoolNamedToken = _tokens.PoolNamedToken
PoolSortToken = _tokens.PoolSortToken
PostNamedToken = _tokens.PostNamedToken
PostSortToken = _tokens.PostSortToken
PostSpecialToken = _tokens.PostSpecialToken
QueryToken = _tokens.QueryToken
SnapshotNamedToken = _tokens.SnapshotNamedToken
TagNamedToken = _tokens.TagNamedToken
TagSortToken = _tokens.TagSortToken
UserNamedToken = _tokens.UserNamedToken
UserSortToken = _tokens.UserSortToken
__doc__ = _tokens.__doc__
#if hasattr(_tokens, "__all__"):
# __all__ = getattr(_tokens, "__all__")

View file

@ -1,11 +0,0 @@
#!/bin/bash
#export MOUNT_DATA=szurubooru/data
export SERVER_MOUNT=./szurubooru/server
#export MOUNT_SQL=szurubooru/pgsql
export BASE_URL=http://localhost:9801
export PORT=9801
docker compose down || true
docker compose up -d

View file

@ -1,3 +0,0 @@
#!/bin/bash
docker compose down

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 310 KiB

Before After
Before After

View file

@ -0,0 +1,179 @@
# rather than editing this file, it is strongly suggested to create config.yaml
# and override only what you need.
# shown in the website title and on the front page
name: integrationland
# full url to the homepage of this szurubooru site, with no trailing slash
domain: https://localhost:9802
# used to salt the users' password hashes and generate filenames for static content
secret: awdopk1mdqsapdoawd21!!!#asw
# Delete thumbnails and source files on post delete
# Original functionality is no, to mitigate the impacts of admins going
# on unchecked post purges.
delete_source_files: no
thumbnails:
avatar_width: 300
avatar_height: 300
post_width: 300
post_height: 300
# settings used to download files from the web on behalf of the api users
user_agent:
max_dl_filesize: 25.0E+6 # maximum filesize limit in bytes
# automatically convert animated GIF uploads to video formats
convert:
gif:
to_webm: false
to_mp4: false
# allow posts to be uploaded even if some image processing errors occur
allow_broken_uploads: false
# used to send password reset e-mails
smtp:
host: # example: localhost
port: # example: 25
user: # example: bot
pass: # example: groovy123
from: # example: noreply@example.com
# if host is left empty the password reset feature will be disabled,
# in which case it is recommended to fill contactEmail so that users
# know who to contact when they want to reset their password
contact_email: # example: bob@example.com. Meant for manual password reset procedures
enable_safety: yes
tag_name_regex: ^\S+$
tag_category_name_regex: ^[^\s%+#/]+$
pool_name_regex: ^\S+$
pool_category_name_regex: ^[^\s%+#/]+$
# don't make these more restrictive unless you want to annoy people; if you do
# customize them, make sure to update the instructions in the registration form
# template as well.
password_regex: '^.{5,}$'
user_name_regex: '^[a-zA-Z0-9_-]{1,32}$'
# webhooks to call when events occur (such as post/tag/user/etc. changes)
# the listed urls will be called with a HTTP POST request with a payload
# containing a snapshot resource as JSON. See doc/API.md for details
webhooks:
# - https://api.example.com/webhooks/
default_rank: regular
privileges:
'users:create:self': anonymous # Registration permission
'users:create:any': administrator
'users:list': regular
'users:view': regular
'users:edit:any:name': moderator
'users:edit:any:pass': moderator
'users:edit:any:email': moderator
'users:edit:any:avatar': moderator
'users:edit:any:rank': moderator
'users:edit:self:name': regular
'users:edit:self:pass': regular
'users:edit:self:email': regular
'users:edit:self:avatar': regular
'users:edit:self:rank': moderator # one can't promote themselves or anyone to upper rank than their own.
'users:delete:any': administrator
'users:delete:self': regular
'user_tokens:list:any': administrator
'user_tokens:list:self': regular
'user_tokens:create:any': administrator
'user_tokens:create:self': regular
'user_tokens:edit:any': administrator
'user_tokens:edit:self': regular
'user_tokens:delete:any': administrator
'user_tokens:delete:self': regular
'posts:create:anonymous': regular
'posts:create:identified': regular
'posts:list': anonymous
'posts:reverse_search': regular
'posts:view': anonymous
'posts:view:featured': anonymous
'posts:edit:content': power
'posts:edit:flags': regular
'posts:edit:notes': regular
'posts:edit:relations': regular
'posts:edit:safety': power
'posts:edit:source': regular
'posts:edit:tags': regular
'posts:edit:thumbnail': power
'posts:feature': moderator
'posts:delete': moderator
'posts:score': regular
'posts:merge': moderator
'posts:favorite': regular
'posts:bulk-edit:tags': power
'posts:bulk-edit:safety': power
'posts:bulk-edit:delete': power
'tags:create': regular
'tags:edit:names': power
'tags:edit:category': power
'tags:edit:description': power
'tags:edit:implications': power
'tags:edit:suggestions': power
'tags:list': regular
'tags:view': anonymous
'tags:merge': moderator
'tags:delete': moderator
'tag_categories:create': moderator
'tag_categories:edit:name': moderator
'tag_categories:edit:color': moderator
'tag_categories:edit:order': moderator
'tag_categories:list': anonymous
'tag_categories:view': anonymous
'tag_categories:delete': moderator
'tag_categories:set_default': moderator
'pools:create': regular
'pools:edit:names': power
'pools:edit:category': power
'pools:edit:description': power
'pools:edit:posts': power
'pools:list': regular
'pools:view': anonymous
'pools:merge': moderator
'pools:delete': moderator
'pool_categories:create': moderator
'pool_categories:edit:name': moderator
'pool_categories:edit:color': moderator
'pool_categories:list': anonymous
'pool_categories:view': anonymous
'pool_categories:delete': moderator
'pool_categories:set_default': moderator
'comments:create': regular
'comments:delete:any': moderator
'comments:delete:own': regular
'comments:edit:any': moderator
'comments:edit:own': regular
'comments:list': regular
'comments:view': regular
'comments:score': regular
'snapshots:list': power
'uploads:create': regular
'uploads:use_downloader': power
## ONLY SET THESE IF DEPLOYING OUTSIDE OF DOCKER
#debug: 0 # generate server logs?
#show_sql: 0 # show sql in server logs?
#data_url: /data/
#data_dir: /var/www/data
## usage: schema://user:password@host:port/database_name
## example: postgres://szuru:dog@localhost:5432/szuru_test
#database:

View file

@ -24,7 +24,7 @@ services:
THREADS:
volumes:
- "sz-data:/data"
- "${SERVER_MOUNT}/config.yaml:/opt/app/config.yaml"
- "./config.yaml:/opt/app/config.yaml"
client:
image: szurubooru/client:latest
@ -32,11 +32,11 @@ services:
- server
environment:
BACKEND_HOST: server
BASE_URL: ${BASE_URL}
BASE_URL: http://localhost:9802
volumes:
- "sz-data:/data:ro"
ports:
- "${PORT}:80"
- "9802:80"
sql:
image: postgres:11-alpine

View file

@ -0,0 +1,6 @@
Jinja2==3.1.4
MarkupSafe==2.1.5
maturin==1.7.0
pdoc==14.6.0
Pygments==2.18.0
loguru==0.7.2

366
tests/python-sync/test.py Normal file
View file

@ -0,0 +1,366 @@
from szurubooru_client import *
from szurubooru_client.tokens import *
from szurubooru_client.models import *
import time, sys
from loguru import logger
import hashlib
import tempfile, pathlib
def connect():
logger.info("Connecting to Szurubooru instance")
anon_client = SzurubooruSyncClient("http://localhost:9802")
error = None
for i in range(5):
try:
anon_client.global_info()
except Exception as e:
error=e
time.sleep(5)
else:
logger.info("Connection successful!")
break
else:
logger.error("Could not connect to Szurubooru instance, error is {}", error)
sys.exit(1)
return anon_client
def create_auth_client(client):
client.create_user("integration_user", "integration_password", rank=UserRank.Administrator)
auth_client = SzurubooruSyncClient("http://localhost:9802", username="integration_user",
password="integration_password", allow_insecure=True)
return auth_client
def test_tag_categories(client):
logger.info("Testing tag categories")
logger.info("Listing tag categories")
tag_cats = client.list_tag_categories()
assert len(tag_cats) == 1
logger.info("Creating tag category")
result_tag_cat = client.create_tag_category("my_tag_cat", color="purple", order=1)
assert result_tag_cat.name == "my_tag_cat"
tag_cats = client.list_tag_categories()
assert len(tag_cats) != 1
assert tag_cats[1].name == "my_tag_cat"
logger.info("Getting tag category")
get_tag_cat = client.get_tag_category("my_tag_cat")
assert get_tag_cat.color == result_tag_cat.color
logger.info("Updating tag category")
update_tag_cat = client.update_tag_category("my_tag_cat", get_tag_cat.version, color="red")
assert update_tag_cat.color != get_tag_cat.color
logger.info("Deleting tag category")
client.delete_tag_category("my_tag_cat", update_tag_cat.version)
tag_cats = client.list_tag_categories()
assert len(tag_cats) == 1
def test_tag(client):
logger.info("Testing tags")
logger.info("Listing tags")
tags = client.list_tags()
assert len(tags.results) == 0
logger.info("Creating tag")
foo_tag = client.create_tag("foo", category="default", description="The foo tag")
assert foo_tag.names == ["foo"]
tags = client.list_tags()
assert len(tags.results) == 1
logger.info("Testing field selection")
tags = client.list_tags(fields=["version", "names", "category"])
assert len(tags.results) == 1
assert tags.results[0].description is None
logger.info("Updating tag")
foo_tag = client.update_tag(foo_tag.names[0], version=foo_tag.version, description="The foo2 tag")
assert foo_tag.description == "The foo2 tag"
logger.info("Getting tag")
foo_tag2 = client.get_tag("foo")
assert foo_tag.description == foo_tag2.description
logger.info("Creating a second tag")
bar_tag = client.create_tag("bar", category="default", description="The bar tag")
assert bar_tag.names == ["bar"]
logger.info("Merging tags")
foo_tag2 = client.merge_tags(bar_tag.names[0], bar_tag.version,
foo_tag2.names[0], foo_tag2.version)
tags = client.list_tags()
assert len(tags.results) == 1
logger.info("Deleting tag")
client.delete_tag(foo_tag2.names[0], foo_tag2.version)
def test_creating_posts(client):
logger.info("Testing posts")
logger.info("Listing posts")
posts = client.list_posts()
assert len(posts.results) == 0
logger.info("Creating post from URL")
wiki_post = client.create_post(url="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Maine_Coon_cat_by_Tomitheos.JPG/225px-Maine_Coon_cat_by_Tomitheos.JPG",
safety=PostSafety.Safe)
posts = client.list_posts()
assert len(posts.results) == 1
logger.info("Updating post")
wiki_post = client.update_post(wiki_post.id, wiki_post.version, source="Wikipedia")
assert wiki_post.source == "Wikipedia"
logger.info("Deleting post")
client.delete_post(wiki_post.id, wiki_post.version)
posts = client.list_posts()
assert len(posts.results) == 0
logger.info("Testing upload by file path")
folly1 = client.create_post(file_path="../folly1.jpg",
tags=["maine_coon", "cat", "folly1"],
safety=PostSafety.Safe)
posts = client.list_posts()
assert len(posts.results) == 1
folly2 = client.create_post(file_path="../folly2.jpg",
tags=["maine_coon", "cat", "folly2"],
safety=PostSafety.Safe)
logger.info("Testing upload with thumbnail")
folly3 = client.create_post(file_path="../folly3.jpg",
thumbnail_path="../folly3_thumb.jpg",
tags=["maine_coon", "cat", "folly3"],
safety=PostSafety.Safe)
logger.info("Searching for a post with image")
folly3_search = client.post_for_image("../folly3.jpg")
assert folly3_search is not None
assert folly3_search.id == folly3.id
logger.info("Reverse image searching")
reverse_search = client.reverse_image_search("../folly3.jpg")
assert reverse_search.exact_post.id == folly3.id
logger.info("Testing temporary upload")
token = client.upload_temporary_file("../folly4.jpg")
folly4 = client.create_post(upload_token=token, tags=["maine_coon", "cat", "folly4"],
safety=PostSafety.Safe)
logger.info("Querying by anonymous tag")
cat_posts = client.list_posts(query=[anonymous_token("cat")])
assert len(cat_posts.results) == 4
logger.info("Querying by named tag")
cat_posts = client.list_posts(query=[named_token(PostNamedToken.Tag, "maine_coon")])
assert len(cat_posts.results) == 4
logger.info("Testing pagination")
cat_posts = client.list_posts(limit=1)
assert cat_posts.total == 4
assert len(cat_posts.results) == 1
cat_posts2 = client.list_posts(limit=1, offset=1)
assert cat_posts.results != cat_posts2.results
logger.info("Testing tag siblings")
tag_occurrences = client.get_tag_siblings("maine_coon")
cat_occurrences = list(filter(lambda x: "cat" in x.tag.names, tag_occurrences))
assert len(cat_occurrences) == 1
logger.info("Rating post")
client.rate_post(folly3.id, 1)
logger.info("Testing rating error validation")
try:
client.rate_post(folly3.id, -2)
except ValueError:
assert True
else:
assert False
logger.info("Favoriting post")
client.favorite_post(folly3.id)
logger.info("Unfavoriting post")
client.unfavorite_post(folly3.id)
logger.info("Featuring post")
client.set_featured_post(folly3.id)
logger.info("Getting featured post")
featured_post = client.get_featured_post()
assert folly3.id == featured_post.id
logger.info("Merging posts")
merged_post = client.merge_post(folly4.id, folly4.version, folly3.id, folly3.version)
assert merged_post.id == folly3.id
def test_pool_categories(client):
logger.info("Testing pool categories")
logger.info("Listing pool categories")
pool_cats = client.list_pool_categories()
assert len(pool_cats) != 0
logger.info("Creating pool category")
pool_cat = client.create_pool_category("cat_pool_category", color="purple")
assert pool_cat.color == "purple"
pool_dog = client.create_pool_category("dog_category", color="orange")
logger.info("Updating pool category")
pool_cat = client.update_pool_category(pool_cat.name, pool_cat.version, color="white")
assert pool_cat.color == "white"
logger.info("Getting pool category")
pool_dog = client.get_pool_category(pool_dog.name)
assert pool_dog.color == "orange"
logger.info("Deleting pool category")
pool_dog = client.delete_pool_category(pool_dog.name, pool_dog.version)
logger.info("Setting default pool category")
client.set_default_pool_category(pool_cat.name)
def test_pools(client):
logger.info("Testing post pools")
logger.info("Listing post pools")
pools = client.list_pools()
assert len(pools.results) == 0
logger.info("Creating pools")
cat_pool = client.create_pool("cats_pool", category="cat_pool_category")
catz_pool = client.create_pool("catz_pool", category="cat_pool_category")
dogs_pool = client.create_pool("dogs_pool", category="cat_pool_category")
logger.info("Getting pool")
cat_pool = client.get_pool(cat_pool.id)
logger.info("Deleting pool")
client.delete_pool(dogs_pool.id, dogs_pool.version)
logger.info("Updating pool")
f4_results = client.list_posts([anonymous_token("cat")])
post_ids= list(map(lambda p: p.id, f4_results.results))
cat_pool = client.update_pool(cat_pool.id, cat_pool.version,
posts=post_ids, description="All cat pictures")
assert len(cat_pool.posts) != 0
logger.info("Merging pools")
merged_pool = client.merge_pools(catz_pool.id, catz_pool.version, cat_pool.id, cat_pool.version)
assert merged_pool.id == cat_pool.id
def test_comments(client):
logger.info("Testing post comments")
logger.info("Listing post comments")
comment_list = client.list_comments()
assert len(comment_list.results) == 0
cat_results = client.list_posts([anonymous_token("cat")])
post_id = cat_results.results[0].id
logger.info("Creating comment")
comment = client.create_comment("Excellent cat!", post_id)
logger.info("Updating comment")
comment = client.update_comment(comment.id, comment.version, text="Beautiful cat!")
assert comment.text == "Beautiful cat!"
logger.info("Getting comment")
comment = client.get_comment(comment.id)
assert comment.text == "Beautiful cat!"
logger.info("Getting all comments for post")
comment_list = client.list_comments([named_token(CommentNamedToken.Post, post_id)])
assert len(comment_list.results) != 0
logger.info("Testing rating comments")
comment = client.rate_comment(comment.id, -1)
assert comment.own_score == -1
try:
client.rate_comment(comment.id, -2)
except SzuruClientError:
assert True
else:
assert False
logger.info("Deleting comment")
client.delete_comment(comment.id, comment.version)
def test_users(client):
logger.info("Testing users")
logger.info("Listing users")
user_list = client.list_users()
logger.info("Creating user with avatar")
user = client.create_user("iu2", "ipass2", rank=UserRank.Regular, avatar_path="../avatar.jpg")
assert user.avatar_style == UserAvatarStyle.Manual
logger.info("Updating user")
user = client.update_user(user.name, user.version, rank=UserRank.Restricted)
logger.info("Getting user")
user = client.get_user(user.name)
logger.info("Deleting user")
client.delete_user(user.name, user.version)
logger.info("Listing user tokens")
tokens = client.list_user_tokens("integration_user")
assert len(tokens) == 0
logger.info("Creating user token")
token = client.create_user_token("integration_user", "My token")
logger.info("Updating user token")
token2 = client.update_user_token("integration_user", token.token, token.version, enabled=False)
assert token2.enabled == False
logger.info("Deleting user token")
client.delete_user_token("integration_user", token2.token, token2.version)
def test_snapshots(client):
logger.info("Testing snapshots")
logger.info("Listing snapshots")
snapshot_list = client.list_snapshots()
assert len(snapshot_list.results) != 0
def test_downloads(client):
logger.info("Testing downloads")
f3_hasher = hashlib.new("sha1")
with open("../folly3.jpg", "rb") as f:
while (byte := f.read(1)):
f3_hasher.update(byte)
f3_post = client.list_posts([anonymous_token("folly3")]).results[0]
with tempfile.TemporaryDirectory() as tmpdirname:
tmpdir = pathlib.Path(tmpdirname)
fname = tmpdir / "folly3_dl.jpg"
#fname = "../folly4_dl.jpg"
client.download_image_to_path(f3_post.id, fname)
dl_hasher = hashlib.new("sha1")
with open(fname, "rb") as f:
while (byte := f.read(1)):
dl_hasher.update(byte)
assert f3_hasher.hexdigest() == dl_hasher.hexdigest()
if __name__ == "__main__":
client = connect()
client = create_auth_client(client)
test_tag_categories(client)
test_tag(client)
test_creating_posts(client)
test_pool_categories(client)
test_pools(client)
test_comments(client)
test_users(client)
test_snapshots(client)
test_downloads(client)

9
tests/python-sync/test.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/bash
set -e
pip uninstall -y szurubooru_client || true
pip install -r requirements.txt
maturin develop -F python -m ../../szurubooru-client/Cargo.toml
docker compose down
docker compose up -d
python test.py && docker compose down

View file

@ -1,12 +1,12 @@
[package]
name = "szurubooru-integration-test"
name = "rust-test"
version = "0.1.0"
edition = "2021"
[dependencies]
chrono = "0.4.38"
sha1 = "0.10.6"
szurubooru-client = { path = "../szurubooru-client" }
szurubooru-client = { path = "../../szurubooru-client" }
tempfile = "3.12.0"
tokio = { version = "1.39.2", features = ["full", "test-util", "tracing"] }
tracing = "0.1.40"

View file

@ -0,0 +1,51 @@
## Example Docker Compose configuration
##
## Use this as a template to set up docker-compose, or as guide to set up other
## orchestration services
version: '2'
services:
server:
image: szurubooru/server:latest
depends_on:
- sql
environment:
## These should be the names of the dependent containers listed below,
## or FQDNs/IP addresses if these services are running outside of Docker
POSTGRES_HOST: sql
## Credentials for database:
POSTGRES_USER: pguser
POSTGRES_PASSWORD: pgpassword
## Commented Values are Default:
#POSTGRES_DB: defaults to same as POSTGRES_USER
#POSTGRES_PORT: 5432
#LOG_SQL: 0 (1 for verbose SQL logs)
THREADS:
volumes:
- "sz-data:/data"
- "./config.yaml:/opt/app/config.yaml"
client:
image: szurubooru/client:latest
depends_on:
- server
environment:
BACKEND_HOST: server
BASE_URL: http://localhost:9801
volumes:
- "sz-data:/data:ro"
ports:
- "9801:80"
sql:
image: postgres:11-alpine
restart: unless-stopped
environment:
POSTGRES_USER: pguser
POSTGRES_PASSWORD: pgpassword
#volumes:
# - "${MOUNT_SQL}/sqldata:/var/lib/postgresql/data"
volumes:
sz-data:

View file

@ -77,21 +77,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
#[tracing::instrument]
async fn start_instance() -> SzurubooruClient {
info!("Starting Szurubooru instance...");
info!("Connecting to Szurubooru instance...");
let anon_client = SzurubooruClient::new_anonymous("http://localhost:9801", true)
.expect("Can't create anonymous client");
Command::new("sh")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.arg("-c")
.arg("./start_szurubooru.sh")
.stderr(Stdio::null())
.stdout(Stdio::null())
.stdin(Stdio::null())
.status()
.await
.expect("Failed to start szurubooru");
let mut connected = false;
let mut error = None;
@ -220,7 +209,11 @@ async fn test_tags(client: &SzurubooruClient) {
info!("Testing field selection");
let tag_list = client
.with_fields(vec!["version", "names", "category"])
.with_fields(vec![
"version".to_string(),
"names".to_string(),
"category".to_string(),
])
.list_tags(None)
.await
.expect("Could not list tags");
@ -269,15 +262,15 @@ async fn test_tags(client: &SzurubooruClient) {
info!("Merging tags");
let merge_tag = MergeTagsBuilder::default()
.remove_version(bar_tag.version)
.remove(bar_tag.names.as_ref().unwrap().first().unwrap())
.remove_tag_version(bar_tag.version)
.remove_tag(bar_tag.names.as_ref().unwrap().first().unwrap().clone())
.merge_to_version(tag_res3.version)
.merge_to(tag_res3.names.as_ref().unwrap().first().unwrap())
.merge_to_tag(tag_res3.names.as_ref().unwrap().first().unwrap().clone())
.build()
.expect("Could not create merge tags object");
let merged_tag = client
.request()
.merge_tag(&merge_tag)
.merge_tags(&merge_tag)
.await
.expect("Could not merge tags");
assert_eq!(tag_res3.names, merged_tag.names);
@ -357,7 +350,7 @@ async fn test_creating_posts(client: &SzurubooruClient) {
.safety(PostSafety::Safe)
.build()
.expect("Could not build first upload object");
let folly1_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly1.jpg");
let folly1_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../folly1.jpg");
let mut folly1_file =
File::open(&folly1_path).expect(&format!("Could not open file {folly1_path:?}"));
let _folly1_post = client
@ -376,7 +369,7 @@ async fn test_creating_posts(client: &SzurubooruClient) {
.safety(PostSafety::Safe)
.build()
.expect("Could not build second upload object");
let folly2_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly2.jpg");
let folly2_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../folly2.jpg");
let _folly2_post = client
.request()
.create_post_from_file_path(folly2_path, None::<String>, &folly2_obj)
@ -393,8 +386,8 @@ async fn test_creating_posts(client: &SzurubooruClient) {
.safety(PostSafety::Safe)
.build()
.expect("Could not build third upload object");
let folly3_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly3.jpg");
let folly3_thumbnail = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly3_thumb.jpg");
let folly3_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../folly3.jpg");
let folly3_thumbnail = Path::new(env!("CARGO_MANIFEST_DIR")).join("../folly3_thumb.jpg");
let folly3_post = client
.request()
.create_post_from_file_path(&folly3_path, Some(folly3_thumbnail), &folly3_obj)
@ -404,10 +397,10 @@ async fn test_creating_posts(client: &SzurubooruClient) {
info!("Searching for post by image");
let matching_posts = client
.request()
.posts_for_file_path(&folly3_path)
.post_for_file_path(&folly3_path)
.await
.expect("Could not search for post by file path");
assert_eq!(matching_posts.results.first().unwrap(), &folly3_post);
assert_eq!(&matching_posts.unwrap(), &folly3_post);
info!("Reverse searching");
let matching_posts = client
@ -418,7 +411,7 @@ async fn test_creating_posts(client: &SzurubooruClient) {
assert!(matching_posts.exact_post.is_some());
info!("Testing temporary upload");
let folly4_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly4.jpg");
let folly4_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../folly4.jpg");
let folly4_temp_upload = client
.request()
.upload_temporary_file_from_path(folly4_path)
@ -681,10 +674,10 @@ async fn test_pools(client: &SzurubooruClient) {
info!("Merging pools");
let merge_pool_obj = MergePoolBuilder::default()
.remove_version(catz_pool.version.unwrap())
.remove(catz_pool.id.unwrap())
.remove_pool_version(catz_pool.version.unwrap())
.remove_pool(catz_pool.id.unwrap())
.merge_to_version(cat_pool.version.unwrap())
.merge_to(cat_pool.id.unwrap())
.merge_to_pool(cat_pool.id.unwrap())
.build()
.expect("Unable to build merge object");
let _cat_pool = client
@ -790,7 +783,7 @@ async fn test_users(client: &SzurubooruClient) {
// Create user is already tested above
info!("Creating user with avatar");
let avatar_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("avatar.jpg");
let avatar_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../avatar.jpg");
let create_user = CreateUpdateUserBuilder::default()
.name("iu2".to_string())
.password("ipass2".to_string())
@ -892,7 +885,7 @@ async fn test_snapshots(client: &SzurubooruClient) {
async fn test_downloads(client: &SzurubooruClient) {
info!("Testing image download");
let folly3_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly3.jpg");
let folly3_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../folly3.jpg");
let mut f3_hasher = Sha1::new();

6
tests/rust-test/test.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/bash
cargo check
docker compose down
docker compose up -d
cargo run && docker compose down