Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions fastapi_dynamic_app/README.rst
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.
1 change: 1 addition & 0 deletions fastapi_dynamic_app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
17 changes: 17 additions & 0 deletions fastapi_dynamic_app/__manifest__.py
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",
],
}
3 changes: 3 additions & 0 deletions fastapi_dynamic_app/models/__init__.py
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
227 changes: 227 additions & 0 deletions fastapi_dynamic_app/models/fastapi_endpoint.py
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)

Check warning on line 116 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L114-L116

Added lines #L114 - L116 were not covered by tests

def _get_fastapi_routers(self) -> List[APIRouter]:
routers = super()._get_fastapi_routers()

if self.app == "dynamic":
routers += [

Check warning on line 122 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L122

Added line #L122 was not covered by tests
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(

Check warning on line 134 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L134

Added line #L134 was not covered by tests
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(

Check warning on line 142 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L141-L142

Added lines #L141 - L142 were not covered by tests
router_option.id for router_option in router_options
)
if any(router_option.auth_method for router_option in router_options):
sub_app = FastAPI()

Check warning on line 146 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L146

Added line #L146 was not covered by tests
for router in router_options.mapped("router_id"):
sub_app.include_router(router._get_router())
sub_app.dependency_overrides.update(

Check warning on line 149 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L148-L149

Added lines #L148 - L149 were not covered by tests
self._get_app_dependencies_overrides()
)
auth_router = next(

Check warning on line 152 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L152

Added line #L152 was not covered by tests
router_option
for router_option in router_options
if router_option.auth_method
)
sub_app.dependency_overrides[

Check warning on line 157 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L157

Added line #L157 was not covered by tests
authenticated_partner_impl
] = self._get_authenticated_partner_from_method(
**auth_router.read()[0]
)

app.mount(prefix, sub_app)

Check warning on line 163 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L163

Added line #L163 was not covered by tests

else:
for router in router_options.mapped("router_id"):
app.include_router(router._get_router(), prefix=prefix)

Check warning on line 167 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L167

Added line #L167 was not covered by tests

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(

Check warning on line 175 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L175

Added line #L175 was not covered by tests
**{
key.replace("dynamic_", ""): value
for key, value in self.read()[0].items()
},
)
if auth:
overrides[authenticated_partner_impl] = auth

Check warning on line 182 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L182

Added line #L182 was not covered by tests

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

Check warning on line 191 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L191

Added line #L191 was not covered by tests

if auth_method == "demo_api_key":
return api_key_based_authenticated_partner_impl

Check warning on line 194 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L194

Added line #L194 was not covered by tests

if auth_method == "demo_specific_partner" and "specific_partner_id" in options:

def endpoint_specific_based_authenticated_partner_impl(

Check warning on line 198 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L198

Added line #L198 was not covered by tests
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])

Check warning on line 202 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L202

Added line #L202 was not covered by tests

return endpoint_specific_based_authenticated_partner_impl

Check warning on line 204 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L204

Added line #L204 was not covered by tests

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", []) + [

Check warning on line 211 in fastapi_dynamic_app/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

fastapi_dynamic_app/models/fastapi_endpoint.py#L210-L211

Added lines #L210 - L211 were not covered by tests
{
"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 fastapi_dynamic_app/models/fastapi_endpoint_router_option.py
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",
)
]
Loading
Loading