website-api-sdk (0.8.0)
Installation
pip install --index-url https://git.yiprawr.dev/api/packages/daniel156161/pypi/simple/ --extra-index-url https://pypi.org/simple website-api-sdkAbout 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 storyclient.health: health/convex endpointclient.submissions: FurAffinity submission archive tables, entries, status/date queries, batch upsert/deleteclient.furaffinity: FurAffinity favs/watchers and submission absorber stateclient.n8n: YouTube channel workflow endpointsclient.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.jsonstores only token/auth dataqueue.sqlite3stores 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
0o600permissions 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:
AuthenticationErrorfor 400/401/403 responsesNotFoundErrorfor 404 responsesRateLimitErrorfor 429 responsesApiErrorfor other non-200 responsesNoAvailableApiUrlErrorwhen 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.