From cc47234295ed893e5f0583adab10513ef4eac3ef Mon Sep 17 00:00:00 2001 From: Nikul-OSI Date: Mon, 28 Nov 2022 18:50:51 +0530 Subject: [PATCH 1/2] [WIP] [16.0] base_rest --- .pre-commit-config.yaml | 6 - base_rest/__manifest__.py | 8 +- base_rest/controllers/api_docs.py | 4 +- base_rest/controllers/main.py | 115 ++++++++++++------ base_rest/http.py | 49 ++++---- base_rest/models/rest_service_registration.py | 12 +- base_rest/static/src/js/swagger.js | 16 +++ base_rest/static/src/js/swagger_ui.js | 2 +- base_rest/views/openapi_template.xml | 13 +- base_rest_datamodel/__manifest__.py | 6 +- base_rest_demo/__manifest__.py | 6 +- base_rest_demo/tests/test_controller.py | 6 +- base_rest_pydantic/__manifest__.py | 6 +- datamodel/__manifest__.py | 7 +- datamodel/tests/test_build_datamodel.py | 3 +- extendable/__manifest__.py | 5 +- extendable/models/ir_http.py | 4 +- requirements.txt | 9 ++ setup/base_rest/odoo/addons/base_rest | 1 + setup/base_rest/setup.py | 6 + .../odoo/addons/base_rest_datamodel | 1 + setup/base_rest_datamodel/setup.py | 6 + .../base_rest_demo/odoo/addons/base_rest_demo | 1 + setup/base_rest_demo/setup.py | 6 + .../odoo/addons/base_rest_pydantic | 1 + setup/base_rest_pydantic/setup.py | 6 + setup/datamodel/odoo/addons/datamodel | 1 + setup/datamodel/setup.py | 6 + setup/extendable/odoo/addons/extendable | 1 + setup/extendable/setup.py | 6 + 30 files changed, 202 insertions(+), 117 deletions(-) create mode 100644 base_rest/static/src/js/swagger.js create mode 120000 setup/base_rest/odoo/addons/base_rest create mode 100644 setup/base_rest/setup.py create mode 120000 setup/base_rest_datamodel/odoo/addons/base_rest_datamodel create mode 100644 setup/base_rest_datamodel/setup.py create mode 120000 setup/base_rest_demo/odoo/addons/base_rest_demo create mode 100644 setup/base_rest_demo/setup.py create mode 120000 setup/base_rest_pydantic/odoo/addons/base_rest_pydantic create mode 100644 setup/base_rest_pydantic/setup.py create mode 120000 setup/datamodel/odoo/addons/datamodel create mode 100644 setup/datamodel/setup.py create mode 120000 setup/extendable/odoo/addons/extendable create mode 100644 setup/extendable/setup.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ebace19..7dc70271 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +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/| ^graphql_base/| ^graphql_demo/| ^model_serializer/| diff --git a/base_rest/__manifest__.py b/base_rest/__manifest__.py index fb9e6a1a..8164844e 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": "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)", @@ -18,12 +18,12 @@ "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", @@ -32,5 +32,5 @@ "apispec>=4.0.0", ] }, - "installable": False, + "installable": True, } diff --git a/base_rest/controllers/api_docs.py b/base_rest/controllers/api_docs.py index fcbc171e..e051787f 100644 --- a/base_rest/controllers/api_docs.py +++ b/base_rest/controllers/api_docs.py @@ -42,9 +42,7 @@ def api(self, collection, service_name): service, controller_class, ): - openapi_doc = service.to_openapi( - default_auth=controller_class._default_auth - ) + openapi_doc = service.to_openapi(default_auth=controller_class) return self.make_json_response(openapi_doc) def _get_api_urls(self): diff --git a/base_rest/controllers/main.py b/base_rest/controllers/main.py index 3bfa0699..b2fd1ef1 100644 --- a/base_rest/controllers/main.py +++ b/base_rest/controllers/main.py @@ -7,12 +7,15 @@ 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 +from odoo.addons.component.core import WorkContext from ..core import _rest_controllers_per_module +# from odoo.http import Controller, ControllerType, Response, request + + _logger = logging.getLogger(__name__) @@ -25,43 +28,44 @@ 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 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 +134,39 @@ 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__ + ): + 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: + path = cls.__module__.split(".") + module = path[2] if path[:2] == ["odoo", "addons"] else "" + cls._module = module # _get_addon_name(cls.__module__, cls._module) + _rest_controllers_per_module[cls].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][-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..a473b899 100644 --- a/base_rest/http.py +++ b/base_rest/http.py @@ -21,7 +21,6 @@ ) from werkzeug.utils import escape -import odoo from odoo.exceptions import ( AccessDenied, AccessError, @@ -29,11 +28,15 @@ UserError, ValidationError, ) -from odoo.http import HttpRequest, Root, SessionExpiredException, request + +# from odoo.http import HttpRequest, Root, SessionExpiredException, request +from odoo.http import Request, SessionExpiredException, request from odoo.tools import ustr from odoo.tools.config import config -from .core import _rest_services_routes +# import odoo +# from .core import _rest_services_routes + _logger = logging.getLogger(__name__) @@ -116,7 +119,7 @@ def get_headers(environ=None): return exception -class HttpRestRequest(HttpRequest): +class HttpRestRequest(Request): """Http request that always return json, usefull for rest api""" def __init__(self, httprequest): @@ -216,22 +219,22 @@ def make_json_response(self, data, headers=None, cookies=None): 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 +# ori_get_request = Application.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) +# +# +# Application.get_request = get_request diff --git a/base_rest/models/rest_service_registration.py b/base_rest/models/rest_service_registration.py index d5d6b975..88cf2d1e 100644 --- a/base_rest/models/rest_service_registration.py +++ b/base_rest/models/rest_service_registration.py @@ -61,11 +61,14 @@ 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 + # ] controllers = [ - (name, cls) for name, cls in controllers if "RestController" not in name + (name) for name in controllers if "RestController" not in name.__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) @@ -104,7 +107,8 @@ def _build_controller(self, service, controller_def): # 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] = name_class + # http.controllers_per_module[addon_name].append(name_class) self._apply_defaults_to_controller_routes(controller_class=ctrl_cls) def _apply_defaults_to_controller_routes(self, controller_class): 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/views/openapi_template.xml b/base_rest/views/openapi_template.xml index 0bcf2d90..de092f1e 100644 --- a/base_rest/views/openapi_template.xml +++ b/base_rest/views/openapi_template.xml @@ -48,7 +48,7 @@ /> - + @@ -56,17 +56,6 @@
- - 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_demo/__manifest__.py b/base_rest_demo/__manifest__.py index 748da544..719c0d18 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)", @@ -18,10 +18,8 @@ "component", "extendable", ], - "data": [], - "demo": [], "external_dependencies": { "python": ["jsondiff", "extendable-pydantic", "pydantic"] }, - "installable": False, + "installable": True, } diff --git a/base_rest_demo/tests/test_controller.py b/base_rest_demo/tests/test_controller.py index f8676543..aae37e8b 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,7 +17,8 @@ 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"] + controllers = Controller.children_classes.get("base_rest_demo", []) + # controllers = controllers_per_module["base_rest_demo"] self.assertEqual(len(controllers), 4) self.assertIn( 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/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/requirements.txt b/requirements.txt index 9cd16292..b3023acd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,10 @@ # generated from manifests external_dependencies +apispec>=4.0.0 +cerberus +extendable +extendable-pydantic +jsondiff +marshmallow +parse-accept-language +pydantic +pyquerystring 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_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, +) From 858fd5a73006699dfc90167fc435a00c99dcdff2 Mon Sep 17 00:00:00 2001 From: Nikul-OSI Date: Fri, 2 Dec 2022 15:12:49 +0530 Subject: [PATCH 2/2] [FIX] test --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index b3023acd..8436bf7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ extendable extendable-pydantic jsondiff marshmallow +marshmallow-objects>=2.0.0 parse-accept-language pydantic pyquerystring