route_decorators.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. #
  2. # Copyright (c) Daniel Sheffield 2023
  3. # All rights reserved
  4. #
  5. # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
  6. from typing import Callable, Iterable, Tuple
  7. from urllib.parse import urlencode
  8. from bottle import request, FormsDict, redirect, HTTPError
  9. import cherrypy
  10. from psycopg import Connection
  11. from psycopg.connection import TupleRow
  12. from .QueryCache import QueryCache, get_hash
  13. from ..data.filter import get_filter, get_query_param
  14. from . import BOOLEAN, PARAMS
  15. from .PageCache import PageCache
  16. from .hash_util import base32_to_hash, hash_to_base32, normalize_base32
  17. def normalize_query(query: FormsDict, allow: Iterable[str] = None) -> Tuple[str, str]:
  18. if query.hash:
  19. _hash = normalize_base32(query.hash)
  20. else:
  21. _hash = None
  22. allow = allow or PARAMS
  23. param = get_filter(query, allow=allow)
  24. norm = urlencode([
  25. (
  26. k, get_query_param(*param[k])
  27. ) if k != 'organic' else (
  28. "organic", BOOLEAN[BOOLEAN.get(query.organic, None)]
  29. ) for k in sorted(param) if param[k]
  30. ])
  31. return norm if not _hash or len(query.keys()) > 1 else None, _hash
  32. def _cache_decorator(func: Callable, query_cache: QueryCache = None, page_cache: PageCache = None):
  33. def wrap(*args, **kwargs):
  34. _, _, path, *_ = request.urlparts
  35. query, _hash = normalize_query(request.params)
  36. if not _hash:
  37. _hashInt = get_hash(query)
  38. _hash = hash_to_base32(_hashInt)
  39. key = (query, _hashInt)
  40. else:
  41. key = (query, base32_to_hash(_hash))
  42. if request.params.reload == "true":
  43. page_cache.remove(key)
  44. # key with tuple to avoid ambiguity
  45. cached = query_cache.get(key)
  46. if not cached:
  47. if query:
  48. cached = query_cache.add(key, None)
  49. else:
  50. return HTTPError(404, f"No query found for hash: {_hash}")
  51. cherrypy.log(f"cached query={cached}")
  52. if not request.params.hash:
  53. if cached and len(cached) > 2000:
  54. return redirect(f"{path}?hash={_hash}")
  55. if cached and request.query_string != cached:
  56. return redirect(f"{path}?{cached}")
  57. return func((cached, key[1]), page_cache, *args, **kwargs)
  58. return wrap
  59. def cache(*args, **kwargs):
  60. if not len(args):
  61. return lambda f: _cache_decorator(f, **kwargs)
  62. raise Exception("decorator argument required")
  63. def _cursor_decorator(func: Callable, connection: Connection[TupleRow] = None):
  64. def wrap(*args, **kwargs):
  65. try:
  66. with connection.cursor() as cur:
  67. return func(cur, *args, **kwargs)
  68. finally:
  69. connection.commit()
  70. return wrap
  71. def cursor(*args, **kwargs):
  72. if not len(args):
  73. return lambda f: _cursor_decorator(f, **kwargs)
  74. raise Exception("decorator argument required")