Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions pydantic/README.rst
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.

.. 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.
4 changes: 4 additions & 0 deletions pydantic/__init__.py
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
21 changes: 21 additions & 0 deletions pydantic/__manifest__.py
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,
}
83 changes: 83 additions & 0 deletions pydantic/builder.py
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])
8 changes: 8 additions & 0 deletions pydantic/context.py
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")
29 changes: 29 additions & 0 deletions pydantic/ir_http.py
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)
Loading