Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[14.0] Add endpoint_cache #54

Merged
merged 1 commit into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
)
Loading