From 696a7c0adbaf56973b76c55f4081370fcaceed7f Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Wed, 8 Jan 2025 11:58:22 +0100
Subject: [PATCH 01/58] [FIX] fastapi: Avoid zombie threads
Each time a fastapi app is created, a new event loop thread is created by the ASGIMiddleware. Unfortunately, every time the cache is cleared, a new app is created with a new even loop thread. This leads to an increase in the number of threads created to manage the asyncio event loop, even though many of them are no longer in use. To avoid this problem, the thread in charge of the event loop is now created only once per thread / process and the result is stored in the thread's local storage. If a new instance of an app needs to be created following a cache reset, this ensures that the same event loop is reused.
refs #484
---
fastapi/models/fastapi_endpoint.py | 25 ++++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py
index 1312f07d1..9f110cfdd 100644
--- a/fastapi/models/fastapi_endpoint.py
+++ b/fastapi/models/fastapi_endpoint.py
@@ -1,7 +1,9 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
+import asyncio
import logging
+import threading
from functools import partial
from itertools import chain
from typing import Any, Callable, Dict, List, Tuple
@@ -19,6 +21,26 @@
_logger = logging.getLogger(__name__)
+# Thread-local storage for event loops
+# Using a thread-local storage allows to have a dedicated event loop per thread
+# and avoid the need to create a new event loop for each request. It's also
+# compatible with the multi-worker mode of Odoo.
+_event_loop_storage = threading.local()
+
+
+def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
+ """
+ Get or create a reusable event loop for the current thread.
+ """
+ if not hasattr(_event_loop_storage, "loop"):
+ loop = asyncio.new_event_loop()
+ loop_thread = threading.Thread(target=loop.run_forever, daemon=True)
+ loop_thread.start()
+ _event_loop_storage.loop = loop
+ _event_loop_storage.thread = loop_thread
+ return _event_loop_storage.loop
+
+
class FastapiEndpoint(models.Model):
_name = "fastapi.endpoint"
@@ -213,7 +235,8 @@ def get_app(self, root_path):
app = FastAPI()
app.mount(record.root_path, record._get_app())
self._clear_fastapi_exception_handlers(app)
- return ASGIMiddleware(app)
+ event_loop = get_or_create_event_loop()
+ return ASGIMiddleware(app, loop=event_loop)
def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None:
"""
From 8c090cd6b2af9082c0fc5235a25c93e8d314aac6 Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Fri, 10 Jan 2025 14:44:18 +0100
Subject: [PATCH 02/58] [IMP] fastapi: add event loop lifecycle management
This commit adds event loop lifecycle management to the FastAPI dispatcher.
Before this commit, an event loop and the thread to run it were created
each time a FastAPI app was created. The drawback of this approach is that
when the app was destroyed (for example, when the cache of app was cleared),
the event loop and the thread were not properly stopped, which could lead
to memory leaks and zombie threads. This commit fixes this issue by creating
a pool of event loops and threads that are shared among all FastAPI apps.
On each call to a FastAPI app, a event loop is requested from the pool and
is returned to the pool when the app is destroyed. At request time of
an event loop, the pool try to reuse an existing event loop and if no event
loop is available, a new event loop is created.
The cache of the FastAPI app is also refactored to use it's own mechanism.
It's now based on a dictionary of queues by root path by database,
where each queue is a pool of FastAPI app. This allows a better management
of the invalidation of the cache. It's now possible to invalidate
the cache of FastAPI app by root path without affecting the cache of others
root paths.
---
fastapi/fastapi_dispatcher.py | 24 ++++++-------
fastapi/middleware.py | 26 ++++++++++++++
fastapi/models/fastapi_endpoint.py | 35 +++---------------
fastapi/pools/__init__.py | 7 ++++
fastapi/pools/event_loop.py | 58 ++++++++++++++++++++++++++++++
fastapi/pools/fastapi_app.py | 52 +++++++++++++++++++++++++++
6 files changed, 159 insertions(+), 43 deletions(-)
create mode 100644 fastapi/middleware.py
create mode 100644 fastapi/pools/__init__.py
create mode 100644 fastapi/pools/event_loop.py
create mode 100644 fastapi/pools/fastapi_app.py
diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py
index 1a8eb3532..3edf29f14 100644
--- a/fastapi/fastapi_dispatcher.py
+++ b/fastapi/fastapi_dispatcher.py
@@ -8,6 +8,7 @@
from .context import odoo_env_ctx
from .error_handlers import convert_exception_to_status_body
+from .pools import fastapi_app_pool
class FastApiDispatcher(Dispatcher):
@@ -24,18 +25,17 @@ def dispatch(self, endpoint, args):
root_path = "/" + environ["PATH_INFO"].split("/")[1]
# TODO store the env into contextvar to be used by the odoo_env
# depends method
- fastapi_endpoint = self.request.env["fastapi.endpoint"].sudo()
- app = fastapi_endpoint.get_app(root_path)
- uid = fastapi_endpoint.get_uid(root_path)
- data = BytesIO()
- with self._manage_odoo_env(uid):
- for r in app(environ, self._make_response):
- data.write(r)
- if self.inner_exception:
- raise self.inner_exception
- return self.request.make_response(
- data.getvalue(), headers=self.headers, status=self.status
- )
+ with fastapi_app_pool.get_app(root_path, request.env) as app:
+ uid = request.env["fastapi.endpoint"].sudo().get_uid(root_path)
+ data = BytesIO()
+ with self._manage_odoo_env(uid):
+ for r in app(environ, self._make_response):
+ data.write(r)
+ if self.inner_exception:
+ raise self.inner_exception
+ return self.request.make_response(
+ data.getvalue(), headers=self.headers, status=self.status
+ )
def handle_error(self, exc):
headers = getattr(exc, "headers", None)
diff --git a/fastapi/middleware.py b/fastapi/middleware.py
new file mode 100644
index 000000000..8f63c2339
--- /dev/null
+++ b/fastapi/middleware.py
@@ -0,0 +1,26 @@
+# Copyright 2025 ACSONE SA/NV
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
+"""
+ASGI middleware for FastAPI.
+
+This module provides an ASGI middleware for FastAPI applications. The middleware
+is designed to ensure managed the lifecycle of the threads used to as event loop
+for the ASGI application.
+
+"""
+
+from typing import Iterable
+
+import a2wsgi
+from a2wsgi.asgi import ASGIResponder
+from a2wsgi.wsgi_typing import Environ, StartResponse
+
+from .pools import event_loop_pool
+
+
+class ASGIMiddleware(a2wsgi.ASGIMiddleware):
+ def __call__(
+ self, environ: Environ, start_response: StartResponse
+ ) -> Iterable[bytes]:
+ with event_loop_pool.get_event_loop() as loop:
+ return ASGIResponder(self.app, loop)(environ, start_response)
diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py
index 9f110cfdd..9db835e0f 100644
--- a/fastapi/models/fastapi_endpoint.py
+++ b/fastapi/models/fastapi_endpoint.py
@@ -1,14 +1,11 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
-import asyncio
import logging
-import threading
from functools import partial
from itertools import chain
from typing import Any, Callable, Dict, List, Tuple
-from a2wsgi import ASGIMiddleware
from starlette.middleware import Middleware
from starlette.routing import Mount
@@ -17,30 +14,12 @@
from fastapi import APIRouter, Depends, FastAPI
from .. import dependencies
+from ..middleware import ASGIMiddleware
+from ..pools import fastapi_app_pool
_logger = logging.getLogger(__name__)
-# Thread-local storage for event loops
-# Using a thread-local storage allows to have a dedicated event loop per thread
-# and avoid the need to create a new event loop for each request. It's also
-# compatible with the multi-worker mode of Odoo.
-_event_loop_storage = threading.local()
-
-
-def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
- """
- Get or create a reusable event loop for the current thread.
- """
- if not hasattr(_event_loop_storage, "loop"):
- loop = asyncio.new_event_loop()
- loop_thread = threading.Thread(target=loop.run_forever, daemon=True)
- loop_thread.start()
- _event_loop_storage.loop = loop
- _event_loop_storage.thread = loop_thread
- return _event_loop_storage.loop
-
-
class FastapiEndpoint(models.Model):
_name = "fastapi.endpoint"
@@ -220,14 +199,9 @@ def _endpoint_registry_route_unique_key(self, routing: Dict[str, Any]):
return f"{self._name}:{self.id}:{path}"
def _reset_app(self):
- self.get_app.clear_cache(self)
+ fastapi_app_pool.invalidate(self.root_path, self.env)
@api.model
- @tools.ormcache("root_path")
- # TODO cache on thread local by db to enable to get 1 middelware by
- # thread when odoo runs in multi threads mode and to allows invalidate
- # specific entries in place og the overall cache as we have to do into
- # the _rest_app method
def get_app(self, root_path):
record = self.search([("root_path", "=", root_path)])
if not record:
@@ -235,8 +209,7 @@ def get_app(self, root_path):
app = FastAPI()
app.mount(record.root_path, record._get_app())
self._clear_fastapi_exception_handlers(app)
- event_loop = get_or_create_event_loop()
- return ASGIMiddleware(app, loop=event_loop)
+ return ASGIMiddleware(app)
def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None:
"""
diff --git a/fastapi/pools/__init__.py b/fastapi/pools/__init__.py
new file mode 100644
index 000000000..08ab00781
--- /dev/null
+++ b/fastapi/pools/__init__.py
@@ -0,0 +1,7 @@
+from .event_loop import EventLoopPool
+from .fastapi_app import FastApiAppPool
+
+event_loop_pool = EventLoopPool()
+fastapi_app_pool = FastApiAppPool()
+
+__all__ = ["event_loop_pool", "fastapi_app_pool"]
diff --git a/fastapi/pools/event_loop.py b/fastapi/pools/event_loop.py
new file mode 100644
index 000000000..a0a02a8f3
--- /dev/null
+++ b/fastapi/pools/event_loop.py
@@ -0,0 +1,58 @@
+# Copyright 2025 ACSONE SA/NV
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
+
+import asyncio
+import queue
+import threading
+from contextlib import contextmanager
+from typing import Generator
+
+
+class EventLoopPool:
+ def __init__(self):
+ self.pool = queue.Queue[tuple[asyncio.AbstractEventLoop, threading.Thread]]()
+
+ def __get_event_loop_and_thread(
+ self,
+ ) -> tuple[asyncio.AbstractEventLoop, threading.Thread]:
+ """
+ Get an event loop from the pool. If no event loop is available, create a new one.
+ """
+ try:
+ return self.pool.get_nowait()
+ except queue.Empty:
+ loop = asyncio.new_event_loop()
+ thread = threading.Thread(target=loop.run_forever, daemon=True)
+ thread.start()
+ return loop, thread
+
+ def __return_event_loop(
+ self, loop: asyncio.AbstractEventLoop, thread: threading.Thread
+ ) -> None:
+ """
+ Return an event loop to the pool for reuse.
+ """
+ self.pool.put((loop, thread))
+
+ def shutdown(self):
+ """
+ Shutdown all event loop threads in the pool.
+ """
+ while not self.pool.empty():
+ loop, thread = self.pool.get_nowait()
+ loop.call_soon_threadsafe(loop.stop)
+ thread.join()
+ loop.close()
+
+ @contextmanager
+ def get_event_loop(self) -> Generator[asyncio.AbstractEventLoop, None, None]:
+ """
+ Get an event loop from the pool. If no event loop is available, create a new one.
+
+ After the context manager exits, the event loop is returned to the pool for reuse.
+ """
+ loop, thread = self.__get_event_loop_and_thread()
+ try:
+ yield loop
+ finally:
+ self.__return_event_loop(loop, thread)
diff --git a/fastapi/pools/fastapi_app.py b/fastapi/pools/fastapi_app.py
new file mode 100644
index 000000000..3508ea373
--- /dev/null
+++ b/fastapi/pools/fastapi_app.py
@@ -0,0 +1,52 @@
+# Copyright 2025 ACSONE SA/NV
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
+
+import queue
+from collections import defaultdict
+from contextlib import contextmanager
+from typing import Generator
+
+from odoo.api import Environment
+
+from fastapi import FastAPI
+
+
+class FastApiAppPool:
+ def __init__(self):
+ self._queue_by_db_by_root_path: dict[
+ str, dict[str, queue.Queue[FastAPI]]
+ ] = defaultdict(lambda: defaultdict(queue.Queue))
+
+ def __get_app(self, env: Environment, root_path: str) -> FastAPI:
+ db_name = env.cr.dbname
+ try:
+ return self._queue_by_db_by_root_path[db_name][root_path].get_nowait()
+ except queue.Empty:
+ env["fastapi.endpoint"].sudo()
+ return env["fastapi.endpoint"].sudo().get_app(root_path)
+
+ def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None:
+ db_name = env.cr.dbname
+ self._queue_by_db_by_root_path[db_name][root_path].put(app)
+
+ @contextmanager
+ def get_app(
+ self, root_path: str, env: Environment
+ ) -> Generator[FastAPI, None, None]:
+ """Return a FastAPI app to be used in a context manager.
+
+ The app is retrieved from the pool if available, otherwise a new one is created.
+ The app is returned to the pool after the context manager exits.
+
+ When used into the FastApiDispatcher class this ensures that the app is reused
+ across multiple requests but only one request at a time uses an app.
+ """
+ app = self.__get_app(env, root_path)
+ try:
+ yield app
+ finally:
+ self.__return_app(env, app, root_path)
+
+ def invalidate(self, root_path: str, env: Environment) -> None:
+ db_name = env.cr.dbname
+ self._queue_by_db_by_root_path[db_name][root_path] = queue.Queue()
From 2db95d5a4a02ec72814310cebd0703c3ea3b4bc5 Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Fri, 10 Jan 2025 16:10:02 +0100
Subject: [PATCH 03/58] [FIX] fastapi: Graceful shutdown of event loop
On server shutdown, ensure that created the event loops are closed properly.
---
fastapi/pools/__init__.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/fastapi/pools/__init__.py b/fastapi/pools/__init__.py
index 08ab00781..31f1fb388 100644
--- a/fastapi/pools/__init__.py
+++ b/fastapi/pools/__init__.py
@@ -1,7 +1,11 @@
from .event_loop import EventLoopPool
from .fastapi_app import FastApiAppPool
+from odoo.service.server import CommonServer
event_loop_pool = EventLoopPool()
fastapi_app_pool = FastApiAppPool()
+
+CommonServer.on_stop(event_loop_pool.shutdown)
+
__all__ = ["event_loop_pool", "fastapi_app_pool"]
From 1bf9751e64a8148fcc2b66b654b55b13b8ba4e98 Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Fri, 10 Jan 2025 16:36:35 +0100
Subject: [PATCH 04/58] [FIX] fastapi: Ensure thread safety of the FastAPI app
cache
defaultdict in python is not thread safe. Since this data structure
is used to store the cache of FastAPI apps, we must ensure that the
access to this cache is thread safe. This is done by using a lock
to protect the access to the cache.
---
fastapi/pools/fastapi_app.py | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/fastapi/pools/fastapi_app.py b/fastapi/pools/fastapi_app.py
index 3508ea373..ffdd4d5e8 100644
--- a/fastapi/pools/fastapi_app.py
+++ b/fastapi/pools/fastapi_app.py
@@ -2,6 +2,7 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import queue
+import threading
from collections import defaultdict
from contextlib import contextmanager
from typing import Generator
@@ -16,18 +17,25 @@ def __init__(self):
self._queue_by_db_by_root_path: dict[
str, dict[str, queue.Queue[FastAPI]]
] = defaultdict(lambda: defaultdict(queue.Queue))
+ self._lock = threading.Lock()
- def __get_app(self, env: Environment, root_path: str) -> FastAPI:
+ def __get_pool(self, env: Environment, root_path: str) -> queue.Queue[FastAPI]:
db_name = env.cr.dbname
+ with self._lock:
+ # default dict is not thread safe but the use
+ return self._queue_by_db_by_root_path[db_name][root_path]
+
+ def __get_app(self, env: Environment, root_path: str) -> FastAPI:
+ pool = self.__get_pool(env, root_path)
try:
- return self._queue_by_db_by_root_path[db_name][root_path].get_nowait()
+ return pool.get_nowait()
except queue.Empty:
env["fastapi.endpoint"].sudo()
return env["fastapi.endpoint"].sudo().get_app(root_path)
def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None:
- db_name = env.cr.dbname
- self._queue_by_db_by_root_path[db_name][root_path].put(app)
+ pool = self.__get_pool(env, root_path)
+ pool.put(app)
@contextmanager
def get_app(
@@ -48,5 +56,6 @@ def get_app(
self.__return_app(env, app, root_path)
def invalidate(self, root_path: str, env: Environment) -> None:
- db_name = env.cr.dbname
- self._queue_by_db_by_root_path[db_name][root_path] = queue.Queue()
+ with self._lock:
+ db_name = env.cr.dbname
+ self._queue_by_db_by_root_path[db_name][root_path] = queue.Queue()
From e657680226992ee28df1ffa4d4c306f201838c06 Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Mon, 13 Jan 2025 17:27:08 +0100
Subject: [PATCH 05/58] [IMP] fastapi: Improves app cache lifecycle
This commit improves the lifecycle of the fastapi app cache.
It first ensures that the cache is effectively invalidated when changes
are made to the app configuration even if theses changes occur into an
other server instance.
It also remove the use of a locking mechanism put in place to ensure a thread
safe access to a value into the cache to avoid potential concurrency issue when
a default value is set to the cache at access time. This lock could lead
to unnecessary contention and reduce the performance benefits of queue.Queue's
fine-grained internal synchronization for a questionable gain. The only
expected gain was to avoid the useless creation of a queue.Queue instance
that would never be used since at the time of puting the value into the cache
we are sure that a value is already present into the dictionary.
---
fastapi/fastapi_dispatcher.py | 2 +-
fastapi/models/fastapi_endpoint.py | 17 ++++--
fastapi/pools/fastapi_app.py | 85 +++++++++++++++++++++++++++---
3 files changed, 91 insertions(+), 13 deletions(-)
diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py
index 3edf29f14..3f2390d4d 100644
--- a/fastapi/fastapi_dispatcher.py
+++ b/fastapi/fastapi_dispatcher.py
@@ -25,7 +25,7 @@ def dispatch(self, endpoint, args):
root_path = "/" + environ["PATH_INFO"].split("/")[1]
# TODO store the env into contextvar to be used by the odoo_env
# depends method
- with fastapi_app_pool.get_app(root_path, request.env) as app:
+ with fastapi_app_pool.get_app(env=request.env, root_path=root_path) as app:
uid = request.env["fastapi.endpoint"].sudo().get_uid(root_path)
data = BytesIO()
with self._manage_odoo_env(uid):
diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py
index 9db835e0f..25e3dc468 100644
--- a/fastapi/models/fastapi_endpoint.py
+++ b/fastapi/models/fastapi_endpoint.py
@@ -15,7 +15,6 @@
from .. import dependencies
from ..middleware import ASGIMiddleware
-from ..pools import fastapi_app_pool
_logger = logging.getLogger(__name__)
@@ -122,10 +121,10 @@ def _registered_endpoint_rule_keys(self):
return tuple(res)
@api.model
- def _routing_impacting_fields(self) -> Tuple[str]:
+ def _routing_impacting_fields(self) -> Tuple[str, ...]:
"""The list of fields requiring to refresh the mount point of the pp
into odoo if modified"""
- return ("root_path",)
+ return ("root_path", "save_http_session")
#
# end of endpoint.route.sync.mixin methods implementation
@@ -199,7 +198,17 @@ def _endpoint_registry_route_unique_key(self, routing: Dict[str, Any]):
return f"{self._name}:{self.id}:{path}"
def _reset_app(self):
- fastapi_app_pool.invalidate(self.root_path, self.env)
+ self._reset_app_cache_marker.clear_cache(self)
+
+ @tools.ormcache()
+ def _reset_app_cache_marker(self):
+ """This methos is used to get a way to mark the orm cache as dirty
+ when the app is reset. By marking the cache as dirty, the system
+ will signal to others instances that the cache is not up to date
+ and that they should invalidate their cache as well. This is required
+ to ensure that any change requiring a reset of the app is propagated
+ to all the running instances.
+ """
@api.model
def get_app(self, root_path):
diff --git a/fastapi/pools/fastapi_app.py b/fastapi/pools/fastapi_app.py
index ffdd4d5e8..991374179 100644
--- a/fastapi/pools/fastapi_app.py
+++ b/fastapi/pools/fastapi_app.py
@@ -1,6 +1,6 @@
# Copyright 2025 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
-
+import logging
import queue
import threading
from collections import defaultdict
@@ -11,19 +11,65 @@
from fastapi import FastAPI
+_logger = logging.getLogger(__name__)
+
class FastApiAppPool:
+ """Pool of FastAPI apps.
+
+ This class manages a pool of FastAPI apps. The pool is organized by database name
+ and root path. Each pool is a queue of FastAPI apps.
+
+ The pool is used to reuse FastAPI apps across multiple requests. This is useful
+ to avoid the overhead of creating a new FastAPI app for each request. The pool
+ ensures that only one request at a time uses an app.
+
+ The proper way to use the pool is to use the get_app method as a context manager.
+ This ensures that the app is returned to the pool after the context manager exits.
+ The get_app method is designed to ensure that the app made available to the
+ caller is unique and not used by another caller at the same time.
+
+ .. code-block:: python
+
+ with fastapi_app_pool.get_app(env=request.env, root_path=root_path) as app:
+ # use the app
+
+ The pool is invalidated when the cache registry is updated. This ensures that
+ the pool is always up-to-date with the latest app configuration. It also
+ ensures that the invalidation is done even in the case of a modification occurring
+ in a different worker process or thread or server instance. This mechanism
+ works because every time an attribute of the fastapi.endpoint model is modified
+ and this attribute is part of the list returned by the `_fastapi_app_fields`,
+ or `_routing_impacting_fields` methods, we reset the cache of a marker method
+ `_reset_app_cache_marker`. As side effect, the cache registry is marked to be
+ updated by the increment of the `cache_sequence` SQL sequence. This cache sequence
+ on the registry is reloaded from the DB on each request made to a specific database.
+ When an app is retrieved from the pool, we always compare the cache sequence of
+ the pool with the cache sequence of the registry. If the two sequences are different,
+ we invalidate the pool and save the new cache sequence on the pool.
+
+ The cache is based on a defaultdict of defaultdict of queue.Queue. We are cautious
+ that the use of defaultdict is not thread-safe for operations that modify the
+ dictionary. However the only operation that modifies the dictionary is the
+ first access to a new key. If two threads access the same key at the same time,
+ the two threads will create two different queues. This is not a problem since
+ at the time of returning an app to the pool, we are sure that a queue exists
+ for the key into the cache and all the created apps are returned to the same
+ valid queue. And the end, the lack of thread-safety for the defaultdict could
+ only lead to a negligible overhead of creating a new queue that will never be
+ used. This is why we consider that the use of defaultdict is safe in this context.
+ """
+
def __init__(self):
self._queue_by_db_by_root_path: dict[
str, dict[str, queue.Queue[FastAPI]]
] = defaultdict(lambda: defaultdict(queue.Queue))
+ self.__cache_sequence = 0
self._lock = threading.Lock()
def __get_pool(self, env: Environment, root_path: str) -> queue.Queue[FastAPI]:
db_name = env.cr.dbname
- with self._lock:
- # default dict is not thread safe but the use
- return self._queue_by_db_by_root_path[db_name][root_path]
+ return self._queue_by_db_by_root_path[db_name][root_path]
def __get_app(self, env: Environment, root_path: str) -> FastAPI:
pool = self.__get_pool(env, root_path)
@@ -39,7 +85,7 @@ def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None:
@contextmanager
def get_app(
- self, root_path: str, env: Environment
+ self, env: Environment, root_path: str
) -> Generator[FastAPI, None, None]:
"""Return a FastAPI app to be used in a context manager.
@@ -49,13 +95,36 @@ def get_app(
When used into the FastApiDispatcher class this ensures that the app is reused
across multiple requests but only one request at a time uses an app.
"""
+ self._check_cache(env)
app = self.__get_app(env, root_path)
try:
yield app
finally:
self.__return_app(env, app, root_path)
- def invalidate(self, root_path: str, env: Environment) -> None:
- with self._lock:
- db_name = env.cr.dbname
+ @property
+ def cache_sequence(self) -> int:
+ return self.__cache_sequence
+
+ @cache_sequence.setter
+ def cache_sequence(self, value: int) -> None:
+ if value != self.__cache_sequence:
+ with self._lock:
+ self.__cache_sequence = value
+
+ def _check_cache(self, env: Environment) -> None:
+ cache_sequence = env.registry.cache_sequence
+ if cache_sequence != self.cache_sequence and self.cache_sequence != 0:
+ _logger.info(
+ "Cache registry updated, reset fastapi_app pool for the current "
+ "database"
+ )
+ self.invalidate(env)
+ self.cache_sequence = cache_sequence
+
+ def invalidate(self, env: Environment, root_path: str | None = None) -> None:
+ db_name = env.cr.dbname
+ if root_path:
self._queue_by_db_by_root_path[db_name][root_path] = queue.Queue()
+ elif db_name in self._queue_by_db_by_root_path:
+ del self._queue_by_db_by_root_path[db_name]
From 488e90be9959fbff256606d5ad3c7466ca1eba41 Mon Sep 17 00:00:00 2001
From: Florian Mounier
Date: Tue, 25 Feb 2025 12:42:50 +0100
Subject: [PATCH 06/58] [FIX] extendable_fastapi: Inherit from last declared
fastapi dispatcher
---
extendable_fastapi/fastapi_dispatcher.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/extendable_fastapi/fastapi_dispatcher.py b/extendable_fastapi/fastapi_dispatcher.py
index b940df9ce..10fcfaf22 100644
--- a/extendable_fastapi/fastapi_dispatcher.py
+++ b/extendable_fastapi/fastapi_dispatcher.py
@@ -3,6 +3,8 @@
from contextlib import contextmanager
+from odoo.http import _dispatchers
+
from odoo.addons.extendable.registry import _extendable_registries_database
from odoo.addons.fastapi.fastapi_dispatcher import (
FastApiDispatcher as BaseFastApiDispatcher,
@@ -11,7 +13,9 @@
from extendable import context
-class FastApiDispatcher(BaseFastApiDispatcher):
+# Inherit from last registered fastapi dispatcher
+# This handles multiple overload of dispatchers
+class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)):
routing_type = "fastapi"
def dispatch(self, endpoint, args):
From f175fa644396c871d66d6c32ed63b0d27f6deed9 Mon Sep 17 00:00:00 2001
From: Florian Mounier
Date: Tue, 25 Feb 2025 12:57:09 +0100
Subject: [PATCH 07/58] [ADD] fastapi_encrypted_errors
---
fastapi_encrypted_errors/README.rst | 99 ++++
fastapi_encrypted_errors/__init__.py | 3 +
fastapi_encrypted_errors/__manifest__.py | 25 +
.../fastapi_dispatcher.py | 35 ++
fastapi_encrypted_errors/models/__init__.py | 1 +
.../models/fastapi_endpoint.py | 47 ++
.../readme/CONTRIBUTORS.rst | 3 +
.../readme/DESCRIPTION.rst | 3 +
fastapi_encrypted_errors/readme/USAGE.rst | 7 +
.../security/ir.model.access.csv | 2 +
.../static/description/index.html | 439 ++++++++++++++++++
fastapi_encrypted_errors/tests/__init__.py | 1 +
.../tests/test_fastapi_encrypted_errors.py | 74 +++
.../views/fastapi_endpoint_views.xml | 31 ++
fastapi_encrypted_errors/wizards/__init__.py | 1 +
.../wizards/wizard_fastapi_decrypt_errors.py | 40 ++
.../wizard_fastapi_decrypt_errors_views.xml | 47 ++
requirements.txt | 1 +
.../odoo/addons/fastapi_encrypted_errors | 1 +
setup/fastapi_encrypted_errors/setup.py | 6 +
20 files changed, 866 insertions(+)
create mode 100644 fastapi_encrypted_errors/README.rst
create mode 100644 fastapi_encrypted_errors/__init__.py
create mode 100644 fastapi_encrypted_errors/__manifest__.py
create mode 100644 fastapi_encrypted_errors/fastapi_dispatcher.py
create mode 100644 fastapi_encrypted_errors/models/__init__.py
create mode 100644 fastapi_encrypted_errors/models/fastapi_endpoint.py
create mode 100644 fastapi_encrypted_errors/readme/CONTRIBUTORS.rst
create mode 100644 fastapi_encrypted_errors/readme/DESCRIPTION.rst
create mode 100644 fastapi_encrypted_errors/readme/USAGE.rst
create mode 100644 fastapi_encrypted_errors/security/ir.model.access.csv
create mode 100644 fastapi_encrypted_errors/static/description/index.html
create mode 100644 fastapi_encrypted_errors/tests/__init__.py
create mode 100644 fastapi_encrypted_errors/tests/test_fastapi_encrypted_errors.py
create mode 100644 fastapi_encrypted_errors/views/fastapi_endpoint_views.xml
create mode 100644 fastapi_encrypted_errors/wizards/__init__.py
create mode 100644 fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py
create mode 100644 fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml
create mode 120000 setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors
create mode 100644 setup/fastapi_encrypted_errors/setup.py
diff --git a/fastapi_encrypted_errors/README.rst b/fastapi_encrypted_errors/README.rst
new file mode 100644
index 000000000..7027f164e
--- /dev/null
+++ b/fastapi_encrypted_errors/README.rst
@@ -0,0 +1,99 @@
+========================
+FastAPI Encrypted Errors
+========================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:296fbef824a5eb64e9bbaedc382ef15e41d146f0cc59d458c0d76de46a54358e
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github
+ :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_encrypted_errors
+ :alt: OCA/rest-framework
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_encrypted_errors
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=16.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module adds a "ref" field in the error response of FastAPI.
+This field is an AES encrypted string that contains the error message / traceback.
+This encrypted string can be decrypted using the endpoint decrypt error wizard.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Usage
+=====
+
+First you have to enable the encryption for an endpoint by checking the `Encrypt Errors` checkbox
+in the endpoint configuration.
+
+To decrypt an error message, you can use the "Decrypt Error" wizard in the
+FastAPI menu.
+
+You can regenerate a new key by clicking on the "Regenerate Key" button next to the `Errors Secret Key` field.
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Akretion
+
+Contributors
+~~~~~~~~~~~~
+
+* `Akretion `_:
+
+ * Florian Mounier
+
+Maintainers
+~~~~~~~~~~~
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+.. |maintainer-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px
+ :target: https://github.com/paradoxxxzero
+ :alt: paradoxxxzero
+
+Current `maintainer `__:
+
+|maintainer-paradoxxxzero|
+
+This module is part of the `OCA/rest-framework `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/fastapi_encrypted_errors/__init__.py b/fastapi_encrypted_errors/__init__.py
new file mode 100644
index 000000000..d7f65dfa1
--- /dev/null
+++ b/fastapi_encrypted_errors/__init__.py
@@ -0,0 +1,3 @@
+from . import models
+from . import wizards
+from . import fastapi_dispatcher
diff --git a/fastapi_encrypted_errors/__manifest__.py b/fastapi_encrypted_errors/__manifest__.py
new file mode 100644
index 000000000..254901404
--- /dev/null
+++ b/fastapi_encrypted_errors/__manifest__.py
@@ -0,0 +1,25 @@
+# Copyright 2024 Akretion (http://www.akretion.com).
+# @author Florian Mounier
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+ "name": "FastAPI Encrypted Errors",
+ "summary": "Adds encrypted error messages to FastAPI error responses.",
+ "version": "16.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "Akretion,Odoo Community Association (OCA)",
+ "maintainers": ["paradoxxxzero"],
+ "website": "https://github.com/OCA/rest-framework",
+ "depends": [
+ "fastapi",
+ ],
+ "data": [
+ "security/ir.model.access.csv",
+ "views/fastapi_endpoint_views.xml",
+ "wizards/wizard_fastapi_decrypt_errors_views.xml",
+ ],
+ "demo": [],
+ "external_dependencies": {
+ "python": ["cryptography"],
+ },
+}
diff --git a/fastapi_encrypted_errors/fastapi_dispatcher.py b/fastapi_encrypted_errors/fastapi_dispatcher.py
new file mode 100644
index 000000000..a3f0524a2
--- /dev/null
+++ b/fastapi_encrypted_errors/fastapi_dispatcher.py
@@ -0,0 +1,35 @@
+# Copyright 2024 Akretion (http://www.akretion.com).
+# @author Florian Mounier
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.http import _dispatchers
+
+from odoo.addons.fastapi.error_handlers import convert_exception_to_status_body
+from odoo.addons.fastapi.fastapi_dispatcher import (
+ FastApiDispatcher as BaseFastApiDispatcher,
+)
+
+
+# Inherit from last registered fastapi dispatcher
+# This handles multiple overload of dispatchers
+class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)):
+ routing_type = "fastapi"
+
+ def handle_error(self, exc):
+ environ = self._get_environ()
+ root_path = "/" + environ["PATH_INFO"].split("/")[1]
+ fastapi_endpoint = (
+ self.request.env["fastapi.endpoint"]
+ .sudo()
+ .search([("root_path", "=", root_path)])
+ )
+ if fastapi_endpoint.encrypt_errors:
+ headers = getattr(exc, "headers", None)
+ status_code, body = convert_exception_to_status_body(exc)
+ if body:
+ body["ref"] = fastapi_endpoint._encrypt_error(exc)
+ return self.request.make_json_response(
+ body, status=status_code, headers=headers
+ )
+
+ return super().handle_error(exc)
diff --git a/fastapi_encrypted_errors/models/__init__.py b/fastapi_encrypted_errors/models/__init__.py
new file mode 100644
index 000000000..b825fab92
--- /dev/null
+++ b/fastapi_encrypted_errors/models/__init__.py
@@ -0,0 +1 @@
+from . import fastapi_endpoint
diff --git a/fastapi_encrypted_errors/models/fastapi_endpoint.py b/fastapi_encrypted_errors/models/fastapi_endpoint.py
new file mode 100644
index 000000000..add3ca2ec
--- /dev/null
+++ b/fastapi_encrypted_errors/models/fastapi_endpoint.py
@@ -0,0 +1,47 @@
+# Copyright 2024 Akretion (http://www.akretion.com).
+# @author Florian Mounier
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import traceback
+import zlib
+
+from cryptography.fernet import Fernet
+
+from odoo import fields, models
+
+
+class FastapiEndpoint(models.Model):
+ _inherit = "fastapi.endpoint"
+
+ encrypt_errors = fields.Boolean(
+ help="Encrypt errors before sending them to the client.",
+ )
+ encrypted_errors_secret_key = fields.Char(
+ help="The secret key used to encrypt errors before sending them to the client.",
+ default=lambda _: Fernet.generate_key(),
+ readonly=True,
+ )
+
+ def action_generate_encrypted_errors_secret_key(self):
+ for record in self:
+ record.encrypted_errors_secret_key = Fernet.generate_key()
+
+ def _encrypt_error(self, exc):
+ self.ensure_one()
+ if not self.encrypt_errors or not self.encrypted_errors_secret_key:
+ return
+
+ # Get full traceback
+ error = "".join(traceback.format_exception(exc))
+ # zlib compression works quite well on tracebacks
+ error = zlib.compress(error.encode("utf-8"))
+ f = Fernet(self.encrypted_errors_secret_key)
+ return f.encrypt(error)
+
+ def _decrypt_error(self, error):
+ self.ensure_one()
+ if not self.encrypt_errors or not self.encrypted_errors_secret_key:
+ return
+
+ f = Fernet(self.encrypted_errors_secret_key)
+ error = f.decrypt(error)
+ return zlib.decompress(error).decode("utf-8")
diff --git a/fastapi_encrypted_errors/readme/CONTRIBUTORS.rst b/fastapi_encrypted_errors/readme/CONTRIBUTORS.rst
new file mode 100644
index 000000000..a4d0ad922
--- /dev/null
+++ b/fastapi_encrypted_errors/readme/CONTRIBUTORS.rst
@@ -0,0 +1,3 @@
+* `Akretion `_:
+
+ * Florian Mounier
diff --git a/fastapi_encrypted_errors/readme/DESCRIPTION.rst b/fastapi_encrypted_errors/readme/DESCRIPTION.rst
new file mode 100644
index 000000000..3ea245c61
--- /dev/null
+++ b/fastapi_encrypted_errors/readme/DESCRIPTION.rst
@@ -0,0 +1,3 @@
+This module adds a "ref" field in the error response of FastAPI.
+This field is an AES encrypted string that contains the error message / traceback.
+This encrypted string can be decrypted using the endpoint decrypt error wizard.
diff --git a/fastapi_encrypted_errors/readme/USAGE.rst b/fastapi_encrypted_errors/readme/USAGE.rst
new file mode 100644
index 000000000..041077d9b
--- /dev/null
+++ b/fastapi_encrypted_errors/readme/USAGE.rst
@@ -0,0 +1,7 @@
+First you have to enable the encryption for an endpoint by checking the `Encrypt Errors` checkbox
+in the endpoint configuration.
+
+To decrypt an error message, you can use the "Decrypt Error" wizard in the
+FastAPI menu.
+
+You can regenerate a new key by clicking on the "Regenerate Key" button next to the `Errors Secret Key` field.
diff --git a/fastapi_encrypted_errors/security/ir.model.access.csv b/fastapi_encrypted_errors/security/ir.model.access.csv
new file mode 100644
index 000000000..d102f0c82
--- /dev/null
+++ b/fastapi_encrypted_errors/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_fastapi_wizard_auth_partner_impersonate,wizard_fastapi_decrypt_errors,model_wizard_fastapi_decrypt_errors,fastapi.group_fastapi_manager,1,1,1,1
diff --git a/fastapi_encrypted_errors/static/description/index.html b/fastapi_encrypted_errors/static/description/index.html
new file mode 100644
index 000000000..0ffce082e
--- /dev/null
+++ b/fastapi_encrypted_errors/static/description/index.html
@@ -0,0 +1,439 @@
+
+
+
+
+
+FastAPI Encrypted Errors
+
+
+
+
+
FastAPI Encrypted Errors
+
+
+
+
This module adds a “ref” field in the error response of FastAPI.
+This field is an AES encrypted string that contains the error message / traceback.
+This encrypted string can be decrypted using the endpoint decrypt error wizard.
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+
+
+
diff --git a/fastapi_encrypted_errors/wizards/__init__.py b/fastapi_encrypted_errors/wizards/__init__.py
new file mode 100644
index 000000000..a160dabbb
--- /dev/null
+++ b/fastapi_encrypted_errors/wizards/__init__.py
@@ -0,0 +1 @@
+from . import wizard_fastapi_decrypt_errors
diff --git a/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py
new file mode 100644
index 000000000..d19bd6d3a
--- /dev/null
+++ b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py
@@ -0,0 +1,40 @@
+# Copyright 2024 Akretion (http://www.akretion.com).
+# @author Florian Mounier
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import traceback
+
+from odoo import fields, models
+
+
+class WizardFastapiDecryptErrors(models.TransientModel):
+ _name = "wizard.fastapi.decrypt.errors"
+ _description = "Wizard to decrypt FastAPI errors"
+
+ error = fields.Text(required=True)
+ fastapi_endpoint_id = fields.Many2one(
+ "fastapi.endpoint",
+ string="FastAPI Endpoint",
+ required=True,
+ default=lambda self: self.env["fastapi.endpoint"].search([], limit=1),
+ )
+ decrypted_error = fields.Text(readonly=True)
+
+ def action_decrypt_error(self):
+ self.ensure_one()
+ try:
+ error = self.fastapi_endpoint_id._decrypt_error(self.error.encode("utf-8"))
+ except Exception:
+ self.decrypted_error = (
+ "Error while decrypting error: \n\n" + traceback.format_exc()
+ )
+ else:
+ self.decrypted_error = error
+
+ return {
+ "type": "ir.actions.act_window",
+ "res_model": "wizard.fastapi.decrypt.errors",
+ "view_mode": "form",
+ "view_type": "form",
+ "res_id": self.id,
+ "target": "new",
+ }
diff --git a/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml
new file mode 100644
index 000000000..62b8a803d
--- /dev/null
+++ b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml
@@ -0,0 +1,47 @@
+
+
+
+
+ wizard.fastapi.decrypt.errors
+
+
+
+
+
+
+ Decrypt Error
+ wizard.fastapi.decrypt.errors
+ ir.actions.act_window
+ form
+ new
+
+
+
+
+
+
diff --git a/requirements.txt b/requirements.txt
index 7e0b84839..fe14d9447 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ apispec
apispec>=4.0.0
cerberus
contextvars
+cryptography
extendable-pydantic
extendable-pydantic>=1.2.0
extendable>=0.0.4
diff --git a/setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors b/setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors
new file mode 120000
index 000000000..101a9234a
--- /dev/null
+++ b/setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors
@@ -0,0 +1 @@
+../../../../fastapi_encrypted_errors
\ No newline at end of file
diff --git a/setup/fastapi_encrypted_errors/setup.py b/setup/fastapi_encrypted_errors/setup.py
new file mode 100644
index 000000000..28c57bb64
--- /dev/null
+++ b/setup/fastapi_encrypted_errors/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
From 983b2ebbee9dfa6d8a8d34bfdda45e8f597f734b Mon Sep 17 00:00:00 2001
From: Florian Mounier
Date: Wed, 26 Feb 2025 19:18:35 +0100
Subject: [PATCH 08/58] [FIX] fastapi: headers is not a dict, it can have
multiple values for a key
---
fastapi/fastapi_dispatcher.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py
index bfb56825c..096293158 100644
--- a/fastapi/fastapi_dispatcher.py
+++ b/fastapi/fastapi_dispatcher.py
@@ -51,7 +51,7 @@ def handle_error(self, exc):
def _make_response(self, status_mapping, headers_tuple, content):
self.status = status_mapping[:3]
- self.headers = dict(headers_tuple)
+ self.headers = headers_tuple
# in case of exception, the method asgi_done_callback of the
# ASGIResponder will trigger an "a2wsgi.error" event with the exception
# instance stored in a tuple with the type of the exception and the traceback.
From 5026f7b12d6d738fcb9302229828db7e7e7f7b6c Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Fri, 14 Mar 2025 13:37:41 +0100
Subject: [PATCH 09/58] [FIX] fastapi: remove useless code
---
fastapi/pools/fastapi_app.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/fastapi/pools/fastapi_app.py b/fastapi/pools/fastapi_app.py
index 991374179..29725c49c 100644
--- a/fastapi/pools/fastapi_app.py
+++ b/fastapi/pools/fastapi_app.py
@@ -76,7 +76,6 @@ def __get_app(self, env: Environment, root_path: str) -> FastAPI:
try:
return pool.get_nowait()
except queue.Empty:
- env["fastapi.endpoint"].sudo()
return env["fastapi.endpoint"].sudo().get_app(root_path)
def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None:
From 1e73351275c1f54f04020300362a93f810446dd5 Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Fri, 14 Mar 2025 12:47:19 +0000
Subject: [PATCH 10/58] [BOT] post-merge updates
---
README.md | 2 +-
fastapi/README.rst | 2 +-
fastapi/__manifest__.py | 2 +-
fastapi/static/description/index.html | 186 +++++++++++++-------------
4 files changed, 96 insertions(+), 96 deletions(-)
diff --git a/README.md b/README.md
index e941e7b5f..f4b0e7f4c 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ addon | version | maintainers | summary
[datamodel](datamodel/) | 16.0.1.0.1 | [](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization
[extendable](extendable/) | 16.0.1.0.2 | [](https://github.com/lmignon) | Extendable classes registry loader for Odoo
[extendable_fastapi](extendable_fastapi/) | 16.0.2.1.1 | [](https://github.com/lmignon) | Allows the use of extendable into fastapi apps
-[fastapi](fastapi/) | 16.0.1.4.5 | [](https://github.com/lmignon) | Odoo FastAPI endpoint
+[fastapi](fastapi/) | 16.0.1.5.0 | [](https://github.com/lmignon) | Odoo FastAPI endpoint
[fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | [](https://github.com/sbidoul) | JWT bearer token authentication for FastAPI.
[fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | [](https://github.com/sbidoul) | Test/demo module for fastapi_auth_jwt.
[graphql_base](graphql_base/) | 16.0.1.0.1 | [](https://github.com/sbidoul) | Base GraphQL/GraphiQL controller
diff --git a/fastapi/README.rst b/fastapi/README.rst
index 574ecc086..be5dbc0f1 100644
--- a/fastapi/README.rst
+++ b/fastapi/README.rst
@@ -7,7 +7,7 @@ Odoo FastAPI
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:ccbcb06116d31f370fa16dda9fb82b273ff770e72e77a346c20ef68a4150500f
+ !! source digest: sha256:f2c0f5d73971853a3588843d02c9deffd2bce00bfbdcaac73ac9c11851e7a9cc
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py
index fa0e6a0d4..db63f08d5 100644
--- a/fastapi/__manifest__.py
+++ b/fastapi/__manifest__.py
@@ -5,7 +5,7 @@
"name": "Odoo FastAPI",
"summary": """
Odoo FastAPI endpoint""",
- "version": "16.0.1.4.5",
+ "version": "16.0.1.5.0",
"license": "LGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"maintainers": ["lmignon"],
diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html
index 3e40636da..562ec9d8f 100644
--- a/fastapi/static/description/index.html
+++ b/fastapi/static/description/index.html
@@ -367,7 +367,7 @@
Odoo FastAPI
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:ccbcb06116d31f370fa16dda9fb82b273ff770e72e77a346c20ef68a4150500f
+!! source digest: sha256:f2c0f5d73971853a3588843d02c9deffd2bce00bfbdcaac73ac9c11851e7a9cc
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
This addon provides the basis to smoothly integrate the FastAPI
@@ -505,9 +505,9 @@
The authentication mechanism<
authentication by using an api key or via basic auth. Since basic auth is already
implemented, we will only implement the api key authentication mechanism.
The authentication mechanism<
can allows the user to select one of these authentication mechanisms by adding
a selection field on the fastapi endpoint model.
string="Authenciation method",)
-def_get_app(self)->FastAPI:
+def_get_app(self)->FastAPI:app=super()._get_app()ifself.app=="demo":# Here we add the overrides to the authenticated_partner_impl method
@@ -926,10 +926,10 @@
This addon provides the basis to smoothly integrate the FastAPI
From b0f142a9357664e634a47e8c0a71bc2c9fa3a45c Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Thu, 10 Apr 2025 10:59:52 +0200
Subject: [PATCH 13/58] [IMP] base_rest_demo: Test to validate the retrying
mechanism
Add a new test to ensure that if a retryable error occurs when processing a call to a base_rest endpoint, the retrying mechanism of odoo works as expected
---
base_rest_demo/services/exception_services.py | 42 +++++++++++++++++++
base_rest_demo/tests/test_exception.py | 14 +++++++
2 files changed, 56 insertions(+)
diff --git a/base_rest_demo/services/exception_services.py b/base_rest_demo/services/exception_services.py
index afc39988b..d07d02774 100644
--- a/base_rest_demo/services/exception_services.py
+++ b/base_rest_demo/services/exception_services.py
@@ -1,6 +1,8 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+from psycopg2 import errorcodes
+from psycopg2.errors import OperationalError
from werkzeug.exceptions import MethodNotAllowed
from odoo import _
@@ -12,9 +14,13 @@
ValidationError,
)
from odoo.http import SessionExpiredException
+from odoo.service.model import MAX_TRIES_ON_CONCURRENCY_FAILURE
+from odoo.addons.base_rest.components.service import to_int
from odoo.addons.component.core import Component
+_CPT_RETRY = 0
+
class ExceptionService(Component):
_inherit = "base.rest.service"
@@ -90,6 +96,23 @@ def bare_exception(self):
"""
raise IOError("My IO error")
+ def retryable_error(self, nbr_retries):
+ """This method is used in the test suite to check that the retrying
+ functionality in case of concurrency error on the database is working
+ correctly for retryable exceptions.
+
+ The output will be the number of retries that have been done.
+
+ This method is mainly used to test the retrying functionality
+ """
+ global _CPT_RETRY
+ if _CPT_RETRY < nbr_retries:
+ _CPT_RETRY += 1
+ raise FakeConcurrentUpdateError("fake error")
+ tryno = _CPT_RETRY
+ _CPT_RETRY = 0
+ return {"retries": tryno}
+
# Validator
def _validator_user_error(self):
return {}
@@ -138,3 +161,22 @@ def _validator_bare_exception(self):
def _validator_return_bare_exception(self):
return {}
+
+ def _validator_retryable_error(self):
+ return {
+ "nbr_retries": {
+ "type": "integer",
+ "required": True,
+ "default": MAX_TRIES_ON_CONCURRENCY_FAILURE,
+ "coerce": to_int,
+ }
+ }
+
+ def _validator_return_retryable_error(self):
+ return {"retries": {"type": "integer"}}
+
+
+class FakeConcurrentUpdateError(OperationalError):
+ @property
+ def pgcode(self):
+ return errorcodes.SERIALIZATION_FAILURE
diff --git a/base_rest_demo/tests/test_exception.py b/base_rest_demo/tests/test_exception.py
index 3f5aa134d..9d955e914 100644
--- a/base_rest_demo/tests/test_exception.py
+++ b/base_rest_demo/tests/test_exception.py
@@ -104,3 +104,17 @@ def test_bare_exception(self):
self.assertEqual(response.headers["content-type"], "application/json")
body = json.loads(response.content.decode("utf-8"))
self.assertDictEqual(body, {"code": 500, "name": "Internal Server Error"})
+
+ @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
+ def test_retrying(self):
+ """Test that the retrying mechanism is working as expected with the
+ FastAPI endpoints in case of POST request with a file.
+ """
+ nbr_retries = 3
+ response = self.url_open(
+ "%s/retryable_error" % self.url,
+ '{"nbr_retries": %d}' % nbr_retries,
+ timeout=20000,
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertDictEqual(response.json(), {"retries": nbr_retries})
From a3bae8c57c493d35843779b0475cc06ba9a0206f Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Thu, 10 Apr 2025 11:04:50 +0200
Subject: [PATCH 14/58] [FIX] rest_log: Don't prevent the retrying mechanism
In case of a retryable error, the initial error must bubble up to the retrying mechanism. If this kind of error is wrapped into another one, the retrying mechanism no more works
---
rest_log/components/service.py | 11 ++++++
rest_log/tests/common.py | 10 +++++
rest_log/tests/test_db_logging.py | 65 ++++++++++++++++++++++++++++++-
3 files changed, 85 insertions(+), 1 deletion(-)
diff --git a/rest_log/components/service.py b/rest_log/components/service.py
index bbb45503f..9aedbf11b 100644
--- a/rest_log/components/service.py
+++ b/rest_log/components/service.py
@@ -7,10 +7,12 @@
import logging
import traceback
+from psycopg2.errors import OperationalError
from werkzeug.urls import url_encode, url_join
from odoo import exceptions, registry
from odoo.http import Response, request
+from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY
from odoo.addons.base_rest.http import JSONEncoder
from odoo.addons.component.core import AbstractComponent
@@ -111,6 +113,15 @@ def _dispatch_exception(
log_entry_url = self._get_log_entry_url(log_entry)
except Exception as e:
_logger.exception("Rest Log Error Creation: %s", e)
+ # let the OperationalError bubble up to the retrying mechanism
+ # We can't wrap the OperationalError because we want to let it
+ # bubble up to the retrying mechanism, it will be handled by
+ # the default handler at the end of the chain.
+ if (
+ isinstance(orig_exception, OperationalError)
+ and orig_exception.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY
+ ):
+ raise orig_exception
raise exception_klass(exc_msg, log_entry_url) from orig_exception
def _get_exception_message(self, exception):
diff --git a/rest_log/tests/common.py b/rest_log/tests/common.py
index f43a4db47..63e16bd01 100644
--- a/rest_log/tests/common.py
+++ b/rest_log/tests/common.py
@@ -4,6 +4,9 @@
import contextlib
+from psycopg2 import errorcodes
+from psycopg2.errors import OperationalError
+
from odoo import exceptions
from odoo.addons.base_rest import restapi
@@ -42,6 +45,7 @@ def fail(self, how):
"value": ValueError,
"validation": exceptions.ValidationError,
"user": exceptions.UserError,
+ "retryable": FakeConcurrentUpdateError,
}
raise exc[how]("Failed as you wanted!")
@@ -61,3 +65,9 @@ def _get_mocked_request(self, env=None, httprequest=None, extra_headers=None):
headers.update(extra_headers or {})
mocked_request.httprequest.headers = headers
yield mocked_request
+
+
+class FakeConcurrentUpdateError(OperationalError):
+ @property
+ def pgcode(self):
+ return errorcodes.SERIALIZATION_FAILURE
diff --git a/rest_log/tests/test_db_logging.py b/rest_log/tests/test_db_logging.py
index 01c1991ae..998f0c8e0 100644
--- a/rest_log/tests/test_db_logging.py
+++ b/rest_log/tests/test_db_logging.py
@@ -13,7 +13,7 @@
from odoo.addons.component.tests.common import new_rollbacked_env
from odoo.addons.rest_log import exceptions as log_exceptions # pylint: disable=W7950
-from .common import TestDBLoggingMixin
+from .common import FakeConcurrentUpdateError, TestDBLoggingMixin
class TestDBLogging(TransactionRestServiceRegistryCase, TestDBLoggingMixin):
@@ -374,3 +374,66 @@ def test_log_exception_value(self):
self._test_exception(
"value", log_exceptions.RESTServiceDispatchException, "ValueError", "severe"
)
+
+
+class TestDBLoggingRetryableError(
+ TransactionRestServiceRegistryCase, TestDBLoggingMixin
+):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls._setup_registry(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ # pylint: disable=W8110
+ cls._teardown_registry(cls)
+ super().tearDownClass()
+
+ def _test_exception(self, test_type, wrapping_exc, exc_name, severity):
+ log_model = self.env["rest.log"].sudo()
+ initial_entries = log_model.search([])
+ # Context: we are running in a transaction case which uses savepoints.
+ # The log machinery is going to rollback the transation when catching errors.
+ # Hence we need a completely separated env for the service.
+ with new_rollbacked_env() as new_env:
+ # Init fake collection w/ new env
+ collection = _PseudoCollection(self._collection_name, new_env)
+ service = self._get_service(self, collection=collection)
+ with self._get_mocked_request(env=new_env):
+ try:
+ service.dispatch("fail", test_type)
+ except Exception as err:
+ # Not using `assertRaises` to inspect the exception directly
+ self.assertTrue(isinstance(err, wrapping_exc))
+ self.assertEqual(
+ service._get_exception_message(err), "Failed as you wanted!"
+ )
+
+ with new_rollbacked_env() as new_env:
+ log_model = new_env["rest.log"].sudo()
+ entry = log_model.search([]) - initial_entries
+ expected = {
+ "collection": service._collection,
+ "state": "failed",
+ "result": "null",
+ "exception_name": exc_name,
+ "exception_message": "Failed as you wanted!",
+ "severity": severity,
+ }
+ self.assertRecordValues(entry, [expected])
+
+ @staticmethod
+ def _get_test_controller(class_or_instance, root_path=None):
+ return super()._get_test_controller(
+ class_or_instance, root_path="/test_log_exception_retryable/"
+ )
+
+ def test_log_exception_retryable(self):
+ # retryable error must bubble up to the retrying mechanism
+ self._test_exception(
+ "retryable",
+ FakeConcurrentUpdateError,
+ "odoo.addons.rest_log.tests.common.FakeConcurrentUpdateError",
+ "warning",
+ )
From 90ae5a4dabe491bd294a767ec5aa9d5e959249dd Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Thu, 10 Apr 2025 09:50:09 +0000
Subject: [PATCH 15/58] [BOT] post-merge updates
---
README.md | 2 +-
fastapi/README.rst | 2 +-
fastapi/__manifest__.py | 2 +-
fastapi/static/description/index.html | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 668d37a41..83eb9f635 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ addon | version | maintainers | summary
[datamodel](datamodel/) | 16.0.1.0.1 | [](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization
[extendable](extendable/) | 16.0.1.0.2 | [](https://github.com/lmignon) | Extendable classes registry loader for Odoo
[extendable_fastapi](extendable_fastapi/) | 16.0.2.1.1 | [](https://github.com/lmignon) | Allows the use of extendable into fastapi apps
-[fastapi](fastapi/) | 16.0.1.5.1 | [](https://github.com/lmignon) | Odoo FastAPI endpoint
+[fastapi](fastapi/) | 16.0.1.6.0 | [](https://github.com/lmignon) | Odoo FastAPI endpoint
[fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | [](https://github.com/sbidoul) | JWT bearer token authentication for FastAPI.
[fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | [](https://github.com/sbidoul) | Test/demo module for fastapi_auth_jwt.
[graphql_base](graphql_base/) | 16.0.1.0.1 | [](https://github.com/sbidoul) | Base GraphQL/GraphiQL controller
diff --git a/fastapi/README.rst b/fastapi/README.rst
index 272e4806a..d12578ae5 100644
--- a/fastapi/README.rst
+++ b/fastapi/README.rst
@@ -7,7 +7,7 @@ Odoo FastAPI
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:9007cc333e60840b5aef872ea7ee577f48b513c8b56fedd8ab7ae0e0623c677e
+ !! source digest: sha256:1a9e09ed26e6e827518e1712413b5b62003b71f34e738c845ad3a0712505104f
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py
index 3ecda5d9f..2a62f9ca2 100644
--- a/fastapi/__manifest__.py
+++ b/fastapi/__manifest__.py
@@ -5,7 +5,7 @@
"name": "Odoo FastAPI",
"summary": """
Odoo FastAPI endpoint""",
- "version": "16.0.1.5.1",
+ "version": "16.0.1.6.0",
"license": "LGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"maintainers": ["lmignon"],
diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html
index e4bda532f..f7468b32d 100644
--- a/fastapi/static/description/index.html
+++ b/fastapi/static/description/index.html
@@ -367,7 +367,7 @@
Odoo FastAPI
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:9007cc333e60840b5aef872ea7ee577f48b513c8b56fedd8ab7ae0e0623c677e
+!! source digest: sha256:1a9e09ed26e6e827518e1712413b5b62003b71f34e738c845ad3a0712505104f
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
This addon provides the basis to smoothly integrate the FastAPI
From e0e48e3c555ccf4ce24b95655e1b1e1a6b59e247 Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Thu, 10 Apr 2025 11:21:03 +0000
Subject: [PATCH 16/58] [BOT] post-merge updates
---
README.md | 2 +-
extendable_fastapi/README.rst | 2 +-
extendable_fastapi/__manifest__.py | 2 +-
extendable_fastapi/static/description/index.html | 14 ++++++++------
4 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index 83eb9f635..36f9724b1 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ addon | version | maintainers | summary
[base_rest_pydantic](base_rest_pydantic/) | 16.0.2.0.1 | | Pydantic binding for base_rest
[datamodel](datamodel/) | 16.0.1.0.1 | [](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization
[extendable](extendable/) | 16.0.1.0.2 | [](https://github.com/lmignon) | Extendable classes registry loader for Odoo
-[extendable_fastapi](extendable_fastapi/) | 16.0.2.1.1 | [](https://github.com/lmignon) | Allows the use of extendable into fastapi apps
+[extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | [](https://github.com/lmignon) | Allows the use of extendable into fastapi apps
[fastapi](fastapi/) | 16.0.1.6.0 | [](https://github.com/lmignon) | Odoo FastAPI endpoint
[fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | [](https://github.com/sbidoul) | JWT bearer token authentication for FastAPI.
[fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | [](https://github.com/sbidoul) | Test/demo module for fastapi_auth_jwt.
diff --git a/extendable_fastapi/README.rst b/extendable_fastapi/README.rst
index 4213d94dc..2a93a308f 100644
--- a/extendable_fastapi/README.rst
+++ b/extendable_fastapi/README.rst
@@ -7,7 +7,7 @@ Extendable Fastapi
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:4e4f5d96294f860ce7f0c4e023431f8ed9ca011c318b5ba4a3cfcd15c31eac1a
+ !! source digest: sha256:1c3abf7259ae8390271785fb3b8f7754dadf175dc1530a27386ae9a77635e4b2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/extendable_fastapi/__manifest__.py b/extendable_fastapi/__manifest__.py
index 56bb68bc1..44bc64aa1 100644
--- a/extendable_fastapi/__manifest__.py
+++ b/extendable_fastapi/__manifest__.py
@@ -5,7 +5,7 @@
"name": "Extendable Fastapi",
"summary": """
Allows the use of extendable into fastapi apps""",
- "version": "16.0.2.1.1",
+ "version": "16.0.2.1.2",
"license": "LGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"maintainers": ["lmignon"],
diff --git a/extendable_fastapi/static/description/index.html b/extendable_fastapi/static/description/index.html
index 88bfd690b..fdf779b2d 100644
--- a/extendable_fastapi/static/description/index.html
+++ b/extendable_fastapi/static/description/index.html
@@ -1,4 +1,3 @@
-
@@ -9,10 +8,11 @@
/*
:Author: David Goodger (goodger@python.org)
-:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
+:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
+Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
@@ -275,7 +275,7 @@
margin-left: 2em ;
margin-right: 2em }
-pre.code .ln { color: grey; } /* line numbers */
+pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
@@ -301,7 +301,7 @@
span.pre {
white-space: pre }
-span.problematic {
+span.problematic, pre.problematic {
color: red }
span.section-subtitle {
@@ -367,7 +367,7 @@
Extendable Fastapi
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:4e4f5d96294f860ce7f0c4e023431f8ed9ca011c318b5ba4a3cfcd15c31eac1a
+!! source digest: sha256:1c3abf7259ae8390271785fb3b8f7754dadf175dc1530a27386ae9a77635e4b2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
This addon is a technical addon used to allows the use of
@@ -443,7 +443,9 @@
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
diff --git a/requirements.txt b/requirements.txt
index 7e0b84839..bf2e66750 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,6 +13,7 @@ graphql_server
jsondiff
marshmallow
marshmallow-objects>=2.0.0
+marshmallow<4.0.0
parse-accept-language
pydantic
pydantic>=2.0.0
From 3285f490f525be57530e89be2b55b7410ad4c724 Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Wed, 14 May 2025 13:34:04 +0000
Subject: [PATCH 20/58] [BOT] post-merge updates
---
README.md | 2 +-
datamodel/README.rst | 2 +-
datamodel/__manifest__.py | 2 +-
datamodel/static/description/index.html | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 36f9724b1..6cba02a1a 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ addon | version | maintainers | summary
[base_rest_datamodel](base_rest_datamodel/) | 16.0.1.0.0 | | Datamodel binding for base_rest
[base_rest_demo](base_rest_demo/) | 16.0.2.0.2 | [](https://github.com/lmignon) | Demo addon for Base REST
[base_rest_pydantic](base_rest_pydantic/) | 16.0.2.0.1 | | Pydantic binding for base_rest
-[datamodel](datamodel/) | 16.0.1.0.1 | [](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization
+[datamodel](datamodel/) | 16.0.1.0.2 | [](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization
[extendable](extendable/) | 16.0.1.0.2 | [](https://github.com/lmignon) | Extendable classes registry loader for Odoo
[extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | [](https://github.com/lmignon) | Allows the use of extendable into fastapi apps
[fastapi](fastapi/) | 16.0.1.6.0 | [](https://github.com/lmignon) | Odoo FastAPI endpoint
diff --git a/datamodel/README.rst b/datamodel/README.rst
index 1209730ea..bad3f8c95 100644
--- a/datamodel/README.rst
+++ b/datamodel/README.rst
@@ -7,7 +7,7 @@ Datamodel
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:5411d4f742eb933a4d05f5f6e1784a7ddc042e7f22b1c08d535e225c306a6955
+ !! source digest: sha256:220702c6d930c27e2dbb4016e1f6bef6e1b9c7b96cff4b3c5ad27fdc5733ad26
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py
index c8fd79a1f..97b1a6310 100644
--- a/datamodel/__manifest__.py
+++ b/datamodel/__manifest__.py
@@ -6,7 +6,7 @@
"summary": """
This addon allows you to define simple data models supporting
serialization/deserialization""",
- "version": "16.0.1.0.1",
+ "version": "16.0.1.0.2",
"license": "LGPL-3",
"development_status": "Beta",
"author": "ACSONE SA/NV, " "Odoo Community Association (OCA)",
diff --git a/datamodel/static/description/index.html b/datamodel/static/description/index.html
index fca018e56..b290f07ce 100644
--- a/datamodel/static/description/index.html
+++ b/datamodel/static/description/index.html
@@ -367,7 +367,7 @@
Datamodel
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:5411d4f742eb933a4d05f5f6e1784a7ddc042e7f22b1c08d535e225c306a6955
+!! source digest: sha256:220702c6d930c27e2dbb4016e1f6bef6e1b9c7b96cff4b3c5ad27fdc5733ad26
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
This addon allows you to define simple data models supporting serialization/deserialization
From 01013f6f5ce27f3ec1ec9a1e019b623272679d79 Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Wed, 14 May 2025 13:37:43 +0000
Subject: [PATCH 21/58] [BOT] post-merge updates
---
README.md | 4 ++--
base_rest_demo/README.rst | 2 +-
base_rest_demo/__manifest__.py | 2 +-
base_rest_demo/static/description/index.html | 14 ++++++++------
rest_log/README.rst | 2 +-
rest_log/__manifest__.py | 2 +-
rest_log/static/description/index.html | 2 +-
7 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 6cba02a1a..646fcee39 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ addon | version | maintainers | summary
[base_rest](base_rest/) | 16.0.1.0.3 | | Develop your own high level REST APIs for Odoo thanks to this addon.
[base_rest_auth_api_key](base_rest_auth_api_key/) | 16.0.1.0.0 | [](https://github.com/lmignon) | Base Rest: Add support for the auth_api_key security policy into the openapi documentation
[base_rest_datamodel](base_rest_datamodel/) | 16.0.1.0.0 | | Datamodel binding for base_rest
-[base_rest_demo](base_rest_demo/) | 16.0.2.0.2 | [](https://github.com/lmignon) | Demo addon for Base REST
+[base_rest_demo](base_rest_demo/) | 16.0.2.0.3 | [](https://github.com/lmignon) | Demo addon for Base REST
[base_rest_pydantic](base_rest_pydantic/) | 16.0.2.0.1 | | Pydantic binding for base_rest
[datamodel](datamodel/) | 16.0.1.0.2 | [](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization
[extendable](extendable/) | 16.0.1.0.2 | [](https://github.com/lmignon) | Extendable classes registry loader for Odoo
@@ -35,7 +35,7 @@ addon | version | maintainers | summary
[graphql_base](graphql_base/) | 16.0.1.0.1 | [](https://github.com/sbidoul) | Base GraphQL/GraphiQL controller
[graphql_demo](graphql_demo/) | 16.0.1.0.1 | [](https://github.com/sbidoul) | GraphQL Demo
[pydantic](pydantic/) | 16.0.1.0.0 | [](https://github.com/lmignon) | Utility addon to ease mapping between Pydantic and Odoo models
-[rest_log](rest_log/) | 16.0.1.0.2 | [](https://github.com/simahawk) | Track REST API calls into DB
+[rest_log](rest_log/) | 16.0.1.0.3 | [](https://github.com/simahawk) | Track REST API calls into DB
Unported addons
diff --git a/base_rest_demo/README.rst b/base_rest_demo/README.rst
index fe4048f21..ad4e0eae6 100644
--- a/base_rest_demo/README.rst
+++ b/base_rest_demo/README.rst
@@ -7,7 +7,7 @@ Base Rest Demo
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:50ac989c1e6343bcb3f97a2f8df7392224cb7d06989f88fba9a14a17ae40d3dd
+ !! source digest: sha256:d55a089f3426610ea4b4762a7e51b8572ae0852a03bf126d2eb6ff4a5c1451f9
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/base_rest_demo/__manifest__.py b/base_rest_demo/__manifest__.py
index 3a161363d..a9efda8c8 100644
--- a/base_rest_demo/__manifest__.py
+++ b/base_rest_demo/__manifest__.py
@@ -5,7 +5,7 @@
"name": "Base Rest Demo",
"summary": """
Demo addon for Base REST""",
- "version": "16.0.2.0.2",
+ "version": "16.0.2.0.3",
"development_status": "Beta",
"license": "LGPL-3",
"author": "ACSONE SA/NV, " "Odoo Community Association (OCA)",
diff --git a/base_rest_demo/static/description/index.html b/base_rest_demo/static/description/index.html
index 58bb0ebf0..3c5ed59ae 100644
--- a/base_rest_demo/static/description/index.html
+++ b/base_rest_demo/static/description/index.html
@@ -1,4 +1,3 @@
-
@@ -9,10 +8,11 @@
/*
:Author: David Goodger (goodger@python.org)
-:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
+:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
+Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
@@ -275,7 +275,7 @@
margin-left: 2em ;
margin-right: 2em }
-pre.code .ln { color: grey; } /* line numbers */
+pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
@@ -301,7 +301,7 @@
span.pre {
white-space: pre }
-span.problematic {
+span.problematic, pre.problematic {
color: red }
span.section-subtitle {
@@ -367,7 +367,7 @@
Base Rest Demo
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:50ac989c1e6343bcb3f97a2f8df7392224cb7d06989f88fba9a14a17ae40d3dd
+!! source digest: sha256:d55a089f3426610ea4b4762a7e51b8572ae0852a03bf126d2eb6ff4a5c1451f9
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
Demo addon to illustrate how to develop self documented REST services thanks
@@ -442,7 +442,9 @@
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
diff --git a/rest_log/README.rst b/rest_log/README.rst
index d7dcae803..dfed37602 100644
--- a/rest_log/README.rst
+++ b/rest_log/README.rst
@@ -7,7 +7,7 @@ REST Log
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:f93e58dbd77af1254adb0cd7ace54af0b0609569883ed4a22028f11f0b663139
+ !! source digest: sha256:ed7eb7cec756c78c39eb3dc04ca382ea64926defada6d97aa72e859db389d8b2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/rest_log/__manifest__.py b/rest_log/__manifest__.py
index 60efce45b..478bee1cd 100644
--- a/rest_log/__manifest__.py
+++ b/rest_log/__manifest__.py
@@ -5,7 +5,7 @@
{
"name": "REST Log",
"summary": "Track REST API calls into DB",
- "version": "16.0.1.0.2",
+ "version": "16.0.1.0.3",
"development_status": "Beta",
"website": "https://github.com/OCA/rest-framework",
"author": "Camptocamp, ACSONE, Odoo Community Association (OCA)",
diff --git a/rest_log/static/description/index.html b/rest_log/static/description/index.html
index 3c9f139e5..56f26b2bf 100644
--- a/rest_log/static/description/index.html
+++ b/rest_log/static/description/index.html
@@ -367,7 +367,7 @@
REST Log
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:f93e58dbd77af1254adb0cd7ace54af0b0609569883ed4a22028f11f0b663139
+!! source digest: sha256:ed7eb7cec756c78c39eb3dc04ca382ea64926defada6d97aa72e859db389d8b2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
When exposing REST services is often useful to see what’s happening
From ed9d8bc23a81cda171010e77f759353a613fb123 Mon Sep 17 00:00:00 2001
From: PicchiSeba
Date: Thu, 27 Feb 2025 12:42:09 +0100
Subject: [PATCH 22/58] [IMP]fastapi: obtain endpoint via common method
---
fastapi/fastapi_dispatcher.py | 6 +++---
fastapi/models/fastapi_endpoint.py | 17 ++++++++++++-----
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py
index c41beb2d2..63aedd4f4 100644
--- a/fastapi/fastapi_dispatcher.py
+++ b/fastapi/fastapi_dispatcher.py
@@ -27,11 +27,11 @@ def dispatch(self, endpoint, args):
# don't parse the httprequest let starlette parse the stream
self.request.params = {} # dict(self.request.get_http_params(), **args)
environ = self._get_environ()
- root_path = "/" + environ["PATH_INFO"].split("/")[1]
+ path = environ["PATH_INFO"]
# TODO store the env into contextvar to be used by the odoo_env
# depends method
- with fastapi_app_pool.get_app(env=request.env, root_path=root_path) as app:
- uid = request.env["fastapi.endpoint"].sudo().get_uid(root_path)
+ with fastapi_app_pool.get_app(env=request.env, root_path=path) as app:
+ uid = request.env["fastapi.endpoint"].sudo().get_uid(path)
data = BytesIO()
with self._manage_odoo_env(uid):
for r in app(environ, self._make_response):
diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py
index 25e3dc468..45dd13319 100644
--- a/fastapi/models/fastapi_endpoint.py
+++ b/fastapi/models/fastapi_endpoint.py
@@ -211,8 +211,15 @@ def _reset_app_cache_marker(self):
"""
@api.model
- def get_app(self, root_path):
- record = self.search([("root_path", "=", root_path)])
+ @tools.ormcache("path")
+ def get_endpoint(self, path):
+ root_path = "/" + path.split("/")[1]
+ endpoint = self.search([("root_path", "=", root_path)])[:1] or False
+ return endpoint
+
+ @api.model
+ def get_app(self, path):
+ record = self.get_endpoint(path)
if not record:
return None
app = FastAPI()
@@ -236,9 +243,9 @@ def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None:
self._clear_fastapi_exception_handlers(route.app)
@api.model
- @tools.ormcache("root_path")
- def get_uid(self, root_path):
- record = self.search([("root_path", "=", root_path)])
+ @tools.ormcache("path")
+ def get_uid(self, path):
+ record = self.get_endpoint(path)
if not record:
return None
return record.user_id.id
From ebd6afe74e7b5029c3a594ba14dabb20dbca6c61 Mon Sep 17 00:00:00 2001
From: jesusvmayor
Date: Mon, 26 May 2025 20:00:43 +0200
Subject: [PATCH 23/58] [FIX]base_rest: Fix rest controllers inheritance.
---
base_rest/controllers/main.py | 7 -------
base_rest_demo/tests/test_controller.py | 21 ++++++++-------------
2 files changed, 8 insertions(+), 20 deletions(-)
diff --git a/base_rest/controllers/main.py b/base_rest/controllers/main.py
index b4166d741..a768fb70f 100644
--- a/base_rest/controllers/main.py
+++ b/base_rest/controllers/main.py
@@ -96,13 +96,6 @@ class ControllerB(ControllerB):
@classmethod
def __init_subclass__(cls):
- if (
- "RestController" in globals()
- and RestController in cls.__bases__
- and Controller not in cls.__bases__
- ):
- # Ensure that Controller's __init_subclass__ kicks in.
- cls.__bases__ += (Controller,)
super().__init_subclass__()
if "RestController" not in globals() or not any(
issubclass(b, RestController) for b in cls.__bases__
diff --git a/base_rest_demo/tests/test_controller.py b/base_rest_demo/tests/test_controller.py
index 678e52618..43a84f489 100644
--- a/base_rest_demo/tests/test_controller.py
+++ b/base_rest_demo/tests/test_controller.py
@@ -18,20 +18,15 @@ def test_controller_registry(self):
# at the end of the start process, our tow controllers must into the
# controller registered
controllers = Controller.children_classes.get("base_rest_demo", [])
-
- self.assertIn(
- BaseRestDemoPrivateApiController,
- controllers,
+ self.assertTrue(
+ any([issubclass(x, BaseRestDemoPrivateApiController) for x in controllers])
)
- self.assertIn(
- BaseRestDemoPublicApiController,
- controllers,
+ self.assertTrue(
+ any([issubclass(x, BaseRestDemoPublicApiController) for x in controllers])
)
- self.assertIn(
- BaseRestDemoNewApiController,
- controllers,
+ self.assertTrue(
+ any([issubclass(x, BaseRestDemoNewApiController) for x in controllers])
)
- self.assertIn(
- BaseRestDemoJwtApiController,
- controllers,
+ self.assertTrue(
+ any([issubclass(x, BaseRestDemoJwtApiController) for x in controllers])
)
From c1855a0c01595b0c21127293ff75a48c0541837b Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Wed, 28 May 2025 09:00:14 +0000
Subject: [PATCH 24/58] [BOT] post-merge updates
---
README.md | 4 +-
base_rest/README.rst | 2 +-
base_rest/__manifest__.py | 2 +-
base_rest/static/description/index.html | 60 ++++++++++----------
base_rest_demo/README.rst | 2 +-
base_rest_demo/__manifest__.py | 2 +-
base_rest_demo/static/description/index.html | 2 +-
7 files changed, 37 insertions(+), 37 deletions(-)
diff --git a/README.md b/README.md
index 646fcee39..8a995725f 100644
--- a/README.md
+++ b/README.md
@@ -21,10 +21,10 @@ Available addons
----------------
addon | version | maintainers | summary
--- | --- | --- | ---
-[base_rest](base_rest/) | 16.0.1.0.3 | | Develop your own high level REST APIs for Odoo thanks to this addon.
+[base_rest](base_rest/) | 16.0.1.0.4 | | Develop your own high level REST APIs for Odoo thanks to this addon.
[base_rest_auth_api_key](base_rest_auth_api_key/) | 16.0.1.0.0 | [](https://github.com/lmignon) | Base Rest: Add support for the auth_api_key security policy into the openapi documentation
[base_rest_datamodel](base_rest_datamodel/) | 16.0.1.0.0 | | Datamodel binding for base_rest
-[base_rest_demo](base_rest_demo/) | 16.0.2.0.3 | [](https://github.com/lmignon) | Demo addon for Base REST
+[base_rest_demo](base_rest_demo/) | 16.0.2.0.4 | [](https://github.com/lmignon) | Demo addon for Base REST
[base_rest_pydantic](base_rest_pydantic/) | 16.0.2.0.1 | | Pydantic binding for base_rest
[datamodel](datamodel/) | 16.0.1.0.2 | [](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization
[extendable](extendable/) | 16.0.1.0.2 | [](https://github.com/lmignon) | Extendable classes registry loader for Odoo
diff --git a/base_rest/README.rst b/base_rest/README.rst
index 0bac51c24..3556e64b2 100644
--- a/base_rest/README.rst
+++ b/base_rest/README.rst
@@ -7,7 +7,7 @@ Base Rest
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:35fb4211e3cdea0ba35463860d4d0cc7bc2909ab39e5b5af9a6733ced5b96232
+ !! source digest: sha256:517d5b1d74542047b404d2130e5d9239fe591f43b1a89ca02339766c8c8a6584
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/base_rest/__manifest__.py b/base_rest/__manifest__.py
index 27f38e2c5..1b6d113ad 100644
--- a/base_rest/__manifest__.py
+++ b/base_rest/__manifest__.py
@@ -6,7 +6,7 @@
"summary": """
Develop your own high level REST APIs for Odoo thanks to this addon.
""",
- "version": "16.0.1.0.3",
+ "version": "16.0.1.0.4",
"development_status": "Beta",
"license": "LGPL-3",
"author": "ACSONE SA/NV, " "Odoo Community Association (OCA)",
diff --git a/base_rest/static/description/index.html b/base_rest/static/description/index.html
index 459de1d5e..b26ae50ef 100644
--- a/base_rest/static/description/index.html
+++ b/base_rest/static/description/index.html
@@ -367,7 +367,7 @@
Base Rest
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:35fb4211e3cdea0ba35463860d4d0cc7bc2909ab39e5b5af9a6733ced5b96232
+!! source digest: sha256:517d5b1d74542047b404d2130e5d9239fe591f43b1a89ca02339766c8c8a6584
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
This addon is deprecated and not fully supported anymore on Odoo 16.
@@ -450,10 +450,10 @@
Other methods are only accessible via HTTP POST routes <string:_service_name> or <string:_service_name>/<string:method_name> or <string:_service_name>/<int:_id> or <string:_service_name>/<int:_id>/<string:method_name>
# The following method are 'public' and can be called from the controller.
-defget(self,_id,message):
+defget(self,_id,message):return{'response':'Get called with message '+message}
-defsearch(self,message):
+defsearch(self,message):return{'response':'Search called search with message '+message}
-defupdate(self,_id,message):
+defupdate(self,_id,message):return{'response':'PUT called with message '+message}# pylint:disable=method-required-super
-defcreate(self,**params):
+defcreate(self,**params):return{'response':'POST called with message '+params['message']}
-defdelete(self,_id):
+defdelete(self,_id):return{'response':'DELETE called with id %s '%_id}# Validator
-def_validator_search(self):
+def_validator_search(self):return{'message':{'type':'string'}}# Validator
-def_validator_get(self):
+def_validator_get(self):# no parameters by defaultreturn{}
-def_validator_update(self):
+def_validator_update(self):return{'message':{'type':'string'}}
-def_validator_create(self):
+def_validator_create(self):return{'message':{'type':'string'}}
Once you have implemented your services (ping, …), you must tell to Odoo
how to access to these services. This process is done by implementing a
controller that inherits from odoo.addons.base_rest.controllers.main.RestController
output_param=restapi.Datamodel("partner.short.info",is_list=True),auth="public",)
-defsearch(self,partner_search_param):
+defsearch(self,partner_search_param):"""
Search for partners
:param partner_search_param: An instance of partner.search.param
diff --git a/base_rest_demo/README.rst b/base_rest_demo/README.rst
index ad4e0eae6..b07df2cc4 100644
--- a/base_rest_demo/README.rst
+++ b/base_rest_demo/README.rst
@@ -7,7 +7,7 @@ Base Rest Demo
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:d55a089f3426610ea4b4762a7e51b8572ae0852a03bf126d2eb6ff4a5c1451f9
+ !! source digest: sha256:18e5691a569699452f29ef673266f29b874bf5c931221af24817846eb59f85a1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/base_rest_demo/__manifest__.py b/base_rest_demo/__manifest__.py
index a9efda8c8..564a8536c 100644
--- a/base_rest_demo/__manifest__.py
+++ b/base_rest_demo/__manifest__.py
@@ -5,7 +5,7 @@
"name": "Base Rest Demo",
"summary": """
Demo addon for Base REST""",
- "version": "16.0.2.0.3",
+ "version": "16.0.2.0.4",
"development_status": "Beta",
"license": "LGPL-3",
"author": "ACSONE SA/NV, " "Odoo Community Association (OCA)",
diff --git a/base_rest_demo/static/description/index.html b/base_rest_demo/static/description/index.html
index 3c5ed59ae..62b5d66ed 100644
--- a/base_rest_demo/static/description/index.html
+++ b/base_rest_demo/static/description/index.html
@@ -367,7 +367,7 @@
Base Rest Demo
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:d55a089f3426610ea4b4762a7e51b8572ae0852a03bf126d2eb6ff4a5c1451f9
+!! source digest: sha256:18e5691a569699452f29ef673266f29b874bf5c931221af24817846eb59f85a1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
Demo addon to illustrate how to develop self documented REST services thanks
From bf65b2a3405f27248730d58d266a8452d567c96f Mon Sep 17 00:00:00 2001
From: Florian Mounier
Date: Mon, 2 Jun 2025 12:56:59 +0200
Subject: [PATCH 25/58] [ADD] auth_partner, fastapi_auth_partner
---
auth_partner/README.rst | 102 ++++
auth_partner/__init__.py | 2 +
auth_partner/__manifest__.py | 38 ++
auth_partner/data/email_data.xml | 67 +++
auth_partner/demo/auth_directory_demo.xml | 9 +
auth_partner/demo/auth_partner_demo.xml | 8 +
auth_partner/demo/res_partner_demo.xml | 7 +
auth_partner/models/__init__.py | 3 +
auth_partner/models/auth_directory.py | 209 ++++++++
auth_partner/models/auth_partner.py | 310 ++++++++++++
auth_partner/models/res_partner.py | 34 ++
auth_partner/readme/CONTRIBUTORS.rst | 4 +
auth_partner/readme/DESCRIPTION.rst | 12 +
auth_partner/readme/USAGE.rst | 8 +
auth_partner/security/ir.model.access.csv | 8 +
auth_partner/security/ir_rule.xml | 26 +
auth_partner/security/res_group.xml | 16 +
auth_partner/static/description/index.html | 447 ++++++++++++++++
auth_partner/tests/__init__.py | 1 +
auth_partner/tests/common.py | 60 +++
auth_partner/tests/test_auth_partner.py | 357 +++++++++++++
auth_partner/views/auth_directory_view.xml | 92 ++++
auth_partner/views/auth_partner_view.xml | 98 ++++
auth_partner/views/res_partner_view.xml | 22 +
auth_partner/wizards/__init__.py | 2 +
.../wizard_auth_partner_force_set_password.py | 37 ++
...d_auth_partner_force_set_password_view.xml | 38 ++
.../wizard_auth_partner_reset_password.py | 59 +++
...izard_auth_partner_reset_password_view.xml | 42 ++
fastapi_auth_partner/README.rst | 136 +++++
fastapi_auth_partner/__init__.py | 4 +
fastapi_auth_partner/__manifest__.py | 32 ++
.../demo/fastapi_endpoint_demo.xml | 26 +
fastapi_auth_partner/dependencies.py | 74 +++
fastapi_auth_partner/models/__init__.py | 3 +
fastapi_auth_partner/models/auth_directory.py | 51 ++
fastapi_auth_partner/models/auth_partner.py | 82 +++
.../models/fastapi_endpoint.py | 55 ++
fastapi_auth_partner/readme/CONTRIBUTORS.rst | 4 +
fastapi_auth_partner/readme/DESCRIPTION.rst | 2 +
fastapi_auth_partner/readme/USAGE.rst | 52 ++
fastapi_auth_partner/routers/__init__.py | 1 +
fastapi_auth_partner/routers/auth.py | 252 +++++++++
fastapi_auth_partner/schemas.py | 40 ++
.../security/ir.model.access.csv | 2 +
fastapi_auth_partner/security/res_group.xml | 13 +
.../static/description/index.html | 477 ++++++++++++++++++
fastapi_auth_partner/tests/__init__.py | 2 +
fastapi_auth_partner/tests/test_auth.py | 243 +++++++++
.../tests/test_fastapi_auth_partner_demo.py | 93 ++++
.../views/auth_directory_view.xml | 29 ++
.../views/auth_partner_view.xml | 31 ++
.../views/fastapi_endpoint_view.xml | 25 +
fastapi_auth_partner/wizards/__init__.py | 2 +
.../wizard_auth_partner_impersonate.py | 29 ++
.../wizard_auth_partner_impersonate_view.xml | 43 ++
.../wizard_auth_partner_reset_password.py | 18 +
...izard_auth_partner_reset_password_view.xml | 17 +
requirements.txt | 2 +
setup/auth_partner/odoo/addons/auth_partner | 1 +
setup/auth_partner/setup.py | 6 +
.../odoo/addons/fastapi_auth_partner | 1 +
setup/fastapi_auth_partner/setup.py | 6 +
63 files changed, 3972 insertions(+)
create mode 100644 auth_partner/README.rst
create mode 100644 auth_partner/__init__.py
create mode 100644 auth_partner/__manifest__.py
create mode 100644 auth_partner/data/email_data.xml
create mode 100644 auth_partner/demo/auth_directory_demo.xml
create mode 100644 auth_partner/demo/auth_partner_demo.xml
create mode 100644 auth_partner/demo/res_partner_demo.xml
create mode 100644 auth_partner/models/__init__.py
create mode 100644 auth_partner/models/auth_directory.py
create mode 100644 auth_partner/models/auth_partner.py
create mode 100644 auth_partner/models/res_partner.py
create mode 100644 auth_partner/readme/CONTRIBUTORS.rst
create mode 100644 auth_partner/readme/DESCRIPTION.rst
create mode 100644 auth_partner/readme/USAGE.rst
create mode 100644 auth_partner/security/ir.model.access.csv
create mode 100644 auth_partner/security/ir_rule.xml
create mode 100644 auth_partner/security/res_group.xml
create mode 100644 auth_partner/static/description/index.html
create mode 100644 auth_partner/tests/__init__.py
create mode 100644 auth_partner/tests/common.py
create mode 100644 auth_partner/tests/test_auth_partner.py
create mode 100644 auth_partner/views/auth_directory_view.xml
create mode 100644 auth_partner/views/auth_partner_view.xml
create mode 100644 auth_partner/views/res_partner_view.xml
create mode 100644 auth_partner/wizards/__init__.py
create mode 100644 auth_partner/wizards/wizard_auth_partner_force_set_password.py
create mode 100644 auth_partner/wizards/wizard_auth_partner_force_set_password_view.xml
create mode 100644 auth_partner/wizards/wizard_auth_partner_reset_password.py
create mode 100644 auth_partner/wizards/wizard_auth_partner_reset_password_view.xml
create mode 100644 fastapi_auth_partner/README.rst
create mode 100644 fastapi_auth_partner/__init__.py
create mode 100644 fastapi_auth_partner/__manifest__.py
create mode 100644 fastapi_auth_partner/demo/fastapi_endpoint_demo.xml
create mode 100644 fastapi_auth_partner/dependencies.py
create mode 100644 fastapi_auth_partner/models/__init__.py
create mode 100644 fastapi_auth_partner/models/auth_directory.py
create mode 100644 fastapi_auth_partner/models/auth_partner.py
create mode 100644 fastapi_auth_partner/models/fastapi_endpoint.py
create mode 100644 fastapi_auth_partner/readme/CONTRIBUTORS.rst
create mode 100644 fastapi_auth_partner/readme/DESCRIPTION.rst
create mode 100644 fastapi_auth_partner/readme/USAGE.rst
create mode 100644 fastapi_auth_partner/routers/__init__.py
create mode 100644 fastapi_auth_partner/routers/auth.py
create mode 100644 fastapi_auth_partner/schemas.py
create mode 100644 fastapi_auth_partner/security/ir.model.access.csv
create mode 100644 fastapi_auth_partner/security/res_group.xml
create mode 100644 fastapi_auth_partner/static/description/index.html
create mode 100644 fastapi_auth_partner/tests/__init__.py
create mode 100644 fastapi_auth_partner/tests/test_auth.py
create mode 100644 fastapi_auth_partner/tests/test_fastapi_auth_partner_demo.py
create mode 100644 fastapi_auth_partner/views/auth_directory_view.xml
create mode 100644 fastapi_auth_partner/views/auth_partner_view.xml
create mode 100644 fastapi_auth_partner/views/fastapi_endpoint_view.xml
create mode 100644 fastapi_auth_partner/wizards/__init__.py
create mode 100644 fastapi_auth_partner/wizards/wizard_auth_partner_impersonate.py
create mode 100644 fastapi_auth_partner/wizards/wizard_auth_partner_impersonate_view.xml
create mode 100644 fastapi_auth_partner/wizards/wizard_auth_partner_reset_password.py
create mode 100644 fastapi_auth_partner/wizards/wizard_auth_partner_reset_password_view.xml
create mode 120000 setup/auth_partner/odoo/addons/auth_partner
create mode 100644 setup/auth_partner/setup.py
create mode 120000 setup/fastapi_auth_partner/odoo/addons/fastapi_auth_partner
create mode 100644 setup/fastapi_auth_partner/setup.py
diff --git a/auth_partner/README.rst b/auth_partner/README.rst
new file mode 100644
index 000000000..ca979ef04
--- /dev/null
+++ b/auth_partner/README.rst
@@ -0,0 +1,102 @@
+============
+Partner Auth
+============
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:c9e735f01c49bc7974e3b9354b6157e19c7486a71626ad8eef81104b628d476b
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github
+ :target: https://github.com/OCA/rest-framework/tree/16.0/auth_partner
+ :alt: OCA/rest-framework
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-auth_partner
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=16.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module adds to the partners the ability to authenticate through directories.
+
+This module does not implement any routing, it only provides the basic mechanisms in a directory for:
+
+ - Registering a partner and sending an welcome email (to validate email address): `_signup`
+ - Authenticating a partner: `_login`
+ - Validating a partner email using a token: `_validate_email`
+ - Impersonating: `_impersonate`, `_impersonating`
+ - Resetting the password with a unique token sent by mail: `_request_reset_password`, `_set_password`
+ - Sending an invite mail when registering a partner from odoo interface for the partner to enter a password: `_send_invite`, `_set_password`
+
+For a routing implementation, see the `fastapi_auth_partner <../fastapi_auth_partner>`_ module.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Usage
+=====
+
+This module isn't meant to be used standalone but you can still see the directories and authenticable partners in:
+
+Settings > Technical > Partner Authentication > Partner
+
+and
+
+Settings > Technical > Partner Authentication > Directory
+
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Akretion
+
+Contributors
+~~~~~~~~~~~~
+
+* `Akretion `_:
+
+ * Sébastien Beau
+ * Florian Mounier
+
+Maintainers
+~~~~~~~~~~~
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+This module is part of the `OCA/rest-framework `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/auth_partner/__init__.py b/auth_partner/__init__.py
new file mode 100644
index 000000000..aee8895e7
--- /dev/null
+++ b/auth_partner/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizards
diff --git a/auth_partner/__manifest__.py b/auth_partner/__manifest__.py
new file mode 100644
index 000000000..99e8a94fa
--- /dev/null
+++ b/auth_partner/__manifest__.py
@@ -0,0 +1,38 @@
+# Copyright 2024 Akretion (http://www.akretion.com).
+# @author Sébastien BEAU
+# @author Florian Mounier
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Partner Auth",
+ "summary": "Implements the base features for a authenticable partner",
+ "version": "16.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "Akretion,Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/rest-framework",
+ "depends": [
+ "auth_signup",
+ "mail",
+ "queue_job",
+ "server_environment",
+ ],
+ "data": [
+ "security/res_group.xml",
+ "security/ir.model.access.csv",
+ "security/ir_rule.xml",
+ "data/email_data.xml",
+ "wizards/wizard_auth_partner_force_set_password_view.xml",
+ "wizards/wizard_auth_partner_reset_password_view.xml",
+ "views/auth_partner_view.xml",
+ "views/auth_directory_view.xml",
+ "views/res_partner_view.xml",
+ ],
+ "demo": [
+ "demo/res_partner_demo.xml",
+ "demo/auth_directory_demo.xml",
+ "demo/auth_partner_demo.xml",
+ ],
+ "external_dependencies": {
+ "python": ["itsdangerous", "pyjwt"],
+ },
+}
diff --git a/auth_partner/data/email_data.xml b/auth_partner/data/email_data.xml
new file mode 100644
index 000000000..92193d06d
--- /dev/null
+++ b/auth_partner/data/email_data.xml
@@ -0,0 +1,67 @@
+
+
+
+ Auth Directory: Reset Password
+ noreply@example.org
+ Reset Password
+ {{object.partner_id.id}}
+
+
+ ${object.partner_id.lang}
+
+
+ Hi
+ Click on the following link to reset your password
+ Reset Password
+
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
This module adds a “ref” field in the error response of FastAPI.
This field is an AES encrypted string that contains the error message / traceback.
This encrypted string can be decrypted using the endpoint decrypt error wizard.
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
@@ -403,15 +408,15 @@