Skip to content

Commit e9d5706

Browse files
committed
[ADD] fastapi_dynamic_app
1 parent 3c7dfb2 commit e9d5706

File tree

14 files changed

+999
-0
lines changed

14 files changed

+999
-0
lines changed

fastapi_dynamic_app/README.rst

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
===================
2+
Fastapi Dynamic App
3+
===================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:08b7e9a66c3ba8a25c56b2487ecc3de272ba577b731be907dcef198c2e5789f3
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github
20+
:target: https://github.com/OCA/rest-framework/tree/18.0/fastapi_dynamic_app
21+
:alt: OCA/rest-framework
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/rest-framework-18-0/rest-framework-18-0-fastapi_dynamic_app
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=18.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module allows to configure fastapi endpoints directly from the odoo
32+
interface. It makes it possible to manage the endpoint routers and their
33+
options, such as the prefix and the authentication method.
34+
35+
It also provide some demo authentication methods.
36+
37+
**Table of contents**
38+
39+
.. contents::
40+
:local:
41+
42+
Usage
43+
=====
44+
45+
Create a FastAPI endpoint and select the Dynamic app for it.
46+
47+
You can now configure its mounted routers in the Routers field and the
48+
authentication method in the Authentication Method field.
49+
50+
A list of the chosen routers will appear in the Dynamic Routers tab.
51+
There you can configure the routers' options, such as the prefix and the
52+
authentication method.
53+
54+
If a router is configured with a prefix, let's say the cart router, it
55+
will be mounted in the app with the prefix unless the authentication
56+
method is set, in which case a sub app will be created for all router
57+
with this prefix.
58+
59+
Bug Tracker
60+
===========
61+
62+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/issues>`_.
63+
In case of trouble, please check there if your issue has already been reported.
64+
If you spotted it first, help us to smash it by providing a detailed and welcomed
65+
`feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20fastapi_dynamic_app%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
66+
67+
Do not contact contributors directly about support or help with technical issues.
68+
69+
Credits
70+
=======
71+
72+
Authors
73+
-------
74+
75+
* Akretion
76+
77+
Contributors
78+
------------
79+
80+
- Florian Mounier florian.mounier@akretion.com
81+
82+
Maintainers
83+
-----------
84+
85+
This module is maintained by the OCA.
86+
87+
.. image:: https://odoo-community.org/logo.png
88+
:alt: Odoo Community Association
89+
:target: https://odoo-community.org
90+
91+
OCA, or the Odoo Community Association, is a nonprofit organization whose
92+
mission is to support the collaborative development of Odoo features and
93+
promote its widespread use.
94+
95+
This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/18.0/fastapi_dynamic_app>`_ project on GitHub.
96+
97+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

fastapi_dynamic_app/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
{
6+
"name": "Fastapi Dynamic App",
7+
"summary": "A Fastapi App That Provides Dynamic Router Configuration",
8+
"version": "18.0.1.0.0",
9+
"license": "AGPL-3",
10+
"author": "Akretion,Odoo Community Association (OCA)",
11+
"website": "https://github.com/OCA/rest-framework",
12+
"depends": ["fastapi"],
13+
"data": [
14+
"security/fastapi_router_security.xml",
15+
"views/fastapi_endpoint_views.xml",
16+
],
17+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import fastapi_endpoint
2+
from . import fastapi_router
3+
from . import fastapi_endpoint_router_option
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
from collections.abc import Callable
5+
from itertools import groupby
6+
from typing import Annotated, Any
7+
8+
from odoo import Command, api, fields, models
9+
10+
from odoo.addons.base.models.res_partner import Partner
11+
from odoo.addons.fastapi.dependencies import (
12+
authenticated_partner_from_basic_auth_user,
13+
authenticated_partner_impl,
14+
odoo_env,
15+
)
16+
from odoo.addons.fastapi.models.fastapi_endpoint_demo import (
17+
api_key_based_authenticated_partner_impl,
18+
)
19+
20+
from fastapi import APIRouter, Depends, FastAPI
21+
22+
23+
class FastapiEndpoint(models.Model):
24+
_inherit = "fastapi.endpoint"
25+
26+
app: str = fields.Selection(
27+
selection_add=[("dynamic", "Dynamic Endpoint")], ondelete={"dynamic": "cascade"}
28+
)
29+
router_ids = fields.Many2many(
30+
"fastapi.router",
31+
string="Routers",
32+
help="The routers to use on this endpoint.",
33+
)
34+
35+
router_option_ids = fields.One2many(
36+
"fastapi.endpoint.router.option",
37+
"endpoint_id",
38+
compute="_compute_router_option_ids",
39+
store=True,
40+
readonly=False,
41+
string="Router Options",
42+
help="The options to use on the routers.",
43+
)
44+
45+
dynamic_auth_method = fields.Selection(
46+
selection=[
47+
(
48+
"http_basic",
49+
"HTTP Basic",
50+
),
51+
(
52+
"demo_api_key",
53+
"Dummy Api Key For Demo",
54+
),
55+
(
56+
"demo_specific_partner",
57+
"Specific Partner For Demo",
58+
),
59+
],
60+
string="Auth method",
61+
)
62+
63+
dynamic_specific_partner_id = fields.Many2one(
64+
"res.partner",
65+
string="Specific Partner",
66+
help="The partner to use for the specific partner demo.",
67+
)
68+
69+
@api.depends("router_ids")
70+
def _compute_router_option_ids(self):
71+
for rec in self:
72+
# Use name module key to avoid virtual ids problems
73+
actual_routers = {
74+
tuple(getattr(router, key) for key in ("name", "module"))
75+
for router in rec.router_ids
76+
}
77+
options_to_remove = rec.router_option_ids.filtered(
78+
lambda router_option, actual_routers=actual_routers: (
79+
(
80+
router_option.router_id.name,
81+
router_option.router_id.module,
82+
)
83+
not in actual_routers
84+
)
85+
)
86+
actual_router_options = {
87+
tuple(getattr(router, key) for key in ("name", "module"))
88+
for router in rec.router_option_ids.mapped("router_id")
89+
}
90+
options_to_create = rec.router_ids.filtered(
91+
lambda r, actual_router_options=actual_router_options: (
92+
(r.name, r.module) not in actual_router_options
93+
)
94+
)
95+
rec.router_option_ids = [
96+
# Delete options for removed routers
97+
(
98+
Command.UNLINK,
99+
opt.id,
100+
)
101+
for opt in options_to_remove
102+
] + [
103+
# Create missing options for new routers
104+
(
105+
Command.CREATE,
106+
0,
107+
{
108+
"router_id": router.id,
109+
"endpoint_id": rec.id,
110+
},
111+
)
112+
for router in options_to_create
113+
]
114+
115+
def _get_view(self, view_id=None, view_type="form", **options):
116+
# Sync once per registry instance, if a module is installed after the first call
117+
# a new registry will be created and the routers will be synced again
118+
if not hasattr(self.env.registry, "_fasapi_routers_synced"):
119+
self.env["fastapi.router"].sync()
120+
self.env.registry._fasapi_routers_synced = True
121+
return super()._get_view(view_id=view_id, view_type=view_type, **options)
122+
123+
def _get_fastapi_routers(self) -> list[APIRouter]:
124+
routers = super()._get_fastapi_routers()
125+
126+
if self.app == "dynamic":
127+
routers += [
128+
router_option.router_id._get_router()
129+
for router_option in self.router_option_ids
130+
if not router_option.prefix
131+
]
132+
133+
return routers
134+
135+
def _get_app(self):
136+
app = super()._get_app()
137+
138+
if self.app == "dynamic":
139+
prefixed_routers = groupby(
140+
self.router_option_ids,
141+
lambda r: r.prefix,
142+
)
143+
for prefix, router_options in prefixed_routers:
144+
if not prefix:
145+
# Handled in _get_fastapi_routers
146+
continue
147+
router_options = self.env["fastapi.endpoint.router.option"].browse(
148+
router_option.id for router_option in router_options
149+
)
150+
if any(router_option.auth_method for router_option in router_options):
151+
sub_app = FastAPI()
152+
for router in router_options.mapped("router_id"):
153+
sub_app.include_router(router._get_router())
154+
sub_app.dependency_overrides.update(
155+
self._get_app_dependencies_overrides()
156+
)
157+
auth_router = next(
158+
router_option
159+
for router_option in router_options
160+
if router_option.auth_method
161+
)
162+
sub_app.dependency_overrides[authenticated_partner_impl] = (
163+
self._get_authenticated_partner_from_method(
164+
**auth_router.read()[0]
165+
)
166+
)
167+
168+
app.mount(prefix, sub_app)
169+
170+
else:
171+
for router in router_options.mapped("router_id"):
172+
app.include_router(router._get_router(), prefix=prefix)
173+
174+
return app
175+
176+
def _get_app_dependencies_overrides(self) -> dict[Callable, Callable]:
177+
overrides = super()._get_app_dependencies_overrides()
178+
179+
if self.app == "dynamic":
180+
auth = self._get_authenticated_partner_from_method(
181+
**{
182+
key.replace("dynamic_", ""): value
183+
for key, value in self.read()[0].items()
184+
},
185+
)
186+
if auth:
187+
overrides[authenticated_partner_impl] = auth
188+
189+
return overrides
190+
191+
@api.model
192+
def _get_authenticated_partner_from_method(
193+
self, auth_method, **options
194+
) -> Callable:
195+
if auth_method == "http_basic":
196+
return authenticated_partner_from_basic_auth_user
197+
198+
if auth_method == "demo_api_key":
199+
return api_key_based_authenticated_partner_impl
200+
201+
if auth_method == "demo_specific_partner" and "specific_partner_id" in options:
202+
203+
def endpoint_specific_based_authenticated_partner_impl(
204+
env: Annotated[api.Environment, Depends(odoo_env)],
205+
) -> Partner:
206+
"""A dummy implementation that takes the configured partner
207+
on the endpoint."""
208+
return env["res.partner"].browse(options["specific_partner_id"][0])
209+
210+
return endpoint_specific_based_authenticated_partner_impl
211+
212+
def _prepare_fastapi_app_params(self) -> dict[str, Any]:
213+
params = super()._prepare_fastapi_app_params()
214+
215+
if self.app == "dynamic":
216+
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
217+
params["openapi_tags"] = params.get("openapi_tags", []) + [
218+
{
219+
"name": prefix,
220+
"description": "Sub application",
221+
"externalDocs": {
222+
"description": "Documentation",
223+
"url": f"{base_url}{self.root_path}{prefix}/docs",
224+
},
225+
}
226+
for prefix in {
227+
router_option.prefix
228+
for router_option in self.router_option_ids
229+
if router_option.prefix and router_option.auth_method
230+
}
231+
]
232+
233+
return params
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from odoo import fields, models
6+
7+
8+
class FastapiEndpointRouterOption(models.Model):
9+
_name = "fastapi.endpoint.router.option"
10+
_description = "FastAPI Endpoint Router Option"
11+
12+
router_id = fields.Many2one("fastapi.router", required=True)
13+
endpoint_id = fields.Many2one("fastapi.endpoint", required=True)
14+
prefix = fields.Char()
15+
16+
auth_method = fields.Selection(
17+
selection=lambda self: self.env["fastapi.endpoint"]
18+
._fields["dynamic_auth_method"]
19+
.selection,
20+
string="Auth method for this router",
21+
)
22+
specific_partner_id = fields.Many2one(
23+
"res.partner",
24+
string="Specific Partner",
25+
help="The partner to use for the specific partner demo.",
26+
)
27+
28+
_sql_constraints = [
29+
(
30+
"name_router_endpoint_unique",
31+
"unique(router_id, endpoint_id)",
32+
"Option already exists",
33+
)
34+
]

0 commit comments

Comments
 (0)