From 0f6d33379b1836401c998e21b1c9feb7f70f5669 Mon Sep 17 00:00:00 2001 From: Dima Anfimov Date: Wed, 16 Sep 2026 00:21:56 +0200 Subject: [PATCH 1/3] docs: add documentation site --- .github/workflows/release_docs.yml | 38 +++++ Makefile | 4 +- docs/contributing.md | 41 +++++ docs/examples/large_payloads_with_s3.py | 58 +++++++ docs/examples/priority_queues.py | 60 +++++++ docs/examples/reliable_task_delivery.py | 55 ++++++ docs/index.md | 217 ++++++++++++++++++++++++ docs/tutorial/large_payloads_with_s3.md | 36 ++++ docs/tutorial/priority_queues.md | 44 +++++ docs/tutorial/reliable_task_delivery.md | 36 ++++ pyproject.toml | 6 + zensical.toml | 81 +++++++++ 12 files changed, 674 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release_docs.yml create mode 100644 docs/contributing.md create mode 100644 docs/examples/large_payloads_with_s3.py create mode 100644 docs/examples/priority_queues.py create mode 100644 docs/examples/reliable_task_delivery.py create mode 100644 docs/index.md create mode 100644 docs/tutorial/large_payloads_with_s3.md create mode 100644 docs/tutorial/priority_queues.md create mode 100644 docs/tutorial/reliable_task_delivery.md create mode 100644 zensical.toml diff --git a/.github/workflows/release_docs.yml b/.github/workflows/release_docs.yml new file mode 100644 index 0000000..190ae38 --- /dev/null +++ b/.github/workflows/release_docs.yml @@ -0,0 +1,38 @@ +name: Release docs + +on: + push: + branches: [main] + paths: + - "docs/**" + - "zensical.toml" + - ".github/workflows/release_docs.yml" + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + enable-cache: true + python-version: "3.12" + version: "latest" + - run: uv sync --only-group docs + - run: uv run zensical build --clean + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: site + - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + id: deployment diff --git a/Makefile b/Makefile index 2d148dc..52fa3f5 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,10 @@ init: ## Install all project dependencies with extras .PHONY: run_docs run_docs: ## Run documentation server - @uv run mkdocs serve --livereload + @uv run zensical serve .PHONY: run_infra -run_infra: ## Run rabbitmq in docker for integration tests +run_infra: ## Run ministack (local AWS emulator) in docker for integration tests @docker compose -f docker-compose.yml up -d ##@ Code quality diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..1a0a10f --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,41 @@ +--- +title: Contributing +--- + +taskiq-sqs follows the same contribution philosophy as the rest of the taskiq ecosystem — see the +[taskiq contribution guide](https://taskiq-python.github.io/contrib.html) for the general rules (found a bug? +open an issue; not sure about something? open a draft PR and ask in the description; and so on). + +The commands below are specific to this repository — it uses [uv](https://docs.astral.sh/uv/) and +[zensical](https://zensical.org/) rather than the tox/VuePress setup described on that page. + +## Setting up the environment + +```bash +git clone https://github.com/taskiq-python/taskiq-sqs.git +cd taskiq-sqs +make init +``` + +Tests need a local AWS emulator ([ministack](https://github.com/ministackorg/ministack)): + +```bash +make run_infra +``` + +## Linting and testing + +```bash +make lint # ruff + mypy +make test # pytest, against the ministack container started above +``` + +`make help` lists every available target. + +## Working with documentation + +This site is built with [zensical](https://zensical.org/). To preview changes locally: + +```bash +make run_docs +``` diff --git a/docs/examples/large_payloads_with_s3.py b/docs/examples/large_payloads_with_s3.py new file mode 100644 index 0000000..7a33924 --- /dev/null +++ b/docs/examples/large_payloads_with_s3.py @@ -0,0 +1,58 @@ +""" +Run worker: + taskiq worker docs.examples.large_payloads_with_s3:broker + +Run this script: + python docs/examples/large_payloads_with_s3.py +""" + +import asyncio + +import dotenv + +from taskiq_sqs import S3OffloadMiddleware, S3ResultBackend, SQSBroker +from taskiq_sqs.types import S3Bucket, SQSQueue + + +dotenv.load_dotenv() + +ENDPOINT_URL = "http://localhost:4566" +AWS_REGION = "us-east-1" + +broker = SQSBroker( + queues=SQSQueue(name="large-payload-queue"), + endpoint_url=ENDPOINT_URL, + aws_region_name=AWS_REGION, +).with_result_backend( + S3ResultBackend( + bucket=S3Bucket(name="large-payload-results"), + endpoint_url=ENDPOINT_URL, + aws_region_name=AWS_REGION, + ), +) +broker.add_middlewares( + S3OffloadMiddleware( + bucket=S3Bucket(name="large-payload-offload"), + max_message_size=200_000, # payloads larger than this many bytes are offloaded to S3 + endpoint_url=ENDPOINT_URL, + aws_region_name=AWS_REGION, + ), +) + + +@broker.task() +async def summarize_document(content: str) -> dict[str, int]: + return {"characters": len(content), "words": len(content.split())} + + +async def main() -> None: + await broker.startup() + document = "taskiq-sqs " * 100_000 # well over the 256 KiB SQS message limit + task = await summarize_document.kiq(document) + result = await task.wait_result(timeout=10) + print(result.return_value) + await broker.shutdown() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/examples/priority_queues.py b/docs/examples/priority_queues.py new file mode 100644 index 0000000..9e23074 --- /dev/null +++ b/docs/examples/priority_queues.py @@ -0,0 +1,60 @@ +""" +Run worker: + taskiq worker docs.examples.priority_queues:broker + +Run this script to kick a batch of bulk events and two ordered urgent alerts: + python docs/examples/priority_queues.py +""" + +import asyncio + +import dotenv + +from taskiq_sqs import SQSBroker +from taskiq_sqs.types import SQSQueue + + +dotenv.load_dotenv() + +ENDPOINT_URL = "http://localhost:4566" +AWS_REGION = "us-east-1" + +broker = SQSBroker( + queues=[ + # bulk, low-priority work: batched together to cut down on SendMessage calls + SQSQueue(name="bulk-queue", is_batching_enabled=True, batch_size=10, batch_timeout=1.0), + # time-sensitive work: FIFO so alerts from the same source stay in order. ContentBasedDeduplication is + # required here since these messages don't set an explicit deduplication_id label. + SQSQueue(name="urgent-queue.fifo", options={"ContentBasedDeduplication": "true"}), + ], + endpoint_url=ENDPOINT_URL, + aws_region_name=AWS_REGION, +) + + +@broker.task() +async def process_bulk_event(event_id: int) -> None: + """Runs on the default queue (the first one in `queues`), since no `queue_name` label is set.""" + print(f"Processed bulk event {event_id}") + + +@broker.task(queue_name="urgent-queue.fifo") +async def process_urgent_alert(source: str, message: str) -> None: + print(f"[{source}] {message}") + + +async def main() -> None: + await broker.startup() + + for event_id in range(5): + await process_bulk_event.kiq(event_id) + + # group_id keeps alerts from the same source ordered relative to each other + await process_urgent_alert.kicker().with_labels(group_id="sensor-1").kiq("sensor-1", "temperature spike") + await process_urgent_alert.kicker().with_labels(group_id="sensor-1").kiq("sensor-1", "temperature back to normal") + + await broker.shutdown() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/examples/reliable_task_delivery.py b/docs/examples/reliable_task_delivery.py new file mode 100644 index 0000000..55f30a1 --- /dev/null +++ b/docs/examples/reliable_task_delivery.py @@ -0,0 +1,55 @@ +""" +Run worker: + taskiq worker docs.examples.reliable_task_delivery:broker + +Run this script: + python docs/examples/reliable_task_delivery.py +""" + +import asyncio +import time + +import dotenv + +from taskiq_sqs import SQSBroker +from taskiq_sqs.types import SQSQueue + + +dotenv.load_dotenv() + +ENDPOINT_URL = "http://localhost:4566" +AWS_REGION = "us-east-1" + +broker = SQSBroker(queues=SQSQueue(name="retry-queue"), endpoint_url=ENDPOINT_URL, aws_region_name=AWS_REGION) + + +@broker.task() +async def send_verification_email(user_id: int) -> None: + print(f"Sent verification email to user {user_id}") + + +async def schedule_with_deadline(user_id: int, *, delay_seconds: int, deadline_seconds: float) -> None: + """Deliver after `delay_seconds`, but give up if no worker picks it up within `deadline_seconds` of now. + + `deadline_seconds` must be greater than `delay_seconds` — the message isn't even visible to a worker + before `delay_seconds` elapses, so a smaller deadline would make it expire before anyone can receive it. + """ + await ( + send_verification_email.kicker() + .with_labels( + delay=delay_seconds, + expiry=time.time() + deadline_seconds, + ) + .kiq(user_id) + ) + + +async def main() -> None: + await broker.startup() + # deliver in 5 seconds, but only if a worker actually picks it up within 30 seconds of being kicked + await schedule_with_deadline(user_id=42, delay_seconds=5, deadline_seconds=30) + await broker.shutdown() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..2427582 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,217 @@ +--- +title: Overview +--- + +This library provides an SQS broker and an S3 result backend for [TaskIQ](https://taskiq-python.github.io/). + +## Installation + +=== "pip" + + ```bash + pip install taskiq-sqs + ``` + +=== "uv" + + ```bash + uv add taskiq-sqs + ``` + +## Basic usage + +Here is an example of how to use the SQS broker with the S3 backend: + +```python +import asyncio +from taskiq_sqs import S3ResultBackend, SQSBroker +from taskiq_sqs.types import S3Bucket, SQSQueue + +broker = SQSBroker( + queues=SQSQueue(name="my-queue"), # by default the broker creates the queue for you if it doesn't exist + endpoint_url="http://localhost:4566", + aws_region_name="us-east-1", +).with_result_backend( + S3ResultBackend( + bucket=S3Bucket(name="response-bucket") # by default backend will create bucket for you if it does not exist + ) +) + +@broker.task() +async def i_love_aws() -> None: + await asyncio.sleep(1) + print("Hello there!") + +async def main() -> None: + await broker.startup() + task = await i_love_aws.kiq() + print(await task.wait_result()) + await broker.shutdown() + +if __name__ == "__main__": + asyncio.run(main()) +``` + +How to run: + +1. run worker first with `taskiq worker examples.example_broker:broker` +2. after that run broker to create a task and wait for result: `python examples/example_broker.py` + +For a set of complete, runnable scenarios that combine several of the features below, see the [Tutorial](tutorial/priority_queues.md). + +## Multiple queues + +`SQSBroker` accepts a single queue or a list of them. The first queue is the default one, used whenever a task doesn't say otherwise. To send a task to a specific queue, set the `queue_name` label with that queue's name: + +```python +from taskiq_sqs import SQSBroker +from taskiq_sqs.types import SQSQueue + +broker = SQSBroker( + queues=[ + SQSQueue(name="default-queue"), + SQSQueue(name="high-priority-queue", wait_time_seconds=5), + ], +) + +@broker.task(queue_name="high-priority-queue") +async def urgent_task() -> None: + ... +``` + +A worker started against this broker consumes from every configured queue at once. Passing a queue name through the `queue_name` label that isn't configured on the broker raises `UnknownQueueError`. + +See [Priority queues](tutorial/priority_queues.md) for a worked example combining this with FIFO queues and batching. + +## Declaring queues + +By default the broker creates a queue on startup if it doesn't exist yet, the same way `S3Bucket` does for buckets. Set `is_declare=False` to require the queue to already exist instead (raises `QueueNotFoundError` if it doesn't). `options` are queue attributes (e.g. `VisibilityTimeout`, `MessageRetentionPeriod`) passed to `CreateQueue`, in AWS's own PascalCase naming, when the queue is declared — they have no effect on a queue that already exists: + +```python +from taskiq_sqs import SQSBroker +from taskiq_sqs.types import SQSQueue + +broker = SQSBroker( + queues=SQSQueue(name="my-queue", options={"VisibilityTimeout": "60", "MessageRetentionPeriod": "86400"}), +) +``` + +FIFO queues get their `FifoQueue` attribute set automatically when declared — no need to include it in `options`. + +`S3Bucket` has the same `options` field, for parameters `CreateBucket` accepts beyond `name` (e.g. `acl`), passed through whenever `S3ResultBackend`/`S3OffloadMiddleware` create the bucket. + +## Delayed tasks + +Set the `delay` label to delay delivery of a task by that many seconds (0-900, SQS's own limit): + +```python +from taskiq_sqs import SQSBroker +from taskiq_sqs.types import SQSQueue + +broker = SQSBroker(queues=SQSQueue(name="my-queue")) + +@broker.task(delay=30) # "delay" is taskiq_sqs.constants.SQS_DELAY_SECONDS_LABEL +async def send_reminder() -> None: + ... +``` + +A value outside the 0-900 range (or not an integer) raises `InvalidDelaySecondsError` when the task is kicked. + +!!! note + Delay can also be combined with [message expiration](#message-expiration) to build a "retry with a deadline" + pattern — see [Reliable task delivery](tutorial/reliable_task_delivery.md). + +## FIFO queues + +A queue whose name ends in `.fifo` is treated as a FIFO queue automatically, matching SQS's own naming rule (`SQSQueue`'s `is_fifo` field only needs to be set to override that default, and the queue's name must still end in `.fifo` for the broker to accept it as FIFO). + +```python +from taskiq_sqs import SQSBroker +from taskiq_sqs.types import SQSQueue + +broker = SQSBroker(queues=SQSQueue(name="my-queue.fifo")) + +@broker.task(group_id="orders") # defaults to the task name if not set +async def process_order() -> None: + ... +``` + +- `group_id` picks the message's `MessageGroupId` (required by SQS for every FIFO message); it defaults to the task's name. +- `deduplication_id` sets `MessageDeduplicationId`; if not set, the queue must have content-based deduplication enabled, or SQS rejects the message. +- The `delay` label (see [Delayed tasks](#delayed-tasks)) is not supported on FIFO queues — SQS only allows delay to be configured on the queue itself, not per message — and raises `FifoDelayNotSupportedError` if used. + +## Message expiration + +Set the `expiry` label to a unix timestamp; if a worker receives the message after that time, it's deleted without being executed: + +```python +import time +from taskiq_sqs import SQSBroker +from taskiq_sqs.types import SQSQueue + +broker = SQSBroker(queues=SQSQueue(name="my-queue")) + +@broker.task() +async def process_event() -> None: + ... + +await process_event.kicker().with_labels(expiry=time.time() + 300).kiq() # discarded if received after 5 minutes +``` + +Expiration is checked by the worker on receipt, not by SQS itself — a message can still sit in the queue past its expiry (e.g. while workers are busy or scaled to zero), it just won't run once picked up. `expiry` must be a non-negative number; anything else raises `InvalidExpiryError` when the task is kicked. + +## Message batching + +Set `is_batching_enabled` on a queue to buffer kicked messages in memory and flush them together via `SendMessageBatch` (up to `batch_size` messages, or after `batch_timeout` seconds, whichever comes first) instead of sending each one immediately: + +```python +from taskiq_sqs import SQSBroker +from taskiq_sqs.types import SQSQueue + +broker = SQSBroker( + queues=SQSQueue(name="my-queue", is_batching_enabled=True, batch_size=10, batch_timeout=1.0), +) +``` + +!!! warning "This trades durability for throughput" + `kick()` returns as soon as the message is buffered in memory, before it has actually reached SQS. If the + process crashes within `batch_timeout` seconds, a buffered message is lost even though `.kiq()` already + returned successfully. `broker.shutdown()` flushes whatever is still pending, so a clean shutdown doesn't + lose anything — only a crash does. + +- Set the `skip_batching` label on a task to send that one message immediately regardless of the queue's setting. +- A `delay` label always sends immediately too — batching doesn't support per-message delay. +- On FIFO queues, messages are grouped by `group_id` before flushing, so a batch never reorders messages within the same group. + +## Offloading large messages to S3 + +SQS messages are limited to 256 KiB. `S3OffloadMiddleware` transparently uploads task payloads that exceed a configurable threshold to S3 before sending them to the queue, and replaces the message with a reference to the uploaded object. The worker downloads the original payload back from S3 before executing the task, and (by default) removes it from S3 afterwards. + +```python +import asyncio +from taskiq_sqs import S3OffloadMiddleware, SQSBroker +from taskiq_sqs.types import S3Bucket, SQSQueue + +broker = SQSBroker(queues=SQSQueue(name="my-queue")) +broker.add_middlewares( + S3OffloadMiddleware( + bucket=S3Bucket(name="offload-bucket"), # created automatically if it doesn't exist + max_message_size=200_000, # payloads larger than this many bytes are offloaded to S3 + ), +) + +@broker.task +async def process_document(content: str) -> int: + return len(content) + + +async def main() -> None: + await broker.startup() + await process_document.kiq("x" * 1_000_000) # too large for SQS, transparently offloaded to S3 + await broker.shutdown() + +if __name__ == "__main__": + asyncio.run(main()) +``` + +See [Large payloads with S3](tutorial/large_payloads_with_s3.md) for a complete example that combines offloading with the S3 result backend. diff --git a/docs/tutorial/large_payloads_with_s3.md b/docs/tutorial/large_payloads_with_s3.md new file mode 100644 index 0000000..93b4325 --- /dev/null +++ b/docs/tutorial/large_payloads_with_s3.md @@ -0,0 +1,36 @@ +--- +title: Large payloads with S3 +--- + +SQS messages are capped at 256 KiB, which real workloads can outgrow quickly — a document to summarize, a batch of +rows to import, a rendered file to process. [`S3OffloadMiddleware`](../index.md#offloading-large-messages-to-s3) +transparently moves oversized payloads through S3 instead, and pairs naturally with +[`S3ResultBackend`](../index.md#basic-usage) for the result on the way back — both are just S3 buckets, configured +the same way. + +```python +--8<-- "docs/examples/large_payloads_with_s3.py" +``` + +This example uses three separate resources: the queue itself, a bucket for offloaded payloads, and a bucket for +results. All three are declared automatically on `startup()`, same as in the basic example. + +To run it: + +1. Start a worker: + + ```bash + taskiq worker docs.examples.large_payloads_with_s3:broker + ``` + +2. In another terminal, run the script. It builds an ~1.1 MB string — well past the SQS limit — kicks it, and waits + for the result: + + ```bash + python docs/examples/large_payloads_with_s3.py + ``` + +You should see something like `{'characters': 1100000, 'words': 100000}` printed once the worker finishes. Behind +the scenes: the middleware uploaded the document to `large-payload-offload` before the message ever reached SQS, +the worker downloaded it back before running `summarize_document`, deleted it from S3 afterwards (the default +`delete_after_execute=True`), and the result itself was written to `large-payload-results`. diff --git a/docs/tutorial/priority_queues.md b/docs/tutorial/priority_queues.md new file mode 100644 index 0000000..3ffc5ad --- /dev/null +++ b/docs/tutorial/priority_queues.md @@ -0,0 +1,44 @@ +--- +title: Priority queues +--- + +A single [`SQSBroker`](../index.md) can consume from several queues at once, so you don't need to run separate +workers for different kinds of work. This example combines two features to build a simple priority system: + +- a **batched** queue for bulk, low-priority events, where a small delay before delivery is an acceptable + trade-off for fewer `SendMessage` calls; +- a **FIFO** queue for urgent, time-sensitive alerts that must stay in order per source and get delivered + immediately. + +```python +--8<-- "docs/examples/priority_queues.py" +``` + +A few things worth noting: + +- `process_bulk_event` has no `queue_name` label, so it goes to `bulk-queue` — the first queue in `queues`, and + therefore the default one. +- `process_urgent_alert` is pinned to `urgent-queue.fifo` via the `queue_name` label on the task decorator itself, + so every call to `.kiq()` for that task goes there without repeating the label each time. +- `urgent-queue.fifo` enables `ContentBasedDeduplication` through `options` at declare time, since the alerts in + this example don't set an explicit `deduplication_id` label. Without one or the other, SQS rejects the message. +- Both alerts share `group_id="sensor-1"`, so SQS guarantees they're delivered in the order they were sent — + "temperature spike" before "temperature back to normal" — something the batched queue makes no promises about. + +To run it: + +1. Start a worker (it consumes from every configured queue automatically): + + ```bash + taskiq worker docs.examples.priority_queues:broker + ``` + +2. In another terminal, kick the tasks: + + ```bash + python docs/examples/priority_queues.py + ``` + +See [Multiple queues](../index.md#multiple-queues), [FIFO queues](../index.md#fifo-queues) and +[Message batching](../index.md#message-batching) for the reference documentation on each of these features +individually. diff --git a/docs/tutorial/reliable_task_delivery.md b/docs/tutorial/reliable_task_delivery.md new file mode 100644 index 0000000..9112a7f --- /dev/null +++ b/docs/tutorial/reliable_task_delivery.md @@ -0,0 +1,36 @@ +--- +title: Reliable task delivery +--- + +[Delayed tasks](../index.md#delayed-tasks) and [message expiration](../index.md#message-expiration) are two +independent features, but combined they give you a "retry after N seconds, but give up after a deadline" pattern +without any extra infrastructure — no scheduler, no separate retry queue. + +```python +--8<-- "docs/examples/reliable_task_delivery.py" +``` + +!!! warning "Deadline must be greater than delay" + `expiry` is an absolute unix timestamp computed once, at kick time. The message isn't even visible to a + worker until `delay_seconds` has elapsed, so if `deadline_seconds <= delay_seconds`, the message will always + be expired by the time anyone could receive it — it'll be silently discarded on delivery instead of running. + Always leave a gap between the two: `deadline_seconds` should account for `delay_seconds` plus however long + you're willing to let the message sit in the queue after it becomes visible. + +To run it: + +1. Start a worker: + + ```bash + taskiq worker docs.examples.reliable_task_delivery:broker + ``` + +2. In another terminal, schedule the task: + + ```bash + python docs/examples/reliable_task_delivery.py + ``` + +The worker won't pick up the message for about 5 seconds (the `delay`), and prints `Sent verification email to user +42` once it does. If you stop the worker for longer than the remaining deadline before restarting it, the message +is silently discarded instead — check the worker logs for `Discarding expired message from queue '...'`. diff --git a/pyproject.toml b/pyproject.toml index 7fa8372..8a1d56b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "Source" = "https://github.com/taskiq-python/taskiq-sqs" "Bug Tracker" = "https://github.com/taskiq-python/taskiq-sqs/issues" "Repository" = "https://github.com/taskiq-python/taskiq-sqs/" +"Documentation" = "https://taskiq-python.github.io/taskiq-sqs/" [dependency-groups] dev = [ @@ -146,6 +147,11 @@ ignore = [ "D", "INP001", ] +"docs/examples/*" = [ + "T201", # print + "D", + "INP001", +] "src/taskiq_sqs/middleware.py" = [ "PLR0913", # too many arguments "PLR0917", # too many positional arguments diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..263e956 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,81 @@ +[project] +site_name = "taskiq-sqs" +site_description = "SQS broker and S3 result backend for TaskIQ." +site_author = "Dima Anfimov" +site_url = "https://taskiq-python.github.io/taskiq-sqs/" + +repo_url = "https://github.com/taskiq-python/taskiq-sqs" +repo_name = "taskiq-python/taskiq-sqs" + +copyright = """ +Copyright © 2025-2026 Dima Anfimov +""" + +nav = [ + { "Overview" = "index.md" }, + { "Tutorial" = [ + "tutorial/priority_queues.md", + "tutorial/reliable_task_delivery.md", + "tutorial/large_payloads_with_s3.md", + ]}, + { "Contributing" = "contributing.md" }, +] + +use_directory_urls = true + +[project.theme] +language = "en" + +features = [ + "content.code.annotate", + "content.code.copy", + "content.code.select", + "content.tabs.link", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.instant.prefetch", + "navigation.path", + "navigation.sections", + "navigation.top", + "navigation.tracking", + "search.highlight", +] + +[project.theme.icon] +repo = "fontawesome/brands/github" + +# Palette toggle for automatic mode +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to light mode" + +# Palette toggle for light mode +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +primary = "indigo" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +# Palette toggle for dark mode +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +primary = "indigo" +toggle.icon = "lucide/moon" +toggle.name = "Switch to system preference" + +[project.theme.font] +text = "Inter" +code = "Jetbrains Mono" + +[project.markdown_extensions.attr_list] +[project.markdown_extensions.admonition] +[project.markdown_extensions.pymdownx.snippets] +[project.markdown_extensions.pymdownx.superfences] +[project.markdown_extensions.pymdownx.details] + +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true From c47cf285d0ed8d4c73d1b6a85f5121d7555f8522 Mon Sep 17 00:00:00 2001 From: Dima Anfimov Date: Wed, 16 Sep 2026 00:35:30 +0200 Subject: [PATCH 2/3] docs: add SEO optimization --- docs/overrides/main.html | 41 ++++++++++++++++++++++++++++++++++++++++ docs/robots.txt | 4 ++++ zensical.toml | 4 ++++ 3 files changed, 49 insertions(+) create mode 100644 docs/overrides/main.html create mode 100644 docs/robots.txt diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 0000000..aa3aaad --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{#- + One title, reused for the tab, Open Graph and Twitter cards, so they can't drift apart. The homepage carries + the tagline, since "taskiq-sqs" alone says nothing about SQS, S3 or TaskIQ. +-#} +{% macro seo_title() -%} + {%- if page and not page.url and config.extra.site_tagline -%} + {{ config.site_name }} — {{ config.extra.site_tagline }} + {%- elif page and page.meta and page.meta.title -%} + {{ page.meta.title }} - {{ config.site_name }} + {%- elif page and page.title -%} + {{ page.title | striptags }} - {{ config.site_name }} + {%- else -%} + {{ config.site_name }} + {%- endif -%} +{%- endmacro %} + +{% macro seo_description() -%} + {%- if page and page.meta and page.meta.description -%} + {{ page.meta.description }} + {%- else -%} + {{ config.site_description }} + {%- endif -%} +{%- endmacro %} + +{% block htmltitle %} + {{ seo_title() }} +{% endblock %} + +{% block extrahead %} + + + + + + + + + +{% endblock %} diff --git a/docs/robots.txt b/docs/robots.txt new file mode 100644 index 0000000..d6e19f5 --- /dev/null +++ b/docs/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://taskiq-python.github.io/taskiq-sqs/sitemap.xml diff --git a/zensical.toml b/zensical.toml index 263e956..acc91e2 100644 --- a/zensical.toml +++ b/zensical.toml @@ -23,8 +23,12 @@ nav = [ use_directory_urls = true +[project.extra] +site_tagline = "SQS broker and S3 result backend for TaskIQ" + [project.theme] language = "en" +custom_dir = "docs/overrides" features = [ "content.code.annotate", From eacd021c67955be16e73cc0e703e19b08ca1ad57 Mon Sep 17 00:00:00 2001 From: Dima Anfimov Date: Wed, 16 Sep 2026 00:45:56 +0200 Subject: [PATCH 3/3] docs: add per-page description --- docs/contributing.md | 3 + docs/overrides/main.html | 41 -------- docs/tutorial/large_payloads_with_s3.md | 3 + docs/tutorial/priority_queues.md | 3 + docs/tutorial/reliable_task_delivery.md | 3 + overrides/main.html | 134 ++++++++++++++++++++++++ zensical.toml | 2 +- 7 files changed, 147 insertions(+), 42 deletions(-) delete mode 100644 docs/overrides/main.html create mode 100644 overrides/main.html diff --git a/docs/contributing.md b/docs/contributing.md index 1a0a10f..1cc59fa 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,5 +1,8 @@ --- title: Contributing +description: >- + Set up taskiq-sqs locally with uv, run lint and tests against the ministack AWS emulator, and + build the docs with zensical. --- taskiq-sqs follows the same contribution philosophy as the rest of the taskiq ecosystem — see the diff --git a/docs/overrides/main.html b/docs/overrides/main.html deleted file mode 100644 index aa3aaad..0000000 --- a/docs/overrides/main.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "base.html" %} - -{#- - One title, reused for the tab, Open Graph and Twitter cards, so they can't drift apart. The homepage carries - the tagline, since "taskiq-sqs" alone says nothing about SQS, S3 or TaskIQ. --#} -{% macro seo_title() -%} - {%- if page and not page.url and config.extra.site_tagline -%} - {{ config.site_name }} — {{ config.extra.site_tagline }} - {%- elif page and page.meta and page.meta.title -%} - {{ page.meta.title }} - {{ config.site_name }} - {%- elif page and page.title -%} - {{ page.title | striptags }} - {{ config.site_name }} - {%- else -%} - {{ config.site_name }} - {%- endif -%} -{%- endmacro %} - -{% macro seo_description() -%} - {%- if page and page.meta and page.meta.description -%} - {{ page.meta.description }} - {%- else -%} - {{ config.site_description }} - {%- endif -%} -{%- endmacro %} - -{% block htmltitle %} - {{ seo_title() }} -{% endblock %} - -{% block extrahead %} - - - - - - - - - -{% endblock %} diff --git a/docs/tutorial/large_payloads_with_s3.md b/docs/tutorial/large_payloads_with_s3.md index 93b4325..6d11bb9 100644 --- a/docs/tutorial/large_payloads_with_s3.md +++ b/docs/tutorial/large_payloads_with_s3.md @@ -1,5 +1,8 @@ --- title: Large payloads with S3 +description: >- + Offload SQS messages past the 256 KiB limit to S3 with S3OffloadMiddleware, and pair it with + S3ResultBackend for the result. --- SQS messages are capped at 256 KiB, which real workloads can outgrow quickly — a document to summarize, a batch of diff --git a/docs/tutorial/priority_queues.md b/docs/tutorial/priority_queues.md index 3ffc5ad..cdbf51c 100644 --- a/docs/tutorial/priority_queues.md +++ b/docs/tutorial/priority_queues.md @@ -1,5 +1,8 @@ --- title: Priority queues +description: >- + Run a batched low-priority queue and a FIFO urgent queue on the same taskiq-sqs broker, + worked example included. --- A single [`SQSBroker`](../index.md) can consume from several queues at once, so you don't need to run separate diff --git a/docs/tutorial/reliable_task_delivery.md b/docs/tutorial/reliable_task_delivery.md index 9112a7f..0039cc9 100644 --- a/docs/tutorial/reliable_task_delivery.md +++ b/docs/tutorial/reliable_task_delivery.md @@ -1,5 +1,8 @@ --- title: Reliable task delivery +description: >- + Combine delayed tasks and message expiration on taskiq-sqs for a retry-then-give-up pattern + with no extra infrastructure. --- [Delayed tasks](../index.md#delayed-tasks) and [message expiration](../index.md#message-expiration) are two diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..5fb5523 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,134 @@ +{% extends "base.html" %} + +{#- + 404.html has no real `page` (`page.ancestors` and `page.toc` are its only + keys) — `page.url` is empty on both the homepage and 404, but only a real + page ever gets a `canonical_url`, so that's what tells them apart. +-#} +{% set is_real_page = page and page.canonical_url %} +{% set is_homepage = is_real_page and page.canonical_url == config.site_url %} + +{#- + One title, reused for the tab, Open Graph and Twitter cards, so they can't drift apart. The homepage carries + the tagline, since "taskiq-sqs" alone says nothing about SQS, S3 or TaskIQ. +-#} +{% macro seo_title() -%} + {%- if is_homepage and config.extra.site_tagline -%} + {{ config.site_name }} — {{ config.extra.site_tagline }} + {%- elif page and page.meta and page.meta.title -%} + {{ page.meta.title }} - {{ config.site_name }} + {%- elif page and page.title -%} + {{ page.title | striptags }} - {{ config.site_name }} + {%- else -%} + {{ config.site_name }} + {%- endif -%} +{%- endmacro %} + +{% macro seo_description() -%} + {%- if page and page.meta and page.meta.description -%} + {{ page.meta.description }} + {%- else -%} + {{ config.site_description }} + {%- endif -%} +{%- endmacro %} + +{#- + A nav section (e.g. "Tutorial") has no page of its own, so its breadcrumb + step points at the first page underneath it. `.canonical_url` is only set + on pages, not sections, so that's what tells the two apart here. +-#} +{% macro node_url(node) -%} + {%- if node.canonical_url -%} + {{- node.canonical_url -}} + {%- elif node.children -%} + {%- set found = namespace(url="") -%} + {%- for child in node.children -%} + {%- if not found.url -%} + {%- set found.url = node_url(child) | trim -%} + {%- endif -%} + {%- endfor -%} + {{- found.url -}} + {%- endif -%} +{%- endmacro %} + +{% block htmltitle %} + {{ seo_title() }} +{% endblock %} + +{% block extrahead %} + + + + + + + + + + + {#- Breadcrumbs are the one rich result a small docs site reliably gets: + they replace the bare URL in the search snippet. -#} + {%- set home_url = config.site_url %} + {#- the page itself is the last step, so a section that merely opens on it + is not a step of its own -#} + {%- set initial_seen = [home_url, page.canonical_url] if page else [home_url] %} + {%- set acc = namespace(trail=[], seen=initial_seen) %} + {%- for ancestor in (page.ancestors if page else []) | reverse %} + {%- set url = node_url(ancestor) | trim %} + {%- if url and url not in acc.seen %} + {%- set acc.seen = acc.seen + [url] %} + {%- set acc.trail = acc.trail + [(ancestor.title, url)] %} + {%- endif %} + {%- endfor %} + {%- set trail = acc.trail %} + {%- if is_real_page and not is_homepage %} + + {%- endif %} + + {%- if is_homepage %} + + {%- endif %} +{% endblock %} diff --git a/zensical.toml b/zensical.toml index acc91e2..58d6d21 100644 --- a/zensical.toml +++ b/zensical.toml @@ -28,7 +28,7 @@ site_tagline = "SQS broker and S3 result backend for TaskIQ" [project.theme] language = "en" -custom_dir = "docs/overrides" +custom_dir = "overrides" features = [ "content.code.annotate",