Metadata-Version: 2.5
Name: mic-core
Version: 0.1.32
Summary: MIC — fondations agnostiques (contracts, model, http, auth, client, observability, healthcheck, resilience, cli, grpc).
Project-URL: Homepage, https://mic.uman.social/
Project-URL: Documentation, https://mic.uman.social/
Project-URL: Repository, https://gitlab.com/code-idea/code-libs/python/mic/mic-struct
Project-URL: Issues, https://gitlab.com/code-idea/code-libs/python/mic/mic-struct/-/issues
Project-URL: Changelog, https://gitlab.com/code-idea/code-libs/python/mic/mic-struct/-/blob/master/CHANGELOG.md
Project-URL: Copyright, http://uman.social
Author: UMAN
License-Expression: MIT
Requires-Python: >=3.14
Requires-Dist: pydantic>=2.6
Provides-Extra: auth
Requires-Dist: pyjwt<3,>=2.14; extra == 'auth'
Provides-Extra: cli
Provides-Extra: client
Requires-Dist: httpx<1,>=0.27; extra == 'client'
Provides-Extra: event-catalog
Requires-Dist: pyyaml<7,>=6; extra == 'event-catalog'
Provides-Extra: event-replay
Provides-Extra: fastapi
Requires-Dist: fastapi<1,>=0.115; extra == 'fastapi'
Requires-Dist: starlette<2,>=1.3.1; extra == 'fastapi'
Requires-Dist: uvicorn[standard]<1,>=0.30; extra == 'fastapi'
Provides-Extra: full-observability
Requires-Dist: opentelemetry-api<2,>=1.27; extra == 'full-observability'
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.27; extra == 'full-observability'
Requires-Dist: opentelemetry-sdk<2,>=1.27; extra == 'full-observability'
Requires-Dist: prometheus-client<1,>=0.20; extra == 'full-observability'
Requires-Dist: sentry-sdk<3,>=2.20; extra == 'full-observability'
Requires-Dist: urllib3<3,>=2.7; extra == 'full-observability'
Provides-Extra: grpc
Requires-Dist: grpcio-health-checking<2,>=1.66; extra == 'grpc'
Requires-Dist: grpcio<2,>=1.66; extra == 'grpc'
Provides-Extra: litestar
Requires-Dist: litestar<3,>=2.22; extra == 'litestar'
Provides-Extra: observability
Requires-Dist: opentelemetry-api<2,>=1.27; extra == 'observability'
Requires-Dist: prometheus-client<1,>=0.20; extra == 'observability'
Provides-Extra: oidc
Requires-Dist: pyjwt[crypto]<3,>=2.14; extra == 'oidc'
Provides-Extra: sentry
Requires-Dist: sentry-sdk<3,>=2.20; extra == 'sentry'
Requires-Dist: urllib3<3,>=2.7; extra == 'sentry'
Provides-Extra: tracing
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.27; extra == 'tracing'
Requires-Dist: opentelemetry-sdk<2,>=1.27; extra == 'tracing'
Description-Content-Type: text/markdown

# mic-core

Fondations agnostiques du framework **MIC** (Model / Interface / Controller).
`mic-core` est le socle : il ne dépend d'aucune brique de persistance ni de
plateforme (voir le DAG `platform → data → core` dans
[`../README.md`](../README.md)).

La page [Hors périmètre][out-of-scope] précise les responsabilités que MIC
laisse durablement à l'application, à l'infrastructure ou à un produit
spécialisé. Le guide [Quitter MIC][leaving-mic] documente comment remplacer les
contrats et adaptateurs du cœur sans changer les contrats publics du service.

[out-of-scope]: https://gitlab.com/code-idea/code-libs/python/mic/mic-struct/-/blob/develop/docs/guides/out-of-scope.md
[leaving-mic]: https://gitlab.com/code-idea/code-libs/python/mic/mic-struct/-/blob/develop/docs/guides/leaving-mic.md

## Prérequis Python

`mic-core` exige Python 3.14 ou une version ultérieure. CPython 3.14 est la
cible actuellement qualifiée par un gate CI bloquant ; Python 3.13 et les
versions antérieures ne sont pas pris en charge. Consultez la
[politique Python][python-runtime-policy] avant une adoption sur un hébergement
dont l'environnement d'exécution ou l'image de base n'est pas sous votre contrôle.

[python-runtime-policy]: https://gitlab.com/code-idea/code-libs/python/mic/mic-struct/-/blob/develop/docs/guides/python-runtime-policy.md

## Erreurs HTTP décidables

Les adaptateurs FastAPI et Litestar exposent `DomainError` avec une enveloppe
canonique contenant `retryable: false`; `TransientError` produit
`retryable: true`. Les clients MIC donnent priorité à ce booléen avant leur
repli sur le statut HTTP. Le [guide de reprise HTTP][http-error-retryability]
détaille le contrat et la règle d'idempotence.

[http-error-retryability]: https://gitlab.com/code-idea/code-libs/python/mic/mic-struct/-/blob/develop/docs/guides/http-error-retryability.md

## Modules

| Module | Rôle |
| -- | -- |
| `mic.contracts` | Contrats statiques (`StrictSchema`) et gouvernés (`SchemaRegistry`) |
| `mic.model` | Erreurs de domaine (`DomainError`, `TransientError`), value objects |
| `mic.tenancy` | Contexte d'organisation et store relationnel à portée obligatoire |
| `mic.http` | Spécifications HTTP **agnostiques** (`HttpRouteSpec`, `HttpResponseSpec`, `HttpCookieSpec`, `HttpRequestContext`) |
| `mic.fastapi` | Adaptateur FastAPI (router builder, middlewares) — extra `[fastapi]` |
| `mic.litestar` | Adaptateur Litestar — extra `[litestar]` |
| `mic.auth` | JWT service-to-service `[auth]` et vérification OIDC/JWKS `[oidc]` |
| `mic.client` | Client HTTP inter-services (httpx) — extra `[client]` |
| `mic.observability` | Logs JSON, métriques Prometheus, traces OTel — extra `[observability]` / `[full-observability]` |
| `mic.healthcheck` | Sondes liveness/readiness |
| `mic.resilience` | Circuit breaker, retry, timeouts |
| `mic.cli` | Squelette CLI (argparse stdlib) — extra `[cli]` |
| `mic.grpc` | Transport gRPC — extra `[grpc]` |
| `mic.event_replay` | Replay d'events archivés vers un bus (CLI `mic-events-replay`) |

Le paquet est un **namespace PEP 420** (pas de `mic/__init__.py`) : `import mic.*`
reste identique côté consommateur, qu'on installe `mic-core` seul ou avec
`mic-datastore` / `mic-platform`.

## Replay d'events (`mic-events-replay`)

Rejoue des events **archivés** (DLQ outbox exportée en JSON, ou archive
ClickHouse long-terme) vers un sink. Cas d'usage : audit forensic, bug-fix
rétroactif (un handler a swallow des events pendant N heures → on les rejoue
après fix), amorçage d'un nouveau read-model depuis l'historique.

```bash
# Dry-run (compte uniquement) depuis un dump JSON
mic-events-replay --from-json dump.json \
  --topic mail.send --from 2026-06-22T08:00:00Z --to 2026-06-22T10:00:00Z

# Source ClickHouse + filtre payload (répétable)
mic-events-replay --from-clickhouse http://clickhouse.svc:8123 \
  --topic mail.send --from 2026-06-22T08:00:00Z --to 2026-06-22T10:00:00Z \
  --filter to_email=user@example.com

# Sink NATS JetStream (republie avec header x-replay) — extra mic-datastore[nats]
mic-events-replay --from-clickhouse http://clickhouse.svc:8123 \
  --topic mail.send --from ... --to ... \
  --to-nats nats://nats-0:4222,nats://nats-1:4222 --nats-stream events
```

Sinks : aucun = **dry-run** ; `--target module:callable` = callback sync ;
`--to-nats` = NATS JetStream. Exit `0` = OK, `1` = aucun event matché
(topic/range mal pinnés ?), `2` = erreur de setup.

### Convention header `x-replay`

Chaque event republié via `--to-nats` porte trois **headers transport**
(le payload n'est **pas** réécrit — flag transparent) :

| Header | Valeur |
| -- | -- |
| `x-replay` | `true` |
| `x-replay-batch-id` | UUID unique par invocation (corrélation / traçabilité) |
| `x-original-event-id` | identifiant stable de l'événement archivé (idempotence) |

⚠️ **Les consumers doivent être idempotents** — un replay redélivre des events
déjà traités. Un consumer idempotent (clé dérivée de `x-original-event-id`,
ou opérations naturellement idempotentes) absorbe les replays sans effet de
bord ; le header `x-replay` lui permet en plus de **tracer ou ignorer**
explicitement un replay sans changer son décodage de payload. Le support des
headers est fourni par `mic.queue` (cf. `mic-datastore`).

## Installation

```bash
pip install "mic-core[fastapi,auth,client,observability]"
```

## Contrats gouvernés

`SchemaRegistry` construit un `StrictSchema` à partir de `FieldDef` chargés
depuis une source de vérité versionnée. Le modèle reste immuable et refuse les
champs inconnus. Le cache TTL évite de reconstruire le modèle à chaque requête ;
un événement distribué appelle `registry.invalidate(key)` sur chaque instance,
et le TTL sert de filet de rattrapage si cet événement est perdu.

```python
from mic.contracts import FieldDef, SchemaRegistry

registry = SchemaRegistry(
    loader=lambda key: (
        FieldDef("cluster", str),
        FieldDef("replicas", int, default=1),
    ),
    ttl=300,
)
Payload = registry.get("deployment@v2")
payload = Payload.model_validate({"cluster": "prod", "replicas": 3})
```

## Isolation par organisation

`TenantScopedRelationalStore` injecte le filtre d'organisation dans les lectures
et comptages, contrôle les insertions et refuse toute opération hors d'un
`tenant_context`. Le repository métier doit recevoir cette vue sécurisée, jamais
le store brut.

```python
from mic.tenancy import TenantScopedRelationalStore, tenant_context

store = TenantScopedRelationalStore(raw_store)
with tenant_context(organization_id):
    rows = store.find(Project, query)
```

Le champ est `organization_id` par défaut (`tenant_field` permet de l'adapter).
Un filtre explicite sur ce champ est refusé : sa gestion appartient au store et
ne peut pas être neutralisée par l'appelant.

### Jetons de service à portée limitée

`ServiceAuthSigner.sign(scopes=[...])` encode le claim JWT standard `scope`.
`ServiceAuthVerifier.verify()` expose les valeurs dans `claims.scopes`, tandis
que `require(token, scope=...)` exige une correspondance exacte et renvoie une
erreur 403 `service_auth.insufficient_scope` si le service authentifié n'a pas
la permission demandée. Les tokens historiques sans scope restent valides pour
`verify()` ; les opérations protégées doivent appeler explicitement
`require()`.

## Extras

| Extra | Tire |
| -- | -- |
| `fastapi` | fastapi + uvicorn[standard] + starlette |
| `litestar` | litestar |
| `auth` | pyjwt |
| `oidc` | pyjwt + cryptography |
| `client` | httpx |
| `observability` | prometheus-client + opentelemetry-api |
| `sentry` | sentry-sdk (+ urllib3 patché) |
| `tracing` | opentelemetry-sdk + exporter OTLP |
| `full-observability` | `observability` + `sentry` + `tracing` |
| `grpc` | grpcio + grpcio-health-checking |
| `cli` | (rien — argparse stdlib) |

## Versionnage

`mic-core` démarre en `0.x` (pré-releases `dev`/`rc` sur TestPyPI). Il **graduera
en `1.0`** une fois les frontières prouvées et les services migrés (fin du split,
cf. [ADR-0001](../../docs/decisions/ADR-0001-split-mic-struct-into-three-packages.md)).
En `0.x`, un bump *minor* peut porter un changement cassant (SemVer 0.x) —
contraindre avec `>=0.1,<1`.

## Release

Voir [`../../RELEASING.md`](../../RELEASING.md) § « Release par paquet »
(`make release-pkg PKG=core VERSION=x.y.z` → tag `core-vX.Y.Z`).
