Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
d3d6247
Import+Frontend: Merge Card Faces Into One Searchable Row, and Flip t…
daveycodez Aug 8, 2026
c98272b
API+Engine: Expose layout, cmc, rarity, color_identity and legalities…
daveycodez Aug 7, 2026
fb7c3a6
Import: Keep the Merged Row's raw_card_blob as the Card Scryfall Sent
daveycodez Aug 9, 2026
5f01742
Engine+API: Six More order= Values, and dir=auto
daveycodez Aug 10, 2026
d6b01b7
Fix: The Prefer Dropdown Sent Values the API Rejects
daveycodez Aug 11, 2026
da6ed08
UI: Offer the Six New Orderings, and Pin the Dropdowns to Their Enums
daveycodez Aug 11, 2026
49ed051
Fix: The Front Face Was Never Uploaded, and the Back Sat in Its Place
daveycodez Aug 11, 2026
0d01e51
Style: Make the Flip Button Look Like Scryfall's, Not Like a Page Widget
daveycodez Aug 11, 2026
2bf3ce8
Fix: Position the Flip Button Against the Card Image, Not Its Container
daveycodez Aug 11, 2026
cf9fa85
Import: Store What a Card Object Needs, Where the Engine Can Read It
daveycodez Aug 10, 2026
3dce9e0
Engine: Store Each Face, So a Face Is Something the Engine Can Read
daveycodez Aug 10, 2026
31f12d3
Engine: Find a Card by Id, Which the Store Could Only Scan For
daveycodez Aug 10, 2026
5c86920
Engine: Read the Compat Residue Into the Store, Packed
daveycodez Aug 10, 2026
205e9d3
Merge remote-tracking branch 'origin/response-fields' into pr912
daveycodez Aug 11, 2026
507c5af
Merge remote-tracking branch 'origin/engine-order-vocabulary' into pr912
daveycodez Aug 11, 2026
ad137af
Engine: Emit the Fields a Scryfall Card Object Needs
daveycodez Aug 10, 2026
03d85f0
Engine: Address a Card by Its External Ids
daveycodez Aug 10, 2026
eb7538c
Engine: Store the Related-Card List
daveycodez Aug 10, 2026
d091acc
Engine: Fuzzy Name Match and Autocomplete, Computed Not Stored
daveycodez Aug 10, 2026
cf11b0d
API: Every Scryfall /cards/* Route, So the Base URL Is the Only Thing…
daveycodez Aug 9, 2026
b2a7fe5
API: Serve /cards/* From the Engine, With SQL Only as the Fallback
daveycodez Aug 11, 2026
8b8dfc9
Engine: Halve What the Compat Residue Costs a Printing
daveycodez Aug 11, 2026
95328a0
Engine: Rank a Price Ordering by the Card's Cheapest Printing
daveycodez Aug 11, 2026
10e800a
API+Engine: Let a Caller Read the EUR and TIX Prices They Sorted By
daveycodez Aug 11, 2026
a28d7ad
Fix: order=artist Put an Artistless Printing at the Wrong End Descending
daveycodez Aug 11, 2026
3de5aef
Fix: The Fuzzy and Autocomplete Engine Paths Were Unreachable, and Tw…
daveycodez Aug 11, 2026
9219fe6
API: /cards/* Sent No Cache-Control at All
daveycodez Aug 11, 2026
b1df91e
Fix: rust-test Has Been Red on This Branch, on clippy
daveycodez Aug 11, 2026
0676f80
Fix: rust-test Has Been Red on This Branch, on clippy
daveycodez Aug 11, 2026
e3f1aac
Fix: One More clippy Error, Visible Only on CI's Pinned Toolchain
daveycodez Aug 11, 2026
a1fe59f
Fix: The Engine's Autocomplete Answers a Different Query Than the SQL…
daveycodez Aug 11, 2026
2e3eec5
Perf: The Legality Registry Was Rebuilt and Re-Sorted Once Per CARD
daveycodez Aug 11, 2026
f3ae266
Fix: That Cache Test Was Flaky, Because the Registry It Reads Is Global
daveycodez Aug 11, 2026
b09adf0
Engine: Build the Scryfall Card Object in the Engine
daveycodez Aug 12, 2026
39c3074
Engine+API: A Planeswalker's Loyalty Never Reached the Card Object
daveycodez Aug 12, 2026
26fc5aa
Test: The Engine Fixture Predated planeswalker_loyalty_text, So Nothi…
daveycodez Aug 12, 2026
e479610
Import: Count the Rulings Postgres Kept, Not the Rows the File Sent
daveycodez Aug 12, 2026
b75e455
API: Serve Rulings Newest First, Which Is the Order Scryfall Serves
daveycodez Aug 12, 2026
07bc871
API: Answer a Miss With the Body Scryfall Sends, Not One Generic String
daveycodez Aug 12, 2026
e098ea3
API: The Rest of the Scryfall Surface — /sets, /catalog/* and /symbology
daveycodez Aug 12, 2026
c3b86f3
Style: ruff format every Python file this branch stack touches
daveycodez Aug 12, 2026
9f29852
API: Send the Cache-Control Scryfall Sends, Not the Card Routes' Tier
daveycodez Aug 12, 2026
d31455f
API: Four Places the Reference Routes Differ From What Scryfall Answers
daveycodez Aug 12, 2026
5dc086c
API: A Mana Value Is a Decimal, and This Answered It as an Integer
daveycodez Aug 12, 2026
16af3d8
Engine: legalities Decoded to {} in Any Worker That Only mmapped the …
daveycodez Aug 12, 2026
b5bf12d
Engine: A Fuzzy Name Lookup Rebuilt the Needle 31,724 Times
daveycodez Aug 13, 2026
5adca49
Engine: The Last By-Name Lookups Still Answered From SQL
daveycodez Aug 13, 2026
d7d5bcd
API: The Engine's Fuzzy Fast Path Had Never Once Returned
daveycodez Aug 13, 2026
f3db3d6
Engine: Give autocomplete_names Its Doc Comment Back
daveycodez Aug 13, 2026
650c388
API: Autocomplete Could Not Find a Card With an Accent in Its Name
daveycodez Aug 13, 2026
26f6abc
Engine: Autocomplete Ordered by Bytes Where the SQL Orders by Characters
daveycodez Aug 13, 2026
b19dbaf
Engine: A Card Carried Its Name Twice, and the Second Copy Was 61 Bytes
daveycodez Aug 13, 2026
70bf3a8
API: The Last Identifier in /cards/collection Still Came From SQL
daveycodez Aug 13, 2026
d182658
Engine: The SELECT Never Fetched the Loyalty Column the Loader Reads
daveycodez Aug 13, 2026
a69d54b
Reference: Five Answers the Reference Surface Got Wrong, All of Them …
daveycodez Aug 16, 2026
6a1c3ba
Reference: The Two Tests That Asserted the Behavior Scryfall Does Not…
daveycodez Aug 16, 2026
bee3b4d
Dispatch: An API Client Asking for Nothing Got a Route Listing, Not a…
daveycodez Aug 16, 2026
44228ec
Dispatch: The Tests That Expected an Exception Where There Is Now an …
daveycodez Aug 16, 2026
bdce2b6
Dispatch: A Wrong Method Is Scryfall's 404, Not an Invented 405
daveycodez Aug 16, 2026
ea60507
Merge remote-tracking branch 'upstream/main' into engine-order-vocabu…
daveycodez Aug 21, 2026
3b0011c
Merge branch 'engine-order-vocabulary-upd' into scryfall-cards-api-upd
daveycodez Aug 21, 2026
9471757
Scryfall Compat: Scryfall Serves Eleven Image Sizes Under image_uris,…
daveycodez Aug 17, 2026
17ce4e8
Scryfall Compat: Follow #963's Seams — Rulings Import on AdminResourc…
daveycodez Aug 21, 2026
aa8f154
Scryfall Compat: The Mixin and Its Tests Read AppContext — cache_gene…
daveycodez Aug 22, 2026
e250f44
Merge branch 'scryfall-cards-api-upd' into scryfall-sets-catalogs-sym…
daveycodez Aug 22, 2026
8218235
Scryfall Reference: The Mirror Imports Ride AdminResource, Where #963…
daveycodez Aug 22, 2026
7813a63
Dispatch: An Unknown Path Is Scryfall's Object for a Client, and #966…
daveycodez Aug 22, 2026
806e7c5
Merge upstream/main (a76f0cb) into engine-order-vocabulary
daveycodez Aug 23, 2026
95b7917
Merge engine-order-vocabulary (upstream a76f0cb) into scryfall-cards-api
daveycodez Aug 23, 2026
37b640c
Merge scryfall-cards-api (upstream a76f0cb) into scryfall-sets-catalo…
daveycodez Aug 23, 2026
c0e1695
Card Objects: The Eleven Fixes the Cloudflare Port Measured Against t…
daveycodez Aug 23, 2026
631d89e
Merge scryfall-cards-api (card-object fixes) into scryfall-sets-catal…
daveycodez Aug 23, 2026
edbcddb
Tests: Pin the Two Card-Object Builders to Each Other With a Shared F…
daveycodez Aug 23, 2026
0249d4c
Merge scryfall-cards-api (card-object parity fixture) into scryfall-s…
daveycodez Aug 23, 2026
b3ab7ca
Merge upstream/main (e24a2c7) into scryfall-cards-api
daveycodez Aug 28, 2026
9952b7e
Archive Version: 2026082703, Because Two Sibling Branches Already Spe…
daveycodez Aug 28, 2026
8e5de9a
Merge scryfall-cards-api (upstream e24a2c7, format 2026082703) into s…
daveycodez Aug 28, 2026
eee656d
Housekeeping: The Merge Swept a Worktree's Cargo Target Into the Tree…
daveycodez Aug 28, 2026
3a47724
Merge upstream/main (66a09c3) into scryfall-cards-api
daveycodez Aug 29, 2026
7494275
Merge scryfall-cards-api (upstream 66a09c3, format 2026082703) into s…
daveycodez Aug 29, 2026
02d868c
Mana: {W/U/P} Is Printed — Parse the Symbol INVENTORY, Not a Rule Abo…
daveycodez Aug 29, 2026
dcc0b6b
Collection: A `{name}` Identifier Has Its OWN Keys — It Missed Every …
daveycodez Aug 31, 2026
36e2d5b
Purchase URIs: A Marketplace Search Spells the Name the Way EDHREC Do…
daveycodez Sep 1, 2026
80ff051
Merge scryfall-cards-api (name-identifier keys, purchase-uri search n…
daveycodez Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,34 @@ python -m client.query_runner
- **GET /search** - Card search with query parameter support
- **GET /favicon.ico** - Favicon for web interface

### Scryfall-Compatible Endpoints

Every route Scryfall documents under `/cards`, answering with Scryfall's own response objects and
175-per-page pagination, so a client can be pointed here by swapping its base URL:

- **GET /cards** and **GET /cards/search** - list routes (`format=json|csv`)
- **GET /cards/named**, **/cards/autocomplete**, **/cards/random** - single-card and catalog lookups
- **POST /cards/collection** - up to 75 identifiers at once
- **GET /cards/:id**, **/cards/:code/:number(/:lang)**, and the multiverse / mtgo / arena /
tcgplayer / cardmarket id namespaces
- **GET /cards/:id/rulings** and its four sibling addressings

`format=text` and `format=image` are available on the single-card routes. What is *not* identical —
chiefly that the corpus is a filtered subset of Scryfall's — is recorded in
[docs/issues/local-scryfall-cards-api.md](docs/issues/local-scryfall-cards-api.md).

The reference half of the API, mirrored from Scryfall rather than derived from the corpus:

- **GET /sets**, **/sets/:code**, **/sets/:id**, **/sets/tcgplayer/:id** - 1,047 Set objects
- **GET /catalog/:name** - all twenty catalogs, 62,187 values
- **GET /symbology** - 84 card symbols
- **GET /symbology/parse-mana?cost=** - computed, not stored, so it answers before the first import

These are mirrored because the corpus cannot answer them: a Set object carries eight fields no card
carries, `card_count` counts printings this instance never imported, and a symbol's `svg_uri` exists
nowhere in the card data. Details in
[docs/issues/local-scryfall-sets-catalogs-symbology.md](docs/issues/local-scryfall-sets-catalogs-symbology.md).

### Admin Endpoints

Data-management routes — importing card data, running score/tag backfills, applying schema
Expand Down
77 changes: 77 additions & 0 deletions api/admin_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@

from api.card_processing import preprocess_card
from api.db.bulk_upsert import bulk_upsert as _bulk_upsert
from api.rulings_import import import_rulings as _import_rulings
from api.scryfall_bulk_data_fetcher import BulkDataKey, ScryfallBulkDataFetcher
from api.scryfall_reference_import import import_catalogs as _import_catalogs
from api.scryfall_reference_import import import_sets as _import_sets
from api.scryfall_reference_import import import_symbology as _import_symbology
from api.settings import settings
from api.tag_import import import_art_tags as _import_art_tags
from api.tag_import import import_oracle_tags as _import_oracle_tags
Expand Down Expand Up @@ -399,6 +403,13 @@ def _run_import_under_lock(self) -> None:
self.backfill_prefer_scores()
self.backfill_cubecobra_scores()
_import_oracle_tags(self.app_context.writer_pool, self._bulk_data_fetcher)
# Rulings feed only /cards/*/rulings, so nothing above or below depends on them; they
# sit here rather than in their own pass so one bulk fetch cycle refreshes everything.
self._import_rulings_quietly()
# Alongside the rulings and for the same reason: reference data nothing else in this
# sequence reads, refreshed on the same cycle so one pass brings the whole surface up
# to date rather than leaving /sets and /catalog to age until a manual call.
self._import_reference_quietly()
self.app_context.reload_engine(force=True)
self._clear_caches()
self.app_context.last_import_time.value = time.time()
Expand Down Expand Up @@ -931,6 +942,72 @@ def import_art_tags(self, **_: object) -> dict[str, Any]:
"""Import art tags from Scryfall bulk data into art_tags, art_tag_relationships, and card_art_tags."""
return _import_art_tags(self.app_context.writer_pool, self._bulk_data_fetcher)

@route()
def import_rulings(self, **_: object) -> dict[str, Any]:
"""Import Scryfall rulings bulk data into magic.rulings, backing the /cards/*/rulings routes.

Returns:
The number of rulings loaded.
"""
return {"rulings_loaded": _import_rulings(self.app_context.writer_pool, self._bulk_data_fetcher)}

def _import_rulings_quietly(self) -> None:
"""Refresh the rulings during a bulk import, logging rather than failing on error.

Rulings are the only data in the import sequence nothing else reads: a card search, the
prefer scores and the engine reload all work without them. Letting a bad rulings file
abort the import would cost the corpus refresh to save a rulings refresh.
"""
try:
_import_rulings(self.app_context.writer_pool, self._bulk_data_fetcher)
except Exception:
logger.exception("Rulings import failed; continuing with the rest of the import")

@route()
def import_sets(self, **_: object) -> dict[str, Any]:
"""Mirror Scryfall's set list into magic.sets, backing the /sets routes.

Returns:
A summary of the load.
"""
return _import_sets(self.app_context.writer_pool, self._bulk_data_fetcher)

@route()
def import_catalogs(self, **_: object) -> dict[str, Any]:
"""Mirror the twenty Scryfall catalogs into magic.catalogs, backing /catalog/*.

Returns:
A summary of the load.
"""
return _import_catalogs(self.app_context.writer_pool, self._bulk_data_fetcher)

@route()
def import_symbology(self, **_: object) -> dict[str, Any]:
"""Mirror Scryfall's card symbols into magic.card_symbols, backing /symbology.

Returns:
A summary of the load.
"""
return _import_symbology(self.app_context.writer_pool, self._bulk_data_fetcher)

def _import_reference_quietly(self) -> None:
"""Refresh sets, catalogs and symbology during a bulk import, logging rather than failing.

Each of the three is independent of the other two and of everything else in the sequence,
so one failing upstream endpoint must not cost the other two their refresh — nor the corpus
its. This is the rulings argument applied three more times: nothing downstream reads any of
these tables, so a stale one degrades three endpoints rather than breaking the import.
"""
for name, step in (
("Set", _import_sets),
("Catalog", _import_catalogs),
("Symbology", _import_symbology),
):
try:
step(self.app_context.writer_pool, self._bulk_data_fetcher)
except Exception:
logger.exception("%s import failed; continuing with the rest of the import", name)

@route()
def import_all_is_tags(self, **_: object) -> dict[str, Any]:
"""Discover and import all is: tags from Scryfall syntax documentation.
Expand Down
Loading
Loading