diff --git a/extendable_fastapi/__manifest__.py b/extendable_fastapi/__manifest__.py index 3958bccc..f4b20592 100644 --- a/extendable_fastapi/__manifest__.py +++ b/extendable_fastapi/__manifest__.py @@ -16,7 +16,7 @@ "external_dependencies": { "python": [ "pydantic>=2.0.0", - "extendable-pydantic>=1.1.0", + "extendable-pydantic>=1.2.0", ], }, "installable": True, diff --git a/extendable_fastapi/readme/newsfragments/.gitignore b/extendable_fastapi/readme/newsfragments/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/extendable_fastapi/readme/newsfragments/380.feature b/extendable_fastapi/readme/newsfragments/380.feature new file mode 100644 index 00000000..2397aa7d --- /dev/null +++ b/extendable_fastapi/readme/newsfragments/380.feature @@ -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. diff --git a/extendable_fastapi/schemas.py b/extendable_fastapi/schemas.py index dfa7a166..c6a71e7d 100644 --- a/extendable_fastapi/schemas.py +++ b/extendable_fastapi/schemas.py @@ -1,26 +1,19 @@ -from extendable_pydantic import ExtendableBaseModel +from typing import Annotated, Generic, TypeVar +from extendable_pydantic import StrictExtendableBaseModel -class StrictExtendableBaseModel( - 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] diff --git a/fastapi/readme/USAGE.rst b/fastapi/readme/USAGE.rst index fd7a2317..ef6f8240 100644 --- a/fastapi/readme/USAGE.rst +++ b/fastapi/readme/USAGE.rst @@ -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' @@ -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 @@ -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], ) diff --git a/fastapi/readme/newsfragments/.gitignore b/fastapi/readme/newsfragments/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/fastapi/readme/newsfragments/380.feature b/fastapi/readme/newsfragments/380.feature new file mode 100644 index 00000000..384c1959 --- /dev/null +++ b/fastapi/readme/newsfragments/380.feature @@ -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. diff --git a/fastapi/schemas.py b/fastapi/schemas.py index c9c38c1b..4a331847 100644 --- a/fastapi/schemas.py +++ b/fastapi/schemas.py @@ -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 + + @total.setter + def total(self, value: int): + warnings.warn( + "The total field is deprecated, please use count instead", + DeprecationWarning, + stacklevel=2, + ) + self.count = value + class Paging(BaseModel): limit: Optional[int] = None diff --git a/requirements.txt b/requirements.txt index 1aaed652..e4ff49d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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