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.
- fastapi_m8-4.3.0/.markdownlint.yaml +43 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/CHANGELOG.md +93 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/PKG-INFO +56 -5
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/README.md +54 -3
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/__init__.py +16 -4
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_compat.py +8 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_version.py +1 -1
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/pyproject.toml +1 -1
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_packaging.py +21 -1
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_public_typing.py +91 -3
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.codacy.yml +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.dockerignore +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.env.example +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.gitattributes +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.github/FUNDING.yml +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.github/dependabot.yml +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.github/workflows/CI.yaml +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.github/workflows/PiPy.yml +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.gitignore +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.gitleaks.toml +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/.pydocstyle +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/AGENTS.md +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/CLAUDE.md +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/LICENSE +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/REPOSITORY_CONTEXT.md +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/SECURITY.md +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/constraints-all.txt +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/constraints.txt +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_api_key.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_app.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_async_stub.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_deps.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_engine.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_events.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_health.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_internal_auth.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_revocation.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/_route_audit.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/config.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/scripts/__init__.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/scripts/docker_start.sh +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/fastapi_m8/scripts/pre_start.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/__init__.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/conftest.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_api_key.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_api_key_deps.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_app.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_app_extra.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_async_stub.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_ci_policy.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_compat.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_config.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_config_file_secrets.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_deps.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_engine.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_event_signing_gate.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_events.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_fixture_matrix_contract.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_health.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_host_header_routing.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_internal_auth.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_meta.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_pre_start.py +0 -0
- {fastapi_m8-4.2.2 → fastapi_m8-4.3.0}/tests/test_revocation.py +0 -0
- {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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-m8
|
|
3
|
-
Version: 4.
|
|
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`
|
|
1424
|
-
`
|
|
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
|
|
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`
|
|
1167
|
-
`
|
|
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
|
|
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,
|
|
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]"
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|