Skip to content

Commit 695eb9e

Browse files
committed
[ADD] fastapi_backport
1 parent fb6b727 commit 695eb9e

File tree

7 files changed

+236
-0
lines changed

7 files changed

+236
-0
lines changed

fastapi_backport/__init__.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
import logging
6+
from . import models
7+
from . import http
8+
from starlette.responses import JSONResponse
9+
from odoo import SUPERUSER_ID, api
10+
from odoo.addons.extendable.models.ir_http import IrHttp
11+
from odoo.addons.fastapi.fastapi_dispatcher import FastApiDispatcher
12+
from odoo.addons.fastapi.tests.common import FastAPITransactionCase
13+
from odoo.tests.common import SavepointCase
14+
15+
_logger = logging.getLogger(__name__)
16+
17+
18+
# Use SavepointCase instead of TransactionCase (16.0 merge)
19+
# And use test mode to avoid deadlock in envioronment RLock
20+
class TestModeSavepointCase(SavepointCase):
21+
def setUp(self):
22+
super().setUp()
23+
self.registry.enter_test_mode(self.env.cr)
24+
25+
def tearDown(self):
26+
self.registry.leave_test_mode()
27+
super().tearDown()
28+
29+
@classmethod
30+
def _patch_app_to_handle_exception(cls, app):
31+
def handle_error(request, exc):
32+
33+
def make_json_response(body, status, headers):
34+
response = JSONResponse(body, status_code=status)
35+
if status == 500:
36+
_logger.error("Error in test request", exc_info=exc)
37+
if headers:
38+
response.headers.update(headers)
39+
return response
40+
41+
request.make_json_response = make_json_response
42+
return FastApiDispatcher(request).handle_error(exc)
43+
44+
app.exception_handlers = {Exception: handle_error}
45+
46+
47+
FastAPITransactionCase.__bases__ = (TestModeSavepointCase,)
48+
49+
50+
@classmethod
51+
def _dispatch(cls):
52+
with cls._extendable_context_registry():
53+
return super(IrHttp, cls)._dispatch()
54+
55+
56+
IrHttp._dispatch = _dispatch
57+
58+
59+
def post_init_hook(cr, registry):
60+
env = api.Environment(cr, SUPERUSER_ID, {})
61+
# this is the trigger that sends notifications when jobs change
62+
_logger.info("Resyncing registries")
63+
endpoints_ids = env["fastapi.endpoint"].search([]).ids
64+
env["fastapi.endpoint"]._handle_registry_sync(endpoints_ids)

fastapi_backport/__manifest__.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
{
6+
"name": "Fastapi Backport",
7+
"summary": "Backport of FastAPI to Odoo 14.0",
8+
"version": "14.0.1.0.0",
9+
"author": " Akretion",
10+
"license": "AGPL-3",
11+
"depends": [
12+
"sixteen_in_fourteen",
13+
"base_contextvars",
14+
"base_future_response",
15+
"fastapi",
16+
"pydantic",
17+
"extendable_fastapi",
18+
"extendable",
19+
],
20+
"post_init_hook": "post_init_hook",
21+
}

fastapi_backport/http.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright 2023 ACSONE SA/NV
2+
# Copyright 2024 Akretion (http://www.akretion.com).
3+
# @author Florian Mounier <florian.mounier@akretion.com>
4+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
5+
6+
import json
7+
import logging
8+
from functools import lru_cache
9+
10+
import werkzeug.datastructures
11+
12+
import odoo
13+
from odoo import http
14+
from odoo.tools import date_utils
15+
16+
from odoo.addons.fastapi.fastapi_dispatcher import FastApiDispatcher
17+
18+
_logger = logging.getLogger(__name__)
19+
20+
21+
class FastapiRootPaths:
22+
_root_paths_by_db = {}
23+
24+
@classmethod
25+
def set_root_paths(cls, db, root_paths):
26+
cls._root_paths_by_db[db] = root_paths
27+
cls.is_fastapi_path.cache_clear()
28+
29+
@classmethod
30+
@lru_cache(maxsize=1024)
31+
def is_fastapi_path(cls, db, path):
32+
return any(
33+
path.startswith(root_path)
34+
for root_path in cls._root_paths_by_db.get(db, [])
35+
)
36+
37+
38+
class FastapiRequest(http.WebRequest):
39+
_request_type = "fastapi"
40+
41+
def __init__(self, *args):
42+
super().__init__(*args)
43+
self.params = {}
44+
self._dispatcher = FastApiDispatcher(self)
45+
46+
def make_response(self, data, headers=None, cookies=None, status=200):
47+
"""Helper for non-HTML responses, or HTML responses with custom
48+
response headers or cookies.
49+
50+
While handlers can just return the HTML markup of a page they want to
51+
send as a string if non-HTML data is returned they need to create a
52+
complete response object, or the returned data will not be correctly
53+
interpreted by the clients.
54+
55+
:param basestring data: response body
56+
:param headers: HTTP headers to set on the response
57+
:type headers: ``[(name, value)]``
58+
:param collections.abc.Mapping cookies: cookies to set on the client
59+
"""
60+
response = http.Response(data, status=status, headers=headers)
61+
if cookies:
62+
for k, v in cookies.items():
63+
response.set_cookie(k, v)
64+
return response
65+
66+
def make_json_response(self, data, headers=None, cookies=None, status=200):
67+
"""Helper for JSON responses, it json-serializes ``data`` and
68+
sets the Content-Type header accordingly if none is provided.
69+
70+
:param data: the data that will be json-serialized into the response body
71+
:param int status: http status code
72+
:param List[(str, str)] headers: HTTP headers to set on the response
73+
:param collections.abc.Mapping cookies: cookies to set on the client
74+
:rtype: :class:`~odoo.http.Response`
75+
"""
76+
data = json.dumps(data, ensure_ascii=False, default=date_utils.json_default)
77+
78+
headers = werkzeug.datastructures.Headers(headers)
79+
headers["Content-Length"] = len(data)
80+
if "Content-Type" not in headers:
81+
headers["Content-Type"] = "application/json; charset=utf-8"
82+
83+
return self.make_response(data, headers.to_wsgi_list(), cookies, status)
84+
85+
def dispatch(self):
86+
return self._dispatcher.dispatch(None, None)
87+
88+
def _handle_exception(self, exception):
89+
_logger.exception(
90+
"Exception during fastapi request handling", exc_info=exception
91+
)
92+
return self._dispatcher.handle_error(exception)
93+
94+
95+
ori_get_request = http.root.__class__.get_request
96+
97+
98+
def get_request(self, httprequest):
99+
db = httprequest.session.db
100+
if db and odoo.service.db.exp_db_exist(db):
101+
# on the very first request processed by a worker,
102+
# registry is not loaded yet
103+
# so we enforce its loading here.
104+
odoo.registry(db)
105+
if FastapiRootPaths.is_fastapi_path(db, httprequest.path):
106+
return FastapiRequest(httprequest)
107+
return ori_get_request(self, httprequest)
108+
109+
110+
http.root.__class__.get_request = get_request

fastapi_backport/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import fastapi_endpoint
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
6+
from odoo import api, models
7+
8+
from ..http import FastapiRootPaths
9+
10+
11+
class FastapiEndpoint(models.Model):
12+
_inherit = "fastapi.endpoint"
13+
14+
@api.model
15+
def _update_root_paths_registry(self):
16+
root_paths = self.env["fastapi.endpoint"].search([]).mapped("root_path")
17+
FastapiRootPaths.set_root_paths(self.env.cr.dbname, root_paths)
18+
19+
def _register_hook(self):
20+
super()._register_hook()
21+
self._update_root_paths_registry()
22+
23+
def _inverse_root_path(self):
24+
super()._inverse_root_path()
25+
self._update_root_paths_registry()
26+
27+
@api.depends("root_path")
28+
def _compute_urls(self):
29+
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
30+
for rec in self:
31+
rec.docs_url = f"{base_url}{rec.root_path}/docs"
32+
rec.redoc_url = f"{base_url}{rec.root_path}/redoc"
33+
rec.openapi_url = f"{base_url}{rec.root_path}/openapi.json"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../fastapi_backport

setup/fastapi_backport/setup.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import setuptools
2+
3+
setuptools.setup(
4+
setup_requires=['setuptools-odoo'],
5+
odoo_addon=True,
6+
)

0 commit comments

Comments
 (0)