showrunner 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
showrunner/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """ShowRunner - A collection of tools for live performances."""
2
+
3
+ import pluggy
4
+
5
+ # hookimpl must be defined before importing ShowRunner (which transitively
6
+ # imports plugins that do ``import showrunner; showrunner.hookimpl``).
7
+ hookimpl = pluggy.HookimplMarker("showrunner")
8
+ """Marker to be used in plugin hook implementations."""
9
+
10
+ from .app import ShowRunner # noqa: E402
11
+
12
+ __all__ = ["hookimpl", "ShowRunner"]
showrunner/app.py ADDED
@@ -0,0 +1,138 @@
1
+ """ShowRunner core application.
2
+
3
+ Manages the plugin lifecycle and wires plugins into the FastAPI application.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import inspect
9
+ import logging
10
+ from importlib.metadata import entry_points
11
+ from pathlib import Path
12
+
13
+ import pluggy
14
+ from fastapi import FastAPI
15
+
16
+ from . import plugins
17
+ from .config import ConfigWatcher, ShowRunnerConfig, load_config
18
+ from .hookspecs import ShowRunnerSpec
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def get_plugin_manager(
24
+ config: ShowRunnerConfig | None = None,
25
+ ) -> pluggy.PluginManager:
26
+ """Create and configure a PluginManager with built-in plugins loaded.
27
+
28
+ Plugins whose name (case-insensitive) appears in
29
+ ``config.plugins.disabled`` are skipped.
30
+ """
31
+ pm = pluggy.PluginManager("showrunner")
32
+ pm.add_hookspecs(ShowRunnerSpec)
33
+
34
+ disabled = set()
35
+ if config is not None:
36
+ disabled = {n.lower() for n in config.plugins.disabled}
37
+
38
+ # Register all built-in plugins
39
+ for plugin_class in plugins.get_builtin_plugins():
40
+ name = plugin_class.__name__.lower()
41
+ if name in disabled:
42
+ logger.info("Skipping disabled plugin %s", plugin_class.__name__)
43
+ continue
44
+ pm.register(plugin_class())
45
+
46
+ # Discover external plugins installed via setuptools entry points.
47
+ # We load them manually (instead of pm.load_setuptools_entrypoints)
48
+ # so that entry points pointing to a class are instantiated first —
49
+ # pluggy requires hook methods to be bound (i.e. on an instance).
50
+ for ep in entry_points(group="showrunner"):
51
+ if pm.get_plugin(ep.name) is not None:
52
+ continue
53
+ plugin = ep.load()
54
+ if inspect.isclass(plugin):
55
+ plugin = plugin()
56
+ name = getattr(plugin, "__name__", type(plugin).__name__).lower()
57
+ if name in disabled:
58
+ logger.info("Skipping disabled external plugin %s", name)
59
+ continue
60
+ pm.register(plugin, name=ep.name)
61
+
62
+ return pm
63
+
64
+
65
+ class ShowRunner:
66
+ """Main ShowRunner application.
67
+
68
+ Holds the plugin manager and the FastAPI app instance.
69
+ Provides lifecycle management for all registered plugins.
70
+ """
71
+
72
+ def __init__(self, config_path: Path | None = None) -> None:
73
+ self.config = load_config(config_path)
74
+ self._config_path = config_path or self.config._source_path
75
+
76
+ # Configure logging from show.toml unless already set by the CLI.
77
+ # basicConfig is a no-op when handlers already exist, so CLI
78
+ # flags (--log-level, --verbose, --quiet) take precedence.
79
+ logging.basicConfig(
80
+ level=self.config.logging.level.upper(),
81
+ format="%(levelname)-8s %(name)s: %(message)s",
82
+ )
83
+
84
+ self.pm = get_plugin_manager(self.config)
85
+ self._config_watcher: ConfigWatcher | None = None
86
+ self.api = FastAPI(title="ShowRunner", version="0.1.0", docs_url="/api")
87
+ self._mount_routes()
88
+
89
+ def _mount_routes(self) -> None:
90
+ """Collect APIRouters from all plugins and include them in the FastAPI app."""
91
+ routers = self.pm.hook.showrunner_get_routes()
92
+ for router in routers:
93
+ if router is not None:
94
+ self.api.include_router(router)
95
+
96
+ def startup(self) -> None:
97
+ """Invoke the startup hook on all plugins and start the config watcher."""
98
+ self.pm.hook.showrunner_startup(app=self)
99
+
100
+ if self._config_path is not None:
101
+ self._config_watcher = ConfigWatcher(self._config_path, self)
102
+ self._config_watcher.start()
103
+ logger.info("Watching %s for changes", self._config_path)
104
+ print(f" Config: {self._config_path} (watching for changes)")
105
+
106
+ def shutdown(self) -> None:
107
+ """Invoke the shutdown hook on all plugins and stop the config watcher."""
108
+ if self._config_watcher is not None:
109
+ self._config_watcher.stop()
110
+ self._config_watcher = None
111
+ self.pm.hook.showrunner_shutdown(app=self)
112
+
113
+ def list_plugins(self) -> list[dict]:
114
+ """Return metadata from all registered plugins."""
115
+ return self.pm.hook.showrunner_register()
116
+
117
+ def list_commands(self) -> list[dict]:
118
+ """Collect CLI commands from all plugins."""
119
+ results = self.pm.hook.showrunner_get_commands()
120
+ # Flatten the list-of-lists into a single list
121
+ return [cmd for group in results if group for cmd in group]
122
+
123
+
124
+ def _create_app() -> FastAPI:
125
+ """Create and start a ShowRunner instance, returning its FastAPI app.
126
+
127
+ Used as the ASGI entry point for ``uvicorn showrunner.app:app --reload``.
128
+ """
129
+ show = ShowRunner()
130
+ show.startup()
131
+ return show.api
132
+
133
+
134
+ def __getattr__(name: str):
135
+ if name == "app":
136
+ globals()["app"] = _create_app()
137
+ return globals()["app"]
138
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")