Skip to content

Commit

Permalink
[IMP] base_rest_pydantic:
Browse files Browse the repository at this point in the history
* Remove pydantic v1 support
* Fix to_response validation instance
* Fix unit tests
  • Loading branch information
marielejeune committed Jul 20, 2023
1 parent c5da488 commit b485a09
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 29 deletions.
2 changes: 1 addition & 1 deletion base_rest_demo/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"pydantic",
],
"external_dependencies": {
"python": ["jsondiff", "extendable-pydantic", "marshmallow", "pydantic"]
"python": ["jsondiff", "extendable-pydantic>=1.0.0", "marshmallow", "pydantic>=2.0.0"]
},
"installable": True,
}
2 changes: 1 addition & 1 deletion base_rest_pydantic/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"installable": True,
"external_dependencies": {
"python": [
"pydantic",
"pydantic>=2.0.0",
]
},
}
36 changes: 15 additions & 21 deletions base_rest_pydantic/restapi.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

import json
from odoo import _
from odoo.exceptions import UserError

from odoo.addons.base_rest import restapi

from pydantic import BaseModel, ValidationError
from pydantic.version import VERSION as PYDANTIC_VERSION

PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")

if PYDANTIC_V2:

def validate_model(cls, data):
return cls.model_validate_json(data)

else:
from pydantic import validate_model


def replace_ref_in_schema(item, original_schema):
Expand Down Expand Up @@ -55,15 +44,20 @@ def from_params(self, service, params):

def to_response(self, service, result):
# do we really need to validate the instance????
json_dict = result.dict()
if PYDANTIC_V2:
orm_mode = result.model_config.get("from_attributes", None)
else:
orm_mode = result.__config__.orm_mode
to_validate = json_dict if orm_mode else result.dict(by_alias=True)
*_, validation_error = validate_model(self._model_cls, to_validate)
if validation_error:
raise SystemError(_("Invalid Response %s") % validation_error)
json_dict = result.model_dump()
orm_mode = result.model_config.get("from_attributes", None)
to_validate = json_dict if orm_mode else result.model_dump(by_alias=True)
# Ensure that to_validate is under json format
try:
json.loads(to_validate)
to_validate_jsonified = to_validate
except TypeError:
to_validate_jsonified = json.dumps(to_validate)

try:
self._model_cls.model_validate_json(to_validate_jsonified)
except ValidationError as validation_error:
raise SystemError(_("Invalid Response")) from validation_error
return json_dict

def to_openapi_query_parameters(self, servic, spec):
Expand Down
19 changes: 14 additions & 5 deletions base_rest_pydantic/tests/test_from_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def setUp(self):
super(TestPydantic, self).setUp()

class Model1(BaseModel):
name: str = None
name: str
description: str = None

self.Model1: BaseModel = Model1
Expand All @@ -33,7 +33,10 @@ def _from_params(self, pydantic_cls: Type[BaseModel], params: dict, **kwargs):
return restapi_pydantic.from_params(mock_service, params)

def test_from_params(self):
params = {"name": "Instance Name", "description": "Instance Description"}
params = {
"name": "Instance Name",
"description": "Instance Description",
}
instance = self._from_params(self.Model1, params)
self.assertEqual(instance.name, params["name"])
self.assertEqual(instance.description, params["description"])
Expand All @@ -45,14 +48,20 @@ def test_from_params_missing_optional_field(self):
self.assertIsNone(instance.description)

def test_from_params_missing_required_field(self):
msg = r"value_error.missing"
msg = r"Field required"
with self.assertRaisesRegex(UserError, msg):
self._from_params(self.Model1, {"description": "Instance Description"})

def test_from_params_pydantic_model_list(self):
params = [
{"name": "Instance Name", "description": "Instance Description"},
{"name": "Instance Name 2", "description": "Instance Description 2"},
{
"name": "Instance Name",
"description": "Instance Description",
},
{
"name": "Instance Name 2",
"description": "Instance Description 2",
},
]
instances = self._from_params(self.Model1, params)
self.assertEqual(len(instances), 2)
Expand Down
11 changes: 10 additions & 1 deletion base_rest_pydantic/tests/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,20 @@ class Model1(BaseModel):
res = self._to_response(instance)
self.assertEqual(res["name"], instance.name)

def test_to_response_validation_failed(self):
class Model1(BaseModel):
name: str = None

instance = Model1(named="Instance 1")
msg = r"Invalid Response"
with self.assertRaisesRegex(SystemError, msg):
self._to_response(instance)

def test_to_response_list(self):
class Model1(BaseModel):
name: str = None

instances = (Model1(name="Instance 1"), Model1(name="Instance 2"))
instances = [Model1(name="Instance 1"), Model1(name="Instance 2")]
res = self._to_response_list(instances)
self.assertEqual(len(res), 2)
self.assertSetEqual({r["name"] for r in res}, {"Instance 1", "Instance 2"})

0 comments on commit b485a09

Please sign in to comment.