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. from psycopg import Connection
  10. from psycopg.connection import TupleRow
  11. from .QueryCache import QueryCache, get_hash
  12. from ..data.filter import get_filter, get_query_param
  13. from . import BOOLEAN, PARAMS
  14. from .PageCache import PageCache
  15. from .hash_util import base32_to_hash, hash_to_base32, normalize_base32
  16. def normalize_query(query: FormsDict, allow: Iterable[str] = None) -> Tuple[str, str]:
  17. if query.hash:
  18. _hash = normalize_base32(query.hash)
  19. else:
  20. _hash = None
  21. allow = allow or PARAMS
  22. param = get_filter(query, allow=allow)
  23. norm = urlencode([
  24. (
  25. k, get_query_param(*param[k])
  26. ) if k != 'organic' else (
  27. "organic", BOOLEAN[BOOLEAN.get(query.organic, None)]
  28. ) for k in sorted(param) if param[k]
  29. ])
  30. return norm if not _hash or len(query.keys()) > 1 else None, _hash
  31. def _cache_decorator(func: Callable, query_cache: QueryCache = None, page_cache: PageCache = None):
  32. def wrap(*args, **kwargs):
  33. _, _, path, *_ = request.urlparts
  34. endpoint = path.split('/', 2)[-1]
  35. query, _hash = normalize_query(request.params)
  36. query = f"{endpoint}?{query}"
  37. if not _hash:
  38. _hashInt = get_hash(query)
  39. _hash = hash_to_base32(_hashInt)
  40. key = (query, _hashInt)
  41. else:
  42. key = (query, base32_to_hash(_hash))
  43. if request.params.reload == "true":
  44. page_cache.remove(key)
  45. # key with tuple to avoid ambiguity
  46. cached = query_cache.get(key)
  47. if not cached:
  48. if query:
  49. cached = query_cache.add(key, None)
  50. else:
  51. return HTTPError(404, f"No query found for hash: {_hash}")
  52. if not request.params.hash:
  53. if cached and len(cached) > 2000:
  54. return redirect(f"{path}?hash={_hash}")
  55. if cached and f"{endpoint}?{request.query_string}" != cached:
  56. return redirect(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")