mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-15 06:55:48 +00:00
feat: load http views dynamically (#113)
This commit is contained in:
@ -92,6 +92,7 @@ class Migrations(LazyLogging):
|
||||
list[Migration]: list of found migrations
|
||||
"""
|
||||
migrations: list[Migration] = []
|
||||
|
||||
package_dir = Path(__file__).resolve().parent
|
||||
modules = [module_name for (_, module_name, _) in iter_modules([str(package_dir)])]
|
||||
|
||||
|
@ -17,18 +17,90 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import Application
|
||||
from aiohttp.web import Application, View
|
||||
from collections.abc import Generator
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from pathlib import Path
|
||||
from pkgutil import ModuleInfo, iter_modules
|
||||
from types import ModuleType
|
||||
from typing import Any, Type, TypeGuard
|
||||
|
||||
from ahriman.web.views.api.docs import DocsView
|
||||
from ahriman.web.views.api.swagger import SwaggerView
|
||||
from ahriman.web.views.index import IndexView
|
||||
from ahriman.web.views import v1, v2
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
__all__ = ["setup_routes"]
|
||||
|
||||
|
||||
def _dynamic_routes(module_root: Path) -> dict[str, Type[View]]:
|
||||
"""
|
||||
extract dynamic routes based on views
|
||||
|
||||
Args:
|
||||
module_root(Path): root module path with views
|
||||
|
||||
Returns:
|
||||
dict[str, Type[View]]: map of the route to its view
|
||||
"""
|
||||
def is_base_view(clz: Any) -> TypeGuard[Type[BaseView]]:
|
||||
return isinstance(clz, type) and issubclass(clz, BaseView)
|
||||
|
||||
routes: dict[str, Type[View]] = {}
|
||||
for module_info in _modules(module_root):
|
||||
module = _module(module_info)
|
||||
|
||||
for attribute_name in dir(module):
|
||||
view = getattr(module, attribute_name)
|
||||
if not is_base_view(view):
|
||||
continue
|
||||
routes.update([(route, view) for route in view.ROUTES])
|
||||
|
||||
return routes
|
||||
|
||||
|
||||
def _module(module_info: ModuleInfo) -> ModuleType:
|
||||
"""
|
||||
load module from its info
|
||||
|
||||
Args:
|
||||
module_info(ModuleInfo): module info descriptor
|
||||
|
||||
Returns:
|
||||
ModuleType: loaded module
|
||||
|
||||
Raises:
|
||||
ValueError: if loader is not an instance of ``SourceFileLoader``
|
||||
"""
|
||||
module_spec = module_info.module_finder.find_spec(module_info.name, None)
|
||||
if module_spec is None:
|
||||
raise ValueError(f"Module specification of {module_info.name} is empty")
|
||||
|
||||
loader = module_spec.loader
|
||||
if not isinstance(loader, SourceFileLoader):
|
||||
raise ValueError(f"Module {module_info.name} loader is not an instance of SourceFileLoader")
|
||||
|
||||
module = ModuleType(loader.name)
|
||||
loader.exec_module(module)
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def _modules(module_root: Path) -> Generator[ModuleInfo, None, None]:
|
||||
"""
|
||||
extract available modules from package
|
||||
|
||||
Args:
|
||||
module_root(Path): module root path
|
||||
|
||||
Yields:
|
||||
ModuleInfo: module information each available module
|
||||
"""
|
||||
for module_info in iter_modules([str(module_root)]):
|
||||
if module_info.ispkg:
|
||||
yield from _modules(module_root / module_info.name)
|
||||
else:
|
||||
yield module_info
|
||||
|
||||
|
||||
def setup_routes(application: Application, static_path: Path) -> None:
|
||||
"""
|
||||
setup all defined routes
|
||||
@ -37,30 +109,8 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
||||
application(Application): web application instance
|
||||
static_path(Path): path to static files directory
|
||||
"""
|
||||
application.router.add_view("/", IndexView)
|
||||
application.router.add_view("/index.html", IndexView)
|
||||
|
||||
application.router.add_view("/api-docs", DocsView)
|
||||
application.router.add_view("/api-docs/swagger.json", SwaggerView)
|
||||
|
||||
application.router.add_static("/static", static_path, follow_symlinks=True)
|
||||
|
||||
application.router.add_view("/api/v1/service/add", v1.AddView)
|
||||
application.router.add_view("/api/v1/service/pgp", v1.PGPView)
|
||||
application.router.add_view("/api/v1/service/rebuild", v1.RebuildView)
|
||||
application.router.add_view("/api/v1/service/process/{process_id}", v1.ProcessView)
|
||||
application.router.add_view("/api/v1/service/remove", v1.RemoveView)
|
||||
application.router.add_view("/api/v1/service/request", v1.RequestView)
|
||||
application.router.add_view("/api/v1/service/search", v1.SearchView)
|
||||
application.router.add_view("/api/v1/service/update", v1.UpdateView)
|
||||
application.router.add_view("/api/v1/service/upload", v1.UploadView)
|
||||
|
||||
application.router.add_view("/api/v1/packages", v1.PackagesView)
|
||||
application.router.add_view("/api/v1/packages/{package}", v1.PackageView)
|
||||
application.router.add_view("/api/v1/packages/{package}/logs", v1.LogsView)
|
||||
application.router.add_view("/api/v2/packages/{package}/logs", v2.LogsView)
|
||||
|
||||
application.router.add_view("/api/v1/status", v1.StatusView)
|
||||
|
||||
application.router.add_view("/api/v1/login", v1.LoginView)
|
||||
application.router.add_view("/api/v1/logout", v1.LogoutView)
|
||||
views = Path(__file__).parent / "views"
|
||||
for route, view in _dynamic_routes(views).items():
|
||||
application.router.add_view(route, view)
|
||||
|
@ -34,6 +34,7 @@ class DocsView(BaseView):
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Unauthorized
|
||||
ROUTES = ["/api-docs"]
|
||||
|
||||
@aiohttp_jinja2.template("api.jinja2")
|
||||
async def get(self) -> dict[str, Any]:
|
||||
|
@ -34,6 +34,7 @@ class SwaggerView(BaseView):
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Unauthorized
|
||||
ROUTES = ["/api-docs/swagger.json"]
|
||||
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
|
@ -38,9 +38,11 @@ class BaseView(View, CorsViewMixin):
|
||||
|
||||
Attributes:
|
||||
OPTIONS_PERMISSION(UserAccess): (class attribute) options permissions of self
|
||||
ROUTES(list[str]): (class attribute) list of supported routes
|
||||
"""
|
||||
|
||||
OPTIONS_PERMISSION = UserAccess.Unauthorized
|
||||
ROUTES: list[str] = []
|
||||
|
||||
@property
|
||||
def configuration(self) -> Configuration:
|
||||
|
@ -44,6 +44,7 @@ class IndexView(BaseView):
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Unauthorized
|
||||
ROUTES = ["/", "/index.html"]
|
||||
|
||||
@aiohttp_jinja2.template("build-status.jinja2")
|
||||
async def get(self) -> dict[str, Any]:
|
||||
|
@ -17,20 +17,3 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.web.views.v1.service.add import AddView
|
||||
from ahriman.web.views.v1.service.pgp import PGPView
|
||||
from ahriman.web.views.v1.service.process import ProcessView
|
||||
from ahriman.web.views.v1.service.rebuild import RebuildView
|
||||
from ahriman.web.views.v1.service.remove import RemoveView
|
||||
from ahriman.web.views.v1.service.request import RequestView
|
||||
from ahriman.web.views.v1.service.search import SearchView
|
||||
from ahriman.web.views.v1.service.update import UpdateView
|
||||
from ahriman.web.views.v1.service.upload import UploadView
|
||||
|
||||
from ahriman.web.views.v1.status.logs import LogsView
|
||||
from ahriman.web.views.v1.status.package import PackageView
|
||||
from ahriman.web.views.v1.status.packages import PackagesView
|
||||
from ahriman.web.views.v1.status.status import StatusView
|
||||
|
||||
from ahriman.web.views.v1.user.login import LoginView
|
||||
from ahriman.web.views.v1.user.logout import LogoutView
|
||||
|
@ -35,6 +35,7 @@ class AddView(BaseView):
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/service/add"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Actions"],
|
||||
|
@ -35,8 +35,9 @@ class PGPView(BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/service/pgp"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Actions"],
|
||||
|
@ -35,6 +35,7 @@ class ProcessView(BaseView):
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
ROUTES = ["/api/v1/service/process/{process_id}"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Actions"],
|
||||
|
@ -35,6 +35,7 @@ class RebuildView(BaseView):
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/service/rebuild"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Actions"],
|
||||
|
@ -35,6 +35,7 @@ class RemoveView(BaseView):
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/service/remove"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Actions"],
|
||||
|
@ -35,6 +35,7 @@ class RequestView(BaseView):
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Reporter
|
||||
ROUTES = ["/api/v1/service/request"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Actions"],
|
||||
|
@ -38,6 +38,7 @@ class SearchView(BaseView):
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
ROUTES = ["/api/v1/service/search"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Actions"],
|
||||
|
@ -35,6 +35,7 @@ class UpdateView(BaseView):
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/service/update"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Actions"],
|
||||
|
@ -39,6 +39,7 @@ class UploadView(BaseView):
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/service/upload"]
|
||||
|
||||
@staticmethod
|
||||
async def save_file(part: BodyPartReader, target: Path, *, max_body_size: int | None = None) -> tuple[str, Path]:
|
||||
|
@ -41,6 +41,7 @@ class LogsView(BaseView):
|
||||
|
||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
ROUTES = ["/api/v1/packages/{package}/logs"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Packages"],
|
||||
|
@ -41,6 +41,7 @@ class PackageView(BaseView):
|
||||
|
||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION = UserAccess.Read
|
||||
ROUTES = ["/api/v1/packages/{package}"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Packages"],
|
||||
|
@ -41,6 +41,7 @@ class PackagesView(BaseView):
|
||||
|
||||
GET_PERMISSION = UserAccess.Read
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/packages"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Packages"],
|
||||
|
@ -41,6 +41,7 @@ class StatusView(BaseView):
|
||||
|
||||
GET_PERMISSION = UserAccess.Read
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/status"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Status"],
|
||||
|
@ -37,6 +37,7 @@ class LoginView(BaseView):
|
||||
"""
|
||||
|
||||
GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized
|
||||
ROUTES = ["/api/v1/login"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Login"],
|
||||
|
@ -36,6 +36,7 @@ class LogoutView(BaseView):
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Unauthorized
|
||||
ROUTES = ["/api/v1/logout"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Login"],
|
||||
|
@ -17,4 +17,3 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.web.views.v2.status.logs import LogsView
|
||||
|
@ -37,6 +37,7 @@ class LogsView(BaseView):
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
ROUTES = ["/api/v2/packages/{package}/logs"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Packages"],
|
||||
|
Reference in New Issue
Block a user