Metadata-Version: 2.4
Name: baobab-mtg-collection
Version: 1.0.0
Summary: Couche métier MTG typée : façade catalogue/core, possessions, requêtes enrichies, inventaire.
License: MIT License
        
        Copyright (c) 2026 Baobab contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/baobabgit/tcg-mtg-baobab-mtg-collection
Project-URL: Repository, https://github.com/baobabgit/tcg-mtg-baobab-mtg-collection
Project-URL: Changelog, https://github.com/baobabgit/tcg-mtg-baobab-mtg-collection/blob/main/CHANGELOG.md
Keywords: baobab,collection,magic-the-gathering,mtg
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.14
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: baobab-collection-core<2.0.0,>=1.0.0
Requires-Dist: baobab-mtg-catalog<2.0.0,>=1.0.0
Provides-Extra: dev
Requires-Dist: bandit[toml]<2.0.0,>=1.7.0; extra == "dev"
Requires-Dist: black<27.0.0,>=26.3.1; extra == "dev"
Requires-Dist: Flake8-pyproject<2.0.0,>=1.2.0; extra == "dev"
Requires-Dist: flake8<8.0.0,>=7.0.0; extra == "dev"
Requires-Dist: mypy<2.0.0,>=1.8.0; extra == "dev"
Requires-Dist: pylint<4.0.0,>=3.0.0; extra == "dev"
Requires-Dist: pytest<10.0.0,>=9.0.3; extra == "dev"
Requires-Dist: pytest-cov<7.0.0,>=5.0.0; extra == "dev"
Requires-Dist: coverage[toml]<8.0.0,>=7.4.0; extra == "dev"
Requires-Dist: build<2.0.0,>=1.0.0; extra == "dev"
Requires-Dist: pip-audit<3.0.0,>=2.7.0; extra == "dev"
Requires-Dist: pipdeptree<3.0.0,>=2.20.0; extra == "dev"
Dynamic: license-file

      # baobab-mtg-collection

Librairie Python métier pour la gestion des possessions Magic: The Gathering.

**État V1 (Features 01 à 08 livrées)** : projection catalogue → core, ajout et cycle de vie des
exemplaires, requêtes enrichies et filtres, agrégats d’inventaire, façade
``MtgCollectionFacade`` et factory ``in_memory``, exports racine documentés (``__version__``,
``BaobabMtgCollectionException``, ``MtgCollectionFacade``). Les **read models** et exceptions
s’importent depuis les barils ``domain`` / ``domain.read_models`` et ``exceptions`` ; le détail
par user story est dans ``docs/users_stories/`` et ``docs/features/00_index_features.md``.

**Documentation de synthèse** : ``docs/architecture.md``, ``docs/business_domain.md``,
``docs/001_specifications.md`` ; avant publication, exécuter la checklist ``docs/release_checklist.md``
(``1.0.0`` PyPI : licence MIT, workflow ``.github/workflows/release-tag.yml`` pour la release GitHub).

**Note Feature 02 (cahier vs code)** : le fichier
``docs/features/feature_02_exceptions_et_modeles_de_lecture.md`` peut encore mentionner des
artefacts **absents** de ``src/`` ; la surface importable réelle est celle des ``__all__`` des
barils ``exceptions`` et ``domain.read_models``.

## Versions Python

- Déclaration : ``requires-python >= 3.14`` dans ``pyproject.toml``.
- Stdlib ``tomllib`` pour les tests de packaging ; pas de ``tomli`` en extra ``dev``.
- La chaîne qualité est alignée sur **Python 3.14** (tests, ``mypy``, ``black``, etc.).

## Configuration qualité (US-01-03)

Tous les outils listés ci-dessous lisent leurs réglages dans ``pyproject.toml`` (sections
``[tool.*]``) : **aucun** fichier parallèle du type ``setup.cfg``, ``.flake8``, ``pytest.ini``,
``mypy.ini``, ``.coveragerc`` ou ``tox.ini`` n’est utilisé ; cela évite les divergences entre
postes et pipelines.

| Outil | Rappel de configuration |
|-------|-------------------------|
| ``black`` | longueur de ligne **100** ; ``target-version`` **py314** |
| ``flake8`` | **100** caractères ; ignores **E203**, **W503** (compatibilité ``black``) |
| ``mypy`` | **strict** + ``warn_unused_configs`` ; exception ``tests.*`` pour décorateurs pytest |
| ``pylint`` | **100** caractères ; note **10/10** ; ``duplicate-code`` désactivé (squelette) |
| ``bandit`` | analyse ``src/`` ; répertoire ``tests/`` exclu |
| ``pytest`` | ``testpaths``, ``pythonpath``, ``--import-mode=importlib`` |
| ``coverage`` | ``fail_under`` **90** ; données et rapports sous ``docs/tests/coverage/`` |

Commandes usuelles (depuis la racine du dépôt, avec l’extra ``dev`` installé) :

```bash
python -m pytest
python -m coverage run -m pytest
python -m coverage report
python -m coverage html
python -m coverage xml
python -m black --check src tests
python -m flake8 src tests
python -m pylint src tests
python -m mypy src tests --strict
python -m bandit -r src
```

Le flag CLI ``--strict`` pour ``mypy`` est redondant avec ``pyproject.toml`` mais reste valide et
explicité dans les checklists projet.

Les garde-fous déclaratifs (seuils, chemins coverage, absence de fichiers de config parallèles)
sont vérifiés par ``tests/baobab_mtg_collection/test_pyproject_quality_configuration.py``.

## API publique racine (US-01-04, US-08-03)

La racine annonce ``__version__``, ``BaobabMtgCollectionException`` et ``MtgCollectionFacade`` dans
``__all__``. ``__version__`` est chargé à l'import ; les deux autres symboles sont résolus par
import paresseux : ``import baobab_mtg_collection`` seul ne charge pas les sous-packages
``exceptions`` ni ``facades``.

La valeur ``__version__`` est la version PEP 440 de la distribution ``baobab-mtg-collection`` via
``importlib.metadata``.

L'import racine minimal ne déclenche ni réseau, ni persistance, ni logique métier MTG.

Imports publics recommandés :

```python
from baobab_mtg_collection import __version__, MtgCollectionFacade, BaobabMtgCollectionException

import baobab_mtg_collection as mtc

assert mtc.__version__ == __version__
```

- Les sous-packages ``exceptions`` et ``domain`` / ``domain.read_models`` listent leurs symboles
  stables dans leur propre ``__all__`` (US-02-04).
- Contrôles associés :
  ``tests/baobab_mtg_collection/test_baobab_mtg_collection_public_exports.py`` ;
  ``tests/baobab_mtg_collection/exceptions/test_public_exports_exceptions.py`` ;
  ``tests/baobab_mtg_collection/domain/test_public_exports_domain.py`` ;
  ``tests/baobab_mtg_collection/domain/read_models/test_public_exports_read_models.py`` ;
  ``tests/baobab_mtg_collection/test_readme_examples.py`` (huit blocs ``python`` du README).

## Façade publique (US-08-01, US-08-02)

``MtgCollectionFacade`` (``baobab_mtg_collection.facades``) câble une ``MtgCatalogFacade`` et les
repositories core (injectés ou mémoire via ``MtgCollectionFacade.in_memory(catalog_facade)``),
puis expose projection, possessions, requêtes enrichies et inventaire.

```python
from baobab_mtg_collection import MtgCollectionFacade
from baobab_mtg_catalog.facades import MtgCatalogFacade

catalog = MtgCatalogFacade.in_memory()
facade = MtgCollectionFacade.in_memory(catalog)
```

Tests : ``tests/baobab_mtg_collection/facades/test_mtg_collection_facade.py`` ; intégration
US-08-04 : ``test_mtg_collection_facade_integration.py``.

## Hiérarchie d’exceptions métier (US-02-01)

Les erreurs métier **de cette librairie** s’importent en priorité depuis
``baobab_mtg_collection.exceptions`` ; la racine peut aussi exposer ``BaobabMtgCollectionException``
(US-08-03, import paresseux). La classe
``BaobabMtgCollectionException`` sert
de racine ; les exceptions spécialisées en héritent **directement** (hiérarchie plate). Un bloc
``except BaobabMtgCollectionException`` intercepte toutes les classes listées ci-dessous.

- ``MtgCatalogReferenceNotFoundException`` — référence catalogue introuvable.
- ``MtgCollectionProjectionException`` — projection collection ou catalogue impossible ou
  incohérente.
- ``InvalidMtgFinishException`` — finition invalide pour le printing.
- ``InvalidMtgQuantityException`` — quantité invalide.
- ``MtgOwnerNotFoundException`` — propriétaire absent côté core.
- ``MtgContainerNotFoundException`` — contenant absent côté core.
- ``MtgDeletedCopyOperationException`` — opération interdite sur copie supprimée logiquement.

Référence : ``docs/features/feature_02_exceptions_et_modeles_de_lecture.md``. Tests miroir sous
``tests/baobab_mtg_collection/exceptions/``.

**Imports recommandés (US-02-04)** : toujours depuis le baril
``baobab_mtg_collection.exceptions`` (voir ``__all__`` dans ce package). Les imports directs
depuis les modules fichiers (ex. ``...exceptions.mtg_owner_not_found_exception``) sont possibles
mais **déconseillés** si vous voulez suivre l’API stable.

```python
from baobab_mtg_collection.exceptions import (
    BaobabMtgCollectionException,
    MtgOwnerNotFoundException,
)

try:
    raise MtgOwnerNotFoundException()
except BaobabMtgCollectionException as err:
    assert "propriétaire" in str(err).lower()
```

## Vue de possession enrichie (US-02-02)

``MtgOwnedCopyView`` (``baobab_mtg_collection.domain.read_models``) regroupe une ``PhysicalCopy`` et
une ``CollectionCard`` du **core**, un ``CardPrinting`` du **catalogue**, et optionnellement une
``CardDefinition`` ainsi que l’extension catalogue (champ ``card_set``, type ``Set`` catalogue
importé sous alias pour éviter le builtin Python). Les champs optionnels sont ``None`` lorsque le
catalogue ne fournit pas l’objet.

**Champs** : obligatoires — ``copy``, ``collection_card``, ``printing`` ; optionnels —
``definition``, ``card_set``.

Imports publics — **canon** = ``domain.read_models`` ; raccourci = ``domain`` (même objet) :

```python
from baobab_mtg_collection.domain.read_models import MtgOwnedCopyView
from baobab_mtg_collection.domain import MtgOwnedCopyView as MtgOwnedCopyViewShortcut

assert MtgOwnedCopyView is MtgOwnedCopyViewShortcut
```

La construction ne fait **aucun** I/O ni validation métier : les services résolvent les instances
puis instancient la vue. Tests : ``tests/baobab_mtg_collection/domain/read_models/``.

**Ajout depuis un printing (US-04-01 à US-04-05, DOC-T-01)** : ``MtgAddOwnedCopyFromPrintingService``
(``baobab_mtg_collection.application.possessions``) orchestre le core (usager, résolution printing,
validation de finition, carte projetée, ``PhysicalCopyApplicationService.create_copy``) et
retourne une ``MtgOwnedCopyView`` enrichie (définition / set catalogue si disponibles). La finition
(``str`` ou ``Finish`` catalogue) est vérifiée par ``MtgPrintingFinishValidator`` contre
``CardPrinting.finishes`` **avant** projection et création d’exemplaire ; pas de substitution
silencieuse ; ``InvalidMtgFinishException`` si la valeur est inconnue, absente du printing, ou
omise alors que plusieurs finitions existent. Si une seule finition est disponible, ``finish=None``
la sélectionne explicitement. ``add_copy_from_printing`` délègue à ``add_copies_from_printing``
avec ``quantity=1``. ``add_copies_from_printing`` exige un entier **strictement positif**
(``bool`` refusé), refuse la quantité **avant** toute persistance (aucune création partielle), crée
**une** ``PhysicalCopy`` distincte par unité et renvoie un **tuple** immuable de vues. Plafond
documenté : ``MTG_MAX_COPIES_PER_ADD_BATCH`` (dix mille) ; au-delà :
``InvalidMtgQuantityException``. Prérequis : mêmes ports que la projection Feature 03
(``MtgCardProjectionService``, ``MtgPrintingResolutionService``, sources définition / set) plus
``UserApplicationService``, ``ContainerApplicationService`` (validation ``container_id`` US-04-04)
et ``PhysicalCopyApplicationService`` injectés. Chaque ``PhysicalCopy``
correspond à **un exemplaire physique réel**. La vue retournée est assemblée via
``MtgOwnedCopyViewBuilder`` (US-04-05). Tests :
``tests/baobab_mtg_collection/application/possessions/``.

**Mise à jour d’exemplaires existants (Feature 05, DOC-T-01)** :
``MtgChangeOwnedCopyPhysicalConditionService`` délègue à
``PhysicalCopyApplicationService.change_physical_condition`` après contrôle du propriétaire et
retourne une ``MtgOwnedCopyView`` enrichie (même résolution printing / catalogue que l’ajout) ;
``MtgOwnedCopyNotFoundException`` si la copie n’est pas accessible pour ce propriétaire.
``MtgChangeOwnedCopyBusinessStatusService`` applique la même logique pour
``change_business_status`` avec un ``PhysicalCopyBusinessStatus`` typé ; une valeur qui n’est pas
une instance de cette énumération lève ``InvalidMtgCopyBusinessStatusException``. La suppression
logique d’une copie (US-05-06) reste un flux distinct de ces changements de statut métier.
``MtgUpdateOwnedCopyNotesService`` met à jour uniquement les **notes** (texte brut non interprété,
longueur max ``MTG_MAX_COPY_NOTES_LEN`` = 4000, alignée sur le core) ; ``None`` efface les notes ;
``InvalidMtgCopyNotesException`` si type ou longueur invalide.
``MtgAttachOwnedCopyToContainerService`` déplace une copie existante vers un contenant (validation
``ContainerApplicationService`` avant ``attach_container`` ; ``MtgContainerNotFoundException`` si
le contenant est absent ou archivé ; pas de création d’exemplaire supplémentaire).
``MtgDetachOwnedCopyFromContainerService`` retire le rattachement (``detach_container``) sans toucher
aux autres champs descriptifs ; si la copie n’avait pas de contenant, le résultat reste cohérent.
``MtgSoftDeleteOwnedCopyService`` applique ``soft_delete_copy`` (copie hors flux actif ensuite ;
``MtgOwnedCopyNotFoundException`` sur lecture ou double suppression) et retourne une
``MtgOwnedCopyView`` d’audit avec la copie marquée supprimée logiquement.

**Requêtes lecture (US-06-01 à US-06-05, Feature 06)** : ``MtgListActiveOwnedCopiesForUserQuery``
(``baobab_mtg_collection.application.queries``) retourne un **tuple** immuable de
``MtgOwnedCopyView`` pour les exemplaires **actifs** d'un ``owner_user_id`` (exclusion des
suppressions logiques par défaut). **US-06-05** : ordre déterministe indépendant de l'ordre
d'insertion ou du dépôt — tri catalogue (nom Oracle normalisé, code d'extension, numéro de
collection, langue du printing, puis ``entity_id`` de l'exemplaire) via
``owned_copy_view_catalog_sort_key`` ; identique avec ou sans ``owned_copy_filter``.
Le port ``PhysicalCopyRepositoryPort`` est utilisé pour ``list_all_physical_copies`` puis
filtrage côté MTG ; hydratation catalogue via ``build_mtg_owned_copy_view_for_physical_copy``.
**US-06-03** : ``owned_copy_filter`` optionnel (``MtgOwnedCopyFilter`` dans
``baobab_mtg_collection.domain.filters``) restreint la liste **après** enrichissement ; critères
combinés en **ET** (extension catalogue, langue, finition, rareté, état matériel, statut métier,
contenant, sous-chaîne insensible à la casse sur nom / textes Oracle) ; aucune écriture dépôt.
**US-06-04** : même filtre avec ``only_without_container=True`` (copies non rangées dans un
contenant), ``location_note_contains`` sur la note d'emplacement libre ; ``container_id`` et
``only_without_container`` ne s'activent pas ensemble (``ValueError`` à la construction du
filtre).
Tests : ``tests/baobab_mtg_collection/application/queries/`` et ``domain/filters``.
``MtgGetOwnedCopyByIdQuery`` retourne une ``MtgOwnedCopyView`` **figée** pour un couple
``copy_id`` / ``owner_user_id`` ; par défaut les exemplaires supprimés logiquement sont invisibles
(``MtgOwnedCopyNotFoundException``), sauf ``include_deleted=True`` pour l’audit.

Pour les requêtes et scénarios plus larges, voir aussi
``docs/features/feature_04_ajout_de_possessions_mtg.md`` et
``docs/features/feature_06_requetes_enrichies_et_filtres_collection.md``.

**Agrégats inventaire (US-07-01 à US-07-05, Feature 07)** : ``MtgInventorySummaryService``
(``baobab_mtg_collection.application.inventory``) s’appuie sur la même liste active que
``MtgListActiveOwnedCopiesForUserQuery.list_for_owner`` (copies supprimées logiquement exclues).
``summarize`` retourne un ``MtgInventorySummary`` **figé** (``total_copies``, décomptes distincts
printings / définitions / extensions, ``available_copies`` = statuts ``ACTIVE`` et ``FOR_TRADE``
uniquement). Les méthodes ``count_by_set``, ``count_by_rarity``, ``count_by_finish``,
``count_by_language``, ``count_by_condition`` et ``count_by_status`` retournent des **tuples**
triés de lignes read model ; la finition par exemplaire suit le **minimum lexicographique** des
valeurs ``Finish`` du printing (une ligne par copie). ``build_full_snapshot`` regroupe synthèse et
tous les axes dans un ``MtgInventoryFullSnapshot`` immuable. Tests :
``tests/baobab_mtg_collection/application/inventory/``.

## Modèles de comptage d’inventaire (US-02-03)

Read models immuables pour les **totaux** et les **lignes par axe** (set catalogue, rareté,
finition, langue, état physique, statut métier).

**Contrats de sortie (Feature 07)** : ces types sont les **DTO** que les services d’agrégats
(prévus dans ``docs/features/feature_07_agregats_inventaire.md``) retourneront ou embarqueront
dans des réponses ; US-02-03 ne livre **pas** le service de calcul.

Aucun calcul d’agrégation dans ces classes : la Feature 07 instancie ces types avec des entiers
déjà calculés.

Les compteurs sont des ``int`` **≥ 0** ; une valeur strictement négative lève
``InvalidMtgQuantityException`` à la construction. Aucun champ prix ni devise.

**Synthèse** : ``MtgInventorySummary`` (``owner_user_id: DomainId``, totaux et décomptes distincts
selon ``docs/001_specifications.md``). **Bundle** : ``MtgInventoryFullSnapshot`` (synthèse + tuples
par axe, US-07-05). **Par axe** : ``MtgSetInventoryCount`` (``SetId``, libellés optionnels
``set_code`` / ``set_name`` quand le set catalogue est résolu),
``MtgRarityInventoryCount`` (``Rarity``), ``MtgFinishInventoryCount`` (``Finish``),
``MtgLanguageInventoryCount`` (``Language``), ``MtgConditionInventoryCount``
(``PhysicalCopyCondition``), ``MtgStatusInventoryCount`` (``PhysicalCopyBusinessStatus``).

Imports publics — **canon** = ``domain.read_models`` ; raccourci = ``domain`` :

```python
from baobab_mtg_collection.domain.read_models import MtgInventorySummary, MtgSetInventoryCount
from baobab_mtg_collection.domain import MtgInventorySummary as SummaryShortcut

assert MtgInventorySummary is SummaryShortcut
```

Tests : ``tests/baobab_mtg_collection/domain/read_models/test_mtg_inventory_summary.py`` et
fichiers ``test_mtg_*_inventory_count.py``. Spécification des totaux :
``docs/001_specifications.md`` ; consommateur cible des sorties agrégées :
``docs/features/feature_07_agregats_inventaire.md``.

## Résolution printing catalogue (US-03-01)

**Feature 03 — périmètre livré (DOC-T-02)** : US-03-01 à US-03-04 (user stories Feature 03 sous
``docs/users_stories/``) sont implémentées ici (résolution, création, idempotence, mapper). Détail
normatif : ``docs/001_specifications.md`` et cahier ``docs/features/`` (fichier
``feature_03_projection_card_printing_vers_collection_card.md``).

Étape isolée de la **Feature 03** : ``MtgPrintingResolutionService`` dépend du port
``MtgCatalogPrintingLookupPort`` (``get_printing_by_id`` → ``CardPrinting | None``). La méthode
``resolve_printing`` accepte une **chaîne non fiable** et appelle toujours
``CardPrintingIdentifier.parse`` (catalogue) avant la lecture. Si absent :
``MtgCatalogReferenceNotFoundException`` avec message **générique** ; l'id parsé est sur
``catalog_printing_id`` pour logs structurés (hors ``str(exception)``). Branchement réel :
``MtgCatalogPrintingQueryAdapter`` sur ``MtgCatalogFacade.printings`` (seul
``CardPrintingNotFoundError`` → ``None``). Tests :
``tests/baobab_mtg_collection/application/projection/``.

**Erreurs (printing introuvable et bordure parse)** :

- **Printing absent** (identifiant **valide** côté catalogue, port ``None``) :
  ``MtgCatalogReferenceNotFoundException`` (import depuis ``baobab_mtg_collection.exceptions``) ;
  message par défaut **sans** UUID dans ``str(exception)`` ; corrélation via
  ``exception.catalog_printing_id``.
- **Chaîne non parseable** : ``InvalidCardPrintingIdentifierError`` (``baobab-mtg-catalog``) lors
  du parse catalogue — **non** convertie en introuvable (pas de fallback silencieux vers un autre
  printing).

**Façade HTTP** : transmettre la **valeur brute** (chaîne) à ``resolve_printing`` ; ne pas
pré-parser avec ``CardPrintingIdentifier.parse`` côté contrôleur (double validation). **Logs** :
ne pas dériver d’IDs depuis ``str(exception)`` ; utiliser ``exc.catalog_printing_id`` dans un
champ structuré (ex. ``extra`` du logger) lorsqu’il est présent.

```python
from baobab_mtg_collection.application.projection import (
    MtgCatalogPrintingQueryAdapter,
    MtgPrintingResolutionService,
)
from baobab_mtg_catalog.facades import MtgCatalogFacade

facade = MtgCatalogFacade.in_memory()
lookup = MtgCatalogPrintingQueryAdapter(facade.printings)
service = MtgPrintingResolutionService(catalog_printing_lookup=lookup)
# printing_id = "<uuid métier>"  # chaîne non fiable (ex. HTTP)
# service.resolve_printing(printing_id)
```

## Première projection ``CollectionCard`` (US-03-02)

**Règle (DOC-T-01) — une projection par printing.** Au plus une ``CollectionCard`` du core par
``CardPrinting`` catalogue sur ce chemin. ``external_id`` stable ``mtg.card_printing:<uuid>``
via ``MtgCardPrintingExternalId.build`` ou ``MtgCardProjectionService.build_external_id``. Carte
déjà présente pour cette clé : ``MtgCollectionProjectionException`` (pas de seconde création
silencieuse). Réutiliser sans recréer : **US-03-03**.

Chemin **création** lorsqu’aucune carte n’existe encore pour l’``external_id``
``mtg.card_printing:<uuid>`` : ``MtgCardProjectionService.create_projection_for_printing``
réutilise ``MtgPrintingResolutionService`` (chaîne brute), charge définition et set catalogue
(``Protocol`` ``MtgCatalogCardDefinitionQuerySource`` / ``MtgCatalogSetQuerySource``, ex.
``facade.definitions`` / ``facade.sets``), mappe via ``MtgPrintingToCollectionCardMapper`` et
persiste uniquement via ``CardApplicationService.create_card``. Doublon d’``external_id`` déjà
présent : ``MtgCollectionProjectionException`` ; définition ou set catalogue manquant : idem
(cause catalogue conservée). Identifiant externe : ``domain.projection.MtgCardPrintingExternalId``.
Tests : ``tests/baobab_mtg_collection/domain/projection/``, ``domain/mappers/``,
``application/projection/test_mtg_card_projection_service.py``.

## Idempotence projection (US-03-03)

**DOC-T-01** — API idempotente : ``MtgCardProjectionService.ensure_collection_card_for_printing``
parcourt ``list_cards`` avec la
même clé ``external_id`` qu’en US-03-02 (tri déterministe par ``entity_id`` avant décision).
**Une** carte → retour sans ``create_card`` ; **aucune** → création comme US-03-02 ;
**plusieurs** cartes même clé → ``MtgCollectionProjectionException`` (données incohérentes).
Concurrence entre deux créations : non garantie en V1 sans verrou côté déploiement ; le port
``CardRepositoryPort`` doit refléter le bon périmètre collection / tenant.

## Projection catalogue → core (US-03-04)

**DOC-T-01 (documentation développeur)** : point d’entrée unique
``MtgPrintingToCollectionCardMapper.to_projection_params`` → ``MtgCollectionCardProjectionParams``
(``baobab_mtg_collection.domain.mappers``). Pas de persistance dans le mapper ; pas de prix ni
données marketplace dans les tags.

| Champ ``create_card`` / params | Origine |
|--------------------------------|---------|
| ``name`` | ``CardDefinition.name`` (nom affichable Oracle) |
| ``external_id`` | ``MtgCardPrintingExternalId.build(printing.card_printing_id)`` |
| ``edition`` | code d’extension catalogue en minuscules |
| ``catalog_version`` | ``MTG_COLLECTION_CARD_CATALOG_VERSION`` (constante projet) |
| ``language`` | langue du ``CardPrinting`` (primitive lisible) |
| ``tags`` | ``mtg``, ``mtg-printing``, ``set:<code>``, ``c:``, ``rarity:``, ``def:``, ``oracle:`` |

Cohérence : ``printing.card_definition_id`` / ``printing.set_id`` doivent correspondre aux objets
passés ; tag > 64 car. (règle core) → ``MtgCollectionProjectionException`` sans troncature. Tests :
``tests/baobab_mtg_collection/domain/mappers/test_mtg_printing_to_collection_card_mapper.py``.

Imports publics :

```python
from baobab_mtg_collection.domain.mappers import (
    MTG_COLLECTION_CARD_CATALOG_VERSION,
    MtgCollectionCardProjectionParams,
    MtgPrintingToCollectionCardMapper,
)
```

## Statut initial (US-01-01)

- Import du package : ``import baobab_mtg_collection`` et des sous-modules
  ``baobab_mtg_collection.application``, ``baobab_mtg_collection.domain``,
  ``baobab_mtg_collection.exceptions``, ``baobab_mtg_collection.facades``,
  ``baobab_mtg_collection.infrastructure``.
- ``domain.read_models`` expose ``MtgOwnedCopyView`` (US-02-02), ``MtgInventorySummary`` et les
  ``Mtg*InventoryCount`` (US-02-03) ; ``exceptions`` la hiérarchie US-02-01 ;
  ``application.projection`` résout un printing (US-03-01), crée la première projection
  ``CollectionCard`` (US-03-02), expose l’``ensure`` idempotent (US-03-03) ; le mapper catalogue
  → core est sous ``domain.mappers`` (US-03-04) ; les autres sous-packages
  bootstrap gardent un ``__all__`` minimal jusqu’aux stories applicatives ; la racine expose
  ``__version__`` (US-01-04, contrat testé).
- Contraintes de développement : `docs/000_dev_constraints.md`.
- Cahier des charges : ``docs/001_specifications.md``.

## Contribution et workflow Git (US-01-05)

- **Commits** : [Conventional Commits](https://www.conventionalcommits.org/) (préfixes ``feat``,
  ``fix``, ``docs``, ``test``, ``chore``, etc.).
- **Branches** : une branche par user story ou correctif ; fusion via PR / revue selon la
  politique d’équipe.
- **Backlogs** : source **canonique** sous ``docs/backlogs/`` ; Feature 01 :
  ``docs/backlogs/feature_01_bootstrap_projet_conforme_aux_contraintes/``.
- **US-01-05** (documentation / journal) : backlog canonique dans le répertoire Feature 01 sous
  ``docs/backlogs/`` (fichier ``backlog_us_01_05_initialiser_la_documentation_et_le_journal_de_developpement.md``).
  Toute note Engineer **hors** dépôt doit rester **alignée** sur ces fiches.
- **Journal** : évolutions notables du code, des tests ou de la documentation dans
  ``docs/dev_diary.md`` (entrées en ordre chronologique **décroissant**).

## Installation (développement)

```bash
python -m venv .venv
source .venv/bin/activate
```

### Dépendances runtime (US-01-02)

Le projet déclare dans ``pyproject.toml`` exactement deux dépendances métier bornées :

- ``baobab-collection-core>=1.0.0,<2.0.0`` ;
- ``baobab-mtg-catalog>=1.0.0,<2.0.0``.

### Installation locale du package sans résolution des dépendances (**cas A**)

Utile pour valider le wheel éditable et l’import du package **sans** télécharger les deps
métier (ni les extras ``dev``) :

```bash
python -m pip install -e . --no-deps
python -c "import baobab_mtg_collection"
```

Équivalent documenté : ``scripts/smoke_install_editable_no_deps.sh`` (depuis la racine du
dépôt, après ``chmod +x`` si besoin).

### Installation complète développeur

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

**Cas B** : résout l’extra `dev` **et** les dépendances runtime (voir ci-dessous).

Après succès, les imports ``baobab_collection_core``, ``baobab_mtg_catalog`` et
``baobab_mtg_collection`` sont vérifiables (test ``test_editable_install_imports`` ; ignoré si les
deps ne sont pas installées).

L’extra ``dev`` regroupe les outils de qualité : bandit, black, Flake8-pyproject, flake8, mypy,
pylint, pytest, pytest-cov, ``coverage[toml]`` (rapports HTML/XML et lecture ``[tool.coverage]``),
build, ``pip-audit``, ``pipdeptree``. Toutes les contraintes sont bornées (pas de ``*``).

**Limite connue** : cette commande **résout aussi** les dépendances runtime listées ci-dessus.
Si ``baobab-collection-core`` et ``baobab-mtg-catalog`` ne sont pas disponibles sur l’index
Python utilisé (ex. PyPI public), ``pip`` échoue avec une erreur du type
``No matching distribution found for baobab-collection-core``. Ce n’est pas un contournement
silencieux : il faut rendre ces distributions accessibles (index privé Baobab via
configuration d’environnement ou ``pip.conf``, wheels locales validées, ou procédure interne
d’installation des artefacts). **Ne pas** committer d’URL privée, de jetons ni de secrets dans
le dépôt.

## Vérification rapide (import)

```python
import baobab_mtg_collection
from baobab_mtg_collection import __version__
import baobab_mtg_collection.application

_ = __version__
```

Les autres sous-packages du bootstrap s’importent de la même façon (voir **Statut initial**).
La version publique est aussi documentée en **API publique racine (US-01-04)**.

## Couverture de code (rapports HTML et XML)

La configuration ``coverage`` dans ``pyproject.toml`` oriente les sorties vers
``docs/tests/coverage/``. Pour régénérer les rapports localement :

```bash
python -m coverage run -m pytest
python -m coverage report
python -m coverage html
python -m coverage xml
```

Livrables attendus dans ce répertoire :

- ``docs/tests/coverage/.coverage`` (données brutes)
- ``docs/tests/coverage/html/`` (rapport HTML)
- ``docs/tests/coverage/coverage.xml`` (rapport XML)

Les artefacts générés peuvent être ignorés par Git selon ``.gitignore`` ; la séquence
ci-dessus reste la référence pour QA/CI et pour reproduire les rapports.

## Sécurité des dépendances

Le projet dépend directement de ``baobab-collection-core`` et ``baobab-mtg-catalog``.

``baobab-mtg-catalog`` peut entraîner transitivement des clients HTTP utilisés pour le catalogue
MTG. Cette surface réseau est surveillée par audit de dépendances.

Le **code** de ``baobab-mtg-collection`` n’introduit pas directement d’appel réseau dans
US-01-02, mais l’**environnement installé** peut contenir une surface HTTP héritée du catalogue
MTG (jusqu’à ``requests`` en transitif — voir la chaîne documentée).

Commandes recommandées :

```bash
python -m bandit -r src
python -m pipdeptree -p baobab-mtg-collection
python -m pip_audit
```

Après ``python -m pip install -e ".[dev]"``, enchaîner l’arbre et ``pip_audit`` via :

```bash
./scripts/audit_dependencies.sh
```

Le script utilise ``.venv/bin/python`` à la racine du dépôt s’il existe, sinon ``python3`` du
``PATH``. Si ``pip_audit`` signale ``pip``, exécuter ``python -m pip install --upgrade pip`` puis
relancer l’audit.

Réexécuter ces contrôles en CI lors des changements de dépendances.

- Aucune URL privée ni token ne doit être commité dans le dépôt.
- La présence de ``requests`` **en transitif** n’autorise pas le code métier de ce dépôt à
  multiplier les appels réseau sans revue.

Documentation détaillée : ``docs/security/dependencies_and_supply_chain.md``.

## Licence

Le code est publié sous **licence MIT** (fichier ``LICENSE`` à la racine du dépôt). Les
métadonnées PEP 621 du ``pyproject.toml`` référencent ce fichier pour les outils de packaging et
PyPI.

## Publication sur PyPI (``1.0.0``)

- **Version courante** : voir ``pyproject.toml`` et ``CHANGELOG.md``.
- **CI** : le workflow ``.github/workflows/release-tag.yml`` se déclenche sur **push d’un tag**
  ``v*`` (ex. ``v1.0.0``). Il exécute la chaîne qualité (black, flake8, mypy strict, pylint, bandit,
  pytest + coverage **≥ 90 %**), construit wheel et sdist, et **crée la release GitHub** si elle
  n’existe pas encore. **Aucun** upload PyPI n’est effectué depuis GitHub Actions.
- **Poste développeur** : après tag poussé (et release GitHub si besoin), publier les artefacts
  depuis la racine du dépôt avec un jeton API PyPI (utilisateur ``__token__``) :

  ```bash
  python -m pip install --upgrade build twine
  python -m build
  python -m twine check dist/*
  TWINE_USERNAME=__token__ TWINE_PASSWORD=<jeton pypi> python -m twine upload dist/*
  ```

  Générer le jeton sur https://pypi.org/manage/account/token/ . Ne pas versionner le secret ;
  préférer des variables d’environnement ou un gestionnaire de secrets local.
- **Dépendances** : ``baobab-collection-core`` et ``baobab-mtg-catalog`` doivent être installables
  **depuis le même index** que celui utilisé par les consommateurs (PyPI public, index privé ou
  wheels préalables), sinon ``pip install baobab-mtg-collection`` échouera à la résolution.
