diff --git a/backend/server/app/app.py b/backend/server/app/app.py index 4f5f75f8..26c86df5 100644 --- a/backend/server/app/app.py +++ b/backend/server/app/app.py @@ -21,6 +21,42 @@ from backend.common.utils.utils import Float32JSONEncoder webbp = Blueprint("webapp", "backend.server.common.web", template_folder="templates") +ONE_WEEK = 7 * 24 * 60 * 60 + + +def _cache_control(always, **cache_kwargs): + """ + Used to easily manage cache control headers on responses. + See Werkzeug for attributes that can be set, eg, no_cache, private, max_age, etc. + https://werkzeug.palletsprojects.com/en/1.0.x/datastructures/#werkzeug.datastructures.ResponseCacheControl + """ + + def inner_cache_control(f): + @wraps(f) + def wrapper(*args, **kwargs): + response = make_response(f(*args, **kwargs)) + if not always and not current_app.app_config.server_config.app__generate_cache_control_headers: + return response + if response.status_code >= 400: + return response + for k, v in cache_kwargs.items(): + setattr(response.cache_control, k, v) + return response + + return wrapper + + return inner_cache_control + + +def cache_control(**cache_kwargs): + """ config driven """ + return _cache_control(False, **cache_kwargs) + + +def cache_control_always(**cache_kwargs): + """ always generate headers, regardless of the config """ + return _cache_control(True, **cache_kwargs) + @webbp.route("/", methods=["GET"]) def dataset_index(): @@ -71,6 +107,7 @@ def rest_get_data_adaptor(func): class HealthAPI(Resource): + @cache_control_always(no_store=True) def get(self): config = current_app.app_config return health_check(config) @@ -78,78 +115,92 @@ class HealthAPI(Resource): class SchemaAPI(Resource): # TODO @mdunitz separate dataset schema and user schema + @cache_control(public=True, max_age=ONE_WEEK) @rest_get_data_adaptor def get(self, data_adaptor): return common_rest.schema_get(data_adaptor) class ConfigAPI(Resource): + @cache_control(public=True, max_age=ONE_WEEK) @rest_get_data_adaptor def get(self, data_adaptor): return common_rest.config_get(current_app.app_config, data_adaptor) class UserInfoAPI(Resource): + @cache_control_always(no_store=True) @rest_get_data_adaptor def get(self, data_adaptor): return common_rest.userinfo_get(current_app.app_config, data_adaptor) class AnnotationsObsAPI(Resource): + @cache_control(public=True, max_age=ONE_WEEK) @rest_get_data_adaptor def get(self, data_adaptor): return common_rest.annotations_obs_get(request, data_adaptor) @requires_authentication + @cache_control(no_store=True) @rest_get_data_adaptor def put(self, data_adaptor): return common_rest.annotations_obs_put(request, data_adaptor) class AnnotationsVarAPI(Resource): + @cache_control(public=True, max_age=ONE_WEEK) @rest_get_data_adaptor def get(self, data_adaptor): return common_rest.annotations_var_get(request, data_adaptor) class DataVarAPI(Resource): + @cache_control(no_store=True) @rest_get_data_adaptor def put(self, data_adaptor): return common_rest.data_var_put(request, data_adaptor) + @cache_control(public=True, max_age=ONE_WEEK) @rest_get_data_adaptor def get(self, data_adaptor): return common_rest.data_var_get(request, data_adaptor) class ColorsAPI(Resource): + @cache_control(public=True, max_age=ONE_WEEK) @rest_get_data_adaptor def get(self, data_adaptor): return common_rest.colors_get(data_adaptor) class DiffExpObsAPI(Resource): + @cache_control(no_store=True) @rest_get_data_adaptor def post(self, data_adaptor): return common_rest.diffexp_obs_post(request, data_adaptor) class LayoutObsAPI(Resource): + @cache_control(public=True, max_age=ONE_WEEK) @rest_get_data_adaptor def get(self, data_adaptor): return common_rest.layout_obs_get(request, data_adaptor) + @cache_control(no_store=True) @rest_get_data_adaptor def put(self, data_adaptor): return common_rest.layout_obs_put(request, data_adaptor) class GenesetsAPI(Resource): + @cache_control(public=True, max_age=ONE_WEEK) @rest_get_data_adaptor def get(self, data_adaptor): return common_rest.genesets_get(request, data_adaptor) @requires_authentication + @cache_control(no_store=True) @rest_get_data_adaptor def put(self, data_adaptor): return common_rest.genesets_put(request, data_adaptor) @@ -157,6 +208,7 @@ class GenesetsAPI(Resource): class GenesetSummaryAPI(Resource): @rest_get_data_adaptor + @cache_control(no_store=True) def get(self, data_adaptor): return common_rest.geneset_summary_get(request, data_adaptor) diff --git a/backend/server/common/config/server_config.py b/backend/server/common/config/server_config.py index d2a593e4..e7b2d2d8 100644 --- a/backend/server/common/config/server_config.py +++ b/backend/server/common/config/server_config.py @@ -27,6 +27,7 @@ class ServerConfig(BaseConfig): self.app__open_browser = default_config["app"]["open_browser"] self.app__force_https = default_config["app"]["force_https"] self.app__flask_secret_key = default_config["app"]["flask_secret_key"] + self.app__generate_cache_control_headers = default_config["app"]["generate_cache_control_headers"] self.authentication__type = default_config["authentication"]["type"] self.authentication__insecure_test_environment = default_config["authentication"][ @@ -73,6 +74,7 @@ class ServerConfig(BaseConfig): self.validate_correct_type_of_configuration_attribute("app__open_browser", bool) self.validate_correct_type_of_configuration_attribute("app__force_https", bool) self.validate_correct_type_of_configuration_attribute("app__flask_secret_key", str) + self.validate_correct_type_of_configuration_attribute("app__generate_cache_control_headers", bool) if self.app__port: try: diff --git a/backend/server/default_config.py b/backend/server/default_config.py index d77cbdc4..aa3c4475 100644 --- a/backend/server/default_config.py +++ b/backend/server/default_config.py @@ -10,6 +10,7 @@ server: open_browser: false force_https: false flask_secret_key: null + generate_cache_control_headers: false authentication: # The authentication types may be "none" or "session" diff --git a/backend/test/fixtures/server_config_outline.py b/backend/test/fixtures/server_config_outline.py index ccf75015..cb4fa085 100644 --- a/backend/test/fixtures/server_config_outline.py +++ b/backend/test/fixtures/server_config_outline.py @@ -7,6 +7,7 @@ f"""server: open_browser: {open_browser} force_https: {force_https} flask_secret_key: {flask_secret_key} + generate_cache_control_headers: {generate_cache_control_headers} authentication: type: {auth_type} insecure_test_environment: {insecure_test_environment} diff --git a/backend/test/test_server/unit/common/config/__init__.py b/backend/test/test_server/unit/common/config/__init__.py index 5ce5f04f..677c3e35 100644 --- a/backend/test/test_server/unit/common/config/__init__.py +++ b/backend/test/test_server/unit/common/config/__init__.py @@ -32,6 +32,7 @@ class ConfigTests(unittest.TestCase): open_browser="false", force_https="false", flask_secret_key="secret", + generate_cache_control_headers="false", auth_type="session", insecure_test_environment="false", index="false", @@ -67,6 +68,7 @@ class ConfigTests(unittest.TestCase): open_browser="false", force_https="false", flask_secret_key="secret", + generate_cache_control_headers="false", auth_type="session", index="false", allowed_matrix_types=[], @@ -116,6 +118,7 @@ class ConfigTests(unittest.TestCase): open_browser=open_browser, force_https=force_https, flask_secret_key=flask_secret_key, + generate_cache_control_headers=generate_cache_control_headers, auth_type=auth_type, index=index, allowed_matrix_types=allowed_matrix_types, diff --git a/backend/test/test_server/unit/common/config/test_server_config.py b/backend/test/test_server/unit/common/config/test_server_config.py index 1a1527f7..647e29fe 100644 --- a/backend/test/test_server/unit/common/config/test_server_config.py +++ b/backend/test/test_server/unit/common/config/test_server_config.py @@ -49,7 +49,7 @@ class TestServerConfig(ConfigTests): def test_complete_config_checks_all_attr(self, mock_check_attrs): mock_check_attrs.side_effect = BaseConfig.validate_correct_type_of_configuration_attribute() self.server_config.complete_config(self.context) - self.assertEqual(mock_check_attrs.call_count, 20) + self.assertEqual(mock_check_attrs.call_count, 21) def test_handle_app__throws_error_if_port_doesnt_exist(self): config = self.get_config(port=99999999)