Metadata-Version: 2.4
Name: riftbound-database
Version: 1.0.0
Summary: Acquisition, normalization, versioning, and persistence for Riftbound data.
Project-URL: Homepage, https://github.com/baobabgit/riftbound-database
Project-URL: Repository, https://github.com/baobabgit/riftbound-database
Project-URL: Issues, https://github.com/baobabgit/riftbound-database/issues
Author: Michel ANDRIANAIVO
License: MIT
License-File: LICENSE
Keywords: database,ingestion,riftbound,sqlalchemy,tcg
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: alembic<2,>=1.13
Requires-Dist: baobab-database[postgresql]<3,>=2.0
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.7
Requires-Dist: riftbound-core<0.2,>=0.1.1
Requires-Dist: sqlalchemy<3,>=2.0
Requires-Dist: typer<1,>=0.12
Provides-Extra: dev
Requires-Dist: bandit<2,>=1.7; extra == 'dev'
Requires-Dist: coverage[toml]<8,>=7; extra == 'dev'
Requires-Dist: mypy<2,>=1.10; extra == 'dev'
Requires-Dist: pytest<10,>=9.0.3; extra == 'dev'
Requires-Dist: ruff<1,>=0.11; extra == 'dev'
Description-Content-Type: text/markdown

# riftbound-database

`riftbound-database` est la librairie d'acquisition, de normalisation,
d'historisation et de persistance des données Riftbound.

Le projet suit cette direction de dépendances :

```text
riftbound-core
    ↑ utilisé par
riftbound-database
    ↓ utilise
baobab-database
```

Le modèle métier canonique appartient à `riftbound-core`. Ce dépôt ne doit pas
redéfinir les cartes, sets, domaines, types, tags, coûts ou textes de carte.
L'accès aux moteurs, sessions et transactions SQLAlchemy sera encapsulé derrière
un adaptateur local de `baobab-database`.

## Prérequis

- Python 3.12 ou 3.13 ;
- PostgreSQL pour l'intégration, SQLite étant réservé aux tests rapides.

## Installation de développement

```bash
python -m venv .venv
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```

La dépendance `riftbound-core` est résolue depuis PyPI (`>=0.1.1,<0.2`).

## Configuration

| Variable | Obligatoire | Valeur par défaut | Rôle |
|---|---:|---|---|
| `RIFTBOUND_DATABASE_URL` | Non au bootstrap | aucune | URL PostgreSQL ; obligatoire en production et pour l'API |
| `RIFTBOUND_STORAGE_PATH` | Non | `storage` | Racine du stockage local |
| `RIFTBOUND_LOG_LEVEL` | Non | `INFO` | Niveau de logs Python |
| `RIFTBOUND_ENVIRONMENT` | Non | `development` | Nom de l'environnement d'exécution |
| `RIFTBOUND_TEST_POSTGRESQL_URL` | Tests E2E | aucune | URL PostgreSQL pour `pytest -m postgresql` |

Les secrets éventuellement présents dans `RIFTBOUND_DATABASE_URL` ne doivent
jamais être écrits dans les logs. Les fichiers `.env` sont ignorés par Git.

## Provider de base de données

Les moteurs, sessions et transactions passent exclusivement par
`riftbound_database.database.BaobabProvider`. PostgreSQL utilise
`baobab-database 2.0.0` avec le driver `psycopg`; SQLite est limité aux tests
sur fichier temporaire.

L'adaptation complète et les options SSL supportées sont documentées dans
[`docs/database-provider.md`](docs/database-provider.md).

## Modèles et migrations

Les modèles SQLAlchemy vivent sous `riftbound_database.database.models`. Les
mappers domaine ↔ persistance sont sous `database/mappers`. Alembic est configuré
via `alembic.ini` et exécutable avec :

```bash
python -m alembic upgrade head
```

Pour les tests locaux sans PostgreSQL, définir `RIFTBOUND_MIGRATION_SQLITE_PATH`
ou laisser Alembic utiliser la base SQLite par défaut sous `.my_cache/`.

## Repositories

Les écritures applicatives passent par `RiftboundUnitOfWork`, qui expose les
repositories sans créer de session locale. Les retours sont limités aux objets
`riftbound-core` ou aux read models sous `database/read_models/`.

```python
from riftbound_database.database.baobab_provider import BaobabProvider
from riftbound_database.database.unit_of_work import RiftboundUnitOfWork

provider = BaobabProvider.for_sqlite("test.db")
unit_of_work = RiftboundUnitOfWork(provider)
with unit_of_work.transaction() as repositories:
    card = repositories.cards.find_by_stable_id("card-001")
```

### Collections utilisateur et decks (v1.0.0)

La persistance applicative pour `riftbound-api` est disponible via les mêmes
repositories transactionnels :

```python
with unit_of_work.transaction() as repositories:
    collection = repositories.collections.get_or_create_default_collection(user_id)
    deck = repositories.decks.create_deck(
        user_id,
        DeckCreatePayload(name="Competitive", visibility="private"),
    )
```

Guide complet : [`docs/collections-decks.md`](docs/collections-decks.md) (tables,
migrations, intégration API, remplacement des adaptateurs mémoire).

## Ingestion

Les imports passent par `BaseImporter`, qui orchestre fetch, stockage raw,
checksum, batch et persistance via les repositories. `SetImporter` importe
des fichiers JSON locaux de façon idempotente avec normalisation, checksum
canonique et stockage sous `normalized_data/sets/`. `CardNormalizer` produit
des DTO canoniques compatibles avec `riftbound-core`, calcule un checksum
stable et signale les champs ambigus via le statut `needs_review`. Les
importers cartes (`LocalFileCardImporter`, `OfficialApiCardImporter`,
`CardGalleryImporter`) normalisent, versionnent et persistent les cartes via
les repositories. Les documents normatifs passent par `RulesImporter`,
`ErrataImporter` et `BanlistImporter` avec normalisation, checksums stables,
historisation (`rules_versions`, snapshots banlist) et statuts
`matched` / `ambiguous` / `needs_review`. Un `FakeImporter` est disponible
pour les tests.

## Assets

Les images de cartes sont téléchargées par `CardAssetDownloader`, validées
(MIME, taille minimale, signature binaire) puis stockées sous
`{RIFTBOUND_STORAGE_PATH}/card_assets/{set_code}/{card_id}/` avec un
`metadata.json` par carte. La persistance relationnelle passe par
`AssetRepository` ; un checksum inchangé évite les écritures inutiles.

```python
from riftbound_database.assets import AssetDownloadRequest, AssetVariant, CardAssetDownloader
from riftbound_database.config.settings import Settings
from riftbound_database.database.baobab_provider import BaobabProvider
from riftbound_database.database.unit_of_work import RiftboundUnitOfWork

settings = Settings()
downloader = CardAssetDownloader()
provider = BaobabProvider.for_sqlite("test.db")
unit_of_work = RiftboundUnitOfWork(provider)
request = AssetDownloadRequest(
    set_code="OGN",
    card_id="card-unit-001",
    variant=AssetVariant.ORIGINAL,
    source_url="https://example.test/card.png",
)
with unit_of_work.transaction() as repositories:
    result = downloader.download(request, settings, repositories.assets)
```

## Synchronisation

Les jobs sous `riftbound_database.sync` orchestrent les importers via un
**`SyncManifest`** JSON (ADR-0002) : chemins locaux + liste `assets[]` avec URLs
source explicites. `FullSyncJob` exécute dans l'ordre
sets → cartes → assets → règles → errata → banlists, avec `dry_run` et
`SyncReport` final.

```python
from riftbound_database.sync import FullSyncJob, SyncContext, SyncManifestLoader

manifest = SyncManifestLoader.load(Path("sync.json"))
context = SyncContext(settings=settings, unit_of_work=unit_of_work, manifest=manifest)
report = FullSyncJob().run(context)
```

## CLI

La CLI Typer est exposée via le script `riftbound-database` (ou
`python -m riftbound_database.cli.main`). Options globales :
`--database-url`, `--sqlite-path`, `--storage-path`, `--log-level`.

```bash
riftbound-database --sqlite-path ./local.db db upgrade
riftbound-database --sqlite-path ./local.db import sets --path ./sets.json
riftbound-database --sqlite-path ./local.db import cards --source local-file --path ./cards.json
riftbound-database --sqlite-path ./local.db import rules --path ./rules.json
riftbound-database --sqlite-path ./local.db import errata --path ./errata.json
riftbound-database --sqlite-path ./local.db import banlist --path ./banlist.json
riftbound-database --sqlite-path ./local.db assets download --manifest ./sync.json
riftbound-database --sqlite-path ./local.db sync full --manifest ./sync.json
riftbound-database checksums verify
riftbound-database export normalized --format json
```

Les imports supportent `--dry-run` et `--limit`. Les commandes retournent un
code de sortie non nul en cas d'échec ; les secrets de connexion ne sont jamais
loggés.

## Qualité

```bash
python -m pytest
python -m coverage run -m pytest
python -m coverage report
python -m ruff check .
python -m mypy src tests
python -m bandit -r src
```

### Tests rapides vs intégration

```bash
# PostgreSQL local via Docker (optionnel)
docker compose up -d postgres
$env:RIFTBOUND_TEST_POSTGRESQL_URL="postgresql+psycopg://riftbound:riftbound@localhost:5432/riftbound_test"

# unitaires (CI par défaut)
python -m pytest -m "not integration and not postgresql"

# migrations SQLite
python -m pytest tests/integration/riftbound_database/database/migrations/

# PostgreSQL (service local requis)
set RIFTBOUND_TEST_POSTGRESQL_URL=postgresql+psycopg://user:pass@localhost:5432/riftbound_test
python -m pytest -m postgresql

# E2E collections/decks PostgreSQL uniquement
python -m pytest tests/integration/riftbound_database/database/test_collections_decks_e2e_postgresql.py -q

# tests contractuels (BL-027)
python -m pytest -m contract -q
```

Voir `docs/contract-tests.md`, `docs/e2e-postgresql.md` et
`docs/collections-decks.md`.

Couverture minimale configurée : **85 %** (`pyproject.toml`).

### CI GitHub Actions

Le workflow `.github/workflows/ci.yml` exécute sur chaque push/PR vers `main` :

- **quality** : ruff, mypy, bandit ;
- **unit** : pytest + coverage (Python 3.12 et 3.13) ;
- **integration** : migrations SQLite + test PostgreSQL via service container.

Les dépendances (`riftbound-core`, `baobab-database`) sont installées depuis PyPI ;
aucun secret GitHub n'est requis pour la CI si tous les packages restent publics.

## Documentation projet

Le cahier des charges et le découpage fonctionnel sont disponibles dans
[`docs/`](docs/README.md).

Guides opérationnels :

- [`docs/collections-decks.md`](docs/collections-decks.md) — intégration collections/decks (v1.0.0) ;
- [`docs/contract-tests.md`](docs/contract-tests.md) — tests contractuels ;
- [`docs/e2e-postgresql.md`](docs/e2e-postgresql.md) — E2E PostgreSQL ;
- [`docs/api-port-compatibility.md`](docs/api-port-compatibility.md) — contrat ports API ;

Le catalogue initial a été livré via les backlogs `BL-001` à `BL-014`. L'épopée
collections/decks (`BL-015` → `BL-029`) clôt la version **1.0.0**.
