Odoo FastAPI - Odoo Apps Store
Odoo FastAPI - Odoo Apps Store
v=n9IKZ1fKYcw
Odoo FastAPI
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.
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.
Table of contents
• Usage
◦ What’s building an API with fastapi?
◦ Dealing with the odoo environment
◦ The dependency injection mechanism
◦ The authentication mechanism
◦ Managing configuration parameters for your app
◦ Dealing with languages
◦ How to extend an existing app
• Changing the implementation of the route handler
• Overriding the dependencies of the route handler
• Adding a new route handler
• Extending the model used as parameter or as response of the route handler
◦ Managing security into the route handlers
◦ How to test your fastapi app
◦ Overall considerations when you develop an fastapi app
◦ Miscellaneous
• Development of a search route handler
• Error handling
• FastAPI addons directory structure
◦ What’s next?
• Known issues / Roadmap
• Changelog
◦ 16.0.1.4.3 (2024-10-01)
◦ 16.0.1.4.1 (2024-07-08)
◦ 16.0.1.4.0 (2024-06-06)
◦ 16.0.1.2.6 (2024-02-20)
◦ 16.0.1.2.5 (2024-01-17)
◦ 16.0.1.2.3 (2023-12-21)
◦ 16.0.1.2.2 (2023-12-12)
◦ 16.0.1.2.1 (2023-11-03)
◦ 16.0.1.2.0 (2023-10-13)
• Bug Tracker
• Credits
◦ Authors
◦ Contributors
◦ Maintainers
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.
• 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.
• 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:
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:
class FastapiEndpoint(models.Model):
_inherit = "fastapi.endpoint"
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’
class FastapiEndpoint(models.Model):
_inherit = "fastapi.endpoint"
# 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.
class FastapiEndpoint(models.Model):
_inherit = "fastapi.endpoint"
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.
class FastapiEndpoint(models.Model):
_inherit = "fastapi.endpoint"
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.
<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:
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.
@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:
By default, the ‘odoo_env’ and ‘fastapi_endpoint’ dependencies are available without extra
work.
Note
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.
When you define a route handler, you can inject the ‘authenticated_partner’ dependency
as a parameter of your route handler.
@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.
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
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.
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.
class FastapiEndpoint(models.Model):
_inherit = "fastapi.endpoint"
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.
_inherit = "fastapi.endpoint"
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
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.
class FastapiEndpoint(models.Model):
_inherit = "fastapi.endpoint"
@api.model
def _fastapi_app_fields(self) -> List[str]:
fields = super()._fastapi_app_fields()
fields.append("demo_auth_method")
return fields
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. 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.
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’.
class FastapiEndpoint(models.Model):
_inherit = "fastapi.endpoint"
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"
class DemoEndpointInherit(models.AbstractModel):
_inherit = "demo.fastapi.endpoint"
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’)
@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.
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.
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
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.
@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’.
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.
{
"name": "John Doe",
"email": "[email protected]"
}
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.
{
"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.
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.
<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>
<!-- 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>
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:
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"})
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.
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"})
• 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 . 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 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. On a technical level, the fastapi documentation 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.
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
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.
.
├── 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:
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.
from fastapi import APIRouter
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:
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.
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.
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.
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.4.3 (2024-10-01)
Features
• ◦ A new parameter is now available on the endpoint model to let you disable the
creation and the store of session files used by Odoo for calls to your application
endpoint. This is usefull to prevent disk space consumption and IO operations if
your application doesn’t need to use this sessions files which are mainly used by
Odoo by to store the session info of logged in users. (#442)
16.0.1.4.1 (2024-07-08)
Bugfixes
• Fix issue with the retry of a POST request with a body content.
Prior to this fix the retry of a POST request with a body content would stuck in a loop
and never complete. This was due to the fact that the request input stream was not
reset after a failed attempt to process the request. (#440)
16.0.1.4.0 (2024-06-06)
Bugfixes
• This change is a complete rewrite of the way the transactions are managed when
integrating a fastapi application into Odoo.
In the previous implementation, specifics error handlers were put in place to catch
exception occurring in the handling of requests made to a fastapi application and to
rollback the transaction in case of error. This was done by registering specifics error
handlers methods to the fastapi application using the ‘add_exception_handler’ method
of the fastapi application. In this implementation, the transaction was rolled back in the
error handler method.
This approach was not working as expected for several reasons:
◦ The handling of the error at the fastapi level prevented the retry mechanism to be
triggered in case of a DB concurrency error. This is because the error was catch at
the fastapi level and never bubbled up to the early stage of the processing of the
request where the retry mechanism is implemented.
◦ The cleanup of the environment and the registry was not properly done in case of
error. In the ‘odoo.service.model.retrying’ method, you can see that the cleanup
process is different in case of error raised by the database and in case of error
raised by the application.
This change fix these issues by ensuring that errors are no more catch at the fastapi
level and bubble up the fastapi processing stack through the event loop required to
transform WSGI to ASGI. As result the transactional nature of the requests to the
fastapi applications is now properly managed by the Odoo framework. (#422)
16.0.1.2.6 (2024-02-20)
Bugfixes
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)
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)
16.0.1.2.2 (2023-12-12)
Bugfixes
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)
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)
Bug Tracker
Bugs are tracked on GitHub 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.
Do not contact contributors directly about support or help with technical issues.
Credits
Authors
• ACSONE SA/NV
Contributors
• Laurent Mignon <[email protected]>
Maintainers
This module is maintained by the OCA.
Current maintainer: