Skip to content

Commit

Permalink
[ADD] extendable_fastapi: New class StrictExtendableBaseModel
Browse files Browse the repository at this point in the history
  • Loading branch information
marielejeune committed Jul 31, 2023
1 parent b834273 commit 6d64994
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 8 deletions.
1 change: 1 addition & 0 deletions extendable_fastapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import fastapi_dispatcher
from .schemas import StrictExtendableBaseModel
2 changes: 1 addition & 1 deletion extendable_fastapi/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"demo": [],
"external_dependencies": {
"python": [
"extendable-pydantic>=1.0.0",
"extendable-pydantic>=1.1.0",
],
},
"installable": True,
Expand Down
29 changes: 29 additions & 0 deletions extendable_fastapi/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from extendable_pydantic import ExtendableBaseModel


class StrictExtendableBaseModel(
ExtendableBaseModel,
revalidate_instances="always",
validate_assignment=True,
extra="forbid",
strict=True,
):
"""
An ExtendableBaseModel with strict validation.
By default, Pydantic does not revalidate instances during validation, nor
when the data is changed. Validation only occurs when the model is created.
This is not suitable for a REST API, where the data is changed after the
model is created or the model is created from a partial set of data and
then updated with more data. This class enforce strict validation by
forcing the revalidation of instances when the method `model_validate` is
called and by ensuring that the values assignment is validated.
The following parameters are added:
* revalidate_instances="always": model instances are revalidated during validation
(default is "never")
* validate_assignment=True: revalidate the model when the data is changed (default is False)
* extra="forbid": Forbid any extra attributes (default is "ignore")
* strict=True: raise an error if a value's type does not match the field's type
annotation (default is False; Pydantic attempts to coerce values to the correct type)
"""
1 change: 1 addition & 0 deletions extendable_fastapi/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_strict_extendable_base_model
68 changes: 68 additions & 0 deletions extendable_fastapi/tests/test_strict_extendable_base_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import warnings
from datetime import date

from extendable_pydantic import ExtendableBaseModel

from pydantic import ValidationError

from ..schemas import StrictExtendableBaseModel
from .common import FastAPITransactionCase


class TestStrictExtendableBaseModel(FastAPITransactionCase):
class Model(ExtendableBaseModel):
x: int
d: date | None

class StrictModel(StrictExtendableBaseModel):
x: int
d: date | None

def test_Model_revalidate_instance_never(self):
# Missing required fields but no re-validation
m = self.Model.model_construct()
self.assertEqual(m.model_validate(m).model_dump(), {})

def test_StrictModel_revalidate_instance_always(self):
# Missing required fields and always revalidate
m = self.StrictModel.model_construct()
with self.assertRaises(ValidationError):
m.model_validate(m)

def test_Model_validate_assignment_false(self):
# Wrong assignment but no re-validation at assignment
m = self.Model(x=1, d=None)
m.x = "TEST"
with warnings.catch_warnings():
# Disable 'Expected `int` but got `str`' warning
warnings.simplefilter("ignore")
self.assertEqual(m.model_dump(), {"x": "TEST", "d": None})

def test_StrictModel_validate_assignment_true(self):
# Wrong assignment and validation at assignment
m = self.StrictModel.model_construct()
m.x = 1 # Validate only this field -> OK even if m.d is not set
with self.assertRaises(ValidationError):
m.x = "TEST"

def test_Model_extra_ignored(self):
# Ignore extra fields
m = self.Model(x=1, z=3, d=None)
self.assertEqual(m.model_dump(), {"x": 1, "d": None})

def test_StrictModel_extra_forbidden(self):
# Forbid extra fields
with self.assertRaises(ValidationError):
self.StrictModel(x=1, z=3, d=None)

def test_Model_strict_false(self):
# Coerce str->date is allowed
m = self.Model(x=1, d=None)
m.d = "2023-01-01"
self.assertTrue(m.model_validate(m))

def test_StrictModel_strict_true(self):
# Coerce str->date is forbidden
m = self.StrictModel(x=1, d=None)
with self.assertRaises(ValidationError):
m.d = "2023-01-01"
19 changes: 12 additions & 7 deletions fastapi/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Odoo FastAPI
:target: https://runbot.odoo-community.org/runbot/271/16.0
:alt: Try me on Runbot

|badge1| |badge2| |badge3| |badge4| |badge5|
|badge1| |badge2| |badge3| |badge4| |badge5|

This addon provides the basis to smoothly integrate the `FastAPI`_
framework into Odoo.
Expand Down Expand Up @@ -862,14 +862,14 @@ complete transparent. This addon is called
`odoo-addon-extendable-fastapi <https://pypi.org/project/odoo-addon-extendable-fastapi/>`_.

When you want to allow other addons to extend a pydantic model, you must
first define the model as an extendable model by using a dedicated metaclass
first define the model as an extendable model by using the dedicated
class from `extendable_pydantic`.

.. code-block:: python
from pydantic import BaseModel
from extendable_pydantic import ExtendableModelMeta
from extendable_pydantic import ExtendableBaseModel
class Partner(BaseModel, metaclass=ExtendableModelMeta):
class Partner(ExtendableBaseModel):
name = 0.1
model_config = ConfigDict(from_attributes=True)
Expand All @@ -890,6 +890,11 @@ pydantic.
"""Return the location"""
return Partner.model_validate(partner)
Some of the Pydantic config parameters are not set by default but are very useful when
using `odoo-addon-fastapi`, hence the `ExtendableBaseModel` model was extended
to `StrictExtendableBaseModel` in **'odoo-addon-extendable-fastapi'**
to force revalidation of the instances at each change, and forbidding the
extra values.

If you need to add a new field into the model **'Partner'**, you can extend it
in your new addon by defining a new model that inherits from the model **'Partner'**.
Expand All @@ -899,7 +904,7 @@ in your new addon by defining a new model that inherits from the model **'Partne
from typing import Optional
from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner
class PartnerExtended(Partner, extends=Partner):
class PartnerExtended(Partner, extends=True):
email: Optional[str]
If your new addon is installed in a database, a call to the route handler
Expand Down Expand Up @@ -1503,7 +1508,7 @@ promote its widespread use.

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-lmignon|
|maintainer-lmignon|

This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/fastapi>`_ project on GitHub.

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ cerberus
contextvars
extendable-pydantic
extendable-pydantic>=1.0.0
extendable-pydantic>=1.1.0
extendable>=0.0.4
fastapi
graphene
Expand Down

0 comments on commit 6d64994

Please sign in to comment.