fastapi-m8 4.2.2__tar.gz → 4.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. fastapi_m8-4.4.0/.markdownlint.yaml +43 -0
  2. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/CHANGELOG.md +198 -0
  3. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/PKG-INFO +77 -6
  4. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/README.md +74 -3
  5. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/constraints-all.txt +1 -1
  6. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/constraints.txt +1 -1
  7. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/__init__.py +83 -6
  8. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_compat.py +39 -2
  9. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_version.py +1 -1
  10. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/pyproject.toml +2 -2
  11. fastapi_m8-4.4.0/tests/test_changelog_version_parity.py +41 -0
  12. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_compat.py +44 -1
  13. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_packaging.py +145 -1
  14. fastapi_m8-4.4.0/tests/test_public_typing.py +275 -0
  15. fastapi_m8-4.4.0/tests/test_version_source_parity.py +32 -0
  16. fastapi_m8-4.2.2/tests/test_public_typing.py +0 -136
  17. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.codacy.yml +0 -0
  18. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.dockerignore +0 -0
  19. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.env.example +0 -0
  20. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.gitattributes +0 -0
  21. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.github/FUNDING.yml +0 -0
  22. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.github/dependabot.yml +0 -0
  23. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.github/workflows/CI.yaml +0 -0
  24. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.github/workflows/PiPy.yml +0 -0
  25. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.gitignore +0 -0
  26. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.gitleaks.toml +0 -0
  27. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/.pydocstyle +0 -0
  28. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/AGENTS.md +0 -0
  29. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/CLAUDE.md +0 -0
  30. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/LICENSE +0 -0
  31. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/REPOSITORY_CONTEXT.md +0 -0
  32. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/SECURITY.md +0 -0
  33. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_api_key.py +0 -0
  34. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_app.py +0 -0
  35. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_async_stub.py +0 -0
  36. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_deps.py +0 -0
  37. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_engine.py +0 -0
  38. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_events.py +0 -0
  39. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_health.py +0 -0
  40. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_internal_auth.py +0 -0
  41. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_revocation.py +0 -0
  42. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/_route_audit.py +0 -0
  43. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/config.py +0 -0
  44. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/scripts/__init__.py +0 -0
  45. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/scripts/docker_start.sh +0 -0
  46. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/fastapi_m8/scripts/pre_start.py +0 -0
  47. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/__init__.py +0 -0
  48. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/conftest.py +0 -0
  49. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_api_key.py +0 -0
  50. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_api_key_deps.py +0 -0
  51. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_app.py +0 -0
  52. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_app_extra.py +0 -0
  53. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_async_stub.py +0 -0
  54. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_ci_policy.py +0 -0
  55. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_config.py +0 -0
  56. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_config_file_secrets.py +0 -0
  57. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_deps.py +0 -0
  58. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_engine.py +0 -0
  59. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_event_signing_gate.py +0 -0
  60. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_events.py +0 -0
  61. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_fixture_matrix_contract.py +0 -0
  62. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_health.py +0 -0
  63. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_host_header_routing.py +0 -0
  64. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_internal_auth.py +0 -0
  65. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_meta.py +0 -0
  66. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_pre_start.py +0 -0
  67. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_revocation.py +0 -0
  68. {fastapi_m8-4.2.2 → fastapi_m8-4.4.0}/tests/test_route_audit.py +0 -0
@@ -0,0 +1,43 @@
1
+ ---
2
+ # markdownlint configuration, also consumed by Codacy's markdownlint engine when
3
+ # the repository is set to use its own configuration files.
4
+ #
5
+ # Scope note: a config file supplies markdownlint's *defaults* for every rule it
6
+ # does not mention, so this file deliberately restates the rules this repository
7
+ # already does not enforce. Without them, adding this file would turn one
8
+ # reported rule into five and ~534 findings across README.md + CHANGELOG.md.
9
+
10
+ # MD024/no-duplicate-heading — restrict to sibling headings.
11
+ #
12
+ # CHANGELOG.md follows Keep a Changelog, where every release section repeats the
13
+ # same subsection names (### Added / ### Changed / ### Security / ### Docs …).
14
+ # Under the default setting that makes each release after the first a stack of
15
+ # duplicate-heading errors — 28 of them at 4.2.2, purely as a function of how
16
+ # many releases the file records, and growing by ~3 with every release. The
17
+ # duplication is the format, not a defect, and renaming the sections to be
18
+ # unique would break the convention every reader of this changelog relies on.
19
+ #
20
+ # siblings_only flags a duplicate only when it shares a parent heading, so
21
+ # "### Added" under [4.3.0] and "### Added" under [4.2.0] both pass, while two
22
+ # "### Added" blocks inside one release — a real editing mistake — still fail.
23
+ MD024:
24
+ siblings_only: true
25
+
26
+ # MD013/line-length — off. README.md and CHANGELOG.md wrap prose for readability
27
+ # rather than to a fixed column, and carry long table rows, URLs and code lines
28
+ # that cannot be wrapped at all (328 findings). Python line length is enforced
29
+ # separately and for real, by ruff (line-length = 88 in pyproject.toml).
30
+ MD013: false
31
+
32
+ # MD060/table-column-style — off. The documentation tables use compact pipes
33
+ # (194 findings); this is a presentation preference with no effect on rendering.
34
+ MD060: false
35
+
36
+ # The three below are off to preserve the status quo, NOT because they are
37
+ # wrong — each flags something worth fixing, and the count is small enough to
38
+ # fix in one pass: MD031 blanks-around-fences (7), MD040 fenced-code-language
39
+ # (3), MD028 no-blanks-blockquote (2). Left as a follow-up rather than folded
40
+ # into an unrelated release commit; re-enable once those 12 spots are cleaned.
41
+ MD031: false
42
+ MD040: false
43
+ MD028: false
@@ -5,6 +5,204 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · Versioning:
5
5
 
6
6
  ---
7
7
 
8
+ ## [4.4.0] — 2026-08-15 · Fail closed on a missing compatibility row; make the bare install importable
9
+
10
+ Two independent release-integrity fixes ship together.
11
+
12
+ `_assert_compat()` read its requirements with
13
+ `COMPAT_MATRIX.get(minor, {})`. A `fastapi-m8` minor with no row yielded an
14
+ empty dict, the validation loop ran zero times, and the function returned
15
+ **successfully** — so a release that forgot its row disabled the
16
+ `auth-sdk-m8` version check entirely, silently, for every consumer on that
17
+ minor. That is fail-**open** on an authorization-adjacent dependency guard.
18
+
19
+ Separately, the package's own minimal install had never been importable — see
20
+ *Fixed*, below.
21
+
22
+ ### Changed
23
+
24
+ - **`_assert_compat()` now fails closed.** An unlisted minor raises
25
+ `RuntimeError` naming the missing row (`"… has no COMPAT_MATRIX row for
26
+ minor 'X.Y' …"`) instead of booting unchecked. The behavior for a *listed*
27
+ minor is unchanged.
28
+
29
+ - **`auth-sdk-m8` floor raised to `>=3.1.3,<4.0.0`** (was `>=3.1.2`) in
30
+ `pyproject.toml`, `constraints.txt`/`constraints-all.txt` (pinned to
31
+ `3.1.3`), and `COMPAT_MATRIX["4.4"]`, updated in place to match — `3.1.3` is
32
+ published, dependency maintenance only (no new SDK API, no source change on
33
+ either side). Folded into this still-unpublished release rather than a
34
+ separate bump, per the same one-bump-per-unpublished-release rule the
35
+ *Notes* below already applies to the bare-install fix.
36
+
37
+ ### Added
38
+
39
+ - **`COMPAT_MATRIX["4.4"] = {"auth-sdk-m8": ">=3.1.3,<4.0.0"}`** — shipped in
40
+ the same commit as the fail-closed change, and load-bearing: without it this
41
+ release would be unbootable for every consumer resolving
42
+ `fastapi-m8>=4.3.0,<5.0.0`. No new SDK API is consumed; the floor tracks
43
+ `pyproject.toml`'s (`>=3.1.2` at the time this row was first added, raised to
44
+ `>=3.1.3` before publish — see the dependency-maintenance bullet above).
45
+ - **`tests/test_compat.py::test_assert_compat_fails_closed_on_unlisted_minor`**
46
+ — reproduces the boot failure on an unlisted minor.
47
+ - **`tests/test_compat.py::test_compat_matrix_current_minor_row_matches_pyproject_floor`**
48
+ — generalizes the existing `4.0`-specific row-vs-floor test to the *current*
49
+ minor, so a stale copy-pasted row can no longer pass.
50
+ `test_compat_matrix_40_row_matches_pyproject_floor` is retained: it also
51
+ backs the `4.0`-gate accept/reject tests.
52
+
53
+ ### Fixed
54
+
55
+ - **`pip install fastapi-m8` with no extras now produces an importable
56
+ package.** Since `4.2.1` — when the SDK re-export block landed — a no-extras
57
+ install succeeded and then `import fastapi_m8` raised
58
+ `ModuleNotFoundError: No module named 'sqlalchemy'`, from `__init__.py`'s
59
+ module-level `from auth_sdk_m8.controllers.base import BaseController`.
60
+ SQLAlchemy arrives only through the `db` extra, so the "minimal (no database)"
61
+ install the README documents had never worked. Reproduced against the
62
+ published wheels for `4.2.0`, `4.2.1`, `4.2.2` and `4.3.0` in a clean
63
+ `python:3.14-slim` container. **No consumer in the fleet was exposed**: all
64
+ three declare `fastapi-m8[db,postgres(,mysql)]`, so they resolve SQLAlchemy
65
+ through the extra.
66
+
67
+ `BaseController` and `TimestampMixin` — the only two re-exports that need the
68
+ `db` extra — are now resolved by a module-level `__getattr__` (PEP 562)
69
+ instead of at import time. Both remain in `__all__`, remain importable as
70
+ `from fastapi_m8 import BaseController`, and **keep their object identity**:
71
+ the accessor returns the SDK object itself and caches it in the module
72
+ globals, so ORM models mixing in the re-exported `TimestampMixin` register
73
+ byte-identical metadata. Touching either without the extra now raises a
74
+ `ModuleNotFoundError` naming `fastapi-m8[db]` rather than a bare
75
+ `No module named 'sqlalchemy'`.
76
+
77
+ Option (a) of three was chosen — deferring the imports, rather than moving
78
+ `sqlalchemy` into the base dependencies or withdrawing the minimal-install
79
+ claim — because it keeps the SDK import boundary intact without forcing an ORM
80
+ on a service that has no database. **PEP 562 moves the failure from
81
+ `import fastapi_m8` to first attribute access; for these two names that is
82
+ still the consumer's own module-import time** (`BaseController` is subclassed
83
+ and `TimestampMixin` is mixed in at class-definition time), so the deferral
84
+ does not push an `ImportError` into a request path. This is recorded in the
85
+ `__getattr__` docstring so the laziness is not later mistaken for a
86
+ runtime-path hazard.
87
+
88
+ - **`tests/test_packaging.py::test_package_imports_without_the_db_extra`** — a
89
+ no-extras clean-install probe alongside the existing one. The existing probe
90
+ installs `--no-deps` into a target dir and therefore runs against whatever the
91
+ development environment already has on `sys.path`, so it is structurally blind
92
+ to this class of defect; the new probe installs the same built wheel and runs
93
+ with `sqlalchemy`/`sqlmodel`/`alembic` made unimportable, asserting first that
94
+ the blocker really blocks. `tests/test_public_typing.py` gains four tests
95
+ covering lazy-lookup stability, the unknown-name `AttributeError`, and the
96
+ actionable missing-extra message.
97
+
98
+ ### Notes
99
+
100
+ - The bare-install fix and the `auth-sdk-m8 3.1.3` floor raise **ship no
101
+ version bump of their own**: both ride this same `4.4.0`, per the wave's
102
+ version-bump rule (one bump per unpublished release).
103
+ - `COMPAT_MATRIX` and `_assert_compat` exist in `fastapi-m8` only; a sweep of
104
+ the other nine Python repositories in the fleet found no second copy, so this
105
+ guard has exactly one implementation.
106
+ - Historical rows are **not** revised by this release. The `"4.2"` row states
107
+ `>=3.1.0` where `pyproject.toml` at the time declared `>=3.1.2`; the new
108
+ row-vs-floor test is scoped to the current minor, and whether historical rows
109
+ are corrected or grandfathered remains an open decision.
110
+
111
+ ---
112
+
113
+ ## [4.3.0] — 2026-08-14 · Complete the SDK re-export surface
114
+
115
+ Adds the five `auth-sdk-m8` primitives the consumer fleet still imports
116
+ directly, so a consumer service can depend on `fastapi-m8` alone and reach
117
+ **zero** `auth_sdk_m8` imports. Additive only: no existing export, signature or
118
+ behavior changes, and no new `auth-sdk-m8` API is required.
119
+
120
+ Until this release the boundary was literally unsatisfiable — five of the twelve
121
+ symbols the fleet imports had no `fastapi_m8` re-export, so every consumer had
122
+ to keep a direct SDK import or invent a local shim.
123
+
124
+ ### Added
125
+
126
+ - **`has_minimum_role`** (from `auth_sdk_m8.authorization`) — the canonical
127
+ role-ordering predicate. One implementation of the hierarchy, re-exported
128
+ rather than re-derived at a call site.
129
+ - **`RoleType`** (from `auth_sdk_m8.schemas.base`) — the role enum those
130
+ thresholds are expressed in, and the argument type of
131
+ `AuthDeps.require_role()`.
132
+ - **`ValidationConstants`** (from `auth_sdk_m8.schemas.shared`) — the shared
133
+ field-length/format constants consumer schemas validate against.
134
+ - **`make_scrape_credential_guard`** (from `auth_sdk_m8.security.guards`) — the
135
+ `/metrics` scrape-credential guard factory.
136
+ - **`REGISTRY`** (from `auth_sdk_m8.observability.metrics`) — the shared
137
+ Prometheus collector registry that `render_metrics` (already re-exported)
138
+ renders.
139
+
140
+ All five are documented in the module docstring's *Reusable SDK primitives*
141
+ block and listed in `__all__`; `has_superuser_privileges`, `BaseController`,
142
+ `ResponseModelBase`, `ResponseMessage`, `TimestampMixin`, `UserModel`,
143
+ `find_dotenv` and `render_metrics` were already re-exported and are unchanged.
144
+
145
+ ### Changed
146
+
147
+ - `COMPAT_MATRIX` gains its `"4.3"` row (`auth-sdk-m8 >=3.1.2,<4.0.0`). Without
148
+ it `_assert_compat()` would find no requirement for the new minor and silently
149
+ skip the startup check. The floor is stated as `>=3.1.2` — the floor
150
+ `pyproject.toml` already declares — rather than `4.2`'s looser `>=3.1.0`.
151
+
152
+ ### Documentation
153
+
154
+ - `README.md` gains a **Reusable SDK Primitives** section listing every
155
+ re-exported symbol, its SDK origin and its purpose, and states the boundary
156
+ rule: a consumer service imports these from `fastapi-m8`, never from
157
+ `auth-sdk-m8`. The database example now imports `TimestampMixin` from
158
+ `fastapi_m8` accordingly, and the compatibility table gains the `4.3.0` row.
159
+
160
+ ### Tests
161
+
162
+ - `tests/test_public_typing.py` type-checks all twelve re-exported primitives
163
+ through the public surface and asserts each one **is** the SDK object it
164
+ claims to re-export (identity, not just importability).
165
+ - `tests/test_packaging.py`'s clean-install probe imports the five new names
166
+ from the built wheel, which is the acceptance condition for this release.
167
+
168
+ ---
169
+
170
+ ## [4.2.2] — 2026-07-31 · Dependency maintenance
171
+
172
+ Reconstructed from Git history (`ed259a1`, `0c3123b`) — the release shipped
173
+ without a changelog entry.
174
+
175
+ ### Changed
176
+
177
+ - `auth-sdk-m8` floor raised to `>=3.1.2,<4.0.0` in `pyproject.toml` and pinned
178
+ to `3.1.2` in the compiled constraints files. No source change.
179
+
180
+ ---
181
+
182
+ ## [4.2.1] — 2026-07-31 · Re-exports, Python floor, docs
183
+
184
+ Reconstructed from Git history (`86830ee` and the commits it bumped for:
185
+ `b105e01`, `23020fe`, `be5c382`, `a57811c`) — the release shipped without a
186
+ changelog entry.
187
+
188
+ ### Added
189
+
190
+ - Re-export of the reusable `auth-sdk-m8` primitives (`has_superuser_privileges`,
191
+ `BaseController`, `ResponseModelBase`, `ResponseMessage`, `TimestampMixin`,
192
+ `UserModel`, `find_dotenv`, `render_metrics`) — the first half of the surface
193
+ `4.3.0` completes.
194
+
195
+ ### Changed
196
+
197
+ - Python floor raised to `>=3.12`; truncated lockfiles regenerated.
198
+
199
+ ### Documentation
200
+
201
+ - Documented hybrid mode's expiry-bounded revocation contract; reformatted the
202
+ embedded Python snippets in `CHANGELOG.md`/`README.md`.
203
+
204
+ ---
205
+
8
206
  ## [4.2.0] — 2026-07-23 · Role-capability demonstration surface (Phase 7)
9
207
 
10
208
  Adds a centralized `require_role(required_role: RoleType)` JWT dependency
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: fastapi-m8
3
- Version: 4.2.2
3
+ Version: 4.4.0
4
4
  Summary: FastAPI application framework for m8 consumer microservices.
5
5
  Author-email: Eli Serra <e.serra173@gmail.com>
6
6
  License: Apache License
@@ -215,7 +215,7 @@ Classifier: Programming Language :: Python :: 3.13
215
215
  Classifier: Topic :: Software Development :: Libraries
216
216
  Requires-Python: >=3.12
217
217
  Requires-Dist: anyio>=4.0
218
- Requires-Dist: auth-sdk-m8[config,events,fastapi,observability,security]<4.0.0,>=3.1.2
218
+ Requires-Dist: auth-sdk-m8[config,events,fastapi,observability,security]<4.0.0,>=3.1.3
219
219
  Requires-Dist: fastapi>=0.136.3
220
220
  Requires-Dist: httpx>=0.27.0
221
221
  Requires-Dist: packaging>=24.0
@@ -284,6 +284,7 @@ boilerplate from every consumer service.
284
284
  - [build_auth_deps()](#build_auth_deps)
285
285
  - [create_db_engine()](#create_db_engine)
286
286
  - [Health Checks](#health-checks)
287
+ - [Reusable SDK Primitives](#reusable-sdk-primitives)
287
288
  7. [Authentication](#authentication)
288
289
  - [Token Modes](#token-modes)
289
290
  - [Role System](#role-system)
@@ -389,6 +390,20 @@ pip install "fastapi-m8[all]"
389
390
 
390
391
  **Runtime requirements:** Python 3.12+
391
392
 
393
+ **What the minimal install gives you (since 4.4.0):** everything except the two
394
+ SQLModel-backed re-exports. `create_app`, `build_auth_deps`,
395
+ `ConsumerServiceSettings`, the health building blocks, the API-key and event-stream
396
+ clients, and the extra-free SDK primitives (`RoleType`, `has_minimum_role`,
397
+ `UserModel`, `REGISTRY`, …) all work with no extras. `BaseController` and
398
+ `TimestampMixin` are resolved lazily and raise a `ModuleNotFoundError` naming
399
+ `fastapi-m8[db]` if you touch them without that extra — install `[db]` (plus a
400
+ driver) as soon as your service has a database.
401
+
402
+ > Before 4.4.0 the minimal install did not actually work: `import fastapi_m8`
403
+ > raised `ModuleNotFoundError: No module named 'sqlalchemy'` on any release from
404
+ > 4.2.1 onward, because those two names were imported at module level. If you are
405
+ > on 4.2.1–4.3.0 and want a database-free install, upgrade to 4.4.0.
406
+
392
407
  ---
393
408
 
394
409
  ## Quick Start
@@ -1162,6 +1177,53 @@ class RedisCheck:
1162
1177
  | `LENIENT` (default) | Any check is `fail` |
1163
1178
  | `STRICT` | Any check is `fail` or `unknown` |
1164
1179
 
1180
+ ### Reusable SDK Primitives
1181
+
1182
+ `fastapi-m8` already depends on `auth-sdk-m8` and pins its major range, so it
1183
+ re-exports the SDK primitives a consumer service actually needs. **Import them
1184
+ from `fastapi_m8`, never from `auth_sdk_m8`.** A consumer service that imports
1185
+ the SDK directly takes on a second dependency whose major range it does not
1186
+ control, and can be broken by an SDK change `fastapi-m8` has already absorbed.
1187
+
1188
+ | Re-export | SDK origin | Purpose |
1189
+ |---|---|---|
1190
+ | `has_superuser_privileges` | `auth_sdk_m8` | Dual-evidence superuser predicate (`role == SUPERADMIN` **and** `is_superuser`) |
1191
+ | `has_minimum_role` | `auth_sdk_m8.authorization` | Canonical role-ordering predicate — the single implementation of the hierarchy |
1192
+ | `RoleType` | `auth_sdk_m8.schemas.base` | Role enum; the argument type of `AuthDeps.require_role()` |
1193
+ | `BaseController` | `auth_sdk_m8.controllers.base` | CRUD controller base class |
1194
+ | `ResponseModelBase` | `auth_sdk_m8.schemas.base` | Base response schema |
1195
+ | `ResponseMessage` | `auth_sdk_m8.schemas.base` | Simple `{"message": ...}` response schema |
1196
+ | `ValidationConstants` | `auth_sdk_m8.schemas.shared` | Shared field length/format constants for consumer schemas |
1197
+ | `TimestampMixin` | `auth_sdk_m8.models.shared` | `created_at` / `updated_at` UTC columns for SQLModel tables |
1198
+ | `UserModel` | `auth_sdk_m8.schemas.user` | The authenticated principal every JWT dependency resolves to |
1199
+ | `find_dotenv` | `auth_sdk_m8.utils.paths` | Locate the service `.env` file |
1200
+ | `render_metrics` | `auth_sdk_m8.observability.metrics` | Render the Prometheus exposition payload |
1201
+ | `REGISTRY` | `auth_sdk_m8.observability.metrics` | The shared Prometheus collector registry `render_metrics` renders |
1202
+ | `make_scrape_credential_guard` | `auth_sdk_m8.security.guards` | Build the `/metrics` scrape-credential guard |
1203
+
1204
+ ```python
1205
+ from fastapi_m8 import (
1206
+ REGISTRY,
1207
+ BaseController,
1208
+ ResponseMessage,
1209
+ ResponseModelBase,
1210
+ RoleType,
1211
+ TimestampMixin,
1212
+ UserModel,
1213
+ ValidationConstants,
1214
+ find_dotenv,
1215
+ has_minimum_role,
1216
+ has_superuser_privileges,
1217
+ make_scrape_credential_guard,
1218
+ render_metrics,
1219
+ )
1220
+ ```
1221
+
1222
+ Every name above is the SDK object itself, not a wrapper — `fastapi_m8.RoleType
1223
+ is auth_sdk_m8.schemas.base.RoleType` — so rerouting an import changes nothing
1224
+ at runtime, including SQLModel/SQLAlchemy table metadata built on
1225
+ `TimestampMixin`.
1226
+
1165
1227
  ---
1166
1228
 
1167
1229
  ## Authentication
@@ -1420,13 +1482,15 @@ TABLES_PREFIX=app
1420
1482
  `SQLALCHEMY_DATABASE_URI` is assembled automatically. You can also set it directly
1421
1483
  to override the assembly.
1422
1484
 
1423
- Define models with `TimestampMixin` from `auth-sdk-m8` (adds `created_at` /
1424
- `updated_at` UTC columns):
1485
+ Define models with `TimestampMixin` (adds `created_at` / `updated_at` UTC
1486
+ columns). It originates in `auth-sdk-m8` and is
1487
+ [re-exported by `fastapi-m8`](#reusable-sdk-primitives) — import it from here,
1488
+ not from the SDK:
1425
1489
 
1426
1490
  ```python
1427
1491
  import uuid
1428
1492
  from sqlmodel import SQLModel, Field
1429
- from auth_sdk_m8.models.shared import TimestampMixin
1493
+ from fastapi_m8 import TimestampMixin
1430
1494
 
1431
1495
 
1432
1496
  class Item(TimestampMixin, SQLModel, table=True):
@@ -1636,6 +1700,8 @@ async def test_health(client):
1636
1700
 
1637
1701
  | `fastapi-m8` | `auth-sdk-m8` | Python |
1638
1702
  |---|---|---|
1703
+ | `4.4.0` | `>=3.1.3, <4.0.0` | 3.12, 3.13, 3.14 |
1704
+ | `4.3.0` | `>=3.1.2, <4.0.0` | 3.12, 3.13, 3.14 |
1639
1705
  | `4.2.0` | `>=3.1.0, <4.0.0` | 3.12, 3.13, 3.14 |
1640
1706
  | `4.1.0` | `>=3.1.0, <4.0.0` | 3.11, 3.12, 3.13, 3.14 |
1641
1707
  | `4.0.0` | `>=3.0.0, <4.0.0` | 3.11, 3.12, 3.13, 3.14 |
@@ -1658,6 +1724,11 @@ The compatibility matrix is enforced at startup via `COMPAT_MATRIX`. A
1658
1724
  `RuntimeError` is raised immediately if the installed `auth-sdk-m8` version is
1659
1725
  outside the supported range.
1660
1726
 
1727
+ Since `4.4.0` the check **fails closed**: if the running `fastapi-m8` minor has
1728
+ no `COMPAT_MATRIX` row at all, startup raises a `RuntimeError` naming the
1729
+ missing row instead of skipping validation. Before `4.4.0` an unlisted minor
1730
+ booted with the dependency check silently disabled.
1731
+
1661
1732
  Check at runtime:
1662
1733
 
1663
1734
  ```python
@@ -27,6 +27,7 @@ boilerplate from every consumer service.
27
27
  - [build_auth_deps()](#build_auth_deps)
28
28
  - [create_db_engine()](#create_db_engine)
29
29
  - [Health Checks](#health-checks)
30
+ - [Reusable SDK Primitives](#reusable-sdk-primitives)
30
31
  7. [Authentication](#authentication)
31
32
  - [Token Modes](#token-modes)
32
33
  - [Role System](#role-system)
@@ -132,6 +133,20 @@ pip install "fastapi-m8[all]"
132
133
 
133
134
  **Runtime requirements:** Python 3.12+
134
135
 
136
+ **What the minimal install gives you (since 4.4.0):** everything except the two
137
+ SQLModel-backed re-exports. `create_app`, `build_auth_deps`,
138
+ `ConsumerServiceSettings`, the health building blocks, the API-key and event-stream
139
+ clients, and the extra-free SDK primitives (`RoleType`, `has_minimum_role`,
140
+ `UserModel`, `REGISTRY`, …) all work with no extras. `BaseController` and
141
+ `TimestampMixin` are resolved lazily and raise a `ModuleNotFoundError` naming
142
+ `fastapi-m8[db]` if you touch them without that extra — install `[db]` (plus a
143
+ driver) as soon as your service has a database.
144
+
145
+ > Before 4.4.0 the minimal install did not actually work: `import fastapi_m8`
146
+ > raised `ModuleNotFoundError: No module named 'sqlalchemy'` on any release from
147
+ > 4.2.1 onward, because those two names were imported at module level. If you are
148
+ > on 4.2.1–4.3.0 and want a database-free install, upgrade to 4.4.0.
149
+
135
150
  ---
136
151
 
137
152
  ## Quick Start
@@ -905,6 +920,53 @@ class RedisCheck:
905
920
  | `LENIENT` (default) | Any check is `fail` |
906
921
  | `STRICT` | Any check is `fail` or `unknown` |
907
922
 
923
+ ### Reusable SDK Primitives
924
+
925
+ `fastapi-m8` already depends on `auth-sdk-m8` and pins its major range, so it
926
+ re-exports the SDK primitives a consumer service actually needs. **Import them
927
+ from `fastapi_m8`, never from `auth_sdk_m8`.** A consumer service that imports
928
+ the SDK directly takes on a second dependency whose major range it does not
929
+ control, and can be broken by an SDK change `fastapi-m8` has already absorbed.
930
+
931
+ | Re-export | SDK origin | Purpose |
932
+ |---|---|---|
933
+ | `has_superuser_privileges` | `auth_sdk_m8` | Dual-evidence superuser predicate (`role == SUPERADMIN` **and** `is_superuser`) |
934
+ | `has_minimum_role` | `auth_sdk_m8.authorization` | Canonical role-ordering predicate — the single implementation of the hierarchy |
935
+ | `RoleType` | `auth_sdk_m8.schemas.base` | Role enum; the argument type of `AuthDeps.require_role()` |
936
+ | `BaseController` | `auth_sdk_m8.controllers.base` | CRUD controller base class |
937
+ | `ResponseModelBase` | `auth_sdk_m8.schemas.base` | Base response schema |
938
+ | `ResponseMessage` | `auth_sdk_m8.schemas.base` | Simple `{"message": ...}` response schema |
939
+ | `ValidationConstants` | `auth_sdk_m8.schemas.shared` | Shared field length/format constants for consumer schemas |
940
+ | `TimestampMixin` | `auth_sdk_m8.models.shared` | `created_at` / `updated_at` UTC columns for SQLModel tables |
941
+ | `UserModel` | `auth_sdk_m8.schemas.user` | The authenticated principal every JWT dependency resolves to |
942
+ | `find_dotenv` | `auth_sdk_m8.utils.paths` | Locate the service `.env` file |
943
+ | `render_metrics` | `auth_sdk_m8.observability.metrics` | Render the Prometheus exposition payload |
944
+ | `REGISTRY` | `auth_sdk_m8.observability.metrics` | The shared Prometheus collector registry `render_metrics` renders |
945
+ | `make_scrape_credential_guard` | `auth_sdk_m8.security.guards` | Build the `/metrics` scrape-credential guard |
946
+
947
+ ```python
948
+ from fastapi_m8 import (
949
+ REGISTRY,
950
+ BaseController,
951
+ ResponseMessage,
952
+ ResponseModelBase,
953
+ RoleType,
954
+ TimestampMixin,
955
+ UserModel,
956
+ ValidationConstants,
957
+ find_dotenv,
958
+ has_minimum_role,
959
+ has_superuser_privileges,
960
+ make_scrape_credential_guard,
961
+ render_metrics,
962
+ )
963
+ ```
964
+
965
+ Every name above is the SDK object itself, not a wrapper — `fastapi_m8.RoleType
966
+ is auth_sdk_m8.schemas.base.RoleType` — so rerouting an import changes nothing
967
+ at runtime, including SQLModel/SQLAlchemy table metadata built on
968
+ `TimestampMixin`.
969
+
908
970
  ---
909
971
 
910
972
  ## Authentication
@@ -1163,13 +1225,15 @@ TABLES_PREFIX=app
1163
1225
  `SQLALCHEMY_DATABASE_URI` is assembled automatically. You can also set it directly
1164
1226
  to override the assembly.
1165
1227
 
1166
- Define models with `TimestampMixin` from `auth-sdk-m8` (adds `created_at` /
1167
- `updated_at` UTC columns):
1228
+ Define models with `TimestampMixin` (adds `created_at` / `updated_at` UTC
1229
+ columns). It originates in `auth-sdk-m8` and is
1230
+ [re-exported by `fastapi-m8`](#reusable-sdk-primitives) — import it from here,
1231
+ not from the SDK:
1168
1232
 
1169
1233
  ```python
1170
1234
  import uuid
1171
1235
  from sqlmodel import SQLModel, Field
1172
- from auth_sdk_m8.models.shared import TimestampMixin
1236
+ from fastapi_m8 import TimestampMixin
1173
1237
 
1174
1238
 
1175
1239
  class Item(TimestampMixin, SQLModel, table=True):
@@ -1379,6 +1443,8 @@ async def test_health(client):
1379
1443
 
1380
1444
  | `fastapi-m8` | `auth-sdk-m8` | Python |
1381
1445
  |---|---|---|
1446
+ | `4.4.0` | `>=3.1.3, <4.0.0` | 3.12, 3.13, 3.14 |
1447
+ | `4.3.0` | `>=3.1.2, <4.0.0` | 3.12, 3.13, 3.14 |
1382
1448
  | `4.2.0` | `>=3.1.0, <4.0.0` | 3.12, 3.13, 3.14 |
1383
1449
  | `4.1.0` | `>=3.1.0, <4.0.0` | 3.11, 3.12, 3.13, 3.14 |
1384
1450
  | `4.0.0` | `>=3.0.0, <4.0.0` | 3.11, 3.12, 3.13, 3.14 |
@@ -1401,6 +1467,11 @@ The compatibility matrix is enforced at startup via `COMPAT_MATRIX`. A
1401
1467
  `RuntimeError` is raised immediately if the installed `auth-sdk-m8` version is
1402
1468
  outside the supported range.
1403
1469
 
1470
+ Since `4.4.0` the check **fails closed**: if the running `fastapi-m8` minor has
1471
+ no `COMPAT_MATRIX` row at all, startup raises a `RuntimeError` naming the
1472
+ missing row instead of skipping validation. Before `4.4.0` an unlisted minor
1473
+ booted with the dependency check silently disabled.
1474
+
1404
1475
  Check at runtime:
1405
1476
 
1406
1477
  ```python
@@ -25,7 +25,7 @@ attrs==26.1.0
25
25
  # via
26
26
  # outcome
27
27
  # trio
28
- auth-sdk-m8[config,events,fastapi,observability,security]==3.1.2
28
+ auth-sdk-m8[config,events,fastapi,observability,security]==3.1.3
29
29
  # via
30
30
  # fastapi-m8
31
31
  # fastapi-m8 (pyproject.toml)
@@ -16,7 +16,7 @@ anyio==4.14.1
16
16
  # fastapi-m8 (pyproject.toml)
17
17
  # httpx
18
18
  # starlette
19
- auth-sdk-m8[config,events,fastapi,observability,security]==3.1.2
19
+ auth-sdk-m8[config,events,fastapi,observability,security]==3.1.3
20
20
  # via
21
21
  # fastapi-m8
22
22
  # fastapi-m8 (pyproject.toml)
@@ -27,11 +27,17 @@ Tier 2 — health building blocks::
27
27
  )
28
28
 
29
29
  Reusable SDK primitives (re-exported from auth-sdk-m8, so consumers depend
30
- only on fastapi-m8)::
30
+ only on fastapi-m8 — a consumer service must never import ``auth_sdk_m8``
31
+ directly)::
31
32
 
32
- from fastapi_m8 import has_superuser_privileges
33
+ from fastapi_m8 import has_superuser_privileges, has_minimum_role, RoleType
33
34
  from fastapi_m8 import BaseController, ResponseModelBase, ResponseMessage
34
- from fastapi_m8 import TimestampMixin, UserModel, find_dotenv, render_metrics
35
+ from fastapi_m8 import TimestampMixin, UserModel, ValidationConstants
36
+ from fastapi_m8 import find_dotenv, render_metrics, REGISTRY
37
+ from fastapi_m8 import make_scrape_credential_guard
38
+
39
+ ``BaseController`` and ``TimestampMixin`` are the only two of those that need
40
+ the ``[db]`` extra; both are resolved lazily — see ``__getattr__`` below.
35
41
 
36
42
  Tier 3 — informational / future::
37
43
 
@@ -39,15 +45,19 @@ Tier 3 — informational / future::
39
45
  from fastapi_m8 import COMPAT_MATRIX, __version__
40
46
  """
41
47
 
48
+ from typing import TYPE_CHECKING, Any
49
+
42
50
  # Tier 1
43
51
  # Reusable SDK primitives — re-exported so consumers only need fastapi-m8,
44
52
  # never a direct auth-sdk-m8 dependency.
45
53
  from auth_sdk_m8 import has_superuser_privileges
46
- from auth_sdk_m8.controllers.base import BaseController
47
- from auth_sdk_m8.models.shared import TimestampMixin
54
+ from auth_sdk_m8.authorization import has_minimum_role
55
+ from auth_sdk_m8.observability.metrics import REGISTRY
48
56
  from auth_sdk_m8.observability.metrics import render as render_metrics
49
- from auth_sdk_m8.schemas.base import ResponseMessage, ResponseModelBase
57
+ from auth_sdk_m8.schemas.base import ResponseMessage, ResponseModelBase, RoleType
58
+ from auth_sdk_m8.schemas.shared import ValidationConstants
50
59
  from auth_sdk_m8.schemas.user import UserModel
60
+ from auth_sdk_m8.security.guards import make_scrape_credential_guard
51
61
  from auth_sdk_m8.utils.paths import find_dotenv
52
62
 
53
63
  from fastapi_m8._api_key import (
@@ -92,6 +102,68 @@ from fastapi_m8._route_audit import BareApiKeyDependency, audit_api_key_routes
92
102
  from fastapi_m8._version import __version__
93
103
  from fastapi_m8.config import ConsumerServiceSettings
94
104
 
105
+ if TYPE_CHECKING: # pragma: no cover - type-checker-only, never executed
106
+ # Imported eagerly for type checkers and IDEs only. At runtime these two
107
+ # names are served by ``__getattr__`` below, so a bare install can import
108
+ # the package without the ``[db]`` extra.
109
+ from auth_sdk_m8.controllers.base import BaseController
110
+ from auth_sdk_m8.models.shared import TimestampMixin
111
+
112
+ # The SDK re-exports that require the ``[db]`` extra, mapped to their source
113
+ # module. ``auth_sdk_m8.controllers.base`` imports ``sqlalchemy.exc`` and
114
+ # ``sqlmodel``; ``auth_sdk_m8.models.shared`` imports ``sqlalchemy`` and
115
+ # ``sqlmodel``. Every other re-export above is extra-free.
116
+ _DB_REEXPORTS: dict[str, str] = {
117
+ "BaseController": "auth_sdk_m8.controllers.base",
118
+ "TimestampMixin": "auth_sdk_m8.models.shared",
119
+ }
120
+
121
+
122
+ def __getattr__(name: str) -> Any:
123
+ """
124
+ Resolve the ``[db]``-extra SDK re-exports on first access (PEP 562).
125
+
126
+ ``pip install fastapi-m8`` with no extras must yield an importable
127
+ package. Importing ``BaseController`` / ``TimestampMixin`` at module level
128
+ made ``import fastapi_m8`` raise ``ModuleNotFoundError: No module named
129
+ 'sqlalchemy'`` on a bare install, because both come from SQLModel-backed
130
+ SDK modules and SQLAlchemy arrives only through the ``db`` extra.
131
+
132
+ Deferring them costs a bare install nothing and keeps the import boundary
133
+ intact: a consumer still writes ``from fastapi_m8 import BaseController``.
134
+ The object returned **is** the SDK object — identity is preserved, so ORM
135
+ models mixing in the re-exported ``TimestampMixin`` register exactly the
136
+ same metadata as before, and the resolved value is cached in the module
137
+ globals so later lookups skip this function entirely.
138
+
139
+ Note on where the failure now surfaces: PEP 562 moves it from
140
+ ``import fastapi_m8`` to first attribute access. For both of these names
141
+ that is still the consumer's own module-import time — ``BaseController``
142
+ is subclassed and ``TimestampMixin`` is mixed in at class-definition
143
+ time — so the deferral does **not** push an ``ImportError`` into a request
144
+ path. It is a packaging fix, not a runtime-path hazard.
145
+ """
146
+ module_path = _DB_REEXPORTS.get(name)
147
+ if module_path is None:
148
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
149
+ from importlib import import_module # noqa: PLC0415
150
+
151
+ try:
152
+ value = getattr(import_module(module_path), name)
153
+ except ModuleNotFoundError as exc:
154
+ raise ModuleNotFoundError(
155
+ f"fastapi_m8.{name} is re-exported from {module_path}, which requires"
156
+ f" the 'db' extra: install fastapi-m8[db] (or fastapi-m8[all])."
157
+ ) from exc
158
+ globals()[name] = value
159
+ return value
160
+
161
+
162
+ def __dir__() -> list[str]:
163
+ """Keep the lazy names discoverable by ``dir()`` and tab completion."""
164
+ return sorted(set(globals()) | set(_DB_REEXPORTS))
165
+
166
+
95
167
  __all__ = [
96
168
  "__version__",
97
169
  # Tier 1
@@ -127,13 +199,18 @@ __all__ = [
127
199
  "HealthAggregatePolicy",
128
200
  # Reusable SDK primitives (from auth-sdk-m8)
129
201
  "has_superuser_privileges",
202
+ "has_minimum_role",
203
+ "RoleType",
130
204
  "BaseController",
131
205
  "ResponseModelBase",
132
206
  "ResponseMessage",
133
207
  "TimestampMixin",
134
208
  "UserModel",
209
+ "ValidationConstants",
135
210
  "find_dotenv",
136
211
  "render_metrics",
212
+ "REGISTRY",
213
+ "make_scrape_credential_guard",
137
214
  # Tier 3
138
215
  "create_async_app",
139
216
  "CAPABILITIES",