Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,46 @@
# Odoo FastAPI
Odoo addon: fastapi
## Installation
```bash
pip install odoo-bringout-oca-rest-framework-fastapi
```
## Dependencies
This addon depends on:
- endpoint_route_handler
## Manifest Information
- **Name**: Odoo FastAPI
- **Version**: 16.0.1.7.0
- **Category**: N/A
- **License**: LGPL-3
- **Installable**: False
## Source
Based on [OCA/rest-framework](https://github.com/OCA/rest-framework) branch 16.0, addon `fastapi`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Fastapi Module - fastapi
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for fastapi. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- endpoint_route_handler

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon fastapi or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-rest-framework-fastapi"
# or
uv pip install odoo-bringout-oca-rest-framework-fastapi"
```

View file

@ -0,0 +1,16 @@
# Models
Detected core models and extensions in fastapi.
```mermaid
classDiagram
class fastapi_endpoint
class endpoint_route_sync_mixin
class fastapi_endpoint
class ir_rule
class res_lang
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: fastapi. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon fastapi
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,73 @@
# Security
Access control and security definitions in fastapi.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[bosnian_translations.json](../bosnian_translations.json)**
- 50 model access rules
- **[bosnian_translations_output.json](../bosnian_translations_output.json)**
- 444 model access rules
- **[CHANGELOG.md](../CHANGELOG.md)**
- 132 model access rules
- **[doc](../doc)**
- **[docker](../docker)**
- **[input](../input)**
- **[nix](../nix)**
- **[odoo.conf](../odoo.conf)**
- 58 model access rules
- **[odoo_packages_bez_l10n.txt](../odoo_packages_bez_l10n.txt)**
- 1947 model access rules
- **[odoo_packages_bringout.txt](../odoo_packages_bringout.txt)**
- 1947 model access rules
- **[odoo_packages.txt](../odoo_packages.txt)**
- 2085 model access rules
- **[output](../output)**
- **[packages](../packages)**
- **[PACKAGES.md](../PACKAGES.md)**
- 298 model access rules
- **[README.md](../README.md)**
- 338 model access rules
- **[scripts](../scripts)**
- **[temp](../temp)**
- **[TRANSLATION_BS_SUMMARY.md](../TRANSLATION_BS_SUMMARY.md)**
- 146 model access rules
## Record Rules
Row-level security rules defined in:
- **[ir_rule+acl.xml](../fastapi/security/ir_rule+acl.xml)**
## Security Groups & Configuration
Security groups and permissions defined in:
- **[fastapi_endpoint.xml](../fastapi/security/fastapi_endpoint.xml)**
- **[ir_rule+acl.xml](../fastapi/security/ir_rule+acl.xml)**
- **[res_groups.xml](../fastapi/security/res_groups.xml)**
- 3 security groups defined
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[fastapi_endpoint.xml](../fastapi/security/fastapi_endpoint.xml)**
- Security groups, categories, and XML-based rules
- **[ir_rule+acl.xml](../fastapi/security/ir_rule+acl.xml)**
- Security groups, categories, and XML-based rules
- **[res_groups.xml](../fastapi/security/res_groups.xml)**
- Security groups, categories, and XML-based rules
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon fastapi
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
from . import models
from . import fastapi_dispatcher
from . import error_handlers

View file

@ -0,0 +1,33 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
{
"name": "Odoo FastAPI",
"summary": """
Odoo FastAPI endpoint""",
"version": "16.0.1.7.0",
"license": "LGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"maintainers": ["lmignon"],
"website": "https://github.com/OCA/rest-framework",
"depends": ["endpoint_route_handler"],
"data": [
"security/res_groups.xml",
"security/fastapi_endpoint.xml",
"security/ir_rule+acl.xml",
"views/fastapi_menu.xml",
"views/fastapi_endpoint.xml",
"views/fastapi_endpoint_demo.xml",
],
"demo": ["demo/fastapi_endpoint_demo.xml"],
"external_dependencies": {
"python": [
"fastapi>=0.110.0",
"python-multipart",
"ujson",
"a2wsgi>=1.10.6",
"parse-accept-language",
]
},
"development_status": "Beta",
}

View file

@ -0,0 +1,10 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
# define context vars to hold the odoo env
from contextvars import ContextVar
from odoo.api import Environment
odoo_env_ctx: ContextVar[Environment] = ContextVar("odoo_env_ctx")

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 ACSONE SA/NV
License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -->
<odoo>
<!-- This is the user that will be used to run the demo app -->
<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>
<!-- This is the group that will be used to run the demo app
This group will only depend on the "group_fastapi_endpoint_runner" group
that provides the minimal access rights to retrieve the user running the
endpoint handlers and performs authentication.
-->
<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>
<!-- This is the endpoint that will be used to run the demo app
This endpoint will be registered on the "/fastapi_demo" path
-->
<record model="fastapi.endpoint" id="fastapi_endpoint_demo">
<field name="name">Fastapi Demo Endpoint</field>
<field
name="description"
><![CDATA[
# A Dummy FastApi Demo
This demo endpoint has been created by inhering from "fastapi.endpoint", registering
a new app into the app selection field and implementing the `_get_fastapi_routers`
methods. See documentation to learn more about how to create a new app.
]]></field>
<field name="app">demo</field>
<field name="root_path">/fastapi_demo</field>
<field name="demo_auth_method">http_basic</field>
<field name="user_id" ref="my_demo_app_user" />
</record>
<record id="fastapi_endpoint_multislash_demo" model="fastapi.endpoint">
<field name="name">Fastapi Multi-Slash Demo Endpoint</field>
<field name="description">
Like the other demo endpoint but with multi-slash
</field>
<field name="app">demo</field>
<field name="root_path">/fastapi/demo-multi</field>
<field name="demo_auth_method">http_basic</field>
<field name="user_id" ref="my_demo_app_user" />
</record>
</odoo>

View file

@ -0,0 +1,171 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
from typing import TYPE_CHECKING, Annotated
from odoo.api import Environment
from odoo.exceptions import AccessDenied
from odoo.addons.base.models.res_partner import Partner
from odoo.addons.base.models.res_users import Users
from fastapi import Depends, Header, HTTPException, Query, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from .context import odoo_env_ctx
from .schemas import Paging
if TYPE_CHECKING:
from .models.fastapi_endpoint import FastapiEndpoint
def company_id() -> int | None:
"""This method may be overriden by the FastAPI app to set the allowed company
in the Odoo env of the endpoint. By default, the company defined on the
endpoint record is used.
"""
return None
def odoo_env(company_id: Annotated[int | None, Depends(company_id)]) -> Environment:
env = odoo_env_ctx.get()
if company_id is not None:
env = env(context=dict(env.context, allowed_company_ids=[company_id]))
yield env
def authenticated_partner_impl() -> Partner:
"""This method has to be overriden when you create your fastapi app
to declare the way your partner will be provided. In some case, this
partner will come from the authentication mechanism (ex jwt token) in other cases
it could comme from a lookup on an email received into an HTTP header ...
See the fastapi_endpoint_demo for an example"""
def optionally_authenticated_partner_impl() -> Partner | None:
"""This method has to be overriden when you create your fastapi app
and you need to get an optional authenticated partner into your endpoint.
"""
def authenticated_partner_env(
partner: Annotated[Partner, Depends(authenticated_partner_impl)]
) -> Environment:
"""Return an environment with the authenticated partner id in the context"""
return partner.with_context(authenticated_partner_id=partner.id).env
def optionally_authenticated_partner_env(
partner: Annotated[Partner | None, Depends(optionally_authenticated_partner_impl)],
env: Annotated[Environment, Depends(odoo_env)],
) -> Environment:
"""Return an environment with the authenticated partner id in the context if
the partner is not None
"""
if partner:
return partner.with_context(authenticated_partner_id=partner.id).env
return env
def authenticated_partner(
partner: Annotated[Partner, Depends(authenticated_partner_impl)],
partner_env: Annotated[Environment, Depends(authenticated_partner_env)],
) -> Partner:
"""If you need to get access to the authenticated partner into your
endpoint, you can add a dependency into the endpoint definition on this
method.
This method is a safe way to declare a dependency without requiring a
specific implementation. It depends on `authenticated_partner_impl`. The
concrete implementation of authenticated_partner_impl has to be provided
when the FastAPI app is created.
This method return a partner into the authenticated_partner_env
"""
return partner_env["res.partner"].browse(partner.id)
def optionally_authenticated_partner(
partner: Annotated[Partner | None, Depends(optionally_authenticated_partner_impl)],
partner_env: Annotated[Environment, Depends(optionally_authenticated_partner_env)],
) -> Partner | None:
"""If you need to get access to the authenticated partner if the call is
authenticated, you can add a dependency into the endpoint definition on this
method.
This method defer from authenticated_partner by the fact that it returns
None if the partner is not authenticated .
"""
if partner:
return partner_env["res.partner"].browse(partner.id)
return None
def paging(
page: Annotated[int, Query(ge=1)] = 1, page_size: Annotated[int, Query(ge=1)] = 80
) -> Paging:
"""Return a Paging object from the page and page_size parameters"""
return Paging(limit=page_size, offset=(page - 1) * page_size)
def basic_auth_user(
credential: Annotated[HTTPBasicCredentials, Depends(HTTPBasic())],
env: Annotated[Environment, Depends(odoo_env)],
) -> Users:
username = credential.username
password = credential.password
try:
uid = (
env["res.users"]
.sudo()
.authenticate(
db=env.cr.dbname, login=username, password=password, user_agent_env=None
)
)
return env["res.users"].browse(uid)
except AccessDenied as ad:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
) from ad
def authenticated_partner_from_basic_auth_user(
user: Annotated[Users, Depends(basic_auth_user)],
env: Annotated[Environment, Depends(odoo_env)],
) -> Partner:
return env["res.partner"].browse(user.sudo().partner_id.id)
def fastapi_endpoint_id() -> int:
"""This method is overriden by the FastAPI app 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)
def accept_language(
accept_language: Annotated[
str | None,
Header(
alias="Accept-Language",
description="The Accept-Language header is used to specify the language "
"of the content to be returned. If a language is not available, the "
"server will return the content in the default language.",
),
] = None,
) -> str:
"""This dependency is used at application level to document the way the language
to use for the response is specified. The header is processed outside of the
fastapi app to initialize the odoo environment with the right language.
"""
return accept_language

View file

@ -0,0 +1,12 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import warnings
warnings.warn(
"The 'depends' package is deprecated. Please use 'dependencies' instead.",
DeprecationWarning,
stacklevel=2,
)
from .dependencies import * # noqa: F403, F401, E402

View file

@ -0,0 +1,78 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
from typing import Tuple
from starlette import status
from starlette.exceptions import HTTPException, WebSocketException
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.exceptions import ExceptionMiddleware
from starlette.responses import JSONResponse
from starlette.websockets import WebSocket
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
from odoo.exceptions import AccessDenied, AccessError, MissingError, UserError
from fastapi import Request
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.utils import is_body_allowed_for_status_code
def convert_exception_to_status_body(exc: Exception) -> Tuple[int, dict]:
body = {}
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
details = "Internal Server Error"
if isinstance(exc, WerkzeugHTTPException):
status_code = exc.code
details = exc.description
elif isinstance(exc, HTTPException):
status_code = exc.status_code
details = exc.detail
elif isinstance(exc, RequestValidationError):
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
details = jsonable_encoder(exc.errors())
elif isinstance(exc, WebSocketRequestValidationError):
status_code = status.WS_1008_POLICY_VIOLATION
details = jsonable_encoder(exc.errors())
elif isinstance(exc, (AccessDenied, AccessError)):
status_code = status.HTTP_403_FORBIDDEN
details = "AccessError"
elif isinstance(exc, MissingError):
status_code = status.HTTP_404_NOT_FOUND
details = "MissingError"
elif isinstance(exc, UserError):
status_code = status.HTTP_400_BAD_REQUEST
details = exc.args[0]
if is_body_allowed_for_status_code(status_code):
# use the same format as in
# fastapi.exception_handlers.http_exception_handler
body = {"detail": details}
return status_code, body
# we need to monkey patch the ServerErrorMiddleware and ExceptionMiddleware classes
# to ensure that all the exceptions that are handled by these specific
# middlewares are let to bubble up to the retrying mechanism and the
# dispatcher error handler to ensure that appropriate action are taken
# regarding the transaction, environment, and registry. These middlewares
# are added by default by FastAPI when creating an application and it's not
# possible to remove them. So we need to monkey patch them.
def pass_through_exception_handler(
self, request: Request, exc: Exception
) -> JSONResponse:
raise exc
def pass_through_websocket_exception_handler(
self, websocket: WebSocket, exc: WebSocketException
) -> None:
raise exc
ServerErrorMiddleware.error_response = pass_through_exception_handler
ExceptionMiddleware.http_exception = pass_through_exception_handler
ExceptionMiddleware.websocket_exception = pass_through_websocket_exception_handler

View file

@ -0,0 +1,124 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
from contextlib import contextmanager
from io import BytesIO
from odoo.http import Dispatcher, request
from .context import odoo_env_ctx
from .error_handlers import convert_exception_to_status_body
from .pools import fastapi_app_pool
class FastApiDispatcher(Dispatcher):
routing_type = "fastapi"
def __init__(self, request):
super().__init__(request)
# Store exception to later raise it in the dispatch method if needed
self.inner_exception = None
@classmethod
def is_compatible_with(cls, request):
return True
def dispatch(self, endpoint, args):
# don't parse the httprequest let starlette parse the stream
self.request.params = {} # dict(self.request.get_http_params(), **args)
environ = self._get_environ()
path = environ["PATH_INFO"]
# TODO store the env into contextvar to be used by the odoo_env
# depends method
with fastapi_app_pool.get_app(env=request.env, root_path=path) as app:
uid = request.env["fastapi.endpoint"].sudo().get_uid(path)
data = BytesIO()
with self._manage_odoo_env(uid):
for r in app(environ, self._make_response):
data.write(r)
if self.inner_exception:
raise self.inner_exception
return self.request.make_response(
data.getvalue(), headers=self.headers, status=self.status
)
def handle_error(self, exc):
headers = getattr(exc, "headers", None)
status_code, body = convert_exception_to_status_body(exc)
return self.request.make_json_response(
body, status=status_code, headers=headers
)
def _make_response(self, status_mapping, headers_tuple, content):
self.status = status_mapping[:3]
self.headers = headers_tuple
self.inner_exception = None
# in case of exception, the method asgi_done_callback of the
# ASGIResponder will trigger an "a2wsgi.error" event with the exception
# instance stored in a tuple with the type of the exception and the traceback.
# The event loop will then be notified and then call the `error_response`
# method of the ASGIResponder. This method will then call the
# `_make_response` method provided as callback to the app with the tuple
# of the exception as content. In this case, we store the exception
# instance in the `inner_exception` attribute to be able to raise it
# in the `dispatch` method.
if (
isinstance(content, tuple)
and len(content) == 3
and isinstance(content[1], Exception)
):
self.inner_exception = content[1]
def _get_environ(self):
try:
# normal case after
# https://github.com/odoo/odoo/commit/cb1d057dcab28cb0b0487244ba99231ee292502e
httprequest = self.request.httprequest._HTTPRequest__wrapped
except AttributeError:
# fallback for older odoo versions
# The try except is the most efficient way to handle this
# as we expect that most of the time the attribute will be there
# and this code will no more be executed if it runs on an up to
# date odoo version. (EAFP: Easier to Ask for Forgiveness than Permission)
httprequest = self.request.httprequest
environ = httprequest.environ
stream = httprequest._get_stream_for_parsing()
# Check if the stream supports seeking
if hasattr(stream, "seekable") and stream.seekable():
# Reset the stream to the beginning to ensure it can be consumed
# again by the application in case of a retry mechanism
stream.seek(0)
else:
# If the stream does not support seeking, we need wrap it
# in a BytesIO object. This way we can seek back to the beginning
# of the stream to read the data again if needed.
if not hasattr(httprequest, "_cached_stream"):
httprequest._cached_stream = BytesIO(stream.read())
stream = httprequest._cached_stream
stream.seek(0)
environ["wsgi.input"] = stream
return environ
@contextmanager
def _manage_odoo_env(self, uid=None):
env = request.env
accept_language = request.httprequest.headers.get("Accept-language")
context = env.context
if accept_language:
lang = (
env["res.lang"].sudo()._get_lang_from_accept_language(accept_language)
)
if lang:
env = env(context=dict(context, lang=lang))
if uid:
env = env(user=uid)
token = odoo_env_ctx.set(env)
try:
yield
# Flush here to ensure all pending computations are being executed with
# authenticated fastapi user before exiting this context manager, as it
# would otherwise be done using the public user on the commit of the DB
# cursor, what could potentially lead to inconsistencies or AccessError.
env.flush_all()
finally:
odoo_env_ctx.reset(token)

View file

@ -0,0 +1,251 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fastapi
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__description
msgid "A short description of the API. It can use Markdown"
msgstr "Kratki opis API-ja. Može koristiti Markdown"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__active
msgid "Active"
msgstr "Aktivan"
#. module: fastapi
#: model:res.groups,name:fastapi.group_fastapi_manager
msgid "Administrator"
msgstr "Administrator"
#. module: fastapi
#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__api_key
msgid "Api Key"
msgstr "API ključ"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__app
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view
msgid "App"
msgstr "Aplikacija"
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view
msgid "Archived"
msgstr "Arhivirano"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__demo_auth_method
msgid "Authentication method"
msgstr "Metoda autentifikacije"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__company_id
msgid "Company"
msgstr "Preduzeće"
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_demo_form_view
msgid "Configuration"
msgstr "Konfiguracija"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_uid
msgid "Created by"
msgstr "Kreirao"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_date
msgid "Created on"
msgstr "Kreirano"
#. module: fastapi
#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__app__demo
msgid "Demo Endpoint"
msgstr "Demo krajnja točka"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__description
msgid "Description"
msgstr "Opis"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__display_name
msgid "Display Name"
msgstr "Prikazani naziv"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__docs_url
msgid "Docs Url"
msgstr "URL dokumentacije"
#. module: fastapi
#: model:ir.module.category,name:fastapi.module_category_fastapi
#: model:ir.ui.menu,name:fastapi.menu_fastapi_root
msgid "FastAPI"
msgstr "FastAPI"
#. module: fastapi
#: model:ir.actions.act_window,name:fastapi.fastapi_endpoint_act_window
#: model:ir.model,name:fastapi.model_fastapi_endpoint
#: model:ir.ui.menu,name:fastapi.fastapi_endpoint_menu
msgid "FastAPI Endpoint"
msgstr "FastAPI krajnja točka"
#. module: fastapi
#: model:res.groups,name:fastapi.group_fastapi_endpoint_runner
msgid "FastAPI Endpoint Runner"
msgstr "FastAPI pokretač krajnjih točaka"
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view
msgid "Group by..."
msgstr "Grupiši po..."
#. module: fastapi
#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__http_basic
msgid "HTTP Basic"
msgstr "HTTP Basic"
#. module: fastapi
#: model:ir.module.category,description:fastapi.module_category_fastapi
msgid "Helps you manage your Fastapi Endpoints"
msgstr "Pomaže vam upravljati vašim FastAPI krajnjim točkama"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__id
msgid "ID"
msgstr "ID"
#. module: fastapi
#: model:ir.model,name:fastapi.model_res_lang
msgid "Languages"
msgstr "Jezici"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint____last_update
msgid "Last Modified on"
msgstr "Zadnje mijenjano"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_uid
msgid "Last Updated by"
msgstr "Zadnji ažurirao"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_date
msgid "Last Updated on"
msgstr "Zadnje ažurirano"
#. module: fastapi
#: model:res.groups,name:fastapi.my_demo_app_group
msgid "My Demo Endpoint Group"
msgstr "Moja demo grupa krajnjih točaka"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__name
msgid "Name"
msgstr "Naziv:"
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__registry_sync
msgid ""
"ON: the record has been modified and registry was not notified.\n"
"No change will be active until this flag is set to false via proper action.\n"
"\n"
"OFF: record in line with the registry, nothing to do."
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__openapi_url
msgid "Openapi Url"
msgstr "Openapi URL"
#. module: fastapi
#: model:ir.model,name:fastapi.model_ir_rule
msgid "Record Rule"
msgstr "Pravilo zapisa"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__redoc_url
msgid "Redoc Url"
msgstr "Redoc URL"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__registry_sync
msgid "Registry Sync"
msgstr "Sinhronizacija registra"
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view
msgid "Registry Sync Required"
msgstr "Potrebna sinhronizacija registra"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__root_path
msgid "Root Path"
msgstr "Osnovna putanja"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__save_http_session
msgid "Save HTTP Session"
msgstr "Snimanje HTTP sesije"
#. module: fastapi
#: model:ir.actions.server,name:fastapi.fastapi_endpoint_action_sync_registry
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_tree_view
msgid "Sync Registry"
msgstr "Sinhroniziraj registar"
#. module: fastapi
#. odoo-python
#: code:addons/fastapi/models/fastapi_endpoint_demo.py:0
#, python-format
msgid "The authentication method is required for app %(app)s"
msgstr "Metoda autentifikacije je potrebna za aplikaciju %(app)s"
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__name
msgid "The title of the API."
msgstr "Naslov API-ja."
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__user_id
msgid "The user to use to execute the API calls."
msgstr "Korisnik koji će se koristiti za izvršavanje API poziva."
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__user_id
#: model:res.groups,name:fastapi.group_fastapi_user
msgid "User"
msgstr "Korisnik"
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__save_http_session
msgid ""
"Whether session should be saved into the session store. This is required if "
"for example you use the Odoo's authentication mechanism. Oherwise chance are"
" high that you don't need it and could turn off this behaviour. Additionaly "
"turning off this option will prevent useless IO operation when storing and "
"reading the session on the disk and prevent unexpecteed disk space "
"consumption."
msgstr ""
#. module: fastapi
#. odoo-python
#: code:addons/fastapi/models/fastapi_endpoint.py:0
#, python-format
msgid "`%(name)s` uses a blacklisted root_path = `%(root_path)s`"
msgstr "`%(name)s` koristi zabranjenu root_path = `%(root_path)s`"

View file

@ -0,0 +1,251 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fastapi
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__description
msgid "A short description of the API. It can use Markdown"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__active
msgid "Active"
msgstr ""
#. module: fastapi
#: model:res.groups,name:fastapi.group_fastapi_manager
msgid "Administrator"
msgstr ""
#. module: fastapi
#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__api_key
msgid "Api Key"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__app
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view
msgid "App"
msgstr ""
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view
msgid "Archived"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__demo_auth_method
msgid "Authentication method"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__company_id
msgid "Company"
msgstr ""
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_demo_form_view
msgid "Configuration"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_uid
msgid "Created by"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_date
msgid "Created on"
msgstr ""
#. module: fastapi
#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__app__demo
msgid "Demo Endpoint"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__description
msgid "Description"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__display_name
msgid "Display Name"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__docs_url
msgid "Docs Url"
msgstr ""
#. module: fastapi
#: model:ir.module.category,name:fastapi.module_category_fastapi
#: model:ir.ui.menu,name:fastapi.menu_fastapi_root
msgid "FastAPI"
msgstr ""
#. module: fastapi
#: model:ir.actions.act_window,name:fastapi.fastapi_endpoint_act_window
#: model:ir.model,name:fastapi.model_fastapi_endpoint
#: model:ir.ui.menu,name:fastapi.fastapi_endpoint_menu
msgid "FastAPI Endpoint"
msgstr ""
#. module: fastapi
#: model:res.groups,name:fastapi.group_fastapi_endpoint_runner
msgid "FastAPI Endpoint Runner"
msgstr ""
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view
msgid "Group by..."
msgstr ""
#. module: fastapi
#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__http_basic
msgid "HTTP Basic"
msgstr ""
#. module: fastapi
#: model:ir.module.category,description:fastapi.module_category_fastapi
msgid "Helps you manage your Fastapi Endpoints"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__id
msgid "ID"
msgstr ""
#. module: fastapi
#: model:ir.model,name:fastapi.model_res_lang
msgid "Languages"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint____last_update
msgid "Last Modified on"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_uid
msgid "Last Updated by"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_date
msgid "Last Updated on"
msgstr ""
#. module: fastapi
#: model:res.groups,name:fastapi.my_demo_app_group
msgid "My Demo Endpoint Group"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__name
msgid "Name"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__registry_sync
msgid ""
"ON: the record has been modified and registry was not notified.\n"
"No change will be active until this flag is set to false via proper action.\n"
"\n"
"OFF: record in line with the registry, nothing to do."
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__openapi_url
msgid "Openapi Url"
msgstr ""
#. module: fastapi
#: model:ir.model,name:fastapi.model_ir_rule
msgid "Record Rule"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__redoc_url
msgid "Redoc Url"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__registry_sync
msgid "Registry Sync"
msgstr ""
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view
msgid "Registry Sync Required"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__root_path
msgid "Root Path"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__save_http_session
msgid "Save HTTP Session"
msgstr ""
#. module: fastapi
#: model:ir.actions.server,name:fastapi.fastapi_endpoint_action_sync_registry
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_tree_view
msgid "Sync Registry"
msgstr ""
#. module: fastapi
#. odoo-python
#: code:addons/fastapi/models/fastapi_endpoint_demo.py:0
#, python-format
msgid "The authentication method is required for app %(app)s"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__name
msgid "The title of the API."
msgstr ""
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__user_id
msgid "The user to use to execute the API calls."
msgstr ""
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__user_id
#: model:res.groups,name:fastapi.group_fastapi_user
msgid "User"
msgstr ""
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__save_http_session
msgid ""
"Whether session should be saved into the session store. This is required if "
"for example you use the Odoo's authentication mechanism. Oherwise chance are"
" high that you don't need it and could turn off this behaviour. Additionaly "
"turning off this option will prevent useless IO operation when storing and "
"reading the session on the disk and prevent unexpecteed disk space "
"consumption."
msgstr ""
#. module: fastapi
#. odoo-python
#: code:addons/fastapi/models/fastapi_endpoint.py:0
#, python-format
msgid "`%(name)s` uses a blacklisted root_path = `%(root_path)s`"
msgstr ""

View file

@ -0,0 +1,269 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fastapi
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-04 09:40+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.4\n"
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__description
msgid "A short description of the API. It can use Markdown"
msgstr "Una breve descrizione dell'API. Può contenere Markdown"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__active
msgid "Active"
msgstr "Attivo"
#. module: fastapi
#: model:res.groups,name:fastapi.group_fastapi_manager
msgid "Administrator"
msgstr "Amministratore"
#. module: fastapi
#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__api_key
msgid "Api Key"
msgstr "Chiave API"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__app
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view
msgid "App"
msgstr "Applicazione"
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view
msgid "Archived"
msgstr "In archivio"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__demo_auth_method
msgid "Authentication method"
msgstr "Metodo autenticazione"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__company_id
msgid "Company"
msgstr "Azienda"
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_demo_form_view
msgid "Configuration"
msgstr "Configurazione"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_uid
msgid "Created by"
msgstr "Creato da"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_date
msgid "Created on"
msgstr "Creato il"
#. module: fastapi
#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__app__demo
msgid "Demo Endpoint"
msgstr "Endpoint esempio"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__description
msgid "Description"
msgstr "Descrizione"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__docs_url
msgid "Docs Url"
msgstr "URL documenti"
#. module: fastapi
#: model:ir.module.category,name:fastapi.module_category_fastapi
#: model:ir.ui.menu,name:fastapi.menu_fastapi_root
msgid "FastAPI"
msgstr "FastAPI"
#. module: fastapi
#: model:ir.actions.act_window,name:fastapi.fastapi_endpoint_act_window
#: model:ir.model,name:fastapi.model_fastapi_endpoint
#: model:ir.ui.menu,name:fastapi.fastapi_endpoint_menu
msgid "FastAPI Endpoint"
msgstr "Endpoint FastAPI"
#. module: fastapi
#: model:res.groups,name:fastapi.group_fastapi_endpoint_runner
msgid "FastAPI Endpoint Runner"
msgstr "Esecutore endopoint FastAPI"
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view
msgid "Group by..."
msgstr "Raggruppa per..."
#. module: fastapi
#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__http_basic
msgid "HTTP Basic"
msgstr "Base HTTP"
#. module: fastapi
#: model:ir.module.category,description:fastapi.module_category_fastapi
msgid "Helps you manage your Fastapi Endpoints"
msgstr "Aiuta nella gestione degli endpoint FastAPI"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__id
msgid "ID"
msgstr "ID"
#. module: fastapi
#: model:ir.model,name:fastapi.model_res_lang
msgid "Languages"
msgstr "Lingue"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint____last_update
msgid "Last Modified on"
msgstr "Ultima modifica il"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_uid
msgid "Last Updated by"
msgstr "Ultimo aggiornamento di"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_date
msgid "Last Updated on"
msgstr "Ultimo aggiornamento il"
#. module: fastapi
#: model:res.groups,name:fastapi.my_demo_app_group
msgid "My Demo Endpoint Group"
msgstr "Il mio gruppo endpoint esempio"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__name
msgid "Name"
msgstr "Nome"
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__registry_sync
msgid ""
"ON: the record has been modified and registry was not notified.\n"
"No change will be active until this flag is set to false via proper action.\n"
"\n"
"OFF: record in line with the registry, nothing to do."
msgstr ""
"Acceso: il record è stato modificato e il registro non è stato notificato.\n"
"Nessuna modifica sarà attiva finchè questa opzione è impostata a falso "
"attraverso un'azione opportuna.\n"
"\n"
"Spento: record allineato con il registro, non c'è niente da fare."
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__openapi_url
msgid "Openapi Url"
msgstr "URL OpenAPI"
#. module: fastapi
#: model:ir.model,name:fastapi.model_ir_rule
msgid "Record Rule"
msgstr "Regola su record"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__redoc_url
msgid "Redoc Url"
msgstr "URL record"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__registry_sync
msgid "Registry Sync"
msgstr "Sincro registro"
#. module: fastapi
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view
msgid "Registry Sync Required"
msgstr "Sincro registro richiesto"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__root_path
msgid "Root Path"
msgstr "Percorso radice"
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__save_http_session
msgid "Save HTTP Session"
msgstr "Salva sessione HTTP"
#. module: fastapi
#: model:ir.actions.server,name:fastapi.fastapi_endpoint_action_sync_registry
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view
#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_tree_view
msgid "Sync Registry"
msgstr "Sincronizza registro"
#. module: fastapi
#. odoo-python
#: code:addons/fastapi/models/fastapi_endpoint_demo.py:0
#, python-format
msgid "The authentication method is required for app %(app)s"
msgstr "Il metodo di autenticazione è richiesto per l'app %(app)s"
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__name
msgid "The title of the API."
msgstr "Titolo dell'API."
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__user_id
msgid "The user to use to execute the API calls."
msgstr "Utente da utilizzare per eseguire la chiamata API."
#. module: fastapi
#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__user_id
#: model:res.groups,name:fastapi.group_fastapi_user
msgid "User"
msgstr "Utente"
#. module: fastapi
#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__save_http_session
msgid ""
"Whether session should be saved into the session store. This is required if "
"for example you use the Odoo's authentication mechanism. Oherwise chance are "
"high that you don't need it and could turn off this behaviour. Additionaly "
"turning off this option will prevent useless IO operation when storing and "
"reading the session on the disk and prevent unexpecteed disk space "
"consumption."
msgstr ""
"Se la sessione deve essere salvata nell'archivio sessioni. Questo è "
"necessario se, ad esempio, si utilizza il meccanismo di autenticazione di "
"Odoo. In caso contrario, è probabile che non se ne abbia bisogno e si può "
"disattivare questo comportamento. Inoltre, disattivando questa opzione si "
"impediranno operazioni di I/O inutili durante l'archiviazione e la lettura "
"della sessione sul disco e si impedirà un consumo inaspettato di spazio su "
"disco."
#. module: fastapi
#. odoo-python
#: code:addons/fastapi/models/fastapi_endpoint.py:0
#, python-format
msgid "`%(name)s` uses a blacklisted root_path = `%(root_path)s`"
msgstr "`%(name)s` utilizza un root_path bloccato = `%(root_path)s`"
#~ msgid "Authenciation method"
#~ msgstr "Metodo autenticazione"

View file

@ -0,0 +1,40 @@
# Copyright 2025 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
"""
ASGI middleware for FastAPI.
This module provides an ASGI middleware for FastAPI applications. The middleware
is designed to ensure managed the lifecycle of the threads used to as event loop
for the ASGI application.
"""
from typing import Iterable
import a2wsgi
from a2wsgi.asgi import ASGIResponder
from a2wsgi.asgi_typing import ASGIApp
from a2wsgi.wsgi_typing import Environ, StartResponse
from .pools import event_loop_pool
class ASGIMiddleware(a2wsgi.ASGIMiddleware):
def __init__(
self,
app: ASGIApp,
wait_time: float | None = None,
) -> None:
# We don't want to use the default event loop policy
# because we want to manage the event loop ourselves
# using the event loop pool.
# Since the the base class check if the given loop is
# None, we can pass False to avoid the initialization
# of the default event loop
super().__init__(app, wait_time, False)
def __call__(
self, environ: Environ, start_response: StartResponse
) -> Iterable[bytes]:
with event_loop_pool.get_event_loop() as loop:
return ASGIResponder(self.app, loop)(environ, start_response)

View file

@ -0,0 +1,4 @@
from .fastapi_endpoint import FastapiEndpoint
from . import fastapi_endpoint_demo
from . import ir_rule
from . import res_lang

View file

@ -0,0 +1,346 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import logging
from functools import partial
from itertools import chain
from typing import Any, Callable, Dict, List, Tuple
from starlette.middleware import Middleware
from starlette.routing import Mount
from odoo import _, api, exceptions, fields, models, tools
from fastapi import APIRouter, Depends, FastAPI
from .. import dependencies
from ..middleware import ASGIMiddleware
_logger = logging.getLogger(__name__)
class FastapiEndpoint(models.Model):
_name = "fastapi.endpoint"
_inherit = "endpoint.route.sync.mixin"
_description = "FastAPI Endpoint"
name: str = fields.Char(required=True, help="The title of the API.")
description: str = fields.Text(
help="A short description of the API. It can use Markdown"
)
root_path: str = fields.Char(
required=True,
index=True,
compute="_compute_root_path",
inverse="_inverse_root_path",
readonly=False,
store=True,
copy=False,
)
app: str = fields.Selection(selection=[], required=True)
user_id = fields.Many2one(
comodel_name="res.users",
string="User",
help="The user to use to execute the API calls.",
default=lambda self: self.env.ref("base.public_user"),
)
docs_url: str = fields.Char(compute="_compute_urls")
redoc_url: str = fields.Char(compute="_compute_urls")
openapi_url: str = fields.Char(compute="_compute_urls")
company_id = fields.Many2one(
"res.company",
compute="_compute_company_id",
store=True,
readonly=False,
domain="[('user_ids', 'in', user_id)]",
)
save_http_session = fields.Boolean(
string="Save HTTP Session",
help="Whether session should be saved into the session store. This is "
"required if for example you use the Odoo's authentication mechanism. "
"Oherwise chance are high that you don't need it and could turn off "
"this behaviour. Additionaly turning off this option will prevent useless "
"IO operation when storing and reading the session on the disk and prevent "
"unexpecteed disk space consumption.",
default=True,
)
@api.depends("root_path")
def _compute_root_path(self):
for rec in self:
rec.root_path = rec._clean_root_path()
def _inverse_root_path(self):
for rec in self:
rec.root_path = rec._clean_root_path()
def _clean_root_path(self):
root_path = (self.root_path or "").strip()
if not root_path.startswith("/"):
root_path = "/" + root_path
return root_path
_blacklist_root_paths = {"/", "/web", "/website"}
@api.constrains("root_path")
def _check_root_path(self):
for rec in self:
if rec.root_path in self._blacklist_root_paths:
raise exceptions.UserError(
_(
"`%(name)s` uses a blacklisted root_path = `%(root_path)s`",
name=rec.name,
root_path=rec.root_path,
)
)
@api.depends("root_path")
def _compute_urls(self):
for rec in self:
rec.docs_url = f"{rec.root_path}/docs"
rec.redoc_url = f"{rec.root_path}/redoc"
rec.openapi_url = f"{rec.root_path}/openapi.json"
@api.depends("user_id")
def _compute_company_id(self):
for endpoint in self:
endpoint.company_id = endpoint.user_id.company_id
#
# endpoint.route.sync.mixin methods implementation
#
def _prepare_endpoint_rules(self, options=None):
return [rec._make_routing_rule(options=options) for rec in self]
def _registered_endpoint_rule_keys(self):
res = []
for rec in self:
routing = rec._get_routing_info()
res.append(rec._endpoint_registry_route_unique_key(routing))
return tuple(res)
@api.model
def _routing_impacting_fields(self) -> Tuple[str, ...]:
"""The list of fields requiring to refresh the mount point of the pp
into odoo if modified"""
return ("root_path", "save_http_session")
#
# end of endpoint.route.sync.mixin methods implementation
#
def write(self, vals):
res = super().write(vals)
self._handle_route_updates(vals)
return res
def action_sync_registry(self):
self.filtered(lambda e: not e.registry_sync).write({"registry_sync": True})
def _handle_route_updates(self, vals):
observed_fields = [self._routing_impacting_fields(), self._fastapi_app_fields()]
refresh_fastapi_app = any([x in vals for x in chain(*observed_fields)])
if refresh_fastapi_app:
self._reset_app()
if "user_id" in vals:
self.get_uid.clear_cache(self)
return False
@api.model
def _fastapi_app_fields(self) -> List[str]:
"""The list of fields requiring to refresh the fastapi app if modified"""
return []
def _make_routing_rule(self, options=None):
"""Generator of rule"""
self.ensure_one()
routing = self._get_routing_info()
options = options or self._default_endpoint_options()
route = "|".join(routing["routes"])
key = self._endpoint_registry_route_unique_key(routing)
endpoint_hash = hash(route)
return self._endpoint_registry.make_rule(
key, route, options, routing, endpoint_hash
)
def _default_endpoint_options(self):
options = {"handler": self._default_endpoint_options_handler()}
return options
def _default_endpoint_options_handler(self):
# The handler is useless in the context of a fastapi endpoint since the
# routing type is "fastapi" and the routing is handled by a dedicated
# dispatcher that will forward the request to the fastapi app.
base_path = "odoo.addons.endpoint_route_handler.controllers.main"
return {
"klass_dotted_path": f"{base_path}.EndpointNotFoundController",
"method_name": "auto_not_found",
}
def _get_routing_info(self):
self.ensure_one()
return {
"type": "fastapi",
"auth": "public",
"methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
"routes": [
f"{self.root_path}/",
f"{self.root_path}/<path:application_path>",
],
"save_session": self.save_http_session,
# csrf ?????
}
def _endpoint_registry_route_unique_key(self, routing: Dict[str, Any]):
route = "|".join(routing["routes"])
path = route.replace(self.root_path, "")
return f"{self._name}:{self.id}:{path}"
def _reset_app(self):
self._get_id_by_root_path_map.clear_cache(self)
self._get_id_for_path.clear_cache(self)
self._reset_app_cache_marker.clear_cache(self)
@tools.ormcache()
def _reset_app_cache_marker(self):
"""This methos is used to get a way to mark the orm cache as dirty
when the app is reset. By marking the cache as dirty, the system
will signal to others instances that the cache is not up to date
and that they should invalidate their cache as well. This is required
to ensure that any change requiring a reset of the app is propagated
to all the running instances.
"""
@api.model
def _normalize_url_path(self, path) -> str:
"""
Normalize a URL path:
* Remove redundant slashes,
* Remove trailing slash (unless it's the root),
* Lowercase for case-insensitive matching
"""
parts = [part.lower() for part in path.strip().split("/") if part]
return "/" + "/".join(parts)
@api.model
def _is_suburl(self, path, prefix) -> bool:
"""
Check if 'path' is a subpath of 'prefix' in URL logic:
* Must start with the prefix followed by a slash
This will ensure that the matching is done one the path
parts and ensures that e.g. /a/b is not prefix of /a/bc.
"""
path = self._normalize_url_path(path)
prefix = self._normalize_url_path(prefix)
if path == prefix:
return True
if path.startswith(prefix + "/"):
return True
return False
@api.model
def _find_first_matching_url_path(self, paths, prefix) -> str | None:
"""
Return the first path that is a subpath of 'prefix',
ordered by longest URL path first (most number of segments).
"""
# Sort by number of segments (shallowest first)
sorted_paths = sorted(
paths,
key=lambda p: len(self._normalize_url_path(p).split("/")),
reverse=True,
)
for path in sorted_paths:
if self._is_suburl(prefix, path):
return path
return None
@api.model
@tools.ormcache()
def _get_id_by_root_path_map(self):
return {r.root_path: r.id for r in self.search([])}
@api.model
@tools.ormcache("path")
def _get_id_for_path(self, path):
id_by_path = self._get_id_by_root_path_map()
root_path = self._find_first_matching_url_path(id_by_path.keys(), path)
return id_by_path.get(root_path)
@api.model
def _get_endpoint(self, path):
id_ = self._get_id_for_path(path)
return self.browse(id_) if id_ else None
@api.model
def get_app(self, path):
record = self._get_endpoint(path)
if not record:
return None
app = FastAPI()
app.mount(record.root_path, record._get_app())
self._clear_fastapi_exception_handlers(app)
return ASGIMiddleware(app)
def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None:
"""
Clear the exception handlers of the given fastapi app.
This method is used to ensure that the exception handlers are handled
by odoo and not by fastapi. We therefore need to remove all the handlers
added by default when instantiating a FastAPI app. Since apps can be
mounted recursively, we need to apply this method to all the apps in the
mounted tree.
"""
app.exception_handlers = {}
for route in app.routes:
if isinstance(route, Mount):
self._clear_fastapi_exception_handlers(route.app)
@api.model
@tools.ormcache("path")
def get_uid(self, path):
record = self._get_endpoint(path)
if not record:
return None
return record.user_id.id
def _get_app(self) -> FastAPI:
app = FastAPI(**self._prepare_fastapi_app_params())
for router in self._get_fastapi_routers():
app.include_router(router=router)
app.dependency_overrides.update(self._get_app_dependencies_overrides())
return app
def _get_app_dependencies_overrides(self) -> Dict[Callable, Callable]:
return {
dependencies.fastapi_endpoint_id: partial(lambda a: a, self.id),
dependencies.company_id: partial(lambda a: a, self.company_id.id),
}
def _prepare_fastapi_app_params(self) -> Dict[str, Any]:
"""Return the params to pass to the Fast API app constructor"""
return {
"title": self.name,
"description": self.description,
"middleware": self._get_fastapi_app_middlewares(),
"dependencies": self._get_fastapi_app_dependencies(),
}
def _get_fastapi_routers(self) -> List[APIRouter]:
"""Return the api routers to use for the instance.
This method must be implemented when registering a new api type.
"""
return []
def _get_fastapi_app_middlewares(self) -> List[Middleware]:
"""Return the middlewares to use for the fastapi app."""
return []
def _get_fastapi_app_dependencies(self) -> List[Depends]:
"""Return the dependencies to use for the fastapi app."""
return [Depends(dependencies.accept_language)]

View file

@ -0,0 +1,105 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
from typing import Annotated, Any, List
from odoo import _, api, fields, models
from odoo.api import Environment
from odoo.exceptions import ValidationError
from odoo.addons.base.models.res_partner import Partner
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import APIKeyHeader
from ..dependencies import (
authenticated_partner_from_basic_auth_user,
authenticated_partner_impl,
odoo_env,
)
from ..routers import demo_router, demo_router_doc
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 Basic")],
string="Authenciation method",
)
def _get_fastapi_routers(self) -> List[APIRouter]:
if self.app == "demo":
return [demo_router]
return super()._get_fastapi_routers()
@api.constrains("app", "demo_auth_method")
def _valdiate_demo_auth_method(self):
for rec in self:
if rec.app == "demo" and not rec.demo_auth_method:
raise ValidationError(
_(
"The authentication method is required for app %(app)s",
app=rec.app,
)
)
@api.model
def _fastapi_app_fields(self) -> List[str]:
fields = super()._fastapi_app_fields()
fields.append("demo_auth_method")
return fields
def _get_app(self):
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
def _prepare_fastapi_app_params(self) -> dict[str, Any]:
params = super()._prepare_fastapi_app_params()
if self.app == "demo":
tags_metadata = params.get("openapi_tags", []) or []
tags_metadata.append({"name": "demo", "description": demo_router_doc})
params["openapi_tags"] = tags_metadata
return params
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"].sudo().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

View file

@ -0,0 +1,27 @@
# Copyright 2022 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
class IrRule(models.Model):
"""Add authenticated_partner_id in record rule evaluation context.
This come from the env current odoo environment which is
populated by dependency method authenticated_partner_env or authenticated_partner
when a route handler depends one of them and is called by the FastAPI service layer.
"""
_inherit = "ir.rule"
@api.model
def _eval_context(self):
ctx = super()._eval_context()
ctx["authenticated_partner_id"] = self.env.context.get(
"authenticated_partner_id", False
)
return ctx
def _compute_domain_keys(self):
"""Return the list of context keys to use for caching ``_compute_domain``."""
return super()._compute_domain_keys() + ["authenticated_partner_id"]

View file

@ -0,0 +1,46 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from collections import defaultdict
from accept_language import parse_accept_language
from odoo import api, models, tools
class ResLang(models.Model):
_inherit = "res.lang"
@api.model
@tools.ormcache("accept_language")
def _get_lang_from_accept_language(self, accept_language):
"""Get the language from the Accept-Language header.
:param accept_language: The Accept-Language header.
:return: The language code.
"""
if not accept_language:
return
parsed_accepted_langs = parse_accept_language(accept_language)
installed_locale_langs = set()
installed_locale_by_lang = defaultdict(list)
for lang_code, _name in self.get_installed():
installed_locale_langs.add(lang_code)
installed_locale_by_lang[lang_code.split("_")[0]].append(lang_code)
# parsed_acccepted_langs is sorted by priority (higher first)
for lang in parsed_accepted_langs:
# we first check if a locale (en_GB) is available into the list of
# available locales into Odoo
locale = None
if lang.locale in installed_locale_langs:
locale = lang.locale
# if no locale language is installed, we look for an available
# locale for the given language (en). We return the first one
# found for this language.
else:
locales = installed_locale_by_lang.get(lang.language)
if locales:
locale = locales[0]
if locale:
return locale

View file

@ -0,0 +1,11 @@
from .event_loop import EventLoopPool
from .fastapi_app import FastApiAppPool
from odoo.service.server import CommonServer
event_loop_pool = EventLoopPool()
fastapi_app_pool = FastApiAppPool()
CommonServer.on_stop(event_loop_pool.shutdown)
__all__ = ["event_loop_pool", "fastapi_app_pool"]

View file

@ -0,0 +1,58 @@
# Copyright 2025 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import asyncio
import queue
import threading
from contextlib import contextmanager
from typing import Generator
class EventLoopPool:
def __init__(self):
self.pool = queue.Queue[tuple[asyncio.AbstractEventLoop, threading.Thread]]()
def __get_event_loop_and_thread(
self,
) -> tuple[asyncio.AbstractEventLoop, threading.Thread]:
"""
Get an event loop from the pool. If no event loop is available, create a new one.
"""
try:
return self.pool.get_nowait()
except queue.Empty:
loop = asyncio.new_event_loop()
thread = threading.Thread(target=loop.run_forever, daemon=True)
thread.start()
return loop, thread
def __return_event_loop(
self, loop: asyncio.AbstractEventLoop, thread: threading.Thread
) -> None:
"""
Return an event loop to the pool for reuse.
"""
self.pool.put((loop, thread))
def shutdown(self):
"""
Shutdown all event loop threads in the pool.
"""
while not self.pool.empty():
loop, thread = self.pool.get_nowait()
loop.call_soon_threadsafe(loop.stop)
thread.join()
loop.close()
@contextmanager
def get_event_loop(self) -> Generator[asyncio.AbstractEventLoop, None, None]:
"""
Get an event loop from the pool. If no event loop is available, create a new one.
After the context manager exits, the event loop is returned to the pool for reuse.
"""
loop, thread = self.__get_event_loop_and_thread()
try:
yield loop
finally:
self.__return_event_loop(loop, thread)

View file

@ -0,0 +1,129 @@
# Copyright 2025 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import logging
import queue
import threading
from collections import defaultdict
from contextlib import contextmanager
from typing import Generator
from odoo.api import Environment
from fastapi import FastAPI
_logger = logging.getLogger(__name__)
class FastApiAppPool:
"""Pool of FastAPI apps.
This class manages a pool of FastAPI apps. The pool is organized by database name
and root path. Each pool is a queue of FastAPI apps.
The pool is used to reuse FastAPI apps across multiple requests. This is useful
to avoid the overhead of creating a new FastAPI app for each request. The pool
ensures that only one request at a time uses an app.
The proper way to use the pool is to use the get_app method as a context manager.
This ensures that the app is returned to the pool after the context manager exits.
The get_app method is designed to ensure that the app made available to the
caller is unique and not used by another caller at the same time.
.. code-block:: python
with fastapi_app_pool.get_app(env=request.env, root_path=root_path) as app:
# use the app
The pool is invalidated when the cache registry is updated. This ensures that
the pool is always up-to-date with the latest app configuration. It also
ensures that the invalidation is done even in the case of a modification occurring
in a different worker process or thread or server instance. This mechanism
works because every time an attribute of the fastapi.endpoint model is modified
and this attribute is part of the list returned by the `_fastapi_app_fields`,
or `_routing_impacting_fields` methods, we reset the cache of a marker method
`_reset_app_cache_marker`. As side effect, the cache registry is marked to be
updated by the increment of the `cache_sequence` SQL sequence. This cache sequence
on the registry is reloaded from the DB on each request made to a specific database.
When an app is retrieved from the pool, we always compare the cache sequence of
the pool with the cache sequence of the registry. If the two sequences are different,
we invalidate the pool and save the new cache sequence on the pool.
The cache is based on a defaultdict of defaultdict of queue.Queue. We are cautious
that the use of defaultdict is not thread-safe for operations that modify the
dictionary. However the only operation that modifies the dictionary is the
first access to a new key. If two threads access the same key at the same time,
the two threads will create two different queues. This is not a problem since
at the time of returning an app to the pool, we are sure that a queue exists
for the key into the cache and all the created apps are returned to the same
valid queue. And the end, the lack of thread-safety for the defaultdict could
only lead to a negligible overhead of creating a new queue that will never be
used. This is why we consider that the use of defaultdict is safe in this context.
"""
def __init__(self):
self._queue_by_db_by_root_path: dict[
str, dict[str, queue.Queue[FastAPI]]
] = defaultdict(lambda: defaultdict(queue.Queue))
self.__cache_sequence = 0
self._lock = threading.Lock()
def __get_pool(self, env: Environment, root_path: str) -> queue.Queue[FastAPI]:
db_name = env.cr.dbname
return self._queue_by_db_by_root_path[db_name][root_path]
def __get_app(self, env: Environment, root_path: str) -> FastAPI:
pool = self.__get_pool(env, root_path)
try:
return pool.get_nowait()
except queue.Empty:
return env["fastapi.endpoint"].sudo().get_app(root_path)
def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None:
pool = self.__get_pool(env, root_path)
pool.put(app)
@contextmanager
def get_app(
self, env: Environment, root_path: str
) -> Generator[FastAPI, None, None]:
"""Return a FastAPI app to be used in a context manager.
The app is retrieved from the pool if available, otherwise a new one is created.
The app is returned to the pool after the context manager exits.
When used into the FastApiDispatcher class this ensures that the app is reused
across multiple requests but only one request at a time uses an app.
"""
self._check_cache(env)
app = self.__get_app(env, root_path)
try:
yield app
finally:
self.__return_app(env, app, root_path)
@property
def cache_sequence(self) -> int:
return self.__cache_sequence
@cache_sequence.setter
def cache_sequence(self, value: int) -> None:
if value != self.__cache_sequence:
with self._lock:
self.__cache_sequence = value
def _check_cache(self, env: Environment) -> None:
cache_sequence = env.registry.cache_sequence
if cache_sequence != self.cache_sequence and self.cache_sequence != 0:
_logger.info(
"Cache registry updated, reset fastapi_app pool for the current "
"database"
)
self.invalidate(env)
self.cache_sequence = cache_sequence
def invalidate(self, env: Environment, root_path: str | None = None) -> None:
db_name = env.cr.dbname
if root_path:
self._queue_by_db_by_root_path[db_name][root_path] = queue.Queue()
elif db_name in self._queue_by_db_by_root_path:
del self._queue_by_db_by_root_path[db_name]

View file

@ -0,0 +1 @@
* Laurent Mignon <laurent.mignon@acsone.eu>

View file

@ -0,0 +1,38 @@
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/

View file

@ -0,0 +1,127 @@
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 <https://github.com/OCA/rest-framework/issues/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 <https://github.com/OCA/rest-framework/issues/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 <https://github.com/OCA/rest-framework/issues/422>`_)
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>`_)

View file

@ -0,0 +1,10 @@
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.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
from .demo_router import router as demo_router
from .demo_router import __doc__ as demo_router_doc

View file

@ -0,0 +1,143 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
"""
The demo router is a router that demonstrates how to use the fastapi
integration with odoo.
"""
from typing import Annotated
from psycopg2 import errorcodes
from psycopg2.errors import OperationalError
from odoo.api import Environment
from odoo.exceptions import AccessError, MissingError, UserError, ValidationError
from odoo.service.model import MAX_TRIES_ON_CONCURRENCY_FAILURE
from odoo.addons.base.models.res_partner import Partner
from fastapi import APIRouter, Depends, File, HTTPException, Query, status
from fastapi.responses import JSONResponse
from ..dependencies import authenticated_partner, fastapi_endpoint, odoo_env
from ..models import FastapiEndpoint
from ..schemas import DemoEndpointAppInfo, DemoExceptionType, DemoUserInfo
router = APIRouter(tags=["demo"])
@router.get("/demo")
async def hello_word():
"""Hello World!"""
return {"Hello": "World"}
@router.get("/demo/exception")
async def exception(exception_type: DemoExceptionType, error_message: str):
"""Raise an exception
This method is used in the test suite to check that any exception
is correctly handled by the fastapi endpoint and that the transaction
is roll backed.
"""
exception_classes = {
DemoExceptionType.user_error: UserError,
DemoExceptionType.validation_error: ValidationError,
DemoExceptionType.access_error: AccessError,
DemoExceptionType.missing_error: MissingError,
DemoExceptionType.http_exception: HTTPException,
DemoExceptionType.bare_exception: NotImplementedError,
}
exception_cls = exception_classes[exception_type]
if exception_cls is HTTPException:
raise exception_cls(status_code=status.HTTP_409_CONFLICT, detail=error_message)
raise exception_classes[exception_type](error_message)
@router.get("/demo/lang")
async def get_lang(env: Annotated[Environment, Depends(odoo_env)]):
"""Returns the language according to the available languages in Odoo and the
Accept-Language header.
This method is used in the test suite to check that the language is correctly
set in the Odoo environment according to the Accept-Language header
"""
return env.context.get("lang")
@router.get("/demo/who_ami")
async def who_ami(
partner: Annotated[Partner, Depends(authenticated_partner)]
) -> DemoUserInfo:
"""Who am I?
Returns the authenticated partner
"""
# This method show you how you can rget the authenticated partner without
# depending on a specific implementation.
return DemoUserInfo(name=partner.name, display_name=partner.display_name)
@router.get(
"/demo/endpoint_app_info",
dependencies=[Depends(authenticated_partner)],
)
async def endpoint_app_info(
endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
) -> DemoEndpointAppInfo:
"""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 DemoEndpointAppInfo.model_validate(endpoint)
_CPT = 0
@router.get("/demo/retrying")
async def retrying(
nbr_retries: Annotated[int, Query(gt=1, lt=MAX_TRIES_ON_CONCURRENCY_FAILURE)],
) -> int:
"""This method is used in the test suite to check that the retrying
functionality in case of concurrency error on the database is working
correctly for retryable exceptions.
The output will be the number of retries that have been done.
This method is mainly used to test the retrying functionality
"""
global _CPT
if _CPT < nbr_retries:
_CPT += 1
raise FakeConcurrentUpdateError("fake error")
tryno = _CPT
_CPT = 0
return tryno
@router.post("/demo/retrying")
async def retrying_post(
nbr_retries: Annotated[int, Query(gt=1, lt=MAX_TRIES_ON_CONCURRENCY_FAILURE)],
file: Annotated[bytes, File()],
) -> JSONResponse:
"""This method is used in the test suite to check that the retrying
functionality in case of concurrency error on the database is working
correctly for retryable exceptions.
The output will be the number of retries that have been done.
This method is mainly used to test the retrying functionality
"""
global _CPT
if _CPT < nbr_retries:
_CPT += 1
raise FakeConcurrentUpdateError("fake error")
tryno = _CPT
_CPT = 0
return JSONResponse(content={"retries": tryno, "file": file.decode("utf-8")})
class FakeConcurrentUpdateError(OperationalError):
@property
def pgcode(self):
return errorcodes.SERIALIZATION_FAILURE

View file

@ -0,0 +1,67 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import warnings
from enum import Enum
from typing import Annotated, Generic, List, Optional, TypeVar
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field
T = TypeVar("T")
class PagedCollection(BaseModel, Generic[T]):
count: Annotated[
int,
Field(
...,
description="Count of items into the system.\n "
"Replaces the total field which is deprecated",
validation_alias=AliasChoices("count", "total"),
),
]
items: List[T]
@computed_field()
@property
def total(self) -> int:
return self.count
@total.setter
def total(self, value: int):
warnings.warn(
"The total field is deprecated, please use count instead",
DeprecationWarning,
stacklevel=2,
)
self.count = value
class Paging(BaseModel):
limit: Optional[int] = None
offset: Optional[int] = None
#############################################################
# here above you can find models only used for the demo app #
#############################################################
class DemoUserInfo(BaseModel):
name: str
display_name: str
class DemoEndpointAppInfo(BaseModel):
id: int
name: str
app: str
auth_method: str = Field(alias="demo_auth_method")
root_path: str
model_config = ConfigDict(from_attributes=True)
class DemoExceptionType(str, Enum):
user_error = "UserError"
validation_error = "ValidationError"
access_error = "AccessError"
missing_error = "MissingError"
http_exception = "HTTPException"
bare_exception = "BareException"

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 ACSONE SA/NV
License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -->
<odoo>
<record model="ir.model.access" id="fastapi_endpoint_access_view">
<field name="name">fastapi.endpoint view</field>
<field name="model_id" ref="model_fastapi_endpoint" />
<field name="group_id" ref="group_fastapi_user" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<record model="ir.model.access" id="fastapi_endpoint_access_manage">
<field name="name">fastapi.endpoint manage</field>
<field name="model_id" ref="model_fastapi_endpoint" />
<field name="group_id" ref="group_fastapi_manager" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="1" />
</record>
</odoo>

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 ACSONE SA/NV
License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -->
<odoo>
<!-- give access to the user running the demo app to the res.user model -->
<record id="res_user_fastapi_running_user" model="ir.model.access">
<field name="name">Fastapi: Running user read users</field>
<field name="model_id" ref="base.model_res_users" />
<field name="group_id" ref="group_fastapi_endpoint_runner" />
<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>
<!-- restrict the access by the user running the demo app to its own user -->
<record id="res_user_fastapi_running_user_rule" model="ir.rule">
<field name="name">Fastapi: Running user rule</field>
<field name="model_id" ref="base.model_res_users" />
<field name="domain_force"> [('id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fastapi_endpoint_runner'))]" />
</record>
<!-- give access to the user running the demo app to the res.partner model -->
<record id="res_partner_fastapi_running_user" model="ir.model.access">
<field name="name">Fastapi: Running user read partners</field>
<field name="model_id" ref="base.model_res_partner" />
<field name="group_id" ref="group_fastapi_endpoint_runner" />
<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>
<!-- restrict the access by the user running the demo app to its own partner
and the authenticated one-->
<record id="res_partner_fastapi_running_user_rule" model="ir.rule">
<field name="name">Fastapi: Running user rule</field>
<field name="model_id" ref="base.model_res_partner" />
<field
name="domain_force"
> ['|', ('user_ids', '=', user.id), ('id', '=', authenticated_partner_id)]</field>
<field name="groups" eval="[(4, ref('group_fastapi_endpoint_runner'))]" />
</record>
<!-- give access by the user running the demo app to the fastapi.enddoint model -->
<record id="fastapi_endpoint_fastapi_running_user" model="ir.model.access">
<field name="name">Fastapi: Running user read endpoints</field>
<field name="model_id" ref="fastapi.model_fastapi_endpoint" />
<field name="group_id" ref="group_fastapi_endpoint_runner" />
<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>
<!-- restrict the access by the user running the demo app to its own endpoints -->
<record id="fastapi_endpoint_fastapi_running_user_rule" model="ir.rule">
<field name="name">Fastapi: Running user rule</field>
<field name="model_id" ref="fastapi.model_fastapi_endpoint" />
<field name="domain_force"> [('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fastapi_endpoint_runner'))]" />
</record>
</odoo>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 ACSONE SA/NV
License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -->
<odoo>
<record model="ir.module.category" id="module_category_fastapi">
<field name="name">FastAPI</field>
<field name="description">Helps you manage your Fastapi Endpoints</field>
<field name="sequence">99</field>
</record>
<record id="group_fastapi_user" model="res.groups">
<field name="name">User</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
<field name="category_id" ref="module_category_fastapi" />
</record>
<record id="group_fastapi_manager" model="res.groups">
<field name="name">Administrator</field>
<field name="category_id" ref="module_category_fastapi" />
<field name="implied_ids" eval="[(4, ref('group_fastapi_user'))]" />
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
</record>
<!-- create a basic group providing the minimal access rights to retrieve
the user running the endpoint handlers and performs authentication -->
<record id="group_fastapi_endpoint_runner" model="res.groups">
<field name="name">FastAPI Endpoint Runner</field>
<field name="category_id" ref="module_category_fastapi" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
from . import test_fastapi
from . import test_fastapi_demo

View file

@ -0,0 +1,164 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import logging
from contextlib import contextmanager
from functools import partial
from typing import Any, Callable, Dict
from starlette import status
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from odoo.api import Environment
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
from odoo.addons.base.models.res_partner import Partner
from odoo.addons.base.models.res_users import Users
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from ..context import odoo_env_ctx
from ..dependencies import (
authenticated_partner_impl,
optionally_authenticated_partner_impl,
)
from ..error_handlers import convert_exception_to_status_body
_logger = logging.getLogger(__name__)
def default_exception_handler(request: Request, exc: Exception) -> Response:
"""
Default exception handler that returns a response with the exception details.
"""
status_code, body = convert_exception_to_status_body(exc)
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# In testing we want to see the exception details of 500 errors
_logger.error("[%d] Error occurred: %s", exc_info=exc)
return JSONResponse(
status_code=status_code,
content=body,
)
@tagged("post_install", "-at_install")
class FastAPITransactionCase(TransactionCase):
"""
This class is a base class for FastAPI tests.
It defines default values for the attributes used to create the test client.
The default values can be overridden by setting the corresponding class attributes.
Default attributes are:
- default_fastapi_app: the FastAPI app to use to create the test client
- default_fastapi_router: the FastAPI router to use to create the test client
- default_fastapi_odoo_env: the Odoo environment that will be used to run
the endpoint implementation
- default_fastapi_running_user: the user that will be used to run the endpoint
implementation
- default_fastapi_authenticated_partner: the partner that will be used to run
to build the authenticated_partner and authenticated_partner_env dependencies
- default_fastapi_dependency_overrides: a dict of dependency overrides that will
be applied to the app when creating the test client
The test client is created by calling the _create_test_client method. When
calling this method, the default values are used unless they are overridden by
passing the corresponding arguments.
Even if you can provide a default value for the default_fastapi_app and
default_fastapi_router attributes, you should always provide only one of them.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.default_fastapi_app: FastAPI | None = None
cls.default_fastapi_router: APIRouter | None = None
cls.default_fastapi_odoo_env: Environment = cls.env
cls.default_fastapi_running_user: Users | None = None
cls.default_fastapi_authenticated_partner: Partner | None = None
cls.default_fastapi_dependency_overrides: Dict[
Callable[..., Any], Callable[..., Any]
] = {}
@contextmanager
def _create_test_client(
self,
app: FastAPI | None = None,
router: APIRouter | None = None,
user: Users | None = None,
partner: Partner | None = None,
env: Environment = None,
dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = None,
raise_server_exceptions: bool = True,
testclient_kwargs=None,
):
"""
Create a test client for the given app or router.
This method is a context manager that yields the test client. It
ensures that the Odoo environment is properly set up when running
the endpoint implementation, and cleaned up after the test client is
closed.
Pay attention to the **'raise_server_exceptions'** argument. It's
default value is **True**. This means that if the endpoint implementation
raises an exception, the test client will raise it. That also means
that if you app includes specific exception handlers, they will not
be called. If you want to test your exception handlers, you should
set this argument to **False**. In this case, the test client will
not raise the exception, but will return it in the response and the
exception handlers will be called.
"""
env = env or self.default_fastapi_odoo_env
user = user or self.default_fastapi_running_user
dependencies = self.default_fastapi_dependency_overrides.copy()
if dependency_overrides:
dependencies.update(dependency_overrides)
if user:
env = env(user=user)
partner = (
partner
or self.default_fastapi_authenticated_partner
or self.env["res.partner"]
)
if partner and authenticated_partner_impl in dependencies:
raise ValueError(
"You cannot provide an override for the authenticated_partner_impl "
"dependency when creating a test client with a partner."
)
if partner or authenticated_partner_impl not in dependencies:
dependencies[authenticated_partner_impl] = partial(lambda a: a, partner)
if partner and optionally_authenticated_partner_impl in dependencies:
raise ValueError(
"You cannot provide an override for the optionally_authenticated_partner_impl "
"dependency when creating a test client with a partner."
)
if partner or optionally_authenticated_partner_impl not in dependencies:
dependencies[optionally_authenticated_partner_impl] = partial(
lambda a: a, partner
)
app = app or self.default_fastapi_app or FastAPI()
router = router or self.default_fastapi_router
if router:
app.include_router(router)
app.dependency_overrides = dependencies
if not raise_server_exceptions:
# Handle exceptions as in FastAPIDispatcher
app.exception_handlers.setdefault(Exception, default_exception_handler)
ctx_token = odoo_env_ctx.set(env)
testclient_kwargs = testclient_kwargs or {}
try:
yield TestClient(
app,
raise_server_exceptions=raise_server_exceptions,
**testclient_kwargs
)
finally:
odoo_env_ctx.reset(ctx_token)

View file

@ -0,0 +1,201 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import os
import unittest
from contextlib import contextmanager
from odoo import sql_db
from odoo.tests.common import HttpCase
from odoo.tools import mute_logger
from fastapi import status
from ..schemas import DemoExceptionType
@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "EndpointHttpCase skipped")
class FastAPIHttpCase(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo")
cls.fastapi_multi_demo_app = cls.env.ref(
"fastapi.fastapi_endpoint_multislash_demo"
)
cls.fastapi_apps = cls.fastapi_demo_app + cls.fastapi_multi_demo_app
cls.fastapi_apps._handle_registry_sync()
lang = (
cls.env["res.lang"]
.with_context(active_test=False)
.search([("code", "=", "fr_BE")])
)
lang.active = True
@contextmanager
def _mocked_commit(self):
with unittest.mock.patch.object(
sql_db.TestCursor, "commit", return_value=None
) as mocked_commit:
yield mocked_commit
def _assert_expected_lang(self, accept_language, expected_lang):
route = "/fastapi_demo/demo/lang"
response = self.url_open(route, headers={"Accept-language": accept_language})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, expected_lang)
def test_call(self):
route = "/fastapi_demo/demo/"
response = self.url_open(route)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b'{"Hello":"World"}')
def test_lang(self):
self._assert_expected_lang("fr,en;q=0.7,en-GB;q=0.3", b'"fr_BE"')
self._assert_expected_lang("en,fr;q=0.7,en-GB;q=0.3", b'"en_US"')
self._assert_expected_lang("fr-FR,en;q=0.7,en-GB;q=0.3", b'"fr_BE"')
self._assert_expected_lang("fr-FR;q=0.1,en;q=1.0,en-GB;q=0.8", b'"en_US"')
def test_retrying(self):
"""Test that the retrying mechanism is working as expected with the
FastAPI endpoints.
"""
nbr_retries = 3
route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}"
response = self.url_open(route, timeout=20)
self.assertEqual(response.status_code, 200)
self.assertEqual(int(response.content), nbr_retries)
def test_retrying_post(self):
"""Test that the retrying mechanism is working as expected with the
FastAPI endpoints in case of POST request with a file.
"""
nbr_retries = 3
route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}"
response = self.url_open(
route, timeout=20, files={"file": ("test.txt", b"test")}
)
self.assertEqual(response.status_code, 200)
self.assertDictEqual(response.json(), {"retries": nbr_retries, "file": "test"})
@mute_logger("odoo.http")
def assert_exception_processed(
self,
exception_type: DemoExceptionType,
error_message: str,
expected_message: str,
expected_status_code: int,
) -> None:
with self._mocked_commit() as mocked_commit:
route = (
"/fastapi_demo/demo/exception?"
f"exception_type={exception_type.value}&error_message={error_message}"
)
response = self.url_open(route, timeout=200)
mocked_commit.assert_not_called()
self.assertDictEqual(
response.json(),
{
"detail": expected_message,
},
)
self.assertEqual(response.status_code, expected_status_code)
def test_user_error(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.user_error,
error_message="test",
expected_message="test",
expected_status_code=status.HTTP_400_BAD_REQUEST,
)
def test_validation_error(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.validation_error,
error_message="test",
expected_message="test",
expected_status_code=status.HTTP_400_BAD_REQUEST,
)
def test_bare_exception(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.bare_exception,
error_message="test",
expected_message="Internal Server Error",
expected_status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
def test_access_error(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.access_error,
error_message="test",
expected_message="AccessError",
expected_status_code=status.HTTP_403_FORBIDDEN,
)
def test_missing_error(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.missing_error,
error_message="test",
expected_message="MissingError",
expected_status_code=status.HTTP_404_NOT_FOUND,
)
def test_http_exception(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.http_exception,
error_message="test",
expected_message="test",
expected_status_code=status.HTTP_409_CONFLICT,
)
@mute_logger("odoo.http")
def test_request_validation_error(self) -> None:
with self._mocked_commit() as mocked_commit:
route = "/fastapi_demo/demo/exception?exception_type=BAD&error_message="
response = self.url_open(route, timeout=200)
mocked_commit.assert_not_called()
self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
def test_no_commit_on_exception(self) -> None:
# this test check that the way we mock the cursor is working as expected
# and that the transaction is rolled back in case of exception.
with self._mocked_commit() as mocked_commit:
url = "/fastapi_demo/demo"
response = self.url_open(url, timeout=600)
self.assertEqual(response.status_code, 200)
mocked_commit.assert_called_once()
self.assert_exception_processed(
exception_type=DemoExceptionType.http_exception,
error_message="test",
expected_message="test",
expected_status_code=status.HTTP_409_CONFLICT,
)
def test_url_matching(self):
# Test the URL mathing method on the endpoint
paths = ["/fastapi", "/fastapi_demo", "/fastapi/v1"]
EndPoint = self.env["fastapi.endpoint"]
self.assertEqual(
EndPoint._find_first_matching_url_path(paths, "/fastapi_demo/test"),
"/fastapi_demo",
)
self.assertEqual(
EndPoint._find_first_matching_url_path(paths, "/fastapi/test"), "/fastapi"
)
self.assertEqual(
EndPoint._find_first_matching_url_path(paths, "/fastapi/v2/test"),
"/fastapi",
)
self.assertEqual(
EndPoint._find_first_matching_url_path(paths, "/fastapi/v1/test"),
"/fastapi/v1",
)
def test_multi_slash(self):
route = "/fastapi/demo-multi/demo/"
response = self.url_open(route, timeout=20)
self.assertEqual(response.status_code, 200)
self.assertIn(self.fastapi_multi_demo_app.root_path, str(response.url))

View file

@ -0,0 +1,111 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
from functools import partial
from requests import Response
from odoo.exceptions import UserError
from odoo.tools.misc import mute_logger
from fastapi import status
from ..dependencies import fastapi_endpoint
from ..routers import demo_router
from ..schemas import DemoEndpointAppInfo, DemoExceptionType
from .common import FastAPITransactionCase
class FastAPIDemoCase(FastAPITransactionCase):
"""The fastapi lib comes with a useful testclient that let's you
easily test your endpoints. Moreover, the dependency overrides functionality
allows you to provide specific implementation for part of the code to avoid
to rely on some tricky http stuff for example: authentication
This test class is an example on how you can test your own code
"""
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.default_fastapi_router = demo_router
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() as test_client:
response: Response = test_client.get("/demo/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(response.json(), {"Hello": "World"})
def test_who_ami(self) -> None:
with self._create_test_client() as test_client:
response: Response = test_client.get("/demo/who_ami")
self.assertEqual(response.status_code, status.HTTP_200_OK)
partner = self.default_fastapi_authenticated_partner
self.assertDictEqual(
response.json(),
{
"name": partner.name,
"display_name": partner.display_name,
},
)
def test_endpoint_info(self) -> None:
demo_app = self.env.ref("fastapi.fastapi_endpoint_demo")
with self._create_test_client(
dependency_overrides={fastapi_endpoint: partial(lambda a: a, demo_app)}
) as test_client:
response: Response = test_client.get("/demo/endpoint_app_info")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(
response.json(),
DemoEndpointAppInfo.model_validate(demo_app).model_dump(by_alias=True),
)
def test_exception_raised(self) -> None:
with self.assertRaisesRegex(UserError, "User Error"):
with self._create_test_client() as test_client:
test_client.get(
"/demo/exception",
params={
"exception_type": DemoExceptionType.user_error.value,
"error_message": "User Error",
},
)
with self.assertRaisesRegex(NotImplementedError, "Bare Exception"):
with self._create_test_client() as test_client:
test_client.get(
"/demo/exception",
params={
"exception_type": DemoExceptionType.bare_exception.value,
"error_message": "Bare Exception",
},
)
@mute_logger("odoo.addons.fastapi.tests.common")
def test_exception_not_raised(self) -> None:
with self._create_test_client(raise_server_exceptions=False) as test_client:
response: Response = test_client.get(
"/demo/exception",
params={
"exception_type": DemoExceptionType.user_error.value,
"error_message": "User Error",
},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictEqual(response.json(), {"detail": "User Error"})
with self._create_test_client(raise_server_exceptions=False) as test_client:
response: Response = test_client.get(
"/demo/exception",
params={
"exception_type": DemoExceptionType.bare_exception.value,
"error_message": "Bare Exception",
},
)
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertDictEqual(response.json(), {"detail": "Internal Server Error"})

View file

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 ACSONE SA/NV
License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -->
<odoo>
<record model="ir.ui.view" id="fastapi_endpoint_form_view">
<field name="name">fastapi.endpoint.form (in fastapi)</field>
<field name="model">fastapi.endpoint</field>
<field name="arch" type="xml">
<form>
<header>
</header>
<sheet>
<field name="active" invisible="1" />
<field name="registry_sync" invisible="1" />
<div class="oe_button_box" name="button_box">
<button
name="action_sync_registry"
string="Sync Registry"
type="object"
class="oe_highlight"
icon="fa-refresh"
attrs="{'invisible': [('registry_sync', '=', True)]}"
/>
</div>
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"
/>
<widget
name="web_ribbon"
title="Registry Sync Required"
bg_color="bg-warning"
attrs="{'invisible': [('registry_sync', '=', True)]}"
/>
<div class="oe_title">
<label for="name" />
<h1><field name="name" /></h1>
</div>
<group>
<group>
<field name="app" />
<field name="root_path" />
<field name="user_id" />
<field name="company_id" />
<field name="description" />
<field name="save_http_session" />
</group>
<group name="resoures">
<field name="docs_url" widget="url" />
<field name="redoc_url" widget="url" />
<field name="openapi_url" widget="url" />
</group>
</group>
<span name="configuration" />
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="fastapi_endpoint_search_view">
<field name="name">fastapi.endpoint.search (in fastapi)</field>
<field name="model">fastapi.endpoint</field>
<field name="arch" type="xml">
<search>
<field name="name" />
<field name="app" />
<filter
string="Archived"
name="inactive"
domain="[('active','=',False)]"
/>
<group expand='0' string='Group by...'>
<filter
string='App'
name="groupby_app"
context="{'group_by': 'app'}"
/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="fastapi_endpoint_tree_view">
<field name="name">fastapi.endpoint.tree (in fastapi)</field>
<field name="model">fastapi.endpoint</field>
<field name="arch" type="xml">
<tree decoration-warning="not registry_sync" decoration-muted="not active">
<field name="active" invisible="1" />
<field name="registry_sync" invisible="1" />
<field name="name" />
<field name="app" />
<field name="root_path" />
<field name="docs_url" widget="url" />
<field name="redoc_url" widget="url" />
<field name="openapi_url" widget="url" />
<button
name="action_sync_registry"
string="Sync Registry"
type="object"
class="oe_highlight"
icon="fa-refresh"
attrs="{'invisible': [('registry_sync', '=', True)]}"
/>
</tree>
</field>
</record>
<record model="ir.actions.server" id="fastapi_endpoint_action_sync_registry">
<field name="name">Sync Registry</field>
<field name="model_id" ref="model_fastapi_endpoint" />
<field name="binding_model_id" ref="model_fastapi_endpoint" />
<field name="binding_type">action</field>
<field name="state">code</field>
<field name="code">
records.action_sync_registry()
</field>
</record>
<record model="ir.actions.act_window" id="fastapi_endpoint_act_window">
<field name="name">FastAPI Endpoint</field>
<field name="res_model">fastapi.endpoint</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="fastapi_endpoint_menu">
<field name="name">FastAPI Endpoint</field>
<field name="parent_id" ref="menu_fastapi_root" />
<field name="action" ref="fastapi_endpoint_act_window" />
<field name="sequence" eval="16" />
</record>
</odoo>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 ACSONE SA/NV
License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -->
<odoo>
<record model="ir.ui.view" id="fastapi_endpoint_demo_form_view">
<field name="name">fastapi.endpoint.demo.form (in fastapi)</field>
<field name="model">fastapi.endpoint</field>
<field name="inherit_id" ref="fastapi_endpoint_form_view" />
<field name="arch" type="xml">
<span name="configuration" position="after">
<group
name="demo_app_configuration"
title="Configuration"
attrs="{'invisible': [('app', '!=', 'demo')]}"
>
<field name="demo_auth_method" />
</group>
</span>
</field>
</record>
</odoo>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 ACSONE SA/NV
License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -->
<odoo>
<record id="menu_fastapi_root" model="ir.ui.menu">
<field name="name">FastAPI</field>
<field name="sequence" eval="400" />
<field name="web_icon">fastapi,static/description/icon.png</field>
<field name="groups_id" eval="[(4, ref('group_fastapi_user'))]" />
</record>
</odoo>

View file

@ -0,0 +1,43 @@
[project]
name = "odoo-bringout-oca-rest-framework-fastapi"
version = "16.0.0"
description = "Odoo FastAPI -
Odoo FastAPI endpoint"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-rest-framework-endpoint_route_handler>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["fastapi"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]