Metadata-Version: 2.5
Name: mic-datastore
Version: 0.1.24
Summary: MIC — persistance, messaging et tâches périodiques (datastorage, cache, locking, outbox, queue, scheduling, realtime).
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: mic-core
Provides-Extra: document
Requires-Dist: pymongo<5,>=4.10; extra == 'document'
Provides-Extra: event-replay
Requires-Dist: clickhouse-connect<1,>=0.7; extra == 'event-replay'
Requires-Dist: mic-core[event-replay]; extra == 'event-replay'
Provides-Extra: graph
Requires-Dist: neo4j<6,>=5.27; extra == 'graph'
Provides-Extra: kafka
Requires-Dist: aiokafka<1,>=0.12; extra == 'kafka'
Provides-Extra: messaging
Requires-Dist: aiokafka<1,>=0.12; extra == 'messaging'
Requires-Dist: nats-py<3,>=2.7; extra == 'messaging'
Provides-Extra: mongo
Requires-Dist: pymongo<5,>=4.10; extra == 'mongo'
Provides-Extra: mysql
Requires-Dist: alembic<2,>=1.13; extra == 'mysql'
Requires-Dist: pymysql<2,>=1.1; extra == 'mysql'
Requires-Dist: sqlmodel<0.1,>=0.0.38; extra == 'mysql'
Provides-Extra: nats
Requires-Dist: nats-py<3,>=2.7; extra == 'nats'
Provides-Extra: neo4j
Requires-Dist: neo4j<6,>=5.27; extra == 'neo4j'
Provides-Extra: oracle
Requires-Dist: alembic<2,>=1.13; extra == 'oracle'
Requires-Dist: oracledb<3,>=2.5; extra == 'oracle'
Requires-Dist: sqlmodel<0.1,>=0.0.38; extra == 'oracle'
Provides-Extra: postgres
Requires-Dist: alembic<2,>=1.13; extra == 'postgres'
Requires-Dist: psycopg[binary]<4,>=3.2; extra == 'postgres'
Requires-Dist: sqlmodel<0.1,>=0.0.38; extra == 'postgres'
Provides-Extra: redis
Requires-Dist: redis<7,>=5.2; extra == 'redis'
Provides-Extra: sql
Requires-Dist: alembic<2,>=1.13; extra == 'sql'
Requires-Dist: sqlmodel<0.1,>=0.0.38; extra == 'sql'
Provides-Extra: ws
Requires-Dist: mic-core[auth]; extra == 'ws'
Requires-Dist: redis<7,>=5.2; extra == 'ws'
Description-Content-Type: text/markdown

# mic-datastore

Briques de **persistance & messaging** du framework MIC. Dépend de `mic-core`
(DAG `platform → data → core` — jamais l'inverse).

## Modules

| Module | Rôle |
| -- | -- |
| `mic.datastorage` | Backends de données : `SqlDataStorage`, `DocumentDataStorage` (MongoDB), `GraphDataStorage` (Neo4j), variantes in-memory |
| `mic.cache` | Cache (in-memory / Redis) + `RevokeBlacklist` (révocation JWT) |
| `mic.shard` | Sélection de shard / session factory |
| `mic.locking` | Verrous distribués (in-memory / Redis) |
| `mic.outbox` | Pattern transactional outbox + dispatcher |
| `mic.eventbus` | Pub/sub synchrone (in-memory / Redis Streams) |
| `mic.queue` | Event bus async durable (NATS JetStream / Kafka) |
| `mic.idempotency` | Store d'idempotence |
| `mic.response_cache` | Middleware de cache de réponses HTTP |
| `mic.read_models` | Read-models (compteurs, leaderboards, timelines, HLL) |
| `mic.realtime` | WebSocket realtime (backend Redis Streams) |
| `mic.scheduling` | Tâches périodiques : verrou, checkpoint, suivi et alerte de silence |

Namespace **PEP 420** (pas de `mic/__init__.py`).

## `mic.realtime` — WebSocket depuis un navigateur

Un navigateur ne peut pas poser d'en-tête `Authorization` sur l'upgrade
WebSocket, et un jeton en query string finit dans les journaux d'accès : le
client passe son jeton en **sous-protocole** et propose en plus un
sous-protocole applicatif, que le serveur doit renvoyer dans l'`accept` (sinon
le navigateur ferme la connexion).

```js
new WebSocket(url, ["app.v1", `bearer.${jwt}`]);
```

```python
from mic.realtime import (
    AsyncWebSocketAuthenticator, WebSocketAuthError, extract_bearer_token,
    make_websocket_endpoint,
)

class OidcAuthenticator(AsyncWebSocketAuthenticator):  # vérif. async (JWKS)
    async def authenticate(self, scope):
        token = extract_bearer_token(scope)
        if token is None or not await verify_with_jwks(token):
            raise WebSocketAuthError("unauthorized")  # → close 4401

endpoint = make_websocket_endpoint(
    room=room,
    authenticator=OidcAuthenticator(),  # ou ServiceAuthWebSocketAuthenticator (sync)
    subprotocol="app.v1",  # renvoyé seulement si proposé ; jamais `bearer.<jwt>`
)
```

`RedisStreamsBackend(client=redis, maxlen=1_000)` plafonne chaque stream de
room via `XADD MAXLEN ~` (défaut `DEFAULT_MAXLEN = 1000`, trim approximatif ;
`maxlen=None` = pas de plafond).

## `mic.scheduling` — tâches périodiques observables

Le planificateur de la plateforme (CronJob Kubernetes, systemd timer, boucle du
service) appelle `run_due()` ; MIC porte les garanties qui doivent rester
identiques quel que soit ce planificateur : exclusion distribuée, reprise,
état d'exécution et détection du silence.

```python
from mic.locking import RedisDistributedLock
from mic.scheduling import (
    PeriodicTaskRunner,
    RedisSchedulingStore,
    SilenceMonitor,
    TaskContext,
    TaskResult,
    periodic,
)

@periodic(
    name="collect:k8s",
    every="15m",
    lock="collect:k8s",
    timeout="10m",
    silence_after="35m",
)
def collect(ctx: TaskContext) -> TaskResult:
    cursor = ctx.checkpoint
    processed, next_cursor = collect_batch(cursor)
    ctx.raise_if_timed_out()  # point d'arrêt sûr entre deux lots
    return TaskResult(processed=processed, checkpoint=next_cursor)

store = RedisSchedulingStore(redis_url="redis://redis:6379/0")
runner = PeriodicTaskRunner(
    lock_backend=RedisDistributedLock(redis_url="redis://redis:6379/0"),
    store=store,
    metrics=metrics_backend,  # MetricsBackend optionnel
)
runner.run_due(collect)

alerts = SilenceMonitor(store=store, metrics=metrics_backend).check([collect])
```

Une tâche en échec ne fait jamais avancer son checkpoint et son exception est
propagée au superviseur. Le timeout est une **échéance coopérative** : le runner
mesure tout dépassement au retour et `TaskContext.raise_if_timed_out()` permet
de l'interrompre entre deux lots. Il ne tue pas un thread Python arbitraire ;
un arrêt forcé reste la responsabilité du superviseur de processus ou du
`activeDeadlineSeconds` d'un CronJob. `SilenceMonitor` doit vivre dans un
processus de supervision distinct si l'on veut détecter la disparition totale
du worker.

Métriques émises via `MetricsBackend` : `mic_scheduling_runs_total`
(`task`, `status`), `mic_scheduling_duration_seconds`,
`mic_scheduling_last_processed`, `mic_scheduling_last_success_timestamp_seconds`
et `mic_scheduling_silent`.

## Installation

```bash
pip install "mic-datastore[postgres,redis]"
```

## Extras

| Extra | Tire |
| -- | -- |
| `sql` | sqlmodel + alembic |
| `postgres` / `mysql` / `oracle` | `sql` + driver (psycopg / PyMySQL / oracledb) |
| `mongo` (alias `document`) | pymongo |
| `neo4j` (alias `graph`) | neo4j |
| `redis` | redis |
| `nats` / `kafka` | nats-py / aiokafka |
| `messaging` | `nats` + `kafka` |
| `ws` | `redis` + `mic-core[auth]` |

## Versionnage

Brique volatile — reste en `0.x` plus longtemps que `mic-core`. Contraindre
avec `>=0.1,<1`. Release : `make release-pkg PKG=data VERSION=x.y.z` → tag
`data-vX.Y.Z` (cf. [`../../RELEASING.md`](../../RELEASING.md)).
