Initial commit

This commit is contained in:
Correl Roush 2021-07-05 18:23:08 -04:00
commit 4c0fdbe89f
12 changed files with 296 additions and 0 deletions

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
.DS_Store
.idea
*.log
tmp/
*.egg-info
poetry.lock
*.py[cod]
*.egg
build
htmlcov

0
README.rst Normal file
View file

20
pyproject.toml Normal file
View file

@ -0,0 +1,20 @@
[tool.poetry]
name = "tutor"
version = "0.1.0"
description = ""
authors = ["Correl Roush <correl@gmail.com>"]
[tool.poetry.dependencies]
python = "^3.9"
tornado = "^6.1"
aiosqlite = "^0.17.0"
click = "^8.0.1"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
black = "^21.6b0"
mypy = "^0.910"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

7
tables.sql Normal file
View file

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS `copies` (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scryfallId TEXT,
isFoil INTEGER NOT NULL DEFAULT 0,
language TEXT,
condition TEXT
);

0
tests/__init__.py Normal file
View file

5
tests/test_tutor.py Normal file
View file

@ -0,0 +1,5 @@
from tutor import __version__
def test_version():
assert __version__ == '0.1.0'

1
tutor/__init__.py Normal file
View file

@ -0,0 +1 @@
__version__ = '0.1.0'

101
tutor/__main__.py Normal file
View file

@ -0,0 +1,101 @@
import dataclasses
import json
import logging
import typing
import aiosqlite
import click
import tornado.ioloop
import tornado.web
import tutor.csvimport
@dataclasses.dataclass
class Card:
uuid: str
name: str
set_code: str
async def search(
db: aiosqlite.Connection,
name: typing.Optional[str],
) -> typing.List[Card]:
db.row_factory = aiosqlite.Row
constraints = []
params = {}
if name:
constraints.append("cards.name LIKE :name")
params["name"] = f"%{name}%"
query = " ".join(
[
"SELECT * FROM cards",
"WHERE" if constraints else "",
" AND ".join(constraints),
"LIMIT 10",
]
)
cursor = await db.execute(query, params)
rows = await cursor.fetchall()
return [
Card(
uuid=row["uuid"],
name=row["name"],
set_code=row["setCode"],
)
for row in rows
]
class SearchHandler(tornado.web.RequestHandler):
async def get(self) -> None:
async with aiosqlite.connect("/media/correlr/Correl/AllPrintings.sqlite") as db:
cards = await search(db, name=self.get_argument("name", None))
self.write(
json.dumps(
[
{
"uuid": card.uuid,
"name": card.name,
"set_code": card.set_code,
}
for card in cards
]
)
)
@click.group()
@click.option("--database", type=click.Path(dir_okay=False), required=True)
@click.pass_context
def cli(ctx, database):
ctx.ensure_object(dict)
ctx.obj['database'] = database
@cli.command()
@click.pass_context
def server(ctx):
app = tornado.web.Application(
[
(r"/", SearchHandler),
],
debug=True,
)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
@cli.command("import")
@click.argument("filename", type=click.Path(dir_okay=False))
@click.pass_context
def import_cards(ctx, filename):
cards = tornado.ioloop.IOLoop.current().run_sync(
lambda: tutor.csvimport.load(ctx.obj, filename)
)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
cli()

0
tutor/app.py Normal file
View file

56
tutor/csvimport.py Normal file
View file

@ -0,0 +1,56 @@
import csv
import logging
import typing
import aiosqlite
import tutor.database
import tutor.models
async def load(settings: dict, filename: str) -> typing.List[tutor.models.Card]:
"""Load cards from a CSV file.
Currently supports the following formats:
- Deckbox (set name match can fail)
- MTGStand (uses Scryfall ID)
"""
cards = []
async with aiosqlite.connect(settings["database"]) as db:
with open(filename) as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
is_foil = "Foil" in row and row["Foil"].lower() == "foil"
quantity = int(row.get("Quantity", row.get("Count", 1)))
if "Scryfall ID" in row:
found = await tutor.database.search(
db, scryfall_id=row["Scryfall ID"]
)
else:
found = await tutor.database.search(
db,
name=row["Name"],
set_name=row["Edition"],
collector_number=row["Card Number"],
foil=is_foil or None,
)
if not found:
logging.warning("Could not find card for row %s", row)
elif len(found) > 1:
logging.warning(
"Found %s possibilities for row %s", len(found), row
)
for card in found:
logging.warning(card)
else:
card = tutor.models.CardCopy(
card=found[0],
foil=is_foil,
language=row["Language"] or "English",
)
logging.info((quantity, card))
for i in range(quantity):
cards.append(card)
await tutor.database.store_copy(db, card)
await db.commit()
return cards

78
tutor/database.py Normal file
View file

@ -0,0 +1,78 @@
import typing
import uuid
import aiosqlite
import tutor.models
async def search(
db: aiosqlite.Connection,
name: typing.Optional[str] = None,
collector_number: typing.Optional[str] = None,
set_code: typing.Optional[str] = None,
set_name: typing.Optional[str] = None,
foil: typing.Optional[bool] = None,
alternate_art: typing.Optional[bool] = None,
scryfall_id: typing.Optional[str] = None,
limit: int = 10,
distinct: bool = True,
) -> typing.List[tutor.models.Card]:
db.row_factory = aiosqlite.Row
constraints = []
params = {}
if name is not None:
constraints.append("cards.name LIKE :name")
params["name"] = name
if collector_number is not None:
constraints.append("cards.number LIKE :number")
params["number"] = collector_number
if set_code is not None:
constraints.append("cards.setCode LIKE :set_code")
params["set_code"] = set_code
if set_name is not None:
constraints.append("sets.name LIKE :set_name")
params["set_name"] = set_name
if foil is not None:
constraints.append("cards.hasFoil IS :foil")
params["foil"] = foil
if alternate_art is not None:
constraints.append("cards.isAlternative IS :alternative")
params["alternative"] = alternate_art
if scryfall_id is not None:
constraints.append("cards.scryfallId LIKE :scryfall_id")
params["scryfall_id"] = scryfall_id
if distinct:
constraints.append("(cards.side IS NULL OR cards.side IS 'a')")
query = " ".join(
[
"SELECT cards.* FROM cards",
"JOIN sets ON (cards.setCode = sets.code)",
"WHERE" if constraints else "",
" AND ".join(constraints),
f"LIMIT {limit}",
]
)
cursor = await db.execute(query, params)
rows = await cursor.fetchall()
return [
tutor.models.Card(
scryfall_id=uuid.UUID(row["scryfallId"]),
name=row["name"],
set_code=row["setCode"],
collector_number=row["number"],
)
for row in rows
]
async def store_copy(db: aiosqlite.Connection, copy: tutor.models.CardCopy) -> None:
await db.execute(
"INSERT INTO copies (scryfallId, isFoil, condition)"
"VALUES (:scryfall_id, :foil, :condition)",
{
"scryfall_id": str(copy.card.scryfall_id),
"foil": copy.foil,
"condition": copy.condition,
},
)

17
tutor/models.py Normal file
View file

@ -0,0 +1,17 @@
import dataclasses
import typing
import uuid
@dataclasses.dataclass
class Card:
scryfall_id: uuid.UUID
name: str
set_code: str
collector_number: str
@dataclasses.dataclass
class CardCopy:
card: Card
foil: bool
language: str = "English"
condition: typing.Optional[str] = None