Source code for oso.framework.plugin.extension
#
# (c) Copyright IBM Corp. 2025
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""PluginExtension class for initializing and configuring plugins."""
from typing import ClassVar, Literal, Type
from flask import Flask, current_app
from flask.views import View
from pydantic import BaseModel
from pydantic.types import ImportString
from oso.framework.config import AutoLoadConfig, ImportListMixin
from oso.framework.core.logging import get_logger
from oso.framework.exceptions import StartupException
from .base import PluginProtocol
from .addons.main import AddonProtocol, BaseAddonConfig
[docs]
class PluginConfig(
AutoLoadConfig,
ImportListMixin({"addons": BaseAddonConfig}),
_config_prefix="plugin"
):
"""
Configuration model for plugins.
Attributes
----------
mode (Literal["frontend", "backend"]): The mode of the plugin.
application (ImportString): The application class or string.
"""
mode: Literal["frontend", "backend"]
application: ImportString
[docs]
class PluginExtension:
"""
The PluginExtension class initializes and configures plugins in the OSO framework.
Attributes
----------
KEY (ClassVar[Literal["oso-plugin"]]): The key for the plugin extension.
config (PluginConfig): The configuration for the plugin.
"""
KEY: ClassVar[Literal["oso-plugin"]] = "oso-plugin"
# TODO: perhaps PluginConfig should be application wide config
def __init__(self, config: PluginConfig):
"""
Initialize the PluginExtension instance.
Args:
config (PluginConfig): The configuration for the plugin.
"""
self.config = config
self._init_addons(config.addons) # type: ignore [reportAttributeAccessError]
def _init_addons(self, addons: list[BaseAddonConfig]):
self.addons: dict[str, AddonProtocol] = {}
for addon in addons:
self.addons[addon.type.NAME] = addon.type.configure(self.config, addon)
[docs]
def init_app(self, app: Flask) -> None:
"""
Initialize the plugin with the Flask application.
Args:
app (Flask): The Flask application instance.
Raises
------
StartupException:
If the plugin doesn't follow the PluginProtocol or
isn't initialized.
"""
self.logger = get_logger(self.KEY)
if not isinstance(self.config.application, PluginProtocol):
raise StartupException(
"Plugin does not follow oso.framework.plugin.PluginProtocol"
)
# Setup plugin instance
self.plugin = self.config.application
if isinstance(self.plugin, Type):
self.plugin = self.plugin()
# Setup basic application
app.extensions = getattr(app, "extensions", {})
if self.KEY not in app.extensions:
app.extensions[self.KEY] = {}
if "self" in app.extensions[self.KEY]:
raise StartupException("Plugin already initialized")
# Initialize APIs
from .api import V1DocumentsApi, V1StatusApi
self._add_endpoint(
app=app,
rule=f"/api/{self.config.mode}/{V1DocumentsApi.ENDPOINT}",
view_func=V1DocumentsApi.as_view(f"plugin-{V1DocumentsApi.ENDPOINT}"),
)
self._add_endpoint(
app=app,
rule=f"/api/{self.config.mode}/{V1StatusApi.ENDPOINT}",
view_func=V1StatusApi.as_view(f"plugin-{V1StatusApi.ENDPOINT}"),
)
# Add ISV supplied APIs
for rule, view in self.plugin.externalViews.items():
self._add_endpoint(
app=app,
rule=f"/api/{rule}",
view_func=view.as_view(f"isv-external-{rule}"),
)
for rule, view in self.plugin.internalViews.items():
self._add_endpoint(
app=app,
rule=f"/internal/{rule}",
view_func=view.as_view(f"isv-internal-{rule}"),
)
# Finish initialization
app.extensions[self.KEY]["self"] = self
app.extensions[self.KEY]["plugin_config"] = self.config # also save config
def _add_endpoint(self, app: Flask, rule: str, view_func: View):
"""
Add an endpoint to the Flask application.
Args:
app (Flask): The Flask application instance.
rule (str): The URL rule for the endpoint.
view_func (View): The view function for the endpoint.
"""
app.add_url_rule(rule=rule, view_func=view_func)
self.logger.debug(f"Added rule {rule}")
[docs]
def current_oso_plugin() -> PluginExtension:
"""
Return the current OSO plugin extension instance.
Returns
-------
PluginExtension: The current plugin extension instance.
"""
return current_app.extensions[PluginExtension.KEY]["self"]
[docs]
def current_oso_plugin_app() -> PluginProtocol:
"""
Return the current plugin instance.
Returns
-------
PluginProtocol: The current plugin instance.
"""
return current_app.extensions[PluginExtension.KEY]["self"].plugin
[docs]
def current_oso_plugin_config() -> BaseModel:
"""
Return the current plugin configuration.
Returns
-------
BaseModel: The current plugin configuration.
"""
return current_app.extensions[PluginExtension.KEY]["plugin_config"]