diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01bd8831..3505fac2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,9 @@ exclude: | (?x) # NOT INSTALLABLE ADDONS - ^base_rest/| - ^base_rest_auth_api_key/| ^base_rest_auth_jwt/| ^base_rest_auth_user_service/| - ^base_rest_datamodel/| - ^base_rest_demo/| - ^base_rest_pydantic/| - ^datamodel/| - ^extendable/| ^model_serializer/| - ^pydantic/| ^rest_log/| # END NOT INSTALLABLE ADDONS # Files and folders generated by bots, to avoid loops diff --git a/base_rest/__init__.py b/base_rest/__init__.py index 431f67ab..5947223b 100644 --- a/base_rest/__init__.py +++ b/base_rest/__init__.py @@ -1,3 +1,10 @@ +import logging from . import models from . import components from . import http + +logging.getLogger(__file__).warning( + "base_rest is deprecated and not fully supported anymore on Odoo 16. " + "Please migrate to the FastAPI migration module. " + "See https://github.com/OCA/rest-framework/pull/291.", +) diff --git a/base_rest/__manifest__.py b/base_rest/__manifest__.py index fb9e6a1a..f29fe6ef 100644 --- a/base_rest/__manifest__.py +++ b/base_rest/__manifest__.py @@ -6,11 +6,11 @@ "summary": """ Develop your own high level REST APIs for Odoo thanks to this addon. """, - "version": "15.0.1.2.0", + "version": "16.0.1.0.0", "development_status": "Beta", "license": "LGPL-3", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", - "maintainers": ["lmignon"], + "maintainers": [], "website": "https://github.com/OCA/rest-framework", "depends": ["component", "web"], "data": [ @@ -18,19 +18,20 @@ "views/base_rest_view.xml", ], "assets": { - "web.assets_common": [ + "web.assets_frontend": [ "base_rest/static/src/scss/base_rest.scss", "base_rest/static/src/js/swagger_ui.js", + "base_rest/static/src/js/swagger.js", ], }, - "demo": [], "external_dependencies": { "python": [ "cerberus", "pyquerystring", "parse-accept-language", - "apispec>=4.0.0", + # adding version causes missing-manifest-dependency false positives + "apispec", ] }, - "installable": False, + "installable": True, } diff --git a/base_rest/apispec/base_rest_service_apispec.py b/base_rest/apispec/base_rest_service_apispec.py index 94ebb2eb..72276d40 100644 --- a/base_rest/apispec/base_rest_service_apispec.py +++ b/base_rest/apispec/base_rest_service_apispec.py @@ -7,6 +7,7 @@ from apispec import APISpec from ..core import _rest_services_databases +from ..tools import ROUTING_DECORATOR_ATTR from .rest_method_param_plugin import RestMethodParamPlugin from .rest_method_security_plugin import RestMethodSecurityPlugin from .restapi_method_route_plugin import RestApiMethodRoutePlugin @@ -62,18 +63,18 @@ def _get_plugins(self): def _add_method_path(self, method): description = textwrap.dedent(method.__doc__ or "") - routing = method.routing + routing = getattr(method, ROUTING_DECORATOR_ATTR) for paths, method in routing["routes"]: for path in paths: self.path( path, operations={method.lower(): {"summary": description}}, - routing=routing, + **{ROUTING_DECORATOR_ATTR: routing}, ) def generate_paths(self): for _name, method in inspect.getmembers(self._service, inspect.ismethod): - routing = getattr(method, "routing", None) + routing = getattr(method, ROUTING_DECORATOR_ATTR, None) if not routing: continue self._add_method_path(method) diff --git a/base_rest/apispec/rest_method_param_plugin.py b/base_rest/apispec/rest_method_param_plugin.py index 64ad6503..dde70782 100644 --- a/base_rest/apispec/rest_method_param_plugin.py +++ b/base_rest/apispec/rest_method_param_plugin.py @@ -4,6 +4,7 @@ from apispec import BasePlugin from ..restapi import RestMethodParam +from ..tools import ROUTING_DECORATOR_ATTR class RestMethodParamPlugin(BasePlugin): @@ -25,7 +26,7 @@ def init_spec(self, spec): self.openapi_version = spec.openapi_version def operation_helper(self, path=None, operations=None, **kwargs): - routing = kwargs.get("routing") + routing = kwargs.get(ROUTING_DECORATOR_ATTR) if not routing: super(RestMethodParamPlugin, self).operation_helper( path, operations, **kwargs @@ -33,14 +34,14 @@ def operation_helper(self, path=None, operations=None, **kwargs): if not operations: return for method, params in operations.items(): - parameters = self._generate_pamareters(routing, method, params) + parameters = self._generate_parameters(routing, method, params) if parameters: params["parameters"] = parameters responses = self._generate_responses(routing, method, params) if responses: params["responses"] = responses - def _generate_pamareters(self, routing, method, params): + def _generate_parameters(self, routing, method, params): parameters = params.get("parameters", []) # add default paramters provided by the sevice parameters.extend(self._default_parameters) diff --git a/base_rest/apispec/rest_method_security_plugin.py b/base_rest/apispec/rest_method_security_plugin.py index 0c2568fa..bdc3d0e5 100644 --- a/base_rest/apispec/rest_method_security_plugin.py +++ b/base_rest/apispec/rest_method_security_plugin.py @@ -3,6 +3,8 @@ from apispec import BasePlugin +from ..tools import ROUTING_DECORATOR_ATTR + class RestMethodSecurityPlugin(BasePlugin): """ @@ -23,7 +25,7 @@ def init_spec(self, spec): spec.components.security_scheme("user", user_scheme) def operation_helper(self, path=None, operations=None, **kwargs): - routing = kwargs.get("routing") + routing = kwargs.get(ROUTING_DECORATOR_ATTR) if not routing: super(RestMethodSecurityPlugin, self).operation_helper( path, operations, **kwargs diff --git a/base_rest/components/service.py b/base_rest/components/service.py index 3793b3c8..ec9d1ba9 100644 --- a/base_rest/components/service.py +++ b/base_rest/components/service.py @@ -11,6 +11,7 @@ from odoo.addons.component.core import AbstractComponent from ..apispec.base_rest_service_apispec import BaseRestServiceAPISpec +from ..tools import ROUTING_DECORATOR_ATTR _logger = logging.getLogger(__name__) @@ -93,7 +94,7 @@ def _prepare_input_params(self, method, params): method_name = method.__name__ if hasattr(method, "skip_secure_params"): return params - routing = getattr(method, "routing", None) + routing = getattr(method, ROUTING_DECORATOR_ATTR, None) if not routing: _logger.warning( "Method %s is not a public method of service %s", @@ -122,7 +123,7 @@ def _prepare_response(self, method, result): method_name = method.__name__ if hasattr(method, "skip_secure_response"): return result - routing = getattr(method, "routing", None) + routing = getattr(method, ROUTING_DECORATOR_ATTR, None) output_param = routing["output_param"] if not output_param: _logger.warning( diff --git a/base_rest/controllers/main.py b/base_rest/controllers/main.py index 3bfa0699..b4166d74 100644 --- a/base_rest/controllers/main.py +++ b/base_rest/controllers/main.py @@ -7,7 +7,7 @@ from werkzeug.exceptions import BadRequest from odoo import models -from odoo.http import Controller, ControllerType, Response, request +from odoo.http import Controller, Response, request from odoo.addons.component.core import WorkContext, _get_addon_name @@ -25,43 +25,7 @@ def __init__(self, name, env): self.id = None -class RestControllerType(ControllerType): - - # pylint: disable=E0213 - def __init__(cls, name, bases, attrs): # noqa: B902 - if ( - "RestController" in globals() - and RestController in bases - and Controller not in bases - ): - # to be registered as a controller into the ControllerType, - # our RestConrtroller must be a direct child of Controller - bases += (Controller,) - super(RestControllerType, cls).__init__(name, bases, attrs) - if "RestController" not in globals() or not any( - issubclass(b, RestController) for b in bases - ): - return - # register the rest controller into the rest controllers registry - root_path = getattr(cls, "_root_path", None) - collection_name = getattr(cls, "_collection_name", None) - if root_path and collection_name: - cls._module = _get_addon_name(cls.__module__) - _rest_controllers_per_module[cls._module].append( - { - "root_path": root_path, - "collection_name": collection_name, - "controller_class": cls, - } - ) - _logger.debug( - "Added rest controller %s for module %s", - _rest_controllers_per_module[cls._module][-1], - cls._module, - ) - - -class RestController(Controller, metaclass=RestControllerType): +class RestController(Controller): """Generic REST Controller This controller is the base controller used by as base controller for all the REST @@ -130,6 +94,38 @@ class ControllerB(ControllerB): _component_context_provider = "component_context_provider" + @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__ + ): + return + # register the rest controller into the rest controllers registry + root_path = getattr(cls, "_root_path", None) + collection_name = getattr(cls, "_collection_name", None) + if root_path and collection_name: + cls._module = _get_addon_name(cls.__module__) + _rest_controllers_per_module[cls._module].append( + { + "root_path": root_path, + "collection_name": collection_name, + "controller_class": cls, + } + ) + _logger.debug( + "Added rest controller %s for module %s", + _rest_controllers_per_module[cls._module][-1], + cls._module, + ) + def _get_component_context(self, collection=None): """ This method can be inherited to add parameter into the component diff --git a/base_rest/http.py b/base_rest/http.py index 4a48aa8b..5dfef3d0 100644 --- a/base_rest/http.py +++ b/base_rest/http.py @@ -11,6 +11,7 @@ import traceback from collections import defaultdict +from markupsafe import escape from werkzeug.exceptions import ( BadRequest, Forbidden, @@ -19,9 +20,7 @@ NotFound, Unauthorized, ) -from werkzeug.utils import escape -import odoo from odoo.exceptions import ( AccessDenied, AccessError, @@ -29,12 +28,16 @@ UserError, ValidationError, ) -from odoo.http import HttpRequest, Root, SessionExpiredException, request +from odoo.http import ( + CSRF_FREE_METHODS, + MISSING_CSRF_WARNING, + Dispatcher, + SessionExpiredException, + request, +) from odoo.tools import ustr from odoo.tools.config import config -from .core import _rest_services_routes - _logger = logging.getLogger(__name__) try: @@ -69,7 +72,7 @@ def wrapJsonException(exception, include_description=False, extra_info=None): get_original_headers = exception.get_headers exception.traceback = "".join(traceback.format_exception(*sys.exc_info())) - def get_body(environ=None): + def get_body(environ=None, scope=None): res = {"code": exception.code, "name": escape(exception.name)} description = exception.get_description(environ) if config.get_misc("base_rest", "dev_mode"): @@ -80,7 +83,7 @@ def get_body(environ=None): res.update(extra_info or {}) return JSONEncoder().encode(res) - def get_headers(environ=None): + def get_headers(environ=None, scope=None): """Get a list of headers.""" _headers = [("Content-Type", "application/json")] for key, value in get_original_headers(environ=environ): @@ -116,29 +119,60 @@ def get_headers(environ=None): return exception -class HttpRestRequest(HttpRequest): - """Http request that always return json, usefull for rest api""" - - def __init__(self, httprequest): - super(HttpRestRequest, self).__init__(httprequest) - if self.httprequest.mimetype == "application/json": - data = self.httprequest.get_data().decode(self.httprequest.charset) - try: - self.params = json.loads(data) - except ValueError as e: - msg = "Invalid JSON data: %s" % str(e) - _logger.info("%s: %s", self.httprequest.path, msg) - raise BadRequest(msg) from e - elif self.httprequest.mimetype == "multipart/form-data": +class RestApiDispatcher(Dispatcher): + """Dispatcher for requests at routes for restapi types""" + + routing_type = "restapi" + + def pre_dispatch(self, rule, args): + res = super().pre_dispatch(rule, args) + httprequest = self.request.httprequest + self.request.params = args + if httprequest.mimetype == "application/json": + data = httprequest.get_data().decode(httprequest.charset) + if data: + try: + self.request.params.update(json.loads(data)) + except (ValueError, json.decoder.JSONDecodeError) as e: + msg = "Invalid JSON data: %s" % str(e) + _logger.info("%s: %s", self.request.httprequest.path, msg) + raise BadRequest(msg) from e + elif httprequest.mimetype == "multipart/form-data": # Do not reassign self.params pass else: # We reparse the query_string in order to handle data structure # more information on https://github.com/aventurella/pyquerystring - self.params = pyquerystring.parse( - self.httprequest.query_string.decode("utf-8") + self.request.params.update( + pyquerystring.parse(httprequest.query_string.decode("utf-8")) ) self._determine_context_lang() + return res + + def dispatch(self, endpoint, args): + """Same as odoo.http.HttpDispatcher, except for the early db check""" + params = dict(self.request.get_http_params(), **args) + + # Check for CSRF token for relevant requests + if ( + self.request.httprequest.method not in CSRF_FREE_METHODS + and endpoint.routing.get("csrf", True) + ): + token = params.pop("csrf_token", None) + if not self.request.validate_csrf(token): + if token is not None: + _logger.warning( + "CSRF validation failed on path '%s'", + self.request.httprequest.path, + ) + else: + _logger.warning(MISSING_CSRF_WARNING, request.httprequest.path) + raise BadRequest("Session expired (invalid CSRF token)") + + if self.request.db: + return self.request.registry["ir.http"]._dispatch(endpoint) + else: + return endpoint(**self.request.params) def _determine_context_lang(self): """ @@ -147,13 +181,13 @@ def _determine_context_lang(self): according to the priority of languages into the headers and those available into Odoo. """ - accepted_langs = self.httprequest.headers.get("Accept-language") + accepted_langs = self.request.httprequest.headers.get("Accept-language") if not accepted_langs: return parsed_accepted_langs = parse_accept_language(accepted_langs) installed_locale_langs = set() installed_locale_by_lang = defaultdict(list) - for lang_code, _name in self.env["res.lang"].get_installed(): + for lang_code, _name in self.request.env["res.lang"].get_installed(): installed_locale_langs.add(lang_code) installed_locale_by_lang[lang_code.split("_")[0]].append(lang_code) @@ -173,14 +207,14 @@ def _determine_context_lang(self): locale = locales[0] if locale: # reset the context to put our new lang. - context = dict(self._context) - context["lang"] = locale - # the setter defiend in odoo.http.WebRequest reset the env - # when setting a new context - self.context = context + self.request.update_context(lang=locale) break - def _handle_exception(self, exception): + @classmethod + def is_compatible_with(cls, request): + return True + + def handle_error(self, exception): """Called within an except block to allow converting exceptions to abitrary responses. Anything returned (except None) will be used as response.""" @@ -188,25 +222,23 @@ def _handle_exception(self, exception): # we don't want to return the login form as plain html page # we want to raise a proper exception return wrapJsonException(Unauthorized(ustr(exception))) - try: - return super(HttpRestRequest, self)._handle_exception(exception) - except MissingError as e: - extra_info = getattr(e, "rest_json_info", None) - return wrapJsonException(NotFound(ustr(e)), extra_info=extra_info) - except (AccessError, AccessDenied) as e: - extra_info = getattr(e, "rest_json_info", None) - return wrapJsonException(Forbidden(ustr(e)), extra_info=extra_info) - except (UserError, ValidationError) as e: - extra_info = getattr(e, "rest_json_info", None) + if isinstance(exception, MissingError): + extra_info = getattr(exception, "rest_json_info", None) + return wrapJsonException(NotFound(ustr(exception)), extra_info=extra_info) + if isinstance(exception, (AccessError, AccessDenied)): + extra_info = getattr(exception, "rest_json_info", None) + return wrapJsonException(Forbidden(ustr(exception)), extra_info=extra_info) + if isinstance(exception, (UserError, ValidationError)): + extra_info = getattr(exception, "rest_json_info", None) return wrapJsonException( - BadRequest(e.args[0]), include_description=True, extra_info=extra_info + BadRequest(exception.args[0]), + include_description=True, + extra_info=extra_info, ) - except HTTPException as e: - extra_info = getattr(e, "rest_json_info", None) - return wrapJsonException(e, extra_info=extra_info) - except Exception as e: # flake8: noqa: E722 - extra_info = getattr(e, "rest_json_info", None) - return wrapJsonException(InternalServerError(e), extra_info=extra_info) + if isinstance(exception, HTTPException): + return exception + extra_info = getattr(exception, "rest_json_info", None) + return wrapJsonException(InternalServerError(exception), extra_info=extra_info) def make_json_response(self, data, headers=None, cookies=None): data = JSONEncoder().encode(data) @@ -214,24 +246,3 @@ def make_json_response(self, data, headers=None, cookies=None): headers = {} headers["Content-Type"] = "application/json" return self.make_response(data, headers=headers, cookies=cookies) - - -ori_get_request = Root.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 to make sure that - # _rest_services_databases is not empty - odoo.registry(db) - rest_routes = _rest_services_routes.get(db, []) - for root_path in rest_routes: - if httprequest.path.startswith(root_path): - return HttpRestRequest(httprequest) - return ori_get_request(self, httprequest) - - -Root.get_request = get_request diff --git a/base_rest/models/rest_service_registration.py b/base_rest/models/rest_service_registration.py index d5d6b975..ee44c3dd 100644 --- a/base_rest/models/rest_service_registration.py +++ b/base_rest/models/rest_service_registration.py @@ -29,11 +29,7 @@ _rest_services_databases, _rest_services_routes, ) -from ..tools import _inspect_methods - -# Decorator attribute added on a route function (cfr Odoo's route) -ROUTING_DECORATOR_ATTR = "routing" - +from ..tools import ROUTING_DECORATOR_ATTR, _inspect_methods _logger = logging.getLogger(__name__) @@ -61,11 +57,11 @@ def _register_hook(self): self.build_registry(services_registry) # we also have to remove the RestController from the # controller_per_module registry since it's an abstract controller - controllers = http.controllers_per_module["base_rest"] + controllers = http.Controller.children_classes["base_rest"] controllers = [ - (name, cls) for name, cls in controllers if "RestController" not in name + cls for cls in controllers if "RestController" not in cls.__name__ ] - http.controllers_per_module["base_rest"] = controllers + http.Controller.children_classes["base_rest"] = controllers # create the final controller providing the http routes for # the services available into the current database self._build_controllers_routes(services_registry) @@ -90,21 +86,20 @@ def _build_controller(self, service, controller_def): # generate an addon name used to register our new controller for # the current database - addon_name = "{}_{}_{}".format( + addon_name = base_controller_cls._module + identifier = "{}_{}_{}".format( self.env.cr.dbname, service._collection.replace(".", "_"), service._usage.replace(".", "_"), ) + base_controller_cls._identifier = identifier # put our new controller into the new addon module ctrl_cls.__module__ = "odoo.addons.{}".format(addon_name) - # instruct the registry that our fake addon is part of the loaded - # modules self.env.registry._init_modules.add(addon_name) # register our conroller into the list of available controllers - name_class = ("{}.{}".format(ctrl_cls.__module__, ctrl_cls.__name__), ctrl_cls) - http.controllers_per_module[addon_name].append(name_class) + http.Controller.children_classes[addon_name].append(ctrl_cls) self._apply_defaults_to_controller_routes(controller_class=ctrl_cls) def _apply_defaults_to_controller_routes(self, controller_class): @@ -398,9 +393,9 @@ def _generate_methods(self): path_sep = "/" root_path = "{}{}{}".format(root_path, path_sep, self._service._usage) for name, method in _inspect_methods(self._service.__class__): - if not hasattr(method, "routing"): + routing = getattr(method, ROUTING_DECORATOR_ATTR, None) + if routing is None: continue - routing = method.routing for routes, http_method in routing["routes"]: method_name = "{}_{}".format(http_method.lower(), name) default_route = routes[0] @@ -424,6 +419,7 @@ def _generate_methods(self): route_params = dict( route=["{}{}".format(root_path, r) for r in routes], methods=[http_method], + type="restapi", ) for attr in {"auth", "cors", "csrf", "save_session"}: if attr in routing: diff --git a/base_rest/readme/DESCRIPTION.rst b/base_rest/readme/DESCRIPTION.rst index 96f88c97..28c8759f 100644 --- a/base_rest/readme/DESCRIPTION.rst +++ b/base_rest/readme/DESCRIPTION.rst @@ -1,3 +1,7 @@ +This addon is deprecated and not fully supported anymore on Odoo 16. +Please migrate to the FastAPI migration module. +See https://github.com/OCA/rest-framework/pull/291. + This addon provides the basis to develop high level REST APIs for Odoo. As Odoo becomes one of the central pieces of enterprise IT systems, it often diff --git a/base_rest/restapi.py b/base_rest/restapi.py index 799d4418..ce61e702 100644 --- a/base_rest/restapi.py +++ b/base_rest/restapi.py @@ -10,7 +10,7 @@ from odoo import _, http from odoo.exceptions import UserError, ValidationError -from .tools import cerberus_to_json +from .tools import ROUTING_DECORATOR_ATTR, cerberus_to_json def method(routes, input_param=None, output_param=None, **kw): @@ -104,7 +104,7 @@ def response_wrap(*args, **kw): response = f(*args, **kw) return response - response_wrap.routing = routing + setattr(response_wrap, ROUTING_DECORATOR_ATTR, routing) response_wrap.original_func = f return response_wrap diff --git a/base_rest/static/src/js/swagger.js b/base_rest/static/src/js/swagger.js new file mode 100644 index 00000000..f2845fb8 --- /dev/null +++ b/base_rest/static/src/js/swagger.js @@ -0,0 +1,16 @@ +odoo.define("base_rest.swagger", function (require) { + "use strict"; + + var publicWidget = require("web.public.widget"); + var SwaggerUi = require("base_rest.swagger_ui"); + + publicWidget.registry.Swagger = publicWidget.Widget.extend({ + selector: "#swagger-ui", + start: function () { + var def = this._super.apply(this, arguments); + var swagger_ui = new SwaggerUi("#swagger-ui"); + swagger_ui.start(); + return def; + }, + }); +}); diff --git a/base_rest/static/src/js/swagger_ui.js b/base_rest/static/src/js/swagger_ui.js index 933c101e..c3ca1ae6 100644 --- a/base_rest/static/src/js/swagger_ui.js +++ b/base_rest/static/src/js/swagger_ui.js @@ -38,7 +38,7 @@ odoo.define("base_rest.swagger_ui", function (require) { onComplete: function () { if (this.web_btn === undefined) { this.web_btn = $( - "" + "" ); $(".topbar").prepend(this.web_btn); } diff --git a/base_rest/static/src/scss/base_rest.scss b/base_rest/static/src/scss/base_rest.scss index 47b92d17..e74f2c31 100644 --- a/base_rest/static/src/scss/base_rest.scss +++ b/base_rest/static/src/scss/base_rest.scss @@ -1,3 +1,7 @@ +#swagger-ui { + overflow: scroll; +} + .swg-odoo-web-btn { font-family: fontAwesome !important; display: block; diff --git a/base_rest/tests/common.py b/base_rest/tests/common.py index 71ac4d43..cd242678 100644 --- a/base_rest/tests/common.py +++ b/base_rest/tests/common.py @@ -26,7 +26,7 @@ _rest_controllers_per_module, _rest_services_databases, ) -from ..tools import _inspect_methods +from ..tools import ROUTING_DECORATOR_ATTR, _inspect_methods class RegistryMixin(object): @@ -61,11 +61,8 @@ def _setup_registry(class_or_instance): class_or_instance._service_registry = RestServicesRegistry() # take a copy of registered controllers - controllers = http.controllers_per_module - http.controllers_per_module = controllers - - class_or_instance._controllers_per_module = copy.deepcopy( - http.controllers_per_module + class_or_instance._controller_children_classes = copy.deepcopy( + http.Controller.children_classes ) class_or_instance._original_addon_rest_controllers_per_module = copy.deepcopy( _rest_controllers_per_module[_get_addon_name(class_or_instance.__module__)] @@ -144,7 +141,9 @@ def my_controller_route_without_auth_2(self): @staticmethod def _teardown_registry(class_or_instance): ComponentRegistryCase._teardown_registry(class_or_instance) - http.controllers_per_module = class_or_instance._controllers_per_module + http.Controller.children_classes = ( + class_or_instance._controller_children_classes + ) db_name = get_db_name() _component_databases[db_name] = class_or_instance._original_components _rest_services_databases[ @@ -169,22 +168,26 @@ def _build_services(class_or_instance, *classes): ) @staticmethod - def _get_controller_for(service): - addon_name = "{}_{}_{}".format( + def _get_controller_for(service, addon="base_rest"): + identifier = "{}_{}_{}".format( get_db_name(), service._collection.replace(".", "_"), service._usage.replace(".", "_"), ) - controllers = http.controllers_per_module.get(addon_name, []) + controllers = [ + controller + for controller in http.Controller.children_classes.get(addon, []) + if getattr(controller, "_identifier", None) == identifier + ] if not controllers: return - return controllers[0][1] + return controllers[-1] @staticmethod def _get_controller_route_methods(controller): methods = {} for name, method in _inspect_methods(controller): - if hasattr(method, "routing"): + if hasattr(method, ROUTING_DECORATOR_ATTR): methods[name] = method return methods diff --git a/base_rest/tests/test_controller_builder.py b/base_rest/tests/test_controller_builder.py index babac520..4a0d0cb3 100644 --- a/base_rest/tests/test_controller_builder.py +++ b/base_rest/tests/test_controller_builder.py @@ -5,6 +5,7 @@ from odoo.addons.component.core import Component from .. import restapi +from ..tools import ROUTING_DECORATOR_ATTR from .common import TransactionRestServiceRegistryCase @@ -107,7 +108,7 @@ def _validator_my_instance_method(self): # the generated method_name is always the {http_method}_{method_name} method = routes["get_get"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -118,12 +119,13 @@ def _validator_my_instance_method(self): "/test_controller/ping/", ], "save_session": True, + "type": "restapi", }, ) method = routes["get_search"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -131,12 +133,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/search", "/test_controller/ping/"], "save_session": True, + "type": "restapi", }, ) method = routes["post_update"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -147,12 +150,13 @@ def _validator_my_instance_method(self): "/test_controller/ping/", ], "save_session": True, + "type": "restapi", }, ) method = routes["put_update"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["PUT"], "auth": "public", @@ -160,12 +164,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/"], "save_session": True, + "type": "restapi", }, ) method = routes["post_create"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -173,12 +178,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/create", "/test_controller/ping/"], "save_session": True, + "type": "restapi", }, ) method = routes["post_delete"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -186,12 +192,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping//delete"], "save_session": True, + "type": "restapi", }, ) method = routes["delete_delete"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["DELETE"], "auth": "public", @@ -199,12 +206,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/"], "save_session": True, + "type": "restapi", }, ) method = routes["post_my_method"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -212,12 +220,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/my_method"], "save_session": True, + "type": "restapi", }, ) method = routes["post_my_instance_method"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -225,6 +234,7 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping//my_instance_method"], "save_session": True, + "type": "restapi", }, ) @@ -284,7 +294,7 @@ def _get_partner_schema(self): method = routes["get_get"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -295,12 +305,13 @@ def _get_partner_schema(self): "/test_controller/partner/", ], "save_session": True, + "type": "restapi", }, ) method = routes["get_get_name"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -308,12 +319,13 @@ def _get_partner_schema(self): "csrf": False, "routes": ["/test_controller/partner//get_name"], "save_session": True, + "type": "restapi", }, ) method = routes["post_update_name"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "user", @@ -321,6 +333,7 @@ def _get_partner_schema(self): "csrf": False, "routes": ["/test_controller/partner//change_name"], "save_session": True, + "type": "restapi", }, ) @@ -378,7 +391,7 @@ def update_name(self, _id, **params): method = routes["get_get"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -389,12 +402,13 @@ def update_name(self, _id, **params): "/test_controller/partner/", ], "save_session": True, + "type": "restapi", }, ) method = routes["get_get_name"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -402,12 +416,13 @@ def update_name(self, _id, **params): "csrf": False, "routes": ["/test_controller/partner//get_name"], "save_session": True, + "type": "restapi", }, ) method = routes["post_update_name"] self.assertDictEqual( - method.routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "user", @@ -415,6 +430,7 @@ def update_name(self, _id, **params): "csrf": False, "routes": ["/test_controller/partner//change_name"], "save_session": True, + "type": "restapi", }, ) @@ -493,24 +509,35 @@ def _validator_get(self): ("save_session", default_save_session), ]: self.assertEqual( - routes["get_new_api_method_without"].routing[attr], + getattr(routes["get_new_api_method_without"], ROUTING_DECORATOR_ATTR)[ + attr + ], default, "wrong %s" % attr, ) - self.assertEqual(routes["get_new_api_method_with"].routing["auth"], "public") self.assertEqual( - routes["get_new_api_method_with"].routing["cors"], "http://my_site" + getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)["auth"], + "public", + ) + self.assertEqual( + getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)["cors"], + "http://my_site", ) self.assertEqual( - routes["get_new_api_method_with"].routing["csrf"], not default_csrf + getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)["csrf"], + not default_csrf, ) self.assertEqual( - routes["get_new_api_method_with"].routing["save_session"], + getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)[ + "save_session" + ], not default_save_session, ) self.assertEqual( - routes["get_get"].routing["auth"], default_auth, "wrong auth for get_get" + getattr(routes["get_get"], ROUTING_DECORATOR_ATTR)["auth"], + default_auth, + "wrong auth for get_get", ) for attr, default in [ @@ -520,12 +547,14 @@ def _validator_get(self): ("save_session", default_save_session), ]: self.assertEqual( - routes["my_controller_route_without"].routing[attr], + getattr(routes["my_controller_route_without"], ROUTING_DECORATOR_ATTR)[ + attr + ], default, "wrong %s" % attr, ) - routing = routes["my_controller_route_with"].routing + routing = getattr(routes["my_controller_route_with"], ROUTING_DECORATOR_ATTR) for attr, value in [ ("auth", "public"), ("cors", "http://with_cors"), @@ -539,7 +568,9 @@ def _validator_get(self): "wrong %s" % attr, ) self.assertEqual( - routes["my_controller_route_without_auth_2"].routing["auth"], + getattr( + routes["my_controller_route_without_auth_2"], ROUTING_DECORATOR_ATTR + )["auth"], None, "wrong auth for my_controller_route_without_auth_2", ) @@ -584,7 +615,9 @@ def _validator_get(self): routes = self._get_controller_route_methods(controller) self.assertEqual( - routes["get_new_api_method_with_public_or"].routing["auth"], + getattr( + routes["get_new_api_method_with_public_or"], ROUTING_DECORATOR_ATTR + )["auth"], "public_or_my_default_auth", ) @@ -622,7 +655,9 @@ def _validator_get(self): routes = self._get_controller_route_methods(controller) self.assertEqual( - routes["get_new_api_method_with_public_or"].routing["auth"], + getattr( + routes["get_new_api_method_with_public_or"], ROUTING_DECORATOR_ATTR + )["auth"], "my_default_auth", ) diff --git a/base_rest/tools.py b/base_rest/tools.py index 6449fcf7..92e78e85 100644 --- a/base_rest/tools.py +++ b/base_rest/tools.py @@ -6,6 +6,8 @@ _logger = logging.getLogger(__name__) +# Decorator attribute added on a route function (cfr Odoo's route) +ROUTING_DECORATOR_ATTR = "original_routing" SUPPORTED_META = ["title", "description", "example", "examples"] diff --git a/base_rest/views/openapi_template.xml b/base_rest/views/openapi_template.xml index 0bcf2d90..64e53e94 100644 --- a/base_rest/views/openapi_template.xml +++ b/base_rest/views/openapi_template.xml @@ -56,17 +56,6 @@
- - diff --git a/base_rest_auth_api_key/__manifest__.py b/base_rest_auth_api_key/__manifest__.py index b0f56cfc..6f5d4e30 100644 --- a/base_rest_auth_api_key/__manifest__.py +++ b/base_rest_auth_api_key/__manifest__.py @@ -6,17 +6,17 @@ "summary": """ Base Rest: Add support for the auth_api_key security policy into the openapi documentation""", - "version": "15.0.1.0.0", + "version": "16.0.1.0.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/rest-framework", "depends": ["base_rest", "auth_api_key"], "maintainers": ["lmignon"], - "installable": False, + "installable": True, "auto_install": True, "external_dependencies": { "python": [ - "apispec>=4.0.0", + "apispec", ] }, } diff --git a/base_rest_datamodel/__manifest__.py b/base_rest_datamodel/__manifest__.py index 9fcebb68..21b11528 100644 --- a/base_rest_datamodel/__manifest__.py +++ b/base_rest_datamodel/__manifest__.py @@ -5,13 +5,11 @@ "name": "Base Rest Datamodel", "summary": """ Datamodel binding for base_rest""", - "version": "15.0.1.1.0", + "version": "16.0.1.0.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/rest-framework", "depends": ["base_rest", "datamodel"], - "data": [], - "demo": [], "external_dependencies": {"python": ["apispec>=4.0.0", "marshmallow"]}, - "installable": False, + "installable": True, } diff --git a/base_rest_datamodel/tests/test_from_params.py b/base_rest_datamodel/tests/test_from_params.py index 348701c9..282ec210 100644 --- a/base_rest_datamodel/tests/test_from_params.py +++ b/base_rest_datamodel/tests/test_from_params.py @@ -1,6 +1,6 @@ # Copyright 2021 Wakari SRL # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -import mock +from unittest import mock from odoo.exceptions import UserError diff --git a/base_rest_datamodel/tests/test_response.py b/base_rest_datamodel/tests/test_response.py index 0424324f..731627b3 100644 --- a/base_rest_datamodel/tests/test_response.py +++ b/base_rest_datamodel/tests/test_response.py @@ -1,7 +1,8 @@ # Copyright 2021 Wakari SRL # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from unittest import mock + import marshmallow -import mock from odoo.addons.base_rest_datamodel import restapi from odoo.addons.datamodel import fields diff --git a/base_rest_demo/__manifest__.py b/base_rest_demo/__manifest__.py index 748da544..0baff6ef 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": "15.0.1.0.1", + "version": "16.0.1.0.0", "development_status": "Beta", "license": "LGPL-3", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", @@ -17,11 +17,10 @@ "base_rest_pydantic", "component", "extendable", + "pydantic", ], - "data": [], - "demo": [], "external_dependencies": { - "python": ["jsondiff", "extendable-pydantic", "pydantic"] + "python": ["jsondiff", "extendable-pydantic", "marshmallow", "pydantic"] }, - "installable": False, + "installable": True, } diff --git a/base_rest_demo/services/partner_new_api_services.py b/base_rest_demo/services/partner_new_api_services.py index c87e4f2f..75ec4a04 100644 --- a/base_rest_demo/services/partner_new_api_services.py +++ b/base_rest_demo/services/partner_new_api_services.py @@ -43,6 +43,36 @@ def get(self, _id): partner_info.is_company = partner.is_company return partner_info + @restapi.method( + [(["//get", "/"], "GET")], + output_param=Datamodel("partner.info"), + auth="public", + ) + def get_by_name(self, name): + """ + Get partner's information by name. + """ + partner = self.env["res.partner"].search([("name", "=", name)], limit=1) + if not partner: + raise FileNotFoundError + PartnerInfo = self.env.datamodels["partner.info"] + partner_info = PartnerInfo(partial=True) + partner_info.id = partner.id + partner_info.name = partner.name + partner_info.street = partner.street + partner_info.street2 = partner.street2 + partner_info.zip_code = partner.zip + partner_info.city = partner.city + partner_info.phone = partner.phone + partner_info.country = self.env.datamodels["country.info"]( + id=partner.country_id.id, name=partner.country_id.name + ) + partner_info.state = self.env.datamodels["state.info"]( + id=partner.state_id.id, name=partner.state_id.name + ) + partner_info.is_company = partner.is_company + return partner_info + @restapi.method( [(["/", "/search"], "GET")], input_param=Datamodel("partner.search.param"), diff --git a/base_rest_demo/tests/__init__.py b/base_rest_demo/tests/__init__.py index 12a4d728..93852ae8 100644 --- a/base_rest_demo/tests/__init__.py +++ b/base_rest_demo/tests/__init__.py @@ -1,3 +1,4 @@ from . import test_controller from . import test_openapi from . import test_exception +from . import test_service diff --git a/base_rest_demo/tests/test_controller.py b/base_rest_demo/tests/test_controller.py index f8676543..678e5261 100644 --- a/base_rest_demo/tests/test_controller.py +++ b/base_rest_demo/tests/test_controller.py @@ -1,7 +1,8 @@ # Copyright 2018 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo.http import controllers_per_module +# from odoo.http import controllers_per_module +from odoo.http import Controller from ..controllers.main import ( BaseRestDemoJwtApiController, @@ -16,38 +17,21 @@ class TestController(CommonCase): def test_controller_registry(self): # at the end of the start process, our tow controllers must into the # controller registered - controllers = controllers_per_module["base_rest_demo"] - self.assertEqual(len(controllers), 4) + controllers = Controller.children_classes.get("base_rest_demo", []) self.assertIn( - ( - "odoo.addons.base_rest_demo.controllers.main." - "BaseRestDemoPrivateApiController", - BaseRestDemoPrivateApiController, - ), + BaseRestDemoPrivateApiController, controllers, ) self.assertIn( - ( - "odoo.addons.base_rest_demo.controllers.main." - "BaseRestDemoPublicApiController", - BaseRestDemoPublicApiController, - ), + BaseRestDemoPublicApiController, controllers, ) self.assertIn( - ( - "odoo.addons.base_rest_demo.controllers.main." - "BaseRestDemoNewApiController", - BaseRestDemoNewApiController, - ), + BaseRestDemoNewApiController, controllers, ) self.assertIn( - ( - "odoo.addons.base_rest_demo.controllers.main." - "BaseRestDemoJwtApiController", - BaseRestDemoJwtApiController, - ), + BaseRestDemoJwtApiController, controllers, ) diff --git a/base_rest_demo/tests/test_exception.py b/base_rest_demo/tests/test_exception.py index d8e1aae7..3f5aa134 100644 --- a/base_rest_demo/tests/test_exception.py +++ b/base_rest_demo/tests/test_exception.py @@ -23,9 +23,13 @@ def setUp(self): super(TestException, self).setUp() self.opener.headers["Content-Type"] = "application/json" - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_user_error(self): - response = self.url_open("%s/user_error" % self.url, "{}") + response = self.url_open( + "%s/user_error" % self.url, + "{}", + headers={"Accept-language": "en-US,en;q=0.5"}, + ) self.assertEqual(response.status_code, 400) self.assertEqual(response.headers["content-type"], "application/json") body = json.loads(response.content.decode("utf-8")) @@ -38,7 +42,7 @@ def test_user_error(self): }, ) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_validation_error(self): response = self.url_open("%s/validation_error" % self.url, "{}") self.assertEqual(response.status_code, 400) @@ -53,7 +57,7 @@ def test_validation_error(self): }, ) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_session_expired(self): response = self.url_open("%s/session_expired" % self.url, "{}") self.assertEqual(response.status_code, 401) @@ -61,7 +65,7 @@ def test_session_expired(self): body = json.loads(response.content.decode("utf-8")) self.assertDictEqual(body, {"code": 401, "name": "Unauthorized"}) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_missing_error(self): response = self.url_open("%s/missing_error" % self.url, "{}") self.assertEqual(response.status_code, 404) @@ -69,7 +73,7 @@ def test_missing_error(self): body = json.loads(response.content.decode("utf-8")) self.assertDictEqual(body, {"code": 404, "name": "Not Found"}) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_access_error(self): response = self.url_open("%s/access_error" % self.url, "{}") self.assertEqual(response.status_code, 403) @@ -77,7 +81,7 @@ def test_access_error(self): body = json.loads(response.content.decode("utf-8")) self.assertDictEqual(body, {"code": 403, "name": "Forbidden"}) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_access_denied(self): response = self.url_open("%s/access_denied" % self.url, "{}") self.assertEqual(response.status_code, 403) @@ -85,15 +89,15 @@ def test_access_denied(self): body = json.loads(response.content.decode("utf-8")) self.assertDictEqual(body, {"code": 403, "name": "Forbidden"}) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_http_exception(self): response = self.url_open("%s/http_exception" % self.url, "{}") self.assertEqual(response.status_code, 405) - self.assertEqual(response.headers["content-type"], "text/html") + self.assertEqual(response.headers["content-type"], "text/html; charset=utf-8") body = response.content self.assertIn(b"Method Not Allowed", body) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_bare_exception(self): response = self.url_open("%s/bare_exception" % self.url, "{}") self.assertEqual(response.status_code, 500) diff --git a/base_rest_demo/tests/test_openapi.py b/base_rest_demo/tests/test_openapi.py index 08ecd501..70c2d65a 100644 --- a/base_rest_demo/tests/test_openapi.py +++ b/base_rest_demo/tests/test_openapi.py @@ -11,7 +11,7 @@ def _fix_server_url(self, openapi_def): # The server url depends of base_url. base_url depends of the odoo # config url = openapi_def["servers"][0]["url"] - url.replace("http://localhost:8069", self.base_url) + url = url.replace("http://localhost:8069", self.base_url) openapi_def["servers"][0]["url"] = url def _fix_openapi_components(self, openapi_def): @@ -26,10 +26,10 @@ def _fix_openapi_components(self, openapi_def): for key in unknow_keys: del security_components[key] - def assertOpenApiDef(self, service, canocincal_json_file, default_auth): + def assertOpenApiDef(self, service, canonical_json_file, default_auth): openapi_def = service.to_openapi(default_auth=default_auth) self._fix_openapi_components(openapi_def) - canonical_def = get_canonical_json(canocincal_json_file) + canonical_def = get_canonical_json(canonical_json_file) self._fix_server_url(canonical_def) self.assertFalse(jsondiff.diff(openapi_def, canonical_def)) diff --git a/base_rest_demo/tests/test_service.py b/base_rest_demo/tests/test_service.py new file mode 100644 index 00000000..b1df78a3 --- /dev/null +++ b/base_rest_demo/tests/test_service.py @@ -0,0 +1,54 @@ +# Copyright 2023 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import json +from urllib.parse import quote + +import odoo.tools +from odoo.tests import HttpCase +from odoo.tests.common import tagged + +from odoo.addons.base_rest.tests.common import RegistryMixin + + +@tagged("-at_install", "post_install") +class TestService(HttpCase, RegistryMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.setUpRegistry() + host = "127.0.0.1" + port = odoo.tools.config["http_port"] + cls.url = "http://%s:%d/base_rest_demo_api/new_api/partner" % (host, port) + cls.partner = cls.env.ref("base.partner_demo") + # Define a subset of the partner values to check against returned payload + cls.expected_partner_values = { + "country": { + "id": cls.partner.country_id.id, + "name": cls.partner.country_id.name, + }, + "id": cls.partner.id, + "name": cls.partner.name, + "state": {"id": cls.partner.state_id.id, "name": cls.partner.state_id.name}, + } + + def test_get(self): + """Test a new api GET method""" + self.authenticate("admin", "admin") + self.opener.headers["Content-Type"] = "application/json" + response = self.url_open( + "%s/%s" % (self.url, self.partner.id), + headers={"Accept-language": "en-US,en;q=0.5"}, + ) + body = json.loads(response.content.decode("utf-8")) + self.assertEqual(body, body | self.expected_partner_values) + + def test_get_by_name(self): + """Test a new api GET method with string argument""" + self.authenticate("admin", "admin") + self.opener.headers["Content-Type"] = "application/json" + response = self.url_open( + "%s/%s" % (self.url, quote(self.partner.name)), + headers={"Accept-language": "en-US,en;q=0.5"}, + ) + body = json.loads(response.content.decode("utf-8")) + self.assertEqual(body, body | self.expected_partner_values) diff --git a/base_rest_pydantic/__manifest__.py b/base_rest_pydantic/__manifest__.py index fad88ac6..ea83d15c 100644 --- a/base_rest_pydantic/__manifest__.py +++ b/base_rest_pydantic/__manifest__.py @@ -5,14 +5,12 @@ "name": "Base Rest Datamodel", "summary": """ Pydantic binding for base_rest""", - "version": "15.0.4.3.0", + "version": "16.0.1.0.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/rest-framework", "depends": ["base_rest"], - "data": [], - "demo": [], - "installable": False, + "installable": True, "external_dependencies": { "python": [ "pydantic", diff --git a/base_rest_pydantic/tests/test_from_params.py b/base_rest_pydantic/tests/test_from_params.py index 2933d5f4..4316bdd8 100644 --- a/base_rest_pydantic/tests/test_from_params.py +++ b/base_rest_pydantic/tests/test_from_params.py @@ -1,8 +1,7 @@ # Copyright 2021 Wakari SRL # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from typing import Type - -import mock +from unittest import mock from odoo.exceptions import UserError from odoo.tests.common import TransactionCase diff --git a/base_rest_pydantic/tests/test_response.py b/base_rest_pydantic/tests/test_response.py index 71a4f706..baf0ec52 100644 --- a/base_rest_pydantic/tests/test_response.py +++ b/base_rest_pydantic/tests/test_response.py @@ -1,8 +1,7 @@ # Copyright 2021 Wakari SRL # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from typing import List - -import mock +from unittest import mock from odoo.tests.common import TransactionCase diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 45a117b4..af37c113 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,15 +6,12 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "15.0.1.0.1", + "version": "16.0.1.0.0", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", "maintainers": ["lmignon"], "website": "https://github.com/OCA/rest-framework", - "depends": [], - "data": [], - "demo": [], "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, - "installable": False, + "installable": True, } diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index fde04c0d..29a2002b 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -2,7 +2,8 @@ # Copyright 2019 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) -import mock +from unittest import mock + from marshmallow_objects.models import Model as MarshmallowModel from odoo import SUPERUSER_ID, api diff --git a/extendable/__manifest__.py b/extendable/__manifest__.py index abd9c3e9..fa684dc0 100644 --- a/extendable/__manifest__.py +++ b/extendable/__manifest__.py @@ -5,13 +5,12 @@ "name": "Extendable", "summary": """ Extendable classes registry loader for Odoo""", - "version": "15.0.1.0.1", + "version": "16.0.1.0.0", "development_status": "Beta", "maintainers": ["lmignon"], "license": "LGPL-3", "author": "ACSONE SA/NV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/rest-framework", - "depends": [], "external_dependencies": {"python": ["extendable"]}, - "installable": False, + "installable": True, } diff --git a/extendable/models/ir_http.py b/extendable/models/ir_http.py index 577179c4..e050ef0e 100644 --- a/extendable/models/ir_http.py +++ b/extendable/models/ir_http.py @@ -15,9 +15,9 @@ class IrHttp(models.AbstractModel): _inherit = "ir.http" @classmethod - def _dispatch(cls): + def _dispatch(cls, endpoint): with cls._extendable_context_registry(): - return super()._dispatch() + return super()._dispatch(endpoint) @classmethod @contextmanager diff --git a/pydantic/__manifest__.py b/pydantic/__manifest__.py index 4af58b5f..811629d1 100644 --- a/pydantic/__manifest__.py +++ b/pydantic/__manifest__.py @@ -5,7 +5,7 @@ "name": "Pydantic", "summary": """ Utility addon to ease mapping between Pydantic and Odoo models""", - "version": "15.0.1.1.1", + "version": "16.0.1.0.0", "development_status": "Beta", "license": "LGPL-3", "maintainers": ["lmignon"], @@ -17,5 +17,5 @@ "external_dependencies": { "python": ["pydantic", "contextvars", "typing-extensions"] }, - "installable": False, + "installable": True, } diff --git a/requirements.txt b/requirements.txt index b64f49c0..33cb03c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,16 @@ # generated from manifests external_dependencies +apispec +apispec>=4.0.0 +cerberus +contextvars +extendable +extendable-pydantic graphene graphql_server +jsondiff +marshmallow +marshmallow-objects>=2.0.0 +parse-accept-language +pydantic +pyquerystring +typing-extensions diff --git a/setup/base_rest/odoo/addons/base_rest b/setup/base_rest/odoo/addons/base_rest new file mode 120000 index 00000000..ead43a36 --- /dev/null +++ b/setup/base_rest/odoo/addons/base_rest @@ -0,0 +1 @@ +../../../../base_rest \ No newline at end of file diff --git a/setup/base_rest/setup.py b/setup/base_rest/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/base_rest_auth_api_key/odoo/addons/base_rest_auth_api_key b/setup/base_rest_auth_api_key/odoo/addons/base_rest_auth_api_key new file mode 120000 index 00000000..58566db9 --- /dev/null +++ b/setup/base_rest_auth_api_key/odoo/addons/base_rest_auth_api_key @@ -0,0 +1 @@ +../../../../base_rest_auth_api_key \ No newline at end of file diff --git a/setup/base_rest_auth_api_key/setup.py b/setup/base_rest_auth_api_key/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest_auth_api_key/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/base_rest_datamodel/odoo/addons/base_rest_datamodel b/setup/base_rest_datamodel/odoo/addons/base_rest_datamodel new file mode 120000 index 00000000..a809cfdf --- /dev/null +++ b/setup/base_rest_datamodel/odoo/addons/base_rest_datamodel @@ -0,0 +1 @@ +../../../../base_rest_datamodel \ No newline at end of file diff --git a/setup/base_rest_datamodel/setup.py b/setup/base_rest_datamodel/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest_datamodel/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/base_rest_demo/odoo/addons/base_rest_demo b/setup/base_rest_demo/odoo/addons/base_rest_demo new file mode 120000 index 00000000..cad58c8b --- /dev/null +++ b/setup/base_rest_demo/odoo/addons/base_rest_demo @@ -0,0 +1 @@ +../../../../base_rest_demo \ No newline at end of file diff --git a/setup/base_rest_demo/setup.py b/setup/base_rest_demo/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest_demo/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/base_rest_pydantic/odoo/addons/base_rest_pydantic b/setup/base_rest_pydantic/odoo/addons/base_rest_pydantic new file mode 120000 index 00000000..07264e9f --- /dev/null +++ b/setup/base_rest_pydantic/odoo/addons/base_rest_pydantic @@ -0,0 +1 @@ +../../../../base_rest_pydantic \ No newline at end of file diff --git a/setup/base_rest_pydantic/setup.py b/setup/base_rest_pydantic/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest_pydantic/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/datamodel/odoo/addons/datamodel b/setup/datamodel/odoo/addons/datamodel new file mode 120000 index 00000000..790184d6 --- /dev/null +++ b/setup/datamodel/odoo/addons/datamodel @@ -0,0 +1 @@ +../../../../datamodel \ No newline at end of file diff --git a/setup/datamodel/setup.py b/setup/datamodel/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/datamodel/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/extendable/odoo/addons/extendable b/setup/extendable/odoo/addons/extendable new file mode 120000 index 00000000..967b5984 --- /dev/null +++ b/setup/extendable/odoo/addons/extendable @@ -0,0 +1 @@ +../../../../extendable \ No newline at end of file diff --git a/setup/extendable/setup.py b/setup/extendable/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/extendable/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/pydantic/odoo/addons/pydantic b/setup/pydantic/odoo/addons/pydantic new file mode 120000 index 00000000..775eac29 --- /dev/null +++ b/setup/pydantic/odoo/addons/pydantic @@ -0,0 +1 @@ +../../../../pydantic \ No newline at end of file diff --git a/setup/pydantic/setup.py b/setup/pydantic/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/pydantic/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)