Skip to content

Commit ff5579f

Browse files
Attakay78claude
andcommitted
docs: add full docstrings and fix sync task SSE notifications
Added Args/Returns docstrings to all public classes, methods, and functions. Fixed TaskStore._notify_change to use call_soon_threadsafe so task_log() entries from sync tasks trigger live dashboard updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2f723e2 commit ff5579f

20 files changed

Lines changed: 551 additions & 239 deletions

docs/api/task-log.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ from fastapi_taskflow import task_log
1010
def task_log(message: str) -> None
1111
```
1212

13-
Emits a timestamped log entry from within a running task. The entry is appended to the task's `logs` list and becomes immediately visible in the live dashboard under the **Logs** tab.
13+
Emits a timestamped log entry from within a running task. The entry is appended to the task's `logs` list and becomes immediately visible in the live dashboard under the **Logs** tab. Works in both sync and async task functions.
1414

1515
## Parameters
1616

docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## v0.4.1
4+
5+
- Added docstrings with `Args:` and `Returns:` descriptions to all public classes, methods, and functions across the codebase.
6+
- Fixed `TaskStore._notify_change` to use `call_soon_threadsafe` so `task_log()` entries from sync tasks trigger live dashboard updates correctly.
7+
8+
---
9+
310
## v0.4.0
411

512
### File logging

docs/guide/logging.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,19 @@ When a task retries, a `--- Retry N ---` separator is inserted automatically bet
2727
2026-04-07T10:00:01 Sending to user@example.com
2828
```
2929

30-
## Async tasks
30+
## Sync and async tasks
3131

32-
`task_log()` works the same in async tasks:
32+
`task_log()` works the same in both sync and async tasks. Sync tasks run in a
33+
thread pool, and log entries are safely handed back to the event loop so the
34+
dashboard updates in real time.
3335

3436
```python
37+
@task_manager.task(retries=1)
38+
def generate_report(user_id: int) -> None:
39+
task_log(f"Starting report for user {user_id}")
40+
data = fetch_data(user_id)
41+
task_log("Report complete")
42+
3543
@task_manager.task(retries=1)
3644
async def process_webhook(payload: dict) -> None:
3745
task_log(f"Received event: {payload['type']}")

fastapi_taskflow/__init__.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
"""
2-
fastapi-taskflow
3-
======================
1+
"""fastapi-taskflow: decorator-driven background tasks with retries and a live dashboard.
42
5-
A lightweight decorator-driven task manager that upgrades FastAPI
6-
``BackgroundTasks`` with retries, visibility, and SQLite persistence —
7-
without changing how developers enqueue tasks.
3+
Upgrades FastAPI ``BackgroundTasks`` with retries, status tracking, and SQLite
4+
persistence without changing how you enqueue tasks.
85
96
Quick start::
107
11-
from fastapi_taskflow import TaskManager, TaskAdmin
8+
from fastapi import Depends, FastAPI
9+
from fastapi_taskflow import TaskAdmin, TaskManager
1210
1311
task_manager = TaskManager(snapshot_db="tasks.db")
1412
@@ -17,7 +15,7 @@ def send_email(address: str) -> None:
1715
...
1816
1917
app = FastAPI()
20-
TaskAdmin(app, task_manager) # mounts /tasks routes + lifecycle
18+
TaskAdmin(app, task_manager) # mounts /tasks routes and manages lifecycle
2119
2220
@app.post("/signup")
2321
def signup(email: str, tasks=Depends(task_manager.get_tasks)):

fastapi_taskflow/admin.py

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@
1818

1919

2020
class TaskAdmin:
21-
"""
22-
Mounts the task observability routes onto a FastAPI application and
23-
manages the snapshot scheduler lifecycle (if enabled on the TaskManager).
21+
"""Mounts task observability routes onto a FastAPI app and manages the scheduler lifecycle.
2422
25-
All work is done in the constructor — no need to keep a reference::
23+
All setup happens in the constructor, so you don't need to keep a reference::
2624
2725
task_manager = TaskManager(snapshot_db="tasks.db")
2826
TaskAdmin(app, task_manager)
@@ -32,18 +30,19 @@ class TaskAdmin:
3230
TaskAdmin(app, task_manager, auth=[("alice", "pw1"), ("bob", "pw2")])
3331
TaskAdmin(app, task_manager, auth=MyBackend(), secret_key="...", token_expiry=3600)
3432
35-
# Custom mount path:
33+
# Custom mount prefix:
3634
TaskAdmin(app, task_manager, path="/admin/tasks")
3735
38-
Routes exposed (relative to *path*):
36+
Routes mounted (relative to *path*):
3937
40-
* ``GET {path}`` — JSON list of all tasks
41-
* ``GET {path}/metrics`` — JSON aggregated statistics
42-
* ``GET {path}/{task_id}`` — JSON single task detail
43-
* ``GET {path}/dashboard`` — live dashboard (HTML)
44-
* ``GET {path}/auth/login`` — login page (when auth is configured)
45-
* ``POST {path}/auth/login`` — process login (when auth is configured)
46-
* ``GET {path}/auth/logout`` — logout (when auth is configured)
38+
* ``GET {path}`` -- JSON list of all tasks
39+
* ``GET {path}/metrics`` -- aggregated statistics
40+
* ``GET {path}/{task_id}`` -- single task detail
41+
* ``POST {path}/{task_id}/retry`` -- retry a failed/interrupted task
42+
* ``GET {path}/dashboard`` -- live HTML dashboard
43+
* ``GET {path}/auth/login`` -- login page (when *auth* is set)
44+
* ``POST {path}/auth/login`` -- process login form
45+
* ``GET {path}/auth/logout`` -- clear session cookie
4746
"""
4847

4948
def __init__(
@@ -58,6 +57,30 @@ def __init__(
5857
secret_key: str | None = None,
5958
poll_interval: float = 30.0,
6059
) -> None:
60+
"""
61+
Args:
62+
app: The FastAPI application to mount routes onto.
63+
task_manager: The :class:`~fastapi_taskflow.manager.TaskManager`
64+
whose tasks will be exposed.
65+
path: URL prefix for all mounted routes. Default is ``"/tasks"``.
66+
display_func_args: When ``True``, task arguments are included in
67+
the dashboard task list. Disable if args may contain sensitive data.
68+
auto_install: When ``True``, calls :meth:`~fastapi_taskflow.manager.TaskManager.install`
69+
on *app* so all ``BackgroundTasks`` routes receive managed injection
70+
automatically. Equivalent to calling ``task_manager.install(app)``
71+
before creating ``TaskAdmin``.
72+
auth: Enables login-protected access to the dashboard and API.
73+
Accepts a ``(username, password)`` tuple, a list of such tuples,
74+
or a :class:`~fastapi_taskflow.auth.TaskAuthBackend` instance for
75+
custom authentication logic. ``None`` (default) means no auth.
76+
token_expiry: Session token lifetime in seconds. Default is 86400 (24 hours).
77+
Only relevant when *auth* is set.
78+
secret_key: HMAC secret used to sign session tokens. A secure random
79+
key is generated automatically when *auth* is set and this is omitted.
80+
Pass an explicit value to keep sessions valid across restarts.
81+
poll_interval: How often (seconds) the dashboard polls for updates when
82+
SSE is unavailable. Default is 30 seconds.
83+
"""
6184
self._task_manager = task_manager
6285

6386
if auto_install:
@@ -110,6 +133,11 @@ def __init__(
110133
app.router.on_shutdown.append(self._close_file_logger)
111134

112135
async def _on_startup(self) -> None:
136+
"""Restore persisted task history and re-dispatch any pending tasks.
137+
138+
Registered as a FastAPI startup event handler when a snapshot backend
139+
is configured. Runs before the app begins accepting requests.
140+
"""
113141
scheduler = self._task_manager._scheduler
114142
assert scheduler is not None
115143
await scheduler.load()
@@ -118,6 +146,12 @@ async def _on_startup(self) -> None:
118146
scheduler.start()
119147

120148
async def _on_shutdown(self) -> None:
149+
"""Stop the background flush loop and persist any remaining tasks.
150+
151+
Registered as a FastAPI shutdown event handler. Flushes completed tasks
152+
to the backend, and if ``requeue_pending=True``, saves unfinished tasks
153+
so they can be re-dispatched on the next startup.
154+
"""
121155
scheduler = self._task_manager._scheduler
122156
assert scheduler is not None
123157
scheduler.stop()
@@ -126,5 +160,6 @@ async def _on_shutdown(self) -> None:
126160
await scheduler.flush_pending()
127161

128162
async def _close_file_logger(self) -> None:
163+
"""Flush and close file log handlers on shutdown."""
129164
if self._task_manager.file_logger is not None:
130165
self._task_manager.file_logger.close()

fastapi_taskflow/auth.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ def authenticate_user(self, username: str, password: str) -> bool:
8282
def resolve_backend(
8383
auth: Union[tuple, list, TaskAuthBackend, None],
8484
) -> TaskAuthBackend | None:
85+
"""Convert the *auth* parameter accepted by :class:`~fastapi_taskflow.admin.TaskAdmin`
86+
into a :class:`TaskAuthBackend`, or return ``None`` for no authentication.
87+
88+
Args:
89+
auth: A ``(username, password)`` tuple, a list of such tuples, an existing
90+
:class:`TaskAuthBackend` instance, or ``None``.
91+
92+
Returns:
93+
A :class:`TaskAuthBackend` instance, or ``None`` if *auth* is ``None``.
94+
95+
Raises:
96+
TypeError: If *auth* is not one of the accepted types.
97+
"""
8598
if auth is None:
8699
return None
87100
if isinstance(auth, TaskAuthBackend):
@@ -111,6 +124,15 @@ def _b64url_decode(s: str) -> bytes:
111124

112125

113126
def create_token(secret_key: str, expiry: int) -> str:
127+
"""Create a signed HMAC-SHA256 JWT with an expiry claim.
128+
129+
Args:
130+
secret_key: Secret used to sign the token.
131+
expiry: Token lifetime in seconds from now.
132+
133+
Returns:
134+
A dot-separated ``header.payload.signature`` token string.
135+
"""
114136
header = _b64url_encode(b'{"alg":"HS256","typ":"JWT"}')
115137
body = _b64url_encode(json.dumps({"exp": int(time.time()) + expiry}).encode())
116138
signing_input = f"{header}.{body}"
@@ -119,6 +141,17 @@ def create_token(secret_key: str, expiry: int) -> str:
119141

120142

121143
def verify_token(secret_key: str, token: str) -> bool:
144+
"""Verify a token created by :func:`create_token`.
145+
146+
Checks both the HMAC signature and the ``exp`` claim.
147+
148+
Args:
149+
secret_key: The same secret used when the token was created.
150+
token: The token string to verify.
151+
152+
Returns:
153+
``True`` if the signature is valid and the token has not expired.
154+
"""
122155
try:
123156
parts = token.split(".")
124157
if len(parts) != 3:
@@ -143,7 +176,15 @@ def verify_token(secret_key: str, token: str) -> bool:
143176

144177

145178
def make_api_guard(secret_key: str):
146-
"""Returns a FastAPI ``Depends`` that raises 401 for invalid/missing tokens."""
179+
"""Return a FastAPI ``Depends`` that rejects requests with invalid or missing tokens.
180+
181+
Args:
182+
secret_key: The secret used to verify the session token cookie.
183+
184+
Returns:
185+
A ``Depends`` instance that raises ``HTTP 401`` when the cookie is
186+
absent or the token signature/expiry check fails.
187+
"""
147188

148189
def _guard(request: Request) -> None:
149190
token = request.cookies.get(COOKIE_NAME, "")
@@ -285,6 +326,18 @@ def create_auth_router(
285326
token_expiry: int,
286327
prefix: str,
287328
) -> APIRouter:
329+
"""Build the login/logout router for the dashboard.
330+
331+
Args:
332+
backend: The auth backend used to validate credentials.
333+
secret_key: HMAC secret for signing session tokens.
334+
token_expiry: Token lifetime in seconds.
335+
prefix: URL prefix (e.g. ``"/tasks"``) prepended to all auth routes.
336+
337+
Returns:
338+
An :class:`~fastapi.APIRouter` with ``GET /auth/login``,
339+
``POST /auth/login``, and ``GET /auth/logout`` routes.
340+
"""
288341
router = APIRouter(prefix=prefix, include_in_schema=False)
289342
login_path = f"{prefix}/auth/login"
290343
dashboard_path = f"{prefix}/dashboard"

fastapi_taskflow/backends/__init__.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
"""
2-
Pluggable snapshot backends for fastapi-taskflow.
1+
"""Pluggable snapshot backends for fastapi-taskflow.
2+
3+
Built-in backends:
34
4-
Built-in backends
5-
-----------------
6-
* :class:`SqliteBackend` — zero-dependency SQLite (default)
7-
* :class:`RedisBackend` — requires ``redis[asyncio]`` (``pip install redis``)
5+
* :class:`SqliteBackend` -- zero-dependency, local SQLite file (default)
6+
* :class:`RedisBackend` -- shared Redis store; requires ``pip install "redis[asyncio]"``
87
9-
Custom backends
10-
---------------
11-
Subclass :class:`SnapshotBackend` and implement the four abstract methods::
8+
To write a custom backend, subclass :class:`SnapshotBackend` and implement
9+
its abstract methods::
1210
1311
from fastapi_taskflow.backends import SnapshotBackend
1412
from fastapi_taskflow.models import TaskRecord
1513
1614
class MyBackend(SnapshotBackend):
1715
async def save(self, records: list[TaskRecord]) -> int: ...
1816
async def load(self) -> list[TaskRecord]: ...
17+
async def save_pending(self, records: list[TaskRecord]) -> int: ...
18+
async def load_pending(self) -> list[TaskRecord]: ...
19+
async def clear_pending(self) -> None: ...
1920
async def close(self) -> None: ...
2021
2122
task_manager = TaskManager(snapshot_backend=MyBackend(...))

0 commit comments

Comments
 (0)