Skip to main content

Python library that helps maintaining Magic: The Gathering Decks and Virtual Binders

Project description

pymtgdeck

Python library for maintaining Magic: The Gathering virtual binders and constrained decks. Card data is represented with pyscryfall ScryfallCard objects (Scryfall-shaped JSON in and out).

  • License: GNU General Public License v3.0 (see LICENSE)
  • Python: 3.12+

Project structure

The package uses a src layout (importable code under src/), tests and fixtures beside the tree root, and uv for lockfile and dev dependencies. Domain types live under entities/; disk persistence under persistence/.

pymtgdeck/
├── LICENSE
├── README.md                 # this file
├── pyproject.toml            # project metadata, pytest config, hatchling build
├── uv.lock                   # locked dependency versions (uv)
├── src/
│   └── pymtgdeck/
│       ├── __init__.py       # public exports: Entry, Binder, Deck, Registry, Backend
│       ├── entities/
│       │   ├── entry.py      # Entry (card + quantity)
│       │   ├── binder.py     # Binder (unlimited collection semantics)
│       │   └── deck.py       # Deck (subclass with size / copy limits)
│       └── persistence/
│           ├── backend.py    # save/load Deck and Binder to JSON files
│           └── registry.py   # scan a folder of saved JSON and list metadata
└── tests/
    ├── utils.py              # helpers: load Scryfall list JSON → first card
    ├── entry_test.py
    ├── binder_test.py
    ├── deck_test.py
    ├── backend_test.py
    ├── registry_test.py
    └── data/
        ├── card-example-1.json
        ├── card-example-2.json
        └── card-example-3.json   # Scryfall API “list” JSON fixtures

Class diagram

Relationships: a Binder holds a list of Entry instances; Deck subclasses Binder and adds validation and aggregate card counting. Backend writes and reads JSON envelopes for Deck and Binder; Registry rescans a directory of those files for a lightweight index. ScryfallCard comes from pyscryfall, not from pymtgdeck.

classDiagram
    direction TB

    class ScryfallCard {
        <<pyscryfall>>
        +from_dict(data) ScryfallCard$
        +to_dict() dict
    }

    class Entry {
        +ScryfallCard card
        +int count
        +to_dict() dict
        +to_json() str
        +from_dict(data) Entry$
        +from_json(s) Entry$
    }

    class Binder {
        +str name
        +list entries
        +add_card(card, count=1)
        +has_card(card) bool
        +remove_card(card, count=1)
        +get_card_count(card) int
        +to_dict() dict
        +from_dict(data) Binder$
    }

    class Deck {
        +str name
        +int max_card_copy_count
        +int max_card_count
        +is_full() bool
        +get_card_count() int
        +get_card_copy_count(card) int
        +is_empty() bool
        +add_card(card, count=1)
        +to_dict() dict
        +from_dict(data) Deck$
    }

    class Backend {
        +Path file_path
        +save(obj) str
        +load(file_name) Deck|Binder
    }

    class Registry {
        +Path path
        +list registry
        +load_file(file_name) Deck|Binder
    }

    Entry --> ScryfallCard : card
    Binder "1" o-- "*" Entry : entries
    Binder <|-- Deck
    Backend ..> Deck : load/save
    Backend ..> Binder : load/save
    Registry ..> Deck : load_file
    Registry ..> Binder : load_file

Note: On Deck, get_card_count() (no arguments) returns the total number of cards in the deck. On Binder, get_card_count(card) returns copies of that card. Deck uses get_card_copy_count(card) for per-card counts.

Installation

From the repository root, using uv:

uv sync

Or install the package in editable mode with your preferred tool (example with pip):

pip install -e .

Runtime dependency: pyscryfall==0.1.2 (declared in pyproject.toml).

Usage examples

Binder (no deck limits)

from pyscryfall import search_cards_by_name
from pymtgdeck import Binder

binder = Binder(name="Trade binder")  # name is optional; used in serialization and persistence
results = search_cards_by_name("Sengir Vampire")
card = results.data[0]

binder.add_card(card, count=2)
assert binder.has_card(card)
assert binder.get_card_count(card) == 2

binder.remove_card(card, count=1)
assert binder.get_card_count(card) == 1

Deck (default limits: 40 cards, 4 copies per card)

from pyscryfall import search_cards_by_name
from pymtgdeck import Deck

deck = Deck(name="Sealed pool")  # or Deck(max_card_count=60, max_card_copy_count=4, name="...")
results = search_cards_by_name("Forest")
forest = results.data[0]

deck.add_card(forest, count=4)
assert deck.get_card_count() == 4  # total cards
assert deck.get_card_copy_count(forest) == 4
assert not deck.is_full()

Default limits match the module constants MAX_CARD_COUNT and MAX_CARD_COPY_COUNT in entities/deck.py (40 and 4); you can override them per deck via the constructor.

Serialization

Binder.to_dict() / Binder.from_dict() include an optional name plus entries. Deck.to_dict() / Deck.from_dict() also persist max_card_copy_count and max_card_count.

from pymtgdeck import Binder, Deck

binder = Binder(name="My binder")
# ... add cards ...

dump = binder.to_dict()
binder2 = Binder.from_dict(dump)

deck = Deck(name="My deck")
# ... add cards ...

deck_dump = deck.to_dict()
deck2 = Deck.from_dict(deck_dump)

Persistence (Backend)

Backend writes each deck or binder to a single JSON file under a configurable directory (default ~/.pymtgdeck). The on-disk shape is an envelope with timestamp, type ("Deck" or "Binder"), name (same as the object’s name), and data (the result of to_dict() on the deck or binder).

The file basename is the SHA-256 hex digest of the UTF-8 encoded name, with a .json suffix. Saving again for the same name raises OSError so you do not silently overwrite an existing file.

from pymtgdeck import Deck, Backend
from pathlib import Path

store = Path("/tmp/mtg-store")
backend = Backend(file_path=store)

deck = Deck(name="FNM")
# ... add cards ...

filename = backend.save(deck)          # returns e.g. "<hex>.json"
restored = backend.load(filename)

Use a non-None name on the deck or binder before save, so the filename is stable and hashing is defined.

Registry scan (Registry)

Registry reads every *.json file in its directory (default ~/.pymtgdeck). For each file whose envelope has type "Deck" or "Binder", it records name, type, and timestamp in an in-memory list. str(registry) pretty-prints that index. For round-tripping files written by Backend, use Backend.load with the basename returned from save; Registry also exposes load_file for reloading (see persistence/registry.py for the exact argument semantics).

Entry and JSON fixtures

Entry wraps one ScryfallCard and a quantity, and can round-trip through dict/JSON shapes compatible with pyscryfall:

from pymtgdeck import Entry

entry = Entry(card, count=3)
data = entry.to_dict()
restored = Entry.from_dict(data)

Tests load cards from files shaped like Scryfall’s card list response (see tests/data/*.json), using ScryfallCardList.from_json_string and taking data[0].

Test procedure

Tests use pytest (dev dependency). Configuration lives in pyproject.toml under [tool.pytest.ini_options] (testpaths = ["tests"], pythonpath = ["."] so src resolves when running from the repo root).

Run the full suite from the repository root:

uv run pytest

If a virtual environment is already activated with dev dependencies installed:

pytest tests/

Useful variants:

pytest tests/ -q              # quiet
pytest tests/deck_test.py     # single module
pytest tests/ -k serialization  # tests whose name contains the substring

The suite covers Entry, Binder, and Deck (add/remove, limits, serialization, optional name), plus Backend save/load and collision behavior. Fixtures are offline JSON files; tests that call search_cards_by_name would need network access and are not part of the default suite.

AI Disclosure

Part of this project has been developed with the help of an AI Model. Specifically I used a locally-hosted QWEN3-CODER using Ollama.

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

pymtgdeck-0.1.0.tar.gz (35.3 kB view details)

Uploaded Source

Built Distribution

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

pymtgdeck-0.1.0-py3-none-any.whl (21.9 kB view details)

Uploaded Python 3

File details

Details for the file pymtgdeck-0.1.0.tar.gz.

File metadata

  • Download URL: pymtgdeck-0.1.0.tar.gz
  • Upload date:
  • Size: 35.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pymtgdeck-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2d27991af0d39547c9a618edafc42fc8869f7674e5201319c7522485cb003b93
MD5 06e2c1895e0e84e25e8512b2da7a98ac
BLAKE2b-256 d3031a1282fa4bdf1247f095bc910ace44259552430a652c0acc6d8ac327ad32

See more details on using hashes here.

File details

Details for the file pymtgdeck-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pymtgdeck-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pymtgdeck-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5e058df38fafb1ebbf9af500829da8625d10af042a95abf2bee9e4a5d2454f22
MD5 1177b294744d5f01fec6915c763d4dea
BLAKE2b-256 fda36731b3656f7d8d08658a0666c77520281088b6908e2f9b1d4a108b908552

See more details on using hashes here.

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