-
-
Notifications
You must be signed in to change notification settings - Fork 304
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c89c938
commit 359df8b
Showing
15 changed files
with
1,009 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
=================== | ||
Fastapi Dynamic App | ||
=================== | ||
|
||
.. | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
!! This file is generated by oca-gen-addon-readme !! | ||
!! changes will be overwritten. !! | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
!! source digest: sha256:08b7e9a66c3ba8a25c56b2487ecc3de272ba577b731be907dcef198c2e5789f3 | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
.. |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-AGPL--3-blue.png | ||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html | ||
:alt: License: AGPL-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/fastapi_dynamic_app | ||
: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-fastapi_dynamic_app | ||
: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 module allows to configure fastapi endpoints directly from the odoo | ||
interface. It makes it possible to manage the endpoint routers and their | ||
options, such as the prefix and the authentication method. | ||
|
||
It also provide some demo authentication methods. | ||
|
||
**Table of contents** | ||
|
||
.. contents:: | ||
:local: | ||
|
||
Usage | ||
===== | ||
|
||
Create a FastAPI endpoint and select the Dynamic app for it. | ||
|
||
You can now configure its mounted routers in the Routers field and the | ||
authentication method in the Authentication Method field. | ||
|
||
A list of the chosen routers will appear in the Dynamic Routers tab. | ||
There you can configure the routers' options, such as the prefix and the | ||
authentication method. | ||
|
||
If a router is configured with a prefix, let's say the cart router, it | ||
will be mounted in the app with the prefix unless the authentication | ||
method is set, in which case a sub app will be created for all router | ||
with this prefix. | ||
|
||
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:%20fastapi_dynamic_app%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 | ||
------- | ||
|
||
* Akretion | ||
|
||
Contributors | ||
------------ | ||
|
||
- Florian Mounier [email protected] | ||
|
||
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. | ||
|
||
This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/fastapi_dynamic_app>`_ project on GitHub. | ||
|
||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Copyright 2024 Akretion (http://www.akretion.com). | ||
# @author Florian Mounier <[email protected]> | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
{ | ||
"name": "Fastapi Dynamic App", | ||
"summary": "A Fastapi App That Provides Dynamic Router Configuration", | ||
"version": "16.0.1.0.0", | ||
"license": "AGPL-3", | ||
"author": "Akretion,Odoo Community Association (OCA)", | ||
"website": "https://github.com/OCA/rest-framework", | ||
"depends": ["fastapi"], | ||
"data": [ | ||
"security/fastapi_router_security.xml", | ||
"views/fastapi_endpoint_views.xml", | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from . import fastapi_endpoint | ||
from . import fastapi_router | ||
from . import fastapi_endpoint_router_option |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
# Copyright 2024 Akretion (http://www.akretion.com). | ||
# @author Florian Mounier <[email protected]> | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
from itertools import groupby | ||
from typing import Annotated, Any, Callable, Dict, List | ||
|
||
from odoo import Command, api, fields, models | ||
|
||
from odoo.addons.base.models.res_partner import Partner | ||
from odoo.addons.fastapi.dependencies import ( | ||
authenticated_partner_from_basic_auth_user, | ||
authenticated_partner_impl, | ||
odoo_env, | ||
) | ||
from odoo.addons.fastapi.models.fastapi_endpoint_demo import ( | ||
api_key_based_authenticated_partner_impl, | ||
) | ||
|
||
from fastapi import APIRouter, Depends, FastAPI | ||
|
||
|
||
class FastapiEndpoint(models.Model): | ||
_inherit = "fastapi.endpoint" | ||
|
||
app: str = fields.Selection( | ||
selection_add=[("dynamic", "Dynamic Endpoint")], ondelete={"dynamic": "cascade"} | ||
) | ||
router_ids = fields.Many2many( | ||
"fastapi.router", | ||
string="Routers", | ||
help="The routers to use on this endpoint.", | ||
) | ||
|
||
router_option_ids = fields.One2many( | ||
"fastapi.endpoint.router.option", | ||
"endpoint_id", | ||
compute="_compute_router_option_ids", | ||
store=True, | ||
readonly=False, | ||
string="Router Options", | ||
help="The options to use on the routers.", | ||
) | ||
|
||
dynamic_auth_method = fields.Selection( | ||
selection=[ | ||
( | ||
"http_basic", | ||
"HTTP Basic", | ||
), | ||
( | ||
"demo_api_key", | ||
"Dummy Api Key For Demo", | ||
), | ||
( | ||
"demo_specific_partner", | ||
"Specific Partner For Demo", | ||
), | ||
], | ||
string="Auth method", | ||
) | ||
|
||
dynamic_specific_partner_id = fields.Many2one( | ||
"res.partner", | ||
string="Specific Partner", | ||
help="The partner to use for the specific partner demo.", | ||
) | ||
|
||
@api.depends("router_ids") | ||
def _compute_router_option_ids(self): | ||
for rec in self: | ||
# Use name module key to avoid virtual ids problems | ||
actual_routers = { | ||
tuple(getattr(router, key) for key in ("name", "module")) | ||
for router in rec.router_ids | ||
} | ||
options_to_remove = rec.router_option_ids.filtered( | ||
lambda router_option: ( | ||
router_option.router_id.name, | ||
router_option.router_id.module, | ||
) | ||
not in actual_routers | ||
) | ||
actual_router_options = { | ||
tuple(getattr(router, key) for key in ("name", "module")) | ||
for router in rec.router_option_ids.mapped("router_id") | ||
} | ||
options_to_create = rec.router_ids.filtered( | ||
lambda r: (r.name, r.module) not in actual_router_options | ||
) | ||
rec.router_option_ids = [ | ||
# Delete options for removed routers | ||
( | ||
Command.UNLINK, | ||
opt.id, | ||
) | ||
for opt in options_to_remove | ||
] + [ | ||
# Create missing options for new routers | ||
( | ||
Command.CREATE, | ||
0, | ||
{ | ||
"router_id": router.id, | ||
"endpoint_id": rec.id, | ||
}, | ||
) | ||
for router in options_to_create | ||
] | ||
|
||
def _get_view(self, view_id=None, view_type="form", **options): | ||
# Sync once per registry instance, if a module is installed after the first call | ||
# a new registry will be created and the routers will be synced again | ||
if not hasattr(self.env.registry, "_fasapi_routers_synced"): | ||
self.env["fastapi.router"].sync() | ||
self.env.registry._fasapi_routers_synced = True | ||
return super()._get_view(view_id=view_id, view_type=view_type, **options) | ||
|
||
def _get_fastapi_routers(self) -> List[APIRouter]: | ||
routers = super()._get_fastapi_routers() | ||
|
||
if self.app == "dynamic": | ||
routers += [ | ||
router_option.router_id._get_router() | ||
for router_option in self.router_option_ids | ||
if not router_option.prefix | ||
] | ||
|
||
return routers | ||
|
||
def _get_app(self): | ||
app = super()._get_app() | ||
|
||
if self.app == "dynamic": | ||
prefixed_routers = groupby( | ||
self.router_option_ids, | ||
lambda r: r.prefix, | ||
) | ||
for prefix, router_options in prefixed_routers: | ||
if not prefix: | ||
# Handled in _get_fastapi_routers | ||
continue | ||
router_options = self.env["fastapi.endpoint.router.option"].browse( | ||
router_option.id for router_option in router_options | ||
) | ||
if any(router_option.auth_method for router_option in router_options): | ||
sub_app = FastAPI() | ||
for router in router_options.mapped("router_id"): | ||
sub_app.include_router(router._get_router()) | ||
sub_app.dependency_overrides.update( | ||
self._get_app_dependencies_overrides() | ||
) | ||
auth_router = next( | ||
router_option | ||
for router_option in router_options | ||
if router_option.auth_method | ||
) | ||
sub_app.dependency_overrides[ | ||
authenticated_partner_impl | ||
] = self._get_authenticated_partner_from_method( | ||
**auth_router.read()[0] | ||
) | ||
|
||
app.mount(prefix, sub_app) | ||
|
||
else: | ||
for router in router_options.mapped("router_id"): | ||
app.include_router(router._get_router(), prefix=prefix) | ||
|
||
return app | ||
|
||
def _get_app_dependencies_overrides(self) -> Dict[Callable, Callable]: | ||
overrides = super()._get_app_dependencies_overrides() | ||
|
||
if self.app == "dynamic": | ||
auth = self._get_authenticated_partner_from_method( | ||
**{ | ||
key.replace("dynamic_", ""): value | ||
for key, value in self.read()[0].items() | ||
}, | ||
) | ||
if auth: | ||
overrides[authenticated_partner_impl] = auth | ||
|
||
return overrides | ||
|
||
@api.model | ||
def _get_authenticated_partner_from_method( | ||
self, auth_method, **options | ||
) -> Callable: | ||
if auth_method == "http_basic": | ||
return authenticated_partner_from_basic_auth_user | ||
|
||
if auth_method == "demo_api_key": | ||
return api_key_based_authenticated_partner_impl | ||
|
||
if auth_method == "demo_specific_partner" and "specific_partner_id" in options: | ||
|
||
def endpoint_specific_based_authenticated_partner_impl( | ||
env: Annotated[api.Environment, Depends(odoo_env)], | ||
) -> Partner: | ||
"""A dummy implementation that takes the configured partner on the endpoint.""" | ||
return env["res.partner"].browse(options["specific_partner_id"][0]) | ||
|
||
return endpoint_specific_based_authenticated_partner_impl | ||
|
||
def _prepare_fastapi_app_params(self) -> Dict[str, Any]: | ||
params = super()._prepare_fastapi_app_params() | ||
|
||
if self.app == "dynamic": | ||
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") | ||
params["openapi_tags"] = params.get("openapi_tags", []) + [ | ||
{ | ||
"name": prefix, | ||
"description": "Sub application", | ||
"externalDocs": { | ||
"description": "Documentation", | ||
"url": f"{base_url}{self.root_path}{prefix}/docs", | ||
}, | ||
} | ||
for prefix in { | ||
router_option.prefix | ||
for router_option in self.router_option_ids | ||
if router_option.prefix and router_option.auth_method | ||
} | ||
] | ||
|
||
return params |
34 changes: 34 additions & 0 deletions
34
fastapi_dynamic_app/models/fastapi_endpoint_router_option.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Copyright 2024 Akretion (http://www.akretion.com). | ||
# @author Florian Mounier <[email protected]> | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from odoo import fields, models | ||
|
||
|
||
class FastapiEndpointRouterOption(models.Model): | ||
_name = "fastapi.endpoint.router.option" | ||
_description = "FastAPI Endpoint Router Option" | ||
|
||
router_id = fields.Many2one("fastapi.router", required=True) | ||
endpoint_id = fields.Many2one("fastapi.endpoint", required=True) | ||
prefix = fields.Char() | ||
|
||
auth_method = fields.Selection( | ||
selection=lambda self: self.env["fastapi.endpoint"] | ||
._fields["dynamic_auth_method"] | ||
.selection, | ||
string="Auth method for this router", | ||
) | ||
specific_partner_id = fields.Many2one( | ||
"res.partner", | ||
string="Specific Partner", | ||
help="The partner to use for the specific partner demo.", | ||
) | ||
|
||
_sql_constraints = [ | ||
( | ||
"name_router_endpoint_unique", | ||
"unique(router_id, endpoint_id)", | ||
"Option already exists", | ||
) | ||
] |
Oops, something went wrong.