From e3a004df204c1fb89c2aa6530c20053f523661b6 Mon Sep 17 00:00:00 2001 From: Correl Date: Thu, 15 Jul 2021 20:51:33 -0400 Subject: [PATCH] Paginate search results --- pyproject.toml | 2 +- tutor/__main__.py | 117 --------------------- tutor/database.py | 3 +- tutor/server.py | 45 +++++++- www/elm.json | 2 + www/public/index.html | 1 + www/src/App.elm | 237 ++++++++++++++++++++++++++++++++++++------ 7 files changed, 252 insertions(+), 155 deletions(-) delete mode 100644 tutor/__main__.py diff --git a/pyproject.toml b/pyproject.toml index 5164b0e..372f5ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ black = "^21.6b0" mypy = "^0.910" [tool.poetry.scripts] -tutor = 'tutor.__main__:cli' +tutor = 'tutor.cli:main' [build-system] requires = ["poetry>=0.12"] diff --git a/tutor/__main__.py b/tutor/__main__.py deleted file mode 100644 index 9658051..0000000 --- a/tutor/__main__.py +++ /dev/null @@ -1,117 +0,0 @@ -import json -import logging - -import aiosqlite -import click -import httpx -import humanize -import tornado.ioloop -import tornado.web - -import tutor.csvimport -import tutor.database -import tutor.scryfall -import tutor.server - - -@click.group() -@click.option( - "--database", - envvar="TUTOR_DATABASE", - type=click.Path(dir_okay=False), - required=True, -) -@click.option( - "--log-level", - type=click.Choice( - ["debug", "info", "warn", "error"], - case_sensitive=False, - ), - default="warn", -) -@click.pass_context -def cli(ctx, database, log_level): - logging.basicConfig( - level={ - "debug": logging.DEBUG, - "info": logging.INFO, - "warn": logging.WARN, - "error": logging.ERROR, - }.get(log_level.lower()) - ) - ctx.ensure_object(dict) - ctx.obj["database"] = database - - -@cli.command() -@click.option("--port", type=int, envvar="TUTOR_PORT", default=8888) -@click.option("--static", envvar="TUTOR_STATIC", type=click.Path(file_okay=False)) -@click.option("--debug", is_flag=True) -@click.pass_context -def server(ctx, port, static, debug): - app = tutor.server.make_app( - { - **ctx.obj, - "static": static, - "debug": debug, - } - ) - app.listen(port) - 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): - tornado.ioloop.IOLoop.current().run_sync( - lambda: tutor.csvimport.load(ctx.obj, filename) - ) - - -@cli.command("update_scryfall") -@click.option("--filename", type=click.Path(dir_okay=False)) -@click.pass_context -def update_scryfall(ctx, filename): - if filename: - with open(filename) as f: - cards = json.loads(f.read()) - else: - response = httpx.get("https://api.scryfall.com/bulk-data/oracle_cards") - info = response.json() - - buffer = b"" - with httpx.stream("GET", info["download_uri"]) as response: - downloaded = response.num_bytes_downloaded - total = int(response.headers["Content-Length"]) - with click.progressbar( - length=total, - label=f"Downloading {humanize.naturalsize(total)}" - " of card data from Scryfall", - ) as bar: - for chunk in response.iter_bytes(): - buffer += chunk - bar.update(response.num_bytes_downloaded - downloaded) - downloaded = response.num_bytes_downloaded - cards = json.loads(buffer) - - async def import_cards(): - async with aiosqlite.connect(ctx.obj["database"]) as db: - with click.progressbar( - cards, - label=f"Importing {humanize.intcomma(len(cards))} cards", - ) as bar: - for card_object in bar: - await tutor.database.store_card( - db, tutor.scryfall.to_card(card_object) - ) - await tutor.database.store_set( - db, card_object["set"].upper(), card_object["set_name"] - ) - await db.commit() - - tornado.ioloop.IOLoop.current().run_sync(import_cards) - - -if __name__ == "__main__": - cli() diff --git a/tutor/database.py b/tutor/database.py index f2199d2..14b5423 100644 --- a/tutor/database.py +++ b/tutor/database.py @@ -93,6 +93,7 @@ async def advanced_search( db: aiosqlite.Connection, search: tutor.search.Search, limit: int = 10, + offset: int = 0, in_collection: typing.Optional[bool] = None, ) -> typing.List[tutor.models.Card]: db.row_factory = aiosqlite.Row @@ -146,7 +147,7 @@ async def advanced_search( " ".join(joins), "WHERE" if constraints else "", " AND ".join(constraints), - f"LIMIT {limit}", + f"LIMIT {offset},{limit}", ] ) cursor = await db.execute(query, params) diff --git a/tutor/server.py b/tutor/server.py index 197fbe9..b195386 100644 --- a/tutor/server.py +++ b/tutor/server.py @@ -1,4 +1,5 @@ import json +import urllib.parse import aiosqlite import tornado.web @@ -7,21 +8,57 @@ import tutor.database import tutor.models import tutor.search + +def update_args(url: str, **qargs) -> str: + parts = urllib.parse.urlsplit(url) + return urllib.parse.urlunsplit( + ( + parts.scheme, + parts.netloc, + parts.path, + urllib.parse.urlencode( + [ + (k, v) + for k, v in urllib.parse.parse_qsl(parts.query) + if k not in qargs.keys() + ] + + list(qargs.items()) + ), + parts.fragment, + ) + ) + + class SearchHandler(tornado.web.RequestHandler): + def set_links(self, **links) -> None: + self.set_header( + "Link", + ", ".join([f'<{url}>; rel="{rel}"' for rel, url in links.items()]), + ) + async def get(self) -> None: async with aiosqlite.connect(self.application.settings["database"]) as db: - name = self.get_argument("name", None) + query = self.get_argument("q", "") in_collection = self.get_argument("in_collection", None) + page = max(1, int(self.get_argument("page", 1))) limit = int(self.get_argument("limit", 10)) - search = tutor.search.search.parse(name) + search = tutor.search.search.parse(query) cards = await tutor.database.advanced_search( db, search, - limit=limit, + limit=limit + 1, + offset=limit * (page - 1), in_collection=in_collection, ) + has_more = cards and len(cards) > limit self.set_header("Content-Type", "application/json") self.set_header("Access-Control-Allow-Origin", "*") + links = {} + if page > 1: + links["prev"] = update_args(self.request.full_url(), page=page - 1) + if has_more: + links["next"] = update_args(self.request.full_url(), page=page + 1) + self.set_links(**links) self.write( json.dumps( [ @@ -35,7 +72,7 @@ class SearchHandler(tornado.web.RequestHandler): card.color_identity ), } - for card in cards + for card in cards[:limit] ] ) ) diff --git a/www/elm.json b/www/elm.json index c6885f9..210a7c5 100644 --- a/www/elm.json +++ b/www/elm.json @@ -12,7 +12,9 @@ "elm/html": "1.0.0", "elm/http": "2.0.0", "elm/json": "1.1.3", + "elm/regex": "1.0.0", "elm/url": "1.0.0", + "elm-community/maybe-extra": "5.2.0", "mdgriffith/elm-ui": "1.1.8" }, "indirect": { diff --git a/www/public/index.html b/www/public/index.html index 064d4ca..645d6cd 100644 --- a/www/public/index.html +++ b/www/public/index.html @@ -3,6 +3,7 @@ Bulk Tagging Dashboard +