decorators.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. # Licensed to the Apache Software Foundation (ASF) under one
  2. # or more contributor license agreements. See the NOTICE file
  3. # distributed with this work for additional information
  4. # regarding copyright ownership. The ASF licenses this file
  5. # to you under the Apache License, Version 2.0 (the
  6. # "License"); you may not use this file except in compliance
  7. # with the License. You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing,
  12. # software distributed under the License is distributed on an
  13. # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. # KIND, either express or implied. See the License for the
  15. # specific language governing permissions and limitations
  16. # under the License.
  17. import logging
  18. from datetime import datetime, timedelta
  19. from functools import wraps
  20. from contextlib2 import contextmanager
  21. from flask import request
  22. from superset import app, cache
  23. from superset.utils.dates import now_as_float
  24. # If a user sets `max_age` to 0, for long the browser should cache the
  25. # resource? Flask-Caching will cache forever, but for the HTTP header we need
  26. # to specify a "far future" date.
  27. FAR_FUTURE = 365 * 24 * 60 * 60 # 1 year in seconds
  28. logger = logging.getLogger(__name__)
  29. @contextmanager
  30. def stats_timing(stats_key, stats_logger):
  31. """Provide a transactional scope around a series of operations."""
  32. start_ts = now_as_float()
  33. try:
  34. yield start_ts
  35. except Exception as e:
  36. raise e
  37. finally:
  38. stats_logger.timing(stats_key, now_as_float() - start_ts)
  39. def etag_cache(max_age, check_perms=bool):
  40. """
  41. A decorator for caching views and handling etag conditional requests.
  42. The decorator adds headers to GET requests that help with caching: Last-
  43. Modified, Expires and ETag. It also handles conditional requests, when the
  44. client send an If-Matches header.
  45. If a cache is set, the decorator will cache GET responses, bypassing the
  46. dataframe serialization. POST requests will still benefit from the
  47. dataframe cache for requests that produce the same SQL.
  48. """
  49. def decorator(f):
  50. @wraps(f)
  51. def wrapper(*args, **kwargs):
  52. # check if the user can access the resource
  53. check_perms(*args, **kwargs)
  54. # for POST requests we can't set cache headers, use the response
  55. # cache nor use conditional requests; this will still use the
  56. # dataframe cache in `superset/viz.py`, though.
  57. if request.method == "POST":
  58. return f(*args, **kwargs)
  59. response = None
  60. if cache:
  61. try:
  62. # build the cache key from the function arguments and any
  63. # other additional GET arguments (like `form_data`, eg).
  64. key_args = list(args)
  65. key_kwargs = kwargs.copy()
  66. key_kwargs.update(request.args)
  67. cache_key = wrapper.make_cache_key(f, *key_args, **key_kwargs)
  68. response = cache.get(cache_key)
  69. except Exception: # pylint: disable=broad-except
  70. if app.debug:
  71. raise
  72. logger.exception("Exception possibly due to cache backend.")
  73. # if no response was cached, compute it using the wrapped function
  74. if response is None:
  75. response = f(*args, **kwargs)
  76. # add headers for caching: Last Modified, Expires and ETag
  77. response.cache_control.public = True
  78. response.last_modified = datetime.utcnow()
  79. expiration = max_age if max_age != 0 else FAR_FUTURE
  80. response.expires = response.last_modified + timedelta(
  81. seconds=expiration
  82. )
  83. response.add_etag()
  84. # if we have a cache, store the response from the request
  85. if cache:
  86. try:
  87. cache.set(cache_key, response, timeout=max_age)
  88. except Exception: # pylint: disable=broad-except
  89. if app.debug:
  90. raise
  91. logger.exception("Exception possibly due to cache backend.")
  92. return response.make_conditional(request)
  93. if cache:
  94. wrapper.uncached = f
  95. wrapper.cache_timeout = max_age
  96. wrapper.make_cache_key = cache._memoize_make_cache_key( # pylint: disable=protected-access
  97. make_name=None, timeout=max_age
  98. )
  99. return wrapper
  100. return decorator