Daniel Dolezal daniel156161
  • Joined on 2024-03-30

website-api-sdk (0.8.0)

Published 2026-06-07 17:36:43 +02:00 by daniel156161

Installation

pip install --index-url https://git.yiprawr.dev/api/packages/daniel156161/pypi/simple/ --extra-index-url https://pypi.org/simple website-api-sdk

About this package

Python SDK for Daniel's website/API server

website-api-sdk

Python SDK for Daniel's website/API server.

Install

uv add website-api-sdk

For local development:

uv sync
uv run ruff check .
uv run pytest

Basic usage

from website_api_sdk import DEFAULT_API_VERSION, WebsiteApiClient

client = WebsiteApiClient(
  urls=['https://api.example'],
  token={
    'token_type': 'Bearer',
    'refresh_token': '...',
  },
  table_name='Archive',
)

print(client.public.get_api_versions())
print(client.submissions.get_all_urls())

By default the SDK sends no X-API-Version header, so the API can use the authenticated user's pinned version.

Force a specific API version for a client/request set:

client = WebsiteApiClient(
  urls=['https://api.example'],
  token={'access_token': '...'},
  api_version=DEFAULT_API_VERSION,
)

You can also provide the version and header together, either directly or in from_config():

client = WebsiteApiClient(
  urls=['https://api.example'],
  token={'access_token': '...'},
  api_version={
    'value': '2026-05-24',
    'header': 'X-API-Version',
  },
)

client = WebsiteApiClient.from_config({
  'url': 'https://api.example',
  'version': {
    'value': '2026-05-24',
    'header': 'X-API-Version',
  },
})

Update the user's pinned API version:

client.set_api_version_pin(DEFAULT_API_VERSION)

Inspect the API version the server used for the last response:

print(client.get_last_api_version_used())

Structure

Endpoint groups are exposed as typed resource objects:

  • client.public: versions, server key, gif, music, cookie, furry interactive story
  • client.health: health/convex endpoint
  • client.submissions: FurAffinity submission archive tables, entries, status/date queries, batch upsert/delete
  • client.furaffinity: FurAffinity favs/watchers and submission absorber state
  • client.n8n: YouTube channel workflow endpoints
  • client.printer: printer REST endpoints

Transport/auth helpers are inherited by the client itself, so direct raw calls are available through client.request(...).

Cache and auth

Only auth is stored as JSON. Queued batch/delete work is stored in SQLite when durable queuing is enabled.

client = WebsiteApiClient(
  urls='https://api.example',
  token={'refresh_token': '...'},
  table_name='Archive',
  cache_path={
    'auth': '.cache/website_api_sdk/auth.json',
    'queue': '.cache/website_api_sdk/queue.sqlite3',
  },
)

With this setup:

  • auth.json stores only token/auth data
  • queue.sqlite3 stores queued batch/delete entries
  • no root/base batch JSON is created
  • no JSON backup/spool files are created
  • no worker ID/PID config is needed
  • auth JSON is written atomically with 0o600 permissions and a lock directory
  • SQLite handles concurrent queue claims between multiple program runs

Batch sends claim pending SQLite rows in one transaction by changing them to inflight. Other instances cannot claim the same rows while they are inflight. If sending fails, claimed rows are restored to pending. On success, claimed rows are deleted.

If you only want shared auth and do not need durable queued batch/delete entries, omit queue:

client = WebsiteApiClient(
  urls='https://api.example',
  token={'refresh_token': '...'},
  table_name='Archive',
  cache_path={
    'auth': '.cache/website_api_sdk/auth.json',
  },
)

In auth-only mode, tokens are still persisted/shared, but queued batch/delete entries stay in memory.

You can inspect cache content safely. Token data is shown first, followed by SQLite queue entries grouped by pending / inflight, action, and table/path. Token-like values are redacted by default:

print(client.show_cache_content(timezone_name='Europe/Berlin'))
cache = client.read_cache_content(timezone_name='Europe/Berlin')

created_at and expires_at are converted to timezone_name when provided. Pass redact_auth=False only if you really need the full token values.

Errors

The SDK raises typed exceptions from website_api_sdk:

  • AuthenticationError for 400/401/403 responses
  • NotFoundError for 404 responses
  • RateLimitError for 429 responses
  • ApiError for other non-200 responses
  • NoAvailableApiUrlError when URL discovery cannot find a healthy API URL

Retry-After headers on 429/503/504 responses are respected automatically. By default the SDK sleeps for the advertised delay, capped at 120 seconds, and retries up to 5 times. Configure this with retry_after_max_retries, retry_after_max_sleep, or inject retry_after_sleep in tests. If the final response still fails, ApiError.retry_after and ApiError.retry_after_raw expose the parsed/raw header values.

Examples

Current API version

WEBSITE_API_URL=https://api.example uv run python examples/get_current_api_version.py

The example calls the public /api/versions endpoint and prints the X-API-Version-Used header captured by client.get_last_api_version_used().

Submission archive

client.select_table('Archive')

client.submissions.upsert_submissions({'url': 'https://www.furaffinity.net/view/123/'})
client.submissions.append({'url': 'https://www.furaffinity.net/view/456/'})
client.submissions.send_batch()

not_downloaded = client.submissions.get_all_not_downloaded_items()

Submission absorber

state = client.furaffinity.get_submission_absorber()
client.furaffinity.save_submission_absorber({
  'ignoredSubmissions': state.get('ignoredSubmissions', []),
  'deleteSubmissions': [],
  'ignoredSeriesByArtist': state.get('ignoredSeriesByArtist', []),
  'url': state.get('url'),
  'page_submission_size': 128,
})

n8n

client.n8n.upsert_youtube_channels({'channel_name': 'Example Channel'})
feed = client.n8n.get_youtube_channel_feed(is_after='2026-01-01', convert_to_time_zone='Europe/Berlin')

Printer

client.printer.upsert_printer('kitchen', model='epson', status=True)
client.printer.add_printer_jobs('kitchen', {'type': 'text', 'content': {'text': 'Hello'}})
client.printer.add_printer_cut_job('kitchen')

Create the matching printer websocket client:

client = WebsiteApiClient(
  urls='https://api.example',
  token={'refresh_token': '...'},
)

ws_client = client.printer.create_printer_websocket_client('kitchen')
await ws_client.start()

Subclass AsyncWebSocketClient if you need custom on_connect, on_message or on_disconnect handling.

Requirements

Requires Python: >=3.11
Details
PyPI
2026-06-07 17:36:43 +02:00
26
Daniel Dolezal
70 KiB
Assets (2)
Versions (5) View all
0.8.0 2026-06-07
0.7.9 2026-06-07
0.7.8 2026-06-07
0.7.7 2026-05-25
0.7.5 2026-05-24