-
-
Notifications
You must be signed in to change notification settings - Fork 354
[14.0][ADD] pydantic: allows pydantic use into Odoo #218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
01d3772
[ADD] pydantic: allows pydantic usage into Odoo
lmignon 1418e06
[IMP] Pydantic: Improve API to be more pythonic
lmignon 25dd584
fixup! [IMP] Pydantic: Improve API to be more pythonic
lmignon bfa2925
[FIX] pydantic: Makes issubclass on BaseModel working with generics
lmignon 9bf4901
[IMP] pydantic: Add tests on models created by inheritance
lmignon fb20133
[IMP] pydantic: Add tests on models created by inheritance
lmignon 9ba9dbd
[IMP] Lift typing-extensions version restriction
qgroulard 9719377
[IMP] Odoo getter: Better handle empty fields
qgroulard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| ======== | ||
| Pydantic | ||
| ======== | ||
|
|
||
| .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
| !! This file is generated by oca-gen-addon-readme !! | ||
| !! changes will be overwritten. !! | ||
| !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
|
|
||
| .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png | ||
| :target: https://odoo-community.org/page/development-status | ||
| :alt: Beta | ||
| .. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png | ||
| :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html | ||
| :alt: License: LGPL-3 | ||
| .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github | ||
| :target: https://github.com/OCA/rest-framework/tree/14.0/pydantic | ||
| :alt: OCA/rest-framework | ||
| .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png | ||
| :target: https://translation.odoo-community.org/projects/rest-framework-14-0/rest-framework-14-0-pydantic | ||
| :alt: Translate me on Weblate | ||
| .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png | ||
| :target: https://runbot.odoo-community.org/runbot/271/14.0 | ||
| :alt: Try me on Runbot | ||
|
|
||
| |badge1| |badge2| |badge3| |badge4| |badge5| | ||
|
|
||
| This addon allows you to define inheritable `Pydantic classes <https://pydantic-docs.helpmanual.io/>`_. | ||
|
|
||
| **Table of contents** | ||
|
|
||
| .. contents:: | ||
| :local: | ||
|
|
||
| Usage | ||
| ===== | ||
|
|
||
| To define your own pydantic model you just need to create a class that inherits from | ||
| ``odoo.addons.pydantic.models.BaseModel`` or a subclass of. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from odoo.addons.pydantic.models import BaseModel | ||
| from pydantic import Field | ||
|
|
||
|
|
||
| class PartnerShortInfo(BaseModel): | ||
| id: str | ||
| name: str | ||
|
|
||
|
|
||
| class PartnerInfo(BaseModel): | ||
| street: str | ||
| street2: str = None | ||
| zip_code: str = None | ||
| city: str | ||
| phone: str = None | ||
| is_componay : bool = Field(None) | ||
|
|
||
|
|
||
| In the preceding code, 2 new models are created, one for each class. If you | ||
| want to extend an existing model, you must pass the extended pydantic model | ||
| trough the `extends` parameter on class declaration. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| class Coordinate(models.BaseModel): | ||
| lat = 0.1 | ||
| lng = 10.1 | ||
|
|
||
| class PartnerInfoWithCoordintate(PartnerInfo, extends=PartnerInfo): | ||
| coordinate: Coordinate = None | ||
|
|
||
| `PartnerInfoWithCoordintate` extends `PartnerInfo`. IOW, Base class are now the | ||
| same and define the same fields and methods. They can be used indifferently into | ||
| the code. All the logic will be provided by the aggregated class. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| partner1 = PartnerInfo.construct() | ||
| partner2 = PartnerInfoWithCoordintate.construct() | ||
|
|
||
| assert partner1.__class__ == partner2.__class__ | ||
| assert PartnerInfo.schema() == PartnerInfoWithCoordinate.schema() | ||
|
|
||
| .. note:: | ||
|
|
||
| Since validation occurs on instance creation, it's important to avoid to | ||
| create an instance of a Pydantic class by usign the normal instance | ||
| constructor `partner = PartnerInfo(..)`. In such a case, if the class is | ||
| extended by an other addon and a required field is added, this code will | ||
| no more work. It's therefore a good practice to use the `construct()` class | ||
| method to create a pydantic instance. | ||
lmignon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| .. caution:: | ||
|
|
||
| Adding required fields to an existing data structure into an extension | ||
| addon violates the `Liskov substitution principle`_ and should generally | ||
| be avoided. This is certainly forbidden in requests data structures. | ||
| When extending response data structures this could be useful to document | ||
| new fields that are guaranteed to be present when extension addons are | ||
| installed. | ||
|
|
||
| In contrast to Odoo, access to a Pydantic class is not done through a specific | ||
| registry. To use a Pydantic class, you just have to import it in your module | ||
| and write your code like in any other python application. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from odoo.addons.my_addons.datamodels import PartnerInfo | ||
| from odoo import models | ||
|
|
||
| class ResPartner(models.Basemodel): | ||
| _inherit = "res.partner" | ||
|
|
||
| def to_json(self): | ||
| return [i._to_partner_info().json() for i in self] | ||
|
|
||
| def _to_partner_info(self): | ||
| self.ensure_one() | ||
| pInfo = PartnerInfo.construct(id=self.id, name=self.name, street=self.street, city=self.city) | ||
| return pInfo | ||
|
|
||
|
|
||
| To support pydantic models that map to Odoo models, Pydantic model instances can | ||
| be created from arbitrary odoo model instances by mapping fields from odoo | ||
| models to fields defined by the pydantic model. To ease the mapping, the addon | ||
| provide a utility class `odoo.addons.pydantic.utils.GenericOdooGetter`. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import pydantic | ||
| from odoo.addons.pydantic import models, utils | ||
|
|
||
| class Group(models.BaseModel): | ||
| name: str | ||
|
|
||
| class Config: | ||
| orm_mode = True | ||
| getter_dict = utils.GenericOdooGetter | ||
|
|
||
| class UserInfo(models.BaseModel): | ||
| name: str | ||
| groups: List[Group] = pydantic.Field(alias="groups_id") | ||
|
|
||
| class Config: | ||
| orm_mode = True | ||
| getter_dict = utils.GenericOdooGetter | ||
|
|
||
| user = self.env.user | ||
| user_info = UserInfo.from_orm(user) | ||
|
|
||
| See the official `Pydantic documentation`_ to discover all the available functionalities. | ||
|
|
||
| .. _`Liskov substitution principle`: https://en.wikipedia.org/wiki/Liskov_substitution_principle | ||
| .. _`Pydantic documentation`: https://pydantic-docs.helpmanual.io/ | ||
|
|
||
| Known issues / Roadmap | ||
| ====================== | ||
|
|
||
| The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Apydantic>`_ | ||
| and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Apydantic>`_ can | ||
| be found on GitHub. | ||
|
|
||
| Bug Tracker | ||
| =========== | ||
|
|
||
| Bugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/issues>`_. | ||
| In case of trouble, please check there if your issue has already been reported. | ||
| If you spotted it first, help us smashing it by providing a detailed and welcomed | ||
| `feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20pydantic%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. | ||
|
|
||
| Do not contact contributors directly about support or help with technical issues. | ||
|
|
||
| Credits | ||
| ======= | ||
|
|
||
| Authors | ||
| ~~~~~~~ | ||
|
|
||
| * ACSONE SA/NV | ||
|
|
||
| Contributors | ||
| ~~~~~~~~~~~~ | ||
|
|
||
| * Laurent Mignon <laurent.mignon@acsone.eu> | ||
|
|
||
| Maintainers | ||
| ~~~~~~~~~~~ | ||
|
|
||
| This module is maintained by the OCA. | ||
|
|
||
| .. image:: https://odoo-community.org/logo.png | ||
| :alt: Odoo Community Association | ||
| :target: https://odoo-community.org | ||
|
|
||
| OCA, or the Odoo Community Association, is a nonprofit organization whose | ||
| mission is to support the collaborative development of Odoo features and | ||
| promote its widespread use. | ||
|
|
||
| .. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px | ||
| :target: https://github.com/lmignon | ||
| :alt: lmignon | ||
|
|
||
| Current `maintainer <https://odoo-community.org/page/maintainer-role>`__: | ||
|
|
||
| |maintainer-lmignon| | ||
|
|
||
| This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/14.0/pydantic>`_ project on GitHub. | ||
|
|
||
| You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| from . import builder | ||
| from . import models | ||
| from . import registry | ||
| from . import ir_http |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Copyright 2021 ACSONE SA/NV | ||
| # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) | ||
|
|
||
| { | ||
| "name": "Pydantic", | ||
| "summary": """ | ||
| Enhance pydantic to allow model extension""", | ||
| "version": "14.0.1.0.0", | ||
| "development_status": "Beta", | ||
| "license": "LGPL-3", | ||
| "maintainers": ["lmignon"], | ||
| "author": "ACSONE SA/NV,Odoo Community Association (OCA)", | ||
| "website": "https://github.com/OCA/rest-framework", | ||
| "depends": [], | ||
| "data": [], | ||
| "demo": [], | ||
| "external_dependencies": { | ||
| "python": ["pydantic", "contextvars", "typing-extensions"] | ||
| }, | ||
| "installable": True, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # Copyright 2021 ACSONE SA/NV | ||
| # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) | ||
|
|
||
| """ | ||
|
|
||
| Pydantic Models Builder | ||
| ======================= | ||
|
|
||
| Build the pydantic models at the build of a registry by resolving the | ||
| inheritance declaration and ForwardRefs type declaration into the models | ||
|
|
||
| """ | ||
| from typing import List, Optional | ||
|
|
||
| import odoo | ||
| from odoo import api, models as omodels | ||
|
|
||
| from .registry import PydanticClassesRegistry, _pydantic_classes_databases | ||
|
|
||
|
|
||
| class PydanticClassesBuilder(omodels.AbstractModel): | ||
| """Build the component classes | ||
|
|
||
| And register them in a global registry. | ||
|
|
||
| Every time an Odoo registry is built, the know pydantic models are cleared and | ||
| rebuilt as well. The pydantic classes are built by taking every models with | ||
| a ``__xreg_name__`` and applying pydantic models with an ``__xreg_base_names__`` | ||
| upon them. | ||
|
|
||
| The final pydantic classes are registered in global registry. | ||
|
|
||
| This class is an Odoo model, allowing us to hook the build of the | ||
| pydantic classes at the end of the Odoo's registry loading, using | ||
| ``_register_hook``. This method is called after all modules are loaded, so | ||
| we are sure that we have all the components Classes and in the correct | ||
| order. | ||
|
|
||
| """ | ||
|
|
||
| _name = "pydantic.classes.builder" | ||
| _description = "Pydantic Classes Builder" | ||
|
|
||
| def _register_hook(self): | ||
| # This method is called by Odoo when the registry is built, | ||
| # so in case the registry is rebuilt (cache invalidation, ...), | ||
| # we have to to rebuild the components. We use a new | ||
| # registry so we have an empty cache and we'll add components in it. | ||
| registry = self._init_global_registry() | ||
| self.build_registry(registry) | ||
|
|
||
| @api.model | ||
| def _init_global_registry(self): | ||
| registry = PydanticClassesRegistry() | ||
| _pydantic_classes_databases[self.env.cr.dbname] = registry | ||
| return registry | ||
|
|
||
| @api.model | ||
| def build_registry( | ||
| self, | ||
| registry: PydanticClassesRegistry, | ||
| states: Optional[List[str]] = None, | ||
| exclude_addons: Optional[List[str]] = None, | ||
| ): | ||
| if not states: | ||
| states = ("installed", "to upgrade") | ||
| # lookup all the installed (or about to be) addons and generate | ||
| # the graph, so we can load the components following the order | ||
| # of the addons' dependencies | ||
| graph = odoo.modules.graph.Graph() | ||
| graph.add_module(self.env.cr, "base") | ||
|
|
||
| query = "SELECT name " "FROM ir_module_module " "WHERE state IN %s " | ||
| params = [tuple(states)] | ||
| if exclude_addons: | ||
| query += " AND name NOT IN %s " | ||
| params.append(tuple(exclude_addons)) | ||
| self.env.cr.execute(query, params) | ||
|
|
||
| module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph] | ||
| graph.add_modules(self.env.cr, module_list) | ||
|
|
||
| registry.init_registry([m.name for m in graph]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Copyright 2021 ACSONE SA/NV | ||
| # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) | ||
|
|
||
| # define context vars to hold the pydantic registry | ||
|
|
||
| from contextvars import ContextVar | ||
|
|
||
| odoo_pydantic_registry = ContextVar("pydantic_registry") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # Copyright 2021 ACSONE SA/NV | ||
| # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) | ||
|
|
||
| from contextlib import contextmanager | ||
|
|
||
| from odoo import models | ||
| from odoo.http import request | ||
|
|
||
| from .context import odoo_pydantic_registry | ||
| from .registry import _pydantic_classes_databases | ||
|
|
||
|
|
||
| class IrHttp(models.AbstractModel): | ||
| _inherit = "ir.http" | ||
|
|
||
| @classmethod | ||
| def _dispatch(cls): | ||
| with cls._pydantic_context_registry(): | ||
| return super()._dispatch() | ||
|
|
||
| @classmethod | ||
| @contextmanager | ||
| def _pydantic_context_registry(cls): | ||
| registry = _pydantic_classes_databases.get(request.env.cr.dbname, {}) | ||
| token = odoo_pydantic_registry.set(registry) | ||
| try: | ||
| yield | ||
| finally: | ||
| odoo_pydantic_registry.reset(token) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.