Skip to main content

Acquisition, normalization, versioning, and persistence for Riftbound data.

Project description

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 :

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

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_SYNC_JOB_STALE_AFTER_SECONDS Non 300 Seuil (secondes) sans heartbeat avant qu'un job RUNNING soit considéré stale
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.

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 :

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/.

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")
    job = repositories.sync_jobs.create(
        SyncJobCreateInput(type=SyncJobType.CARDS, mode=SyncJobMode.INCREMENTAL),
    )

Collections utilisateur et decks (v1.0.0)

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

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 (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.

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.

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)

Synchronisation pilotée (sync_job, v1.1.0)

Lorsqu'un sync_job_id est fourni, les imports appellent périodiquement heartbeat() et vérifient is_cancel_requested() via ImportExecutionSupervisor. L'annulation est coopérative : elle ne tue pas le processus worker ; le job en cours détecte la demande, interrompt proprement l'import et passe le sync_job en CANCELLED. Les jobs RUNNING sans heartbeat récent sont détectables via SyncJobStaleDetector (seuil configurable par RIFTBOUND_SYNC_JOB_STALE_AFTER_SECONDS).

from riftbound_database.sync import SyncJobStaleDetector

detector = SyncJobStaleDetector(unit_of_work, settings)
stale_jobs = detector.list_stale_jobs()

Concurrence et idempotence

Deux demandes identiques (même type + source) sans force=True retournent le job actif existant au lieu d'en créer un second silencieusement. Avec force=True, un nouveau job est créé même si un job actif existe déjà.

from riftbound_database.ingestion.services.sync_job_service import SyncJobService

with unit_of_work.transaction() as repositories:
    service = SyncJobService(repositories.sync_jobs)
    job = service.create_card_sync_job("admin", {"set_code": "OGN"}, force=False)
    same = service.create_card_sync_job("admin", {"set_code": "OGN"}, force=False)
    assert job.id == same.id

Guide complet : docs/sync-jobs.md (modèle, CLI, frontière API/Web, checklist intégration riftbound-api).

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.

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 --sqlite-path ./local.db sync jobs list
riftbound-database --sqlite-path ./local.db sync jobs status <job-uuid>
riftbound-database --sqlite-path ./local.db sync jobs logs <job-uuid>
riftbound-database --sqlite-path ./local.db sync jobs cancel <job-uuid>
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é

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

# 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

# E2E sync jobs PostgreSQL uniquement
python -m pytest tests/integration/riftbound_database/sync/test_sync_jobs_e2e_postgresql.py -q

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

Voir docs/v1.0.0/contract-tests.md, docs/v1.0.0/e2e-postgresql.md, docs/v1.0.0/collections-decks.md et docs/sync-jobs.md.

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

CI GitHub Actions

La suite de tests est factorisée dans .github/workflows/reusable-tests.yml et invoquée par :

  • CI (.github/workflows/ci.yml) : chaque push/PR vers main ou release/** ;
  • Release readiness (.github/workflows/release-readiness.yml) : déclenchement manuel ou sur branches release/** — exécute la même suite puis génère les snapshots integration-snapshot.md et analysis-snapshot.md (artefacts) ;
  • Release (.github/workflows/release.yml) : sur tag v*.*.* — tests PostgreSQL, gates bloquants (alignement tag / __version__, section CHANGELOG datée, guide de migration si nouvelles révisions Alembic, version absente de PyPI), puis build et publication PyPI.

Jobs de la suite partagée :

  • qualité : ruff, mypy, bandit ;
  • unitaires : pytest + couverture (Python 3.12 et 3.13) ;
  • intégration : migrations SQLite + scénarios 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/.

Guides opérationnels :

Le catalogue initial a été livré via les backlogs BL-001 à BL-014. L'épopée collections/decks (BL-015BL-029) clôt la version 1.0.0 ; l'épopée sync jobs (BL-030BL-042) clôt la version 1.1.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

riftbound_database-1.1.0.tar.gz (324.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

riftbound_database-1.1.0-py3-none-any.whl (209.7 kB view details)

Uploaded Python 3

File details

Details for the file riftbound_database-1.1.0.tar.gz.

File metadata

  • Download URL: riftbound_database-1.1.0.tar.gz
  • Upload date:
  • Size: 324.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for riftbound_database-1.1.0.tar.gz
Algorithm Hash digest
SHA256 4900133efeb7184aad03b8181b858ef7a4fe41ddd6eb84c79710aef6841468bd
MD5 01227f07713272e6c5fd017fdc1a76c8
BLAKE2b-256 4efa999b87a501f3265b13568932e441bf805d5fe0519051ec862b0d8d2e8f7c

See more details on using hashes here.

Provenance

The following attestation bundles were made for riftbound_database-1.1.0.tar.gz:

Publisher: release.yml on baobabgit/riftbound-database

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file riftbound_database-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for riftbound_database-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6384875de9d4eadc296a8d857d68e2bd060aabbc39198cd3d6464a384d946355
MD5 26eefdf8ba8f8e0027b26164d7df8ec1
BLAKE2b-256 f7ef9cf29eaace0c106922a3c15fa11901a650e96eba8c5ce6666f2977db787c

See more details on using hashes here.

Provenance

The following attestation bundles were made for riftbound_database-1.1.0-py3-none-any.whl:

Publisher: release.yml on baobabgit/riftbound-database

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page