# # Copyright (c) Daniel Sheffield 2023 # All rights reserved # # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY import os from threading import Thread from typing import Tuple from bottle import ( route, request, response, static_file, FormsDict, ) from psycopg import Cursor, connect from psycopg.rows import TupleRow from urllib.parse import parse_qs from .QueryCache import QueryCache from .route_decorators import cache, cursor from .query_to_xml import get_categories, get_groups, get_products, get_tags from .CachedLoadingPage import CachedLoadingPage from .PageCache import PageCache from . import trend as worker host = f"host={os.getenv('HOST')}" db = f"dbname={os.getenv('DB', 'grocery')}" user = f"user={os.getenv('USER', 'das')}" password = f"password={os.getenv('PASSWORD','')}" if not password.split('=',1)[1]: password = '' conn = connect(f"{host} {db} {user} {password}") @route('/grocery/static/') def send_static(filename): return static_file(filename, root='app/rest/static') def trend_thread(conn, path, forms): def cb(queue): return Thread(target=worker.trend, args=( queue, conn, path, forms )).start() return cb PAGE_CACHE = PageCache(100) QUERY_CACHE = QueryCache(None) @route('/grocery/trend', method=['GET', 'POST']) @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE) def trend(key: Tuple[str, int], cache: PageCache): _, _, path, *_ = request.urlparts page = cache[key] if page is None: form = key_to_form(key) page = cache.add(key, CachedLoadingPage([], trend_thread(conn, path, form))) for i in iter_page(page): yield i def query_to_form(query): form = FormsDict() for k, v in parse_qs(query).items(): for item in v: form.append(k, item) return form def key_to_form(key): query, _ = key return query_to_form(query) def iter_page(page): # copy first to avoid races resp = list(page.value) pos = len(resp) yield ''.join(resp) while not page.loaded: page.update() # all changes since last yield resp = list(page.value[pos:]) pos = pos + len(resp) yield ''.join(resp) # possibly have not yielded the entire page if pos < len(page.value): yield ''.join(page.value[pos:]) @route('/grocery/groups', method=['GET', 'POST']) @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE) @cursor(connection=conn) def groups(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache): form = key_to_form(key) response.content_type = 'application/xhtml+xml; charset=utf-8' return get_groups(cur, form) @route('/grocery/categories', method=['GET', 'POST']) @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE) @cursor(connection=conn) def categories(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache): form = key_to_form(key) response.content_type = 'application/xhtml+xml; charset=utf-8' return get_categories(cur, form) @route('/grocery/products', method=['GET', 'POST']) @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE) @cursor(connection=conn) def products(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache): form = key_to_form(key) response.content_type = 'application/xhtml+xml; charset=utf-8' return get_products(cur, form) @route('/grocery/tags', method=['GET', 'POST']) @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE) @cursor(connection=conn) def tags(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache): form = key_to_form(key) response.content_type = 'application/xhtml+xml; charset=utf-8' return get_tags(cur, form)