odoo-addon-fastapi


Nameodoo-addon-fastapi JSON
Version 16.0.1.2.9 PyPI version JSON
download
home_pagehttps://github.com/OCA/rest-framework
SummaryOdoo FastAPI endpoint
upload_time2024-04-19 13:40:19
maintainerNone
docs_urlNone
authorACSONE SA/NV,Odoo Community Association (OCA)
requires_python>=3.10
licenseLGPL-3
keywords
VCS
bugtrack_url
requirements a2wsgi apispec apispec cerberus contextvars extendable-pydantic extendable-pydantic extendable fastapi graphene graphql_server jsondiff marshmallow marshmallow-objects parse-accept-language pydantic pydantic pyquerystring python-multipart typing-extensions ujson
Travis-CI No Travis.
coveralls test coverage No coveralls.
            ============
Odoo FastAPI
============

.. 
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   !! This file is generated by oca-gen-addon-readme !!
   !! changes will be overwritten.                   !!
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   !! source digest: sha256:f3cc5405e18a54d995ec4f61dcaab89c32e72e625a72d0cb3bf1ba6c0800d6be
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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/16.0/fastapi
    :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-16-0/rest-framework-16-0-fastapi
    :alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
    :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=16.0
    :alt: Try me on Runboat

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

This addon provides the basis to smoothly integrate the `FastAPI`_
framework into Odoo.

This integration allows you to use all the goodies from `FastAPI`_ to build custom
APIs for your Odoo server based on standard Python type hints.

**What is building an API?**

An API is a set of functions that can be called from the outside world. The
goal of an API is to provide a way to interact with your application from the
outside world without having to know how it works internally. A common mistake
when you are building an API is to expose all the internal functions of your
application and therefore create a tight coupling between the outside world and
your internal datamodel and business logic. This is not a good idea because it
makes it very hard to change your internal datamodel and business logic without
breaking the outside world.

When you are building an API, you define a contract between the outside world
and your application. This contract is defined by the functions that you expose
and the parameters that you accept. This contract is the API. When you change
your internal datamodel and business logic, you can still keep the same API
contract and therefore you don't break the outside world. Even if you change
your implementation, as long as you keep the same API contract, the outside
world will still work. This is the beauty of an API and this is why it is so
important to design a good API.

A good API is designed to be stable and to be easy to use. It's designed to
provide high-level functions related to a specific use case. It's designed to
be easy to use by hiding the complexity of the internal datamodel and business
logic. A common mistake when you are building an API is to expose all the internal
functions of your application and let the oustide world deal with the complexity
of your internal datamodel and business logic. Don't forget that on a transactional
point of view, each call to an API function is a transaction. This means that
if a specific use case requires multiple calls to your API, you should provide
a single function that does all the work in a single transaction. This why APIs
methods are called high-level and atomic functions.

.. _FastAPI:  https://fastapi.tiangolo.com/

**Table of contents**

.. contents::
   :local:

Usage
=====

What's building an API with fastapi?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

FastAPI is a modern, fast (high-performance), web framework for building APIs
with Python 3.7+ based on standard Python type hints. This addons let's you
keep advantage of the fastapi framework and use it with Odoo.

Before you start, we must define some terms:

* **App**: A FastAPI app is a collection of routes, dependencies, and other
  components that can be used to build a web application.
* **Router**: A router is a collection of routes that can be mounted in an
  app.
* **Route**: A route is a mapping between an HTTP method and a path, and
  defines what should happen when the user requests that path.
* **Dependency**: A dependency is a callable that can be used to get some
  information from the user request, or to perform some actions before the
  request handler is called.
* **Request**: A request is an object that contains all the information
  sent by the user's browser as part of an HTTP request.
* **Response**: A response is an object that contains all the information
  that the user's browser needs to build the result page.
* **Handler**: A handler is a function that takes a request and returns a
  response.
* **Middleware**: A middleware is a function that takes a request and a
  handler, and returns a response.

The FastAPI framework is based on the following principles:

* **Fast**: Very high performance, on par with NodeJS and Go (thanks to Starlette
  and Pydantic). [One of the fastest Python frameworks available]
* **Fast to code**: Increase the speed to develop features by about 200% to 300%.
* **Fewer bugs**: Reduce about 40% of human (developer) induced errors.
* **Intuitive**: Great editor support. Completion everywhere. Less time
  debugging.
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
* **Short**: Minimize code duplication. Multiple features from each parameter
  declaration. Fewer bugs.
* **Robust**: Get production-ready code. With automatic interactive documentation.
* **Standards-based**: Based on (and fully compatible with) the open standards
  for APIs: OpenAPI (previously known as Swagger) and JSON Schema.
* **Open Source**: FastAPI is fully open-source, under the MIT license.

The first step is to install the fastapi addon. You can do it with the
following command:

    $ pip install odoo-addon-fastapi

Once the addon is installed, you can start building your API. The first thing
you need to do is to create a new addon that depends on 'fastapi'. For example,
let's create an addon called *my_demo_api*.

Then, you need to declare your app by defining a model that inherits from
'fastapi.endpoint' and add your app name into the app field. For example:

.. code-block:: python

    from odoo import fields, models

    class FastapiEndpoint(models.Model):

        _inherit = "fastapi.endpoint"

        app: str = fields.Selection(
            selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"}
        )

The **'fastapi.endpoint'** model is the base model for all the endpoints. An endpoint
instance is the mount point for a fastapi app into Odoo. When you create a new
endpoint, you can define the app that you want to mount in the **'app'** field
and the path where you want to mount it in the **'path'** field.

figure:: static/description/endpoint_create.png

    FastAPI Endpoint

Thanks to the **'fastapi.endpoint'** model, you can create as many endpoints as
you want and mount as many apps as you want in each endpoint. The endpoint is
also the place where you can define configuration parameters for your app. A
typical example is the authentication method that you want to use for your app
when accessed at the endpoint path.

Now, you can create your first router. For that, you need to define a global
variable into your fastapi_endpoint module called for example 'demo_api_router'

.. code-block:: python

    from fastapi import APIRouter
    from odoo import fields, models

    class FastapiEndpoint(models.Model):

        _inherit = "fastapi.endpoint"

        app: str = fields.Selection(
            selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"}
        )

    # create a router
    demo_api_router = APIRouter()


To make your router available to your app, you need to add it to the list of routers
returned by the **_get_fastapi_routers** method of your fastapi_endpoint model.

.. code-block:: python

    from fastapi import APIRouter
    from odoo import api, fields, models

    class FastapiEndpoint(models.Model):

        _inherit = "fastapi.endpoint"

        app: str = fields.Selection(
            selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"}
        )

        def _get_fastapi_routers(self):
            if self.app == "demo":
                return [demo_api_router]
            return super()._get_fastapi_routers()

    # create a router
    demo_api_router = APIRouter()

Now, you can start adding routes to your router. For example, let's add a route
that returns a list of partners.

.. code-block:: python

    from typing import Annotated

    from fastapi import APIRouter
    from pydantic import BaseModel

    from odoo import api, fields, models
    from odoo.api import Environment

    from odoo.addons.fastapi.dependencies import odoo_env

    class FastapiEndpoint(models.Model):

        _inherit = "fastapi.endpoint"

        app: str = fields.Selection(
            selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"}
        )

        def _get_fastapi_routers(self):
            if self.app == "demo":
                return [demo_api_router]
            return super()._get_fastapi_routers()

    # create a router
    demo_api_router = APIRouter()

    class PartnerInfo(BaseModel):
        name: str
        email: str

    @demo_api_router.get("/partners", response_model=list[PartnerInfo])
    def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]:
        return [
            PartnerInfo(name=partner.name, email=partner.email)
            for partner in env["res.partner"].search([])
        ]

Now, you can start your Odoo server, install your addon and create a new endpoint
instance for your app. Once it's done click on the docs url to access the
interactive documentation of your app.

Before trying to test your app, you need to define on the endpoint instance the
user that will be used to run the app. You can do it by setting the **'user_id'**
field. This information is the most important one because it's the basis for
the security of your app. The user that you define in the endpoint instance
will be used to run the app and to access the database. This means that the
user will be able to access all the data that he has access to in Odoo. To ensure
the security of your app, you should create a new user that will be used only
to run your app and that will have no access to the database.

.. code-block:: xml

  <record
        id="my_demo_app_user"
        model="res.users"
        context="{'no_reset_password': True, 'no_reset_password': True}"
    >
    <field name="name">My Demo Endpoint User</field>
    <field name="login">my_demo_app_user</field>
    <field name="groups_id" eval="[(6, 0, [])]" />
  </record>

At the same time you should create a new group that will be used to define the
access rights of the user that will run your app. This group should imply
the predefined group **'FastAPI Endpoint Runner'**. This group defines the
minimum access rights that the user needs to:

* access the endpoint instance it belongs to
* access to its own user record
* access to the partner record that is linked to its user record

.. code-block:: xml

  <record id="my_demo_app_group" model="res.groups">
    <field name="name">My Demo Endpoint Group</field>
    <field name="users" eval="[(4, ref('my_demo_app_user'))]" />
    <field name="implied_ids" eval="[(4, ref('fastapi.group_fastapi_endpoint_runner'))]" />
  </record>


Now, you can test your app. You can do it by clicking on the 'Try it out' button
of the route that you have defined. The result of the request will be displayed
in the 'Response' section and contains the list of partners.

.. note::
  The **'FastAPI Endpoint Runner'** group ensures that the user cannot access any
  information others than the 3 ones mentioned above. This means that for every
  information that you want to access from your app, you need to create the
  proper ACLs and record rules. (see `Managing security into the route handlers`_)
  It's a good practice to use a dedicated user into a specific group from the
  beginning of your project and in your tests. This will force you to define
  the proper security rules for your endoints.

Dealing with the odoo environment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The **'odoo.addons.fastapi.dependencies'** module provides a set of functions that you can use
to inject reusable dependencies into your routes. For example, the **'odoo_env'**
function returns the current odoo environment. You can use it to access the
odoo models and the database from your route handlers.

.. code-block:: python

    from typing import Annotated

    from odoo.api import Environment
    from odoo.addons.fastapi.dependencies import odoo_env

    @demo_api_router.get("/partners", response_model=list[PartnerInfo])
    def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]:
        return [
            PartnerInfo(name=partner.name, email=partner.email)
            for partner in env["res.partner"].search([])
        ]

As you can see, you can use the **'Depends'** function to inject the dependency
into your route handler. The **'Depends'** function is provided by the
**'fastapi'** framework. You can use it to inject any dependency into your route
handler. As your handler is a python function, the only way to get access to
the odoo environment is to inject it as a dependency. The fastapi addon provides
a set of function that can be used as dependencies:

* **'odoo_env'**: Returns the current odoo environment.
* **'fastapi_endpoint'**: Returns the current fastapi endpoint model instance.
* **'authenticated_partner'**: Returns the authenticated partner.
* **'authenticated_partner_env'**: Returns the current odoo environment with the
  authenticated_partner_id into the context.

By default, the **'odoo_env'** and **'fastapi_endpoint'** dependencies are
available without extra work.

.. note::
  Even if 'odoo_env' and 'authenticated_partner_env' returns the current odoo
  environment, they are not the same. The 'odoo_env' dependency returns the
  environment without any modification while the 'authenticated_partner_env'
  adds the authenticated partner id into the context of the environment. As it will
  be explained in the section `Managing security into the route handlers`_ dedicated
  to the security, the presence of the authenticated partner id into the context
  is the key information that will allow you to enforce the security of your endpoint
  methods. As consequence, you should always use the 'authenticated_partner_env'
  dependency instead of the 'odoo_env' dependency for all the methods that are
  not public.

The dependency injection mechanism
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The **'odoo_env'** dependency relies on a simple implementation that retrieves
the current odoo environment from ContextVar variable initialized at the start
of the request processing by the specific request dispatcher processing the
fastapi requests.

The **'fastapi_endpoint'** dependency relies on the 'dependency_overrides' mechanism
provided by the **'fastapi'** module. (see the fastapi documentation for more
details about the dependency_overrides mechanism). If you take a look at the
current implementation of the **'fastapi_endpoint'** dependency, you will see
that the method depends of two parameters: **'endpoint_id'** and **'env'**. Each
of these parameters are dependencies themselves.

.. code-block:: python

    def fastapi_endpoint_id() -> int:
        """This method is overriden by default to make the fastapi.endpoint record
        available for your endpoint method. To get the fastapi.endpoint record
        in your method, you just need to add a dependency on the fastapi_endpoint method
        defined below
        """


    def fastapi_endpoint(
        _id: Annotated[int, Depends(fastapi_endpoint_id)],
        env: Annotated[Environment, Depends(odoo_env)],
    ) -> "FastapiEndpoint":
        """Return the fastapi.endpoint record"""
        return env["fastapi.endpoint"].browse(_id)


As you can see, one of these dependencies is the **'fastapi_endpoint_id'**
dependency and has no concrete implementation. This method is used as a contract
that must be implemented/provided at the time the fastapi app is created.
Here comes the power of the dependency_overrides mechanism.

If you take a look at the **'_get_app'** method of the **'FastapiEndpoint'** model,
you will see that the **'fastapi_endpoint_id'** dependency is overriden by
registering a specific method that returns the id of the current fastapi endpoint
model instance for the original method.

.. code-block:: python

    def _get_app(self) -> FastAPI:
        app = FastAPI(**self._prepare_fastapi_endpoint_params())
        for router in self._get_fastapi_routers():
            app.include_router(prefix=self.root_path, router=router)
        app.dependency_overrides[dependencies.fastapi_endpoint_id] = partial(
            lambda a: a, self.id
        )

This kind of mechanism is very powerful and allows you to inject any dependency
into your route handlers and moreover, define an abstract dependency that can be
used by any other addon and for which the implementation could depend on the
endpoint configuration.

The authentication mechanism
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To make our app not tightly coupled with a specific authentication mechanism,
we will use the **'authenticated_partner'** dependency. As for the
**'fastapi_endpoint'** this dependency depends on an abstract dependency.

When you define a route handler, you can inject the **'authenticated_partner'**
dependency as a parameter of your route handler.

.. code-block:: python

    from odoo.addons.base.models.res_partner import Partner


    @demo_api_router.get("/partners", response_model=list[PartnerInfo])
    def get_partners(
        env: Annotated[Environment, Depends(odoo_env)], partner: Annotated[Partner, Depends(authenticated_partner)]
    ) -> list[PartnerInfo]:
        return [
            PartnerInfo(name=partner.name, email=partner.email)
            for partner in env["res.partner"].search([])
        ]


At this stage, your handler is not tied to a specific authentication mechanism
but only expects to get a partner as a dependency. Depending on your needs, you
can implement different authentication mechanism available for your app.
The fastapi addon provides a default authentication mechanism using the
'BasicAuth' method. This authentication mechanism is implemented in the
**'odoo.addons.fastapi.dependencies'** module and relies on functionalities provided
by the **'fastapi.security'** module.

.. code-block:: python

      def authenticated_partner(
          env: Annotated[Environment, Depends(odoo_env)],
          security: Annotated[HTTPBasicCredentials, Depends(HTTPBasic())],
      ) -> "res.partner":
          """Return the authenticated partner"""
          partner = env["res.partner"].search(
              [("email", "=", security.username)], limit=1
          )
          if not partner:
              raise HTTPException(
                  status_code=status.HTTP_401_UNAUTHORIZED,
                  detail="Invalid authentication credentials",
                  headers={"WWW-Authenticate": "Basic"},
              )
          if not partner.check_password(security.password):
              raise HTTPException(
                  status_code=status.HTTP_401_UNAUTHORIZED,
                  detail="Invalid authentication credentials",
                  headers={"WWW-Authenticate": "Basic"},
              )
          return partner

As you can see, the **'authenticated_partner'** dependency relies on the
**'HTTPBasic'** dependency provided by the **'fastapi.security'** module.
In this dummy implementation, we just check that the provided credentials
can be used to authenticate a user in odoo. If the authentication is successful,
we return the partner record linked to the authenticated user.

In some cases you could want to implement a more complex authentication mechanism
that could rely on a token or a session. In this case, you can override the
**'authenticated_partner'** dependency by registering a specific method that
returns the authenticated partner. Moreover, you can make it configurable on
the fastapi endpoint model instance.

To do it, you just need to implement a specific method for each of your
authentication mechanism and allows the user to select one of these methods
when he creates a new fastapi endpoint. Let's say that we want to allow the
authentication by using an api key or via basic auth. Since basic auth is already
implemented, we will only implement the api key authentication mechanism.

.. code-block:: python

  from fastapi.security import APIKeyHeader

  def api_key_based_authenticated_partner_impl(
      api_key: Annotated[str, Depends(
          APIKeyHeader(
              name="api-key",
              description="In this demo, you can use a user's login as api key.",
          )
      )],
      env: Annotated[Environment, Depends(odoo_env)],
  ) -> Partner:
      """A dummy implementation that look for a user with the same login
      as the provided api key
      """
      partner = env["res.users"].search([("login", "=", api_key)], limit=1).partner_id
      if not partner:
          raise HTTPException(
              status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect API Key"
          )
      return partner

As for the 'BasicAuth' authentication mechanism, we also rely on one of the native
security dependency provided by the **'fastapi.security'** module.

Now that we have an implementation for our two authentication mechanisms, we
can allows the user to select one of these authentication mechanisms by adding
a selection field on the fastapi endpoint model.

.. code-block:: python

  from odoo import fields, models

  class FastapiEndpoint(models.Model):

      _inherit = "fastapi.endpoint"

      app: str = fields.Selection(
        selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"}
      )
      demo_auth_method = fields.Selection(
          selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")],
          string="Authenciation method",
      )

.. note::
  A good practice is to prefix specific configuration fields of your app with
  the name of your app. This will avoid conflicts with other app when the
  'fastapi.endpoint' model is extended for other 'app'.

Now that we have a selection field that allows the user to select the
authentication method, we can use the dependency override mechanism to
provide the right implementation of the **'authenticated_partner'** dependency
when the app is instantiated.

.. code-block:: python

  from odoo.addons.fastapi.dependencies import authenticated_partner
  class FastapiEndpoint(models.Model):

      _inherit = "fastapi.endpoint"

      app: str = fields.Selection(
        selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"}
      )
      demo_auth_method = fields.Selection(
          selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")],
          string="Authenciation method",
      )

    def _get_app(self) -> FastAPI:
        app = super()._get_app()
        if self.app == "demo":
            # Here we add the overrides to the authenticated_partner_impl method
            # according to the authentication method configured on the demo app
            if self.demo_auth_method == "http_basic":
                authenticated_partner_impl_override = (
                    authenticated_partner_from_basic_auth_user
                )
            else:
                authenticated_partner_impl_override = (
                    api_key_based_authenticated_partner_impl
                )
            app.dependency_overrides[
                authenticated_partner_impl
            ] = authenticated_partner_impl_override
        return app


To see how the dependency override mechanism works, you can take a look at the
demo app provided by the fastapi addon. If you choose the app 'demo' in the
fastapi endpoint form view, you will see that the authentication method
is configurable. You can also see that depending on the authentication method
configured on your fastapi endpoint, the documentation will change.

.. note::
  At time of writing, the dependency override mechanism is not supported by
  the fastapi documentation generator. A fix has been proposed and is waiting
  to be merged. You can follow the progress of the fix on `github
  <https://github.com/tiangolo/fastapi/pull/5452>`_

Managing configuration parameters for your app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

As we have seen in the previous section, you can add configuration fields
on the fastapi endpoint model to allow the user to configure your app (as for
any odoo model you extend). When you need to access these configuration fields
in your route handlers, you can use the **'odoo.addons.fastapi.dependencies.fastapi_endpoint'**
dependency method to retrieve the 'fastapi.endpoint' record associated to the
current request.

.. code-block:: python

  from pydantic import BaseModel, Field
  from odoo.addons.fastapi.dependencies import fastapi_endpoint

  class EndpointAppInfo(BaseModel):
    id: str
    name: str
    app: str
    auth_method: str = Field(alias="demo_auth_method")
    root_path: str
    model_config = ConfigDict(from_attributes=True)


    @demo_api_router.get(
        "/endpoint_app_info",
        response_model=EndpointAppInfo,
        dependencies=[Depends(authenticated_partner)],
    )
    async def endpoint_app_info(
        endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
    ) -> EndpointAppInfo:
        """Returns the current endpoint configuration"""
        # This method show you how to get access to current endpoint configuration
        # It also show you how you can specify a dependency to force the security
        # even if the method doesn't require the authenticated partner as parameter
        return EndpointAppInfo.model_validate(endpoint)

Some of the configuration fields of the fastapi endpoint could impact the way
the app is instantiated. For example, in the previous section, we have seen
that the authentication method configured on the 'fastapi.endpoint' record is
used in order to provide the right implementation of the **'authenticated_partner'**
when the app is instantiated. To ensure that the app is re-instantiated when
an element of the configuration used in the instantiation of the app is
modified, you must override the **'_fastapi_app_fields'** method to add the
name of the fields that impact the instantiation of the app into the returned
list.

.. code-block:: python

  class FastapiEndpoint(models.Model):

      _inherit = "fastapi.endpoint"

      app: str = fields.Selection(
        selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"}
      )
      demo_auth_method = fields.Selection(
          selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")],
          string="Authenciation method",
      )

      @api.model
      def _fastapi_app_fields(self) -> List[str]:
          fields = super()._fastapi_app_fields()
          fields.append("demo_auth_method")
          return fields

Dealing with languages
~~~~~~~~~~~~~~~~~~~~~~

The fastapi addon parses the Accept-Language header of the request to determine
the language to use. This parsing is done by respecting the `RFC 7231 specification
<https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5>`_. That means that
the language is determined by the first language found in the header that is
supported by odoo (with care of the priority order). If no language is found in
the header, the odoo default language is used. This language is then used to
initialize the Odoo's environment context used by the route handlers. All this
makes the management of languages very easy. You don't have to worry about. This
feature is also documented by default into the generated openapi documentation
of your app to instruct the api consumers how to request a specific language.


How to extend an existing app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When you develop a fastapi app, in a native python app it's not possible
to extend an existing one. This limitation doesn't apply to the fastapi addon
because the fastapi endpoint model is designed to be extended. However, the
way to extend an existing app is not the same as the way to extend an odoo model.

First of all, it's important to keep in mind that when you define a route, you
are actually defining a contract between the client and the server. This
contract is defined by the route path, the method (GET, POST, PUT, DELETE,
etc.), the parameters and the response. If you want to extend an existing app,
you must ensure that the contract is not broken. Any change to the contract
will respect the `Liskov substitution principle
<https://en.wikipedia.org/wiki/Liskov_substitution_principle>`_. This means
that the client should not be impacted by the change.

What does it mean in practice? It means that you can't change the route path
or the method of an existing route. You can't change the name of a parameter
or the type of a response. You can't add a new parameter or a new response.
You can't remove a parameter or a response. If you want to change the contract,
you must create a new route.

What can you change?

* You can change the implementation of the route handler.
* You can override the dependencies of the route handler.
* You can add a new route handler.
* You can extend the model used as parameter or as response of the route handler.

Let's see how to do that.

Changing the implementation of the route handler
================================================

Let's say that you want to change the implementation of the route handler
**'/demo/echo'**. Since a route handler is just a python method, it could seems
a tedious task since we are not into a model method and therefore we can't
take advantage of the Odoo inheritance mechanism.

However, the fastapi addon provides a way to do that. Thanks to the **'odoo_env'**
dependency method, you can access the current odoo environment. With this
environment, you can access the registry and therefore the model you want to
delegate the implementation to. If you want to change the implementation of
the route handler **'/demo/echo'**, the only thing you have to do is to
inherit from the model where the implementation is defined and override the
method **'echo'**.

.. code-block:: python

  from pydantic import BaseModel
  from fastapi import Depends, APIRouter
  from odoo import models
  from odoo.addons.fastapi.dependencies import odoo_env

  class FastapiEndpoint(models.Model):

      _inherit = "fastapi.endpoint"

      def _get_fastapi_routers(self) -> List[APIRouter]:
          routers = super()._get_fastapi_routers()
          routers.append(demo_api_router)
          return routers

  demo_api_router = APIRouter()

  @demo_api_router.get(
      "/echo",
      response_model=EchoResponse,
      dependencies=[Depends(odoo_env)],
  )
  async def echo(
      message: str,
      odoo_env: Annotated[Environment, Depends(odoo_env)],
  ) -> EchoResponse:
      """Echo the message"""
      return EchoResponse(message=odoo_env["demo.fastapi.endpoint"].echo(message))

  class EchoResponse(BaseModel):
      message: str

  class DemoEndpoint(models.AbstractModel):

      _name = "demo.fastapi.endpoint"
      _description = "Demo Endpoint"

      def echo(self, message: str) -> str:
          return message

  class DemoEndpointInherit(models.AbstractModel):

      _inherit = "demo.fastapi.endpoint"

      def echo(self, message: str) -> str:
          return f"Hello {message}"


.. note::

  It's a good programming practice to implement the business logic outside
  the route handler. This way, you can easily test your business logic without
  having to test the route handler. In the example above, the business logic
  is implemented in the method **'echo'** of the model **'demo.fastapi.endpoint'**.
  The route handler just delegate the implementation to this method.


Overriding the dependencies of the route handler
================================================

As you've previously seen, the dependency injection mechanism of fastapi is
very powerful. By designing your route handler to rely on dependencies with
a specific functional scope, you can easily change the implementation of the
dependency without having to change the route handler. With such a design, you
can even define abstract dependencies that must be implemented by the concrete
application. This is the case of the **'authenticated_partner'** dependency in our
previous example. (you can find the implementation of this dependency in the
file **'odoo/addons/fastapi/dependencies.py'** and it's usage in the file
**'odoo/addons/fastapi/models/fastapi_endpoint_demo.py'**)

Adding a new route handler
==========================

Let's say that you want to add a new route handler **'/demo/echo2'**.
You could be tempted to add this new route handler in your new addons by
importing the router of the existing app and adding the new route handler to
it.

.. code-block:: python

  from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router

  @demo_api_router.get(
      "/echo2",
      response_model=EchoResponse,
      dependencies=[Depends(odoo_env)],
  )
  async def echo2(
      message: str,
      odoo_env: Annotated[Environment, Depends(odoo_env)],
  ) -> EchoResponse:
      """Echo the message"""
      echo = odoo_env["demo.fastapi.endpoint"].echo2(message)
      return EchoResponse(message=f"Echo2: {echo}")

The problem with this approach is that you unconditionally add the new route
handler to the existing app even if the app is called for a different database
where your new addon is not installed.

The solution is to define a new router and to add it to the list of routers
returned by the method **'_get_fastapi_routers'** of the model
**'fastapi.endpoint'** you are inheriting from into your new addon.

.. code-block:: python

  class FastapiEndpoint(models.Model):

      _inherit = "fastapi.endpoint"

      def _get_fastapi_routers(self) -> List[APIRouter]:
          routers = super()._get_fastapi_routers()
          if self.app == "demo":
              routers.append(additional_demo_api_router)
          return routers

  additional_demo_api_router = APIRouter()

  @additional_demo_api_router.get(
      "/echo2",
      response_model=EchoResponse,
      dependencies=[Depends(odoo_env)],
  )
  async def echo2(
      message: str,
      odoo_env: Annotated[Environment, Depends(odoo_env)],
  ) -> EchoResponse:
      """Echo the message"""
      echo = odoo_env["demo.fastapi.endpoint"].echo2(message)
      return EchoResponse(message=f"Echo2: {echo}")


In this way, the new router is added to the list of routers of your app only if
the app is called for a database where your new addon is installed.

Extending the model used as parameter or as response of the route handler
=========================================================================

The fastapi python library uses the pydantic library to define the models. By
default, once a model is defined, it's not possible to extend it. However, a
companion python library called
`extendable_pydantic <https://pypi.org/project/extendable_pydantic/>`_ provides
a way to use inheritance with pydantic models to extend an existing model. If
used alone, it's your responsibility to instruct this library the list of
extensions to apply to a model and the order to apply them. This is not very
convenient. Fortunately, an dedicated odoo addon exists to make this process
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

.. code-block:: python

  from pydantic import BaseModel
  from extendable_pydantic import ExtendableModelMeta

  class Partner(BaseModel, metaclass=ExtendableModelMeta):
    name = 0.1
    model_config = ConfigDict(from_attributes=True)

As any other pydantic model, you can now use this model as parameter or as response
of a route handler. You can also use all the features of models defined with
pydantic.

.. code-block:: python

  @demo_api_router.get(
      "/partner",
      response_model=Location,
      dependencies=[Depends(authenticated_partner)],
  )
  async def partner(
      partner: Annotated[ResPartner, Depends(authenticated_partner)],
  ) -> Partner:
      """Return the location"""
      return Partner.model_validate(partner)


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'**.

.. code-block:: python

  from typing import Optional
  from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner

  class PartnerExtended(Partner, extends=Partner):
      email: Optional[str]

If your new addon is installed in a database, a call to the route handler
**'/demo/partner'** will return a response with the new field **'email'** if a
value is provided by the odoo record.

.. code-block:: python

  {
    "name": "John Doe",
    "email": "jhon.doe@acsone.eu"
  }

If your new addon is not installed in a database, a call to the route handler
**'/demo/partner'** will only return the name of the partner.

.. code-block:: python

  {
    "name": "John Doe"
  }

.. note::

  The liskov substitution principle has also to be respected. That means that
  if you extend a model, you must add new required fields or you must provide
  default values for the new optional fields.

Managing security into the route handlers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default the route handlers are processed using the user configured on the
**'fastapi.endpoint'** model instance. (default is the Public user).
You have seen previously how to define a dependency that will be used to enforce
the authentication of a partner. When a method depends on this dependency, the
'authenticated_partner_id' key is added to the context of the partner environment.
(If you don't need the partner as dependency but need to get an environment
with the authenticated user, you can use the dependency 'authenticated_partner_env' instead of
'authenticated_partner'.)

The fastapi addon extends the 'ir.rule' model to add into the evaluation context
of the security rules the key 'authenticated_partner_id' that contains the id
of the authenticated partner.

As briefly introduced in a previous section, a good practice when you develop a
fastapi app and you want to protect your data in an efficient and traceable way is to:

* create a new user specific to the app but with any access rights.
* create a security group specific to the app and add the user to this group. (This
  group must implies the group 'AFastAPI Endpoint Runner' that give the
  minimal access rights)
* for each model you want to protect:

  * add a 'ir.model.access' record for the model to allow read access to your model
    and add the group to the record.
  * create a new 'ir.rule' record for the model that restricts the access to the
    records of the model to the authenticated partner by using the key
    'authenticated_partner_id' in domain of the rule. (or to the user defined on
    the 'fastapi.endpoint' model instance if the method is public)

* add a dependency on the 'authenticated_partner' to your handlers when you need
  to access the authenticated partner or ensure that the service is called by an
  authenticated partner.

.. code-block:: xml

  <record
        id="my_demo_app_user"
        model="res.users"
        context="{'no_reset_password': True, 'no_reset_password': True}"
    >
    <field name="name">My Demo Endpoint User</field>
    <field name="login">my_demo_app_user</field>
    <field name="groups_id" eval="[(6, 0, [])]" />
  </record>

  <record id="my_demo_app_group" model="res.groups">
    <field name="name">My Demo Endpoint Group</field>
    <field name="users" eval="[(4, ref('my_demo_app_user'))]" />
    <field name="implied_ids" eval="[(4, ref('group_fastapi_endpoint_runner'))]" />
  </record>

  <!-- acl for the model 'sale.order' -->
  <record id="sale_order_demo_app_access" model="ir.model.access">
    <field name="name">My Demo App: access to sale.order</field>
    <field name="model_id" ref="model_sale_order"/>
    <field name="group_id" ref="my_demo_app_group"/>
    <field name="perm_read" eval="True"/>
    <field name="perm_write" eval="False"/>
    <field name="perm_create" eval="False"/>
    <field name="perm_unlink" eval="False"/>
  </record>

  <!-- a record rule to allows the authenticated partner to access only its sale orders -->
  <record id="demo_app_sale_order_rule" model="ir.rule">
    <field name="name">Sale Order Rule</field>
    <field name="model_id" ref="model_sale_order"/>
    <field name="domain_force">[('partner_id', '=', authenticated_partner_id)]</field>
    <field name="groups" eval="[(4, ref('my_demo_app_group'))]"/>
  </record>

How to test your fastapi app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Thanks to the starlette test client, it's possible to test your fastapi app
in a very simple way. With the test client, you can call your route handlers
as if they were real http endpoints. The test client is available in the
**'fastapi.testclient'** module.

Once again the dependency injection mechanism comes to the rescue by allowing
you to inject into the test client specific implementations of the dependencies
normally provided by the normal processing of the request by the fastapi app.
(for example, you can inject a mock of the dependency 'authenticated_partner'
to test the behavior of your route handlers when the partner is not authenticated,
you can also inject a mock for the odoo_env etc...)

The fastapi addon provides a base class for the test cases that you can use to
write your tests. This base class is **'odoo.fastapi.tests.common.FastAPITransactionCase'**.
This class mainly provides the method **'_create_test_client'** that you can
use to create a test client for your fastapi app. This method encapsulates the
creation of the test client and the injection of the dependencies. It also
ensures that the odoo environment is make available into the context of the
route handlers. This method is designed to be used when you need to test your
app or when you need to test a specific router (It's therefore easy to defines
tests for routers in an addon that doesn't provide a fastapi endpoint).

With this base class, writing a test for a route handler is as simple as:

.. code-block:: python

  from odoo.fastapi.tests.common import FastAPITransactionCase

  from odoo.addons.fastapi import dependencies
  from odoo.addons.fastapi.routers import demo_router

  class FastAPIDemoCase(FastAPITransactionCase):

      @classmethod
      def setUpClass(cls) -> None:
          super().setUpClass()
          cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user")
          cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"})

      def test_hello_world(self) -> None:
          with self._create_test_client(router=demo_router) as test_client:
              response: Response = test_client.get("/demo/")
          self.assertEqual(response.status_code, status.HTTP_200_OK)
          self.assertDictEqual(response.json(), {"Hello": "World"})


In the previous example, we created a test client for the demo_router. We could
have created a test client for the whole app by not specifying the router but
the app instead.

.. code-block:: python

  from odoo.fastapi.tests.common import FastAPITransactionCase

  from odoo.addons.fastapi import dependencies
  from odoo.addons.fastapi.routers import demo_router

  class FastAPIDemoCase(FastAPITransactionCase):

      @classmethod
      def setUpClass(cls) -> None:
          super().setUpClass()
          cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user")
          cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"})

      def test_hello_world(self) -> None:
          demo_endpoint = self.env.ref("fastapi.fastapi_endpoint_demo")
          with self._create_test_client(app=demo_endpoint._get_app()) as test_client:
              response: Response = test_client.get(f"{demo_endpoint.root_path}/demo/")
          self.assertEqual(response.status_code, status.HTTP_200_OK)
          self.assertDictEqual(response.json(), {"Hello": "World"})


Overall considerations when you develop an fastapi app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Developing a fastapi app requires to follow some good practices to ensure that
the app is robust and easy to maintain. Here are some of them:

* A route handler must be as simple as possible. It must not contain any
  business logic. The business logic must be implemented into the service
  layer. The route handler must only call the service layer and return the
  result of the service layer. To ease extension on your business logic, your
  service layer can be implemented as an odoo abstract model that can be
  inherited by other addons.

* A route handler should not expose the internal data structure and api of Odoo.
  It should provide the api that is needed by the client. More widely, an app
  provides a set of services that address a set of use cases specific to
  a well defined functional domain. You must always keep in mind that your api
  will remain the same for a long time even if you upgrade your odoo version
  of modify your business logic.

* A route handler is a transactional unit of work. When you design your api
  you must ensure that the completeness of a use case is guaranteed by a single
  transaction. If you need to perform several transactions to complete a use
  case, you introduce a risk of inconsistency in your data or extra complexity
  in your client code.

* Properly handle the errors. The route handler must return a proper error
  response when an error occurs. The error response must be consistent with
  the rest of the api. The error response must be documented in the api
  documentation. By default, the **'odoo-addon-fastapi'** module handles
  the common exception types defined in the **'odoo.exceptions'** module
  and returns a proper error response with the corresponding http status code.
  An error in the route handler must always return an error response with a
  http status code different from 200. The error response must contain a
  human readable message that can be displayed to the user. The error response
  can also contain a machine readable code that can be used by the client to
  handle the error in a specific way.

* When you design your json document through the pydantic models, you must
  use the appropriate data types. For example, you must use the data type
  **'datetime.date'** to represent a date and not a string. You must also
  properly define the constraints on the fields. For example, if a field
  is optional, you must use the data type **'typing.Optional'**.
  `pydantic`_ provides everything you need to
  properly define your json document.

* Always use an appropriate pydantic model as request and/or response for
  your route handler. Constraints on the fields of the pydantic model must
  apply to the specific use case. For example, if your route handler is used
  to create a sale order, the pydantic model must not contain the field
  'id' because the id of the sale order will be generated by the route handler.
  But if the id is required afterwords, the pydantic model for the response
  must contain the field 'id' as required.

* Uses descriptive property names in your json documents. For example, avoid the
  use of documents providing a flat list of key value pairs.

* Be consistent in the naming of your fields into your json documents. For example,
  if you use 'id' to represent the id of a sale order, you must use 'id' to represent
  the id of all the other objects.

* Be consistent in the naming style of your fields. Always prefer underscore
  to camel case.

* Always use plural for the name of the fields that contain a list of items.
  For example, if you have a field 'lines' that contains a list of sale order
  lines, you must use 'lines' and not 'line'.

* You can't expect that a client will provide you the identifier of a specific
  record in odoo (for example the id of a carrier) if you don't provide a
  specific route handler to retrieve the list of available records. Sometimes,
  the client must share with odoo the identity of a specific record to be
  able to perform an appropriate action specific to this record (for example,
  the processing of a payment is different for each payment acquirer). In this
  case, you must provide a specific attribute that allows both the client and
  odoo to identify the record. The field 'provider' on a payment acquirer allows
  you to identify a specific record in odoo. This kind of approach
  allows both the client and odoo to identify the record without having to rely
  on the id of the record. (This will ensure that the client will not break
  if the id of the record is changed in odoo for example when tests are run
  on an other database).

* Always use the same name for the same kind of object. For example, if you
  have a field 'lines' that contains a list of sale order lines, you must
  use the same name for the same kind of object in all the other json documents.

* Manage relations between objects in your json documents the same way.
  By default, you should return the id of the related object in the json document.
  But this is not always possible or convenient, so you can also return the
  related object in the json document. The main advantage of returning the id
  of the related object is that it allows you to avoid the `n+1 problem
  <https://restfulapi.net/rest-api-n-1-problem/>`_ . The
  main advantage of returning the related object in the json document is that
  it allows you to avoid an extra call to retrieve the related object.
  By keeping in mind the pros and cons of each approach, you can choose the
  best one for your use case. Once it's done, you must be consistent in the
  way you manage the relations of the same object.

* It's not always a good idea to name your fields into your json documents
  with the same name as the fields of the corresponding odoo model. For example,
  in your document representing a sale order, you must not use the name 'order_line'
  for the field that contains the list of sale order lines. The name 'order_line'
  in addition to being confusing and not consistent with the best practices, is
  not auto-descriptive. The name 'lines' is much better.

* Keep a defensive programming approach. If you provide a route handler that
  returns a list of records, you must ensure that the computation of the list
  is not too long or will not drain your server resources. For example,
  for search route handlers, you must ensure that the search is limited to
  a reasonable number of records by default.

* 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 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'
  and not 'sale_order'.



* ... and many more.

We could write a book about the best practices to follow when you design your api
but we will stop here. This list is the result of our experience at `ACSONE SA/NV
<https://acsone.eu>`_ and it evolves over time. It's a kind of rescue kit that we
would provide to a new developer that starts to design an api. This kit must
be accompanied with the reading of some useful resources link like the `REST Guidelines
<https://www.belgif.be/specification/rest/api-guide/>`_. On a technical level,
the `fastapi  documentation <https://fastapi.tiangolo.com/>`_ provides a lot of
useful information as well, with a lot of examples. Last but not least, the
`pydantic`_ documentation is also very useful.

Miscellaneous
~~~~~~~~~~~~~

Development of a search route handler
=====================================

The **'odoo-addon-fastapi'** module provides 2 useful piece of code to help
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 count of records.

.. code-block:: python

    from typing import Annotated
    from pydantic import BaseModel

    from odoo.api import Environment
    from odoo.addons.fastapi.dependencies import paging, authenticated_partner_env
    from odoo.addons.fastapi.schemas import PagedCollection, Paging

    class SaleOrder(BaseModel):
        id: int
        name: str
        model_config = ConfigDict(from_attributes=True)


    @router.get(
        "/sale_orders",
        response_model=PagedCollection[SaleOrder],
        response_model_exclude_unset=True,
    )
    def get_sale_orders(
        paging: Annotated[Paging, Depends(paging)],
        env: Annotated[Environment, Depends(authenticated_partner_env)],
    ) -> PagedCollection[SaleOrder]:
        """Get the list of sale orders."""
        count = env["sale.order"].search_count([])
        orders = env["sale.order"].search([], limit=paging.limit, offset=paging.offset)
        return PagedCollection[SaleOrder](
            count=count,
            items=[SaleOrder.model_validate(order) for order in orders],
        )

.. note::

    The **'odoo.addons.fastapi.schemas.Paging'** and **'odoo.addons.fastapi.schemas.PagedCollection'**
    pydantic models are not designed to be extended to not introduce a
    dependency between the **'odoo-addon-fastapi'** module and the **'odoo-addon-extendable'**


Customization of the error handling
===================================

The error handling a very important topic in the design of the fastapi integration
with odoo. It must ensure that the error messages are properly return to the client
and that the transaction is properly roll backed. The **'fastapi'** module provides
a way to register custom error handlers. The **'odoo.addons.fastapi.error_handlers'**
module provides the default error handlers that are registered by default when
a new instance of the **'FastAPI'** class is created. When an app is initialized in
'fastapi.endpoint' model, the method `_get_app_exception_handlers` is called to
get a dictionary of error handlers. This method is designed to be overridden
in a custom module to provide custom error handlers. You can override the handler
for a specific exception class or you can add a new handler for a new exception
or even replace all the handlers by your own handlers. Whatever you do, you must
ensure that the transaction is properly roll backed.

Some could argue that the error handling can't be extended since the error handlers
are global method not defined in an odoo model. Since the method providing the
the error handlers definitions is defined on the 'fastapi.endpoint' model, it's
not a problem at all, you just need to think another way to do it that by inheritance.

A solution could be to develop you own error handler to be able to process the
error and chain the call to the default error handler.

.. code-block:: python

    class MyCustomErrorHandler():
        def __init__(self, next_handler):
            self.next_handler = next_handler

        def __call__(self, request: Request, exc: Exception) -> JSONResponse:
            # do something with the error
            response = self.next_handler(request, exc)
            # do something with the response
            return response


With this solution, you can now register your custom error handler by overriding
the method `_get_app_exception_handlers` in your custom module.

.. code-block:: python

    class FastapiEndpoint(models.Model):
        _inherit = "fastapi.endpoint"

        def _get_app_exception_handlers(
            self,
        ) -> Dict[
            Union[int, Type[Exception]],
            Callable[[Request, Exception], Union[Response, Awaitable[Response]]],
        ]:
            handlers = super()._get_app_exception_handlers()
            access_error_handler = handlers.get(odoo.exceptions.AccessError)
            handlers[odoo.exceptions.AccessError] = MyCustomErrorHandler(access_error_handler)
            return handlers

In the previous example, we extend the error handler for the 'AccessError' exception
for all the endpoints. You can do the same for a specific app by checking the
'app' field of the 'fastapi.endpoint' record before registering your custom error
handler.

FastAPI addons directory structure
==================================

When you develop a new addon to expose an api with fastapi, it's a good practice
to follow the same directory structure and naming convention for the files
related to the api. It will help you to easily find the files related to the api
and it will help the other developers to understand your code.

Here is the directory structure that we recommend. It's based on practices that
are used in the python community when developing a fastapi app.

.. code-block::

  .
  ├── x_api
  │   ├── data
  │   │   ├── ... .xml
  │   ├── demo
  │   │   ├── ... .xml
  │   ├── i18n
  │   │   ├── ... .po
  │   ├── models
  │   │   ├── __init__.py
  │   │   ├── fastapi_endpoint.py  # your app
  │   │   └── ... .py
  │   └── routers
  │   │   ├── __init__.py
  │   │   ├── items.py
  │   │   └── ... .py
  │   ├── schemas | schemas.py
  │   │   ├── __init__.py
  │   │   ├── my_model.py  # pydantic model
  │   │   └── ... .py
  │   ├── security
  │   │   ├── ... .xml
  │   ├── views
  │   │   ├── ... .xml
  │   ├── __init__.py
  │   ├── __manifest__.py
  │   ├── dependencies.py  # custom dependencies
  │   ├── error_handlers.py  # custom error handlers


* The **'models'** directory contains the odoo models. When you define a new
  app, as for the others addons, you will add your new model inheriting from
  the **'fastapi.endpoint'** model in this directory.
* The **'routers'** directory contains the fastapi routers. You will add your
  new routers in this directory. Each route starting with the same prefix should
  be grouped in the same file. For example, all the routes starting with
  '/items' should be defined in the **'items.py'** file. The **'__init__.py'**
  file in this directory is used to import all the routers defined in the
  directory and create a global router that can be used in an app. For example,
  in your **'items.py'** file, you will define a router like this:

  .. code-block:: python

    router = APIRouter(tags=["items"])

    router.get("/items", response_model=List[Item])
    def list_items():
        pass

  In the **'__init__.py'** file, you will import the router and add it to the global
  router or your addon.

  .. code-block:: python

    from fastapi import APIRouter

    from .items import router as items_router

    router = APIRouter()
    router.include_router(items_router)

* The **'schemas.py'** will be used to define the pydantic models. For complex
  APIs with a lot of models, it will be better to create a **'schemas'** directory
  and split the models in different files.  The **'__init__.py'** file in this
  directory will be used to import all the models defined in the directory.
  For example, in your **'my_model.py'**
  file, you will define a model like this:

  .. code-block:: python

    from pydantic import BaseModel

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

  In the **'__init__.py'** file, you will import the model's classes from the
  files in the directory.

  .. code-block:: python

    from .my_model import MyModel

  This will allow to always import the models from the schemas module whatever
  the models are spread across different files or defined in the **'schemas.py'**
  file.

  .. code-block:: python

    from x_api_addon.schemas import MyModel

* The **'dependencies.py'** file contains the custom dependencies that you
  will use in your routers. For example, you can define a dependency to
  check the access rights of the user.
* The **'error_handlers.py'** file contains the custom error handlers that you
  will use in your routers. The **'odoo-addon-fastapi'** module provides the
  default error handlers for the common odoo exceptions. Chance are that you
  will not need to define your own error handlers. But if you need to do it,
  you can define them in this file.

What's next?
~~~~~~~~~~~~

The **'odoo-addon-fastapi'** module is still in its early stage of development.
It will evolve over time to integrate your feedback and to provide the missing
features. It's now up to you to try it and to provide your feedback.

.. _pydantic: https://docs.pydantic.dev/

Known issues / Roadmap
======================

The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Afastapi>`_
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Afastapi>`_ can
be found on GitHub.

The **FastAPI** module provides an easy way to use WebSockets. Unfortunately, this
support is not 'yet' available in the **Odoo** framework. The challenge is high
because the integration of the fastapi is based on the use of a specific middleware
that convert the WSGI request consumed by odoo to a ASGI request. The question
is to know if it is also possible to develop the same kind of bridge for the
WebSockets and to stream large responses.

Changelog
=========

16.0.1.2.6 (2024-02-20)
~~~~~~~~~~~~~~~~~~~~~~~

**Bugfixes**

- Fix compatibility issues with the latest Odoo version

  From https://github.com/odoo/odoo/commit/cb1d057dcab28cb0b0487244ba99231ee292502e
  the original werkzeug HTTPRequest class has been wrapped in a new class to keep
  under control the attributes developers use. This changes take care of this
  new implementation but also keep compatibility with the old ones. (`#414 <https://github.com/OCA/rest-framework/issues/414>`_)


16.0.1.2.5 (2024-01-17)
~~~~~~~~~~~~~~~~~~~~~~~

**Bugfixes**

- Odoo has done an update and now, it checks domains of ir.rule on creation and modification.

  The ir.rule 'Fastapi: Running user rule' uses a field (authenticate_partner_id) that comes from the context.
  This field wasn't always set and this caused an error when Odoo checked the domain.
  So now it is set to *False* by default. (`#410 <https://github.com/OCA/rest-framework/issues/410>`_)


16.0.1.2.3 (2023-12-21)
~~~~~~~~~~~~~~~~~~~~~~~

**Bugfixes**

- In case of exception in endpoint execution, close the database cursor after rollback.

  This is to ensure that the *retrying* method in *service/model.py* does not try
  to flush data to the database. (`#405 <https://github.com/OCA/rest-framework/issues/405>`_)


16.0.1.2.2 (2023-12-12)
~~~~~~~~~~~~~~~~~~~~~~~

**Bugfixes**

- When using the 'FastAPITransactionCase' class, allows to specify a specific
  override of the 'authenticated_partner_impl' method into the list of
  overrides to apply. Before this change, the 'authenticated_partner_impl'
  override given in the 'overrides' parameter was always overridden in the
  '_create_test_client' method of the 'FastAPITransactionCase' class. It's now
  only overridden if the 'authenticated_partner_impl' method is not already
  present in the list of overrides to apply and no specific partner is given.
  If a specific partner is given at same time of an override for the
  'authenticated_partner_impl' method, an error is raised. (`#396 <https://github.com/OCA/rest-framework/issues/396>`_)


16.0.1.2.1 (2023-11-03)
~~~~~~~~~~~~~~~~~~~~~~~

**Bugfixes**

- Fix a typo in the Field declaration of the 'count' attribute of the 'PagedCollection' schema.

  Misspelt parameter was triggering a deprecation warning due to recent versions of Pydantic seeing it as an arbitrary parameter. (`#389 <https://github.com/OCA/rest-framework/issues/389>`_)


16.0.1.2.0 (2023-10-13)
~~~~~~~~~~~~~~~~~~~~~~~

**Features**

- 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. (`#380 <https://github.com/OCA/rest-framework/issues/380>`_)

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 to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20fastapi%0Aversion:%2016.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/16.0/fastapi>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.



            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/OCA/rest-framework",
    "name": "odoo-addon-fastapi",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": null,
    "author": "ACSONE SA/NV,Odoo Community Association (OCA)",
    "author_email": "support@odoo-community.org",
    "download_url": null,
    "platform": null,
    "description": "============\nOdoo FastAPI\n============\n\n.. \n   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n   !! This file is generated by oca-gen-addon-readme !!\n   !! changes will be overwritten.                   !!\n   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n   !! source digest: sha256:f3cc5405e18a54d995ec4f61dcaab89c32e72e625a72d0cb3bf1ba6c0800d6be\n   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png\n    :target: https://odoo-community.org/page/development-status\n    :alt: Beta\n.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png\n    :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html\n    :alt: License: LGPL-3\n.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github\n    :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi\n    :alt: OCA/rest-framework\n.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png\n    :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi\n    :alt: Translate me on Weblate\n.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png\n    :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=16.0\n    :alt: Try me on Runboat\n\n|badge1| |badge2| |badge3| |badge4| |badge5|\n\nThis addon provides the basis to smoothly integrate the `FastAPI`_\nframework into Odoo.\n\nThis integration allows you to use all the goodies from `FastAPI`_ to build custom\nAPIs for your Odoo server based on standard Python type hints.\n\n**What is building an API?**\n\nAn API is a set of functions that can be called from the outside world. The\ngoal of an API is to provide a way to interact with your application from the\noutside world without having to know how it works internally. A common mistake\nwhen you are building an API is to expose all the internal functions of your\napplication and therefore create a tight coupling between the outside world and\nyour internal datamodel and business logic. This is not a good idea because it\nmakes it very hard to change your internal datamodel and business logic without\nbreaking the outside world.\n\nWhen you are building an API, you define a contract between the outside world\nand your application. This contract is defined by the functions that you expose\nand the parameters that you accept. This contract is the API. When you change\nyour internal datamodel and business logic, you can still keep the same API\ncontract and therefore you don't break the outside world. Even if you change\nyour implementation, as long as you keep the same API contract, the outside\nworld will still work. This is the beauty of an API and this is why it is so\nimportant to design a good API.\n\nA good API is designed to be stable and to be easy to use. It's designed to\nprovide high-level functions related to a specific use case. It's designed to\nbe easy to use by hiding the complexity of the internal datamodel and business\nlogic. A common mistake when you are building an API is to expose all the internal\nfunctions of your application and let the oustide world deal with the complexity\nof your internal datamodel and business logic. Don't forget that on a transactional\npoint of view, each call to an API function is a transaction. This means that\nif a specific use case requires multiple calls to your API, you should provide\na single function that does all the work in a single transaction. This why APIs\nmethods are called high-level and atomic functions.\n\n.. _FastAPI:  https://fastapi.tiangolo.com/\n\n**Table of contents**\n\n.. contents::\n   :local:\n\nUsage\n=====\n\nWhat's building an API with fastapi?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nFastAPI is a modern, fast (high-performance), web framework for building APIs\nwith Python 3.7+ based on standard Python type hints. This addons let's you\nkeep advantage of the fastapi framework and use it with Odoo.\n\nBefore you start, we must define some terms:\n\n* **App**: A FastAPI app is a collection of routes, dependencies, and other\n  components that can be used to build a web application.\n* **Router**: A router is a collection of routes that can be mounted in an\n  app.\n* **Route**: A route is a mapping between an HTTP method and a path, and\n  defines what should happen when the user requests that path.\n* **Dependency**: A dependency is a callable that can be used to get some\n  information from the user request, or to perform some actions before the\n  request handler is called.\n* **Request**: A request is an object that contains all the information\n  sent by the user's browser as part of an HTTP request.\n* **Response**: A response is an object that contains all the information\n  that the user's browser needs to build the result page.\n* **Handler**: A handler is a function that takes a request and returns a\n  response.\n* **Middleware**: A middleware is a function that takes a request and a\n  handler, and returns a response.\n\nThe FastAPI framework is based on the following principles:\n\n* **Fast**: Very high performance, on par with NodeJS and Go (thanks to Starlette\n  and Pydantic). [One of the fastest Python frameworks available]\n* **Fast to code**: Increase the speed to develop features by about 200% to 300%.\n* **Fewer bugs**: Reduce about 40% of human (developer) induced errors.\n* **Intuitive**: Great editor support. Completion everywhere. Less time\n  debugging.\n* **Easy**: Designed to be easy to use and learn. Less time reading docs.\n* **Short**: Minimize code duplication. Multiple features from each parameter\n  declaration. Fewer bugs.\n* **Robust**: Get production-ready code. With automatic interactive documentation.\n* **Standards-based**: Based on (and fully compatible with) the open standards\n  for APIs: OpenAPI (previously known as Swagger) and JSON Schema.\n* **Open Source**: FastAPI is fully open-source, under the MIT license.\n\nThe first step is to install the fastapi addon. You can do it with the\nfollowing command:\n\n    $ pip install odoo-addon-fastapi\n\nOnce the addon is installed, you can start building your API. The first thing\nyou need to do is to create a new addon that depends on 'fastapi'. For example,\nlet's create an addon called *my_demo_api*.\n\nThen, you need to declare your app by defining a model that inherits from\n'fastapi.endpoint' and add your app name into the app field. For example:\n\n.. code-block:: python\n\n    from odoo import fields, models\n\n    class FastapiEndpoint(models.Model):\n\n        _inherit = \"fastapi.endpoint\"\n\n        app: str = fields.Selection(\n            selection_add=[(\"demo\", \"Demo Endpoint\")], ondelete={\"demo\": \"cascade\"}\n        )\n\nThe **'fastapi.endpoint'** model is the base model for all the endpoints. An endpoint\ninstance is the mount point for a fastapi app into Odoo. When you create a new\nendpoint, you can define the app that you want to mount in the **'app'** field\nand the path where you want to mount it in the **'path'** field.\n\nfigure:: static/description/endpoint_create.png\n\n    FastAPI Endpoint\n\nThanks to the **'fastapi.endpoint'** model, you can create as many endpoints as\nyou want and mount as many apps as you want in each endpoint. The endpoint is\nalso the place where you can define configuration parameters for your app. A\ntypical example is the authentication method that you want to use for your app\nwhen accessed at the endpoint path.\n\nNow, you can create your first router. For that, you need to define a global\nvariable into your fastapi_endpoint module called for example 'demo_api_router'\n\n.. code-block:: python\n\n    from fastapi import APIRouter\n    from odoo import fields, models\n\n    class FastapiEndpoint(models.Model):\n\n        _inherit = \"fastapi.endpoint\"\n\n        app: str = fields.Selection(\n            selection_add=[(\"demo\", \"Demo Endpoint\")], ondelete={\"demo\": \"cascade\"}\n        )\n\n    # create a router\n    demo_api_router = APIRouter()\n\n\nTo make your router available to your app, you need to add it to the list of routers\nreturned by the **_get_fastapi_routers** method of your fastapi_endpoint model.\n\n.. code-block:: python\n\n    from fastapi import APIRouter\n    from odoo import api, fields, models\n\n    class FastapiEndpoint(models.Model):\n\n        _inherit = \"fastapi.endpoint\"\n\n        app: str = fields.Selection(\n            selection_add=[(\"demo\", \"Demo Endpoint\")], ondelete={\"demo\": \"cascade\"}\n        )\n\n        def _get_fastapi_routers(self):\n            if self.app == \"demo\":\n                return [demo_api_router]\n            return super()._get_fastapi_routers()\n\n    # create a router\n    demo_api_router = APIRouter()\n\nNow, you can start adding routes to your router. For example, let's add a route\nthat returns a list of partners.\n\n.. code-block:: python\n\n    from typing import Annotated\n\n    from fastapi import APIRouter\n    from pydantic import BaseModel\n\n    from odoo import api, fields, models\n    from odoo.api import Environment\n\n    from odoo.addons.fastapi.dependencies import odoo_env\n\n    class FastapiEndpoint(models.Model):\n\n        _inherit = \"fastapi.endpoint\"\n\n        app: str = fields.Selection(\n            selection_add=[(\"demo\", \"Demo Endpoint\")], ondelete={\"demo\": \"cascade\"}\n        )\n\n        def _get_fastapi_routers(self):\n            if self.app == \"demo\":\n                return [demo_api_router]\n            return super()._get_fastapi_routers()\n\n    # create a router\n    demo_api_router = APIRouter()\n\n    class PartnerInfo(BaseModel):\n        name: str\n        email: str\n\n    @demo_api_router.get(\"/partners\", response_model=list[PartnerInfo])\n    def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]:\n        return [\n            PartnerInfo(name=partner.name, email=partner.email)\n            for partner in env[\"res.partner\"].search([])\n        ]\n\nNow, you can start your Odoo server, install your addon and create a new endpoint\ninstance for your app. Once it's done click on the docs url to access the\ninteractive documentation of your app.\n\nBefore trying to test your app, you need to define on the endpoint instance the\nuser that will be used to run the app. You can do it by setting the **'user_id'**\nfield. This information is the most important one because it's the basis for\nthe security of your app. The user that you define in the endpoint instance\nwill be used to run the app and to access the database. This means that the\nuser will be able to access all the data that he has access to in Odoo. To ensure\nthe security of your app, you should create a new user that will be used only\nto run your app and that will have no access to the database.\n\n.. code-block:: xml\n\n  <record\n        id=\"my_demo_app_user\"\n        model=\"res.users\"\n        context=\"{'no_reset_password': True, 'no_reset_password': True}\"\n    >\n    <field name=\"name\">My Demo Endpoint User</field>\n    <field name=\"login\">my_demo_app_user</field>\n    <field name=\"groups_id\" eval=\"[(6, 0, [])]\" />\n  </record>\n\nAt the same time you should create a new group that will be used to define the\naccess rights of the user that will run your app. This group should imply\nthe predefined group **'FastAPI Endpoint Runner'**. This group defines the\nminimum access rights that the user needs to:\n\n* access the endpoint instance it belongs to\n* access to its own user record\n* access to the partner record that is linked to its user record\n\n.. code-block:: xml\n\n  <record id=\"my_demo_app_group\" model=\"res.groups\">\n    <field name=\"name\">My Demo Endpoint Group</field>\n    <field name=\"users\" eval=\"[(4, ref('my_demo_app_user'))]\" />\n    <field name=\"implied_ids\" eval=\"[(4, ref('fastapi.group_fastapi_endpoint_runner'))]\" />\n  </record>\n\n\nNow, you can test your app. You can do it by clicking on the 'Try it out' button\nof the route that you have defined. The result of the request will be displayed\nin the 'Response' section and contains the list of partners.\n\n.. note::\n  The **'FastAPI Endpoint Runner'** group ensures that the user cannot access any\n  information others than the 3 ones mentioned above. This means that for every\n  information that you want to access from your app, you need to create the\n  proper ACLs and record rules. (see `Managing security into the route handlers`_)\n  It's a good practice to use a dedicated user into a specific group from the\n  beginning of your project and in your tests. This will force you to define\n  the proper security rules for your endoints.\n\nDealing with the odoo environment\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe **'odoo.addons.fastapi.dependencies'** module provides a set of functions that you can use\nto inject reusable dependencies into your routes. For example, the **'odoo_env'**\nfunction returns the current odoo environment. You can use it to access the\nodoo models and the database from your route handlers.\n\n.. code-block:: python\n\n    from typing import Annotated\n\n    from odoo.api import Environment\n    from odoo.addons.fastapi.dependencies import odoo_env\n\n    @demo_api_router.get(\"/partners\", response_model=list[PartnerInfo])\n    def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]:\n        return [\n            PartnerInfo(name=partner.name, email=partner.email)\n            for partner in env[\"res.partner\"].search([])\n        ]\n\nAs you can see, you can use the **'Depends'** function to inject the dependency\ninto your route handler. The **'Depends'** function is provided by the\n**'fastapi'** framework. You can use it to inject any dependency into your route\nhandler. As your handler is a python function, the only way to get access to\nthe odoo environment is to inject it as a dependency. The fastapi addon provides\na set of function that can be used as dependencies:\n\n* **'odoo_env'**: Returns the current odoo environment.\n* **'fastapi_endpoint'**: Returns the current fastapi endpoint model instance.\n* **'authenticated_partner'**: Returns the authenticated partner.\n* **'authenticated_partner_env'**: Returns the current odoo environment with the\n  authenticated_partner_id into the context.\n\nBy default, the **'odoo_env'** and **'fastapi_endpoint'** dependencies are\navailable without extra work.\n\n.. note::\n  Even if 'odoo_env' and 'authenticated_partner_env' returns the current odoo\n  environment, they are not the same. The 'odoo_env' dependency returns the\n  environment without any modification while the 'authenticated_partner_env'\n  adds the authenticated partner id into the context of the environment. As it will\n  be explained in the section `Managing security into the route handlers`_ dedicated\n  to the security, the presence of the authenticated partner id into the context\n  is the key information that will allow you to enforce the security of your endpoint\n  methods. As consequence, you should always use the 'authenticated_partner_env'\n  dependency instead of the 'odoo_env' dependency for all the methods that are\n  not public.\n\nThe dependency injection mechanism\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe **'odoo_env'** dependency relies on a simple implementation that retrieves\nthe current odoo environment from ContextVar variable initialized at the start\nof the request processing by the specific request dispatcher processing the\nfastapi requests.\n\nThe **'fastapi_endpoint'** dependency relies on the 'dependency_overrides' mechanism\nprovided by the **'fastapi'** module. (see the fastapi documentation for more\ndetails about the dependency_overrides mechanism). If you take a look at the\ncurrent implementation of the **'fastapi_endpoint'** dependency, you will see\nthat the method depends of two parameters: **'endpoint_id'** and **'env'**. Each\nof these parameters are dependencies themselves.\n\n.. code-block:: python\n\n    def fastapi_endpoint_id() -> int:\n        \"\"\"This method is overriden by default to make the fastapi.endpoint record\n        available for your endpoint method. To get the fastapi.endpoint record\n        in your method, you just need to add a dependency on the fastapi_endpoint method\n        defined below\n        \"\"\"\n\n\n    def fastapi_endpoint(\n        _id: Annotated[int, Depends(fastapi_endpoint_id)],\n        env: Annotated[Environment, Depends(odoo_env)],\n    ) -> \"FastapiEndpoint\":\n        \"\"\"Return the fastapi.endpoint record\"\"\"\n        return env[\"fastapi.endpoint\"].browse(_id)\n\n\nAs you can see, one of these dependencies is the **'fastapi_endpoint_id'**\ndependency and has no concrete implementation. This method is used as a contract\nthat must be implemented/provided at the time the fastapi app is created.\nHere comes the power of the dependency_overrides mechanism.\n\nIf you take a look at the **'_get_app'** method of the **'FastapiEndpoint'** model,\nyou will see that the **'fastapi_endpoint_id'** dependency is overriden by\nregistering a specific method that returns the id of the current fastapi endpoint\nmodel instance for the original method.\n\n.. code-block:: python\n\n    def _get_app(self) -> FastAPI:\n        app = FastAPI(**self._prepare_fastapi_endpoint_params())\n        for router in self._get_fastapi_routers():\n            app.include_router(prefix=self.root_path, router=router)\n        app.dependency_overrides[dependencies.fastapi_endpoint_id] = partial(\n            lambda a: a, self.id\n        )\n\nThis kind of mechanism is very powerful and allows you to inject any dependency\ninto your route handlers and moreover, define an abstract dependency that can be\nused by any other addon and for which the implementation could depend on the\nendpoint configuration.\n\nThe authentication mechanism\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTo make our app not tightly coupled with a specific authentication mechanism,\nwe will use the **'authenticated_partner'** dependency. As for the\n**'fastapi_endpoint'** this dependency depends on an abstract dependency.\n\nWhen you define a route handler, you can inject the **'authenticated_partner'**\ndependency as a parameter of your route handler.\n\n.. code-block:: python\n\n    from odoo.addons.base.models.res_partner import Partner\n\n\n    @demo_api_router.get(\"/partners\", response_model=list[PartnerInfo])\n    def get_partners(\n        env: Annotated[Environment, Depends(odoo_env)], partner: Annotated[Partner, Depends(authenticated_partner)]\n    ) -> list[PartnerInfo]:\n        return [\n            PartnerInfo(name=partner.name, email=partner.email)\n            for partner in env[\"res.partner\"].search([])\n        ]\n\n\nAt this stage, your handler is not tied to a specific authentication mechanism\nbut only expects to get a partner as a dependency. Depending on your needs, you\ncan implement different authentication mechanism available for your app.\nThe fastapi addon provides a default authentication mechanism using the\n'BasicAuth' method. This authentication mechanism is implemented in the\n**'odoo.addons.fastapi.dependencies'** module and relies on functionalities provided\nby the **'fastapi.security'** module.\n\n.. code-block:: python\n\n      def authenticated_partner(\n          env: Annotated[Environment, Depends(odoo_env)],\n          security: Annotated[HTTPBasicCredentials, Depends(HTTPBasic())],\n      ) -> \"res.partner\":\n          \"\"\"Return the authenticated partner\"\"\"\n          partner = env[\"res.partner\"].search(\n              [(\"email\", \"=\", security.username)], limit=1\n          )\n          if not partner:\n              raise HTTPException(\n                  status_code=status.HTTP_401_UNAUTHORIZED,\n                  detail=\"Invalid authentication credentials\",\n                  headers={\"WWW-Authenticate\": \"Basic\"},\n              )\n          if not partner.check_password(security.password):\n              raise HTTPException(\n                  status_code=status.HTTP_401_UNAUTHORIZED,\n                  detail=\"Invalid authentication credentials\",\n                  headers={\"WWW-Authenticate\": \"Basic\"},\n              )\n          return partner\n\nAs you can see, the **'authenticated_partner'** dependency relies on the\n**'HTTPBasic'** dependency provided by the **'fastapi.security'** module.\nIn this dummy implementation, we just check that the provided credentials\ncan be used to authenticate a user in odoo. If the authentication is successful,\nwe return the partner record linked to the authenticated user.\n\nIn some cases you could want to implement a more complex authentication mechanism\nthat could rely on a token or a session. In this case, you can override the\n**'authenticated_partner'** dependency by registering a specific method that\nreturns the authenticated partner. Moreover, you can make it configurable on\nthe fastapi endpoint model instance.\n\nTo do it, you just need to implement a specific method for each of your\nauthentication mechanism and allows the user to select one of these methods\nwhen he creates a new fastapi endpoint. Let's say that we want to allow the\nauthentication by using an api key or via basic auth. Since basic auth is already\nimplemented, we will only implement the api key authentication mechanism.\n\n.. code-block:: python\n\n  from fastapi.security import APIKeyHeader\n\n  def api_key_based_authenticated_partner_impl(\n      api_key: Annotated[str, Depends(\n          APIKeyHeader(\n              name=\"api-key\",\n              description=\"In this demo, you can use a user's login as api key.\",\n          )\n      )],\n      env: Annotated[Environment, Depends(odoo_env)],\n  ) -> Partner:\n      \"\"\"A dummy implementation that look for a user with the same login\n      as the provided api key\n      \"\"\"\n      partner = env[\"res.users\"].search([(\"login\", \"=\", api_key)], limit=1).partner_id\n      if not partner:\n          raise HTTPException(\n              status_code=status.HTTP_401_UNAUTHORIZED, detail=\"Incorrect API Key\"\n          )\n      return partner\n\nAs for the 'BasicAuth' authentication mechanism, we also rely on one of the native\nsecurity dependency provided by the **'fastapi.security'** module.\n\nNow that we have an implementation for our two authentication mechanisms, we\ncan allows the user to select one of these authentication mechanisms by adding\na selection field on the fastapi endpoint model.\n\n.. code-block:: python\n\n  from odoo import fields, models\n\n  class FastapiEndpoint(models.Model):\n\n      _inherit = \"fastapi.endpoint\"\n\n      app: str = fields.Selection(\n        selection_add=[(\"demo\", \"Demo Endpoint\")], ondelete={\"demo\": \"cascade\"}\n      )\n      demo_auth_method = fields.Selection(\n          selection=[(\"api_key\", \"Api Key\"), (\"http_basic\", \"HTTP Bacic\")],\n          string=\"Authenciation method\",\n      )\n\n.. note::\n  A good practice is to prefix specific configuration fields of your app with\n  the name of your app. This will avoid conflicts with other app when the\n  'fastapi.endpoint' model is extended for other 'app'.\n\nNow that we have a selection field that allows the user to select the\nauthentication method, we can use the dependency override mechanism to\nprovide the right implementation of the **'authenticated_partner'** dependency\nwhen the app is instantiated.\n\n.. code-block:: python\n\n  from odoo.addons.fastapi.dependencies import authenticated_partner\n  class FastapiEndpoint(models.Model):\n\n      _inherit = \"fastapi.endpoint\"\n\n      app: str = fields.Selection(\n        selection_add=[(\"demo\", \"Demo Endpoint\")], ondelete={\"demo\": \"cascade\"}\n      )\n      demo_auth_method = fields.Selection(\n          selection=[(\"api_key\", \"Api Key\"), (\"http_basic\", \"HTTP Bacic\")],\n          string=\"Authenciation method\",\n      )\n\n    def _get_app(self) -> FastAPI:\n        app = super()._get_app()\n        if self.app == \"demo\":\n            # Here we add the overrides to the authenticated_partner_impl method\n            # according to the authentication method configured on the demo app\n            if self.demo_auth_method == \"http_basic\":\n                authenticated_partner_impl_override = (\n                    authenticated_partner_from_basic_auth_user\n                )\n            else:\n                authenticated_partner_impl_override = (\n                    api_key_based_authenticated_partner_impl\n                )\n            app.dependency_overrides[\n                authenticated_partner_impl\n            ] = authenticated_partner_impl_override\n        return app\n\n\nTo see how the dependency override mechanism works, you can take a look at the\ndemo app provided by the fastapi addon. If you choose the app 'demo' in the\nfastapi endpoint form view, you will see that the authentication method\nis configurable. You can also see that depending on the authentication method\nconfigured on your fastapi endpoint, the documentation will change.\n\n.. note::\n  At time of writing, the dependency override mechanism is not supported by\n  the fastapi documentation generator. A fix has been proposed and is waiting\n  to be merged. You can follow the progress of the fix on `github\n  <https://github.com/tiangolo/fastapi/pull/5452>`_\n\nManaging configuration parameters for your app\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAs we have seen in the previous section, you can add configuration fields\non the fastapi endpoint model to allow the user to configure your app (as for\nany odoo model you extend). When you need to access these configuration fields\nin your route handlers, you can use the **'odoo.addons.fastapi.dependencies.fastapi_endpoint'**\ndependency method to retrieve the 'fastapi.endpoint' record associated to the\ncurrent request.\n\n.. code-block:: python\n\n  from pydantic import BaseModel, Field\n  from odoo.addons.fastapi.dependencies import fastapi_endpoint\n\n  class EndpointAppInfo(BaseModel):\n    id: str\n    name: str\n    app: str\n    auth_method: str = Field(alias=\"demo_auth_method\")\n    root_path: str\n    model_config = ConfigDict(from_attributes=True)\n\n\n    @demo_api_router.get(\n        \"/endpoint_app_info\",\n        response_model=EndpointAppInfo,\n        dependencies=[Depends(authenticated_partner)],\n    )\n    async def endpoint_app_info(\n        endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],\n    ) -> EndpointAppInfo:\n        \"\"\"Returns the current endpoint configuration\"\"\"\n        # This method show you how to get access to current endpoint configuration\n        # It also show you how you can specify a dependency to force the security\n        # even if the method doesn't require the authenticated partner as parameter\n        return EndpointAppInfo.model_validate(endpoint)\n\nSome of the configuration fields of the fastapi endpoint could impact the way\nthe app is instantiated. For example, in the previous section, we have seen\nthat the authentication method configured on the 'fastapi.endpoint' record is\nused in order to provide the right implementation of the **'authenticated_partner'**\nwhen the app is instantiated. To ensure that the app is re-instantiated when\nan element of the configuration used in the instantiation of the app is\nmodified, you must override the **'_fastapi_app_fields'** method to add the\nname of the fields that impact the instantiation of the app into the returned\nlist.\n\n.. code-block:: python\n\n  class FastapiEndpoint(models.Model):\n\n      _inherit = \"fastapi.endpoint\"\n\n      app: str = fields.Selection(\n        selection_add=[(\"demo\", \"Demo Endpoint\")], ondelete={\"demo\": \"cascade\"}\n      )\n      demo_auth_method = fields.Selection(\n          selection=[(\"api_key\", \"Api Key\"), (\"http_basic\", \"HTTP Bacic\")],\n          string=\"Authenciation method\",\n      )\n\n      @api.model\n      def _fastapi_app_fields(self) -> List[str]:\n          fields = super()._fastapi_app_fields()\n          fields.append(\"demo_auth_method\")\n          return fields\n\nDealing with languages\n~~~~~~~~~~~~~~~~~~~~~~\n\nThe fastapi addon parses the Accept-Language header of the request to determine\nthe language to use. This parsing is done by respecting the `RFC 7231 specification\n<https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5>`_. That means that\nthe language is determined by the first language found in the header that is\nsupported by odoo (with care of the priority order). If no language is found in\nthe header, the odoo default language is used. This language is then used to\ninitialize the Odoo's environment context used by the route handlers. All this\nmakes the management of languages very easy. You don't have to worry about. This\nfeature is also documented by default into the generated openapi documentation\nof your app to instruct the api consumers how to request a specific language.\n\n\nHow to extend an existing app\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nWhen you develop a fastapi app, in a native python app it's not possible\nto extend an existing one. This limitation doesn't apply to the fastapi addon\nbecause the fastapi endpoint model is designed to be extended. However, the\nway to extend an existing app is not the same as the way to extend an odoo model.\n\nFirst of all, it's important to keep in mind that when you define a route, you\nare actually defining a contract between the client and the server. This\ncontract is defined by the route path, the method (GET, POST, PUT, DELETE,\netc.), the parameters and the response. If you want to extend an existing app,\nyou must ensure that the contract is not broken. Any change to the contract\nwill respect the `Liskov substitution principle\n<https://en.wikipedia.org/wiki/Liskov_substitution_principle>`_. This means\nthat the client should not be impacted by the change.\n\nWhat does it mean in practice? It means that you can't change the route path\nor the method of an existing route. You can't change the name of a parameter\nor the type of a response. You can't add a new parameter or a new response.\nYou can't remove a parameter or a response. If you want to change the contract,\nyou must create a new route.\n\nWhat can you change?\n\n* You can change the implementation of the route handler.\n* You can override the dependencies of the route handler.\n* You can add a new route handler.\n* You can extend the model used as parameter or as response of the route handler.\n\nLet's see how to do that.\n\nChanging the implementation of the route handler\n================================================\n\nLet's say that you want to change the implementation of the route handler\n**'/demo/echo'**. Since a route handler is just a python method, it could seems\na tedious task since we are not into a model method and therefore we can't\ntake advantage of the Odoo inheritance mechanism.\n\nHowever, the fastapi addon provides a way to do that. Thanks to the **'odoo_env'**\ndependency method, you can access the current odoo environment. With this\nenvironment, you can access the registry and therefore the model you want to\ndelegate the implementation to. If you want to change the implementation of\nthe route handler **'/demo/echo'**, the only thing you have to do is to\ninherit from the model where the implementation is defined and override the\nmethod **'echo'**.\n\n.. code-block:: python\n\n  from pydantic import BaseModel\n  from fastapi import Depends, APIRouter\n  from odoo import models\n  from odoo.addons.fastapi.dependencies import odoo_env\n\n  class FastapiEndpoint(models.Model):\n\n      _inherit = \"fastapi.endpoint\"\n\n      def _get_fastapi_routers(self) -> List[APIRouter]:\n          routers = super()._get_fastapi_routers()\n          routers.append(demo_api_router)\n          return routers\n\n  demo_api_router = APIRouter()\n\n  @demo_api_router.get(\n      \"/echo\",\n      response_model=EchoResponse,\n      dependencies=[Depends(odoo_env)],\n  )\n  async def echo(\n      message: str,\n      odoo_env: Annotated[Environment, Depends(odoo_env)],\n  ) -> EchoResponse:\n      \"\"\"Echo the message\"\"\"\n      return EchoResponse(message=odoo_env[\"demo.fastapi.endpoint\"].echo(message))\n\n  class EchoResponse(BaseModel):\n      message: str\n\n  class DemoEndpoint(models.AbstractModel):\n\n      _name = \"demo.fastapi.endpoint\"\n      _description = \"Demo Endpoint\"\n\n      def echo(self, message: str) -> str:\n          return message\n\n  class DemoEndpointInherit(models.AbstractModel):\n\n      _inherit = \"demo.fastapi.endpoint\"\n\n      def echo(self, message: str) -> str:\n          return f\"Hello {message}\"\n\n\n.. note::\n\n  It's a good programming practice to implement the business logic outside\n  the route handler. This way, you can easily test your business logic without\n  having to test the route handler. In the example above, the business logic\n  is implemented in the method **'echo'** of the model **'demo.fastapi.endpoint'**.\n  The route handler just delegate the implementation to this method.\n\n\nOverriding the dependencies of the route handler\n================================================\n\nAs you've previously seen, the dependency injection mechanism of fastapi is\nvery powerful. By designing your route handler to rely on dependencies with\na specific functional scope, you can easily change the implementation of the\ndependency without having to change the route handler. With such a design, you\ncan even define abstract dependencies that must be implemented by the concrete\napplication. This is the case of the **'authenticated_partner'** dependency in our\nprevious example. (you can find the implementation of this dependency in the\nfile **'odoo/addons/fastapi/dependencies.py'** and it's usage in the file\n**'odoo/addons/fastapi/models/fastapi_endpoint_demo.py'**)\n\nAdding a new route handler\n==========================\n\nLet's say that you want to add a new route handler **'/demo/echo2'**.\nYou could be tempted to add this new route handler in your new addons by\nimporting the router of the existing app and adding the new route handler to\nit.\n\n.. code-block:: python\n\n  from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router\n\n  @demo_api_router.get(\n      \"/echo2\",\n      response_model=EchoResponse,\n      dependencies=[Depends(odoo_env)],\n  )\n  async def echo2(\n      message: str,\n      odoo_env: Annotated[Environment, Depends(odoo_env)],\n  ) -> EchoResponse:\n      \"\"\"Echo the message\"\"\"\n      echo = odoo_env[\"demo.fastapi.endpoint\"].echo2(message)\n      return EchoResponse(message=f\"Echo2: {echo}\")\n\nThe problem with this approach is that you unconditionally add the new route\nhandler to the existing app even if the app is called for a different database\nwhere your new addon is not installed.\n\nThe solution is to define a new router and to add it to the list of routers\nreturned by the method **'_get_fastapi_routers'** of the model\n**'fastapi.endpoint'** you are inheriting from into your new addon.\n\n.. code-block:: python\n\n  class FastapiEndpoint(models.Model):\n\n      _inherit = \"fastapi.endpoint\"\n\n      def _get_fastapi_routers(self) -> List[APIRouter]:\n          routers = super()._get_fastapi_routers()\n          if self.app == \"demo\":\n              routers.append(additional_demo_api_router)\n          return routers\n\n  additional_demo_api_router = APIRouter()\n\n  @additional_demo_api_router.get(\n      \"/echo2\",\n      response_model=EchoResponse,\n      dependencies=[Depends(odoo_env)],\n  )\n  async def echo2(\n      message: str,\n      odoo_env: Annotated[Environment, Depends(odoo_env)],\n  ) -> EchoResponse:\n      \"\"\"Echo the message\"\"\"\n      echo = odoo_env[\"demo.fastapi.endpoint\"].echo2(message)\n      return EchoResponse(message=f\"Echo2: {echo}\")\n\n\nIn this way, the new router is added to the list of routers of your app only if\nthe app is called for a database where your new addon is installed.\n\nExtending the model used as parameter or as response of the route handler\n=========================================================================\n\nThe fastapi python library uses the pydantic library to define the models. By\ndefault, once a model is defined, it's not possible to extend it. However, a\ncompanion python library called\n`extendable_pydantic <https://pypi.org/project/extendable_pydantic/>`_ provides\na way to use inheritance with pydantic models to extend an existing model. If\nused alone, it's your responsibility to instruct this library the list of\nextensions to apply to a model and the order to apply them. This is not very\nconvenient. Fortunately, an dedicated odoo addon exists to make this process\ncomplete transparent. This addon is called\n`odoo-addon-extendable-fastapi <https://pypi.org/project/odoo-addon-extendable-fastapi/>`_.\n\nWhen you want to allow other addons to extend a pydantic model, you must\nfirst define the model as an extendable model by using a dedicated metaclass\n\n.. code-block:: python\n\n  from pydantic import BaseModel\n  from extendable_pydantic import ExtendableModelMeta\n\n  class Partner(BaseModel, metaclass=ExtendableModelMeta):\n    name = 0.1\n    model_config = ConfigDict(from_attributes=True)\n\nAs any other pydantic model, you can now use this model as parameter or as response\nof a route handler. You can also use all the features of models defined with\npydantic.\n\n.. code-block:: python\n\n  @demo_api_router.get(\n      \"/partner\",\n      response_model=Location,\n      dependencies=[Depends(authenticated_partner)],\n  )\n  async def partner(\n      partner: Annotated[ResPartner, Depends(authenticated_partner)],\n  ) -> Partner:\n      \"\"\"Return the location\"\"\"\n      return Partner.model_validate(partner)\n\n\nIf you need to add a new field into the model **'Partner'**, you can extend it\nin your new addon by defining a new model that inherits from the model **'Partner'**.\n\n.. code-block:: python\n\n  from typing import Optional\n  from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner\n\n  class PartnerExtended(Partner, extends=Partner):\n      email: Optional[str]\n\nIf your new addon is installed in a database, a call to the route handler\n**'/demo/partner'** will return a response with the new field **'email'** if a\nvalue is provided by the odoo record.\n\n.. code-block:: python\n\n  {\n    \"name\": \"John Doe\",\n    \"email\": \"jhon.doe@acsone.eu\"\n  }\n\nIf your new addon is not installed in a database, a call to the route handler\n**'/demo/partner'** will only return the name of the partner.\n\n.. code-block:: python\n\n  {\n    \"name\": \"John Doe\"\n  }\n\n.. note::\n\n  The liskov substitution principle has also to be respected. That means that\n  if you extend a model, you must add new required fields or you must provide\n  default values for the new optional fields.\n\nManaging security into the route handlers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBy default the route handlers are processed using the user configured on the\n**'fastapi.endpoint'** model instance. (default is the Public user).\nYou have seen previously how to define a dependency that will be used to enforce\nthe authentication of a partner. When a method depends on this dependency, the\n'authenticated_partner_id' key is added to the context of the partner environment.\n(If you don't need the partner as dependency but need to get an environment\nwith the authenticated user, you can use the dependency 'authenticated_partner_env' instead of\n'authenticated_partner'.)\n\nThe fastapi addon extends the 'ir.rule' model to add into the evaluation context\nof the security rules the key 'authenticated_partner_id' that contains the id\nof the authenticated partner.\n\nAs briefly introduced in a previous section, a good practice when you develop a\nfastapi app and you want to protect your data in an efficient and traceable way is to:\n\n* create a new user specific to the app but with any access rights.\n* create a security group specific to the app and add the user to this group. (This\n  group must implies the group 'AFastAPI Endpoint Runner' that give the\n  minimal access rights)\n* for each model you want to protect:\n\n  * add a 'ir.model.access' record for the model to allow read access to your model\n    and add the group to the record.\n  * create a new 'ir.rule' record for the model that restricts the access to the\n    records of the model to the authenticated partner by using the key\n    'authenticated_partner_id' in domain of the rule. (or to the user defined on\n    the 'fastapi.endpoint' model instance if the method is public)\n\n* add a dependency on the 'authenticated_partner' to your handlers when you need\n  to access the authenticated partner or ensure that the service is called by an\n  authenticated partner.\n\n.. code-block:: xml\n\n  <record\n        id=\"my_demo_app_user\"\n        model=\"res.users\"\n        context=\"{'no_reset_password': True, 'no_reset_password': True}\"\n    >\n    <field name=\"name\">My Demo Endpoint User</field>\n    <field name=\"login\">my_demo_app_user</field>\n    <field name=\"groups_id\" eval=\"[(6, 0, [])]\" />\n  </record>\n\n  <record id=\"my_demo_app_group\" model=\"res.groups\">\n    <field name=\"name\">My Demo Endpoint Group</field>\n    <field name=\"users\" eval=\"[(4, ref('my_demo_app_user'))]\" />\n    <field name=\"implied_ids\" eval=\"[(4, ref('group_fastapi_endpoint_runner'))]\" />\n  </record>\n\n  <!-- acl for the model 'sale.order' -->\n  <record id=\"sale_order_demo_app_access\" model=\"ir.model.access\">\n    <field name=\"name\">My Demo App: access to sale.order</field>\n    <field name=\"model_id\" ref=\"model_sale_order\"/>\n    <field name=\"group_id\" ref=\"my_demo_app_group\"/>\n    <field name=\"perm_read\" eval=\"True\"/>\n    <field name=\"perm_write\" eval=\"False\"/>\n    <field name=\"perm_create\" eval=\"False\"/>\n    <field name=\"perm_unlink\" eval=\"False\"/>\n  </record>\n\n  <!-- a record rule to allows the authenticated partner to access only its sale orders -->\n  <record id=\"demo_app_sale_order_rule\" model=\"ir.rule\">\n    <field name=\"name\">Sale Order Rule</field>\n    <field name=\"model_id\" ref=\"model_sale_order\"/>\n    <field name=\"domain_force\">[('partner_id', '=', authenticated_partner_id)]</field>\n    <field name=\"groups\" eval=\"[(4, ref('my_demo_app_group'))]\"/>\n  </record>\n\nHow to test your fastapi app\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThanks to the starlette test client, it's possible to test your fastapi app\nin a very simple way. With the test client, you can call your route handlers\nas if they were real http endpoints. The test client is available in the\n**'fastapi.testclient'** module.\n\nOnce again the dependency injection mechanism comes to the rescue by allowing\nyou to inject into the test client specific implementations of the dependencies\nnormally provided by the normal processing of the request by the fastapi app.\n(for example, you can inject a mock of the dependency 'authenticated_partner'\nto test the behavior of your route handlers when the partner is not authenticated,\nyou can also inject a mock for the odoo_env etc...)\n\nThe fastapi addon provides a base class for the test cases that you can use to\nwrite your tests. This base class is **'odoo.fastapi.tests.common.FastAPITransactionCase'**.\nThis class mainly provides the method **'_create_test_client'** that you can\nuse to create a test client for your fastapi app. This method encapsulates the\ncreation of the test client and the injection of the dependencies. It also\nensures that the odoo environment is make available into the context of the\nroute handlers. This method is designed to be used when you need to test your\napp or when you need to test a specific router (It's therefore easy to defines\ntests for routers in an addon that doesn't provide a fastapi endpoint).\n\nWith this base class, writing a test for a route handler is as simple as:\n\n.. code-block:: python\n\n  from odoo.fastapi.tests.common import FastAPITransactionCase\n\n  from odoo.addons.fastapi import dependencies\n  from odoo.addons.fastapi.routers import demo_router\n\n  class FastAPIDemoCase(FastAPITransactionCase):\n\n      @classmethod\n      def setUpClass(cls) -> None:\n          super().setUpClass()\n          cls.default_fastapi_running_user = cls.env.ref(\"fastapi.my_demo_app_user\")\n          cls.default_fastapi_authenticated_partner = cls.env[\"res.partner\"].create({\"name\": \"FastAPI Demo\"})\n\n      def test_hello_world(self) -> None:\n          with self._create_test_client(router=demo_router) as test_client:\n              response: Response = test_client.get(\"/demo/\")\n          self.assertEqual(response.status_code, status.HTTP_200_OK)\n          self.assertDictEqual(response.json(), {\"Hello\": \"World\"})\n\n\nIn the previous example, we created a test client for the demo_router. We could\nhave created a test client for the whole app by not specifying the router but\nthe app instead.\n\n.. code-block:: python\n\n  from odoo.fastapi.tests.common import FastAPITransactionCase\n\n  from odoo.addons.fastapi import dependencies\n  from odoo.addons.fastapi.routers import demo_router\n\n  class FastAPIDemoCase(FastAPITransactionCase):\n\n      @classmethod\n      def setUpClass(cls) -> None:\n          super().setUpClass()\n          cls.default_fastapi_running_user = cls.env.ref(\"fastapi.my_demo_app_user\")\n          cls.default_fastapi_authenticated_partner = cls.env[\"res.partner\"].create({\"name\": \"FastAPI Demo\"})\n\n      def test_hello_world(self) -> None:\n          demo_endpoint = self.env.ref(\"fastapi.fastapi_endpoint_demo\")\n          with self._create_test_client(app=demo_endpoint._get_app()) as test_client:\n              response: Response = test_client.get(f\"{demo_endpoint.root_path}/demo/\")\n          self.assertEqual(response.status_code, status.HTTP_200_OK)\n          self.assertDictEqual(response.json(), {\"Hello\": \"World\"})\n\n\nOverall considerations when you develop an fastapi app\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nDeveloping a fastapi app requires to follow some good practices to ensure that\nthe app is robust and easy to maintain. Here are some of them:\n\n* A route handler must be as simple as possible. It must not contain any\n  business logic. The business logic must be implemented into the service\n  layer. The route handler must only call the service layer and return the\n  result of the service layer. To ease extension on your business logic, your\n  service layer can be implemented as an odoo abstract model that can be\n  inherited by other addons.\n\n* A route handler should not expose the internal data structure and api of Odoo.\n  It should provide the api that is needed by the client. More widely, an app\n  provides a set of services that address a set of use cases specific to\n  a well defined functional domain. You must always keep in mind that your api\n  will remain the same for a long time even if you upgrade your odoo version\n  of modify your business logic.\n\n* A route handler is a transactional unit of work. When you design your api\n  you must ensure that the completeness of a use case is guaranteed by a single\n  transaction. If you need to perform several transactions to complete a use\n  case, you introduce a risk of inconsistency in your data or extra complexity\n  in your client code.\n\n* Properly handle the errors. The route handler must return a proper error\n  response when an error occurs. The error response must be consistent with\n  the rest of the api. The error response must be documented in the api\n  documentation. By default, the **'odoo-addon-fastapi'** module handles\n  the common exception types defined in the **'odoo.exceptions'** module\n  and returns a proper error response with the corresponding http status code.\n  An error in the route handler must always return an error response with a\n  http status code different from 200. The error response must contain a\n  human readable message that can be displayed to the user. The error response\n  can also contain a machine readable code that can be used by the client to\n  handle the error in a specific way.\n\n* When you design your json document through the pydantic models, you must\n  use the appropriate data types. For example, you must use the data type\n  **'datetime.date'** to represent a date and not a string. You must also\n  properly define the constraints on the fields. For example, if a field\n  is optional, you must use the data type **'typing.Optional'**.\n  `pydantic`_ provides everything you need to\n  properly define your json document.\n\n* Always use an appropriate pydantic model as request and/or response for\n  your route handler. Constraints on the fields of the pydantic model must\n  apply to the specific use case. For example, if your route handler is used\n  to create a sale order, the pydantic model must not contain the field\n  'id' because the id of the sale order will be generated by the route handler.\n  But if the id is required afterwords, the pydantic model for the response\n  must contain the field 'id' as required.\n\n* Uses descriptive property names in your json documents. For example, avoid the\n  use of documents providing a flat list of key value pairs.\n\n* Be consistent in the naming of your fields into your json documents. For example,\n  if you use 'id' to represent the id of a sale order, you must use 'id' to represent\n  the id of all the other objects.\n\n* Be consistent in the naming style of your fields. Always prefer underscore\n  to camel case.\n\n* Always use plural for the name of the fields that contain a list of items.\n  For example, if you have a field 'lines' that contains a list of sale order\n  lines, you must use 'lines' and not 'line'.\n\n* You can't expect that a client will provide you the identifier of a specific\n  record in odoo (for example the id of a carrier) if you don't provide a\n  specific route handler to retrieve the list of available records. Sometimes,\n  the client must share with odoo the identity of a specific record to be\n  able to perform an appropriate action specific to this record (for example,\n  the processing of a payment is different for each payment acquirer). In this\n  case, you must provide a specific attribute that allows both the client and\n  odoo to identify the record. The field 'provider' on a payment acquirer allows\n  you to identify a specific record in odoo. This kind of approach\n  allows both the client and odoo to identify the record without having to rely\n  on the id of the record. (This will ensure that the client will not break\n  if the id of the record is changed in odoo for example when tests are run\n  on an other database).\n\n* Always use the same name for the same kind of object. For example, if you\n  have a field 'lines' that contains a list of sale order lines, you must\n  use the same name for the same kind of object in all the other json documents.\n\n* Manage relations between objects in your json documents the same way.\n  By default, you should return the id of the related object in the json document.\n  But this is not always possible or convenient, so you can also return the\n  related object in the json document. The main advantage of returning the id\n  of the related object is that it allows you to avoid the `n+1 problem\n  <https://restfulapi.net/rest-api-n-1-problem/>`_ . The\n  main advantage of returning the related object in the json document is that\n  it allows you to avoid an extra call to retrieve the related object.\n  By keeping in mind the pros and cons of each approach, you can choose the\n  best one for your use case. Once it's done, you must be consistent in the\n  way you manage the relations of the same object.\n\n* It's not always a good idea to name your fields into your json documents\n  with the same name as the fields of the corresponding odoo model. For example,\n  in your document representing a sale order, you must not use the name 'order_line'\n  for the field that contains the list of sale order lines. The name 'order_line'\n  in addition to being confusing and not consistent with the best practices, is\n  not auto-descriptive. The name 'lines' is much better.\n\n* Keep a defensive programming approach. If you provide a route handler that\n  returns a list of records, you must ensure that the computation of the list\n  is not too long or will not drain your server resources. For example,\n  for search route handlers, you must ensure that the search is limited to\n  a reasonable number of records by default.\n\n* As a corollary of the previous point, a search handler must always use the\n  pagination mechanism with a reasonable default page size. The result list\n  must be enclosed in a json document that contains the count of records into\n  the system matching your search criteria and the list of records for the given\n  page and size.\n\n* Use plural for the name of a service. For example, if you provide a service\n  that allows you to manage the sale orders, you must use the name 'sale_orders'\n  and not 'sale_order'.\n\n\n\n* ... and many more.\n\nWe could write a book about the best practices to follow when you design your api\nbut we will stop here. This list is the result of our experience at `ACSONE SA/NV\n<https://acsone.eu>`_ and it evolves over time. It's a kind of rescue kit that we\nwould provide to a new developer that starts to design an api. This kit must\nbe accompanied with the reading of some useful resources link like the `REST Guidelines\n<https://www.belgif.be/specification/rest/api-guide/>`_. On a technical level,\nthe `fastapi  documentation <https://fastapi.tiangolo.com/>`_ provides a lot of\nuseful information as well, with a lot of examples. Last but not least, the\n`pydantic`_ documentation is also very useful.\n\nMiscellaneous\n~~~~~~~~~~~~~\n\nDevelopment of a search route handler\n=====================================\n\nThe **'odoo-addon-fastapi'** module provides 2 useful piece of code to help\nyou be consistent when writing a route handler for a search route.\n\n1. A dependency method to use to specify the pagination parameters in the same\n   way for all the search route handlers: **'odoo.addons.fastapi.paging'**.\n2. A PagedCollection pydantic model to use to return the result of a search route\n   handler enclosed in a json document that contains the count of records.\n\n.. code-block:: python\n\n    from typing import Annotated\n    from pydantic import BaseModel\n\n    from odoo.api import Environment\n    from odoo.addons.fastapi.dependencies import paging, authenticated_partner_env\n    from odoo.addons.fastapi.schemas import PagedCollection, Paging\n\n    class SaleOrder(BaseModel):\n        id: int\n        name: str\n        model_config = ConfigDict(from_attributes=True)\n\n\n    @router.get(\n        \"/sale_orders\",\n        response_model=PagedCollection[SaleOrder],\n        response_model_exclude_unset=True,\n    )\n    def get_sale_orders(\n        paging: Annotated[Paging, Depends(paging)],\n        env: Annotated[Environment, Depends(authenticated_partner_env)],\n    ) -> PagedCollection[SaleOrder]:\n        \"\"\"Get the list of sale orders.\"\"\"\n        count = env[\"sale.order\"].search_count([])\n        orders = env[\"sale.order\"].search([], limit=paging.limit, offset=paging.offset)\n        return PagedCollection[SaleOrder](\n            count=count,\n            items=[SaleOrder.model_validate(order) for order in orders],\n        )\n\n.. note::\n\n    The **'odoo.addons.fastapi.schemas.Paging'** and **'odoo.addons.fastapi.schemas.PagedCollection'**\n    pydantic models are not designed to be extended to not introduce a\n    dependency between the **'odoo-addon-fastapi'** module and the **'odoo-addon-extendable'**\n\n\nCustomization of the error handling\n===================================\n\nThe error handling a very important topic in the design of the fastapi integration\nwith odoo. It must ensure that the error messages are properly return to the client\nand that the transaction is properly roll backed. The **'fastapi'** module provides\na way to register custom error handlers. The **'odoo.addons.fastapi.error_handlers'**\nmodule provides the default error handlers that are registered by default when\na new instance of the **'FastAPI'** class is created. When an app is initialized in\n'fastapi.endpoint' model, the method `_get_app_exception_handlers` is called to\nget a dictionary of error handlers. This method is designed to be overridden\nin a custom module to provide custom error handlers. You can override the handler\nfor a specific exception class or you can add a new handler for a new exception\nor even replace all the handlers by your own handlers. Whatever you do, you must\nensure that the transaction is properly roll backed.\n\nSome could argue that the error handling can't be extended since the error handlers\nare global method not defined in an odoo model. Since the method providing the\nthe error handlers definitions is defined on the 'fastapi.endpoint' model, it's\nnot a problem at all, you just need to think another way to do it that by inheritance.\n\nA solution could be to develop you own error handler to be able to process the\nerror and chain the call to the default error handler.\n\n.. code-block:: python\n\n    class MyCustomErrorHandler():\n        def __init__(self, next_handler):\n            self.next_handler = next_handler\n\n        def __call__(self, request: Request, exc: Exception) -> JSONResponse:\n            # do something with the error\n            response = self.next_handler(request, exc)\n            # do something with the response\n            return response\n\n\nWith this solution, you can now register your custom error handler by overriding\nthe method `_get_app_exception_handlers` in your custom module.\n\n.. code-block:: python\n\n    class FastapiEndpoint(models.Model):\n        _inherit = \"fastapi.endpoint\"\n\n        def _get_app_exception_handlers(\n            self,\n        ) -> Dict[\n            Union[int, Type[Exception]],\n            Callable[[Request, Exception], Union[Response, Awaitable[Response]]],\n        ]:\n            handlers = super()._get_app_exception_handlers()\n            access_error_handler = handlers.get(odoo.exceptions.AccessError)\n            handlers[odoo.exceptions.AccessError] = MyCustomErrorHandler(access_error_handler)\n            return handlers\n\nIn the previous example, we extend the error handler for the 'AccessError' exception\nfor all the endpoints. You can do the same for a specific app by checking the\n'app' field of the 'fastapi.endpoint' record before registering your custom error\nhandler.\n\nFastAPI addons directory structure\n==================================\n\nWhen you develop a new addon to expose an api with fastapi, it's a good practice\nto follow the same directory structure and naming convention for the files\nrelated to the api. It will help you to easily find the files related to the api\nand it will help the other developers to understand your code.\n\nHere is the directory structure that we recommend. It's based on practices that\nare used in the python community when developing a fastapi app.\n\n.. code-block::\n\n  .\n  \u251c\u2500\u2500 x_api\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 data\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 ... .xml\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 demo\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 ... .xml\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 i18n\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 ... .po\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 models\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 __init__.py\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 fastapi_endpoint.py  # your app\n  \u2502\u00a0\u00a0 \u2502   \u2514\u2500\u2500 ... .py\n  \u2502\u00a0\u00a0 \u2514\u2500\u2500 routers\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 __init__.py\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 items.py\n  \u2502\u00a0\u00a0 \u2502   \u2514\u2500\u2500 ... .py\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 schemas | schemas.py\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 __init__.py\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 my_model.py  # pydantic model\n  \u2502\u00a0\u00a0 \u2502   \u2514\u2500\u2500 ... .py\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 security\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 ... .xml\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 views\n  \u2502\u00a0\u00a0 \u2502   \u251c\u2500\u2500 ... .xml\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 __manifest__.py\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 dependencies.py  # custom dependencies\n  \u2502\u00a0\u00a0 \u251c\u2500\u2500 error_handlers.py  # custom error handlers\n\n\n* The **'models'** directory contains the odoo models. When you define a new\n  app, as for the others addons, you will add your new model inheriting from\n  the **'fastapi.endpoint'** model in this directory.\n* The **'routers'** directory contains the fastapi routers. You will add your\n  new routers in this directory. Each route starting with the same prefix should\n  be grouped in the same file. For example, all the routes starting with\n  '/items' should be defined in the **'items.py'** file. The **'__init__.py'**\n  file in this directory is used to import all the routers defined in the\n  directory and create a global router that can be used in an app. For example,\n  in your **'items.py'** file, you will define a router like this:\n\n  .. code-block:: python\n\n    router = APIRouter(tags=[\"items\"])\n\n    router.get(\"/items\", response_model=List[Item])\n    def list_items():\n        pass\n\n  In the **'__init__.py'** file, you will import the router and add it to the global\n  router or your addon.\n\n  .. code-block:: python\n\n    from fastapi import APIRouter\n\n    from .items import router as items_router\n\n    router = APIRouter()\n    router.include_router(items_router)\n\n* The **'schemas.py'** will be used to define the pydantic models. For complex\n  APIs with a lot of models, it will be better to create a **'schemas'** directory\n  and split the models in different files.  The **'__init__.py'** file in this\n  directory will be used to import all the models defined in the directory.\n  For example, in your **'my_model.py'**\n  file, you will define a model like this:\n\n  .. code-block:: python\n\n    from pydantic import BaseModel\n\n    class MyModel(BaseModel):\n        name: str\n        description: str = None\n\n  In the **'__init__.py'** file, you will import the model's classes from the\n  files in the directory.\n\n  .. code-block:: python\n\n    from .my_model import MyModel\n\n  This will allow to always import the models from the schemas module whatever\n  the models are spread across different files or defined in the **'schemas.py'**\n  file.\n\n  .. code-block:: python\n\n    from x_api_addon.schemas import MyModel\n\n* The **'dependencies.py'** file contains the custom dependencies that you\n  will use in your routers. For example, you can define a dependency to\n  check the access rights of the user.\n* The **'error_handlers.py'** file contains the custom error handlers that you\n  will use in your routers. The **'odoo-addon-fastapi'** module provides the\n  default error handlers for the common odoo exceptions. Chance are that you\n  will not need to define your own error handlers. But if you need to do it,\n  you can define them in this file.\n\nWhat's next?\n~~~~~~~~~~~~\n\nThe **'odoo-addon-fastapi'** module is still in its early stage of development.\nIt will evolve over time to integrate your feedback and to provide the missing\nfeatures. It's now up to you to try it and to provide your feedback.\n\n.. _pydantic: https://docs.pydantic.dev/\n\nKnown issues / Roadmap\n======================\n\nThe `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Afastapi>`_\nand `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Afastapi>`_ can\nbe found on GitHub.\n\nThe **FastAPI** module provides an easy way to use WebSockets. Unfortunately, this\nsupport is not 'yet' available in the **Odoo** framework. The challenge is high\nbecause the integration of the fastapi is based on the use of a specific middleware\nthat convert the WSGI request consumed by odoo to a ASGI request. The question\nis to know if it is also possible to develop the same kind of bridge for the\nWebSockets and to stream large responses.\n\nChangelog\n=========\n\n16.0.1.2.6 (2024-02-20)\n~~~~~~~~~~~~~~~~~~~~~~~\n\n**Bugfixes**\n\n- Fix compatibility issues with the latest Odoo version\n\n  From https://github.com/odoo/odoo/commit/cb1d057dcab28cb0b0487244ba99231ee292502e\n  the original werkzeug HTTPRequest class has been wrapped in a new class to keep\n  under control the attributes developers use. This changes take care of this\n  new implementation but also keep compatibility with the old ones. (`#414 <https://github.com/OCA/rest-framework/issues/414>`_)\n\n\n16.0.1.2.5 (2024-01-17)\n~~~~~~~~~~~~~~~~~~~~~~~\n\n**Bugfixes**\n\n- Odoo has done an update and now, it checks domains of ir.rule on creation and modification.\n\n  The ir.rule 'Fastapi: Running user rule' uses a field (authenticate_partner_id) that comes from the context.\n  This field wasn't always set and this caused an error when Odoo checked the domain.\n  So now it is set to *False* by default. (`#410 <https://github.com/OCA/rest-framework/issues/410>`_)\n\n\n16.0.1.2.3 (2023-12-21)\n~~~~~~~~~~~~~~~~~~~~~~~\n\n**Bugfixes**\n\n- In case of exception in endpoint execution, close the database cursor after rollback.\n\n  This is to ensure that the *retrying* method in *service/model.py* does not try\n  to flush data to the database. (`#405 <https://github.com/OCA/rest-framework/issues/405>`_)\n\n\n16.0.1.2.2 (2023-12-12)\n~~~~~~~~~~~~~~~~~~~~~~~\n\n**Bugfixes**\n\n- When using the 'FastAPITransactionCase' class, allows to specify a specific\n  override of the 'authenticated_partner_impl' method into the list of\n  overrides to apply. Before this change, the 'authenticated_partner_impl'\n  override given in the 'overrides' parameter was always overridden in the\n  '_create_test_client' method of the 'FastAPITransactionCase' class. It's now\n  only overridden if the 'authenticated_partner_impl' method is not already\n  present in the list of overrides to apply and no specific partner is given.\n  If a specific partner is given at same time of an override for the\n  'authenticated_partner_impl' method, an error is raised. (`#396 <https://github.com/OCA/rest-framework/issues/396>`_)\n\n\n16.0.1.2.1 (2023-11-03)\n~~~~~~~~~~~~~~~~~~~~~~~\n\n**Bugfixes**\n\n- Fix a typo in the Field declaration of the 'count' attribute of the 'PagedCollection' schema.\n\n  Misspelt parameter was triggering a deprecation warning due to recent versions of Pydantic seeing it as an arbitrary parameter. (`#389 <https://github.com/OCA/rest-framework/issues/389>`_)\n\n\n16.0.1.2.0 (2023-10-13)\n~~~~~~~~~~~~~~~~~~~~~~~\n\n**Features**\n\n- The field *total* in the *PagedCollection* schema is replaced by the field *count*.\n  The field *total* is now deprecated and will be removed in the next major version.\n  This change is backward compatible. The json document returned will now\n  contain both fields *total* and *count* with the same value. In your python\n  code the field *total*, if used, will fill the field *count* with the same\n  value. You are encouraged to use the field *count* instead of *total* and adapt\n  your code accordingly. (`#380 <https://github.com/OCA/rest-framework/issues/380>`_)\n\nBug Tracker\n===========\n\nBugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/issues>`_.\nIn case of trouble, please check there if your issue has already been reported.\nIf you spotted it first, help us to smash it by providing a detailed and welcomed\n`feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20fastapi%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.\n\nDo not contact contributors directly about support or help with technical issues.\n\nCredits\n=======\n\nAuthors\n~~~~~~~\n\n* ACSONE SA/NV\n\nContributors\n~~~~~~~~~~~~\n\n* Laurent Mignon <laurent.mignon@acsone.eu>\n\nMaintainers\n~~~~~~~~~~~\n\nThis module is maintained by the OCA.\n\n.. image:: https://odoo-community.org/logo.png\n   :alt: Odoo Community Association\n   :target: https://odoo-community.org\n\nOCA, or the Odoo Community Association, is a nonprofit organization whose\nmission is to support the collaborative development of Odoo features and\npromote its widespread use.\n\n.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px\n    :target: https://github.com/lmignon\n    :alt: lmignon\n\nCurrent `maintainer <https://odoo-community.org/page/maintainer-role>`__:\n\n|maintainer-lmignon| \n\nThis module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/fastapi>`_ project on GitHub.\n\nYou are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.\n\n\n",
    "bugtrack_url": null,
    "license": "LGPL-3",
    "summary": "Odoo FastAPI endpoint",
    "version": "16.0.1.2.9",
    "project_urls": {
        "Homepage": "https://github.com/OCA/rest-framework"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "fff609135fea9de1a2ee934ebe9c107a6d2373a00cf2dbe840be7049c38dea7c",
                "md5": "9c054ea001f7bb8c8fafcb97bce9e83d",
                "sha256": "83bb82105d8119c121148353260591dc0694b3f677fe956975e9fec3e775d869"
            },
            "downloads": -1,
            "filename": "odoo_addon_fastapi-16.0.1.2.9-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "9c054ea001f7bb8c8fafcb97bce9e83d",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 178434,
            "upload_time": "2024-04-19T13:40:19",
            "upload_time_iso_8601": "2024-04-19T13:40:19.975806Z",
            "url": "https://files.pythonhosted.org/packages/ff/f6/09135fea9de1a2ee934ebe9c107a6d2373a00cf2dbe840be7049c38dea7c/odoo_addon_fastapi-16.0.1.2.9-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-04-19 13:40:19",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "OCA",
    "github_project": "rest-framework",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [
        {
            "name": "a2wsgi",
            "specs": []
        },
        {
            "name": "apispec",
            "specs": []
        },
        {
            "name": "apispec",
            "specs": [
                [
                    ">=",
                    "4.0.0"
                ]
            ]
        },
        {
            "name": "cerberus",
            "specs": []
        },
        {
            "name": "contextvars",
            "specs": []
        },
        {
            "name": "extendable-pydantic",
            "specs": []
        },
        {
            "name": "extendable-pydantic",
            "specs": [
                [
                    ">=",
                    "1.2.0"
                ]
            ]
        },
        {
            "name": "extendable",
            "specs": [
                [
                    ">=",
                    "0.0.4"
                ]
            ]
        },
        {
            "name": "fastapi",
            "specs": []
        },
        {
            "name": "graphene",
            "specs": []
        },
        {
            "name": "graphql_server",
            "specs": []
        },
        {
            "name": "jsondiff",
            "specs": []
        },
        {
            "name": "marshmallow",
            "specs": []
        },
        {
            "name": "marshmallow-objects",
            "specs": [
                [
                    ">=",
                    "2.0.0"
                ]
            ]
        },
        {
            "name": "parse-accept-language",
            "specs": []
        },
        {
            "name": "pydantic",
            "specs": []
        },
        {
            "name": "pydantic",
            "specs": [
                [
                    ">=",
                    "2.0.0"
                ]
            ]
        },
        {
            "name": "pyquerystring",
            "specs": []
        },
        {
            "name": "python-multipart",
            "specs": []
        },
        {
            "name": "typing-extensions",
            "specs": []
        },
        {
            "name": "ujson",
            "specs": []
        }
    ],
    "lcname": "odoo-addon-fastapi"
}
        
Elapsed time: 0.24587s