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.
Files changed (196) hide show
  1. mic_struct-0.0.1/.gitignore +28 -0
  2. mic_struct-0.0.1/CHANGELOG.md +582 -0
  3. mic_struct-0.0.1/LICENSE +21 -0
  4. mic_struct-0.0.1/PKG-INFO +435 -0
  5. mic_struct-0.0.1/README.md +286 -0
  6. mic_struct-0.0.1/pyproject.toml +391 -0
  7. mic_struct-0.0.1/src/mic/__init__.py +9 -0
  8. mic_struct-0.0.1/src/mic/_lazy.py +80 -0
  9. mic_struct-0.0.1/src/mic/auth/__init__.py +43 -0
  10. mic_struct-0.0.1/src/mic/auth/revoke_blacklist.py +247 -0
  11. mic_struct-0.0.1/src/mic/auth/service_auth.py +317 -0
  12. mic_struct-0.0.1/src/mic/authz/__init__.py +60 -0
  13. mic_struct-0.0.1/src/mic/authz/base.py +44 -0
  14. mic_struct-0.0.1/src/mic/authz/errors.py +27 -0
  15. mic_struct-0.0.1/src/mic/authz/in_memory.py +91 -0
  16. mic_struct-0.0.1/src/mic/authz/opa.py +131 -0
  17. mic_struct-0.0.1/src/mic/authz/openfga.py +167 -0
  18. mic_struct-0.0.1/src/mic/cache/__init__.py +31 -0
  19. mic_struct-0.0.1/src/mic/cache/backends.py +185 -0
  20. mic_struct-0.0.1/src/mic/cache/cluster/__init__.py +35 -0
  21. mic_struct-0.0.1/src/mic/cache/cluster/consistent_hash.py +116 -0
  22. mic_struct-0.0.1/src/mic/cache/cluster/redis_cluster.py +191 -0
  23. mic_struct-0.0.1/src/mic/cli/__init__.py +24 -0
  24. mic_struct-0.0.1/src/mic/cli/builder.py +176 -0
  25. mic_struct-0.0.1/src/mic/cli/spec.py +68 -0
  26. mic_struct-0.0.1/src/mic/client/__init__.py +26 -0
  27. mic_struct-0.0.1/src/mic/client/async_service_client.py +210 -0
  28. mic_struct-0.0.1/src/mic/client/service_client.py +229 -0
  29. mic_struct-0.0.1/src/mic/contracts/__init__.py +15 -0
  30. mic_struct-0.0.1/src/mic/contracts/envelope.py +45 -0
  31. mic_struct-0.0.1/src/mic/contracts/strict_schema.py +48 -0
  32. mic_struct-0.0.1/src/mic/datastorage/__init__.py +98 -0
  33. mic_struct-0.0.1/src/mic/datastorage/base.py +316 -0
  34. mic_struct-0.0.1/src/mic/datastorage/document/__init__.py +14 -0
  35. mic_struct-0.0.1/src/mic/datastorage/document/engine.py +166 -0
  36. mic_struct-0.0.1/src/mic/datastorage/document/in_memory.py +104 -0
  37. mic_struct-0.0.1/src/mic/datastorage/graph/__init__.py +12 -0
  38. mic_struct-0.0.1/src/mic/datastorage/graph/engine.py +131 -0
  39. mic_struct-0.0.1/src/mic/datastorage/keyvalue/__init__.py +11 -0
  40. mic_struct-0.0.1/src/mic/datastorage/keyvalue/in_memory.py +65 -0
  41. mic_struct-0.0.1/src/mic/datastorage/sql/__init__.py +10 -0
  42. mic_struct-0.0.1/src/mic/datastorage/sql/engine.py +152 -0
  43. mic_struct-0.0.1/src/mic/eventbus/__init__.py +70 -0
  44. mic_struct-0.0.1/src/mic/eventbus/base.py +54 -0
  45. mic_struct-0.0.1/src/mic/eventbus/in_memory.py +81 -0
  46. mic_struct-0.0.1/src/mic/eventbus/redis_streams.py +168 -0
  47. mic_struct-0.0.1/src/mic/fastapi/__init__.py +79 -0
  48. mic_struct-0.0.1/src/mic/fastapi/_resolve.py +86 -0
  49. mic_struct-0.0.1/src/mic/fastapi/middlewares.py +673 -0
  50. mic_struct-0.0.1/src/mic/fastapi/router.py +186 -0
  51. mic_struct-0.0.1/src/mic/grpc/__init__.py +75 -0
  52. mic_struct-0.0.1/src/mic/grpc/client.py +175 -0
  53. mic_struct-0.0.1/src/mic/grpc/errors.py +110 -0
  54. mic_struct-0.0.1/src/mic/grpc/interceptors.py +183 -0
  55. mic_struct-0.0.1/src/mic/grpc/rpc_spec.py +125 -0
  56. mic_struct-0.0.1/src/mic/grpc/server.py +56 -0
  57. mic_struct-0.0.1/src/mic/grpc/spec.py +42 -0
  58. mic_struct-0.0.1/src/mic/healthcheck/__init__.py +40 -0
  59. mic_struct-0.0.1/src/mic/healthcheck/readiness.py +239 -0
  60. mic_struct-0.0.1/src/mic/http/__init__.py +40 -0
  61. mic_struct-0.0.1/src/mic/http/api_key.py +120 -0
  62. mic_struct-0.0.1/src/mic/http/auth_gate.py +335 -0
  63. mic_struct-0.0.1/src/mic/http/basic_auth.py +114 -0
  64. mic_struct-0.0.1/src/mic/http/bearer.py +67 -0
  65. mic_struct-0.0.1/src/mic/http/correlation.py +169 -0
  66. mic_struct-0.0.1/src/mic/http/csrf.py +173 -0
  67. mic_struct-0.0.1/src/mic/http/request_context.py +55 -0
  68. mic_struct-0.0.1/src/mic/http/route_spec.py +140 -0
  69. mic_struct-0.0.1/src/mic/idempotency/__init__.py +77 -0
  70. mic_struct-0.0.1/src/mic/idempotency/store.py +716 -0
  71. mic_struct-0.0.1/src/mic/litestar/__init__.py +64 -0
  72. mic_struct-0.0.1/src/mic/litestar/_resolve.py +86 -0
  73. mic_struct-0.0.1/src/mic/litestar/middlewares.py +503 -0
  74. mic_struct-0.0.1/src/mic/litestar/router.py +241 -0
  75. mic_struct-0.0.1/src/mic/locking/__init__.py +68 -0
  76. mic_struct-0.0.1/src/mic/locking/base.py +51 -0
  77. mic_struct-0.0.1/src/mic/locking/in_memory.py +64 -0
  78. mic_struct-0.0.1/src/mic/locking/lock_handle.py +26 -0
  79. mic_struct-0.0.1/src/mic/locking/redis_backend.py +116 -0
  80. mic_struct-0.0.1/src/mic/locking/scope.py +58 -0
  81. mic_struct-0.0.1/src/mic/model/__init__.py +21 -0
  82. mic_struct-0.0.1/src/mic/model/errors.py +130 -0
  83. mic_struct-0.0.1/src/mic/observability/__init__.py +208 -0
  84. mic_struct-0.0.1/src/mic/observability/caller.py +102 -0
  85. mic_struct-0.0.1/src/mic/observability/circuit_breaker_metrics.py +111 -0
  86. mic_struct-0.0.1/src/mic/observability/log_context_middleware.py +140 -0
  87. mic_struct-0.0.1/src/mic/observability/logging.py +442 -0
  88. mic_struct-0.0.1/src/mic/observability/metrics.py +105 -0
  89. mic_struct-0.0.1/src/mic/observability/metrics_backend.py +656 -0
  90. mic_struct-0.0.1/src/mic/observability/metrics_guard.py +185 -0
  91. mic_struct-0.0.1/src/mic/observability/redact_logs.py +101 -0
  92. mic_struct-0.0.1/src/mic/observability/redact_traces.py +115 -0
  93. mic_struct-0.0.1/src/mic/observability/sentry.py +98 -0
  94. mic_struct-0.0.1/src/mic/observability/tracing.py +207 -0
  95. mic_struct-0.0.1/src/mic/observability/tracing_middleware.py +142 -0
  96. mic_struct-0.0.1/src/mic/outbox/__init__.py +111 -0
  97. mic_struct-0.0.1/src/mic/outbox/base.py +161 -0
  98. mic_struct-0.0.1/src/mic/outbox/dispatcher.py +345 -0
  99. mic_struct-0.0.1/src/mic/outbox/event.py +117 -0
  100. mic_struct-0.0.1/src/mic/outbox/in_memory.py +225 -0
  101. mic_struct-0.0.1/src/mic/outbox/sanitize.py +130 -0
  102. mic_struct-0.0.1/src/mic/outbox/sql.py +412 -0
  103. mic_struct-0.0.1/src/mic/py.typed +0 -0
  104. mic_struct-0.0.1/src/mic/queue/__init__.py +71 -0
  105. mic_struct-0.0.1/src/mic/queue/base.py +99 -0
  106. mic_struct-0.0.1/src/mic/queue/in_memory.py +87 -0
  107. mic_struct-0.0.1/src/mic/queue/kafka.py +120 -0
  108. mic_struct-0.0.1/src/mic/queue/nats.py +161 -0
  109. mic_struct-0.0.1/src/mic/ratelimit/__init__.py +89 -0
  110. mic_struct-0.0.1/src/mic/ratelimit/base.py +64 -0
  111. mic_struct-0.0.1/src/mic/ratelimit/in_memory.py +97 -0
  112. mic_struct-0.0.1/src/mic/ratelimit/middleware.py +138 -0
  113. mic_struct-0.0.1/src/mic/ratelimit/redis_backend.py +240 -0
  114. mic_struct-0.0.1/src/mic/ratelimit/spec.py +385 -0
  115. mic_struct-0.0.1/src/mic/read_models/__init__.py +64 -0
  116. mic_struct-0.0.1/src/mic/read_models/counter.py +78 -0
  117. mic_struct-0.0.1/src/mic/read_models/leaderboard.py +92 -0
  118. mic_struct-0.0.1/src/mic/read_models/membership_set.py +86 -0
  119. mic_struct-0.0.1/src/mic/read_models/timeline.py +97 -0
  120. mic_struct-0.0.1/src/mic/read_models/unique_count.py +73 -0
  121. mic_struct-0.0.1/src/mic/realtime/__init__.py +134 -0
  122. mic_struct-0.0.1/src/mic/realtime/auth.py +103 -0
  123. mic_struct-0.0.1/src/mic/realtime/backend.py +129 -0
  124. mic_struct-0.0.1/src/mic/realtime/endpoint.py +140 -0
  125. mic_struct-0.0.1/src/mic/realtime/redis_streams.py +141 -0
  126. mic_struct-0.0.1/src/mic/realtime/room.py +64 -0
  127. mic_struct-0.0.1/src/mic/resilience/__init__.py +44 -0
  128. mic_struct-0.0.1/src/mic/resilience/circuit_breaker.py +301 -0
  129. mic_struct-0.0.1/src/mic/response_cache/__init__.py +100 -0
  130. mic_struct-0.0.1/src/mic/response_cache/_stampede.py +90 -0
  131. mic_struct-0.0.1/src/mic/response_cache/_storage.py +109 -0
  132. mic_struct-0.0.1/src/mic/response_cache/middleware.py +289 -0
  133. mic_struct-0.0.1/src/mic/response_cache/rule.py +106 -0
  134. mic_struct-0.0.1/src/mic/shard/__init__.py +63 -0
  135. mic_struct-0.0.1/src/mic/shard/selector.py +145 -0
  136. mic_struct-0.0.1/src/mic/shard/session_factory.py +99 -0
  137. mic_struct-0.0.1/template/cookiecutter.json +8 -0
  138. mic_struct-0.0.1/template/hooks/post_gen_project.py +151 -0
  139. mic_struct-0.0.1/template/hooks/pre_gen_project.py +72 -0
  140. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/.gitignore +45 -0
  141. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/.python-version +1 -0
  142. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/README.md +69 -0
  143. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/alembic/env.py +74 -0
  144. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/alembic/script.py.mako +29 -0
  145. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/alembic/versions/20260507_baseline_widgets.py +44 -0
  146. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/alembic.ini +56 -0
  147. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/__init__.py +4 -0
  148. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/__init__.py +0 -0
  149. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/config/__init__.py +0 -0
  150. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/config/settings.py +88 -0
  151. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/context/__init__.py +5 -0
  152. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/context/execution_context.py +26 -0
  153. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/providers/__init__.py +6 -0
  154. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/providers/auth.py +21 -0
  155. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/providers/database.py +12 -0
  156. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/startup/__init__.py +6 -0
  157. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/startup/http_routes.py +21 -0
  158. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/startup/http_runtime.py +128 -0
  159. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/wiring/__init__.py +6 -0
  160. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/wiring/system.py +13 -0
  161. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/bootstrap/wiring/widgets.py +15 -0
  162. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/entrypoints/__init__.py +0 -0
  163. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/entrypoints/cli/__init__.py +0 -0
  164. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/entrypoints/http/__init__.py +0 -0
  165. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/entrypoints/http/main.py +27 -0
  166. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/__init__.py +0 -0
  167. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/cli/__init__.py +0 -0
  168. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/fastapi/__init__.py +0 -0
  169. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/fastapi/middlewares/__init__.py +5 -0
  170. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/fastapi/middlewares/auth.py +58 -0
  171. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/frameworks/litestar/__init__.py +0 -0
  172. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/__init__.py +12 -0
  173. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/__init__.py +0 -0
  174. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/controller/__init__.py +6 -0
  175. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/controller/health.py +19 -0
  176. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/controller/readiness.py +37 -0
  177. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/interface/__init__.py +5 -0
  178. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/interface/http/__init__.py +5 -0
  179. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/system/interface/http/routes.py +28 -0
  180. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/__init__.py +21 -0
  181. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/controller/__init__.py +6 -0
  182. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/controller/create_widget.py +23 -0
  183. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/controller/read_widget.py +20 -0
  184. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/interface/__init__.py +5 -0
  185. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/interface/http/__init__.py +6 -0
  186. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/interface/http/payloads.py +12 -0
  187. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/interface/http/routes.py +73 -0
  188. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/model/__init__.py +5 -0
  189. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/model/persistence/__init__.py +5 -0
  190. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/model/persistence/widget.py +22 -0
  191. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/modules/widgets/model/repositories/__init__.py +0 -0
  192. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/platform/__init__.py +0 -0
  193. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/platform/services/__init__.py +0 -0
  194. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/app/shared/__init__.py +0 -0
  195. mic_struct-0.0.1/template/{{cookiecutter.service_name}}/pyproject.toml +99 -0
  196. 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.
@@ -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.