pyapi.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  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 trend_thread(conn, path, forms):
  34. def cb(queue):
  35. return Thread(target=worker.trend, args=(
  36. queue, conn, path, forms
  37. )).start()
  38. return cb
  39. PAGE_CACHE = PageCache(100)
  40. QUERY_CACHE = QueryCache(None)
  41. @route('/grocery/trend', method=['GET', 'POST'])
  42. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  43. def trend(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([], trend_thread(conn, path, form)))
  49. for i in iter_page(page):
  50. yield i
  51. def query_to_form(query):
  52. form = FormsDict()
  53. for k, v in parse_qs(query).items():
  54. for item in v:
  55. form.append(k, item)
  56. return form
  57. def key_to_form(key):
  58. query, _ = key
  59. return query_to_form(query)
  60. def iter_page(page):
  61. # copy first to avoid races
  62. resp = list(page.value)
  63. pos = len(resp)
  64. yield ''.join(resp)
  65. while not page.loaded:
  66. page.update()
  67. # all changes since last yield
  68. resp = list(page.value[pos:])
  69. pos = pos + len(resp)
  70. yield ''.join(resp)
  71. # possibly have not yielded the entire page
  72. if pos < len(page.value):
  73. yield ''.join(page.value[pos:])
  74. @route('/grocery/groups', method=['GET', 'POST'])
  75. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  76. @cursor(connection=conn)
  77. def groups(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache):
  78. form = key_to_form(key)
  79. response.content_type = 'application/xhtml+xml; charset=utf-8'
  80. return get_groups(cur, form)
  81. @route('/grocery/categories', method=['GET', 'POST'])
  82. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  83. @cursor(connection=conn)
  84. def categories(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache):
  85. form = key_to_form(key)
  86. response.content_type = 'application/xhtml+xml; charset=utf-8'
  87. return get_categories(cur, form)
  88. @route('/grocery/products', method=['GET', 'POST'])
  89. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  90. @cursor(connection=conn)
  91. def products(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache):
  92. form = key_to_form(key)
  93. response.content_type = 'application/xhtml+xml; charset=utf-8'
  94. return get_products(cur, form)
  95. @route('/grocery/tags', method=['GET', 'POST'])
  96. @cache(query_cache=QUERY_CACHE, page_cache=PAGE_CACHE)
  97. @cursor(connection=conn)
  98. def tags(cur: Cursor[TupleRow], key: Tuple[str, int], cache: PageCache):
  99. form = key_to_form(key)
  100. response.content_type = 'application/xhtml+xml; charset=utf-8'
  101. return get_tags(cur, form)