fastapi-m8 4.2.2__tar.gz → 4.3.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 (65) hide show
  1. fastapi_m8-4.3.0/.markdownlint.yaml +43 -0
  2. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/CHANGELOG.md +93 -0
  3. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/PKG-INFO +56 -5
  4. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/README.md +54 -3
  5. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/__init__.py +16 -4
  6. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_compat.py +8 -0
  7. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_version.py +1 -1
  8. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/pyproject.toml +1 -1
  9. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_packaging.py +21 -1
  10. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_public_typing.py +91 -3
  11. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.codacy.yml +0 -0
  12. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.dockerignore +0 -0
  13. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.env.example +0 -0
  14. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.gitattributes +0 -0
  15. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.github/FUNDING.yml +0 -0
  16. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.github/dependabot.yml +0 -0
  17. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.github/workflows/CI.yaml +0 -0
  18. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.github/workflows/PiPy.yml +0 -0
  19. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.gitignore +0 -0
  20. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.gitleaks.toml +0 -0
  21. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.pydocstyle +0 -0
  22. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/AGENTS.md +0 -0
  23. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/CLAUDE.md +0 -0
  24. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/LICENSE +0 -0
  25. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/REPOSITORY_CONTEXT.md +0 -0
  26. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/SECURITY.md +0 -0
  27. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/constraints-all.txt +0 -0
  28. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/constraints.txt +0 -0
  29. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_api_key.py +0 -0
  30. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_app.py +0 -0
  31. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_async_stub.py +0 -0
  32. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_deps.py +0 -0
  33. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_engine.py +0 -0
  34. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_events.py +0 -0
  35. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_health.py +0 -0
  36. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_internal_auth.py +0 -0
  37. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_revocation.py +0 -0
  38. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_route_audit.py +0 -0
  39. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/config.py +0 -0
  40. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/scripts/__init__.py +0 -0
  41. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/scripts/docker_start.sh +0 -0
  42. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/scripts/pre_start.py +0 -0
  43. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/__init__.py +0 -0
  44. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/conftest.py +0 -0
  45. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_api_key.py +0 -0
  46. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_api_key_deps.py +0 -0
  47. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_app.py +0 -0
  48. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_app_extra.py +0 -0
  49. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_async_stub.py +0 -0
  50. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_ci_policy.py +0 -0
  51. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_compat.py +0 -0
  52. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_config.py +0 -0
  53. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_config_file_secrets.py +0 -0
  54. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_deps.py +0 -0
  55. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_engine.py +0 -0
  56. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_event_signing_gate.py +0 -0
  57. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_events.py +0 -0
  58. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_fixture_matrix_contract.py +0 -0
  59. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_health.py +0 -0
  60. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_host_header_routing.py +0 -0
  61. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_internal_auth.py +0 -0
  62. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_meta.py +0 -0
  63. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_pre_start.py +0 -0
  64. {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_revocation.py +0 -0
  65. {fastapi_m8-4.2.2 → fastapi_m8-4.3.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,99 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · Versioning:
5
5
 
6
6
  ---
7
7
 
8
+ ## [4.3.0] — 2026-08-14 · Complete the SDK re-export surface
9
+
10
+ Adds the five `auth-sdk-m8` primitives the consumer fleet still imports
11
+ directly, so a consumer service can depend on `fastapi-m8` alone and reach
12
+ **zero** `auth_sdk_m8` imports. Additive only: no existing export, signature or
13
+ behavior changes, and no new `auth-sdk-m8` API is required.
14
+
15
+ Until this release the boundary was literally unsatisfiable — five of the twelve
16
+ symbols the fleet imports had no `fastapi_m8` re-export, so every consumer had
17
+ to keep a direct SDK import or invent a local shim.
18
+
19
+ ### Added
20
+
21
+ - **`has_minimum_role`** (from `auth_sdk_m8.authorization`) — the canonical
22
+ role-ordering predicate. One implementation of the hierarchy, re-exported
23
+ rather than re-derived at a call site.
24
+ - **`RoleType`** (from `auth_sdk_m8.schemas.base`) — the role enum those
25
+ thresholds are expressed in, and the argument type of
26
+ `AuthDeps.require_role()`.
27
+ - **`ValidationConstants`** (from `auth_sdk_m8.schemas.shared`) — the shared
28
+ field-length/format constants consumer schemas validate against.
29
+ - **`make_scrape_credential_guard`** (from `auth_sdk_m8.security.guards`) — the
30
+ `/metrics` scrape-credential guard factory.
31
+ - **`REGISTRY`** (from `auth_sdk_m8.observability.metrics`) — the shared
32
+ Prometheus collector registry that `render_metrics` (already re-exported)
33
+ renders.
34
+
35
+ All five are documented in the module docstring's *Reusable SDK primitives*
36
+ block and listed in `__all__`; `has_superuser_privileges`, `BaseController`,
37
+ `ResponseModelBase`, `ResponseMessage`, `TimestampMixin`, `UserModel`,
38
+ `find_dotenv` and `render_metrics` were already re-exported and are unchanged.
39
+
40
+ ### Changed
41
+
42
+ - `COMPAT_MATRIX` gains its `"4.3"` row (`auth-sdk-m8 >=3.1.2,<4.0.0`). Without
43
+ it `_assert_compat()` would find no requirement for the new minor and silently
44
+ skip the startup check. The floor is stated as `>=3.1.2` — the floor
45
+ `pyproject.toml` already declares — rather than `4.2`'s looser `>=3.1.0`.
46
+
47
+ ### Documentation
48
+
49
+ - `README.md` gains a **Reusable SDK Primitives** section listing every
50
+ re-exported symbol, its SDK origin and its purpose, and states the boundary
51
+ rule: a consumer service imports these from `fastapi-m8`, never from
52
+ `auth-sdk-m8`. The database example now imports `TimestampMixin` from
53
+ `fastapi_m8` accordingly, and the compatibility table gains the `4.3.0` row.
54
+
55
+ ### Tests
56
+
57
+ - `tests/test_public_typing.py` type-checks all twelve re-exported primitives
58
+ through the public surface and asserts each one **is** the SDK object it
59
+ claims to re-export (identity, not just importability).
60
+ - `tests/test_packaging.py`'s clean-install probe imports the five new names
61
+ from the built wheel, which is the acceptance condition for this release.
62
+
63
+ ---
64
+
65
+ ## [4.2.2] — 2026-07-31 · Dependency maintenance
66
+
67
+ Reconstructed from Git history (`ed259a1`, `0c3123b`) — the release shipped
68
+ without a changelog entry.
69
+
70
+ ### Changed
71
+
72
+ - `auth-sdk-m8` floor raised to `>=3.1.2,<4.0.0` in `pyproject.toml` and pinned
73
+ to `3.1.2` in the compiled constraints files. No source change.
74
+
75
+ ---
76
+
77
+ ## [4.2.1] — 2026-07-31 · Re-exports, Python floor, docs
78
+
79
+ Reconstructed from Git history (`86830ee` and the commits it bumped for:
80
+ `b105e01`, `23020fe`, `be5c382`, `a57811c`) — the release shipped without a
81
+ changelog entry.
82
+
83
+ ### Added
84
+
85
+ - Re-export of the reusable `auth-sdk-m8` primitives (`has_superuser_privileges`,
86
+ `BaseController`, `ResponseModelBase`, `ResponseMessage`, `TimestampMixin`,
87
+ `UserModel`, `find_dotenv`, `render_metrics`) — the first half of the surface
88
+ `4.3.0` completes.
89
+
90
+ ### Changed
91
+
92
+ - Python floor raised to `>=3.12`; truncated lockfiles regenerated.
93
+
94
+ ### Documentation
95
+
96
+ - Documented hybrid mode's expiry-bounded revocation contract; reformatted the
97
+ embedded Python snippets in `CHANGELOG.md`/`README.md`.
98
+
99
+ ---
100
+
8
101
  ## [4.2.0] — 2026-07-23 · Role-capability demonstration surface (Phase 7)
9
102
 
10
103
  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.3.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
@@ -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)
@@ -1162,6 +1163,53 @@ class RedisCheck:
1162
1163
  | `LENIENT` (default) | Any check is `fail` |
1163
1164
  | `STRICT` | Any check is `fail` or `unknown` |
1164
1165
 
1166
+ ### Reusable SDK Primitives
1167
+
1168
+ `fastapi-m8` already depends on `auth-sdk-m8` and pins its major range, so it
1169
+ re-exports the SDK primitives a consumer service actually needs. **Import them
1170
+ from `fastapi_m8`, never from `auth_sdk_m8`.** A consumer service that imports
1171
+ the SDK directly takes on a second dependency whose major range it does not
1172
+ control, and can be broken by an SDK change `fastapi-m8` has already absorbed.
1173
+
1174
+ | Re-export | SDK origin | Purpose |
1175
+ |---|---|---|
1176
+ | `has_superuser_privileges` | `auth_sdk_m8` | Dual-evidence superuser predicate (`role == SUPERADMIN` **and** `is_superuser`) |
1177
+ | `has_minimum_role` | `auth_sdk_m8.authorization` | Canonical role-ordering predicate — the single implementation of the hierarchy |
1178
+ | `RoleType` | `auth_sdk_m8.schemas.base` | Role enum; the argument type of `AuthDeps.require_role()` |
1179
+ | `BaseController` | `auth_sdk_m8.controllers.base` | CRUD controller base class |
1180
+ | `ResponseModelBase` | `auth_sdk_m8.schemas.base` | Base response schema |
1181
+ | `ResponseMessage` | `auth_sdk_m8.schemas.base` | Simple `{"message": ...}` response schema |
1182
+ | `ValidationConstants` | `auth_sdk_m8.schemas.shared` | Shared field length/format constants for consumer schemas |
1183
+ | `TimestampMixin` | `auth_sdk_m8.models.shared` | `created_at` / `updated_at` UTC columns for SQLModel tables |
1184
+ | `UserModel` | `auth_sdk_m8.schemas.user` | The authenticated principal every JWT dependency resolves to |
1185
+ | `find_dotenv` | `auth_sdk_m8.utils.paths` | Locate the service `.env` file |
1186
+ | `render_metrics` | `auth_sdk_m8.observability.metrics` | Render the Prometheus exposition payload |
1187
+ | `REGISTRY` | `auth_sdk_m8.observability.metrics` | The shared Prometheus collector registry `render_metrics` renders |
1188
+ | `make_scrape_credential_guard` | `auth_sdk_m8.security.guards` | Build the `/metrics` scrape-credential guard |
1189
+
1190
+ ```python
1191
+ from fastapi_m8 import (
1192
+ REGISTRY,
1193
+ BaseController,
1194
+ ResponseMessage,
1195
+ ResponseModelBase,
1196
+ RoleType,
1197
+ TimestampMixin,
1198
+ UserModel,
1199
+ ValidationConstants,
1200
+ find_dotenv,
1201
+ has_minimum_role,
1202
+ has_superuser_privileges,
1203
+ make_scrape_credential_guard,
1204
+ render_metrics,
1205
+ )
1206
+ ```
1207
+
1208
+ Every name above is the SDK object itself, not a wrapper — `fastapi_m8.RoleType
1209
+ is auth_sdk_m8.schemas.base.RoleType` — so rerouting an import changes nothing
1210
+ at runtime, including SQLModel/SQLAlchemy table metadata built on
1211
+ `TimestampMixin`.
1212
+
1165
1213
  ---
1166
1214
 
1167
1215
  ## Authentication
@@ -1420,13 +1468,15 @@ TABLES_PREFIX=app
1420
1468
  `SQLALCHEMY_DATABASE_URI` is assembled automatically. You can also set it directly
1421
1469
  to override the assembly.
1422
1470
 
1423
- Define models with `TimestampMixin` from `auth-sdk-m8` (adds `created_at` /
1424
- `updated_at` UTC columns):
1471
+ Define models with `TimestampMixin` (adds `created_at` / `updated_at` UTC
1472
+ columns). It originates in `auth-sdk-m8` and is
1473
+ [re-exported by `fastapi-m8`](#reusable-sdk-primitives) — import it from here,
1474
+ not from the SDK:
1425
1475
 
1426
1476
  ```python
1427
1477
  import uuid
1428
1478
  from sqlmodel import SQLModel, Field
1429
- from auth_sdk_m8.models.shared import TimestampMixin
1479
+ from fastapi_m8 import TimestampMixin
1430
1480
 
1431
1481
 
1432
1482
  class Item(TimestampMixin, SQLModel, table=True):
@@ -1636,6 +1686,7 @@ async def test_health(client):
1636
1686
 
1637
1687
  | `fastapi-m8` | `auth-sdk-m8` | Python |
1638
1688
  |---|---|---|
1689
+ | `4.3.0` | `>=3.1.2, <4.0.0` | 3.12, 3.13, 3.14 |
1639
1690
  | `4.2.0` | `>=3.1.0, <4.0.0` | 3.12, 3.13, 3.14 |
1640
1691
  | `4.1.0` | `>=3.1.0, <4.0.0` | 3.11, 3.12, 3.13, 3.14 |
1641
1692
  | `4.0.0` | `>=3.0.0, <4.0.0` | 3.11, 3.12, 3.13, 3.14 |
@@ -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)
@@ -905,6 +906,53 @@ class RedisCheck:
905
906
  | `LENIENT` (default) | Any check is `fail` |
906
907
  | `STRICT` | Any check is `fail` or `unknown` |
907
908
 
909
+ ### Reusable SDK Primitives
910
+
911
+ `fastapi-m8` already depends on `auth-sdk-m8` and pins its major range, so it
912
+ re-exports the SDK primitives a consumer service actually needs. **Import them
913
+ from `fastapi_m8`, never from `auth_sdk_m8`.** A consumer service that imports
914
+ the SDK directly takes on a second dependency whose major range it does not
915
+ control, and can be broken by an SDK change `fastapi-m8` has already absorbed.
916
+
917
+ | Re-export | SDK origin | Purpose |
918
+ |---|---|---|
919
+ | `has_superuser_privileges` | `auth_sdk_m8` | Dual-evidence superuser predicate (`role == SUPERADMIN` **and** `is_superuser`) |
920
+ | `has_minimum_role` | `auth_sdk_m8.authorization` | Canonical role-ordering predicate — the single implementation of the hierarchy |
921
+ | `RoleType` | `auth_sdk_m8.schemas.base` | Role enum; the argument type of `AuthDeps.require_role()` |
922
+ | `BaseController` | `auth_sdk_m8.controllers.base` | CRUD controller base class |
923
+ | `ResponseModelBase` | `auth_sdk_m8.schemas.base` | Base response schema |
924
+ | `ResponseMessage` | `auth_sdk_m8.schemas.base` | Simple `{"message": ...}` response schema |
925
+ | `ValidationConstants` | `auth_sdk_m8.schemas.shared` | Shared field length/format constants for consumer schemas |
926
+ | `TimestampMixin` | `auth_sdk_m8.models.shared` | `created_at` / `updated_at` UTC columns for SQLModel tables |
927
+ | `UserModel` | `auth_sdk_m8.schemas.user` | The authenticated principal every JWT dependency resolves to |
928
+ | `find_dotenv` | `auth_sdk_m8.utils.paths` | Locate the service `.env` file |
929
+ | `render_metrics` | `auth_sdk_m8.observability.metrics` | Render the Prometheus exposition payload |
930
+ | `REGISTRY` | `auth_sdk_m8.observability.metrics` | The shared Prometheus collector registry `render_metrics` renders |
931
+ | `make_scrape_credential_guard` | `auth_sdk_m8.security.guards` | Build the `/metrics` scrape-credential guard |
932
+
933
+ ```python
934
+ from fastapi_m8 import (
935
+ REGISTRY,
936
+ BaseController,
937
+ ResponseMessage,
938
+ ResponseModelBase,
939
+ RoleType,
940
+ TimestampMixin,
941
+ UserModel,
942
+ ValidationConstants,
943
+ find_dotenv,
944
+ has_minimum_role,
945
+ has_superuser_privileges,
946
+ make_scrape_credential_guard,
947
+ render_metrics,
948
+ )
949
+ ```
950
+
951
+ Every name above is the SDK object itself, not a wrapper — `fastapi_m8.RoleType
952
+ is auth_sdk_m8.schemas.base.RoleType` — so rerouting an import changes nothing
953
+ at runtime, including SQLModel/SQLAlchemy table metadata built on
954
+ `TimestampMixin`.
955
+
908
956
  ---
909
957
 
910
958
  ## Authentication
@@ -1163,13 +1211,15 @@ TABLES_PREFIX=app
1163
1211
  `SQLALCHEMY_DATABASE_URI` is assembled automatically. You can also set it directly
1164
1212
  to override the assembly.
1165
1213
 
1166
- Define models with `TimestampMixin` from `auth-sdk-m8` (adds `created_at` /
1167
- `updated_at` UTC columns):
1214
+ Define models with `TimestampMixin` (adds `created_at` / `updated_at` UTC
1215
+ columns). It originates in `auth-sdk-m8` and is
1216
+ [re-exported by `fastapi-m8`](#reusable-sdk-primitives) — import it from here,
1217
+ not from the SDK:
1168
1218
 
1169
1219
  ```python
1170
1220
  import uuid
1171
1221
  from sqlmodel import SQLModel, Field
1172
- from auth_sdk_m8.models.shared import TimestampMixin
1222
+ from fastapi_m8 import TimestampMixin
1173
1223
 
1174
1224
 
1175
1225
  class Item(TimestampMixin, SQLModel, table=True):
@@ -1379,6 +1429,7 @@ async def test_health(client):
1379
1429
 
1380
1430
  | `fastapi-m8` | `auth-sdk-m8` | Python |
1381
1431
  |---|---|---|
1432
+ | `4.3.0` | `>=3.1.2, <4.0.0` | 3.12, 3.13, 3.14 |
1382
1433
  | `4.2.0` | `>=3.1.0, <4.0.0` | 3.12, 3.13, 3.14 |
1383
1434
  | `4.1.0` | `>=3.1.0, <4.0.0` | 3.11, 3.12, 3.13, 3.14 |
1384
1435
  | `4.0.0` | `>=3.0.0, <4.0.0` | 3.11, 3.12, 3.13, 3.14 |
@@ -27,11 +27,14 @@ 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
35
38
 
36
39
  Tier 3 — informational / future::
37
40
 
@@ -43,11 +46,15 @@ Tier 3 — informational / future::
43
46
  # Reusable SDK primitives — re-exported so consumers only need fastapi-m8,
44
47
  # never a direct auth-sdk-m8 dependency.
45
48
  from auth_sdk_m8 import has_superuser_privileges
49
+ from auth_sdk_m8.authorization import has_minimum_role
46
50
  from auth_sdk_m8.controllers.base import BaseController
47
51
  from auth_sdk_m8.models.shared import TimestampMixin
52
+ from auth_sdk_m8.observability.metrics import REGISTRY
48
53
  from auth_sdk_m8.observability.metrics import render as render_metrics
49
- from auth_sdk_m8.schemas.base import ResponseMessage, ResponseModelBase
54
+ from auth_sdk_m8.schemas.base import ResponseMessage, ResponseModelBase, RoleType
55
+ from auth_sdk_m8.schemas.shared import ValidationConstants
50
56
  from auth_sdk_m8.schemas.user import UserModel
57
+ from auth_sdk_m8.security.guards import make_scrape_credential_guard
51
58
  from auth_sdk_m8.utils.paths import find_dotenv
52
59
 
53
60
  from fastapi_m8._api_key import (
@@ -127,13 +134,18 @@ __all__ = [
127
134
  "HealthAggregatePolicy",
128
135
  # Reusable SDK primitives (from auth-sdk-m8)
129
136
  "has_superuser_privileges",
137
+ "has_minimum_role",
138
+ "RoleType",
130
139
  "BaseController",
131
140
  "ResponseModelBase",
132
141
  "ResponseMessage",
133
142
  "TimestampMixin",
134
143
  "UserModel",
144
+ "ValidationConstants",
135
145
  "find_dotenv",
136
146
  "render_metrics",
147
+ "REGISTRY",
148
+ "make_scrape_credential_guard",
137
149
  # Tier 3
138
150
  "create_async_app",
139
151
  "CAPABILITIES",
@@ -105,6 +105,14 @@ COMPAT_MATRIX: dict[str, dict[str, str]] = {
105
105
  # change. The SDK floor remains >=3.1.0,<4.0.0 (no new SDK API required).
106
106
  # See CHANGELOG and Phase 7 (role-gated examples and privileged-action audit).
107
107
  "4.2": {"auth-sdk-m8": ">=3.1.0,<4.0.0"},
108
+ # 4.3 (MINOR) completes the SDK re-export surface so a consumer service can
109
+ # reach zero direct auth_sdk_m8 imports: has_minimum_role, RoleType,
110
+ # ValidationConstants, make_scrape_credential_guard and REGISTRY join the
111
+ # Tier-1 "Reusable SDK primitives" block in __init__.py. Additive only — no
112
+ # new SDK API is required, but the floor is stated as >=3.1.2 because that
113
+ # is the floor pyproject.toml actually declares and the version whose
114
+ # module layout these five names are re-exported from. See CHANGELOG.
115
+ "4.3": {"auth-sdk-m8": ">=3.1.2,<4.0.0"},
108
116
  }
109
117
 
110
118
  _EXTRAS = "[config,security,fastapi,observability]"
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "4.2.2"
3
+ __version__ = "4.3.0"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-m8"
7
- version = "4.2.2"
7
+ version = "4.3.0"
8
8
  description = "FastAPI application framework for m8 consumer microservices."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -80,6 +80,11 @@ class TestInstalledWheelImports:
80
80
  API-key route, and prove ``audit_api_key_routes`` flags it — the same
81
81
  behavior asserted against the source tree in test_route_audit.py, now
82
82
  proven against the packaged artifact.
83
+
84
+ Also imports the reusable SDK primitives from the built wheel. A
85
+ consumer service depends on ``fastapi-m8`` alone, so "the re-export
86
+ exists in the source tree" is not the guarantee it needs — "the
87
+ re-export ships in the artifact" is.
83
88
  """
84
89
  install_dir = tmp_path / "site"
85
90
  install_dir.mkdir()
@@ -141,7 +146,22 @@ settings = fm8.ConsumerServiceSettings(
141
146
  auth = fm8.build_auth_deps(settings)
142
147
  assert auth.get_current_api_key_principal is not None
143
148
 
144
- from auth_sdk_m8.schemas.base import RoleType
149
+ from fastapi_m8 import (
150
+ REGISTRY,
151
+ RoleType,
152
+ ValidationConstants,
153
+ has_minimum_role,
154
+ make_scrape_credential_guard,
155
+ )
156
+ import auth_sdk_m8.observability.metrics as _sdk_metrics
157
+ import auth_sdk_m8.schemas.base as _sdk_base
158
+
159
+ assert has_minimum_role(RoleType.WRITER, RoleType.READER) is True
160
+ assert has_minimum_role(RoleType.USER, RoleType.READER) is False
161
+ assert RoleType is _sdk_base.RoleType
162
+ assert REGISTRY is _sdk_metrics.REGISTRY
163
+ assert ValidationConstants.KEY_REGEX.match("probe-key") is not None
164
+ assert callable(make_scrape_credential_guard(None))
145
165
 
146
166
  assert callable(auth.get_current_active_reader)
147
167
  assert callable(auth.require_role)
@@ -11,10 +11,13 @@ consumer's own mypy run.
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import importlib
14
15
  import subprocess
15
16
  import sys
16
17
  from pathlib import Path
17
18
 
19
+ import pytest
20
+
18
21
  import fastapi_m8
19
22
 
20
23
  REPO_ROOT = Path(__file__).resolve().parents[1]
@@ -24,11 +27,14 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
24
27
  _TYPING_SNIPPET = '''
25
28
  from __future__ import annotations
26
29
 
27
- from fastapi import Depends, FastAPI
30
+ from pathlib import Path
31
+
32
+ from fastapi import Depends, FastAPI, Response
28
33
 
29
34
  from fastapi_m8 import (
30
35
  API_KEY_HEADER,
31
36
  CAPABILITIES,
37
+ REGISTRY,
32
38
  ApiKeyIntrospectionError,
33
39
  ApiKeyQuotaExceededError,
34
40
  AppLifecycle,
@@ -36,6 +42,7 @@ from fastapi_m8 import (
36
42
  AuthEventStreamClient,
37
43
  AuthStreamEvent,
38
44
  BareApiKeyDependency,
45
+ BaseController,
39
46
  COMPAT_MATRIX,
40
47
  ConsumerServiceSettings,
41
48
  DbEngine,
@@ -45,7 +52,13 @@ from fastapi_m8 import (
45
52
  HealthConfig,
46
53
  HealthStatus,
47
54
  InternalAuthProvider,
55
+ ResponseMessage,
56
+ ResponseModelBase,
57
+ RoleType,
48
58
  ServiceTokenInternalAuth,
59
+ TimestampMixin,
60
+ UserModel,
61
+ ValidationConstants,
49
62
  __version__,
50
63
  audit_api_key_routes,
51
64
  build_auth_deps,
@@ -58,10 +71,13 @@ from fastapi_m8 import (
58
71
  derive_api_key_introspection_url,
59
72
  derive_service_token_url,
60
73
  derive_stream_url,
74
+ find_dotenv,
75
+ has_minimum_role,
76
+ has_superuser_privileges,
77
+ make_scrape_credential_guard,
78
+ render_metrics,
61
79
  )
62
80
  from auth_sdk_m8.schemas.api_key import ApiKeyPrincipal
63
- from auth_sdk_m8.schemas.base import RoleType
64
- from auth_sdk_m8.schemas.user import UserModel
65
81
 
66
82
 
67
83
  def build(settings: ConsumerServiceSettings) -> AuthDeps:
@@ -111,6 +127,38 @@ def audit(app: FastAPI, auth: AuthDeps) -> list[BareApiKeyDependency]:
111
127
  if bare_dep is None:
112
128
  return []
113
129
  return audit_api_key_routes(app, bare_dependency=bare_dep)
130
+
131
+
132
+ def use_role_predicates(user: UserModel) -> bool:
133
+ """The re-exported authorization predicates keep their SDK signatures."""
134
+ if has_superuser_privileges(user.role, user.is_superuser):
135
+ return True
136
+ return has_minimum_role(user.role, RoleType.WRITER)
137
+
138
+
139
+ def use_metrics_surface(app: FastAPI, credential: str | None) -> None:
140
+ """render_metrics / REGISTRY / the scrape guard stay typed through the
141
+ re-export."""
142
+ guard = make_scrape_credential_guard(credential)
143
+
144
+ @app.get("/metrics", dependencies=[Depends(guard)])
145
+ def metrics() -> Response:
146
+ payload, content_type = render_metrics()
147
+ return Response(content=payload, media_type=content_type)
148
+
149
+ list(REGISTRY.collect())
150
+
151
+
152
+ def use_schema_and_model_primitives() -> tuple[str, bool]:
153
+ """The schema/controller/model/constant primitives are real types, not Any."""
154
+ dotenv: Path = find_dotenv()
155
+ message: ResponseMessage = ResponseMessage(success=True, msg="ok")
156
+ wrapper: ResponseModelBase = ResponseModelBase(success=True, data=None)
157
+ controller: type[BaseController] = BaseController
158
+ mixin: type[TimestampMixin] = TimestampMixin
159
+ matched = ValidationConstants.KEY_REGEX.match(message.msg) is not None
160
+ assert controller is not None and mixin is not None and wrapper.success
161
+ return str(dotenv), matched
114
162
  '''
115
163
 
116
164
 
@@ -134,3 +182,43 @@ def test_all_exports_resolve_to_real_objects() -> None:
134
182
  assert hasattr(fastapi_m8, name), (
135
183
  f"{name!r} is listed in fastapi_m8.__all__ but is not importable"
136
184
  )
185
+
186
+
187
+ # Every reusable SDK primitive fastapi-m8 re-exports, mapped to the module it
188
+ # must come from. A consumer service imports these from fastapi_m8 and never
189
+ # from auth_sdk_m8, so a re-export that silently becomes a local wrapper — or
190
+ # quietly disappears — has to fail here rather than in three consumer repos.
191
+ _SDK_REEXPORTS: dict[str, str] = {
192
+ "has_superuser_privileges": "auth_sdk_m8.authorization",
193
+ "has_minimum_role": "auth_sdk_m8.authorization",
194
+ "RoleType": "auth_sdk_m8.schemas.base",
195
+ "BaseController": "auth_sdk_m8.controllers.base",
196
+ "ResponseModelBase": "auth_sdk_m8.schemas.base",
197
+ "ResponseMessage": "auth_sdk_m8.schemas.base",
198
+ "ValidationConstants": "auth_sdk_m8.schemas.shared",
199
+ "TimestampMixin": "auth_sdk_m8.models.shared",
200
+ "UserModel": "auth_sdk_m8.schemas.user",
201
+ "find_dotenv": "auth_sdk_m8.utils.paths",
202
+ "REGISTRY": "auth_sdk_m8.observability.metrics",
203
+ "make_scrape_credential_guard": "auth_sdk_m8.security.guards",
204
+ }
205
+
206
+
207
+ @pytest.mark.parametrize(("name", "module_path"), sorted(_SDK_REEXPORTS.items()))
208
+ def test_sdk_primitive_is_the_sdk_object_not_a_wrapper(
209
+ name: str, module_path: str
210
+ ) -> None:
211
+ """Each re-export *is* the SDK object, so rerouting an import is a no-op."""
212
+ module = importlib.import_module(module_path)
213
+ assert getattr(fastapi_m8, name) is getattr(module, name), (
214
+ f"fastapi_m8.{name} is not {module_path}.{name}"
215
+ )
216
+ assert name in fastapi_m8.__all__
217
+
218
+
219
+ def test_render_metrics_is_the_sdk_render_function() -> None:
220
+ """The one re-export that is renamed still points at the SDK function."""
221
+ from auth_sdk_m8.observability import metrics
222
+
223
+ assert fastapi_m8.render_metrics is metrics.render
224
+ assert fastapi_m8.REGISTRY is metrics.REGISTRY
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes