Compare commits

..

3 commits

Author SHA1 Message Date
f52b963154 Add a README 2021-07-16 00:46:58 -04:00
1fd1d81a2a Fix name search 2021-07-16 00:46:43 -04:00
a1cf52312e Support GTE and LTE on color and rarity 2021-07-15 23:56:25 -04:00
4 changed files with 151 additions and 49 deletions

58
README.org Normal file
View file

@ -0,0 +1,58 @@
#+TITLE: Tutor
A collection manager for Magic: The Gathering playing cards.
* Searching
Text in the search bar will be used to filter cards having that text in their
name. Additionally, the keyword expressions below can be used to search for
cards with certain properties.
** Examples
- ~bolt~ :: Find all cards with "bolt" in the name
- ~"God of"~ :: Find all cards with "God of" in the name
- ~t:legendary t:creature c:jund~ :: Find all legendary creatures with a color
identity of red/blue/green
- ~color<=ubg~ :: Find all spells that are blue, black, green, or any
combination thereof.
- ~color:red set:stx rarity>=rare~ :: Find all red cards in Strixhaven that are
rare or mythic
- ~t:enchantment o:"enters the battlefield"~ :: Find all enchantments with ETB
effects
** Keywords
*** Colors
- Keywords :: =c=, =color=
- Operators :: ~:~ (matches), ~>=~ (greater than or equal to), ~<=~ (less than
or equal to)
Matches cards of the chosen color or colors.
- Single colors :: =w= or =white=, =u= or =blue=, =b= or =black, =g= or =green=, =r= or =red=
- Any combination of abbreviated single colors :: e.g.: =rg=, =uw=, or =wubgr=
- Ravnican guilds :: =boros= (white/red), =golgari= (green/black), =selesnya=
(green/white), =dimir= (blue/black), =orzhov= (white/black), =izzet=
(blue/red), =gruul= (red/green), =azorius= (white/blue), =rakdos= (black/red),
=simic= (green/blue)
- Alaran shards :: =bant= (white/green/blue), =esper= (blue/white/black),
=grixis= (black/blue/red), =jund= (red/blue/green), =naya= (green/red/white)
- Tarkirian wedges :: =abzan= (white/black/green), =jeskai= (white/blue/red),
=sultai= (blue/black/green), =mardu= (white/black/red), =temur=
(blue/red/green)
*** Sets
- Keywords :: =s=, =set=, =e=, =expansion=
- Operators :: ~:~ (matches)
*** Rarity
- Keywords :: =r=, =rarity=
- Operators :: ~:~ (matches), ~>=~ (greater than or equal to), ~<=~ (less than
or equal to)
*** Type
- Keywords :: =t=, =type=
- Operators :: ~:~ (matches)
*** Oracle Text
- Keywords :: =o=, =oracle=
- Operators :: ~:~ (matches)

View file

View file

@ -102,38 +102,56 @@ async def advanced_search(
params = {} params = {}
sets = [] sets = []
logging.debug("Performing search for: %s", search) logger.debug("Performing search for: %s", search)
for i, criterion in enumerate(search.criteria): for i, criterion in enumerate(search.criteria):
param = f"param_{i}" param = f"param_{i}"
if isinstance(criterion, tutor.search.Name): if isinstance(criterion, tutor.search.Name):
constraints.append(f"cards.name LIKE :{param}") constraints.append(f"cards.name LIKE :{param}")
params[param] = f"%{criterion.name}%" params[param] = f"%{criterion.text}%"
if isinstance(criterion, tutor.search.Type): if isinstance(criterion, tutor.search.Type):
constraints.append(f"cards.type_line LIKE :{param}") constraints.append(f"cards.type_line LIKE :{param}")
params[param] = f"%{criterion.name}%" params[param] = f"%{criterion.text}%"
if isinstance(criterion, tutor.search.IsExpansion): if isinstance(criterion, tutor.search.Expansion):
constraints.append(f"cards.set_code LIKE :{param}") constraints.append(f"cards.set_code LIKE :{param}")
params[param] = criterion.set_code params[param] = criterion.set_code
if isinstance(criterion, tutor.search.InExpansion): if isinstance(criterion, tutor.search.Color):
sets.append(criterion.set_code) if criterion.operator == tutor.search.Operator.matches:
if isinstance(criterion, tutor.search.IsColor):
constraints.append(f"cards.color_identity LIKE :{param}") constraints.append(f"cards.color_identity LIKE :{param}")
params[param] = tutor.models.Color.to_string(criterion.colors) params[param] = tutor.models.Color.to_string(criterion.colors)
if isinstance(criterion, tutor.search.IsRarity): if criterion.operator == tutor.search.Operator.lte:
constraints.append(
"({})".format(
" OR ".join(
[
f"cards.color_identity LIKE :{param}_{color}"
for color in criterion.colors
]
)
)
)
params.update(
{f"{param}_{color}": str(color) for color in criterion.colors}
)
params[param] = tutor.models.Color.to_string(criterion.colors)
if criterion.operator == tutor.search.Operator.gte:
constraints.append(f"cards.color_identity LIKE :{param}")
params[param] = "%{}%".format(
"%".join(tutor.models.Color.to_string(criterion.colors))
)
if isinstance(criterion, tutor.search.Rarity):
if criterion.operator == tutor.search.Operator.matches:
constraints.append(f"cards.rarity LIKE :{param}") constraints.append(f"cards.rarity LIKE :{param}")
params[param] = str(criterion.rarity) params[param] = str(criterion.rarity)
if criterion.operator == tutor.search.Operator.lte:
constraints.append(f"rarities.rarity_ord <= :{param}")
params[param] = criterion.rarity.value
if criterion.operator == tutor.search.Operator.gte:
constraints.append(f"rarities.rarity_ord >= :{param}")
params[param] = criterion.rarity.value
if isinstance(criterion, tutor.search.Oracle): if isinstance(criterion, tutor.search.Oracle):
constraints.append(f"cards.oracle_text LIKE :{param}") constraints.append(f"cards.oracle_text LIKE :{param}")
params[param] = f"%{criterion.text}%" params[param] = f"%{criterion.text}%"
if sets:
set_params = {f"set_{i}": set_code for i, set_code in enumerate(sets)}
constraints.append(
"cards.set_code IN ({})".format(
", ".join([f":{key}" for key in set_params.keys()])
)
)
params.update(set_params)
if in_collection is not None: if in_collection is not None:
if in_collection: if in_collection:
joins.append("JOIN copies ON (cards.scryfall_id = copies.scryfall_id)") joins.append("JOIN copies ON (cards.scryfall_id = copies.scryfall_id)")
@ -157,6 +175,7 @@ async def advanced_search(
f"LIMIT {offset},{limit}", f"LIMIT {offset},{limit}",
] ]
) )
logger.debug("Query: %s", (query, params))
cursor = await db.execute(query, params) cursor = await db.execute(query, params)
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [ return [

View file

@ -1,4 +1,5 @@
import dataclasses import dataclasses
import enum
import functools import functools
import typing import typing
@ -7,38 +8,35 @@ import parsy
import tutor.models import tutor.models
class Operator(enum.Enum):
matches = ":"
gte = ">="
lte = "<="
@dataclasses.dataclass
class Criterion: class Criterion:
... operator: Operator
@dataclasses.dataclass @dataclasses.dataclass
class IsExpansion(Criterion): class Expansion(Criterion):
set_code: parsy.string set_code: parsy.string
@dataclasses.dataclass @dataclasses.dataclass
class InExpansion(Criterion): class Color(Criterion):
set_code: parsy.string
@dataclasses.dataclass
class IsColor(Criterion):
colors: typing.Set[tutor.models.Color] colors: typing.Set[tutor.models.Color]
@dataclasses.dataclass @dataclasses.dataclass
class IsRarity(Criterion): class Rarity(Criterion):
rarity: tutor.models.Rarity rarity: tutor.models.Rarity
@dataclasses.dataclass
class Name(Criterion):
name: parsy.string
@dataclasses.dataclass @dataclasses.dataclass
class Type(Criterion): class Type(Criterion):
name: parsy.string text: parsy.string
@dataclasses.dataclass @dataclasses.dataclass
@ -46,6 +44,12 @@ class Oracle(Criterion):
text: parsy.string text: parsy.string
@dataclasses.dataclass
class Name(Criterion):
operator: Operator = Operator.matches
text: parsy.string = ""
@dataclasses.dataclass @dataclasses.dataclass
class Search: class Search:
criteria: typing.List[Criterion] criteria: typing.List[Criterion]
@ -63,7 +67,10 @@ W, U, B, G, R = (
tutor.models.Color.Green, tutor.models.Color.Green,
tutor.models.Color.Red, tutor.models.Color.Red,
) )
matches = parsy.string(":")
matches = parsy.string(":").map(Operator)
gte = parsy.string(">=").map(Operator)
lte = parsy.string("<=").map(Operator)
color = ( color = (
ustring("w").result(W) ustring("w").result(W)
@ -112,38 +119,56 @@ wedge = (
| lstring("temur").result({U, R, G}) | lstring("temur").result({U, R, G})
) )
colors = single_color | guild | shard | wedge | multicolor any_color = single_color | guild | shard | wedge | multicolor
colors = parsy.seq(
_keyword=lstring_from("c", "color", "colors"),
operator=matches | gte | lte,
colors=any_color,
).combine_dict(Color)
expansion_string = ( expansion_string = (
parsy.regex(r"[a-zA-Z0-9]+").map(lambda s: s.upper()).desc("expansion set code") parsy.regex(r"[a-zA-Z0-9]+").map(lambda s: s.upper()).desc("expansion set code")
) )
is_expansion = lstring_from("e", "expansion", "s", "set") >> matches >> expansion_string.map(IsExpansion) expansion = parsy.seq(
_keyword=lstring_from("e", "expansion", "s", "set"),
operator=matches,
set_code=expansion_string,
).combine_dict(Expansion)
in_expansion = lstring("in") >> matches >> expansion_string.map(InExpansion) # in_expansion = lstring("in") >> matches >> expansion_string.map(InExpansion)
rarity = ( any_rarity = (
lstring_from("c", "common").result(tutor.models.Rarity.Common) lstring_from("c", "common").result(tutor.models.Rarity.Common)
| lstring_from("u", "uncommon").result(tutor.models.Rarity.Uncommon) | lstring_from("u", "uncommon").result(tutor.models.Rarity.Uncommon)
| lstring_from("r", "rare").result(tutor.models.Rarity.Rare) | lstring_from("r", "rare").result(tutor.models.Rarity.Rare)
| lstring_from("m", "mythic").result(tutor.models.Rarity.Mythic) | lstring_from("m", "mythic").result(tutor.models.Rarity.Mythic)
) )
is_rarity = lstring_from("r", "rarity") >> matches >> rarity.map(IsRarity) rarity = parsy.seq(
_keyword=lstring_from("r", "rarity"),
is_color = lstring_from("c", "color", "colors") >> matches >> colors.map(IsColor) operator=matches | gte | lte,
rarity=any_rarity,
).combine_dict(Rarity)
string_literal = parsy.regex(r'"[^"]*"').map(lambda s: s[1:-1]) | parsy.regex(r"[^\s]+") string_literal = parsy.regex(r'"[^"]*"').map(lambda s: s[1:-1]) | parsy.regex(r"[^\s]+")
has_type = lstring_from("t", "type") >> matches >> string_literal.map(Type) type_line = parsy.seq(
_keyword=lstring_from("t", "type"),
operator=matches,
text=string_literal,
).combine_dict(Type)
has_oracle = lstring_from("o", "oracle") >> matches >> string_literal.map(Oracle) oracle = parsy.seq(
_keyword=lstring_from("o", "oracle"),
operator=matches,
text=string_literal,
).combine_dict(Oracle)
name = string_literal.map(Name) name = parsy.seq(text=string_literal).combine_dict(Name)
criterion = ( criterion = colors | expansion | rarity | type_line | oracle | name
is_expansion | in_expansion | is_rarity | is_color | has_type | has_oracle | name
)
padding = parsy.regex(r"\s*") padding = parsy.regex(r"\s*")
search = padding >> (criterion << padding).many().map(Search) search = padding >> (criterion << padding).many().map(Search)