diff --git a/endpoint_cache/__init__.py b/endpoint_cache/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/endpoint_cache/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/endpoint_cache/__manifest__.py b/endpoint_cache/__manifest__.py new file mode 100644 index 00000000..46e332dd --- /dev/null +++ b/endpoint_cache/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Endpoint cache", + "summary": """Provide basic caching utils for endpoints""", + "version": "14.0.1.0.0", + "license": "LGPL-3", + "development_status": "Alpha", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["simahawk"], + "website": "https://github.com/OCA/web-api", + "depends": ["endpoint"], + "data": ["views/endpoint_view.xml"], +} diff --git a/endpoint_cache/models/__init__.py b/endpoint_cache/models/__init__.py new file mode 100644 index 00000000..95c62dfe --- /dev/null +++ b/endpoint_cache/models/__init__.py @@ -0,0 +1 @@ +from . import endpoint_mixin diff --git a/endpoint_cache/models/endpoint_mixin.py b/endpoint_cache/models/endpoint_mixin.py new file mode 100644 index 00000000..a6770efb --- /dev/null +++ b/endpoint_cache/models/endpoint_mixin.py @@ -0,0 +1,91 @@ +# Copyright 2024 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import _, api, exceptions, fields, models +from odoo.tools import date_utils + +from odoo.addons.http_routing.models.ir_http import slugify_one + + +class EndpointMixin(models.AbstractModel): + + _inherit = "endpoint.mixin" + + cache_policy = fields.Selection( + selection=[ + ("day", "Daily"), + ("week", "Weekly"), + ("month", "Monthly"), + ], + default="day", + ) + # cache_preheat = fields.Boolean() # TODO + + def _endpoint_cache_make_name(self, ext, suffix=None): + parts = [ + "endpoint_cache", + slugify_one(self.name).replace("-", "_"), + ] + if suffix: + parts.append(suffix) + if ext: + parts.append(ext) + return ".".join(parts) + + def _endpoint_cache_get(self, name): + att = ( + self.env["ir.attachment"] + .sudo() + .search(self._endpoint_cache_get_domain(name), limit=1) + ) + self._logger.debug("_endpoint_cache_get found att=%s", att.id) + return att.raw + + def _endpoint_cache_get_domain(self, cache_name): + now = fields.Datetime.now() + from_datetime = date_utils.start_of(now, self.cache_policy) + to_datetime = date_utils.end_of(now, self.cache_policy) + return [ + ("name", "=", cache_name), + ("res_model", "=", self._name), + ("res_id", "=", self.id), + ("create_date", ">=", from_datetime), + ("create_date", "<=", to_datetime), + ] + + def _endpoint_cache_store(self, name, raw_data, mimetype=None): + self._logger.debug("_endpoint_cache_store store att=%s", name) + if not name.startswith("endpoint_cache"): + raise exceptions.UserError(_("Cache name must start with 'endpoint_cache'")) + return ( + self.env["ir.attachment"] + .sudo() + .create( + { + "type": "binary", + "name": name, + "raw": raw_data, + "mimetype": mimetype, + "res_model": self._name, + "res_id": self.id, + } + ) + ) + + def _endpoint_cache_gc_domain(self, cache_name): + now = fields.Datetime.now() + gc_from = date_utils.subtract(now, days=30) + return [ + ("name", "like", "endpoint_cache%"), + ("res_model", "=", self._name), + ("res_id", "=", self.id), + ("create_date", "<=", gc_from), + ] + + @api.autovacuum + def _endpoint_cache_gc(self): + """Garbage collector for old caches""" + self.env["ir.attachment"].sudo().search( + self._endpoint_cache_gc_domain(self._name) + ).unlink() diff --git a/endpoint_cache/readme/CONTRIBUTORS.rst b/endpoint_cache/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..f1c71bce --- /dev/null +++ b/endpoint_cache/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Simone Orsi diff --git a/endpoint_cache/readme/DESCRIPTION.rst b/endpoint_cache/readme/DESCRIPTION.rst new file mode 100644 index 00000000..0487ad69 --- /dev/null +++ b/endpoint_cache/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Technical module provide basic caching configuration and utils for endpoints. diff --git a/endpoint_cache/readme/ROADMAP.rst b/endpoint_cache/readme/ROADMAP.rst new file mode 100644 index 00000000..3007460c --- /dev/null +++ b/endpoint_cache/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Add cache pre-heating diff --git a/endpoint_cache/readme/USAGE.rst b/endpoint_cache/readme/USAGE.rst new file mode 100644 index 00000000..ecda0445 --- /dev/null +++ b/endpoint_cache/readme/USAGE.rst @@ -0,0 +1,17 @@ +Example of usage in an endpoint:: + +.. code-block:: python + + # get the name of the cache + cache_name = endpoint._make_cache_name("json") + # check if the cache exists + cached = endpoint._endpoint_cache_get(cache_name) + if cached: + result = cached + else: + result = json.dumps(env["my.model"]._get_a_very_expensive_computed_result()) + # cache does not exist, create it0 + endpoint._endpoint_cache_store(cache_name, result) + + resp = Response(result, content_type="application/json", status=200) + result = dict(response=resp) diff --git a/endpoint_cache/tests/__init__.py b/endpoint_cache/tests/__init__.py new file mode 100644 index 00000000..c3f07a7d --- /dev/null +++ b/endpoint_cache/tests/__init__.py @@ -0,0 +1 @@ +from . import test_endpoint diff --git a/endpoint_cache/tests/test_endpoint.py b/endpoint_cache/tests/test_endpoint.py new file mode 100644 index 00000000..ac20eb1d --- /dev/null +++ b/endpoint_cache/tests/test_endpoint.py @@ -0,0 +1,80 @@ +# Copyright 2024 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from freezegun import freeze_time + +from odoo import exceptions + +from odoo.addons.endpoint.tests.common import CommonEndpoint + + +class TestEndpoint(CommonEndpoint): + @classmethod + def _setup_records(cls): + super()._setup_records() + cls.endpoint1 = cls.env.ref("endpoint.endpoint_demo_1") + cls.endpoint2 = cls.env.ref("endpoint.endpoint_demo_2") + + def test_cache_name(self): + self.assertEqual( + self.endpoint1._endpoint_cache_make_name("json"), + "endpoint_cache.demo_endpoint_1.json", + ) + self.assertEqual( + self.endpoint2._endpoint_cache_make_name("json"), + "endpoint_cache.demo_endpoint_2.json", + ) + + def test_cache_store_bad_name(self): + with self.assertRaisesRegex( + exceptions.UserError, "Cache name must start with 'endpoint_cache'" + ): + self.endpoint1._endpoint_cache_store("test", b"test") + + def test_cache_store_and_get(self): + self.endpoint1._endpoint_cache_store("endpoint_cache.test", b"test") + data = self.endpoint1._endpoint_cache_get("endpoint_cache.test") + self.assertEqual(data, b"test") + + def test_cache_gc(self): + dt1 = "2024-07-01 00:00:00" + with freeze_time(dt1): + cache1 = self.endpoint1._endpoint_cache_store( + "endpoint_cache.test", b"test" + ) + cache1._write( + { + "create_date": dt1, + } + ) + dt2 = "2024-07-10 00:00:00" + with freeze_time(dt2): + cache2 = self.endpoint1._endpoint_cache_store( + "endpoint_cache.test2", b"test2" + ) + cache2._write( + { + "create_date": dt2, + } + ) + dt2 = "2024-07-20 00:00:00" + with freeze_time(dt2): + cache3 = self.endpoint1._endpoint_cache_store( + "endpoint_cache.test3", b"test3" + ) + cache3._write( + { + "create_date": dt2, + } + ) + with freeze_time("2024-08-01 00:00:00"): + self.endpoint1._endpoint_cache_gc() + self.assertFalse(cache1.exists()) + self.assertTrue(cache2.exists()) + self.assertTrue(cache3.exists()) + with freeze_time("2024-08-12 00:00:00"): + self.endpoint1._endpoint_cache_gc() + self.assertFalse(cache1.exists()) + self.assertFalse(cache2.exists()) + self.assertTrue(cache3.exists()) diff --git a/endpoint_cache/views/endpoint_view.xml b/endpoint_cache/views/endpoint_view.xml new file mode 100644 index 00000000..b8d0e72e --- /dev/null +++ b/endpoint_cache/views/endpoint_view.xml @@ -0,0 +1,35 @@ + + + + + + endpoint.endpoint + + + + + + + +
+
+# get the name of the cache
+cache_name = endpoint._make_cache_name("json")
+# check if the cache exists
+cached = endpoint._endpoint_cache_get(cache_name)
+if cached:
+    result = cached
+else:
+    result = json.dumps(env["my.model"]._get_a_very_expensive_computed_result())
+    # cache does not exist, create it0
+    endpoint._endpoint_cache_store(cache_name, result)
+resp = Response(result, content_type="application/json", status=200)
+result = dict(response=resp)
+              
+
+
+
+
+ +
diff --git a/setup/endpoint_cache/odoo/addons/endpoint_cache b/setup/endpoint_cache/odoo/addons/endpoint_cache new file mode 120000 index 00000000..b0a3895c --- /dev/null +++ b/setup/endpoint_cache/odoo/addons/endpoint_cache @@ -0,0 +1 @@ +../../../../endpoint_cache \ No newline at end of file diff --git a/setup/endpoint_cache/setup.py b/setup/endpoint_cache/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/endpoint_cache/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)