From 4eea8016c1c6623f9785dba3c7b38e73e0ab3d6c Mon Sep 17 00:00:00 2001 From: PicchiSeba Date: Wed, 11 Jun 2025 09:26:23 +0200 Subject: [PATCH] [IMP]fastapi: expose FastAPI docs only if field is set --- fastapi/README.rst | 12 +- fastapi/models/fastapi_endpoint.py | 8 +- fastapi/readme/USAGE.rst | 6 + fastapi/static/description/index.html | 275 +++++++++++++------------- fastapi/tests/test_fastapi.py | 42 ++++ fastapi/views/fastapi_endpoint.xml | 1 + 6 files changed, 199 insertions(+), 145 deletions(-) diff --git a/fastapi/README.rst b/fastapi/README.rst index 417ab2d2f..e6a5701f5 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ============ Odoo FastAPI ============ @@ -17,7 +13,7 @@ Odoo FastAPI .. |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/license-LGPL--3-blue.png +.. |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 @@ -251,6 +247,12 @@ Now, you can start your Odoo server, install your addon and create a new endpoin instance for your app. Once it's done click on the docs url to access the interactive documentation of your app. +**Note**: FastAPI automatically exposes three services to easily interact with +the APIs and their structure (Swagger, Redoc, OpenAPI). +When exposing your Odoo instance to the open internet you might want to disable +these services. To do so toggle the "Expose FastAPI docs" checkbox in the FastAPI +Endpoint form view. + 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 diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 25e3dc468..992f5098b 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -65,6 +65,7 @@ class FastapiEndpoint(models.Model): "unexpecteed disk space consumption.", default=True, ) + expose_doc_urls = fields.Boolean("Expose FastAPI docs", default=True) @api.depends("root_path") def _compute_root_path(self): @@ -150,7 +151,7 @@ def _handle_route_updates(self, vals): @api.model def _fastapi_app_fields(self) -> List[str]: """The list of fields requiring to refresh the fastapi app if modified""" - return [] + return ["expose_doc_urls"] def _make_routing_rule(self, options=None): """Generator of rule""" @@ -258,12 +259,15 @@ def _get_app_dependencies_overrides(self) -> Dict[Callable, Callable]: def _prepare_fastapi_app_params(self) -> Dict[str, Any]: """Return the params to pass to the Fast API app constructor""" - return { + to_return = { "title": self.name, "description": self.description, "middleware": self._get_fastapi_app_middlewares(), "dependencies": self._get_fastapi_app_dependencies(), } + if not self.expose_doc_urls: + to_return |= {"docs_url": None, "redoc_url": None, "openapi_url": None} + return to_return def _get_fastapi_routers(self) -> List[APIRouter]: """Return the api routers to use for the instance. diff --git a/fastapi/readme/USAGE.rst b/fastapi/readme/USAGE.rst index f03942a38..9069f6b3a 100644 --- a/fastapi/readme/USAGE.rst +++ b/fastapi/readme/USAGE.rst @@ -170,6 +170,12 @@ Now, you can start your Odoo server, install your addon and create a new endpoin instance for your app. Once it's done click on the docs url to access the interactive documentation of your app. +**Note**: FastAPI automatically exposes three services to easily interact with +the APIs and their structure (Swagger, Redoc, OpenAPI). +When exposing your Odoo instance to the open internet you might want to disable +these services. To do so toggle the "Expose FastAPI docs" checkbox in the FastAPI +Endpoint form view. + 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 diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 1acafecb0..ca6329172 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Odoo FastAPI -
+
+

Odoo FastAPI

- - -Odoo Community Association - -
-

Odoo FastAPI

-

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

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 @@ -458,9 +453,9 @@

Odoo FastAPI

-

Usage

+

Usage

-

What’s building an API with fastapi?

+

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.

@@ -510,9 +505,9 @@

What’s building an API with fas

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:

-from odoo import fields, models
+from odoo import fields, models
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -535,10 +530,10 @@ 

What’s building an API with fas

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’

-from fastapi import APIRouter
-from odoo import fields, models
+from fastapi import APIRouter
+from odoo import fields, models
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -552,10 +547,10 @@ 

What’s building an API with fas

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.

-from fastapi import APIRouter
-from odoo import api, fields, models
+from fastapi import APIRouter
+from odoo import api, fields, models
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -563,7 +558,7 @@ 

What’s building an API with fas selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} ) - def _get_fastapi_routers(self): + def _get_fastapi_routers(self): if self.app == "demo": return [demo_api_router] return super()._get_fastapi_routers() @@ -574,17 +569,17 @@

What’s building an API with fas

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

-from typing import Annotated
+from typing import Annotated
 
-from fastapi import APIRouter
-from pydantic import BaseModel
+from fastapi import APIRouter
+from pydantic import BaseModel
 
-from odoo import api, fields, models
-from odoo.api import Environment
+from odoo import api, fields, models
+from odoo.api import Environment
 
-from odoo.addons.fastapi.dependencies import odoo_env
+from odoo.addons.fastapi.dependencies import odoo_env
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -592,7 +587,7 @@ 

What’s building an API with fas selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} ) - def _get_fastapi_routers(self): + def _get_fastapi_routers(self): if self.app == "demo": return [demo_api_router] return super()._get_fastapi_routers() @@ -600,12 +595,12 @@

What’s building an API with fas # create a router demo_api_router = APIRouter() -class PartnerInfo(BaseModel): +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]: +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([]) @@ -614,6 +609,11 @@

What’s building an API with fas

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.

+

Note: FastAPI automatically exposes three services to easily interact with +the APIs and their structure (Swagger, Redoc, OpenAPI). +When exposing your Odoo instance to the open internet you might want to disable +these services. To do so toggle the “Expose FastAPI docs” checkbox in the FastAPI +Endpoint form view.

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 @@ -664,19 +664,19 @@

What’s building an API with fas

-

Dealing with the odoo environment

+

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.

-from typing import Annotated
+from typing import Annotated
 
-from odoo.api import Environment
-from odoo.addons.fastapi.dependencies import odoo_env
+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]:
+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([])
@@ -712,7 +712,7 @@ 

Dealing with the odoo environment

-

The authentication mechanism

+

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.

-from odoo.addons.base.models.res_partner import Partner
+from odoo.addons.base.models.res_partner import Partner
 
 
 @demo_api_router.get("/partners", response_model=list[PartnerInfo])
-def get_partners(
+def get_partners(
     env: Annotated[Environment, Depends(odoo_env)], partner: Annotated[Partner, Depends(authenticated_partner)]
 ) -> list[PartnerInfo]:
     return [
@@ -789,7 +789,7 @@ 

The authentication mechanism< ‘odoo.addons.fastapi.dependencies’ module and relies on functionalities provided by the ‘fastapi.security’ module.

-def authenticated_partner(
+def authenticated_partner(
     env: Annotated[Environment, Depends(odoo_env)],
     security: Annotated[HTTPBasicCredentials, Depends(HTTPBasic())],
 ) -> "res.partner":
@@ -827,9 +827,9 @@ 

The authentication mechanism< 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.

-from fastapi.security import APIKeyHeader
+from fastapi.security import APIKeyHeader
 
-def api_key_based_authenticated_partner_impl(
+def api_key_based_authenticated_partner_impl(
     api_key: Annotated[str, Depends(
         APIKeyHeader(
             name="api-key",
@@ -854,9 +854,9 @@ 

The authentication mechanism< can allows the user to select one of these authentication mechanisms by adding a selection field on the fastapi endpoint model.

-from odoo import fields, models
+from odoo import fields, models
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -879,8 +879,8 @@ 

The authentication mechanism< provide the right implementation of the ‘authenticated_partner’ dependency when the app is instantiated.

-from odoo.addons.fastapi.dependencies import authenticated_partner
-class FastapiEndpoint(models.Model):
+from odoo.addons.fastapi.dependencies import authenticated_partner
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -892,7 +892,7 @@ 

The authentication mechanism< string="Authenciation method", ) - def _get_app(self) -> FastAPI: + def _get_app(self) -> FastAPI: app = super()._get_app() if self.app == "demo": # Here we add the overrides to the authenticated_partner_impl method @@ -923,7 +923,7 @@

The authentication mechanism<

-

Managing configuration parameters for your app

+

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 @@ -931,10 +931,10 @@

Managing configuration parameters dependency method to retrieve the ‘fastapi.endpoint’ record associated to the current request.

-from pydantic import BaseModel, Field
-from odoo.addons.fastapi.dependencies import fastapi_endpoint
+from pydantic import BaseModel, Field
+from odoo.addons.fastapi.dependencies import fastapi_endpoint
 
-class EndpointAppInfo(BaseModel):
+class EndpointAppInfo(BaseModel):
   id: str
   name: str
   app: str
@@ -948,7 +948,7 @@ 

Managing configuration parameters response_model=EndpointAppInfo, dependencies=[Depends(authenticated_partner)], ) - async def endpoint_app_info( + async def endpoint_app_info( endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], ) -> EndpointAppInfo: """Returns the current endpoint configuration""" @@ -967,7 +967,7 @@

Managing configuration parameters name of the fields that impact the instantiation of the app into the returned list.

-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -980,14 +980,14 @@ 

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

-

Dealing with languages

+

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. That means that the language is determined by the first language found in the header that is @@ -999,7 +999,7 @@

Dealing with languages

of your app to instruct the api consumers how to request a specific language.

-

How to extend an existing app

+

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 @@ -1027,7 +1027,7 @@

How to extend an existing app

-

Changing the implementation of the route handler

+

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 @@ -1040,16 +1040,16 @@

Changing the implementation of th inherit from the model where the implementation is defined and override the method ‘echo’.

-from pydantic import BaseModel
-from fastapi import Depends, APIRouter
-from odoo import models
-from odoo.addons.fastapi.dependencies import odoo_env
+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):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
-    def _get_fastapi_routers(self) -> List[APIRouter]:
+    def _get_fastapi_routers(self) -> List[APIRouter]:
         routers = super()._get_fastapi_routers()
         routers.append(demo_api_router)
         return routers
@@ -1061,29 +1061,29 @@ 

Changing the implementation of th response_model=EchoResponse, dependencies=[Depends(odoo_env)], ) -async def echo( +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): +class EchoResponse(BaseModel): message: str -class DemoEndpoint(models.AbstractModel): +class DemoEndpoint(models.AbstractModel): _name = "demo.fastapi.endpoint" _description = "Demo Endpoint" - def echo(self, message: str) -> str: + def echo(self, message: str) -> str: return message -class DemoEndpointInherit(models.AbstractModel): +class DemoEndpointInherit(models.AbstractModel): _inherit = "demo.fastapi.endpoint" - def echo(self, message: str) -> str: + def echo(self, message: str) -> str: return f"Hello {message}"

-

Overriding the dependencies of the route handler

+

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 @@ -1108,20 +1108,20 @@

Overriding the dependencies of t ‘odoo/addons/fastapi/models/fastapi_endpoint_demo.py’)

-

Adding a new route handler

+

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.

-from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router
+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(
+async def echo2(
     message: str,
     odoo_env: Annotated[Environment, Depends(odoo_env)],
 ) -> EchoResponse:
@@ -1136,11 +1136,11 @@ 

Adding a new route handler‘_get_fastapi_routers’ of the model ‘fastapi.endpoint’ you are inheriting from into your new addon.

-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
-    def _get_fastapi_routers(self) -> List[APIRouter]:
+    def _get_fastapi_routers(self) -> List[APIRouter]:
         routers = super()._get_fastapi_routers()
         if self.app == "demo":
             routers.append(additional_demo_api_router)
@@ -1153,7 +1153,7 @@ 

Adding a new route handler response_model=EchoResponse, dependencies=[Depends(odoo_env)], ) -async def echo2( +async def echo2( message: str, odoo_env: Annotated[Environment, Depends(odoo_env)], ) -> EchoResponse: @@ -1165,7 +1165,7 @@

Adding a new route handler

-

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

+

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 @@ -1179,10 +1179,10 @@

Extending the model used as para

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

-from pydantic import BaseModel
-from extendable_pydantic import ExtendableModelMeta
+from pydantic import BaseModel
+from extendable_pydantic import ExtendableModelMeta
 
-class Partner(BaseModel, metaclass=ExtendableModelMeta):
+class Partner(BaseModel, metaclass=ExtendableModelMeta):
   name = 0.1
   model_config = ConfigDict(from_attributes=True)
 
@@ -1195,7 +1195,7 @@

Extending the model used as para response_model=Location, dependencies=[Depends(authenticated_partner)], ) -async def partner( +async def partner( partner: Annotated[ResPartner, Depends(authenticated_partner)], ) -> Partner: """Return the location""" @@ -1204,10 +1204,10 @@

Extending the model used as para

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’.

-from typing import Optional
-from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner
+from typing import Optional
+from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner
 
-class PartnerExtended(Partner, extends=Partner):
+class PartnerExtended(Partner, extends=Partner):
     email: Optional[str]
 

If your new addon is installed in a database, a call to the route handler @@ -1233,7 +1233,7 @@

Extending the model used as para default values for the new optional fields.

-

Managing security into the route handlers

+

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 @@ -1303,7 +1303,7 @@

Managing security into the route

-

How to test your fastapi app

+

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 @@ -1325,20 +1325,20 @@

How to test your fastapi app 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:

-from odoo.fastapi.tests.common import FastAPITransactionCase
+from odoo.fastapi.tests.common import FastAPITransactionCase
 
-from odoo.addons.fastapi import dependencies
-from odoo.addons.fastapi.routers import demo_router
+from odoo.addons.fastapi import dependencies
+from odoo.addons.fastapi.routers import demo_router
 
-class FastAPIDemoCase(FastAPITransactionCase):
+class FastAPIDemoCase(FastAPITransactionCase):
 
     @classmethod
-    def setUpClass(cls) -> None:
+    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:
+    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)
@@ -1348,20 +1348,20 @@ 

How to test your fastapi app have created a test client for the whole app by not specifying the router but the app instead.

-from odoo.fastapi.tests.common import FastAPITransactionCase
+from odoo.fastapi.tests.common import FastAPITransactionCase
 
-from odoo.addons.fastapi import dependencies
-from odoo.addons.fastapi.routers import demo_router
+from odoo.addons.fastapi import dependencies
+from odoo.addons.fastapi.routers import demo_router
 
-class FastAPIDemoCase(FastAPITransactionCase):
+class FastAPIDemoCase(FastAPITransactionCase):
 
     @classmethod
-    def setUpClass(cls) -> None:
+    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:
+    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/")
@@ -1370,7 +1370,7 @@ 

How to test your fastapi app

-

Overall considerations when you develop an fastapi app

+

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:

-

Miscellaneous

+

Miscellaneous

-

Development of a search route handler

+

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.

    @@ -1496,14 +1496,14 @@

    Development of a search route ha handler enclosed in a json document that contains the count of records.

-from typing import Annotated
-from pydantic import BaseModel
+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
+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):
+class SaleOrder(BaseModel):
     id: int
     name: str
     model_config = ConfigDict(from_attributes=True)
@@ -1514,7 +1514,7 @@ 

Development of a search route ha response_model=PagedCollection[SaleOrder], response_model_exclude_unset=True, ) -def get_sale_orders( +def get_sale_orders( paging: Annotated[Paging, Depends(paging)], env: Annotated[Environment, Depends(authenticated_partner_env)], ) -> PagedCollection[SaleOrder]: @@ -1534,7 +1534,7 @@

Development of a search route ha

-

Error handling

+

Error handling

The error handling is a very important topic in the design of the fastapi integration with odoo. By default, when instantiating the fastapi app, the fastapi library declare a default exception handler that will catch any exception raised by the @@ -1552,7 +1552,7 @@

Error handling

add a custom exception handler in your app, it will be ignored.

-

FastAPI addons directory structure

+

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 @@ -1605,15 +1605,15 @@

FastAPI addons directory structu router = APIRouter(tags=["items"]) router.get("/items", response_model=List[Item]) -def list_items(): +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
+from fastapi import APIRouter
 
-from .items import router as items_router
+from .items import router as items_router
 
 router = APIRouter()
 router.include_router(items_router)
@@ -1626,22 +1626,22 @@ 

FastAPI addons directory structu For example, in your ‘my_model.py’ file, you will define a model like this:

-from pydantic import BaseModel
+from pydantic import BaseModel
 
-class MyModel(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
+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
+from x_api_addon.schemas import MyModel
 
  • The ‘dependencies.py’ file contains the custom dependencies that you @@ -1656,14 +1656,14 @@

    FastAPI addons directory structu

  • -

    What’s next?

    +

    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.

    -

    Known issues / Roadmap

    +

    Known issues / Roadmap

    The roadmap and known issues can be found on GitHub.

    @@ -1675,9 +1675,9 @@

    Known issues / Roadmap

    WebSockets and to stream large responses.

    -

    Changelog

    +

    Changelog

    -

    16.0.1.4.1 (2024-07-08)

    +

    16.0.1.4.1 (2024-07-08)

    Bugfixes

    -

    16.0.1.4.0 (2024-06-06)

    +

    16.0.1.4.0 (2024-06-06)

    Bugfixes

    • This change is a complete rewrite of the way the transactions are managed when @@ -1729,7 +1729,7 @@

      16.0.1.4.0 (2024-06-06)

    -

    16.0.1.2.6 (2024-02-20)

    +

    16.0.1.2.6 (2024-02-20)

    Bugfixes

    -

    16.0.1.2.5 (2024-01-17)

    +

    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.

      @@ -1752,7 +1752,7 @@

      16.0.1.2.5 (2024-01-17)

    -

    16.0.1.2.3 (2023-12-21)

    +

    16.0.1.2.3 (2023-12-21)

    Bugfixes

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

      @@ -1762,7 +1762,7 @@

      16.0.1.2.3 (2023-12-21)

    -

    16.0.1.2.2 (2023-12-12)

    +

    16.0.1.2.2 (2023-12-12)

    Bugfixes

    • When using the ‘FastAPITransactionCase’ class, allows to specify a specific @@ -1777,7 +1777,7 @@

      16.0.1.2.2 (2023-12-12)

    -

    16.0.1.2.1 (2023-11-03)

    +

    16.0.1.2.1 (2023-11-03)

    Bugfixes

    • Fix a typo in the Field declaration of the ‘count’ attribute of the ‘PagedCollection’ schema.

      @@ -1786,7 +1786,7 @@

      16.0.1.2.1 (2023-11-03)

    -

    16.0.1.2.0 (2023-10-13)

    +

    16.0.1.2.0 (2023-10-13)

    Features

    • The field total in the PagedCollection schema is replaced by the field count. @@ -1800,7 +1800,7 @@

      16.0.1.2.0 (2023-10-13)

    -

    Bug Tracker

    +

    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 @@ -1808,21 +1808,21 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • ACSONE SA/NV
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -1837,6 +1837,5 @@

    Maintainers

    - diff --git a/fastapi/tests/test_fastapi.py b/fastapi/tests/test_fastapi.py index 37b11a961..d1f1afc58 100644 --- a/fastapi/tests/test_fastapi.py +++ b/fastapi/tests/test_fastapi.py @@ -4,6 +4,7 @@ import os import unittest from contextlib import contextmanager +from pathlib import Path from odoo import sql_db from odoo.tests.common import HttpCase @@ -41,6 +42,17 @@ def _assert_expected_lang(self, accept_language, expected_lang): self.assertEqual(response.status_code, 200) self.assertEqual(response.content, expected_lang) + def endpoint_path(self, endpoint, path): + """Properly contatenate and endpoint's root_path and another path""" + if path[0] == "/": + # concatenating two paths where the second one starts with a '/' makes the + # resulting path exactly the same as this second path + # eg. Path("/root") / Path("/endpoint") -> PosixPath("/endpoint") + # We want to avoid that, we should get a result like + # -> PosixPath("/root/endpoint") + path = path[1:] + return str(Path(endpoint.root_path) / path) + def test_call(self): route = "/fastapi_demo/demo/" response = self.url_open(route) @@ -75,6 +87,36 @@ def test_retrying_post(self): self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"retries": nbr_retries, "file": "test"}) + def test_expose_docs(self): + self.assertNotIn( + "docs_url", self.fastapi_demo_app._prepare_fastapi_app_params().keys() + ) + response = self.url_open( + self.endpoint_path(self.fastapi_demo_app, "/docs"), timeout=20 + ) + self.assertEqual(response.status_code, 200) + + unexposed_endpoint = self.env["fastapi.endpoint"].create( + { + "name": "Test Endpoint - non exposed", + "root_path": "/test-endpoint/", + "app": "demo", + "demo_auth_method": "api_key", + "expose_doc_urls": False, + } + ) + self.assertIn( + "docs_url", unexposed_endpoint._prepare_fastapi_app_params().keys() + ) + self.assertEqual( + unexposed_endpoint._prepare_fastapi_app_params().get("docs_url"), None + ) + unexposed_endpoint._handle_registry_sync() + response = self.url_open( + self.endpoint_path(unexposed_endpoint, "/docs"), timeout=20 + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + @mute_logger("odoo.http") def assert_exception_processed( self, diff --git a/fastapi/views/fastapi_endpoint.xml b/fastapi/views/fastapi_endpoint.xml index 6a6ede3fb..a2a8cc2b9 100644 --- a/fastapi/views/fastapi_endpoint.xml +++ b/fastapi/views/fastapi_endpoint.xml @@ -49,6 +49,7 @@ +