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, analytics, persistence)
│       ├── entities/
│       │   ├── entry.py      # Entry (card + quantity)
│       │   ├── binder.py     # Binder (unlimited collection semantics)
│       │   ├── deck.py       # Deck (subclass with size / copy limits)
│       │   ├── types.py      # MTG helpers (e.g. is_basic_land)
│       │   └── analytics.py  # deck CMC stats (numpy)
│       └── 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
    ├── analytics_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. Analytics (entities/analytics.py) exposes module-level CMC helpers that take a Deck and use Types to skip basic lands. 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
    }

    class Types {
        <<module>>
        +is_basic_land(card) bool$
        +BASIC_LAND_NAMES list
    }

    class Analytics {
        <<module>>
        +deck_min_cmc(deck) int$
        +deck_max_cmc(deck) int$
        +deck_cmc_distribution(deck) tuple$
        +deck_cmc_histogram(deck) tuple$
    }

    class Numpy {
        <<numpy>>
    }

    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
    Analytics ..> Deck : CMC stats
    Analytics ..> Types : exclude basic lands
    Analytics ..> Numpy : bincount, histogram
    Types ..> ScryfallCard : card.name

Notes:

  • 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.
  • Analytics functions are exported from pymtgdeck but are not methods on Deck; they count one CMC per Entry (not per copy). See Deck analytics (CMC).

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 dependencies: pyscryfall==0.1.2 and numpy>=2.4.6 (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].

Deck analytics (CMC)

Analytics helpers live in entities/analytics.py and are exported from the package root. They inspect a Deck’s entries (one row per distinct card), use each card’s integer CMC (int(entry.card.cmc)), and exclude basic lands (Island, Plains, Swamp, Mountain, Forest) via is_basic_land in entities/types.py. Copy counts (Entry.count) do not change the distribution—only whether a non–basic-land card appears in the deck.

Function Role
deck_min_cmc(deck) Lowest CMC among non–basic-land entries
deck_max_cmc(deck) Highest CMC among non–basic-land entries
deck_cmc_distribution(deck) (counts, bin_edges) — per-entry counts per CMC bucket (numpy.bincount shape)
deck_cmc_histogram(deck) Histogram over the distribution buckets (numpy.histogram shape)

An empty deck (or one with only basic lands) yields empty arrays from the distribution/histogram helpers. deck_min_cmc / deck_max_cmc raise ValueError when there is no qualifying entry.

from pymtgdeck import Deck, deck_cmc_distribution, deck_max_cmc, deck_min_cmc

deck = Deck(name="Curve check")
# ... add cards ...

low, high = deck_min_cmc(deck), deck_max_cmc(deck)
counts, bin_edges = deck_cmc_distribution(deck)

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), Backend save/load and collision behavior, and deck CMC analytics (analytics_test.py). 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.2.tar.gz (71.0 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.2-py3-none-any.whl (24.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pymtgdeck-0.1.2.tar.gz
  • Upload date:
  • Size: 71.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","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.2.tar.gz
Algorithm Hash digest
SHA256 f29905c64d3bced940c8fa74916910663fe656779b7e2fb06ad494299d5594d3
MD5 929be38d9a81b3d20cc11a69ff90b257
BLAKE2b-256 2b92be381c3023ea9014fc73a4f81b59974e7daca5a2c245af2a8d5398ce08fe

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pymtgdeck-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 24.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 03e4cde971b12100d313f5fa13516df7e30e6d019fed281a7c618666550d9e57
MD5 32e679a203ff3cd843c822310f763bc0
BLAKE2b-256 0c4beb4546313bb2a24c212d2c46e191c9517c41b663b303fed69cda6695476e

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