Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
547 changes: 414 additions & 133 deletions scripts/capture_fixtures.py

Large diffs are not rendered by default.

Empty file added tests/sweep/__init__.py
Empty file.
125 changes: 125 additions & 0 deletions tests/sweep/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Sweep Fixture Corpus

## Selection criterion

Every fixture in this directory exists to give the property sweep engine
a payload that contains a specific Scryfall API field. Fixtures are chosen
so that, collectively, every non-trivial `@property` accessor across every
mixin is exercised by at least one fixture — including optional fields that
appear only on certain card types (vanguard, battle, attraction, …) and
fields that appear only on individual faces of multi-faced cards.

This corpus is intentionally independent of the usage fixture corpus in
`tests/usage/fixtures/`. The two corpora are selected under different
pressures and must not be coupled: a fixture refresh in one corpus must
not silently break the other.

## Corpus contents (33 fixtures)

### Card layout pins (12)

The 12 layout pins from the usage corpus, re-captured independently.
Full duplication is intended — the corpora must be able to drift apart
as they are refreshed and extended under their respective pressures.

| Fixture | Layout | Endpoint |
|---|---|---|
| `cards_named__black_lotus` | normal | `cards/named` |
| `cards_by_id__normal` | normal | `cards/id` |
| `cards_by_id__transform` | transform | `cards/id` |
| `cards_by_id__modal_dfc` | modal_dfc | `cards/id` |
| `cards_by_id__split` | split | `cards/id` |
| `cards_by_id__adventure` | adventure | `cards/id` |
| `cards_by_id__saga` | saga | `cards/id` |
| `cards_by_id__meld` | meld | `cards/id` |
| `cards_by_id__flip` | flip | `cards/id` |
| `cards_by_id__leveler` | leveler | `cards/id` |
| `cards_by_id__class` | class | `cards/id` |
| `cards_by_id__token` | token | `cards/id` |

### Card accessor pins (11)

One card per accessor group not reliably covered by the layout corpus.

| Fixture | Discovery query | Covered fields |
|---|---|---|
| `cards_by_id__vanguard` | `t:vanguard order:released dir:asc` | `hand_modifier`, `life_modifier`, `promo_types` |
| `cards_by_id__japanese` | `lang:ja year>=2020 -is:dfc order:released dir:asc` | `printed_name`, `printed_text`, `printed_type_line` |
| `cards_by_id__attraction` | `t:attraction order:released dir:asc` | `attraction_lights` |
| `cards_by_id__indicator` | `has:indicator order:released dir:asc` | `color_indicator` |
| `cards_by_id__content_warning` | `is:contentwarning order:released dir:asc` | `content_warning` |
| `cards_by_id__battle` | `t:battle -is:dfc order:released dir:asc` | `defense` |
| `cards_by_id__flavor_name` | `has:flavorname order:released dir:asc` | `flavor_name` |
| `cards_by_id__planeswalker` | `t:planeswalker order:released dir:asc` | `loyalty` |
| `cards_by_id__etched` | `is:etched order:released dir:asc` | `tcgplayer_etched_id` |
| `cards_by_id__variation` | `is:variation order:released dir:asc` | `variation_of` |
| `cards_by_id__watermark` | `has:watermark order:released dir:asc` | `watermark` |

### Card face accessor pins (4)

Cards chosen because their `card_faces` array contains the target field.

| Fixture | Discovery query | Covered fields (on `card_faces`) |
|---|---|---|
| `cards_by_id__reversible` | `is:reversible order:released dir:asc` | `cmc`, `layout`, `oracle_id` |
| `cards_by_id__battle_dfc` | `t:battle is:dfc order:released dir:asc` | `defense` |
| `cards_by_id__planeswalker_transform` | `t:planeswalker is:transform order:released dir:asc` | `loyalty` |
| `cards_by_id__japanese_dfc` | `lang:ja is:dfc year>=2020 order:released dir:asc` | `printed_name`, `printed_text`, `printed_type_line` |

### Non-card fixtures (6)

| Fixture | Purpose |
|---|---|
| `sets_by_code__onc` | Set object; ONC covers 21/21 set accessors (LEA in usage covers 14) |
| `bulk_data_by_id__oracle_cards` | Bulk-data object |
| `catalogs_creature_types` | Catalog envelope |
| `cards_search__lea_red` | List envelope covering `has_more` and `total_cards` |
| `cards_search__mana_t_warning` | List envelope covering `warnings` |
| `cards_search__paginated` | List envelope covering `next_page` (truncated; see below) |

`/sets` is not captured — 1047 objects / 621 KB; the set spec item corpus
comes from the single set object (`sets_by_code__onc`).

## Provenance format

Every fixture is a JSON object with two top-level keys:

```json
{
"_provenance": {
"captured_at": "<ISO 8601 UTC timestamp>",
"endpoint": "<Scryfall endpoint label>",
"source_url": "<full URL that was fetched>",
"discovered_via": "<query or method used to identify this pin>",
"scryfall_id": "<UUID, when the response has an id field>",
"truncated_to": 2
},
"payload": { ... }
}
```

`truncated_to` appears only on `cards_search__paginated`, where the `data`
array was trimmed to preserve `has_more: true` and `next_page` while keeping
the file small. All other fixtures are stored verbatim as returned by the API.

## Refreshing the corpus

Run the capture script to re-pull all sweep fixtures from the live Scryfall API:

```bash
python scripts/capture_fixtures.py sweep
```

To refresh a single fixture:

```bash
python scripts/capture_fixtures.py sweep cards_by_id__vanguard
```

Each card is pinned by its Scryfall UUID. The `discovered_via` field in the
fixture's `_provenance` records the search query that originally located it,
so a pin can be re-derived by running that query on Scryfall and taking the
first result's `id`.

Scryfall enforces a 10 req/s rate limit; the capture script pauses 110 ms
between requests to stay within budget.
22 changes: 22 additions & 0 deletions tests/sweep/fixtures/bulk_data_by_id__oracle_cards.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"_provenance": {
"captured_at": "2026-08-11T00:00:00.000000+00:00",
"endpoint": "bulk-data/id",
"source_url": "https://api.scryfall.com/bulk-data/27bf3214-1271-490b-bdfe-c0be6c23d02e",
"scryfall_id": "27bf3214-1271-490b-bdfe-c0be6c23d02e",
"discovered_via": "bulk-data (oracle_cards type)"
},
"payload": {
"object": "bulk_data",
"id": "27bf3214-1271-490b-bdfe-c0be6c23d02e",
"type": "oracle_cards",
"updated_at": "2026-05-30T21:07:31.013+00:00",
"uri": "https://api.scryfall.com/bulk-data/27bf3214-1271-490b-bdfe-c0be6c23d02e",
"name": "Oracle Cards",
"description": "A JSON file containing one Scryfall card object for each Oracle ID on Scryfall. The chosen sets for the cards are an attempt to return the most up-to-date recognizable version of the card.",
"size": 173115837,
"download_uri": "https://data.scryfall.io/oracle-cards/oracle-cards-20260530210731.json",
"content_type": "application/json",
"content_encoding": "gzip"
}
}
185 changes: 185 additions & 0 deletions tests/sweep/fixtures/cards_by_id__adventure.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
{
"_provenance": {
"captured_at": "2026-08-11T00:00:00.000000+00:00",
"endpoint": "cards/id",
"source_url": "https://api.scryfall.com/cards/c7d5e394-8e41-442e-ae97-a478a61e1b9d",
"scryfall_id": "c7d5e394-8e41-442e-ae97-a478a61e1b9d",
"discovered_via": "is:adventure order:released dir:asc"
},
"payload": {
"object": "card",
"id": "c7d5e394-8e41-442e-ae97-a478a61e1b9d",
"oracle_id": "ea27c8bf-33c5-443f-b8b2-51235efc2491",
"multiverse_ids": [
472967
],
"mtgo_id": 78096,
"arena_id": 70152,
"tcgplayer_id": 198997,
"cardmarket_id": 400494,
"name": "Ardenvale Tactician // Dizzying Swoop",
"lang": "en",
"released_at": "2019-10-04",
"uri": "https://api.scryfall.com/cards/c7d5e394-8e41-442e-ae97-a478a61e1b9d",
"scryfall_uri": "https://scryfall.com/card/eld/5/ardenvale-tactician-dizzying-swoop?utm_source=api",
"layout": "adventure",
"highres_image": true,
"image_status": "highres_scan",
"image_uris": {
"small": "https://cards.scryfall.io/small/front/c/7/c7d5e394-8e41-442e-ae97-a478a61e1b9d.jpg?1572489629",
"normal": "https://cards.scryfall.io/normal/front/c/7/c7d5e394-8e41-442e-ae97-a478a61e1b9d.jpg?1572489629",
"large": "https://cards.scryfall.io/large/front/c/7/c7d5e394-8e41-442e-ae97-a478a61e1b9d.jpg?1572489629",
"png": "https://cards.scryfall.io/png/front/c/7/c7d5e394-8e41-442e-ae97-a478a61e1b9d.png?1572489629",
"art_crop": "https://cards.scryfall.io/art_crop/front/c/7/c7d5e394-8e41-442e-ae97-a478a61e1b9d.jpg?1572489629",
"border_crop": "https://cards.scryfall.io/border_crop/front/c/7/c7d5e394-8e41-442e-ae97-a478a61e1b9d.jpg?1572489629"
},
"mana_cost": "{1}{W}{W} // {1}{W}",
"cmc": 3.0,
"type_line": "Creature — Human Knight // Instant — Adventure",
"power": "2",
"toughness": "3",
"colors": [
"W"
],
"color_identity": [
"W"
],
"keywords": [
"Flying"
],
"card_faces": [
{
"object": "card_face",
"name": "Ardenvale Tactician",
"mana_cost": "{1}{W}{W}",
"type_line": "Creature — Human Knight",
"oracle_text": "Flying",
"power": "2",
"toughness": "3",
"flavor_text": "In a castle that prizes loyalty above all, the bond between knight and griffin is unmatched.",
"artist": "Jason Rainville",
"artist_id": "6ed7e669-579b-443d-b223-e5cbcb2a7483",
"illustration_id": "627d6163-08a5-40b8-938c-264d01ec29bb"
},
{
"object": "card_face",
"name": "Dizzying Swoop",
"mana_cost": "{1}{W}",
"type_line": "Instant — Adventure",
"oracle_text": "Tap up to two target creatures. (Then exile this card. You may cast the creature later from exile.)",
"artist": "Jason Rainville",
"artist_id": "6ed7e669-579b-443d-b223-e5cbcb2a7483"
}
],
"all_parts": [
{
"object": "related_card",
"id": "a8d50cf9-03e6-467a-aadf-84a24106ffa9",
"component": "combo_piece",
"name": "On an Adventure",
"type_line": "Card",
"uri": "https://api.scryfall.com/cards/a8d50cf9-03e6-467a-aadf-84a24106ffa9"
},
{
"object": "related_card",
"id": "bd6ccd0b-5279-431f-b65a-7fdbdffd1a90",
"component": "combo_piece",
"name": "Ardenvale Tactician // Dizzying Swoop",
"type_line": "Creature — Human Knight // Instant — Adventure",
"uri": "https://api.scryfall.com/cards/bd6ccd0b-5279-431f-b65a-7fdbdffd1a90"
}
],
"legalities": {
"standard": "not_legal",
"future": "not_legal",
"historic": "legal",
"timeless": "legal",
"gladiator": "legal",
"pioneer": "legal",
"modern": "legal",
"legacy": "legal",
"pauper": "legal",
"vintage": "legal",
"penny": "legal",
"commander": "legal",
"oathbreaker": "legal",
"standardbrawl": "not_legal",
"brawl": "legal",
"alchemy": "not_legal",
"paupercommander": "legal",
"duel": "legal",
"oldschool": "not_legal",
"premodern": "not_legal",
"predh": "not_legal",
"tlr": "legal"
},
"games": [
"arena",
"paper",
"mtgo"
],
"reserved": false,
"game_changer": false,
"foil": true,
"nonfoil": true,
"finishes": [
"nonfoil",
"foil"
],
"oversized": false,
"promo": false,
"reprint": false,
"variation": false,
"set_id": "a90a7b2f-9dd8-4fc7-9f7d-8ea2797ec782",
"set": "eld",
"set_name": "Throne of Eldraine",
"set_type": "expansion",
"set_uri": "https://api.scryfall.com/sets/a90a7b2f-9dd8-4fc7-9f7d-8ea2797ec782",
"set_search_uri": "https://api.scryfall.com/cards/search?order=set&q=e%3Aeld&unique=prints",
"scryfall_set_uri": "https://scryfall.com/sets/eld?utm_source=api",
"rulings_uri": "https://api.scryfall.com/cards/c7d5e394-8e41-442e-ae97-a478a61e1b9d/rulings",
"prints_search_uri": "https://api.scryfall.com/cards/search?order=released&q=oracleid%3Aea27c8bf-33c5-443f-b8b2-51235efc2491&unique=prints",
"collector_number": "5",
"digital": false,
"rarity": "common",
"flavor_text": "In a castle that prizes loyalty above all, the bond between knight and griffin is unmatched.",
"card_back_id": "0aeebaf5-8c7d-4636-9e82-8c27447861f7",
"artist": "Jason Rainville",
"artist_ids": [
"6ed7e669-579b-443d-b223-e5cbcb2a7483"
],
"illustration_id": "627d6163-08a5-40b8-938c-264d01ec29bb",
"border_color": "black",
"frame": "2015",
"full_art": false,
"textless": false,
"booster": true,
"story_spotlight": false,
"edhrec_rank": 15543,
"penny_rank": 7421,
"preview": {
"source": "Wizards of the Coast",
"source_uri": "https://magic.wizards.com/en/articles/archive/card-preview/adventure-adventure-2019-09-12",
"previewed_at": "2019-09-12"
},
"prices": {
"usd": "0.09",
"usd_foil": "0.21",
"usd_etched": null,
"eur": "0.14",
"eur_foil": "0.14",
"tix": "0.03"
},
"related_uris": {
"gatherer": "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=472967&printed=false",
"tcgplayer_infinite_articles": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=tcgplayer.com%2Fsearch%2Farticles&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Farticles%3FproductLineName%3Dmagic%26q%3DArdenvale%2BTactician%2B%252F%252F%2BDizzying%2BSwoop",
"tcgplayer_infinite_decks": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=tcgplayer.com%2Fsearch%2Fdecks&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Fdecks%3FproductLineName%3Dmagic%26q%3DArdenvale%2BTactician%2B%252F%252F%2BDizzying%2BSwoop",
"edhrec": "https://edhrec.com/route/?cc=Ardenvale+Tactician"
},
"purchase_uris": {
"tcgplayer": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&u=https%3A%2F%2Fwww.tcgplayer.com%2Fproduct%2F198997%3Fpage%3D1",
"cardmarket": "https://www.cardmarket.com/en/Magic/Products?idProduct=400494&referrer=scryfall&utm_campaign=card_prices&utm_medium=text&utm_source=scryfall",
"cardhoarder": "https://www.cardhoarder.com/cards/78096?affiliate_id=scryfall&ref=card-profile&utm_campaign=affiliate&utm_medium=card&utm_source=scryfall"
}
}
}
Loading
Loading