From 695eb9ec1e0d4836f58a2651d12ece2b7e1ea69c Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 19 Jun 2024 11:35:26 +0200 Subject: [PATCH] [ADD] fastapi_backport --- fastapi_backport/__init__.py | 64 ++++++++++ fastapi_backport/__manifest__.py | 21 ++++ fastapi_backport/http.py | 110 ++++++++++++++++++ fastapi_backport/models/__init__.py | 1 + fastapi_backport/models/fastapi_endpoint.py | 33 ++++++ .../odoo/addons/fastapi_backport | 1 + setup/fastapi_backport/setup.py | 6 + 7 files changed, 236 insertions(+) create mode 100644 fastapi_backport/__init__.py create mode 100644 fastapi_backport/__manifest__.py create mode 100644 fastapi_backport/http.py create mode 100644 fastapi_backport/models/__init__.py create mode 100644 fastapi_backport/models/fastapi_endpoint.py create mode 120000 setup/fastapi_backport/odoo/addons/fastapi_backport create mode 100644 setup/fastapi_backport/setup.py diff --git a/fastapi_backport/__init__.py b/fastapi_backport/__init__.py new file mode 100644 index 000000000..d5cb66b1b --- /dev/null +++ b/fastapi_backport/__init__.py @@ -0,0 +1,64 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from . import models +from . import http +from starlette.responses import JSONResponse +from odoo import SUPERUSER_ID, api +from odoo.addons.extendable.models.ir_http import IrHttp +from odoo.addons.fastapi.fastapi_dispatcher import FastApiDispatcher +from odoo.addons.fastapi.tests.common import FastAPITransactionCase +from odoo.tests.common import SavepointCase + +_logger = logging.getLogger(__name__) + + +# Use SavepointCase instead of TransactionCase (16.0 merge) +# And use test mode to avoid deadlock in envioronment RLock +class TestModeSavepointCase(SavepointCase): + def setUp(self): + super().setUp() + self.registry.enter_test_mode(self.env.cr) + + def tearDown(self): + self.registry.leave_test_mode() + super().tearDown() + + @classmethod + def _patch_app_to_handle_exception(cls, app): + def handle_error(request, exc): + + def make_json_response(body, status, headers): + response = JSONResponse(body, status_code=status) + if status == 500: + _logger.error("Error in test request", exc_info=exc) + if headers: + response.headers.update(headers) + return response + + request.make_json_response = make_json_response + return FastApiDispatcher(request).handle_error(exc) + + app.exception_handlers = {Exception: handle_error} + + +FastAPITransactionCase.__bases__ = (TestModeSavepointCase,) + + +@classmethod +def _dispatch(cls): + with cls._extendable_context_registry(): + return super(IrHttp, cls)._dispatch() + + +IrHttp._dispatch = _dispatch + + +def post_init_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + # this is the trigger that sends notifications when jobs change + _logger.info("Resyncing registries") + endpoints_ids = env["fastapi.endpoint"].search([]).ids + env["fastapi.endpoint"]._handle_registry_sync(endpoints_ids) diff --git a/fastapi_backport/__manifest__.py b/fastapi_backport/__manifest__.py new file mode 100644 index 000000000..23c9beb90 --- /dev/null +++ b/fastapi_backport/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Fastapi Backport", + "summary": "Backport of FastAPI to Odoo 14.0", + "version": "14.0.1.0.0", + "author": " Akretion", + "license": "AGPL-3", + "depends": [ + "sixteen_in_fourteen", + "base_contextvars", + "base_future_response", + "fastapi", + "pydantic", + "extendable_fastapi", + "extendable", + ], + "post_init_hook": "post_init_hook", +} diff --git a/fastapi_backport/http.py b/fastapi_backport/http.py new file mode 100644 index 000000000..89f3401e9 --- /dev/null +++ b/fastapi_backport/http.py @@ -0,0 +1,110 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import json +import logging +from functools import lru_cache + +import werkzeug.datastructures + +import odoo +from odoo import http +from odoo.tools import date_utils + +from odoo.addons.fastapi.fastapi_dispatcher import FastApiDispatcher + +_logger = logging.getLogger(__name__) + + +class FastapiRootPaths: + _root_paths_by_db = {} + + @classmethod + def set_root_paths(cls, db, root_paths): + cls._root_paths_by_db[db] = root_paths + cls.is_fastapi_path.cache_clear() + + @classmethod + @lru_cache(maxsize=1024) + def is_fastapi_path(cls, db, path): + return any( + path.startswith(root_path) + for root_path in cls._root_paths_by_db.get(db, []) + ) + + +class FastapiRequest(http.WebRequest): + _request_type = "fastapi" + + def __init__(self, *args): + super().__init__(*args) + self.params = {} + self._dispatcher = FastApiDispatcher(self) + + def make_response(self, data, headers=None, cookies=None, status=200): + """Helper for non-HTML responses, or HTML responses with custom + response headers or cookies. + + While handlers can just return the HTML markup of a page they want to + send as a string if non-HTML data is returned they need to create a + complete response object, or the returned data will not be correctly + interpreted by the clients. + + :param basestring data: response body + :param headers: HTTP headers to set on the response + :type headers: ``[(name, value)]`` + :param collections.abc.Mapping cookies: cookies to set on the client + """ + response = http.Response(data, status=status, headers=headers) + if cookies: + for k, v in cookies.items(): + response.set_cookie(k, v) + return response + + def make_json_response(self, data, headers=None, cookies=None, status=200): + """Helper for JSON responses, it json-serializes ``data`` and + sets the Content-Type header accordingly if none is provided. + + :param data: the data that will be json-serialized into the response body + :param int status: http status code + :param List[(str, str)] headers: HTTP headers to set on the response + :param collections.abc.Mapping cookies: cookies to set on the client + :rtype: :class:`~odoo.http.Response` + """ + data = json.dumps(data, ensure_ascii=False, default=date_utils.json_default) + + headers = werkzeug.datastructures.Headers(headers) + headers["Content-Length"] = len(data) + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json; charset=utf-8" + + return self.make_response(data, headers.to_wsgi_list(), cookies, status) + + def dispatch(self): + return self._dispatcher.dispatch(None, None) + + def _handle_exception(self, exception): + _logger.exception( + "Exception during fastapi request handling", exc_info=exception + ) + return self._dispatcher.handle_error(exception) + + +ori_get_request = http.root.__class__.get_request + + +def get_request(self, httprequest): + db = httprequest.session.db + if db and odoo.service.db.exp_db_exist(db): + # on the very first request processed by a worker, + # registry is not loaded yet + # so we enforce its loading here. + odoo.registry(db) + if FastapiRootPaths.is_fastapi_path(db, httprequest.path): + return FastapiRequest(httprequest) + return ori_get_request(self, httprequest) + + +http.root.__class__.get_request = get_request diff --git a/fastapi_backport/models/__init__.py b/fastapi_backport/models/__init__.py new file mode 100644 index 000000000..b825fab92 --- /dev/null +++ b/fastapi_backport/models/__init__.py @@ -0,0 +1 @@ +from . import fastapi_endpoint diff --git a/fastapi_backport/models/fastapi_endpoint.py b/fastapi_backport/models/fastapi_endpoint.py new file mode 100644 index 000000000..8a0e0f9d5 --- /dev/null +++ b/fastapi_backport/models/fastapi_endpoint.py @@ -0,0 +1,33 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import api, models + +from ..http import FastapiRootPaths + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + @api.model + def _update_root_paths_registry(self): + root_paths = self.env["fastapi.endpoint"].search([]).mapped("root_path") + FastapiRootPaths.set_root_paths(self.env.cr.dbname, root_paths) + + def _register_hook(self): + super()._register_hook() + self._update_root_paths_registry() + + def _inverse_root_path(self): + super()._inverse_root_path() + self._update_root_paths_registry() + + @api.depends("root_path") + def _compute_urls(self): + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + for rec in self: + rec.docs_url = f"{base_url}{rec.root_path}/docs" + rec.redoc_url = f"{base_url}{rec.root_path}/redoc" + rec.openapi_url = f"{base_url}{rec.root_path}/openapi.json" diff --git a/setup/fastapi_backport/odoo/addons/fastapi_backport b/setup/fastapi_backport/odoo/addons/fastapi_backport new file mode 120000 index 000000000..5b915f44c --- /dev/null +++ b/setup/fastapi_backport/odoo/addons/fastapi_backport @@ -0,0 +1 @@ +../../../../fastapi_backport \ No newline at end of file diff --git a/setup/fastapi_backport/setup.py b/setup/fastapi_backport/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/fastapi_backport/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)