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,47 @@
# Extendable Fastapi
Odoo addon: extendable_fastapi
## Installation
```bash
pip install odoo-bringout-oca-rest-framework-extendable_fastapi
```
## Dependencies
This addon depends on:
- fastapi
- extendable
## Manifest Information
- **Name**: Extendable Fastapi
- **Version**: 16.0.2.1.2
- **Category**: N/A
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/rest-framework](https://github.com/OCA/rest-framework) branch 16.0, addon `extendable_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 Extendable_fastapi Module - extendable_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 extendable_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,6 @@
# Dependencies
This addon depends on:
- [fastapi](../../odoo-bringout-oca-rest-framework-fastapi)
- [extendable](../../odoo-bringout-oca-rest-framework-extendable)

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 extendable_fastapi or install in UI.

View file

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

View file

@ -0,0 +1,12 @@
# Models
Detected core models and extensions in extendable_fastapi.
```mermaid
classDiagram
class fastapi_endpoint
```
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: extendable_fastapi. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon extendable_fastapi
- License: LGPL-3

View file

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

View file

@ -0,0 +1,8 @@
# Security
This module does not define custom security rules or access controls beyond Odoo defaults.
Default Odoo security applies:
- Base user access through standard groups
- Model access inherited from dependencies
- No custom row-level security rules

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 extendable_fastapi
```

View file

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

View file

@ -0,0 +1,113 @@
==================
Extendable Fastapi
==================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:1c3abf7259ae8390271785fb3b8f7754dadf175dc1530a27386ae9a77635e4b2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github
:target: https://github.com/OCA/rest-framework/tree/16.0/extendable_fastapi
:alt: OCA/rest-framework
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-extendable_fastapi
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This addon is a technical addon used to allows the use of
`extendable <https://pypi.org/project/extendable/>`_
classes in the implementation of your fastapi endpoint handlers. It also
allows you to use `extendable_pydantic <https://pypi.org/project/extendable_pydantic/>`_
models when defining your endpoint handlers request and response models.
**Table of contents**
.. contents::
:local:
Changelog
=========
16.0.2.1.1 (2023-11-07)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Fix registry corruption when running tests difined in a class inheriting of the *FastAPITransactionCase* class if an error occurs in the *setUpClass* after the call to super(). (`#392 <https://github.com/OCA/rest-framework/issues/392>`_)
16.0.2.1.0 (2023-10-13)
~~~~~~~~~~~~~~~~~~~~~~~
**Features**
- * New base schemas: *PagedCollection*. This schema is used to define the
the structure of a paged collection of resources. This schema is similar
to the ones defined in the Odoo's **fastapi** addon but works as/with
extendable models.
* The *StrictExtendableBaseModel* has been moved to the *extendable_pydantic*
python lib. You should consider to import it from there. (`#380 <https://github.com/OCA/rest-framework/issues/380>`_)
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20extendable_fastapi%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* ACSONE SA/NV
Contributors
~~~~~~~~~~~~
* Laurent Mignon <laurent.mignon@acsone.eu> (https://acsone.eu)
* Marie Lejeune <marie.lejeune@acsone.eu> (https://acsone.eu)
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px
:target: https://github.com/lmignon
:alt: lmignon
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-lmignon|
This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/extendable_fastapi>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,2 @@
from . import fastapi_dispatcher
from .schemas import StrictExtendableBaseModel

View file

@ -0,0 +1,23 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
{
"name": "Extendable Fastapi",
"summary": """
Allows the use of extendable into fastapi apps""",
"version": "16.0.2.1.2",
"license": "LGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"maintainers": ["lmignon"],
"website": "https://github.com/OCA/rest-framework",
"depends": ["fastapi", "extendable"],
"data": [],
"demo": [],
"external_dependencies": {
"python": [
"pydantic>=2.0.0",
"extendable-pydantic>=1.2.0",
],
},
"installable": True,
}

View file

@ -0,0 +1,34 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
from contextlib import contextmanager
from odoo.http import _dispatchers
from odoo.addons.extendable.registry import _extendable_registries_database
from odoo.addons.fastapi.fastapi_dispatcher import (
FastApiDispatcher as BaseFastApiDispatcher,
)
from extendable import context
# Inherit from last registered fastapi dispatcher
# This handles multiple overload of dispatchers
class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)):
routing_type = "fastapi"
def dispatch(self, endpoint, args):
with self._manage_extendable_context():
return super().dispatch(endpoint, args)
@contextmanager
def _manage_extendable_context(self):
env = self.request.env
registry = _extendable_registries_database.get(env.cr.dbname, {})
token = context.extendable_registry.set(registry)
try:
response = yield
finally:
context.extendable_registry.reset(token)
return response

View file

@ -0,0 +1,13 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
#
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"

View file

@ -0,0 +1,14 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Automatically generated\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"

View file

@ -0,0 +1 @@
from . import fastapi_endpoint_demo

View file

@ -0,0 +1,20 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
from fastapi import APIRouter
from ..tests.routers import demo_pydantic_router
class FastapiEndpoint(models.Model):
_inherit = "fastapi.endpoint"
def _get_fastapi_routers(self) -> list[APIRouter]:
# Add router defined for tests to the demo app
routers = super()._get_fastapi_routers()
if self.app == "demo":
routers.append(demo_pydantic_router)
return routers

View file

@ -0,0 +1,2 @@
* Laurent Mignon <laurent.mignon@acsone.eu> (https://acsone.eu)
* Marie Lejeune <marie.lejeune@acsone.eu> (https://acsone.eu)

View file

@ -0,0 +1,5 @@
This addon is a technical addon used to allows the use of
`extendable <https://pypi.org/project/extendable/>`_
classes in the implementation of your fastapi endpoint handlers. It also
allows you to use `extendable_pydantic <https://pypi.org/project/extendable_pydantic/>`_
models when defining your endpoint handlers request and response models.

View file

@ -0,0 +1,20 @@
16.0.2.1.1 (2023-11-07)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Fix registry corruption when running tests difined in a class inheriting of the *FastAPITransactionCase* class if an error occurs in the *setUpClass* after the call to super(). (`#392 <https://github.com/OCA/rest-framework/issues/392>`_)
16.0.2.1.0 (2023-10-13)
~~~~~~~~~~~~~~~~~~~~~~~
**Features**
- * New base schemas: *PagedCollection*. This schema is used to define the
the structure of a paged collection of resources. This schema is similar
to the ones defined in the Odoo's **fastapi** addon but works as/with
extendable models.
* The *StrictExtendableBaseModel* has been moved to the *extendable_pydantic*
python lib. You should consider to import it from there. (`#380 <https://github.com/OCA/rest-framework/issues/380>`_)

View file

@ -0,0 +1,19 @@
from typing import Annotated, Generic, TypeVar
from extendable_pydantic import StrictExtendableBaseModel
from pydantic import Field
T = TypeVar("T")
class PagedCollection(StrictExtendableBaseModel, Generic[T]):
"""A paged collection of items"""
# This is a generic model. The type of the items is defined by the generic type T.
# It provides you a common way to return a paged collection of items of
# extendable models. It's based on the StrictExtendableBaseModel to ensure
# a strict validation when used within the odoo fastapi framework.
count: Annotated[int, Field(..., description="The count of items into the system")]
items: list[T]

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,460 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Extendable Fastapi</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="extendable-fastapi">
<h1 class="title">Extendable Fastapi</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:1c3abf7259ae8390271785fb3b8f7754dadf175dc1530a27386ae9a77635e4b2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/rest-framework/tree/16.0/extendable_fastapi"><img alt="OCA/rest-framework" src="https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-extendable_fastapi"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This addon is a technical addon used to allows the use of
<a class="reference external" href="https://pypi.org/project/extendable/">extendable</a>
classes in the implementation of your fastapi endpoint handlers. It also
allows you to use <a class="reference external" href="https://pypi.org/project/extendable_pydantic/">extendable_pydantic</a>
models when defining your endpoint handlers request and response models.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#changelog" id="toc-entry-1">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-2">16.0.2.1.1 (2023-11-07)</a></li>
<li><a class="reference internal" href="#section-2" id="toc-entry-3">16.0.2.1.0 (2023-10-13)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-1">Changelog</a></h1>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-2">16.0.2.1.1 (2023-11-07)</a></h2>
<p><strong>Bugfixes</strong></p>
<ul class="simple">
<li>Fix registry corruption when running tests difined in a class inheriting of the <em>FastAPITransactionCase</em> class if an error occurs in the <em>setUpClass</em> after the call to super(). (<a class="reference external" href="https://github.com/OCA/rest-framework/issues/392">#392</a>)</li>
</ul>
</div>
<div class="section" id="section-2">
<h2><a class="toc-backref" href="#toc-entry-3">16.0.2.1.0 (2023-10-13)</a></h2>
<p><strong>Features</strong></p>
<ul class="simple">
<li><ul class="first">
<li>New base schemas: <em>PagedCollection</em>. This schema is used to define the
the structure of a paged collection of resources. This schema is similar
to the ones defined in the Odoos <strong>fastapi</strong> addon but works as/with
extendable models.</li>
<li>The <em>StrictExtendableBaseModel</em> has been moved to the <em>extendable_pydantic</em>
python lib. You should consider to import it from there. (<a class="reference external" href="https://github.com/OCA/rest-framework/issues/380">#380</a>)</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/rest-framework/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/rest-framework/issues/new?body=module:%20extendable_fastapi%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-5">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-6">Authors</a></h2>
<ul class="simple">
<li>ACSONE SA/NV</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-7">Contributors</a></h2>
<ul class="simple">
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt; (<a class="reference external" href="https://acsone.eu">https://acsone.eu</a>)</li>
<li>Marie Lejeune &lt;<a class="reference external" href="mailto:marie.lejeune&#64;acsone.eu">marie.lejeune&#64;acsone.eu</a>&gt; (<a class="reference external" href="https://acsone.eu">https://acsone.eu</a>)</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/lmignon"><img alt="lmignon" src="https://github.com/lmignon.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rest-framework/tree/16.0/extendable_fastapi">OCA/rest-framework</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,2 @@
from . import test_generic_extendable
from . import test_strict_extendable_base_model

View file

@ -0,0 +1,17 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
from odoo.addons.extendable.tests.common import ExtendableMixin
from odoo.addons.fastapi.tests.common import (
FastAPITransactionCase as BaseFastAPITransactionCase,
)
class FastAPITransactionCase(BaseFastAPITransactionCase, ExtendableMixin):
"""Base class for FastAPI tests using extendable classes."""
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.init_extendable_registry()
cls.addClassCleanup(cls.reset_extendable_registry)

View file

@ -0,0 +1,100 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from typing import Annotated
from odoo import api
from odoo.addons.fastapi.dependencies import odoo_env
from fastapi import APIRouter, Depends
from .schemas import Customer, PrivateCustomer, PrivateUser, User, UserSearchResponse
demo_pydantic_router = APIRouter(tags=["demo_pydantic"])
@demo_pydantic_router.get("/{user_id}")
def get(env: Annotated[api.Environment, Depends(odoo_env)], user_id: int) -> User:
"""
Get a specific user using its Odoo id.
"""
user = env["res.users"].sudo().search([("id", "=", user_id)])
if not user:
raise ValueError("No user found")
return User.from_user(user)
@demo_pydantic_router.get("/private/{user_id}")
def get_private(
env: Annotated[api.Environment, Depends(odoo_env)], user_id: int
) -> User:
"""
Get a specific user using its Odoo id.
"""
user = env["res.users"].sudo().search([("id", "=", user_id)])
if not user:
raise ValueError("No user found")
return PrivateUser.from_user(user)
@demo_pydantic_router.post("/post_user")
def post_user(user: User) -> UserSearchResponse:
"""A demo endpoint to test the extendable pydantic model integration
with fastapi and odoo.
Type of the request body is User. This model is the base model
of ExtendedUser. At runtime, the documentation and the processing
of the request body should be done as if the type of the request body
was ExtendedUser.
"""
return UserSearchResponse(total=1, items=[user])
@demo_pydantic_router.post("/post_private_user")
def post_private_user(user: PrivateUser) -> User:
"""A demo endpoint to test the extendable pydantic model integration
with fastapi and odoo.
Type of the request body is PrivateUser. This model inherits base model
User but does not extend it.
This method will return attributes from the declared response type.
It will never return attribute of a derived type from the declared response
type, even if in the route implementation we return an instance of the
derived type.
"""
return user
@demo_pydantic_router.post("/post_private_user_generic")
def post_private_user_generic(user: PrivateUser) -> UserSearchResponse:
"""A demo endpoint to test the extendable pydantic model integration
with fastapi and odoo.
Type of the request body is PrivateUser. This model inherits base model
User but does not extend it.
This method will return attributes from the declared response type.
It will never return attribute of a derived type from the declared response
type, even if in the route implementation we return an instance of the
derived type. This assertion is also true with generics.
"""
return UserSearchResponse(total=1, items=[user])
@demo_pydantic_router.post("/post_private_customer")
def post_private_customer(customer: PrivateCustomer) -> Customer:
"""A demo endpoint to test the extendable pydantic model integration
with fastapi and odoo, and more particularly the extra="forbid" config parameter.
Type of the request body is PrivateCustomer. This model inherits base model
Customer but does not extend it.
This method will return attributes from the declared response type.
It will never return attribute of a derived type from the declared response
type, even if in the route implementation we return an instance of the
derived type.
Since Customer has extra fields forbidden, this route is not supposed to work.
"""
return customer

View file

@ -0,0 +1,94 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from typing import Generic, TypeVar
from extendable_pydantic import ExtendableBaseModel
# User models
class User(ExtendableBaseModel, revalidate_instances="always"):
"""
We MUST set revalidate_instances="always" to be sure that FastAPI validates
responses of this model type.
"""
name: str
@classmethod
def from_user(cls, user):
return cls.model_construct(name=user.name)
class ExtendedUser(User, extends=True):
address: str
@classmethod
def from_user(cls, user):
res = super().from_user(user)
if user.street or user.city:
# Dummy address construction
res.address = (user.street or "") + (user.city or "")
return res
class PrivateUser(User):
password: str
@classmethod
def from_user(cls, user):
res = super().from_user(user)
res.password = user.password
return res
_T = TypeVar("_T")
class SearchResponse(ExtendableBaseModel, Generic[_T]):
total: int
items: list[_T]
class UserSearchResponse(SearchResponse[User]):
"""We declare the generic type of the items of the list as User
which is the base model of the extended. When used, it should be resolved
to ExtendedUser, but items of PrivateUser class must stay private and not be returned"""
# Customer models: same as above but with extra="forbid"
class Customer(ExtendableBaseModel, revalidate_instances="always", extra="forbid"):
"""
Same hierarchy as User models, but with an extra config parameter:
forbid extra fields.
"""
name: str
@classmethod
def from_customer(cls, customer):
return cls.model_construct(name=customer.name)
class ExtendedCustomer(Customer, extends=True):
address: str
@classmethod
def from_customer(cls, customer):
res = super().from_customer(customer)
if customer.street or customer.city:
# Dummy address construction
res.address = (customer.street or "") + (customer.city or "")
return res
class PrivateCustomer(Customer):
password: str
@classmethod
def from_customer(cls, customer):
res = super().from_customer(customer)
res.password = customer.password
return res

View file

@ -0,0 +1,208 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from requests import Response
from odoo.tests.common import tagged
from fastapi.exceptions import ResponseValidationError
from .common import FastAPITransactionCase
from .routers import demo_pydantic_router
from .schemas import PrivateCustomer, PrivateUser, User
@tagged("post_install", "-at_install")
class TestUser(FastAPITransactionCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
def test_app_components(self):
with self._create_test_client(router=demo_pydantic_router) as test_client:
to_openapi = test_client.app.openapi()
# Check post input and output types
self.assertEqual(
to_openapi["paths"]["/post_user"]["post"]["requestBody"]["content"][
"application/json"
]["schema"]["$ref"],
"#/components/schemas/User",
)
self.assertEqual(
to_openapi["paths"]["/post_user"]["post"]["responses"]["200"][
"content"
]["application/json"]["schema"]["$ref"],
"#/components/schemas/UserSearchResponse",
)
self.assertEqual(
to_openapi["paths"]["/post_private_user"]["post"]["requestBody"][
"content"
]["application/json"]["schema"]["$ref"],
"#/components/schemas/PrivateUser",
)
self.assertEqual(
to_openapi["paths"]["/post_private_user"]["post"]["responses"]["200"][
"content"
]["application/json"]["schema"]["$ref"],
"#/components/schemas/User",
)
self.assertEqual(
to_openapi["paths"]["/post_private_user_generic"]["post"][
"requestBody"
]["content"]["application/json"]["schema"]["$ref"],
"#/components/schemas/PrivateUser",
)
self.assertEqual(
to_openapi["paths"]["/post_private_user_generic"]["post"]["responses"][
"200"
]["content"]["application/json"]["schema"]["$ref"],
"#/components/schemas/UserSearchResponse",
)
# Check Pydantic model extension
self.assertEqual(
set(to_openapi["components"]["schemas"]["User"]["properties"].keys()),
{"name", "address"},
)
self.assertEqual(
set(
to_openapi["components"]["schemas"]["PrivateUser"][
"properties"
].keys()
),
{"name", "address", "password"},
)
self.assertEqual(
to_openapi["components"]["schemas"]["UserSearchResponse"]["properties"][
"items"
]["items"]["$ref"],
"#/components/schemas/User",
)
def test_post_user(self):
name = "Jean Dupont"
address = "Rue du Puits 12, 4000 Liège"
pydantic_data = User(name=name, address=address)
# Assert that class was correctly extended
self.assertTrue(pydantic_data.address)
with self._create_test_client(router=demo_pydantic_router) as test_client:
response: Response = test_client.post(
"/post_user", content=pydantic_data.model_dump_json()
)
self.assertEqual(response.status_code, 200)
res = response.json()
self.assertEqual(res["total"], 1)
user = res["items"][0]
self.assertEqual(user["name"], name)
self.assertEqual(user["address"], address)
self.assertFalse("password" in user.keys())
def test_post_private_user(self):
"""
/post_private_user return attributes from User, but not PrivateUser
Security check: this method should never return attributes from
derived type PrivateUser, even thought a PrivateUser object
is given as input.
"""
name = "Jean Dupont"
address = "Rue du Puits 12, 4000 Liège"
password = "dummy123"
pydantic_data = PrivateUser(name=name, address=address, password=password)
# Assert that class was correctly extended
self.assertTrue(pydantic_data.address)
self.assertTrue(pydantic_data.password)
with self._create_test_client(router=demo_pydantic_router) as test_client:
response: Response = test_client.post(
"/post_private_user", content=pydantic_data.model_dump_json()
)
self.assertEqual(response.status_code, 200)
user = response.json()
self.assertEqual(user["name"], name)
self.assertEqual(user["address"], address)
# Private attrs were not returned
self.assertFalse("password" in user.keys())
def test_post_private_user_generic(self):
"""
/post_private_user_generic return attributes from User, but not PrivateUser
Security check: this method should never return attributes from
derived type PrivateUser, even thought a PrivateUser object
is given as input.
This test is specifically made to test this assertion with generics.
"""
name = "Jean Dupont"
address = "Rue du Puits 12, 4000 Liège"
password = "dummy123"
pydantic_data = PrivateUser(name=name, address=address, password=password)
# Assert that class was correctly extended
self.assertTrue(pydantic_data.address)
self.assertTrue(pydantic_data.password)
with self._create_test_client(router=demo_pydantic_router) as test_client:
response: Response = test_client.post(
"/post_private_user_generic", content=pydantic_data.model_dump_json()
)
self.assertEqual(response.status_code, 200)
res = response.json()
self.assertEqual(res["total"], 1)
user = res["items"][0]
self.assertEqual(user["name"], name)
self.assertEqual(user["address"], address)
# Private attrs were not returned
self.assertFalse("password" in user.keys())
def test_get_user_failed_no_address(self):
"""
Try to get a specific user but having no address
-> Error because address is a required field on User (extended) class
:return:
"""
user = self.env["res.users"].create(
{
"name": "Michel Dupont",
"login": "michel",
}
)
with self._create_test_client(
router=demo_pydantic_router
) as test_client, self.assertRaises(ResponseValidationError):
test_client.get(f"/{user.id}")
def test_get_user_failed_no_pwd(self):
"""
Try to get a specific user having an address but no password.
-> No error because return type is User, not PrivateUser
:return:
"""
user = self.env["res.users"].create(
{
"name": "Michel Dupont",
"login": "michel",
"street": "Rue du Moulin",
}
)
self.assertFalse(user.password)
with self._create_test_client(router=demo_pydantic_router) as test_client:
response: Response = test_client.get(f"/private/{user.id}")
self.assertEqual(response.status_code, 200)
def test_extra_forbid_response_fails(self):
"""
If adding extra="forbid" to the User model, we cannot write
a router with a response type = User and returning PrivateUser
in the code
"""
name = "Jean Dupont"
address = "Rue du Puits 12, 4000 Liège"
password = "dummy123"
pydantic_data = PrivateCustomer(name=name, address=address, password=password)
with self.assertRaises(ResponseValidationError), self._create_test_client(
router=demo_pydantic_router
) as test_client:
test_client.post(
"/post_private_customer", content=pydantic_data.model_dump_json()
)

View file

@ -0,0 +1,63 @@
import warnings
from datetime import date
from extendable_pydantic import ExtendableBaseModel
from pydantic import ValidationError
from ..schemas import StrictExtendableBaseModel
from .common import FastAPITransactionCase
class TestStrictExtendableBaseModel(FastAPITransactionCase):
class Model(ExtendableBaseModel):
x: int
d: date | None
class StrictModel(StrictExtendableBaseModel):
x: int
d: date | None
def test_Model_revalidate_instance_never(self):
# Missing required fields but no re-validation
m = self.Model.model_construct()
self.assertEqual(m.model_validate(m).model_dump(), {})
def test_StrictModel_revalidate_instance_always(self):
# Missing required fields and always revalidate
m = self.StrictModel.model_construct()
with self.assertRaises(ValidationError):
m.model_validate(m)
def test_Model_validate_assignment_false(self):
# Wrong assignment but no re-validation at assignment
m = self.Model(x=1, d=None)
m.x = "TEST"
with warnings.catch_warnings():
# Disable 'Expected `int` but got `str`' warning
warnings.simplefilter("ignore")
self.assertEqual(m.model_dump(), {"x": "TEST", "d": None})
def test_StrictModel_validate_assignment_true(self):
# Wrong assignment and validation at assignment
m = self.StrictModel.model_construct()
m.x = 1 # Validate only this field -> OK even if m.d is not set
with self.assertRaises(ValidationError):
m.x = "TEST"
def test_Model_extra_ignored(self):
# Ignore extra fields
m = self.Model(x=1, z=3, d=None)
self.assertEqual(m.model_dump(), {"x": 1, "d": None})
def test_StrictModel_extra_forbidden(self):
# Forbid extra fields
with self.assertRaises(ValidationError):
self.StrictModel(x=1, z=3, d=None)
def test_StrictModel_strict_false(self):
# Coerce str->date is allowed to enable coercion from JSON
# by FastAPI
m = self.StrictModel(x=1, d=None)
m.d = "2023-01-01"
self.assertTrue(m.model_validate(m))

View file

@ -0,0 +1,44 @@
[project]
name = "odoo-bringout-oca-rest-framework-extendable_fastapi"
version = "16.0.0"
description = "Extendable Fastapi -
Allows the use of extendable into fastapi apps"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-rest-framework-fastapi>=16.0.0",
"odoo-bringout-oca-rest-framework-extendable>=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 = ["extendable_fastapi"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]