Skip to content

Commit

Permalink
[ADD] fastapi_dynamic_app
Browse files Browse the repository at this point in the history
  • Loading branch information
paradoxxxzero committed Dec 3, 2024
1 parent c89c938 commit 359df8b
Show file tree
Hide file tree
Showing 15 changed files with 1,009 additions and 0 deletions.
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)

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

0 comments on commit 359df8b

Please sign in to comment.