pyapi.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. #
  2. # Copyright (c) Daniel Sheffield 2023
  3. # All rights reserved
  4. #
  5. # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
  6. import os
  7. from threading import Thread
  8. from typing import Tuple
  9. from bottle import (
  10. route, request, response,
  11. static_file,
  12. FormsDict,
  13. )
  14. from psycopg import Cursor, connect
  15. from psycopg.rows import TupleRow
  16. from urllib.parse import parse_qs
  17. from .QueryCache import QueryCache
  18. from .route_decorators import cache, cursor
  19. from .query_to_xml import get_categories, get_groups, get_products, get_tags
  20. from .CachedLoadingPage import CachedLoadingPage
  21. from .PageCache import PageCache
  22. from . import trend as worker
  23. host = f"host={os.getenv('HOST')}"
  24. db = f"dbname={os.getenv('DB', 'grocery')}"
  25. user = f"user={os.getenv('USER', 'das')}"
  26. password = f"password={os.getenv('PASSWORD','')}"
  27. if not password.split('=',1)[1]:
  28. password = ''
  29. conn = connect(f"{host} {db} {user} {password}")
  30. @route('/grocery/static/<filename:path>')
  31. def send_static(filename):
  32. return static_file(filename, root='app/rest/static')
  33. def new_thread(target, conn, path, forms):
  34. def cb(queue):
  35. return Thread(target=target, args=(
  36. queue, conn, path, forms
  37. )).start()
  38. return cb
  39. PAGE_CACHE = PageCache(100)
  40. QUERY_CACHE = QueryCache(None)
  41. @route('/grocery/volume', method=['GET', 'POST'])
  42. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  43. def volume(key: Tuple[str, int], cache: PageCache):
  44. _, _, path, *_ = request.urlparts
  45. page = cache[key]
  46. if page is None:
  47. form = key_to_form(key)
  48. page = cache.add(key, CachedLoadingPage([], new_thread(worker.volume, conn, path, form)))
  49. for i in iter_page(page):
  50. yield i
  51. @route('/grocery/trend', method=['GET', 'POST'])
  52. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  53. def trend(key: Tuple[str, int], cache: PageCache):
  54. _, _, path, *_ = request.urlparts
  55. page = cache[key]
  56. if page is None:
  57. form = key_to_form(key)
  58. page = cache.add(key, CachedLoadingPage([], new_thread(worker.trend, conn, path, form)))
  59. for i in iter_page(page):
  60. yield i
  61. def query_to_form(query):
  62. form = FormsDict()
  63. for k, v in parse_qs(query).items():
  64. for item in v:
  65. form.append(k, item)
  66. return form
  67. def key_to_form(key):
  68. query, _ = key
  69. return query_to_form(query)
  70. def iter_page(page):
  71. # copy first to avoid races
  72. resp = list(page.value)
  73. pos = len(resp)
  74. yield ''.join(resp)
  75. while not page.loaded:
  76. page.update()
  77. # all changes since last yield
  78. resp = list(page.value[pos:])
  79. pos = pos + len(resp)
  80. yield ''.join(resp)
  81. # possibly have not yielded the entire page
  82. if pos < len(page.value):
  83. yield ''.join(page.value[pos:])
  84. @route('/grocery/groups', method=['GET', 'POST'])
  85. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  86. @cursor(connection=conn)
  87. def groups(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache):
  88. form = key_to_form(key)
  89. response.content_type = 'application/xhtml+xml; charset=utf-8'
  90. return get_groups(cur, form)
  91. @route('/grocery/categories', method=['GET', 'POST'])
  92. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  93. @cursor(connection=conn)
  94. def categories(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache):
  95. form = key_to_form(key)
  96. response.content_type = 'application/xhtml+xml; charset=utf-8'
  97. return get_categories(cur, form)
  98. @route('/grocery/products', method=['GET', 'POST'])
  99. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  100. @cursor(connection=conn)
  101. def products(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache):
  102. form = key_to_form(key)
  103. response.content_type = 'application/xhtml+xml; charset=utf-8'
  104. return get_products(cur, form)
  105. @route('/grocery/tags', method=['GET', 'POST'])
  106. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  107. @cursor(connection=conn)
  108. def tags(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache):
  109. form = key_to_form(key)
  110. response.content_type = 'application/xhtml+xml; charset=utf-8'
  111. return get_tags(cur, form)