From 8366a76373dcea8671ef7a0b8636bbdfc1c74a7b Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 17 Jun 2024 15:03:31 +0200 Subject: [PATCH] [ADD] fastapi_backport --- fastapi_backport/__init__.py | 32 ++++++ fastapi_backport/__manifest__.py | 20 ++++ fastapi_backport/http.py | 107 ++++++++++++++++++ fastapi_backport/models/__init__.py | 1 + fastapi_backport/models/fastapi_endpoint.py | 25 ++++ .../odoo/addons/fastapi_backport | 1 + setup/fastapi_backport/setup.py | 6 + 7 files changed, 192 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..648a1d6e2 --- /dev/null +++ b/fastapi_backport/__init__.py @@ -0,0 +1,32 @@ +# 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 odoo import SUPERUSER_ID, api +from odoo.addons.extendable.models.ir_http import IrHttp +from odoo.addons.fastapi.tests.common import FastAPITransactionCase +from odoo.tests.common import SavepointCase + +_logger = logging.getLogger(__name__) + +FastAPITransactionCase.__bases__ = (SavepointCase,) + + +@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..57f79d446 --- /dev/null +++ b/fastapi_backport/__manifest__.py @@ -0,0 +1,20 @@ +# 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_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..48c827a36 --- /dev/null +++ b/fastapi_backport/http.py @@ -0,0 +1,107 @@ +# 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 +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 + + +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): + try: + return self._dispatcher.dispatch(None, None) + except Exception as e: + return self._handle_exception(e) + + def _handle_exception(self, 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..235009a1f --- /dev/null +++ b/fastapi_backport/models/fastapi_endpoint.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). + + +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() 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, +)