-
-
Notifications
You must be signed in to change notification settings - Fork 302
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ADD] pydantic: allows pydantic usage into Odoo
- Loading branch information
Showing
19 changed files
with
1,558 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,141 @@ | ||
======== | ||
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 | ||
|
||
|badge1| |badge2| |badge3| | ||
|
||
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`` | ||
|
||
.. code-block:: python | ||
from odoo.addons.pydantic.models import BaseModel | ||
from pydantic import Field | ||
class PartnerShortInfo(BaseModel): | ||
_name = "partner.short.info" | ||
id: str | ||
name: str | ||
class PartnerInfo(BaseModel): | ||
_name = "partner.info" | ||
_inherit = "partner.short.info" | ||
street: str | ||
street2: str = None | ||
zip_code: str = None | ||
city: str | ||
phone: str = None | ||
is_componay : bool = Field(None) | ||
As for odoo models, you can extend the `base` pydantic model by inheriting of `base`. | ||
|
||
.. code-block:: python | ||
class Base(BaseModel): | ||
_inherit = "base" | ||
def _my_method(self): | ||
pass | ||
Pydantic model classes are available through the `pydantic_registry` registry provided by the Odoo's environment. | ||
|
||
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, | ||
your pydantic model should inherit from 'odoo_orm_mode' | ||
|
||
.. code-block:: python | ||
class UserInfo(models.BaseModel): | ||
_name = "user" | ||
_inherit = "odoo_orm_mode" | ||
name: str | ||
groups: List["group"] = pydantic.Field(alias="groups_id") | ||
class Group(models.BaseModel): | ||
_name="group" | ||
_inherit = "odoo_orm_mode" | ||
name: str | ||
user = self.env.user | ||
UserInfoCls = self.env.pydantic_registry["user"] | ||
user_info = UserInfoCls.from_orm(user) | ||
See the official Pydantic documentation_ to discover all the available functionalities. | ||
|
||
.. _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 <[email protected]> | ||
|
||
Maintainers | ||
~~~~~~~~~~~ | ||
|
||
.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px | ||
:target: https://github.com/lmignon | ||
:alt: lmignon | ||
|
||
Current maintainer: | ||
|
||
|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. |
This file contains 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,3 @@ | ||
from . import builder | ||
from . import models | ||
from . import registry |
This file contains 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,23 @@ | ||
# 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", | ||
] | ||
}, | ||
"installable": True, | ||
} |
This file contains 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,104 @@ | ||
# 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 | ||
|
||
from .registry import PydanticClassesRegistry, _pydantic_classes_databases | ||
|
||
|
||
class PydanticClassesBuilder(models.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 ``_name`` and applying pydantic models with an ``_inherits`` 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) | ||
registry.ready = True | ||
|
||
@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) | ||
|
||
# Here we have a graph of installed modules. By iterating on the graph, | ||
# we get the modules from the most generic one to the most specialized | ||
# one. We walk through the graph to build the definition of the classes | ||
# to assemble. The goal is to have for each class name the final | ||
# picture of all the pieces required to build the right hierarchy. | ||
# It's required to avoid to change the bases of an already build class | ||
# each time a module extend the initial implementation as Odoo is | ||
# doing with `Model`. The final definition of a class could depend on | ||
# the potential metaclass associated to the class (a metaclass is a | ||
# class factory). It's therefore not safe to modify on the fly | ||
# the __bases__ attribute of a class once it's constructed since | ||
# the factory method of the metaclass depends these 'bases' | ||
# __new__(mcs, name, bases, new_namespace, **kwargs). | ||
# 'bases' could therefore be processed by the factory in a way or an | ||
# other to build the final class. If you modify the bases after the | ||
# class creation, the logic implemented by the factory will not be | ||
# applied to the new bases and your class could be in an incoherent | ||
# state. | ||
for module in graph: | ||
registry.load_pydantic_classes(module) | ||
registry.build_pydantic_classes() | ||
registry.update_forward_refs() |
This file contains 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,102 @@ | ||
# Copyright 2021 ACSONE SA/NV | ||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) | ||
|
||
import collections | ||
from typing import DefaultDict, List, TypeVar, Union | ||
|
||
import pydantic | ||
from pydantic.utils import ClassAttribute | ||
|
||
from . import utils | ||
|
||
ModelType = TypeVar("Model", bound="BaseModel") | ||
|
||
|
||
class BaseModel(pydantic.BaseModel): | ||
_name: str = None | ||
_inherit: Union[List[str], str] = None | ||
|
||
_pydantic_classes_by_module: DefaultDict[ | ||
str, List[ModelType] | ||
] = collections.defaultdict(list) | ||
|
||
def __init_subclass__(cls, **kwargs): | ||
super().__init_subclass__(**kwargs) | ||
if cls != BaseModel: | ||
cls.__normalize__definition__() | ||
cls.__register__() | ||
|
||
@classmethod | ||
def __normalize__definition__(cls): | ||
"""Normalize class definition | ||
Compute the module name | ||
Compute and validate the model name if class is a subclass | ||
of another BaseModel; | ||
Ensure that _inherit is a list | ||
""" | ||
parents = cls._inherit | ||
if isinstance(parents, str): | ||
parents = [parents] | ||
elif parents is None: | ||
parents = [] | ||
name = cls._name or (parents[0] if len(parents) == 1 else None) | ||
if not name: | ||
raise TypeError(f"Extended pydantic class {cls} must have a name") | ||
cls._name = ClassAttribute("_module", name) | ||
cls._module = ClassAttribute("_module", utils._get_addon_name(cls.__module__)) | ||
cls.__config__.title = name | ||
# all BaseModels except 'base' implicitly inherit from 'base' | ||
if name != "base": | ||
parents = list(parents) + ["base"] | ||
cls._inherit = ClassAttribute("_inherit", parents) | ||
|
||
@classmethod | ||
def __register__(cls): | ||
"""Register the class into the list of classes defined by the module""" | ||
if "tests" not in cls.__module__.split(":"): | ||
cls._pydantic_classes_by_module[cls._module].append(cls) | ||
|
||
|
||
class Base(BaseModel): | ||
"""This is the base pydantic BaseModel for every BaseModels | ||
It is implicitely inherited by all BaseModels. | ||
All your base are belong to us | ||
""" | ||
|
||
_name = "base" | ||
|
||
|
||
class OdooOrmMode(BaseModel): | ||
"""Generic model that can be used to instantiate pydantis model from | ||
odoo models | ||
Usage: | ||
.. code-block:: python | ||
class UserInfo(models.BaseModel): | ||
_name = "user" | ||
_inherit = "odoo_orm_mode" | ||
name: str | ||
groups: List["group"] = pydantic.Field(alias="groups_id") | ||
class Group(models.BaseModel): | ||
_name="group" | ||
_inherit = "odoo_orm_mode" | ||
name: str | ||
user = self.env.user | ||
UserInfoCls = self.env.pydantic_registry["user"] | ||
user_info = UserInfoCls.from_orm(user) | ||
""" | ||
|
||
_name = "odoo_orm_mode" | ||
|
||
class Config: | ||
orm_mode = True | ||
getter_dict = utils.GenericOdooGetter |
This file contains 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 @@ | ||
* Laurent Mignon <[email protected]> |
This file contains 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 @@ | ||
This addon allows you to define inheritable `Pydantic classes <https://pydantic-docs.helpmanual.io/>`_. |
This file contains 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,3 @@ | ||
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. |
Oops, something went wrong.