Changelog¶
All notable changes to tempest-fastapi-sdk are listed below.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.34.0] — 2026-06-04¶
Added¶
- Server-side session module — new
tempest_fastapi_sdk.sessionspackage ships a full alternative to the JWT auth flow. Public surface:SessionStoreProtocol +MemorySessionStore(dev/tests) +RedisSessionStore(production). Stores keep sessions by the SHA-256 hash of the cookie id; the plaintext only lives in the Set-Cookie. Redis store gets TTL eviction for free; both stores exposeget/set/delete/delete_by_user/list_by_user.Sessionschema —session_id(hashed),user_id,created_at,expires_at,last_seen_at,ip,user_agent,data(free-form JSON bag).SessionAuthservice — authenticates credentials viaPasswordUtils+UserModel, mints sessions, slides TTL on resolve, rotates on login (anti-fixation), revokes one or all sessions per user.SessionMiddleware— reads the cookie, populatesrequest.state.sessionso handlers never re-resolve.make_session_dependency(required=...)— FastAPI dependency returning the resolved session or raisingUnauthorizedExceptionwhen required.make_session_router(service, session_factory=..., prefix=...)— bundled 5-endpoint router:POST /auth/session/login,POST /auth/session/logout,GET /auth/session/me,GET /auth/session/list,DELETE /auth/session/{id}.SessionLoginSchema,SessionResponseSchema,SessionSummarySchema— typed DTOs for the router.
SessionSettingsmixin —SESSION_TTL_SECONDS,SESSION_SLIDING,SESSION_COOKIE_{NAME,DOMAIN,PATH,SECURE,HTTPONLY,SAMESITE},SESSION_ROTATE_ON_LOGIN. Every field carriestitle/description/examples.
Documentation¶
docs/recipes/sessions.{md,en.md}— new bilingual recipe with the JWT-vs-session decision table, setup wiring, endpoints, store comparison (Memory vs Redis vs custom), middleware semantics, security model (hash-at-rest, anti-fixation, SameSite, anti-enumeration, instant revocation) and when NOT to use sessions.docs/reference.md— newtempest_fastapi_sdk.sessionssection withmkdocstringsentries forSessionAuth,make_session_router,SessionMiddleware,make_session_dependency,SessionStore/MemorySessionStore/RedisSessionStore, every schema.mkdocs.yml— recipe added to the navigation in both languages with the matching i18n translation entry.
Tests¶
- 16 new cases under
tests/sessions/covering theMemorySessionStorelifecycle (set/get/expire/delete/list + user-scoped wipe),SessionAuth(authenticate / login / resolve+slide / rotate / revoke_all + anti-enumeration), and router integration (login sets cookie,mereturns session,mereturns 401 without cookie, logout clears cookie, list marks current).
Migration¶
- v0.34.0 is purely additive. No public-API breaking change. Existing JWT flows keep working untouched; sessions are a separate opt-in path.
[0.33.0] — 2026-06-04¶
Added¶
- WebSocket router (
tempest_fastapi_sdk.make_websocket_router+WebSocketHub). Newtempest_fastapi_sdk.websocketsmodule ships the three concerns every WebSocket endpoint has to get right:- Bearer auth at the handshake via
?token=<jwt>query string ORSec-WebSocket-Protocol: bearer,<jwt>subprotocol (preferred — does not leak to proxy logs). Thebearer_resolvercallable maps the token to a user UUID;Nonecloses the socket with code4401before the handler runs. - Heartbeat ping/pong with timeout. The router emits
{"type": "ping"}everyWS_HEARTBEAT_SECONDS(default30) and closes with code4408when the matching{"type": "pong"}does not arrive withinWS_HEARTBEAT_TIMEOUT_SECONDS(default60). - In-process registry (
WebSocketHub) tracking every live connection by user UUID + topic subscriptions. Exposessend_to(user_id, envelope),broadcast(envelope, topic=None),subscribe/unsubscribe,online_users()andconnection_count()— usable from any HTTP handler in the same FastAPI app. Per-user capWS_MAX_CONNECTIONS_PER_USER(default5) evicts the oldest connection with code4429when exceeded. Dead peers are evicted transparently onsend_jsonfailure.
- Bearer auth at the handshake via
WSEnvelopeschema — canonical{type, data, request_id}envelope for SDK-managed frames (ping/pong) and the recommended shape for application messages.WebSocketConnectiondataclass — public handle returned byWebSocketHub.registerso handlers can pass a stableconnection_idtosubscribe/unsubscribe.WebSocketSettingsmixin —WS_HEARTBEAT_SECONDS,WS_HEARTBEAT_TIMEOUT_SECONDS,WS_MAX_CONNECTIONS_PER_USER,WS_MAX_MESSAGE_BYTESwith fulltitle/description/examplesmetadata.
Documentation¶
docs/recipes/websocket.{md,en.md}— new bilingual recipe covering setup, query-vs-subprotocol auth comparison, JavaScript client snippet with heartbeat + reconnect, broadcast / send_to / topic patterns, every close-code table, settings reference, and the single-process vs multi-replica trade-offs.docs/reference.{md,en.md}— new sectiontempest_fastapi_sdk.websocketswithmkdocstringsentries forWebSocketHub,WebSocketConnection,make_websocket_router,WSEnvelope.mkdocs.yml— recipe added to the navigation in both languages with the matching i18n translation entry.
Migration¶
- v0.33.0 is purely additive. No public-API breaking change. The new module imports lazily; existing services that don't mount the router pay no startup cost.
[0.32.1] — 2026-06-04¶
Changed¶
- Top-level
__all__now re-exports the bundled auth surface. AddsUserAuthService,make_auth_router,BaseUserTokenModel,UserTokenPurpose,make_user_token_model,AuthSettings, every auth schema (SignupSchema/SignupResponseSchema/LoginSchema/LoginResponseSchema/ActivationToken/ActivationResponseSchema/PasswordResetToken/PasswordResetRequestSchema/PasswordResetResponseSchema/PasswordResetConfirmSchema) to the public re-export list. Runtime imports already worked; this satisfies strict re-export checkers (pyright/basedpyright/Pylance strict) without project-levelpyrightconfig.json.
Documentation¶
- Full audit + fix pass against the actual SDK code. Every recipe,
tutorial section, README block and learning-project example was
cross-checked against the source; whatever didn't match was rewritten.
Highlights of what changed:
docs/tutorial.{md,en.md}/README.md: router section now callscontroller.signup/controller.get_by_id/ a realcontroller.paginate(...)invocation — the previouscontroller.create/controller.get/controller.list_paginatednames did not exist onBaseControllerand would have raisedAttributeErroron every endpoint.page_sizeconsistently replaces the bogussizequery/JSON key throughout pagination snippets — the realBasePaginationFilterSchemafield ispage_sizeand thepaginate(...)dict returns"page_size", not"size".BaseUserModelexamples no longer claim apassword_hashcolumn — the real column ishashed_passwordand the docs now construct rows with it.docs/recipes/testing.{md,en.md}rewritten:async with TestClient(app)(which doesn't work —TestClientis sync) replaced byhttpx.AsyncClient(transport=ASGITransport(app=app));test_database/test_session/create_test_enginesignatures and return shapes corrected to match the helpers actually shipped undertempest_fastapi_sdk.testing.docs/recipes/security.{md,en.md}fully rewritten: every claim pointed at fictional API (RedisThrottleBackend,MemoryThrottleBackend,ThrottleStatus.LOCKED,throttle.check(),throttle.record_failure(),.attempts_left,set_cookie(key=..., same_site=SameSite.LAX),get_client_ip(..., trusted_proxies={...}),accept_private=False, an HMAC-pepper claim onhash_opaque_token(..., secret=...), aReferrer-PolicyinDEFAULT_STATIC_SECURITY_HEADERS). New recipe documents the realAttemptThrottle/ThrottleStatus/set_cookie/clear_cookie/get_client_ipsurface.docs/recipes/http.{md,en.md}:JWT_TTL_HOURS(does not exist) replaced byJWT_ACCESS_TTL_SECONDS;RSAWebhookSignatureVerifier(encoding="base64", hash_algorithm="sha256")(also fictional) rewritten to the realalgorithm="sha256"kwarg;request.client.host or "anon"rewritten to handlerequest.client is Nonesafely;controller.list_paginatedreplaced with a realcontroller.paginate(...)call.docs/recipes/auth-flow.{md,en.md}:SignupResponseSchemaexample body now matches the real shape (user_id/activation_required/activation_url/access_token/refresh_token— no fictionalemail/is_activefields);tempest db initprereq is called out beforetempest db revision; UUID example replaces the bogus ULID-style placeholder.docs/recipes/uploads.{md,en.md}: phantomsettings.UPLOAD_BACKENDfield (it isn't onUploadSettings) now has to be declared on the project's ownSettingssubclass in a copy-pasteable snippet;UploadFile.filename(typedstr | None) is now fallen back to"upload.bin"before being passed wherestris required; theUploadUtils.__init__mkdir side-effect is explicitly called out.docs/recipes/metrics.{md,en.md}no longer stops atMetricsUtils— a full Prometheus exposition section was added coveringPrometheusMiddleware+make_prometheus_registry+make_prometheus_router, the[prometheus]extra, scrape config, and the rationale for not mounting the JSON snapshot on/metrics.docs/recipes/queue-tasks.{md,en.md}:NameErrorin the outbox dispatcher fixed (broker/queue_brokershadowing that would crash on import).docs/recipes/realtime.{md,en.md}: missingStreamingResponseimport added; producer pattern rewritten to cancel on client disconnect (the previous fire-and-forget pattern leaked tasks).docs/recipes/admin.{md,en.md}:settings.ADMIN_SECRET_KEY(doesn't exist) replaced with the scaffold'ssettings.JWT_SECRET;__tablename__ = "user"replaced with the scaffold's actual"users".docs/recipes/database.{md,en.md}:filters={"deleted_at": None}(silently skipped — returns deleted rows) replaced with a rawselect(...).where(col.is_(None))query; the tuple comparison(col_a, col_b) > (val_a, val_b)(invalid in SQLAlchemy) replaced withtuple_(col_a, col_b) > tuple_(...); the cursor example now decodesstate["value"]back todatetimeso Postgres tuple comparison doesn't fail on a str-vs-timestamp clash; missingselect/AsyncSession/Anyimports added.docs/recipes/cli.{md,en.md}: default--extrasvalue corrected fromauthto the realauth,admin;--model myapp.models.user:Userexample renamed toUserModelwith a comment explaining theBaseUserModelsubclass requirement.docs/recipes/cache.{md,en.md}: the previously free-floatingawait cache.connect()is now shown inside a real@asynccontextmanagerlifespan — without itcache.clientraisesRuntimeErroron first use.docs/recipes/logging.{md,en.md}: malformed"2026-05-16T20:14:33.412+00:00Z"timestamp (impossible — the formatter strips+00:00and appendsZ) corrected to"2026-05-16T20:14:33.412Z".docs/recipes/br-helpers.{md,en.md}:request.json()(a coroutine in FastAPI/Starlette) nowawait-ed inside anasync defhandler with theRequestimport included.docs/recipes/storage.{md,en.md}: stale "v0.24.0 will introduceS3Backend" promise replaced with a pointer to the uploads recipe whereMinIOUploadStoragealready lives.docs/architecture.{md,en.md}:paginate(...)row in the BaseService table now listspage_sizeinstead ofsize; a note next toUserController(UserService(UserRepository(session)))explains the requiredBaseRepositorysubclass withmodel=UserModel.docs/installation.{md,en.md}: version pins bumped from>=0.19.0to>=0.32.0;tempest user createexample now passes the required--emailflag instead of relying on a non-existent prompt.README.md: SDK version pin in the pyproject snippet bumped from>=0.13.1to>=0.32.0; all the same router / pagination / model-field fixes applied.
Migration¶
- Zero breaking changes — this is a pure documentation + re-export audit on top of the v0.32.0 surface.
[0.32.0] — 2026-06-04¶
Added¶
- Backend-only auth mode (
AuthSettings.AUTH_BACKEND_LINKS=True). When enabled,make_auth_routermounts three extra HTML endpoints on top of the JSON ones already exposed — a project can run the full signup → activate → reset cycle without any frontend route handling tokens. New endpoints:GET /auth/activate/{token}— consumes the activation token and renders an HTML success page (or an error page on bad / expired / used tokens).GET /auth/password-reset/{token}— peeks the token (does NOT consume it) and renders an HTML form with the new-password input + confirmation.POST /auth/password-reset/{token}(application/x-www-form-urlencoded) — processes the form, validates the password floor, confirms the reset, and renders success or error HTML.
- Five new bundled Jinja2 templates under
tempest_fastapi_sdk/auth/templates:activation_success.html,activation_error.html,password_reset_form.html,password_reset_success.html,password_reset_error.html. All responsive, inline-styled, mobile-friendly. Shadow them by dropping same-named files into thetemplate_diryou pass tomake_auth_router. make_auth_router(template_dir=...)parameter — point the router at a project-owned directory whose templates override the bundled defaults. Only consulted whenAUTH_BACKEND_LINKS=True.UserAuthService.peek_token(session, token, purpose)— new service method that validates a token and returns the(token_record, user)pair without markingused_at. Used byGET /auth/password-reset/{token}to render the form before the user actually submits.AuthSettingsgains six fields documenting the new flow:AUTH_BACKEND_LINKS: bool(defaultFalse)AUTH_LOGIN_URL: str | None(defaultNone) — URL for the "Go to login" button rendered on success/error pagesAUTH_ACTIVATION_SUCCESS_TEMPLATE/AUTH_ACTIVATION_ERROR_TEMPLATEAUTH_PASSWORD_RESET_FORM_TEMPLATE/AUTH_PASSWORD_RESET_SUCCESS_TEMPLATE/AUTH_PASSWORD_RESET_ERROR_TEMPLATE
tempest_fastapi_sdk.auth.page_renderer.render_auth_page— standalone Jinja2 renderer reused by the router; doesn't requireEmailUtilsto be wired (only the[email]extra for Jinja2 itself).
Changed¶
make_auth_routersignature now accepts the optionaltemplate_dir: str | None = Nonekeyword. Existing call sites remain source-compatible.- All JSON endpoints (
POST /auth/signup,POST /auth/activate/{token},POST /auth/login,POST /auth/password-reset/request,POST /auth/password-reset/confirm) stay mounted exactly as in v0.31.x — Backend-only Mode E is purely additive.
Documentation¶
docs/recipes/auth-flow.{md,en.md}gains the new Mode E (backend-only) section:.envblock, Mermaid sequence diagram of the activation flow, bundled-template reference table, override walkthrough, trade-offs callout (zero frontend dep, no JWT auto-delivery, requires[email]extra). The "Four operating modes" section was renamed to "Five operating modes" and the TOC entry updated.
Migration¶
- v0.32.0 has no breaking changes. To opt into backend-only
mode, flip
AUTH_BACKEND_LINKS=Truein the.envand updateAUTH_ACTIVATION_URL_TEMPLATE/AUTH_PASSWORD_RESET_URL_TEMPLATEto point at your backend instead of your frontend. Everything else is wired automatically.
[0.31.4] — 2026-06-04¶
Changed¶
- Explicit re-exports across
settings/,auth/, anddb/__init__.py. Every symbol is now re-exported using the PEP 484from x import Y as Yform in addition to__all__. Reason: third-party consumers run a mixed bag of type-checkers (mypy, pyright, pylance, basedpyright) at different strictness levels and without project-awarepyrightconfig.json. Either form alone is theoretically PEP 484 compliant, but basedpyright and Pylance strict still flag barefrom foo import Barinside__init__.pyas "private import usage" unless the symbol is aliased withas Bar. Pairing the two patterns silences every IDE with no project-level config required. No behavior change — same runtime imports, same public surface, same wheel contents.
Documentation¶
- Auth-flow recipe rewritten end-to-end (
docs/recipes/auth-flow.{md,en.md}):- New table of contents at the top.
- New "Email anatomy" section disambiguating the three concepts that confused readers (opaque token vs URL template vs Jinja2 template) with a Mermaid sequence diagram of the full flow.
- "Operating modes" expanded from three to four explicit
modes (A. production / B. dev with local SMTP / C. dev without
SMTP / D. CI), each with a copy-paste
.envblock. - New "Mailhog vs smtp4dev" comparison table + ready-to-use
docker-compose.yamlsnippets for both containers — the recipe now covers local SMTP interception out of the box. - "Customizing templates" rewritten with clearer prose, the full
context-variable table, and a copy-paste minimal
emails/activation.htmlexample.
CLAUDE.mdgains a new "Explicit re-exports in every__init__.py" rule documenting the dualas Y+__all__pattern and flagging bare re-exports as a structural defect.
[0.31.3] — 2026-06-04¶
Documentation¶
- Comprehensive documentation refresh to reflect the v0.23.0 → v0.31.2 surface. No code change.
README.md: extras table now lists[http]+[prometheus]; module map coversBaseUserTokenModel,UserTokenPurpose,BASE_COLUMN_ORDER,reorder_base_columns_first,compose_hooks,AuthSettings,tempest_fastapi_sdk.auth,utils.http_client,utils.storage_backends. Roadmap section rewritten with every shipped release v0.23.0 → v0.31.2.docs/index.{md,en.md}: module map updated with the new exports (auth,storage,IdempotencyMiddleware,BodySizeLimitMiddleware,CSRFMiddleware,PrometheusMiddleware, OAuth clients,HTTPClient, bulk repo ops). Hero paragraph rewritten to mention the new layers.docs/installation.{md,en.md}: extras table now lists[minio],[http],[prometheus]; CLI section documentstempest generate,tempest db <subcommand>,tempest user <subcommand>.docs/tutorial.{md,en.md}: new "Auth flow already ships" admonition pointing readers at the bundledUserAuthService+make_auth_routershortcut.docs/recipes/auth-flow.{md,en.md}(new): full PT-BR + EN-US recipe covering the bundled signup / activate / login / password-reset flow —UserTokenModelconcretization, settings flags, the three operating modes (production, dev without SMTP, CI tests), template overrides, security guarantees.docs/roadmap.{md,en.md}: full rewrite with Tier S / A / B status tables (Status + Where columns), every release detailed v0.23.0 → v0.31.2, "What's next" section for v0.32.0+ (OpenTelemetry) and v0.33.0+ (outbox).docs/reference.{md,en.md}:mkdocstringsentries added forBodySizeLimitMiddleware,CSRFMiddleware, OAuth clients,PrometheusMiddleware+make_prometheus_router,HTTPClientRetryPolicy+CircuitOpenError, the fulltempest_fastapi_sdk.authmodule (service, router, schemas),reorder_base_columns_first,compose_hooks.docs/learning/marketplace/index.{md,en.md}: stack table now points atUserAuthService+make_auth_routeras the default auth path,BaseRepository.bulk_create_values/bulk_upsertfor stock seed,PrometheusMiddleware,BodySizeLimitMiddleware, OAuth clients,CSRFMiddleware,HTTPClient. Implementation order's step 1 rewritten to use the bundled auth recipe.mkdocs.yml:Auth flow (signup/reset)entry added to nav + i18n translation map.CLAUDE.md: "What the SDK currently covers" section rewritten as a structured category bullet list (Auth, DB, Observability, HTTP layer, Pagination, Settings, SSE, Throttle, Upload, MinIO/S3, Email, WebPush, Cache, Queue/tasks, BR validators, Admin panel, CLI).
[0.31.2] — 2026-06-04¶
Changed¶
UserAuthServicemethod signatures now typesessionassqlalchemy.ext.asyncio.AsyncSessioninstead ofAny. All seven public methods (signup,activate,login,request_password_reset,confirm_password_reset,_issue_token,_consume_token) declare the real type so mypy + IDE autocomplete can flag wrong shapes at the call site. No behavior change — only the annotations tightened. Aligned with the v0.25.1 "avoidAnyin SDK code" rule that the auth module had drifted away from when it landed in v0.31.0.
[0.31.1] — 2026-06-04¶
Changed¶
ActivationTokenandPasswordResetTokenare now PydanticBaseSchemasubclasses instead ofdataclassinstances. Keeps the auth module aligned with the SDK's gold-standard DTO convention — every "thing returned to the caller" is a Pydantic schema with fulltitle/description/examplesmetadata. The fields are the same; the constructor signature is the same. Callers that destructure via attribute access (activation.token,activation.url) keep working unchanged.- Both token schemas moved from
tempest_fastapi_sdk.auth.servicetotempest_fastapi_sdk.auth.schemas— re-exports at the package root are unchanged, so existing imports (from tempest_fastapi_sdk import ActivationToken) keep resolving. - Every auth schema now carries a thorough class-level
docstring describing the flow that uses it, the meaning of
each attribute, and the security / behavior contract (e.g.
why
PasswordResetResponseSchema.messageis always identical, whyActivationToken.tokenis only handed back once).
[0.31.0] — 2026-06-04¶
Added¶
-
Bundled auth flow — new
tempest_fastapi_sdk.authmodule ships service + router + schemas + templates so signup, activation, login, and password reset land end-to-end with a singleinclude_routercall: -
UserAuthService— generic over the concreteUserModelUserTokenModel. Methods:signup,activate,login,request_password_reset,confirm_password_reset,issue_jwt_pair. Every method accepts the activeAsyncSessionso the caller controls transaction boundaries.
make_auth_router(service, session_factory=…)mountsPOST /auth/signup,POST /auth/activate/{token},POST /auth/login,POST /auth/password-reset/request,POST /auth/password-reset/confirm.-
DTOs:
SignupSchema/SignupResponseSchema,LoginSchema/LoginResponseSchema,ActivationResponseSchema,PasswordResetRequestSchema/PasswordResetResponseSchema,PasswordResetConfirmSchema. Every field carriestitle/description/examplesper the SDK convention. -
BaseUserTokenModel(abstract) for one-shot activation / reset tokens, plusmake_user_token_model(user_table, …)for test fixtures. The plaintext token is returned exactly once; only the SHA-256 hash is persisted (via existinggenerate_opaque_token/hash_opaque_tokenhelpers). Tokens carry apurpose(UserTokenPurposeStrEnum:activation/password_reset/email_verification) -
expires_at+used_at. -
AuthSettingsmixin exposing every knob the bundled flow needs: -
AUTH_AUTO_ACTIVATE— skip activation email entirely (dev / CI mode); user is born active and the signup response carries JWTs immediately. AUTH_RETURN_TOKEN_IN_RESPONSE— surface the activation / reset link in the JSON body instead of (or in addition to) sending the email. Toggles automatically whenEmailUtilsisn't wired.AUTH_ACTIVATION_TTL_SECONDS(default 7d) /AUTH_PASSWORD_RESET_TTL_SECONDS(default 1h).AUTH_ACTIVATION_URL_TEMPLATE/AUTH_PASSWORD_RESET_URL_TEMPLATE— front-end URL skeleton with{token}placeholder.AUTH_ACTIVATION_TEMPLATE/AUTH_PASSWORD_RESET_TEMPLATE— Jinja2 template names.-
AUTH_PASSWORD_MIN_LENGTH(default 12). -
Default email templates bundled under
tempest_fastapi_sdk/auth/templates/activation.htmlandpassword_reset.html.EmailUtils.render_templatefalls back to the SDK directory when the caller-suppliedtemplate_dirdoesn't ship one with the same name, so the bundled flow renders out of the box. Override by placing a matching file in the project's template directory.
Changed¶
[email]extra now pullsemail-validatorso the PydanticEmailStrfields used bySignupSchema/ login / reset DTOs validate without a separate dependency.EmailUtils.render_templatenow accepts callers without an explicittemplate_dir— the SDK's bundled auth templates are reachable by default.
Security¶
- Password-reset request endpoint always returns 202 and a generic message. Probing emails can no longer enumerate account existence.
- Activation + reset tokens are stored hashed (SHA-256, 48
bytes of entropy on the plaintext). One-shot —
used_atprevents replay. TTL-bounded.
[0.30.3] — 2026-06-04¶
Fixed¶
-
Noisy lint output after
tempest db revision. The post-write hooks ran in the wrong order —ruff_fixfirst,ruff_formatsecond — so the linter loudly complained aboutW291(trailing whitespace in the docstring header) andE501(longsa.Columnlines) that the formatter would fix on the very next hook. The final file was correct but stdout looked like the revision failed. Two adjustments: -
Hooks reordered to
ruff_format, ruff_fixso the formatter wraps lines + strips whitespace before the linter sees them. - Both hooks pass
--quietso the "found N errors / N fixed / M remaining" preamble is suppressed when nothing actionable is left.
Existing projects: re-run tempest db init against an empty
alembic.ini to regenerate, or hand-edit
[post_write_hooks] to match the new layout.
[0.30.2] — 2026-06-04¶
Security¶
-
alembic.inino longer stamps the database URL. The generated ini ships withsqlalchemy.url =empty so credentials never enter version control. Both the SDKenv.pytemplate andAlembicHelper.configresolve the URL at runtime: -
db_urlpassed to the constructor or via--database-urlon the CLI. DATABASE_URLenv var (loaded from.env).src.core.settings.settings.DATABASE_URLin scaffolded projects.
When none of the three is set the env.py raises
RuntimeError("DATABASE_URL is empty. Set it on the
environment, in src/core/settings.py, or pass --database-url
to the CLI.") so missing config fails loudly instead of
silently connecting to whatever was left on the ini.
Migration for existing projects¶
Open alembic.ini and blank the sqlalchemy.url line:
Then rotate the leaked credential at the database (assume the
secret is compromised the moment it landed in a Git commit).
Append alembic.ini to a code-search / CI hook so the line
stays empty across future PRs.
If your alembic/env.py was older than v0.21.x and does not
import tempest_fastapi_sdk.db.alembic_hooks, rerun
tempest db init against an empty file to regenerate it, or
copy the new template from
tempest_fastapi_sdk/db/_alembic_templates/env.py.template.
[0.30.1] — 2026-06-04¶
Added¶
reorder_base_columns_firstAlembic hook intempest_fastapi_sdk.db.alembic_hooks. Wired into the scaffoldedenv.pyso every autogenerated migration emitsid→is_active→created_at→updated_atat the top of everyop.create_table, followed by the table's own constraints + subclass columns in their original relative order. The 4BaseModelcolumns now ship in the documented order without manual editing.compose_hooks(*hooks)helper for chaining multipleprocess_revision_directivescallables.BASE_COLUMN_ORDERtuple re-exported at the package root for tools that want to mirror the convention elsewhere.
Docs¶
- Existing projects: copy the new
env.pysnippet fromtempest_fastapi_sdk/db/_alembic_templates/env.py.template(theprocess_revision_directives=reorder_base_columns_firstargument is added to bothcontext.configurecalls), or re-runtempest db initagainst an emptyalembic.inito regenerate. Futuretempest db revision --autogeneratepicks up the hook automatically.
[0.30.0] — 2026-06-04¶
Added¶
-
tempest dbsubcommand group — Alembic wrapper backed by the existingAlembicHelper. Commands: -
tempest db init— scaffoldalembic.ini+alembic/env.py. tempest db revision -m "<msg>" [--manual]— create a new migration (autogenerate by default).tempest db upgrade [target]— apply pending migrations (headby default).tempest db downgrade [target]— roll back (default-1, i.e. one step).tempest db current— print the applied revision.tempest db history [-v]— list revisions newest → oldest.
DATABASE_URL resolves in this order: --database-url flag
→ DATABASE_URL env var →
src.core.settings.settings.DATABASE_URL →
sqlalchemy.url from alembic.ini. The async driver
suffix is stripped automatically before Alembic runs.
-
tempest usersubcommand group — seed and inspect users via the project's concreteUserModel(defaultsrc.db.models:UserModel, overridable with--model). Bootstraps the first admin row so/adminlogin works without manual SQL. -
tempest user create --email X --password Y [--admin]— insert one user. Omitting--passwordreads it interactively (no shell history leak). Password ≥ 8 chars enforced. tempest user list [--admin]— printid email +admin/... active/inactiveper row.
Docs¶
docs/recipes/cli{,.en}.mdadds full sections fortempest dbandtempest userwith flag reference + theDATABASE_URLresolution order.docs/learning/marketplace/index{,.en}.mdsetup block now runstempest db revision+tempest db upgrade+tempest user create --adminbetween the docker compose up and theuv run python main.pyso/adminlogin works on first run.README.mdCommand-line interface recipe grows the same two sections.
[0.29.1] — 2026-06-04¶
Fixed¶
-
Scaffold no longer ships an empty
usertable — the scaffoldedsrc/db/models/__init__.pywas empty, so Alembic's--autogeneratefound no models inBaseModel.metadataand never emitted theusertable. The result:/adminlogin failed because the table didn't exist. The fix: -
New
src/db/models/user.py.tmplships a concreteUserModel(BaseUserModel)mapped to theuserstable. src/db/models/__init__.py.tmplre-exportsBaseModelUserModelso Alembic sees the metadata.
src/api/app.py.tmplnow wiresAdminSite+AdminModel(UserModel)+UserModelAuthBackend+make_admin_routerout of the box.tempest newdefault extras bumped fromauthtoauth,adminso the admin wiring boots without a manual extras tweak.
Upgrade path for an already-scaffolded project: copy the new
UserModel definition into src/db/models/user.py,
re-export from src/db/models/__init__.py, then run
uv run alembic revision --autogenerate -m "user table"
followed by uv run alembic upgrade head.
[0.29.0] — 2026-06-04¶
Fixed¶
- Postgres 18 mount path. v0.26.0 bumped the pinned image to
postgres:18-alpinebut kept the historicalpostgres-data:/var/lib/postgresql/datamount. Postgres 18+ reorganized the data layout — the image now refuses to start withError: in 18+, these Docker images are configured to store database data in a format which is compatible with "pg_ctlcluster" (...) Counter to that, there appears to be PostgreSQL data in: /var/lib/postgresql/data (unused mount/volume). The generator now emitspostgres-data:/var/lib/postgresql(no/datasuffix); Postgres creates the version-specific subdirectory inside.
Upgrade path for existing projects:
docker compose down -v # WIPES local data — back up first
tempest generate --docker --force
docker compose up -d
Added¶
CSRFMiddleware+make_csrf_token_dependency— full double-submit-cookie CSRF guard for cookie-authenticated endpoints. Unsafe verbs (POST/PUT/PATCH/DELETE) must carry both thecsrf_tokencookie and a matchingX-CSRF-Tokenheader; mismatch returns 403 with the SDK envelope{"code": "CSRF_VALIDATION_FAILED"}.
Safe methods always pass. exclude_paths lets bearer-auth
/api/ routes skip the check (JWT bearer is not subject to
CSRF since the browser doesn't auto-attach it).
generate_csrf_token(n_bytes=32) mints fresh tokens;
make_csrf_token_dependency() returns a FastAPI dependency
that the login/template endpoint can call to seed the cookie.
-
OAuth2 / OIDC providers under
tempest_fastapi_sdk.api.oauth: -
GoogleOAuthClient— Google identity, OIDC-compatible, default scopesopenid email profile. GitHubOAuthClient— GitHub OAuth (not OIDC; user info viaGET /user), default scopesread:user user:email.OIDCProvider— generic discovery-driven OIDC client for Auth0 / Keycloak / Okta / Microsoft Entra / Cognito. Pass the authorize / token / userinfo URLs explicitly.
All providers share the same surface — build_authorize_url(state, **extra),
exchange_code(code) -> OAuthTokens, fetch_user(tokens) -> OAuthUser.
Identity is normalized to OAuthUser(provider, subject, email,
name, picture, raw) so the application sees one shape
regardless of IdP. CSRF-grade state via generate_oauth_state().
Built on the v0.28.0 HTTPClient for retries + circuit-breaker
on the IdP — handy when Auth0 / Google occasionally hiccup.
Requires the [http] extra.
[0.28.0] — 2026-06-04¶
Added¶
-
Prometheus
/metricsendpoint + middleware. Newtempest_fastapi_sdk.api.routers.metricsmodule exposes: -
PrometheusMiddleware— trackshttp_requests_total{method, path, status}(Counter),http_request_duration_seconds{method, path}(Histogram),http_requests_in_progress{method}(Gauge). Uses the matched route template as thepathlabel so cardinality stays bounded. make_prometheus_registry()— freshCollectorRegistrydecoupled from the default singleton.make_prometheus_router(registry=…, path="/metrics", dependencies=…)—GET /metricsrendering the exposition format. Pair withDepends(require_x_token)in production.
Requires the new [prometheus] extra (prometheus-client).
-
HTTPClient— typed httpx wrapper attempest_fastapi_sdk.utils.http_client: -
Bounded retries with exponential backoff (
RetryPolicy(max_attempts, backoff_initial_seconds, backoff_max_seconds, retry_statuses)); retries on network errors + configurable 5xx/429. - Per-host circuit breaker — trips after
failure_thresholdconsecutive failures, half-open afterrecovery_seconds; raisesCircuitOpenErrorwhile open. X-Request-IDpropagation from therequest_id_ctxcontextvar to outbound requests so correlation flows downstream.- Verb-level conveniences (
get/post/put/patch/delete) on top of the unifiedrequest()core.
Requires the new [http] extra (httpx).
-
BodySizeLimitMiddleware— short-circuits oversized requests at the ASGI layer: -
Header check on
Content-Length(fast path). - Streaming check for chunked / unknown-length bodies.
exclude_pathslets specific routes (e.g. media uploads) opt out and enforce their own per-endpoint limit.-
Responds
413with the SDK envelope{"code": "REQUEST_BODY_TOO_LARGE", "details": {"max_bytes": …}}. -
BaseRepository.bulk_create_values(rows)— singleINSERT … VALUES (…), (…)round-trip for batch persistence without unit-of-work overhead. -
BaseRepository.bulk_upsert(rows, conflict_columns, update_columns=None)— dialect-awareINSERT … ON CONFLICT DO UPDATE. Picks Postgres or SQLite syntax automatically; raisesNotImplementedErroron other dialects so the caller can fall back to aSELECT FOR UPDATEloop.
Changed¶
[all]extra now includeshttpxandprometheus-client.
[0.27.0] — 2026-06-04¶
Added¶
-
New documentation section: Learning Projects (PT-BR + EN-US). Didactic projects built end-to-end on the SDK so users can learn how the primitives compose in a realistic scenario.
-
First learning project: 🛒 Marketplace — Mercado Livre / Shopee–style multi-tenant sales platform without external integrations. Covers the full SDK stack:
-
Business rules — every domain invariant numbered (U-01… G-04) with rationale. 41 rules across 10 sections.
- Domain model — UML class diagram, ER diagram, enum diagrams (Mermaid), per-entity invariant table, and entity → SDK primitive mapping.
- Critical flows — sequence diagrams for the 5 trickiest
flows (signup, member invitation, product creation with
images, idempotent checkout, shipping with SSE), plus state
machines for
OrderandInvitation. - Endpoint map — full REST surface as a table (method + path + auth role + idempotency + status + description).
Exercises: BaseUserModel, PasswordUtils, JWTUtils,
make_jwt_user_dependency, make_role_dependency,
BaseRepository[T], generate_opaque_token,
EmailUtils.render_template, UploadUtils +
MinIOUploadStorage, IdempotencyMiddleware,
EventStream / sse_response, AsyncTaskBrokerManager,
AsyncBrokerManager, AsyncRedisManager + @cached,
MetricsUtils, register_exception_handlers + the
AppException hierarchy, configure_logging +
make_logs_router.
Docs¶
docs/learning/index{,.en}.md— section index with the catalog of learning projects (Marketplace shipped; library, scheduling, recurring billing planned).docs/learning/marketplace/{index,business-rules,domain,flows,api}{,.en}.md— 10 new bilingual pages.mkdocs.ymladds the section to PT nav and the i18nnav_translationsblock (now 31 navigation elements, was 23).
[0.26.0] — 2026-05-31¶
Added¶
tempest generate --docker— regeneratedocker-compose.yaml(and refresh the.env.exampleservice block) in an existing project. Reads the project'spyproject.tomlto discover the currently pinned SDK extras unless--extrasis given explicitly. Refuses to overwrite a hand-edited compose file without--force. The.env.exampleaddendum is idempotent — re-running the command does not duplicate the service blocks.
Flags:
--docker— selects the compose generator.--path / -p— project root (default: cwd).--extras— override discovered extras.--name— override the container-name prefix.-
--force / -f— overwrite existing compose file. -
All Pydantic schemas and settings mixins now ship
title+description+examplesmetadata on every field. JSON-Schema consumers (FastAPI/docs,/redoc, IDE tooling,pydantic.model_json_schema()) render rich metadata out of the box; OpenAPI examples populate the Swagger UI examples picker without further configuration.
Surfaces covered:
settings.mixins— every*Settingsmixin (ServerSettings,LogSettings,DatabaseSettings,RedisSettings,RabbitMQSettings,JWTSettings,CORSSettings,EmailSettings,UploadSettings,TokenSettings,WebPushSettings,TaskIQSettings,MinIOSettings).schemas.pagination—BasePaginationFilterSchema,BasePaginationSchema,CursorPaginationFilterSchema,CursorPaginationSchema.schemas.response—BaseResponseSchema.schemas.logs—LogEntrySchema.webpush.schemas—WebPushKeysSchema,WebPushSubscriptionSchema,WebPushPayloadSchema.
Changed¶
-
docker-compose.yamlimage tags bumped to the current major releases on Docker Hub: -
postgres:16-alpine→postgres:18-alpine. Postgres 14+ has usedscram-sha-256by default; no client-side change required. redis:7-alpine→redis:8-alpine. Note Redis 8.0+ ships under a tri-license (RSALv2 / SSPLv1 / AGPLv3); the earlier<=7.2.4line was 3-Clause BSD. Internal use is unaffected; redistribution may need to pick a compatible license tier.rabbitmq:3-management-alpine→rabbitmq:4-management-alpine.RABBITMQ_DEFAULT_USER/RABBITMQ_DEFAULT_PASSremain functional;RABBITMQ_DEFAULT_VHOST=/made explicit in the rendered compose.
Per-service tightening:
redisnow boots with--appendonly yesso dev data survives container restarts;start_period: 5slets the healthcheck wait for the AOF rewrite path.rabbitmqhealthcheck usesrabbitmq-diagnostics -q ping(quiet) withstart_period: 30sto absorb the broker's cold boot.postgreshealthcheck gainsstart_period: 10s.
[0.25.1] — 2026-05-31¶
Changed¶
-
Tighter typing across the SDK's public surface —
Anyremoved from most signatures. The recent backend/protocol additions landed withAnyin places where a concrete type orProtocolis just as ergonomic: -
UploadStorage.write_stream(..., validator=…)andUploadUtils.save(..., storage=…)now accept the explicitContentValidator = Callable[[bytes], bool]andUploadStorage | Nonetypes. Mypy and IDEs can now flag wrong shapes instead of waving them through. RedisIdempotencyStore(client=…)takes a new_RedisLikeProtocol(asyncget(key)/set(key, value, ex)) so the cache is decoupled fromredis-pyfor type-checking while still accepting any compatible client.make_app_exception_handler/make_http_exception_handler/make_unhandled_exception_handlerreturn typedCallable[[Request, ExcT], Awaitable[JSONResponse]]aliases (AppExceptionHandler,HTTPExceptionHandler,UnhandledExceptionHandler).AsyncMinIOClient.__aexit__andObjectStat.rawannotate their real types (TracebackType | None,minio.datatypes.Object) viaTYPE_CHECKINGimports.EmailUtils._jinja_env,_aiofiles,_aiosmtplibuseModuleType | None/jinja2.Environment | Noneinstead ofAny.
The behavior is unchanged — only the types tightened, so callers see better autocomplete and downstream refactors stay honest.
Removed¶
tempest_fastapi_sdk.utils.storage_backends._stream_upload_filehelper (was private, unused).
[0.25.0] — 2026-05-31¶
Added¶
-
tempest newnow generates adocker-compose.yamlwired with only the supporting infrastructure the picked extras require. The mapping: -
[cache]→ Redis 7 (alpine) [queue]or[tasks]→ RabbitMQ 3 with the management UI exposed athttp://localhost:15672[minio]→ MinIO + a one-shot bootstrap container that creates theuploadsbucket[email]→ MailHog (catches outbound SMTP, UI athttp://localhost:8025)
Postgres is always wired (the SDK's DB primitives are core), so
every scaffolded project gets a one-command path to a real
database via docker compose up -d. The scaffolded .env keeps
SQLite as the default URL so the smoke run works without Docker.
-
.env.examplegains a service-aware addendum matching the same extras → environment variables. Picking--extras cache,miniowritesREDIS_URL,MINIO_ENDPOINT,MINIO_ACCESS_KEY(etc.) into.env.exampleso the developer can copy the file straight to.envand the service connects to the compose-spawned containers without further editing. -
tempest_fastapi_sdk.cli.docker_composemodule exposesgenerate(project_name, extras)andenv_block_for(extras)as public helpers. Both pin image tags to specific versions that are smoke-tested with the SDK; bumping the pins should go through the smoke suite before being released.
Changed¶
- The post-scaffold "Next steps" hint printed by
tempest newnow reminds the developer to rundocker compose up -dbeforeuv run python main.py.
[0.24.0] — 2026-05-31¶
Added¶
-
UploadUtilspluggable storage backends. NewUploadStorageprotocol undertempest_fastapi_sdk.utilswith two ready implementations: -
LocalUploadStorage(base_dir)— disk-backed, matches the historicalUploadUtilsbehavior. MinIOUploadStorage(client, bucket=None)— wraps theAsyncMinIOClientshipped in v0.23.0, requires the[minio]extra.
UploadUtils.save() now accepts an optional storage=
keyword. When provided, the validated upload is sent to the
backend instead of the local filesystem — validation pipeline
(extension / MIME / size / magic bytes / content_validator)
is identical for both targets. Calls without storage=
continue to write to upload_dir unchanged.
IdempotencyMiddlewareundertempest_fastapi_sdk.api.middlewares. Caches the full response forPOST/PUT/PATCH/DELETErequests keyed by(method, path, Idempotency-Key)so client retries don't re-execute the handler. Opt-in per request — endpoints without the header pass through.
Two stores ship out of the box:
MemoryIdempotencyStore— async-lock-guarded dict with TTL eviction. Single-replica only.RedisIdempotencyStore(client, prefix="idem:")— backed by an async Redis client. Required in multi-replica deployments.
Custom backends can implement the IdempotencyStore protocol.
EmailUtils.render_template(template_name, context). Optional Jinja2 template rendering for transactional emails. Passtemplate_dirat construction time, then callrender_templateto produce the HTML / text body fed intosend(). HTML autoescaping is enabled for.html/.htm/.xmltemplates so caller-supplied values can't break out into markup.
Changed¶
[email]extra now ships Jinja2 alongsideaiosmtplibsorender_templateworks without a separate[admin]dependency. Existing installs should re-pull the extra:pip install -U "tempest-fastapi-sdk[email]".
Fixed¶
tests/utils/test_lazy_extras.py::hide_modulefixture now fully restoressys.moduleson teardown. The previous version only saved entries matching the_hidetarget, so tests that reimported the wholetempest_fastapi_sdkpackage leaked the freshly-built class objects into later tests — surfaced aspytest.raises(...)failing to catch exceptions that were raised under a different (re-imported) class identity.
[0.23.0] — 2026-05-31¶
Added¶
- MinIO / S3 object storage module. New
tempest_fastapi_sdk.storagepackage exportingAsyncMinIOClientandObjectStat. Async-friendly facade over the officialminiopackage — every blocking call is wrapped inasyncio.to_thread, so the FastAPI event loop stays responsive while uploads/downloads run in the executor.
Operations covered:
- Buckets —
bucket_exists,ensure_bucket,list_buckets,remove_bucket. - Objects —
put_object(bytes or file-like),fput_object(from disk),get_object_bytes,fget_object,stream_object(chunked async iterator),stat_object,list_objects(prefix + recursion),remove_object,copy_object. - Presigned URLs —
presigned_get_url,presigned_put_urlwithtimedeltaexpiry.
The full synchronous minio.Minio client stays accessible via
the .client attribute when you need surface beyond the wrapper
(SSE-KMS, lifecycle XML, bucket replication, etc.).
-
MinIOSettingsmixin undertempest_fastapi_sdk.settings, exposingMINIO_ENDPOINT,MINIO_ACCESS_KEY,MINIO_SECRET_KEY,MINIO_SECURE,MINIO_REGION,MINIO_DEFAULT_BUCKET. Re-exported at the package root. -
New
[minio]extra —pip install "tempest-fastapi-sdk[minio]". Theminiopackage is lazy-loaded atAsyncMinIOClient.__init__so projects without storage don't pay the import cost.
Docs¶
- New
docs/recipes/storage{,.en}.mdrecipe with end-to-end examples:UploadFileupload, streaming download, presigned upload (direct from browser), presigned download, prefix listing, copy/move. docs/reference.mdadds entries forAsyncMinIOClientandObjectStat.
[0.22.1] — 2026-05-31¶
Fixed¶
- File-descriptor leak in
configure_logging. Callingconfigure_loggingtwice (normal in tests, and in any service that supports hot-reload) removed the previous file handlers without closing them. After ~100 reconfigure cycles the kernel refused new file opens withOSError: [Errno 24] Too many open files. The previous handlers are nowclose()-d beforeremoveHandlerso each call releases its FDs.
Security¶
UploadUtils.delete()now refuses paths outsideupload_dir. Before,utils.delete("/etc/passwd")would happilyunlink()whatever the caller passed. Any service that forwarded a user-supplied filename todelete()was effectively giving the caller anrmprimitive bounded only by process permissions. The method now resolves the input againstupload_dir, treats relative inputs as relative toupload_dir, and raisesInvalidFileTypeException(withreason="escapes upload_dir") for anything that escapes — including absolute paths to unrelated directories and..-style traversal.
Callers that already passed paths returned by
UploadUtils.save() keep working unchanged; only path-traversal
attempts begin to raise.
Changed¶
make_app_exception_handlerdocstring now accurately states thatlog_leveldefaults tologging.INFO(the previous text claimedlogging.ERROR, which is what the function was originally intended to do but never matched the signature).
[0.22.0] — 2026-05-31¶
Changed¶
-
configure_loggingnow writes to stdout andlogs/by default. Before, file logging only activated when the caller passedlog_dir=...; the default scaffold often forgot it and the service ran with no on-disk audit trail. The new defaults: -
log_dirdefault is now"logs"(wasNone). - New
stdout: bool = Trueflag controls the terminal handler. - New
file_output: bool = Trueflag controls the per-level +500.logfile handlers. - Passing
stdout=False, file_output=FalseraisesValueError— that combination silences every handler and is almost always a mistake.
Backwards compatibility: passing log_dir=None (the old sentinel
for "no files") still works and produces stdout-only behavior.
Test suites that don't want logs/ files in cwd should now pass
file_output=False explicitly.
LogUtils(name=..., level=..., json_output=...)andLogUtils.configure(...)forward the same three flags (log_dir,stdout,file_output) so the imperative wrapper stays aligned withconfigure_logging.
Migration¶
If your service was relying on configure_logging(level=..., json_output=...)
to be stdout-only, either:
# Option A — opt out explicitly:
configure_logging(level="INFO", file_output=False)
# Option B — keep stdout-only via the old sentinel:
configure_logging(level="INFO", log_dir=None)
If your test suite spins up configure_logging or LogUtils
in-process, pass file_output=False to avoid stray logs/
folders in the working directory.
[0.21.3] — 2026-05-31¶
Changed¶
-
AppExceptionand 4xxHTTPExceptionnow emit anINFO-level log line. Before, both paths were silent — the response went out with the SDK envelope but operators saw nothing in stdout orinfo.logfor a401,404,422(etc.), making "API returned 4xx but I see no trace" debugging painful. The new behavior: -
AppExceptionwithstatus_code < 500→INFOlog, no traceback, no500.logmarker. AppExceptionwithstatus_code >= 500→log_level(defaultERROR) + traceback +HTTP_500_MARKERso the record lands in500.log.HTTPException4xx →INFOlog, no traceback. 5xx behavior unchanged (already logged atlog_level+ 500.log).- Unhandled
Exceptioncatch-all unchanged.
The log line includes the request method, path, status code,
exception code (for AppException) and request id — enough to
grep for in info.log without paging the operator at 3am.
Added¶
make_app_exception_handler(*, log_level)factory exposed viatempest_fastapi_sdk.api.handlersand re-exported at the package root. The existingapp_exception_handlercallable is kept as a thin wrapper for backwards compatibility.
[0.21.2] — 2026-05-31¶
Fixed¶
- Alembic wiped
configure_loggingduring lifespan. The stockalembic/env.py(generated byalembic init) ends withfileConfig(config.config_file_name). That call defaults todisable_existing_loggers=TrueAND honors[logger_root]inalembic.ini, which the SDK was writing aslevel = WARN, handlers = stderr. WhenAlembicHelper.upgrade()ran duringlifespan, every SDK logger configured viaconfigure_logginggot disabled and root was reset to WARN + stderr. The 500 catch-all handler kept building responses but nothing reached stdout,error.logor500.log— operators saw the 500 in the browser and zero log lines.
Two-part fix shipped in the SDK templates:
-
env.py.template— the call is now guarded on the presence of a[loggers]section AND usesdisable_existing_loggers=False, so it never wipes the host's logging tree: -
AlembicHelper.init()stops emitting the[loggers]/[handlers]/[formatters]/[logger_*]/[handler_*]/[formatter_*]sections into the generatedalembic.ini. Alembic's own loggers inherit from root (which the host configures), and the guardedfileConfigabove no-ops when no[loggers]section exists.
Upgrade path for existing projects: re-run tempest new --force
(or AlembicHelper.init) to regenerate the templates, OR
manually patch alembic/env.py to wrap the fileConfig call as
shown above and remove the [loggers]/[handlers]/[formatters]
blocks from alembic.ini.
[0.21.1] — 2026-05-31¶
Fixed¶
raise HTTPException(500, ...)bypassed the SDK 500 logger. Starlette intercepts everyHTTPExceptioninside its ownExceptionMiddlewareand routes it to a default handler that emits a bareJSONResponse({"detail": exc.detail})with no log entry. The 0.21.0 catch-allExceptionhandler never saw those raises, sotempest-fastapi-sdk[0.21.0]users hitting a 5xx endpoint reportedInternal Server Errorin the browser with zero output in stdout /error.log/500.log.
Added a third handler — make_http_exception_handler registered
for starlette.exceptions.HTTPException — that:
- logs every 5xx (
status_code >= 500) at ERROR withexc_info=excandHTTP_500_MARKERso the record lands in botherror.logand the dedicated500.log; - returns the SDK envelope (
detail/code/details), preserving the original status code and any custom headers, so frontends consuming the same envelope acrossAppExceptionand rawHTTPExceptiondon't need to branch; - leaves 4xx HTTPExceptions untouched (Starlette's default body and no log) since those represent normal client outcomes.
make_http_exception_handler and the existing log_traceback
/ log_level knobs on register_exception_handlers are wired
end-to-end; opt out of the trace with
register_exception_handlers(app, log_traceback=False) when an
APM is already capturing the stack.
[0.21.0] — 2026-05-31¶
Added¶
-
File logging to a
logs/directory + a/logsreader endpoint.configure_logginggained alog_dirparameter. When set (the scaffold defaults it to"logs"viaLOG_DIR), the stdout handler is kept and one JSON file per level is written —debug.log,info.log,warning.log,error.log,critical.log— each receiving only its own level (exact match, neverlevel >=). A dedicated500.logcaptures only uncaught-500 records: the catch-all exception handler now flags them withHTTP_500_MARKER, so grave failures are isolated and never buried. A 500 therefore appears in botherror.logand500.log. File handlers always emit JSON regardless ofjson_output. -
make_logs_router— a paginated, filterable, authenticated log reader.GET /logsreads the on-disk JSON files and returns aBasePaginationSchema[LogEntrySchema](newest first). Query params:source(all| each level |500),q(message substring),start/end(ISO-8601 range),page,page_size. Gated by a shared-secretX-Tokenheader viamake_token_dependency— an emptyTOKEN_SECRETdisables the check (dev only). New exports:make_logs_router,LogSource,LogEntrySchema. Thecreate_appscaffold wires the router and passeslog_dir/token_secretautomatically.
Fixed¶
tempest fixnow always formats, even when lint violations remain.run_ruff_fixranruff check --fixand short-circuited on its exit code before reachingruff format. Butruff check --fixexits non-zero whenever any residual violation it cannot autofix is left (an over-length string/comment, an undefined name, …) — so a single unfixable line silently skipped the formatter for the whole file, leaving long code lines un-wrapped and extra blank lines intact. The formatter now runs unconditionally; the lint exit code is still surfaced afterwards so CI keeps failing on the leftover issues.
Note: ruff format (and therefore tempest fix) never wraps long
string literals or comments — this matches Black. Those E501
lines stay and must be shortened by hand or silenced with
# noqa: E501.
- Autogenerated migrations are now lint-clean out of the box.
AlembicHelper.init()writes a[post_write_hooks]block into the generatedalembic.inithat runsruff check --fixfollowed byruff formaton every freshly created revision file. Previously the files Alembic emits failedtempest lint(ruff check) withW291(trailing whitespace in the docstring header whendown_revisionisNone—Revises:) andE501(over-lengthsa.Column(...)lines):
W291 Trailing whitespace
--> alembic/versions/...add_todos_table.py:4:9
E501 Line too long (120 > 88)
--> alembic/versions/...add_todos_table.py:30:89
The hooks resolve the project's own ruff configuration, so every
selected rule that is autofixable (I, UP, W, E501, …) is
cleared at generation time. Requires ruff on PATH — already a
dev dependency in every tempest new scaffold.
[0.20.0] — 2026-05-31¶
Changed¶
- BREAKING — pagination uses
page_sizeeverywhere instead ofsize. The field onBasePaginationFilterSchemawas namedsize(default10) while the controller / service / repository keyword argument was namedpage_size(default20), forcing every consumer to rename the attribute on the way through:
# before — required renaming + a default-value gotcha
result = await controller.paginate(
filters=f.get_conditions(),
page=f.page,
page_size=f.size,
...,
)
Aligned the request schema, the response envelope and the repository return dict on a single name + default:
BasePaginationFilterSchema.size→BasePaginationFilterSchema.page_size- Default
10→20 BasePaginationFilterSchema.get_conditions()strips["page", "page_size", "order_by", "ascending"]BasePaginationSchema.size→BasePaginationSchema.page_sizeBaseRepository.paginatereturn dict key"size"→"page_size".BaseService.paginateandBaseController.paginatepropagate the new key.build_pagination_link_header(size=..., size_param="size")→build_pagination_link_header(page_size=..., size_param="page_size"). URLs now look like?page=2&page_size=20by default. Passsize_param="size"to keep the old query-string spelling without renaming the function argument.
Migration: rename size to page_size on every consumer; if a
service relied on the previous default of 10 items per page,
pass page_size=10 explicitly. The admin router's _Pagination
helper now reads result["page_size"] from the repository
response.
[0.19.2] — 2026-05-31¶
Added¶
- Explicit
log_tracebackflag on the 500 catch-all handler. The default isTrue— every uncaught exception emits the full traceback vialogger.log(..., exc_info=exc)so the operator always has it. Setlog_traceback=Falseonly when an APM agent / Sentry / equivalent is already capturing the failure and the duplicated stack noise is unwanted. The flag is forwarded byregister_exception_handlersandmake_unhandled_exception_handler.
[0.19.1] — 2026-05-31¶
Fixed¶
- Unhandled exceptions returned a bare
Internal Server Errorstring with no log entry.register_exception_handlersonly wired a handler forAppException, so every uncaughtException(e.g.RuntimeError,KeyError, downstream library failures) fell through to Starlette's default — which writes nothing beyond the access line and returns a six-word body. Operators were left blind to real failures. -
Added a catch-all
Exceptionhandler that logs the full traceback at ERROR via thetempest_fastapi_sdk.api.handlerslogger (so the application'sLogUtils/configure_loggingsetup picks it up), attaches the activeX-Request-IDfor correlation, and returns the canonical SDK envelope: -
register_exception_handlers(app, include_traceback=True)embeds the formatted traceback underdetails.tracebackso development environments can surface the failure in the response body too. Production callers leave it off so module paths / SQL fragments / object reprs don't leak. register_exception_handlers(app, log_level=logging.WARNING)overrides the log level when needed.- Reads the request ID from the contextvar first, then falls
back to the
X-Request-IDheader —BaseHTTPMiddlewarespawns a child task so the contextvar set inRequestIDMiddleware.dispatchdoesn't always reach the handler. - New
make_unhandled_exception_handlerfactory exported fromtempest_fastapi_sdk.api.
Documentation¶
- Repository recipe in
docs/recipes/database.mdand the README Alembic walk-through still showed the deprecatedclass UserRepository(BaseRepository[UserModel]): model = UserModelDjango-style class-attribute pattern dropped in 0.16.0. Replaced with the constructor signaturesuper().__init__(session, model=UserModel).
[0.19.0] — 2026-05-30¶
Added¶
- MkDocs Material documentation site auto-deployed to GitHub
Pages at https://mauriciobenjamin700.github.io/tempest-fastapi-sdk/.
Sixteen pages total: landing, installation, architecture (with
Mermaid layering + request-lifecycle diagrams), the eleven-step
tutorial as one linear page, twelve thematic recipe pages
(Database, HTTP, Cache, Real-time, Queue & Tasks, Logging, Metrics,
Admin, Testing, CLI, Security, Brazilian helpers), an auto-generated
API Reference via
mkdocstrings, a migration guide, a contributing guide and the bundled CHANGELOG. - New
[docs]dependency group (mkdocs,mkdocs-material,mkdocstrings[python],pymdown-extensions,mkdocs-include-markdown-plugin) installed viauv sync --group docs. make docs-serve/make docs-build/make docsMakefile targets for local docs work (live reload athttp://127.0.0.1:8000)..github/workflows/docs.ymlpublishes the site to GitHub Pages on every push tomainthat touches docs, the package, the README or the CHANGELOG.- README now opens with a docs-site banner linking to Home / Tutorial / Recipes / API reference so readers landing on PyPI or GitHub reach the prose-rich version in one click.
[0.18.0] — 2026-05-30¶
Added¶
tempest fix— one-shot "organize the project" CLI command that runsruff check --fix <target>followed byruff format <target>. Sorts and dedupes imports, drops unused imports, normalizes string quotes to double, strips trailing whitespace, then normalizes indentation / line length / blank lines / trailing newlines. Pass--unsafeto also apply ruff's unsafe-fixes pass.py.typedmarker shipped inside the wheel so downstream mypy reads the SDK's inline type hints instead of bailing out withSkipping analyzing "tempest_fastapi_sdk": module is installed, but missing library stubs or py.typed marker. PEP 561-compliant.
[0.16.2] — 2026-05-30¶
Fixed¶
tempest new .still rejected the.shorthand when the cwd basename contained a hyphen. 0.16.1 special-cased.but then validated the derived name (Path.cwd().name) with the strict Python-identifier regex^[a-z][a-z0-9_]*$, so a real-world cwd liketodolist-apidied witherror: project name must match ^[a-z][a-z0-9_]*$. The derived name is now matched against a PEP 503 normalized distribution-name regex (^[a-z0-9](../[a-z0-9._-]*[a-z0-9])?$) — the same shape pyproject accepts under[project] name. Explicit names (tempest new myproject) keep the stricter Python-identifier rule because the string is also used as the package directory name.
Changed¶
tempest new(no positional argument) now defaults to.. Previously typer rejected the bare invocation withMissing argument 'NAME'. The default matches the scaffold-in-current-directory shape: runningtempest newinside an empty project directory writesmain.py/pyproject.toml/src//tests/directly under that directory. Pass an explicit name to keep the legacy "create a new subdir" behavior.
[0.16.1] — 2026-05-30¶
Fixed¶
- CLI required
jinja2even though[admin]was not installed. Importingtempest_fastapi_sdkeagerly walked intoadmin/router.py, which had a top-levelfrom fastapi.templating import Jinja2Templates. Starlette'stemplatingmodule raisesImportError("jinja2 must be installed to use Jinja2Templates")at import time, sotempest --version(or any other CLI command) blew up on environments that legitimately skipped the admin extra. The import is now deferred insidemake_admin_router, raising a clearInstall with pip install tempest-fastapi-sdk[admin]only when the router is actually constructed. tempest new .rejected the.shorthand for "scaffold here". The positionalnamewas always run through the Python-identifier regex before resolving the target, sotempest new .died witherror: project name must match ^[a-z][a-z0-9_]*$. The CLI now accepts.and treats it as "scaffold flatly in the current working directory"; the package name is derived from the cwd's basename (still validated).--pathis rejected alongside.because the target is unambiguous.
[0.16.0] — 2026-05-30¶
Repository and exception APIs join the admin in dropping Django-style class-attribute configuration. The constructor signature is now the contract; subclasses survive only when they add behavior (custom queries, except DomainError) — never to "fill in" a required class attribute.
Changed¶
- BREAKING —
BaseRepository.model/not_found_exceptionare constructor kwargs, not class attributes. Plain CRUD works without a subclass:
Subclasses kept around for custom queries forward both via super().__init__:
class UserRepository(BaseRepository[UserModel]):
def __init__(self, session: AsyncSession) -> None:
super().__init__(
session,
model=UserModel,
not_found_exception=UserNotFoundError,
not_found_message="Usuário não encontrado",
)
Replaces the previous class UserRepository(BaseRepository[UserModel]): model = UserModel; not_found_exception: ClassVar[...] = ... form. The synthesized _build_default_repository_class helper in admin/config.py is gone — AdminModel.build_repository now calls BaseRepository(session, model=self.model) directly.
- BREAKING —
AppException.codeis a plainstrclass attribute and is overridable at the raise site viacode=. Same forstatus_code=. Thecode: ClassVar[str]annotation is removed from every shipped subclass (NotFoundException,ConflictException,ForbiddenException,UnauthorizedException,ValidationException,TooManyRequestsException,InvalidTokenException,ExpiredTokenException,FileTooLargeException,InvalidFileTypeException). Subclasses still exist forisinstance/except DomainErrormatching; class-level defaults still work; constructor wins when both are present:
raise NotFoundException(
"Pedido não encontrado",
code="ORDER_NOT_FOUND",
details={"order_id": str(order_id)},
)
[0.15.0] — 2026-05-30¶
Admin configuration is now a plain typed instance instead of a Django-style subclass. The class form (class UserAdmin(AdminModel[UserModel]) with ClassVar attributes and the @site.register decorator) is gone — register a constructed instance instead. Field options accept real SQLAlchemy column attributes, so typos surface in the editor rather than at runtime.
Changed¶
- BREAKING —
AdminModelis an instance, not a subclass. Replace
@site.register
class UserAdmin(AdminModel[UserModel]):
model = UserModel
list_display: ClassVar[list[str]] = ["email", "is_admin"]
ordering = "-created_at"
with
site.register(AdminModel(
model=UserModel,
list_display=[UserModel.email, UserModel.is_admin],
ordering=desc(UserModel.created_at),
))
list_display, list_filter, search_fields, readonly_fields and identity_field accept SQLAlchemy column attributes (UserModel.email) or plain strings. ordering accepts a column (ascending), desc(column) / asc(column), or a "-field" string. AdminSite.register / get / require / iter_models now take and return instances. The @site.register decorator form is removed.
Added¶
FieldRef/OrderRef— public type aliases for the admin field- and ordering-reference unions, exported from the package root.
Fixed¶
- Admin list-view descending
orderingraisedAttributeError. A configured"-created_at"was passed verbatim topaginate(order_by=...), which didgetattr(model, "-created_at"). Ordering is now normalized to a(column, ascending)pair, so descending orders anddesc()/asc()wrappers work correctly.
[0.13.1] — 2026-05-30¶
Fixed¶
- PyPI wheel duplicate-filename rejection.
tool.hatch.build.targets.wheel.force-includewas double-listing the admin templates and static assets (already picked up by the default package scan), producing a wheel that PyPI rejected with400 Invalid distribution file. ZIP archive not accepted: Duplicate filename in local headers. Removed the redundant directives;admin/templates/andadmin/static/continue to be bundled by hatchling's default sdist/wheel rules.
[0.13.0] — 2026-05-30¶
Django-style admin site — Phase 1 (read-only). Mount under /admin so the database port can stay private; operators sign in with a user row owned by the application instead of a shared admin password.
Added¶
BaseUserModel— abstractBaseModelsubclass withemail(unique, lowercased),hashed_password,is_admin,last_login_at, plusset_password()/check_password()/normalize_email()helpers.AdminAuthBackendABC +UserModelAuthBackenddefault. Enforcesis_admin=Trueandis_active=True, stampslast_login_at, exposesprincipal_id/load_principal/display_nameso custom backends (LDAP, OAuth, external IAM) plug into the same flow.AdminSite— slug registry withregister/unregister/requireand decorator-style usage (@site.register).AdminModel[ModelT]— Django-flavored declarative configuration:list_display,list_filter,search_fields,readonly_fields,ordering,page_size,identity_field,verbose_name(_plural),repository_class. Auto-synthesizes a default repository when one is not supplied.make_admin_router— wires the HTML routes: login / logout / dashboard / list (paginated + search + filter) / detail (read-only) / static. Jinja2 templates + minimal admin.css ship with the wheel.SignedCookieSessionStore— itsdangerousTimestampSigner, signed HttpOnly + Secure + SameSite=Lax cookie scoped to the admin prefix, 8-hour default lifetime, per-session CSRF token.- New optional extra
[admin](jinja2,itsdangerous).
[0.11.0] — 2026-05-30¶
Added¶
BaseStrEnum/BaseIntEnum— shared enum bases undertempest_fastapi_sdk.core.enumswithvalues()/keys()/to_dict()helpers so str- and int-valued enums no longer need a per-project base class. Exported from the package root.
Changed¶
BaseService.map_to_responseis now async-aware. The base awaits the repository'smap_to_responseonly when it returns an awaitable (inspect.isawaitable), so concrete services with async mappers no longer need to override the read methods. Existing sync mappers keep working unchanged.
[0.10.0] — 2026-05-30¶
Security hardening primitives, hoisted from a downstream service so every project inherits the same defenses instead of re-rolling them.
Fixed¶
RateLimitMiddlewarekeyed on the transport peer behind a proxy. The default key wasrequest.client.host, which is the reverse-proxy IP once the app is fronted by one — collapsing every client into a single bucket (one abuser exhausts everyone's quota; the limit is effectively global). Addedtrusted_ip_header=so the key is the client IP resolved from a single edge-set header (e.g."x-real-ip"). Default behavior is unchanged (peer IP) for the no-proxy case.
Added¶
get_client_ip/get_client_ip_from_scope— spoof-resistant client IP resolution. Trusts only a single, explicitly named edge-set header (never the client-controlledX-Forwarded-For), falling back to the transport peer.AttemptThrottle+TooManyRequestsException(429) — a backend-agnostic fixed-window failure counter for login / OTP / code verification flows. Keyed by any string, counts only failures, raises a 429 withRetry-Afterwhen the budget is exhausted, and fails open on a backend outage. Works with any async Redis-like client (ThrottleBackendprotocol).generate_opaque_token/hash_opaque_token/verify_opaque_token— single-use opaque tokens hashed at rest (SHA-256) with constant-time verification, for password reset / email verification / magic links. Pure standard library.HardenedStaticFiles— aStaticFilessubclass that stampsX-Content-Type-Options: nosniff, a locked-downContent-Security-PolicyandCross-Origin-Resource-Policyon every response, so serving user-uploaded files can't become a stored-XSS vector. Headers configurable viaDEFAULT_STATIC_SECURITY_HEADERS.set_cookie/clear_cookie— secure-by-default cookie helpers (HttpOnly,Secure,SameSite) with matching set/clear flags so logout actually drops the cookie.RSAWebhookSignatureVerifier— asymmetric (RSA-SHA256/384/512) webhook signature verification for providers that sign with a private key and publish a public key (OpenPix/Woovi-style), complementing the existing HMACWebhookSignatureVerifier. Requirescryptography.
[0.9.0] — 2026-05-30¶
Added¶
UploadUtilsmagic-byte content verification. New opt-inverify_magic_bytes=Trueconstructor flag sniffs the first bytes of every upload and rejects content whose real type does not match its declaredContent-Type/ theallowed_mimetypesallow-list — closing the polyglot hole where an HTML+JS payload served asimage/jpegpassed the extension/MIME check. Recognizes JPEG, PNG, GIF, BMP, WebP and PDF.sniff_mime(prefix)helper (exported at the top level) — magic-byte MIME detector usable on its own to build customcontent_validatorpredicates.UploadUtils.save(..., content_validator=...)— optional predicate run on the first chunk; returningFalseaborts the save and removes the partial file before any further bytes are written.UploadUtils.save(..., filename=...)— explicit, deterministic final filename (e.g.f"{user_id}.jpg"), reduced to its basename and guarded against path traversal. Takes precedence overkeep_original_name.
All four additions are backwards compatible — existing UploadUtils calls
behave exactly as in 0.8.0 unless the new options are passed.
[0.8.0] — 2026-05-17¶
Breaking changes¶
ServerSettingsfield rename.HOST,PORTandDEBUGwere renamed toSERVER_HOST,SERVER_PORTandSERVER_DEBUG, and a newSERVER_RELOADfield was added.LOG_LEVELandLOG_JSONmoved out to a newLogSettingsmixin.- Migration: rename the matching env vars in every
.env/ deployment manifest and replacesettings.HOST/settings.PORT/settings.DEBUG/settings.LOG_LEVEL/settings.LOG_JSONaccordingly. MixLogSettingsintoSettingsif the service was relying onServerSettingsfor the log fields. - See the Migration guide 0.7 → 0.8 in the README for the full checklist.
Added — Settings mixins (Tier 1)¶
LogSettings(LOG_LEVEL,LOG_JSON) — extracted fromServerSettings.EmailSettings(SMTP_HOST,SMTP_PORT,SMTP_USERNAME,SMTP_PASSWORD,SMTP_FROM_ADDR,SMTP_USE_TLS,SMTP_USE_SSL,SMTP_TIMEOUT_SECONDS).UploadSettings(UPLOAD_DIR,UPLOAD_MAX_SIZE_BYTES,UPLOAD_ALLOWED_EXTENSIONS,UPLOAD_ALLOWED_MIMETYPES).TokenSettings(TOKEN_SECRET).WebPushSettings(VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY,VAPID_SUBJECT,WEBPUSH_DEFAULT_TTL_SECONDS).TaskIQSettings(TASKIQ_BROKER_URL,TASKIQ_RESULT_BACKEND_URL).
Added — API helpers (Tier 2)¶
tempest_fastapi_sdk.run_server(app, *, settings=None, host=None, port=None, reload=None, **uvicorn_kwargs)— canonicalsrc/server.pyentry point.make_bearer_token_dependency(tokens, soft=False, ...)—Authorization: Bearer <jwt>decoder returning the claims dict.make_jwt_user_dependency(tokens, user_loader, *, soft=False, subject_claim="sub", ...)— bearer + user loader in one factory.is_valid_cep,normalize_cep,CEP,CEP_PATTERN— Brazilian zipcode validators intempest_fastapi_sdk.utils.regex.
Added — Opt-in primitives (Tier 3)¶
tempest_fastapi_sdk.cache.cached(redis, ttl=300, key_prefix="", serializer=..., deserializer=..., skip_cache=...)— Redis-backed function cache decorator.make_tool_spec_router(spec, *, path="/tool-spec", tag="meta")—/tool-specmanifest router; accepts dict / sync / async providers.make_role_dependency(tokens, roles, *, require_all=False, ...)andmake_permission_dependency(tokens, permissions, *, require_all=True, ...)— JWT-claim-based authorization.
Added — Advanced primitives (Tier 4)¶
tempest_fastapi_sdk.WebhookSignatureVerifier(secret, *, algorithm, header_name, encoding, prefix)— HMAC webhook signature verification with FastAPI dependency factory.tempest_fastapi_sdk.RateLimitMiddleware(max_requests, window_seconds, key_func, exempt_paths)— in-process sliding-window rate limiter.build_pagination_link_header(base_url, *, page, size, pages, extra_params, page_param, size_param)— RFC 8288Linkheader builder for offset paginated responses.
Docs¶
- README — full reorganization, every new primitive has a recipe with full code samples. New sections: Periodic tasks scheduler, Programmatic server entry point, JWT bearer / current-user / role dependencies, CEP, Cache decorator, Tool-spec router, Webhook signature verification, Pagination Link headers, Rate limit middleware, Utility helpers, Outbox dispatcher pattern, Migration guide 0.7 → 0.8.
- Tutorial sections 1–11 realigned to the canonical layout mandated by
the SDK consumers' shared
CLAUDE.md(singlemain.pyone-liner,src/server.pyexposingrun(),src/api/app.pywithcreate_app(),src/db/repositories/location, mandatorysrc/controllers/pass-through,src/api/dependencies/package). - Reference section — method tables for
AsyncDatabaseManager,AsyncRedisManager,AsyncBrokerManager,AsyncTaskBrokerManagerandAsyncTaskScheduler.
Dev¶
- Added
uvicorn>=0.30.0to the dev dependency group sorun_servertests can monkey-patchuvicorn.run.
[0.7.3] — 2026-05-17¶
- Hardened request-ID middleware, SSE writer, web-push dispatcher and database manager lifecycle.
[0.7.2] — 2026-05-16¶
- Release packaging fix only.
[0.7.1] — 2026-05-16¶
Changed¶
- Optional extras (
[auth],[email],[upload],[cache],[webpush],[metrics],[queue],[tasks]) are now lazy-loaded at first instantiation, soimport tempest_fastapi_sdkworks when only a subset of extras is installed.
[0.7.0] — 2026-05-15¶
Added¶
LogUtils,configure_logging,JSONFormatter,RequestIDMiddlewareand therequest_id_ctxcontextvar.MetricsUtils(CPU / memory / disk / GPU snapshots).AsyncBrokerManager(FastStream wrapper,[queue]extra) andAsyncTaskBrokerManager(TaskIQ wrapper,[tasks]extra).
[0.6.0] — 2026-05-13¶
Added¶
- SSE primitives (
EventStream,ServerSentEvent,sse_response). - Web Push dispatch (
WebPushDispatcher,WebPushSubscriptionSchema,WebPushPayloadSchema,WebPushGoneError,[webpush]extra).
[0.5.0] — 2026-05-10¶
Added¶
AsyncRedisManager([cache]extra).- CORS helpers (
apply_cors,CORSSettings). - Composable settings mixins (
ServerSettings,DatabaseSettings,RedisSettings,RabbitMQSettings,JWTSettings,CORSSettings).
[0.4.0] — 2026-05-07¶
Added¶
make_health_router, audit / soft-delete mixins, cursor pagination.
[0.3.0] — 2026-05-04¶
Added¶
BaseController+BaseServicegenerics, DI scaffolding, logging,tempest_fastapi_sdk.testinghelpers.
[0.2.0]¶
Changed¶
- Drop Python 3.10 support; SDK now targets Python ≥ 3.11.
[0.1.0]¶
- Initial public release.