Skip to content
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

[16.0][IMP] extendable_fastapi: Add generics schemas #380

Merged
merged 2 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion extendable_fastapi/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"external_dependencies": {
"python": [
"pydantic>=2.0.0",
"extendable-pydantic>=1.1.0",
"extendable-pydantic>=1.2.0",
],
},
"installable": True,
Expand Down
Empty file.
7 changes: 7 additions & 0 deletions extendable_fastapi/readme/newsfragments/380.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
* New base schemas: *PagedCollection*. This schema is used to define the
the structure of a paged collection of resources. This schema is similar
to the ones defined in the Odoo's **fastapi** addon but works as/with
extendable models.

* The *StrictExtendableBaseModel* has been moved to the *extendable_pydantic*
python lib. You should consider to import it from there.
37 changes: 15 additions & 22 deletions extendable_fastapi/schemas.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
from extendable_pydantic import ExtendableBaseModel
from typing import Annotated, Generic, TypeVar

from extendable_pydantic import StrictExtendableBaseModel

class StrictExtendableBaseModel(
Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbidoul @sebastienbeau @qgroulard I'm not sure it's the right place to define these base schemas. Even if they are mainly used with fastapi, we also use the StrictExtendableBaseModel when we define models to be stored into a search engine. This leads to a useless dependency on fastapi. Do we've to create a specific addon for the StrictExtendableBaseModel? I could also move it to the extendable_fastapi python lib.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could also move it to the extendable_fastapi python lib.

But this PR is for extendable_fastapi ?

Question: why would one want to extend PagedCollection or Paging? Or is this necessary for compatibility when T is extendable?

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, at least for PagedCollection, it's necessary for compatibility when T is extendable.

Regarding StrictExtendableBaseModel, it's not defined at the right place or we should not use-it when defining schemas for the search engine index.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes I see. So you mean defining StrictExtendableBaseModel in extendable_pydantic, right?

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, makes sense to me. Let's do that.

ExtendableBaseModel,
revalidate_instances="always",
validate_assignment=True,
extra="forbid",
):
"""
An ExtendableBaseModel with strict validation.
from pydantic import Field

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 enforces strict validation by
forcing the revalidation of instances when the method `model_validate` is
called and by ensuring that the values assignment is validated.
T = TypeVar("T")

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")
"""

class PagedCollection(StrictExtendableBaseModel, Generic[T]):
"""A paged collection of items"""

# This is a generic model. The type of the items is defined by the generic type T.
# It provides you a common way to return a paged collection of items of
# extendable models. It's based on the StrictExtendableBaseModel to ensure
# a strict validation when used within the odoo fastapi framework.

count: Annotated[int, Field(..., description="The count of items into the system")]
items: list[T]
9 changes: 5 additions & 4 deletions fastapi/readme/USAGE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1116,8 +1116,9 @@ the app is robust and easy to maintain. Here are some of them:

* As a corollary of the previous point, a search handler must always use the
pagination mechanism with a reasonable default page size. The result list
must be enclosed in a json document that contains the total number of records
and the list of records.
must be enclosed in a json document that contains the count of records into
the system matching your search criteria and the list of records for the given
page and size.

* Use plural for the name of a service. For example, if you provide a service
that allows you to manage the sale orders, you must use the name 'sale_orders'
Expand Down Expand Up @@ -1149,7 +1150,7 @@ you be consistent when writing a route handler for a search route.
1. A dependency method to use to specify the pagination parameters in the same
way for all the search route handlers: **'odoo.addons.fastapi.paging'**.
2. A PagedCollection pydantic model to use to return the result of a search route
handler enclosed in a json document that contains the total number of records.
handler enclosed in a json document that contains the count of records.

.. code-block:: python

Expand Down Expand Up @@ -1179,7 +1180,7 @@ you be consistent when writing a route handler for a search route.
count = env["sale.order"].search_count([])
orders = env["sale.order"].search([], limit=paging.limit, offset=paging.offset)
return PagedCollection[SaleOrder](
total=count,
count=count,
items=[SaleOrder.model_validate(order) for order in orders],
)

Expand Down
Empty file.
7 changes: 7 additions & 0 deletions fastapi/readme/newsfragments/380.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
The field *total* in the *PagedCollection* schema is replaced by the field *count*.
The field *total* is now deprecated and will be removed in the next major version.
This change is backward compatible. The json document returned will now
contain both fields *total* and *count* with the same value. In your python
code the field *total*, if used, will fill the field *count* with the same
value. You are encouraged to use the field *count* instead of *total* and adapt
your code accordingly.
31 changes: 26 additions & 5 deletions fastapi/schemas.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
# Copyright 2022 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import warnings
from enum import Enum
from typing import Generic, List, Optional, TypeVar
from typing import Annotated, Generic, List, Optional, TypeVar

from pydantic import BaseModel, ConfigDict, Field
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field

T = TypeVar("T")


class PagedCollection(BaseModel, Generic[T]):

total: int
count: Annotated[
int,
Field(
...,
desciption="Count of items into the system.\n "
"Replaces the total field which is deprecated",
validation_alias=AliasChoices("count", "total"),
),
]
items: List[T]

@computed_field()
@property
def total(self) -> int:
return self.count

Check warning on line 27 in fastapi/schemas.py

View check run for this annotation

Codecov / codecov/patch

fastapi/schemas.py#L27

Added line #L27 was not covered by tests

@total.setter
def total(self, value: int):
warnings.warn(

Check warning on line 31 in fastapi/schemas.py

View check run for this annotation

Codecov / codecov/patch

fastapi/schemas.py#L31

Added line #L31 was not covered by tests
"The total field is deprecated, please use count instead",
DeprecationWarning,
stacklevel=2,
)
self.count = value

Check warning on line 36 in fastapi/schemas.py

View check run for this annotation

Codecov / codecov/patch

fastapi/schemas.py#L36

Added line #L36 was not covered by tests


class Paging(BaseModel):
limit: Optional[int] = None
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ apispec>=4.0.0
cerberus
contextvars
extendable-pydantic
extendable-pydantic>=1.1.0
extendable-pydantic>=1.2.0
extendable>=0.0.4
fastapi
graphene
Expand Down