From a317dd31a8d2814d9fc999849e16d12074d5b926 Mon Sep 17 00:00:00 2001 From: Correl Date: Wed, 14 Jun 2023 21:42:10 -0400 Subject: [PATCH] Add deck POST/PUT --- tutor/database.py | 16 +++++++-- tutor/models.py | 1 + tutor/server.py | 90 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/tutor/database.py b/tutor/database.py index 558f7a4..0cd0641 100644 --- a/tutor/database.py +++ b/tutor/database.py @@ -361,11 +361,23 @@ async def store_deck_card( """ INSERT INTO "deck_list" ("deck_id", "oracle_id", "quantity") VALUES (%(deck_id)s, %(oracle_id)s, %(quantity)s) + ON CONFLICT ("deck_id", "oracle_id") DO UPDATE + SET "quantity" = "deck_list"."quantity" + EXCLUDED."quantity" """, {"deck_id": deck_id, "oracle_id": str(oracle_id), "quantity": quantity}, ) +async def clear_deck(db: psycopg.AsyncCursor, deck_id: int) -> None: + await db.execute( + """ + DELETE FROM "deck_list" + WHERE "deck_id" = %(deck_id)s + """, + {"deck_id": deck_id}, + ) + + async def get_decks( db: psycopg.AsyncCursor, limit: int = 10, offset: int = 0 ) -> typing.List[tutor.models.Deck]: @@ -479,8 +491,8 @@ async def get_deck( 'quantity', "deck_list"."quantity" ))) AS "cards" FROM "decks" - LEFT JOIN "deck_list" USING ("deck_id") - LEFT JOIN "oracle_latest" USING ("oracle_id") + JOIN "deck_list" USING ("deck_id") + JOIN "oracle_latest" USING ("oracle_id") WHERE "decks"."deck_id" = %(deck_id)s GROUP BY "decks"."deck_id" , "decks"."name" diff --git a/tutor/models.py b/tutor/models.py index 14d646d..cd65c96 100644 --- a/tutor/models.py +++ b/tutor/models.py @@ -119,6 +119,7 @@ class CardConstraint: language: typing.Optional[str] = None foil: typing.Optional[str] = None + @dataclasses.dataclass class DeckCard: card: Card diff --git a/tutor/server.py b/tutor/server.py index 226f900..8a25773 100644 --- a/tutor/server.py +++ b/tutor/server.py @@ -1,7 +1,11 @@ +import asyncio import decimal import importlib.metadata import importlib.resources +import io +import itertools import json +import logging import re import typing import urllib.parse @@ -13,12 +17,16 @@ import tornado.ioloop import tornado.web import tornado_openapi3.handler import yaml +from tornado.ioloop import IOLoop import tutor.database import tutor.models import tutor.search +logger = logging.getLogger(__name__) + + def update_args(url: str, **qargs) -> str: parts = urllib.parse.urlsplit(url) return urllib.parse.urlunsplit( @@ -212,7 +220,33 @@ class CollectionHandler(RequestHandler): ) +class DeckImporter: + line_pattern = re.compile(r"^(?P\d+)x? (?P[^\]$]+)") + + def __init__(self) -> None: + self.imported: int = 0 + self.last_chunk: bytes = b"" + + def process( + self, chunk: bytes = b"" + ) -> typing.Iterator[tutor.models.CardConstraint]: + lines = (self.last_chunk + chunk).splitlines() + self.last_chunk = lines.pop() + for line in lines: + if match := self.line_pattern.match(line.decode("utf8").strip()): + for card in itertools.repeat( + tutor.models.CardConstraint(name=match.group("name")), + int(match.group("quantity")), + ): + self.imported += 1 + yield card + + class DecksHandler(RequestHandler): + def initialize(self) -> None: + self.importer = DeckImporter() + self.deck_id = None + async def get(self) -> None: page = max(1, int(self.get_argument("page", "1"))) limit = int(self.get_argument("limit", "10")) @@ -233,22 +267,68 @@ class DecksHandler(RequestHandler): self.write(json.dumps(decks, cls=JSONEncoder)) async def post(self) -> None: - ... + self.set_header("Content-Type", "application/json") + self.set_header("Access-Control-Allow-Origin", "*") + self.write(json.dumps({})) +@tornado.web.stream_request_body class DeckHandler(RequestHandler): + async def prepare(self) -> None: + self.connection = await self.pool.getconn() + self.deck_id = self.path_args[0] + content_type = self.request.headers.get("Content-Type", "text/plain") + if content_type == "text/plain": + self.importer = DeckImporter() + if self.request.method == "PUT": + async with self.connection.cursor() as cursor: + await tutor.database.clear_deck(cursor, self.deck_id) + + async def data_received(self, chunk) -> None: + for card in self.importer.process(chunk): + await self.add_card(card) + + async def add_card(self, card: tutor.models.CardConstraint) -> None: + async with self.connection.cursor() as cursor: + oracle_id = await tutor.database.oracle_id_by_name(cursor, card.name) + if oracle_id: + logger.debug("Adding card %s to deck %s", card.name, self.deck_id) + await tutor.database.store_deck_card(cursor, self.deck_id, oracle_id, 1) + + def on_finish(self) -> None: + IOLoop.current().add_callback(self.close_connection) + + async def close_connection(self) -> None: + await self.pool.putconn(self.connection) + async def get(self, deck_id) -> None: self.set_header("Content-Type", "application/json") self.set_header("Access-Control-Allow-Origin", "*") - async with self.pool.connection() as conn: - async with conn.cursor() as cursor: - deck = await tutor.database.get_deck(cursor, deck_id) + async with self.connection.cursor() as cursor: + deck = await tutor.database.get_deck(cursor, deck_id) if not deck: raise tornado.web.HTTPError(404) self.write(json.dumps(deck, cls=JSONEncoder)) + async def post(self, deck_id) -> None: + for card in self.importer.process(): + await self.add_card(card) + async with self.connection.cursor() as cursor: + deck = await tutor.database.get_deck(cursor, deck_id) + await self.connection.commit() + self.set_header("Content-Type", "application/json") + self.set_header("Access-Control-Allow-Origin", "*") + self.write(json.dumps(deck, cls=JSONEncoder)) + async def put(self, deck_id) -> None: - ... + for card in self.importer.process(): + await self.add_card(card) + async with self.connection.cursor() as cursor: + deck = await tutor.database.get_deck(cursor, deck_id) + await self.connection.commit() + self.set_header("Content-Type", "application/json") + self.set_header("Access-Control-Allow-Origin", "*") + self.write(json.dumps(deck, cls=JSONEncoder)) class TemplateHandler(RequestHandler):