Skip to content

Commit

Permalink
Add endpoint_cache
Browse files Browse the repository at this point in the history
  • Loading branch information
simahawk committed Jul 11, 2024
1 parent dbc3cdc commit e5c252d
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 0 deletions.
1 change: 1 addition & 0 deletions endpoint_cache/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
15 changes: 15 additions & 0 deletions endpoint_cache/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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"],
}
1 change: 1 addition & 0 deletions endpoint_cache/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import endpoint_mixin
105 changes: 105 additions & 0 deletions endpoint_cache/models/endpoint_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Copyright 2024 Camptocamp SA
# @author: Simone Orsi <[email protected]>
# 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()

def action_view_cache_attachments(self):
"""Action to view cache attachments"""
action = self.env["ir.actions.actions"]._for_xml_id("base.action_attachment")
action["domain"] = self._endpoint_view_cache_domain()
action["name"] = _("Cache results")
return action

def _endpoint_view_cache_domain(self):
return [
("name", "like", "endpoint_cache%"),
("res_model", "=", self._name),
("res_id", "=", self.id),
]
1 change: 1 addition & 0 deletions endpoint_cache/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Simone Orsi <[email protected]>
1 change: 1 addition & 0 deletions endpoint_cache/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Technical module provide basic caching configuration and utils for endpoints.
1 change: 1 addition & 0 deletions endpoint_cache/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add cache pre-heating
15 changes: 15 additions & 0 deletions endpoint_cache/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Example of usage in an endpoint::

# get the name of the cache
cache_name = endpoint._endpoint_cache_make_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 it
endpoint._endpoint_cache_store(cache_name, result)
resp = Response(result, content_type="application/json", status=200)
result = dict(response=resp)
1 change: 1 addition & 0 deletions endpoint_cache/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_endpoint
80 changes: 80 additions & 0 deletions endpoint_cache/tests/test_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright 2024 Camptocamp SA
# @author: Simone Orsi <[email protected]>
# 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())
41 changes: 41 additions & 0 deletions endpoint_cache/views/endpoint_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 Camptocamp SA
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>

<record model="ir.ui.view" id="endpoint_endpoint_form_view">
<field name="model">endpoint.endpoint</field>
<field name="inherit_id" ref="endpoint.endpoint_endpoint_form_view" />
<field name="arch" type="xml">
<notebook>
<page string="Cache">
<group>
<field name="cache_policy" />
<button
class="oe_inline"
name="action_view_cache_attachments"
string="View caches"
type="object"
/>
</group>
<hr string="Cache usage example" />
<pre>
# get the name of the cache
cache_name = endpoint._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)
</pre>
</page>
</notebook>
</field>
</record>

</odoo>
1 change: 1 addition & 0 deletions setup/endpoint_cache/odoo/addons/endpoint_cache
6 changes: 6 additions & 0 deletions setup/endpoint_cache/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

0 comments on commit e5c252d

Please sign in to comment.