mic-struct 0.0.1__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.
- mic_struct-0.0.1/.gitignore +28 -0
- mic_struct-0.0.1/CHANGELOG.md +582 -0
- mic_struct-0.0.1/LICENSE +21 -0
- mic_struct-0.0.1/PKG-INFO +435 -0
- mic_struct-0.0.1/README.md +286 -0
- mic_struct-0.0.1/pyproject.toml +391 -0
- mic_struct-0.0.1/src/mic/__init__.py +9 -0
- mic_struct-0.0.1/src/mic/_lazy.py +80 -0
- mic_struct-0.0.1/src/mic/auth/__init__.py +43 -0
- mic_struct-0.0.1/src/mic/auth/revoke_blacklist.py +247 -0
- mic_struct-0.0.1/src/mic/auth/service_auth.py +317 -0
- mic_struct-0.0.1/src/mic/authz/__init__.py +60 -0
- mic_struct-0.0.1/src/mic/authz/base.py +44 -0
- mic_struct-0.0.1/src/mic/authz/errors.py +27 -0
- mic_struct-0.0.1/src/mic/authz/in_memory.py +91 -0
- mic_struct-0.0.1/src/mic/authz/opa.py +131 -0
- mic_struct-0.0.1/src/mic/authz/openfga.py +167 -0
- mic_struct-0.0.1/src/mic/cache/__init__.py +31 -0
- mic_struct-0.0.1/src/mic/cache/backends.py +185 -0
- mic_struct-0.0.1/src/mic/cache/cluster/__init__.py +35 -0
- mic_struct-0.0.1/src/mic/cache/cluster/consistent_hash.py +116 -0
- mic_struct-0.0.1/src/mic/cache/cluster/redis_cluster.py +191 -0
- mic_struct-0.0.1/src/mic/cli/__init__.py +24 -0
- mic_struct-0.0.1/src/mic/cli/builder.py +176 -0
- mic_struct-0.0.1/src/mic/cli/spec.py +68 -0
- mic_struct-0.0.1/src/mic/client/__init__.py +26 -0
- mic_struct-0.0.1/src/mic/client/async_service_client.py +210 -0
- mic_struct-0.0.1/src/mic/client/service_client.py +229 -0
- mic_struct-0.0.1/src/mic/contracts/__init__.py +15 -0
- mic_struct-0.0.1/src/mic/contracts/envelope.py +45 -0
- mic_struct-0.0.1/src/mic/contracts/strict_schema.py +48 -0
- mic_struct-0.0.1/src/mic/datastorage/__init__.py +98 -0
- mic_struct-0.0.1/src/mic/datastorage/base.py +316 -0
- mic_struct-0.0.1/src/mic/datastorage/document/__init__.py +14 -0
- mic_struct-0.0.1/src/mic/datastorage/document/engine.py +166 -0
- mic_struct-0.0.1/src/mic/datastorage/document/in_memory.py +104 -0
- mic_struct-0.0.1/src/mic/datastorage/graph/__init__.py +12 -0
- mic_struct-0.0.1/src/mic/datastorage/graph/engine.py +131 -0
- mic_struct-0.0.1/src/mic/datastorage/keyvalue/__init__.py +11 -0
- mic_struct-0.0.1/src/mic/datastorage/keyvalue/in_memory.py +65 -0
- mic_struct-0.0.1/src/mic/datastorage/sql/__init__.py +10 -0
- mic_struct-0.0.1/src/mic/datastorage/sql/engine.py +152 -0
- mic_struct-0.0.1/src/mic/eventbus/__init__.py +70 -0
- mic_struct-0.0.1/src/mic/eventbus/base.py +54 -0
- mic_struct-0.0.1/src/mic/eventbus/in_memory.py +81 -0
- mic_struct-0.0.1/src/mic/eventbus/redis_streams.py +168 -0
- mic_struct-0.0.1/src/mic/fastapi/__init__.py +79 -0
- mic_struct-0.0.1/src/mic/fastapi/_resolve.py +86 -0
- mic_struct-0.0.1/src/mic/fastapi/middlewares.py +673 -0
- mic_struct-0.0.1/src/mic/fastapi/router.py +186 -0
- mic_struct-0.0.1/src/mic/grpc/__init__.py +75 -0
- mic_struct-0.0.1/src/mic/grpc/client.py +175 -0
- mic_struct-0.0.1/src/mic/grpc/errors.py +110 -0
- mic_struct-0.0.1/src/mic/grpc/interceptors.py +183 -0
- mic_struct-0.0.1/src/mic/grpc/rpc_spec.py +125 -0
- mic_struct-0.0.1/src/mic/grpc/server.py +56 -0
- mic_struct-0.0.1/src/mic/grpc/spec.py +42 -0
- mic_struct-0.0.1/src/mic/healthcheck/__init__.py +40 -0
- mic_struct-0.0.1/src/mic/healthcheck/readiness.py +239 -0
- mic_struct-0.0.1/src/mic/http/__init__.py +40 -0
- mic_struct-0.0.1/src/mic/http/api_key.py +120 -0
- mic_struct-0.0.1/src/mic/http/auth_gate.py +335 -0
- mic_struct-0.0.1/src/mic/http/basic_auth.py +114 -0
- mic_struct-0.0.1/src/mic/http/bearer.py +67 -0
- mic_struct-0.0.1/src/mic/http/correlation.py +169 -0
- mic_struct-0.0.1/src/mic/http/csrf.py +173 -0
- mic_struct-0.0.1/src/mic/http/request_context.py +55 -0
- mic_struct-0.0.1/src/mic/http/route_spec.py +140 -0
- mic_struct-0.0.1/src/mic/idempotency/__init__.py +77 -0
- mic_struct-0.0.1/src/mic/idempotency/store.py +716 -0
- mic_struct-0.0.1/src/mic/litestar/__init__.py +64 -0
- mic_struct-0.0.1/src/mic/litestar/_resolve.py +86 -0
- mic_struct-0.0.1/src/mic/litestar/middlewares.py +503 -0
- mic_struct-0.0.1/src/mic/litestar/router.py +241 -0
- mic_struct-0.0.1/src/mic/locking/__init__.py +68 -0
- mic_struct-0.0.1/src/mic/locking/base.py +51 -0
- mic_struct-0.0.1/src/mic/locking/in_memory.py +64 -0
- mic_struct-0.0.1/src/mic/locking/lock_handle.py +26 -0
- mic_struct-0.0.1/src/mic/locking/redis_backend.py +116 -0
- mic_struct-0.0.1/src/mic/locking/scope.py +58 -0
- mic_struct-0.0.1/src/mic/model/__init__.py +21 -0
- mic_struct-0.0.1/src/mic/model/errors.py +130 -0
- mic_struct-0.0.1/src/mic/observability/__init__.py +208 -0
- mic_struct-0.0.1/src/mic/observability/caller.py +102 -0
- mic_struct-0.0.1/src/mic/observability/circuit_breaker_metrics.py +111 -0
- mic_struct-0.0.1/src/mic/observability/log_context_middleware.py +140 -0
- mic_struct-0.0.1/src/mic/observability/logging.py +442 -0
- mic_struct-0.0.1/src/mic/observability/metrics.py +105 -0
- mic_struct-0.0.1/src/mic/observability/metrics_backend.py +656 -0
- mic_struct-0.0.1/src/mic/observability/metrics_guard.py +185 -0
- mic_struct-0.0.1/src/mic/observability/redact_logs.py +101 -0
- mic_struct-0.0.1/src/mic/observability/redact_traces.py +115 -0
- mic_struct-0.0.1/src/mic/observability/sentry.py +98 -0
- mic_struct-0.0.1/src/mic/observability/tracing.py +207 -0
- mic_struct-0.0.1/src/mic/observability/tracing_middleware.py +142 -0
- mic_struct-0.0.1/src/mic/outbox/__init__.py +111 -0
- mic_struct-0.0.1/src/mic/outbox/base.py +161 -0
- mic_struct-0.0.1/src/mic/outbox/dispatcher.py +345 -0
- mic_struct-0.0.1/src/mic/outbox/event.py +117 -0
- mic_struct-0.0.1/src/mic/outbox/in_memory.py +225 -0
- mic_struct-0.0.1/src/mic/outbox/sanitize.py +130 -0
- mic_struct-0.0.1/src/mic/outbox/sql.py +412 -0
- mic_struct-0.0.1/src/mic/py.typed +0 -0
- mic_struct-0.0.1/src/mic/queue/__init__.py +71 -0
- mic_struct-0.0.1/src/mic/queue/base.py +99 -0
- mic_struct-0.0.1/src/mic/queue/in_memory.py +87 -0
- mic_struct-0.0.1/src/mic/queue/kafka.py +120 -0
- mic_struct-0.0.1/src/mic/queue/nats.py +161 -0
- mic_struct-0.0.1/src/mic/ratelimit/__init__.py +89 -0
- mic_struct-0.0.1/src/mic/ratelimit/base.py +64 -0
- mic_struct-0.0.1/src/mic/ratelimit/in_memory.py +97 -0
- mic_struct-0.0.1/src/mic/ratelimit/middleware.py +138 -0
- mic_struct-0.0.1/src/mic/ratelimit/redis_backend.py +240 -0
- mic_struct-0.0.1/src/mic/ratelimit/spec.py +385 -0
- mic_struct-0.0.1/src/mic/read_models/__init__.py +64 -0
- mic_struct-0.0.1/src/mic/read_models/counter.py +78 -0
- mic_struct-0.0.1/src/mic/read_models/leaderboard.py +92 -0
- mic_struct-0.0.1/src/mic/read_models/membership_set.py +86 -0
- mic_struct-0.0.1/src/mic/read_models/timeline.py +97 -0
- mic_struct-0.0.1/src/mic/read_models/unique_count.py +73 -0
- mic_struct-0.0.1/src/mic/realtime/__init__.py +134 -0
- mic_struct-0.0.1/src/mic/realtime/auth.py +103 -0
- mic_struct-0.0.1/src/mic/realtime/backend.py +129 -0
- mic_struct-0.0.1/src/mic/realtime/endpoint.py +140 -0
- mic_struct-0.0.1/src/mic/realtime/redis_streams.py +141 -0
- mic_struct-0.0.1/src/mic/realtime/room.py +64 -0
- mic_struct-0.0.1/src/mic/resilience/__init__.py +44 -0
- mic_struct-0.0.1/src/mic/resilience/circuit_breaker.py +301 -0
- mic_struct-0.0.1/src/mic/response_cache/__init__.py +100 -0
- mic_struct-0.0.1/src/mic/response_cache/_stampede.py +90 -0
- mic_struct-0.0.1/src/mic/response_cache/_storage.py +109 -0
- mic_struct-0.0.1/src/mic/response_cache/middleware.py +289 -0
- mic_struct-0.0.1/src/mic/response_cache/rule.py +106 -0
- mic_struct-0.0.1/src/mic/shard/__init__.py +63 -0
- mic_struct-0.0.1/src/mic/shard/selector.py +145 -0
- mic_struct-0.0.1/src/mic/shard/session_factory.py +99 -0
- mic_struct-0.0.1/template/cookiecutter.json +8 -0
- mic_struct-0.0.1/template/hooks/post_gen_project.py +151 -0
- mic_struct-0.0.1/template/hooks/pre_gen_project.py +72 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/.gitignore +45 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/.python-version +1 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/README.md +69 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/alembic/env.py +74 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/alembic/script.py.mako +29 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/alembic/versions/20260507_baseline_widgets.py +44 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/alembic.ini +56 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/__init__.py +4 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/config/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/config/settings.py +88 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/context/__init__.py +5 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/context/execution_context.py +26 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/providers/__init__.py +6 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/providers/auth.py +21 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/providers/database.py +12 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/startup/__init__.py +6 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/startup/http_routes.py +21 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/startup/http_runtime.py +128 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/wiring/__init__.py +6 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/wiring/system.py +13 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/wiring/widgets.py +15 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/entrypoints/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/entrypoints/cli/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/entrypoints/http/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/entrypoints/http/main.py +27 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/cli/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/fastapi/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/fastapi/middlewares/__init__.py +5 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/fastapi/middlewares/auth.py +58 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/litestar/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/__init__.py +12 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/controller/__init__.py +6 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/controller/health.py +19 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/controller/readiness.py +37 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/interface/__init__.py +5 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/interface/http/__init__.py +5 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/interface/http/routes.py +28 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/__init__.py +21 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/controller/__init__.py +6 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/controller/create_widget.py +23 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/controller/read_widget.py +20 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/interface/__init__.py +5 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/interface/http/__init__.py +6 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/interface/http/payloads.py +12 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/interface/http/routes.py +73 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/model/__init__.py +5 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/model/persistence/__init__.py +5 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/model/persistence/widget.py +22 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/model/repositories/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/platform/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/platform/services/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/shared/__init__.py +0 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/pyproject.toml +99 -0
- mic_struct-0.0.1/template/{{cookiecutter.service_name}}/tools/bump_service.py +148 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
.venv/
|
|
5
|
+
.mypy_cache/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
|
|
9
|
+
# Build / packaging
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
.coverage
|
|
14
|
+
coverage.xml
|
|
15
|
+
|
|
16
|
+
# OS / Editor
|
|
17
|
+
.DS_Store
|
|
18
|
+
Thumbs.db
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
*.bak
|
|
23
|
+
|
|
24
|
+
# Reports / temp
|
|
25
|
+
reports/
|
|
26
|
+
.benchmarks/
|
|
27
|
+
.mutmut-cache/
|
|
28
|
+
.wily/
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **mic-struct** are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.0.1] — 2026-05-22
|
|
9
|
+
|
|
10
|
+
First public release of `mic-struct` — the building blocks below have
|
|
11
|
+
been consolidated, hardened, and locked behind a 100 % line + branch
|
|
12
|
+
coverage gate (+ xenon complexity gate, bandit SAST, pip-audit). Pre-1.0:
|
|
13
|
+
per semver 0.x, minor bumps may carry breaking changes.
|
|
14
|
+
|
|
15
|
+
### Removed — 2026-05-22 : `mic.feature_flags` + `mic.graphql` (orphelins, RDL-423)
|
|
16
|
+
|
|
17
|
+
Décision du jalon **RDL-423** (revue des modules orphelins, ADR-0012) : ces deux
|
|
18
|
+
modules n'avaient **aucun consumer** dans la flotte rdlink et **aucun plan
|
|
19
|
+
d'adoption** (≠ briques de montée en charge `queue`/`shard`, gardées différées) :
|
|
20
|
+
|
|
21
|
+
- `mic.feature_flags` — `FeatureFlagEngine` ABC + backends Unleash / GrowthBook /
|
|
22
|
+
in-memory. Extras `[unleash]` / `[growthbook]` supprimés.
|
|
23
|
+
- `mic.graphql` — adapter Strawberry code-first. Extra `[graphql]` + dépendance
|
|
24
|
+
`strawberry-graphql` supprimés.
|
|
25
|
+
|
|
26
|
+
**Breaking** (acceptable pré-1.0) : `mic-struct[graphql|unleash|growthbook]` et
|
|
27
|
+
les imports `mic.feature_flags.*` / `mic.graphql.*` ne sont plus disponibles. À
|
|
28
|
+
ré-introduire (rebrand `mic-struct-experimental` ou ré-import) si un besoin
|
|
29
|
+
produit émerge. **−737 LOC.**
|
|
30
|
+
|
|
31
|
+
### Added — 2026-05-22 : outillage qualité complexité + duplication (RDL-616)
|
|
32
|
+
|
|
33
|
+
Quatre outils, deux niveaux :
|
|
34
|
+
|
|
35
|
+
**Report-only** (hors gate, à la demande) :
|
|
36
|
+
- `make complexity` — `radon cc` (complexité cyclomatique A–F) + `radon mi`
|
|
37
|
+
(maintainability index) sur `src/mic`.
|
|
38
|
+
- `make complexity-trend` — `wily build` + `wily diff` (tendance dans l'historique
|
|
39
|
+
git ; cache `.wily/` git-ignoré ; requiert un arbre propre).
|
|
40
|
+
|
|
41
|
+
**Gate bloquant** (dans `make quality`/`gate`) :
|
|
42
|
+
- `make complexity-gate` — `xenon --max-absolute C --max-average A src/mic`.
|
|
43
|
+
Seuils dérivés de la baseline mic (moyenne **A=2.48**, pire bloc **C**, aucun
|
|
44
|
+
D/E/F) → passe à vert sans refactor. Pure Python → CI-safe.
|
|
45
|
+
|
|
46
|
+
**Duplication** (Node/npx, hors gate Python) :
|
|
47
|
+
- `make duplication` — `jscpd` seuil **3 %** (config `.jscpd.json` ; baseline mic
|
|
48
|
+
**1.49 % lignes / 2.39 % tokens**). À basculer bloquant côté CI une fois
|
|
49
|
+
l'image node-capable (cf. RDL-606).
|
|
50
|
+
|
|
51
|
+
Seuils proposés à ratifier dans l'ADR **RDL-601**. wily 1.25 épingle radon
|
|
52
|
+
`>=5.1,<5.2` (compatible xenon 0.9 + py314).
|
|
53
|
+
|
|
54
|
+
### Added — 2026-05-22 : `mic.grpc.GrpcRpcSpec` + `build_grpc_servicers` (couche gRPC dict-handler, RDL-266/267)
|
|
55
|
+
|
|
56
|
+
Couche gRPC *framework-agnostique* dict-in / dict-out, complémentaire de
|
|
57
|
+
`GrpcServiceSpec` (servicers générés par protoc) :
|
|
58
|
+
|
|
59
|
+
- `GrpcRpcSpec` : description d'un RPC unaire (`service_name`, `rpc_name`,
|
|
60
|
+
`handler: Callable[[dict], dict]`) — les handlers ne dépendent **pas** de
|
|
61
|
+
grpcio/protobuf, donc testables hors transport.
|
|
62
|
+
- `build_grpc_servicers(rpcs=...)` : regroupe par service et génère
|
|
63
|
+
dynamiquement une classe `Servicer` par service (une méthode par RPC) qui
|
|
64
|
+
duck-type le servicer protoc. Traduit `DomainError` → `INVALID_ARGUMENT` et
|
|
65
|
+
`TransientError` → `UNAVAILABLE` via `context.abort` (grpcio importé
|
|
66
|
+
paresseusement → module import-safe sans l'extra `[grpc]`).
|
|
67
|
+
|
|
68
|
+
Élimine la duplication identique de cette couche dans `rdlink-extract` et
|
|
69
|
+
`rdlink-moderation` (`app/shared/interface/grpc/rpc_spec.py` +
|
|
70
|
+
`app/frameworks/grpc/service_builder.py`) au profit d'un import `from mic.grpc`.
|
|
71
|
+
|
|
72
|
+
### Added — 2026-05-21 : `HttpRequestContext.subject_id` (identité caller au handler)
|
|
73
|
+
|
|
74
|
+
Champ optionnel `subject_id: str | None = None` sur `HttpRequestContext` :
|
|
75
|
+
l'identité résolue de l'appelant (posée par le `SubjectResolverMiddleware` du
|
|
76
|
+
consumer) est désormais transmise au handler, sans coupler les routes au
|
|
77
|
+
mécanisme d'auth. Publié en `0.0.1.dev10` (le champ avait été ajouté après la
|
|
78
|
+
publication de `dev9`, d'où le re-publish pour les consumers comme l'analytics
|
|
79
|
+
`unique_searchers` de rdlink-api).
|
|
80
|
+
|
|
81
|
+
### Added — 2026-05-20 : `mic.observability.RedactingLogFilter` (redaction logs, RDL-490)
|
|
82
|
+
|
|
83
|
+
Pilier *Logs* de la redaction MELT (épic RDL-488) — **clôt l'épic** (events ✅,
|
|
84
|
+
metrics ✅, traces ✅, logs ✅). RDL-490 spécifiait un processor structlog, mais
|
|
85
|
+
`mic.observability.logging` est en **stdlib `logging`** ; l'équivalent stdlib
|
|
86
|
+
est un `logging.Filter`. Reframé en conséquence :
|
|
87
|
+
|
|
88
|
+
- `RedactingLogFilter(logging.Filter)` : redacte récursivement les **champs
|
|
89
|
+
extra** d'un `LogRecord` (`logger.info(..., extra={...})`) avant émission,
|
|
90
|
+
selon des `FieldRedactors` injectés. À attacher au(x) handler(s).
|
|
91
|
+
- `redact_fields(value, redactors)` : primitive de redaction **récursive**
|
|
92
|
+
(dicts / listes / tuples imbriqués), renvoie une **copie** (n'altère pas
|
|
93
|
+
l'entrée). Complète `apply_field_redactors` (top-level).
|
|
94
|
+
- Champs standard du `LogRecord` jamais touchés ; stdlib-only (pas d'extra).
|
|
95
|
+
|
|
96
|
+
Portée V1 : champs extra structurés (vecteur PII principal). Le texte libre des
|
|
97
|
+
tracebacks (`exc_info`) n'est pas parsé — scrubber à la source.
|
|
98
|
+
|
|
99
|
+
### Added — 2026-05-20 : `mic.observability.RedactingSpanProcessor` (redaction traces, RDL-491)
|
|
100
|
+
|
|
101
|
+
Dernier pilier de la redaction MELT (épic RDL-488 ; events ✅ `sanitize_payload`,
|
|
102
|
+
metrics ✅ `SanitizingMetricsBackend`, **traces** = ce ticket). `SpanProcessor`
|
|
103
|
+
OTel qui scrubbe les attributs de span **et** d'events (`db.statement`,
|
|
104
|
+
`exception.message`, `http.request.body`, …) avant l'export OTLP/Tempo, selon
|
|
105
|
+
des `FieldRedactors` injectés (mécanisme, pas politique).
|
|
106
|
+
|
|
107
|
+
- Redaction in-place via le hook `_on_ending` (le seul moment où le span est
|
|
108
|
+
mutable — `on_end` reçoit un `ReadableSpan` immuable) : mutation directe de
|
|
109
|
+
`span._attributes` + reconstruction de `span._events` (les `Event` étant
|
|
110
|
+
immuables).
|
|
111
|
+
- Order-indépendant : tous les `_on_ending` tournent avant tous les `on_end`,
|
|
112
|
+
donc l'exporter voit toujours le span redacté.
|
|
113
|
+
- Extra `[tracing]` (subclasse `opentelemetry.sdk.trace.SpanProcessor`). Le test
|
|
114
|
+
d'intégration valide que le hook est appelé → fail-closed visible si une
|
|
115
|
+
version OTel le change (jamais d'export en clair silencieux).
|
|
116
|
+
|
|
117
|
+
### Added — 2026-05-20 : `mic.observability.SanitizingMetricsBackend` (garde-fou labels, RDL-493)
|
|
118
|
+
|
|
119
|
+
Décorateur `MetricsBackend` qui protège le TSDB Prometheus — pilier *Metrics*
|
|
120
|
+
de la redaction MELT (épic RDL-488, complète `sanitize_payload` côté events) :
|
|
121
|
+
|
|
122
|
+
- **Refus des labels sensitive/secret** (`denied_labels` injectés par le
|
|
123
|
+
consumer — mécanisme, pas politique, comme `TopicRedactionPolicy`) : un
|
|
124
|
+
`email`/`token`/`user_id` en label = fuite PII/secret dans les séries.
|
|
125
|
+
- **Cap de cardinalité** par métrique (`max_cardinality`, défaut 10 000) :
|
|
126
|
+
garde-fou contre l'explosion de séries (OOM Prometheus). Les combinaisons
|
|
127
|
+
déjà vues continuent de passer ; les nouvelles au-delà du cap sont refusées.
|
|
128
|
+
- **Deux modes** : `strict=True` (dev/CI) lève `MetricLabelViolation` ;
|
|
129
|
+
`strict=False` (prod) logge un warning, incrémente `mic_metric_labels_rejected_total{reason}`
|
|
130
|
+
(`denied_label` / `cardinality`) et **drop** l'émission fautive (jamais de
|
|
131
|
+
crash du hot path métier pour un souci d'observabilité).
|
|
132
|
+
|
|
133
|
+
Import-safe (ne tire que `MetricsBackend`, pas `prometheus_client`).
|
|
134
|
+
|
|
135
|
+
### Changed — 2026-05-20 : `TracingMiddleware` — `SpanKind.SERVER` + `http.route` (RDL-593 port)
|
|
136
|
+
|
|
137
|
+
Port upstream depuis le middleware custom rdlink-api (RDL-513) :
|
|
138
|
+
|
|
139
|
+
- Le span racine HTTP est désormais créé avec `kind=SpanKind.SERVER`
|
|
140
|
+
(sémantique span entrant — distingue le span serveur des spans clients
|
|
141
|
+
sortants dans le backend de traces).
|
|
142
|
+
- Attribut `http.route` (template de route, ex. `/users/{id}`) posé quand
|
|
143
|
+
le framework l'a renseigné dans `scope["route"]` après match — permet de
|
|
144
|
+
grouper les spans par pattern plutôt que par path concret (avec IDs). Set
|
|
145
|
+
en `finally` (présent même sur exception) ; absent sans crash sur un 404
|
|
146
|
+
(pas de route) ou un scope sans `.path`.
|
|
147
|
+
|
|
148
|
+
Rétro-compatible (attributs additifs ; passthrough inchangé sans OTel).
|
|
149
|
+
|
|
150
|
+
### Added — 2026-05-20 : `mic.observability.metrics_backend` (MetricsBackend ABC + RDL-592)
|
|
151
|
+
|
|
152
|
+
Harmonisation : `mic.observability` gagne l'abstraction backend des
|
|
153
|
+
implémentations custom rdlink (api/sync/ai), en complément des wrappers
|
|
154
|
+
thin Prometheus (`mic.observability.metrics`). Nouveau module
|
|
155
|
+
`mic.observability.metrics_backend` :
|
|
156
|
+
|
|
157
|
+
- `MetricsBackend` **ABC** — `increment` / `observe` (Summary) /
|
|
158
|
+
`set_gauge` / `observe_histogram` (Histogram agrégat server-side, fallback
|
|
159
|
+
`observe`) / `snapshot` / `render_prometheus`. Découple le call-site métier
|
|
160
|
+
du transport → un service injecte `InMemoryMetrics` en test, `Prometheus`
|
|
161
|
+
en prod.
|
|
162
|
+
- `InMemoryMetrics` (accumulateur thread-safe, `snapshot()`), `StatsDMetrics`
|
|
163
|
+
(UDP push), `PrometheusMetrics` (registry `prometheus_client` lazy ;
|
|
164
|
+
Counter/Summary/Histogram/Gauge ; gauges auto `*_metrics_backend_info` +
|
|
165
|
+
`*_process_start_time_seconds`).
|
|
166
|
+
- Type **Summary** (`observe` → quantiles client-side) distinct de
|
|
167
|
+
l'**Histogram** (`observe_histogram` → `histogram_quantile()` multi-replicas).
|
|
168
|
+
- **SLO bucket defaults** `DEFAULT_LATENCY_BUCKETS` (5 ms → 10 s, cible
|
|
169
|
+
p95 < 200 ms / p99 < 500 ms).
|
|
170
|
+
- **Sanitizers** (absorbe RDL-493) : `sanitize_metric_name` (namespace +
|
|
171
|
+
`.`/`-`/espaces → `_`), `sanitize_label_name`, `sanitize_labels` avec
|
|
172
|
+
allowlist `allowed=` — garde-fou anti high-cardinality. `PrometheusMetrics`
|
|
173
|
+
applique le filtre via `allowed_labels=`.
|
|
174
|
+
|
|
175
|
+
InMemory/StatsD restent stdlib-only (pas d'extra requis) ; Prometheus tire
|
|
176
|
+
`[observability]` en lazy à la construction.
|
|
177
|
+
|
|
178
|
+
### Added — 2026-05-20 : `mic.ratelimit` couche policy déclarative (RDL-591)
|
|
179
|
+
|
|
180
|
+
Harmonisation : `mic.ratelimit` gagne la couche policy déclarative des
|
|
181
|
+
implémentations custom rdlink-api, tout en conservant son backend
|
|
182
|
+
token-bucket robuste (anti clock-drift, EVALSHA, `TransientError`). Le
|
|
183
|
+
module `mic.ratelimit.spec` ajoute :
|
|
184
|
+
|
|
185
|
+
- `RateLimitSpec(bucket, requests_per_window, window_seconds, key)` —
|
|
186
|
+
politique lisible « N req / fenêtre », qui dérive ses paramètres
|
|
187
|
+
token-bucket (`capacity` = burst, `refill_per_second` = débit moyen).
|
|
188
|
+
- `RateLimitRegistry` — map `route_name → spec` avec **fallback global**
|
|
189
|
+
(`default`) ; `lookup` (exact) vs `resolve` (avec fallback).
|
|
190
|
+
- **Extracteurs d'identité composables**, framework-agnostiques (opèrent
|
|
191
|
+
sur un `KeyContext` headers/cookies/body/client_host/subject, pas sur
|
|
192
|
+
le scope ASGI brut) : `ip_key`, `subject_key`, `body_field_key`,
|
|
193
|
+
`cookie_key`, `header_key`, `static_key` + combinateurs
|
|
194
|
+
`first_available` (OR/fallback) et `combine` (AND). Retournent `None`
|
|
195
|
+
quand l'identité est non résolvable (politique « laisse passer » —
|
|
196
|
+
jamais de 429 forcé chez la victime).
|
|
197
|
+
- `enforce(spec, context, *, limiter, cost=1)` — moteur pur : extrait
|
|
198
|
+
l'identité, namespace la clé (`bucket:identity`), délègue au backend.
|
|
199
|
+
- `RateLimiter.try_consume` accepte désormais un **override per-call**
|
|
200
|
+
`capacity` / `refill_per_second` (rétro-compatible, `None` = défauts
|
|
201
|
+
constructeur) — un seul backend (client Redis partagé) sert toutes les
|
|
202
|
+
routes avec leurs politiques distinctes.
|
|
203
|
+
|
|
204
|
+
Hors scope (reste app-spécifique) : la résolution métier de l'identité
|
|
205
|
+
(lookup session depuis cookie, normalisation email OTP) se branche en
|
|
206
|
+
amont via les extracteurs.
|
|
207
|
+
|
|
208
|
+
### Added — 2026-05-20 : `mic.outbox.sanitize_payload` (redaction MELT par-topic)
|
|
209
|
+
|
|
210
|
+
Mécanisme de redaction des payloads outbox pour l'observability
|
|
211
|
+
(logs / traces / event-store public / audit long-terme), sans toucher
|
|
212
|
+
au payload original qui sert à la livraison. La **politique** (quels
|
|
213
|
+
champs, quelle stratégie) est config injectée par le consumer (dérivée
|
|
214
|
+
de l'ADR data-classification MELT) — `mic.outbox` fournit l'outil, pas
|
|
215
|
+
les listes PII app-spécifiques.
|
|
216
|
+
|
|
217
|
+
- `sanitize_payload(event, *, policy)` → copie redacted (l'event
|
|
218
|
+
original n'est pas muté).
|
|
219
|
+
- `TopicRedactionPolicy(default, per_topic)` : redactors par-field,
|
|
220
|
+
fusionnés (per_topic override default).
|
|
221
|
+
- 3 redactors standard alignés sur les stratégies MELT :
|
|
222
|
+
`redact_drop` (secret → `[REDACTED]`), `redact_mask` (sensitive
|
|
223
|
+
traces → `***`), `redact_hash` (sensitive logs/events → blake2 court
|
|
224
|
+
corrélable). Réutilise `mic.observability.logging.apply_field_redactors`.
|
|
225
|
+
|
|
226
|
+
Top-level only en V1 (les payloads outbox rdlink sont plats). Pur-stdlib
|
|
227
|
+
(pas de dépendance à l'extra `[observability]`).
|
|
228
|
+
|
|
229
|
+
### Added — 2026-05-20 : `mic.outbox` maturité V2 (lease-based claim + backoff + dédup temporel)
|
|
230
|
+
|
|
231
|
+
Suite de l'harmonisation V1 (idempotency_key + priority). `mic.outbox`
|
|
232
|
+
acquiert la maturité des implémentations outbox custom rdlink (api +
|
|
233
|
+
sync) — objectif : devenir le store production-grade que les consumers
|
|
234
|
+
peuvent adopter sans régression. Tout est rétro-compatible (defaults
|
|
235
|
+
neutres ; `pending()` et le comportement at-least-once classique
|
|
236
|
+
inchangés).
|
|
237
|
+
|
|
238
|
+
**Lease-based claim** (`OutboxStore.claim` + `recover_expired_processing`) :
|
|
239
|
+
|
|
240
|
+
- Nouveau `OutboxStatus.PROCESSING` + champ `OutboxEvent.next_run_at`.
|
|
241
|
+
- `claim(*, limit, lease_ttl_seconds, now)` : passe les events
|
|
242
|
+
claimables en `PROCESSING` avec un lease (`next_run_at = now +
|
|
243
|
+
lease_ttl`). Sur SQL : `FOR UPDATE SKIP LOCKED` → multi-worker safe
|
|
244
|
+
(events disjoints). Impl par défaut dans l'ABC = fallback `pending()`.
|
|
245
|
+
- `recover_expired_processing(*, now)` : leases expirés → `PENDING`
|
|
246
|
+
(reprise après crash worker). Impl par défaut = no-op.
|
|
247
|
+
- `OutboxDispatcher(use_claim=True, lease_ttl_seconds=...)` : `run_once`
|
|
248
|
+
recover puis claim au lieu de `pending`.
|
|
249
|
+
|
|
250
|
+
**Retry backoff + auto-DLQ** (`mark_failed`) :
|
|
251
|
+
|
|
252
|
+
- `mark_failed(..., backoff_seconds=0, max_attempts=None)` : si
|
|
253
|
+
`backoff_seconds > 0`, l'event reste `PENDING` avec `next_run_at =
|
|
254
|
+
now + backoff` (re-claim différé) ; si `max_attempts` atteint, auto-DLQ
|
|
255
|
+
(terminal) sans que le dispatcher ait à gérer le cap.
|
|
256
|
+
|
|
257
|
+
**Dédup temporel** (`enqueue(..., dedup_window_seconds=0)`) :
|
|
258
|
+
|
|
259
|
+
- Si `> 0` et qu'un event de même `(aggregate_type, aggregate_id,
|
|
260
|
+
event_type)` existe dans la fenêtre, retourne l'existant (absorbe un
|
|
261
|
+
burst d'events identiques sans clé pré-calculée). Testé après
|
|
262
|
+
`idempotency_key`.
|
|
263
|
+
|
|
264
|
+
Migration schéma (consumers SQL via Alembic) : 1 colonne supplémentaire
|
|
265
|
+
`next_run_at` (TIMESTAMPTZ nullable) sur table `outbox`, + le status
|
|
266
|
+
`processing` dans les valeurs possibles. `prepare()` (dev/tests) à jour.
|
|
267
|
+
|
|
268
|
+
Hors scope (restent app-spécifiques) : workflow status `acked`/`published`,
|
|
269
|
+
~30 routes admin CRUD, dispatchers spécialisés par topic.
|
|
270
|
+
|
|
271
|
+
### Added — 2026-05-20 : `mic.outbox` harmonisation V1 (idempotency_key + priority)
|
|
272
|
+
|
|
273
|
+
`OutboxEvent` gagne deux champs optionnels, rétro-compatibles (defaults
|
|
274
|
+
neutres) :
|
|
275
|
+
|
|
276
|
+
- **`idempotency_key: str | None = None`** — dédup côté **producteur**.
|
|
277
|
+
Quand `enqueue` reçoit un event dont la clé existe déjà dans le store,
|
|
278
|
+
il retourne l'event EXISTANT au lieu d'insérer un doublon (insert
|
|
279
|
+
idempotent). Implémenté sur `SqlOutboxStore` (SELECT-before-INSERT
|
|
280
|
+
partagé avec la session de l'enqueue → dédup atomique) et
|
|
281
|
+
`InMemoryOutboxStore`. La colonne SQL porte une contrainte `UNIQUE`
|
|
282
|
+
(plusieurs `NULL` permis — events sans clé ne collisionnent jamais).
|
|
283
|
+
- **`priority: int = 100`** — ordre de drain. **Plus bas = dispatché en
|
|
284
|
+
premier** (convention nice/cron) ; à priorité égale, FIFO `created_at`.
|
|
285
|
+
Tant que tous les events gardent le défaut 100, l'ordre reste
|
|
286
|
+
strictement FIFO (rétro-compat). `pending()` ordonne désormais par
|
|
287
|
+
`(priority, created_at)` côté SQL et `(priority, insertion)` côté RAM.
|
|
288
|
+
|
|
289
|
+
Motivation : aligner `mic.outbox` sur les besoins réels des consumers
|
|
290
|
+
(rdlink-api avait ces deux features inline sur 14 producers). Le swap
|
|
291
|
+
d'un store custom vers `mic.outbox.SqlOutboxStore` devient nettement
|
|
292
|
+
moins lossy. Hors scope V1 (restent app-spécifiques) : workflow status
|
|
293
|
+
enrichi (`processing`/`published`), `dedup_window_seconds` temporel,
|
|
294
|
+
retry backoff `next_run_at`.
|
|
295
|
+
|
|
296
|
+
Migration schéma : 2 nouvelles colonnes nullables sur la table `outbox`
|
|
297
|
+
(`idempotency_key` UNIQUE indexée, `priority` indexée default 100). Pas
|
|
298
|
+
de backfill requis (defaults). Pour les apps Alembic : ajouter une
|
|
299
|
+
migration ; `prepare()` (dev/tests) crée le schéma à jour.
|
|
300
|
+
|
|
301
|
+
### Added — 2026-05-19 : `mic.auth.ServiceAuthSigner.sign(extra_claims=...)`
|
|
302
|
+
|
|
303
|
+
`ServiceAuthSigner.sign()` accepte un paramètre optionnel
|
|
304
|
+
`extra_claims: dict[str, Any] | None` pour embedder des claims JWT
|
|
305
|
+
additionnels aux côtés du jeu standard (`iss`, `aud`, `iat`, `exp`,
|
|
306
|
+
`sub`, `jti`). Use case canonique : propager une identité applicative
|
|
307
|
+
secondaire (`profile_id`, `tenant_id`, `role`) pour que le consumer
|
|
308
|
+
puisse skipper un lookup DB sur les hot paths.
|
|
309
|
+
|
|
310
|
+
Sécurité : toute tentative d'override d'un claim réservé (`iss`,
|
|
311
|
+
`aud`, `iat`, `exp`, `sub`, `jti`, `nbf`) via `extra_claims` lève
|
|
312
|
+
`ValueError` au site d'appel — un override silencieux permettrait à
|
|
313
|
+
un caller de forger `aud`, faussement étendre `exp`, etc.
|
|
314
|
+
|
|
315
|
+
`ServiceAuthClaims` gagne un champ `extra: dict[str, Any] = {}` que
|
|
316
|
+
`ServiceAuthVerifier.verify()` peuple avec les claims non-standard.
|
|
317
|
+
Les tokens signés avec la version antérieure (sans extra_claims)
|
|
318
|
+
round-trippent avec `extra == {}`.
|
|
319
|
+
|
|
320
|
+
### BREAKING — 2026-05-17 : HTTP framework adapters reorganised
|
|
321
|
+
|
|
322
|
+
The FastAPI and Litestar integration code has been moved out of
|
|
323
|
+
`mic.http.*` into dedicated opt-in packages :
|
|
324
|
+
|
|
325
|
+
- `mic.http.fastapi_adapter` → `mic.fastapi.router`
|
|
326
|
+
- `mic.http.fastapi_middlewares` → `mic.fastapi.middlewares`
|
|
327
|
+
- `mic.http.litestar_adapter` → `mic.litestar.router`
|
|
328
|
+
- `mic.http._resolve` → `mic.fastapi._resolve` + `mic.litestar._resolve`
|
|
329
|
+
(per-package copy ; the module is private to each adapter)
|
|
330
|
+
|
|
331
|
+
The top-level convenience exports (`build_fastapi_router`, the three
|
|
332
|
+
middlewares, `build_litestar_router`) are now available via lazy
|
|
333
|
+
loading from `mic.fastapi` and `mic.litestar` :
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
# Before
|
|
337
|
+
from mic.http.fastapi_adapter import build_fastapi_router
|
|
338
|
+
from mic.http.fastapi_middlewares import ServiceAuthMiddleware
|
|
339
|
+
|
|
340
|
+
# After
|
|
341
|
+
from mic.fastapi import build_fastapi_router, ServiceAuthMiddleware
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The `[fastapi]` and `[litestar]` extras are kept (still pull `fastapi`
|
|
345
|
+
+ `uvicorn` / `litestar` respectively). They guard the OPT-IN packages
|
|
346
|
+
so a consumer who imports nothing from `mic.fastapi.*` / `mic.litestar.*`
|
|
347
|
+
never pulls those framework deps transitively.
|
|
348
|
+
|
|
349
|
+
**Rationale** : MIC dependency-inversion principle. `mic.http`
|
|
350
|
+
exposes only framework-agnostic primitives (`HttpRouteSpec`,
|
|
351
|
+
`HttpResponseSpec`, `HttpCookieSpec`, `HttpRequestContext`,
|
|
352
|
+
`HttpHandler`). The framework-specific code lives in clearly-named
|
|
353
|
+
sibling packages — a consumer who uses neither FastAPI nor Litestar
|
|
354
|
+
never imports a single line of framework code.
|
|
355
|
+
|
|
356
|
+
**Migration** : substitute the import paths as shown above. No code
|
|
357
|
+
changes — the symbols themselves (`build_fastapi_router`, middleware
|
|
358
|
+
class names, signatures) are unchanged. ~5 minutes per service.
|
|
359
|
+
|
|
360
|
+
The cookiecutter template under `template/` still references the old
|
|
361
|
+
import paths and is currently broken until a refonte commit lands —
|
|
362
|
+
documented at the top of the template files.
|
|
363
|
+
|
|
364
|
+
### Added
|
|
365
|
+
|
|
366
|
+
**Core — HTTP, contracts, auth**
|
|
367
|
+
|
|
368
|
+
- `mic.contracts` — `StrictSchema` (Pydantic v2, `extra=forbid`,
|
|
369
|
+
`frozen=True`), `HttpSuccessEnvelope[T]`, `wrap_http_success()`.
|
|
370
|
+
- `mic.model` — `DomainError(code, message, details)` + `TransientError`
|
|
371
|
+
(machine-readable, 4xx vs 503 mapping).
|
|
372
|
+
- `mic.http` — framework-agnostic `HttpRouteSpec` / `HttpResponseSpec` /
|
|
373
|
+
`HttpCookieSpec` / `HttpRequestContext` primitives. `HttpResponseSpec`
|
|
374
|
+
exposes an optional `cookies: tuple[HttpCookieSpec, ...]` field;
|
|
375
|
+
`HttpCookieSpec` defaults to `HttpOnly + Secure + SameSite=Lax +
|
|
376
|
+
Path=/` — the combination that covers 99 % of well-secured
|
|
377
|
+
session/auth cookies. Common patterns (clear-cookie on logout,
|
|
378
|
+
double-submit CSRF token with `httponly=False`, refresh-token with
|
|
379
|
+
`Path=/auth/refresh`, cross-domain federation with
|
|
380
|
+
`samesite="none"`) are expressed as declarative spec ; consumers
|
|
381
|
+
materialise them when translating `HttpResponseSpec` to their
|
|
382
|
+
framework response.
|
|
383
|
+
- `mic.http.bearer` — `parse_bearer_token(header)` : extract a Bearer
|
|
384
|
+
JWT from an `Authorization` header (RFC 6750, scheme case-insensitive).
|
|
385
|
+
- `mic.http.basic_auth` — `parse_basic_auth(header) -> (username,
|
|
386
|
+
password)` : decode Basic Auth credentials (RFC 7617, UTF-8). Strict
|
|
387
|
+
base64 validation (rejects crafted payloads with invalid chars);
|
|
388
|
+
password can be empty (caller decides semantic).
|
|
389
|
+
- `mic.http.api_key` — `parse_api_key(header, scheme=None)` : extract
|
|
390
|
+
an API key. Default `scheme=None` treats the header value as the key
|
|
391
|
+
itself (`X-API-Key` pattern, à la Stripe / OpenAI). With `scheme="ApiKey"`
|
|
392
|
+
(or any custom name), expects `Authorization: ApiKey <key>` format.
|
|
393
|
+
- `mic.grpc.make_authed_channel` — new `subject_provider:
|
|
394
|
+
Callable[[], str | None] = None` kwarg for end-user delegated auth.
|
|
395
|
+
When set, the interceptor calls it per-RPC to resolve the `sub`
|
|
396
|
+
claim ; pattern : the caller's HTTP middleware sets `user_id` in a
|
|
397
|
+
ContextVar, the `subject_provider` reads it at the outbound RPC
|
|
398
|
+
moment. The downstream service then logs the operation with the
|
|
399
|
+
real user identity, not just the calling service's. Default `None`
|
|
400
|
+
= JWT without `sub` (service-to-service pure).
|
|
401
|
+
- `mic.idempotency.IdempotencyStore` — fusion of the single-flight
|
|
402
|
+
Stripe-style pattern with the "check-then-act with payload-hash
|
|
403
|
+
mismatch detection" pattern :
|
|
404
|
+
- `IdempotencyRecord.payload_hash: str | None` (new optional field,
|
|
405
|
+
backward-compatible — legacy records without the field still
|
|
406
|
+
dedupe).
|
|
407
|
+
- `IdempotencyAttempt.mismatch: bool` (new) — set when
|
|
408
|
+
`acquire_or_replay` observes a cached record whose stored
|
|
409
|
+
`payload_hash` differs from the caller-provided one. No lock is
|
|
410
|
+
acquired in that case ; the caller returns 409 Stripe-style.
|
|
411
|
+
- `IdempotencyDecision(outcome, record)` dataclass — return type of
|
|
412
|
+
the new `evaluate(key, *, payload_hash=None)` method, a check-only
|
|
413
|
+
path that does not acquire a lock (caller composes their own
|
|
414
|
+
persistence — accepts the TOCTOU race window).
|
|
415
|
+
- `compute_payload_hash(payload)` — SHA-256 canonical (json.dumps
|
|
416
|
+
sort_keys + tight separators) helper, exposed for callers that
|
|
417
|
+
want to compute the hash themselves.
|
|
418
|
+
- `mic.fastapi.MetricsMiddleware` + `mic.litestar.MetricsMiddleware` —
|
|
419
|
+
measure latency + status per request and delegate the emission to a
|
|
420
|
+
consumer-injected `MetricsRecorder` (Protocol with one method:
|
|
421
|
+
`record(method, route, status, duration_seconds)`). Route label
|
|
422
|
+
defaults to `"unknown"` when no template matched, to keep
|
|
423
|
+
cardinality bounded for Prometheus. The middleware is a no-op when
|
|
424
|
+
`recorder=None`, useful for test harnesses that don't wire a backend.
|
|
425
|
+
- `mic.http.csrf` — framework-agnostic CSRF double-submit cookie/header
|
|
426
|
+
engine: `CsrfPolicy` (overrideable header/cookie names + auth-cookie
|
|
427
|
+
list + exempt paths/prefixes + protected methods), `CsrfDecision`
|
|
428
|
+
dataclass with `outcome` (proceed/exempt/reject) + machine-readable
|
|
429
|
+
`reason`, pure `evaluate_csrf(...)` helper. Adapters
|
|
430
|
+
`mic.fastapi.CsrfMiddleware` + `mic.litestar.CsrfMiddleware`
|
|
431
|
+
materialise the engine output ; the 403 response shape is decided
|
|
432
|
+
by the consumer via an `on_reject(request, reason)` callback —
|
|
433
|
+
mic stays agnostic of the envelope catalog.
|
|
434
|
+
- `mic.http.HttpRouteSpec.auth: AuthMode = AuthMode.PUBLIC` — optional
|
|
435
|
+
per-route auth mode declaration, consumed by
|
|
436
|
+
`mic.fastapi.AuthGateMiddleware` / `mic.litestar.AuthGateMiddleware`
|
|
437
|
+
via `build_auth_policy(route_modes={(r.method, r.path): r.auth for r in routes})`.
|
|
438
|
+
Default ``PUBLIC`` is backward-compatible — consumers that don't use
|
|
439
|
+
the gate auth ignore the field (just an extra frozen attribute on
|
|
440
|
+
the dataclass).
|
|
441
|
+
- `mic.http.correlation` — framework-agnostic HTTP correlation engine:
|
|
442
|
+
`CorrelationSpec` (overridable header names + ID factories),
|
|
443
|
+
`CorrelationStart` / `CorrelationFinish` dataclasses, pure
|
|
444
|
+
`start_correlation(...)` / `finish_correlation(...)` helpers.
|
|
445
|
+
Adapters `mic.fastapi.CorrelationMiddleware` +
|
|
446
|
+
`mic.litestar.CorrelationMiddleware` materialise the engine output
|
|
447
|
+
on the request (`request.state.{correlation_id,request_id,duration_ms}`)
|
|
448
|
+
and the response headers (`X-Correlation-Id`, `X-Request-Id`,
|
|
449
|
+
`X-Response-Time-ms` defaults). Optional `on_start` callback lets
|
|
450
|
+
the consumer propagate the ids into its structured logger
|
|
451
|
+
context-var (mic stays agnostic of the logger).
|
|
452
|
+
- `mic.http.auth_gate` — framework-agnostic per-route auth gate
|
|
453
|
+
engine : `AuthMode` (PUBLIC / USER / ADMIN / SERVICE), `AuthDecision`,
|
|
454
|
+
`AuthPolicy`, `evaluate_auth()`, `build_auth_policy()`. Sister
|
|
455
|
+
primitive to `mic.fastapi.ServiceAuthMiddleware`, but for the
|
|
456
|
+
matricial case (every route declares its own auth mode, multiple
|
|
457
|
+
credential sources : cookie + bearer, role check + crypto verify
|
|
458
|
+
via opt-in callbacks). Adapters `mic.fastapi.AuthGateMiddleware`
|
|
459
|
+
and `mic.litestar.AuthGateMiddleware` materialise the decision into
|
|
460
|
+
an HTTP response via consumer-provided `on_missing_credentials` /
|
|
461
|
+
`on_forbidden` callbacks — mic does not impose an error body shape
|
|
462
|
+
(the consumer keeps its envelope catalog).
|
|
463
|
+
- All 4 parsers emit `DomainError` with categorised codes
|
|
464
|
+
(`service_auth.invalid_header`, `basic_auth.invalid_header`,
|
|
465
|
+
`api_key.invalid_header`) — middlewares map them to HTTP 401 +
|
|
466
|
+
`WWW-Authenticate` header. The parsing is purely transport — secret
|
|
467
|
+
verification (constant-time compare against a hash, DB lookup,
|
|
468
|
+
rate-limit per key, etc.) stays on the caller.
|
|
469
|
+
- `mic.auth` — `ServiceAuthSigner` / `ServiceAuthVerifier`: HS256
|
|
470
|
+
audience-bound JWT with double-key rotation and an issuer whitelist.
|
|
471
|
+
The verifier iterates all secrets (timing-safe) and never leaks the
|
|
472
|
+
underlying PyJWT message or the issuer whitelist back to the caller.
|
|
473
|
+
`sign()` auto-generates a unique `jti` claim (RFC 7519 §4.1.7) per
|
|
474
|
+
token via `uuid.uuid4().hex` (128 bits of entropy), with an optional
|
|
475
|
+
override for advanced use cases (deterministic test fixtures, trace-id
|
|
476
|
+
propagation). `ServiceAuthClaims.jti` is exposed so consumers can do
|
|
477
|
+
per-token revocation through `RevokeBlacklist.revoke_jti(claims.jti,
|
|
478
|
+
ttl=remaining_lifetime)` without coordination signer↔caller. Legacy
|
|
479
|
+
tokens emitted by services that don't propagate `jti` still verify
|
|
480
|
+
(the claim stays `None`).
|
|
481
|
+
- `mic.auth` — `RevokeBlacklist`: revocation tracker for short-lived
|
|
482
|
+
JWTs backed by any `CacheBackend`. Two complementary patterns —
|
|
483
|
+
per-`jti` (single-token revoke, e.g. logout-this-device or refresh
|
|
484
|
+
rotation) and per-subject revoke-epoch (mass revocation, e.g.
|
|
485
|
+
"logout everywhere" or password change). Auto-cleanup via TTL
|
|
486
|
+
(Redis EXPIRE), O(1) per check. Decoupled from `ServiceAuthVerifier`
|
|
487
|
+
so the verifier stays stateless and the blacklist stays optional.
|
|
488
|
+
- `mic.client` — `ServiceHttpClient`: thin httpx wrapper with
|
|
489
|
+
auto-signing and canonical error mapping.
|
|
490
|
+
- `mic.client` — `AsyncServiceHttpClient`: async-native variant of
|
|
491
|
+
`ServiceHttpClient`, built on `httpx.AsyncClient`. Same contract
|
|
492
|
+
(auto JWT signing, 4xx→`DomainError`, 5xx/network/timeout→
|
|
493
|
+
`TransientError`, optional `CircuitBreaker.async_call`, optional
|
|
494
|
+
`Idempotency-Key` factory, W3C trace-context propagation). Async
|
|
495
|
+
lifecycle (`aclose()` / `__aenter__` / `__aexit__`). For BFFs and
|
|
496
|
+
async-native services that fan out via `asyncio.gather` instead of
|
|
497
|
+
`ThreadPoolExecutor`.
|
|
498
|
+
- `mic.cli` — `CliCommandSpec`, `build_parser()`, `dispatch()`:
|
|
499
|
+
argparse-based operator CLI scaffold (stdlib only).
|
|
500
|
+
|
|
501
|
+
**Data — storage, caching, sharding**
|
|
502
|
+
|
|
503
|
+
- `mic.datastorage` — `DataStorage` ABC (mandatory lifecycle: `check_health` /
|
|
504
|
+
`prepare` / `dispose`) + optional capability mixins
|
|
505
|
+
(`KeyValueCapability` / `DocumentCapability` / `GraphCapability`).
|
|
506
|
+
Implementations: `SqlDataStorage` (SQLModel), `DocumentDataStorage` (MongoDB),
|
|
507
|
+
`GraphDataStorage` (Neo4j), `InMemoryKeyValueDataStorage`,
|
|
508
|
+
`InMemoryDocumentDataStorage`.
|
|
509
|
+
- `mic.cache` — `CacheBackend` ABC + `InMemoryCache`, `RedisCache`,
|
|
510
|
+
`RedisClusterCache` (with `ConsistentHashRing`).
|
|
511
|
+
- `mic.response_cache` — ASGI middleware: HTTP response caching with
|
|
512
|
+
ETag, cache-stampede protection, and SHA-256 vary keys (length-prefixed
|
|
513
|
+
per field to prevent cross-user key collisions).
|
|
514
|
+
- `mic.read_models` — denormalised CQRS read-model helpers (Timeline,
|
|
515
|
+
Counter, Leaderboard, Membership, UniqueCount).
|
|
516
|
+
- `mic.shard` — application-level Postgres sharding (`ShardSelector`,
|
|
517
|
+
`ShardedSessionFactory`).
|
|
518
|
+
|
|
519
|
+
**Messaging — events, queues, realtime**
|
|
520
|
+
|
|
521
|
+
- `mic.eventbus` — `EventBus` ABC: sync pub/sub (`InMemoryEventBus`,
|
|
522
|
+
`RedisStreamsEventBus`).
|
|
523
|
+
- `mic.queue` — `AsyncEventBus` ABC: durable async pub/sub
|
|
524
|
+
(`NATSEventBus`, `KafkaEventBus`, in-memory) with per-message ack.
|
|
525
|
+
- `mic.realtime` — WebSocket fan-out: `RealtimeBackend` ABC (in-memory /
|
|
526
|
+
Redis Streams), `Room`, audience-bound WebSocket authenticator.
|
|
527
|
+
- `mic.outbox` — transactional outbox pattern: `OutboxStore` ABC +
|
|
528
|
+
`OutboxDispatcher` with at-least-once delivery, a capped retry budget
|
|
529
|
+
(`max_attempts` → dead-letter), a transient-exception whitelist, and
|
|
530
|
+
Prometheus counters.
|
|
531
|
+
|
|
532
|
+
**Reliability — resilience, rate limiting, idempotency, locking**
|
|
533
|
+
|
|
534
|
+
- `mic.resilience` — `CircuitBreaker` (sync + async). Half-open state
|
|
535
|
+
admits exactly one probe at a time; concurrent calls are rejected
|
|
536
|
+
rather than amplified onto a still-failing upstream.
|
|
537
|
+
- `mic.ratelimit` — `RateLimiter` ABC: token bucket. `RedisRateLimiter`
|
|
538
|
+
derives time from the Redis server clock (`redis.call('TIME')`), so a
|
|
539
|
+
replica with a skewed wall clock cannot bypass the limit.
|
|
540
|
+
- `mic.idempotency` — `IdempotencyStore` with a single-flight
|
|
541
|
+
`acquire_or_replay` / `complete` / `fail` API (a distributed lock
|
|
542
|
+
closes the check-then-act window, so concurrent requests with the
|
|
543
|
+
same `Idempotency-Key` run the handler exactly once).
|
|
544
|
+
- `mic.locking` — `DistributedLock` ABC: `InMemoryDistributedLock` and
|
|
545
|
+
`RedisDistributedLock` (Redlock-style `SET NX` + Lua-atomic release).
|
|
546
|
+
|
|
547
|
+
**Policy & observability**
|
|
548
|
+
|
|
549
|
+
- `mic.authz` — `AuthorizationEngine` ABC: inline RBAC, OPA Rego,
|
|
550
|
+
OpenFGA ReBAC.
|
|
551
|
+
- `mic.feature_flags` — `FeatureFlagEngine` ABC: in-memory, Unleash,
|
|
552
|
+
GrowthBook.
|
|
553
|
+
- `mic.observability` — `configure_logging()` (JSON / text, idempotent,
|
|
554
|
+
with automatic OpenTelemetry `trace_id` / `span_id` injection when a
|
|
555
|
+
span is active) + idempotent Prometheus `counter()` / `gauge()` /
|
|
556
|
+
`histogram()` + opt-in Sentry helper + OpenTelemetry tracing.
|
|
557
|
+
- `mic.healthcheck` — `ReadinessChecker` and probes; distinguishes
|
|
558
|
+
`/health` (liveness) from `/ready` (dependency readiness).
|
|
559
|
+
|
|
560
|
+
**Transports**
|
|
561
|
+
|
|
562
|
+
- `mic.grpc` — `GrpcServiceSpec`, server + client builders, audience-bound
|
|
563
|
+
auth interceptor, gRPC health-check service.
|
|
564
|
+
- `mic.graphql` — `GraphqlSchemaSpec`, Strawberry code-first schema +
|
|
565
|
+
ASGI mount + audience-bound auth extension.
|
|
566
|
+
|
|
567
|
+
**Packaging & tooling**
|
|
568
|
+
|
|
569
|
+
- Every optional integration sits behind a `pip` extra; a bare
|
|
570
|
+
`pip install mic-struct` pulls only Pydantic. Importing any
|
|
571
|
+
`mic.<package>` is safe even when its extra is not installed —
|
|
572
|
+
the missing-dependency error is raised (with a clear `pip install`
|
|
573
|
+
hint) only when a dependency-backed symbol is actually used.
|
|
574
|
+
- Cookiecutter scaffold under `template/` — answer 3 questions, get a
|
|
575
|
+
complete FastAPI or Litestar service that passes `make gate` at 100 %
|
|
576
|
+
coverage on the first run.
|
|
577
|
+
- Quality gate: `black`, `ruff` (incl. `PT011` — every `pytest.raises`
|
|
578
|
+
needs a `match=`), `mypy --strict`, `bandit`, `pip-audit`, a
|
|
579
|
+
documentation-drift lint, and 100 % line + branch test coverage
|
|
580
|
+
(1124 tests). Mutation testing and performance benchmarks run in
|
|
581
|
+
nightly CI.
|
|
582
|
+
- Python 3.14, MIT-licensed.
|
mic_struct-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mic-struct contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|