Skip to content

PhilMcClelland/pyfall

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pyfall

A Python library for parsing Scryfall search syntax into an Abstract Syntax Tree (AST).

Pyfall lets you parse Scryfall's powerful card search syntax and compile it to SQL or other query languages for searching your own card database.

Pyfall is not affiliated with, endorsed by, or sponsored by Scryfall.

Installation

pip install pyfall

Quick Start

from pyfall import parse
from pyfall.backends import SQLBackend

# Parse a Scryfall query
ast = parse("t:creature c:green cmc<=3")

# Compile to SQL
backend = SQLBackend()
sql, params = backend.compile(ast)

# Use with your database
cursor.execute(f"SELECT * FROM cards WHERE {sql}", params)

CLI Usage

Pyfall includes a command-line interface for testing queries:

# Parse a query and show the AST
python -m pyfall parse "t:creature c:green"

# Compile a query to SQL
python -m pyfall sql "t:creature c:green"

# Validate a query
python -m pyfall validate "t:creature c:green"

Supported Syntax

Pyfall supports ~99% of Scryfall's search syntax. The canonical reference is Scryfall's syntax documentation.

Text Filters

Syntax Description Example
name (plain text) Card name lightning bolt
t: / type: Type line t:creature, t:"legendary artifact"
o: / oracle: Oracle text o:draw, o:"enters the battlefield"
fo: / fulloracle: Full oracle text (includes reminder) fo:flying
kw: / keyword: Keyword abilities kw:flying, kw:trample
ft: / flavor: Flavor text ft:chandra
a: / artist: Artist name a:"Rebecca Guay"
wm: / watermark: Watermark wm:phyrexian

Color Filters

Syntax Description Example
c: / color: Card colors c:w, c:wu, c:colorless
id: / identity: Color identity id:wubrg, id:c
produces: Mana production produces:g, produces:wu
devotion: Devotion (mana pips) devotion:ww, devotion:uuu

Color Values

  • Single letters: w (white), u (blue), b (black), r (red), g (green)
  • Colorless: c or colorless
  • Multicolor: m or multicolor
  • Guild nicknames: azorius, dimir, rakdos, gruul, selesnya, orzhov, izzet, golgari, boros, simic
  • Shard/Wedge nicknames: bant, esper, grixis, jund, naya, abzan, jeskai, sultai, mardu, temur

Color Comparison Modes

c:wu      # Has at least white AND blue (may have more colors)
c=wu      # Has exactly white and blue (no other colors)
c<=wu     # Colors are subset of white/blue (could be W, U, WU, or colorless)
c>=wu     # Has at least white and blue (same as c:wu)

Numeric Filters

Syntax Description Example
cmc: / mv: / manavalue: Mana value cmc:3, cmc<=2, mv>=5
pow: / power: Power pow:4, pow>=3, pow:*
tou: / toughness: Toughness tou:5, tou<3
pt: / powtou: Power + Toughness pt:6
loy: / loyalty: Loyalty loy:4
def: / defense: Defense (battles) def:5
usd: / eur: / tix: Price usd<1, eur>=10
year: Release year year:2023, year>=2020
date: Release date date>=2023-01-01, date:now
artists: Artist count artists:2
illustrations: Illustration count illustrations>=2

Special Numeric Values

  • even / odd: Parity check for CMC (cmc:even)
  • *: Variable power/toughness (pow:*)

Collector Number

Syntax Description Example
cn: / number: Collector number cn:100, cn>=50
Range syntax Number range cn:100-200

Set & Rarity

Syntax Description Example
set: / s: / e: Set code set:neo, s:mh2
b: / block: Block code b:zendikar
st: Set type st:core, st:expansion
r: / rarity: Rarity r:mythic, r:r, r>=uncommon
in: Ever printed in/at in:rare, in:paper, in:ja

Format Legality

Syntax Description Example
f: / format: / legal: Legal in format f:commander, f:modern
banned: Banned in format banned:legacy
restricted: Restricted in format restricted:vintage

Card Properties (is:)

Pyfall supports all Scryfall is: properties:

Type Properties

is:legendary, is:creature, is:instant, is:sorcery, is:artifact, is:enchantment, is:planeswalker, is:land, is:battle, is:tribal, is:kindred

Layout Properties

is:split, is:flip, is:transform, is:meld, is:leveler, is:dfc, is:mdfc, is:tdfc, is:adventure, is:saga, is:class, is:prototype, is:case

Card Characteristics

is:spell, is:permanent, is:historic, is:vanilla, is:frenchvanilla, is:bear, is:modal, is:party, is:outlaw, is:manland

Format Properties

is:commander, is:brawler, is:companion, is:partner, is:reserved

Mana Properties

is:hybrid, is:phyrexian

Print Properties

is:foil, is:nonfoil, is:promo, is:fullart, is:textless, is:reprint, is:firstprint, is:digital, is:oversized, is:hires, is:spotlight, is:booster

Land Cycle Shortcuts

is:dual, is:fetchland, is:shockland, is:checkland, is:fastland, is:slowland, is:painland, is:filterland, is:triland, is:triome, is:canopyland, is:bikeland, is:bondland, is:bounceland, is:gainland, is:pathway, is:scryland, is:surveilland, is:shadowland, is:storageland, is:tangoland, is:creatureland

Other Properties

is:funny, is:etched, is:glossy, is:alchemy, is:rebalanced, is:universesbeyond, is:colorshifted, is:datestamped

Has Properties (has:)

Syntax Description
has:indicator Has color indicator
has:watermark Has watermark
has:foil Available in foil
has:nonfoil Available in nonfoil
has:promo Is a promo
has:colors Has colors (not colorless)
has:flavor Has flavor text
has:oracle Has oracle text
has:loyalty Has loyalty (planeswalker)
has:pt Has power/toughness
has:multifaced Has multiple faces

Mana Cost

Syntax Description Example
m: / mana: Mana cost pattern m:{2}{W}{W}, m:{G/U}

Supported mana symbols:

  • Colored: {W}, {U}, {B}, {R}, {G}
  • Generic: {1}, {2}, {X}, {Y}
  • Colorless: {C}
  • Snow: {S}
  • Hybrid: {W/U}, {2/W}
  • Phyrexian: {W/P}, {U/P}

Frame & Border

Syntax Description Example
frame: Frame style frame:2015, frame:future
border: Border color border:black, border:borderless
stamp: Security stamp stamp:oval, stamp:acorn

Game Availability

Syntax Description Example
game: Game availability game:paper, game:arena, game:mtgo

Regex Support

Syntax Description Example
name:/pattern/ Name regex name:/^Lightning/
type:/pattern/ Type regex type:/Goblin.*Warrior/
oracle:/pattern/ Oracle regex oracle:/\{T\}:.*damage/
flavor:/pattern/ Flavor regex flavor:/fire/

Note: Regex patterns are converted to SQL LIKE patterns by default. For true regex support, use database-specific backends (see below).

Logical Operators

Syntax Description Example
(space) AND (implicit) t:creature c:green
or OR t:creature or t:artifact
- NOT (negation) -t:creature, -c:red
() Grouping c:w (t:creature or t:enchantment)

Display Directives

These operators parse successfully but affect result presentation rather than filtering:

Syntax Description Example
unique: Result grouping unique:cards, unique:prints, unique:art
order: Sort field order:cmc, order:name, order:usd
direction: Sort direction direction:asc, direction:desc
prefer: Print preference prefer:oldest, prefer:usd-low
include: Include extras include:extras
cheapest: Price preference cheapest:usd
display: Display mode display:grid, display:images

Note: Display directives return 1=1 from the SQL backend since they don't filter results. Use extract_directives() to handle them in your application layer.

Stub Filters (Require External Data)

These filters parse correctly but require external data not in Scryfall bulk downloads:

Syntax Notes
cube: Requires Scryfall cube API data
art: / atag: / arttag: Requires Scryfall art tag data
function: / otag: / oracletag: Requires Scryfall oracle tag data
new: Requires printing history analysis

Enable strict_mode=True to raise UnsupportedFilterError instead of silently matching all cards.

Backend Configuration

Column Mapping

Map Scryfall field names to your database columns:

from pyfall.backends import SQLBackend

backend = SQLBackend(
    column_map={
        'name': 'card_name',
        'type': 'card_type_line',
        'oracle': 'rules_text',
        'colors': 'card_colors',
        'cmc': 'mana_value',
    },
    table_name='my_cards',
    case_insensitive=True,
)

Strict Mode

Enable strict mode to raise errors on stub filters instead of silently matching all:

backend = SQLBackend(strict_mode=True)

try:
    sql, params = backend.compile(parse("cube:vintage"))
except UnsupportedFilterError as e:
    print(f"Filter not supported: {e}")

Database-Specific Features

PostgreSQL

from pyfall.backends import create_postgres_backend

# Factory creates backend with PostgreSQL regex support
backend = create_postgres_backend()

ast = parse("name:/^Lightning/")
sql, params = backend.compile(ast)
# Uses ~* operator for case-insensitive regex

Or configure manually:

from pyfall.backends import SQLBackend, postgres_regex
from pyfall import RegexFilter

backend = SQLBackend(
    node_compilers={
        RegexFilter: postgres_regex,  # Case-insensitive
        # Or: postgres_regex_case_sensitive for case-sensitive
    }
)

MySQL

from pyfall.backends import SQLBackend, mysql_regex
from pyfall import RegexFilter

backend = SQLBackend(
    node_compilers={
        RegexFilter: mysql_regex,
    }
)

SQLite

from pyfall.backends import SQLBackend, sqlite_regex
from pyfall import RegexFilter

# Note: Requires REGEXP function to be loaded in SQLite
backend = SQLBackend(
    node_compilers={
        RegexFilter: sqlite_regex,
    }
)

Custom Node Compilers

Override compilation for any filter type:

from pyfall.backends import SQLBackend
from pyfall import CubeFilter, IsFilter

def my_cube_filter(node, backend):
    """Look up cube membership in your own table."""
    return (
        "id IN (SELECT card_id FROM cube_cards WHERE cube = ?)",
        [node.value]
    )

def my_fetchland_check(node, backend):
    """Use a curated list of fetchland oracle IDs."""
    if node.property == 'fetchland':
        return (
            "oracle_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            [
                # Onslaught fetches
                "a1b2c3...", "d4e5f6...",
                # Zendikar fetches
                "g7h8i9...", "j0k1l2...",
                # etc.
            ]
        )
    # Fall back to default for other is: properties
    return backend._compile_is(node)

backend = SQLBackend(
    node_compilers={
        CubeFilter: my_cube_filter,
        IsFilter: my_fetchland_check,
    }
)

Utility Functions

Walking the AST

from pyfall import parse, walk, find_all, TypeFilter, ColorFilter

ast = parse("t:creature c:green or t:artifact")

# Iterate all nodes
for node in walk(ast):
    print(type(node).__name__)

# Find specific node types
type_filters = find_all(ast, TypeFilter)
color_filters = find_all(ast, ColorFilter)

Pretty Printing

from pyfall import parse, pretty_print

ast = parse("t:creature c:green cmc<=3")
print(pretty_print(ast))
# Output:
# AndGroup(
#   TypeFilter(value='creature')
#   ColorFilter(colors=['G'], mode='at_least')
#   CMCFilter(op=LE, value=3.0)
# )

Query Validation

from pyfall import parse, validate

result = validate("t:creature c:green")
print(result.is_valid)        # True
print(result.filters_used)    # {TypeFilter, ColorFilter}
print(result.has_directives)  # False
print(result.stub_filters)    # set()

result = validate("cube:vintage")
print(result.stub_filters)    # {CubeFilter}
print(result.warnings)        # ['cube: requires external data']

Extracting Display Directives

from pyfall import parse, extract_directives

ast = parse("t:creature unique:art order:name")
filters, directives = extract_directives(ast)

# filters: AndGroup with just TypeFilter
# directives: {'unique': UniqueFilter(mode='art'), 'order': OrderFilter(field='name')}

AST Node Types

Logical Nodes

  • AndGroup - Logical AND of children
  • OrGroup - Logical OR of children
  • NotFilter - Negation

Text Filters

  • NameFilter - Card name
  • TypeFilter - Type line
  • OracleFilter - Oracle text
  • FullOracleFilter - Full oracle (with reminder)
  • FlavorFilter - Flavor text
  • KeywordFilter - Keywords

Color Filters

  • ColorFilter - Card colors
  • ColorIdentityFilter - Color identity
  • ProducesFilter - Mana production
  • DevotionFilter - Devotion count

Numeric Filters

  • CMCFilter - Mana value
  • PowerFilter - Power
  • ToughnessFilter - Toughness
  • PowerToughnessFilter - P+T combined
  • LoyaltyFilter - Loyalty
  • DefenseFilter - Defense
  • PriceFilter - Price (usd/eur/tix)
  • CollectorNumberFilter - Collector number
  • PrintsFilter - Print count
  • DateFilter - Release date
  • ArtistCountFilter - Artist count
  • IllustrationsFilter - Illustration count
  • YearFilter - Release year

Set/Rarity

  • SetFilter - Set code
  • BlockFilter - Block code
  • SetTypeFilter - Set type
  • RarityFilter - Rarity

Format

  • FormatFilter - Format legality

Properties

  • IsFilter - Card properties
  • HasFilter - Has properties
  • ManaCostFilter - Mana cost pattern
  • IncludeFilter - Include extras

Metadata

  • ArtistFilter - Artist name
  • LanguageFilter - Language
  • WatermarkFilter - Watermark
  • FrameFilter - Frame style
  • BorderFilter - Border color
  • GameFilter - Game availability
  • CubeFilter - Cube membership
  • StampFilter - Security stamp
  • InFilter - Ever printed in
  • ArtTagFilter - Art tags
  • OracleTagFilter - Oracle tags
  • NewFilter - New property

Display/Sort (Directives)

  • UniqueFilter - Result grouping
  • OrderFilter - Sort order
  • PreferFilter - Print preference
  • CheapestFilter - Price preference
  • DisplayFilter - Display mode

Mana Symbols

  • ManaSymbol - Base class
  • GenericMana - Generic mana ({1}, {X})
  • ColoredMana - Colored mana ({W}, {U})
  • HybridMana - Hybrid mana ({W/U})
  • PhyrexianMana - Phyrexian mana ({W/P})
  • ColorlessMana - Colorless mana ({C})
  • SnowMana - Snow mana ({S})

Regex

  • RegexFilter - Regex pattern match

Known Semantic Differences from Scryfall

Some filters have different semantics than Scryfall:

Filter Scryfall Pyfall Default
in:rare Ever printed at rare (any printing) Current printing is rare
in:m / in:r Treats as set code (no match) Treats as rarity abbreviation
is:dual Exact list of 10 ABUR duals Pattern match on type line
is:fetchland Curated list Pattern match on oracle text
name:/pattern/ True regex Converted to LIKE (approximate)
m:{Q} Error: "Unknown mana symbols" Accepts as GenericMana
c:xyz Error: "Unknown color" Accepts with empty color list
cmc:abc Error: "Value must be a number" Falls back to 0.0

in: Filter Disambiguation

Scryfall disambiguates in: values in this order:

  1. Full rarity names only: in:rare, in:mythic, in:uncommon work; in:r, in:m do not expand to rarities
  2. Game platforms: in:paper, in:mtgo, in:arena
  3. Language codes: in:ru, in:en, in:ja, etc. (2-3 letter ISO codes)
  4. Set codes: Any other value is treated as a set code

Pyfall currently treats single-letter rarity abbreviations (m, r, c, u) as rarities, which differs from Scryfall.

Color Filter Defaults

Filter Default Mode
c:wu AT_LEAST (has at least W and U, may have more)
id:wu AT_MOST (identity is subset of WU)
c=wu EXACT (has exactly W and U)
c<=wu AT_MOST (colors are subset of WU)
c>=wu AT_LEAST (same as c:wu)

Silent Normalizations

Pyfall accepts some inputs that Scryfall rejects:

  • Invalid mana symbols: m:{Q} becomes GenericMana(value='Q') instead of an error
  • Invalid colors: c:xyz results in an empty color filter instead of an error
  • Non-numeric CMC: cmc:abc falls back to 0.0 instead of an error

For stricter validation matching Scryfall's behavior, consider adding input validation before parsing.

Recommended: Use strict_mode for Production

For production use, enable strict_mode to get errors on stub filters that require external data:

from pyfall import parse, SQLBackend, UnsupportedFilterError

backend = SQLBackend(strict_mode=True)

try:
    sql, params = backend.compile(parse("cube:vintage"))
except UnsupportedFilterError as e:
    print(f"Filter not supported: {e}")

For exact Scryfall semantics on in: filters, you'll need a printings table with historical data. For is:dual/is:fetchland, use node_compilers to inject curated card lists.

Format Legality Schema

The default FormatFilter compilation assumes a JSON column structure:

{"commander": "legal", "modern": "banned", "vintage": "restricted"}

For different schemas (e.g., separate legalities table), override with node_compilers:

def my_format_filter(node, backend):
    """Check format legality in normalized table."""
    return (
        "id IN (SELECT card_id FROM legalities WHERE format = ? AND status = ?)",
        [node.value, node.status]
    )

backend = SQLBackend(
    node_compilers={FormatFilter: my_format_filter}
)

Development

# Clone the repo
git clone https://github.com/PhilMcClelland/pyfall.git
cd pyfall

# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run with coverage
pytest --cov=pyfall

# Type checking
mypy src/pyfall

# Linting
ruff check src/pyfall

License

MIT License - see LICENSE for details.

Acknowledgments

  • Scryfall for their excellent search syntax and API
  • The Magic: The Gathering community

About

Scryfall syntax library for querying from a Scryfall bulk export

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages