pluginforge 0.4.0__tar.gz → 0.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pluginforge
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Application-agnostic plugin framework built on pluggy
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -105,18 +105,24 @@ for plugin in pm.get_active_plugins():
105
105
  # Mount FastAPI routes
106
106
  from fastapi import FastAPI
107
107
  app = FastAPI()
108
- pm.mount_routes(app) # Routes at /api/plugins/{name}/
108
+ pm.mount_routes(app) # Routes under /api/ (configurable prefix)
109
109
  ```
110
110
 
111
111
  ## Features
112
112
 
113
113
  - **YAML Configuration** - App config, per-plugin config, and i18n strings
114
114
  - **Plugin Lifecycle** - init, activate, deactivate with error handling
115
+ - **Hot-Reload** - Swap plugins at runtime without app restart
115
116
  - **Enable/Disable** - Control plugins via config lists
116
117
  - **Dependency Resolution** - Topological sorting with circular dependency detection
117
- - **FastAPI Integration** - Auto-mount plugin routes under `/api/plugins/{name}/`
118
+ - **Extension Points** - Query plugins by interface with `get_extensions(type)`
119
+ - **Config Schema Validation** - Declare expected config types per plugin
120
+ - **Health Checks** - Monitor plugin status via `health_check()`
121
+ - **Pre-Activate Hooks** - Reject plugins before activation (license checks, etc.)
122
+ - **FastAPI Integration** - Mount plugin routes with configurable prefix
118
123
  - **Alembic Support** - Collect migration directories from plugins
119
124
  - **i18n** - Multi-language strings from YAML with fallback
125
+ - **Security** - Plugin name validation and path traversal prevention
120
126
 
121
127
  For detailed documentation, see the [Wiki](https://github.com/astrapi69/pluginforge/wiki).
122
128
 
@@ -161,10 +167,13 @@ The full documentation is available in the [Wiki](https://github.com/astrapi69/p
161
167
  - [Discovery and Dependencies](https://github.com/astrapi69/pluginforge/wiki/Discovery-and-Dependencies)
162
168
  - [Lifecycle](https://github.com/astrapi69/pluginforge/wiki/Lifecycle)
163
169
  - [Hooks](https://github.com/astrapi69/pluginforge/wiki/Hooks)
170
+ - [Extensions](https://github.com/astrapi69/pluginforge/wiki/Extensions)
164
171
  - [FastAPI Integration](https://github.com/astrapi69/pluginforge/wiki/FastAPI-Integration)
165
172
  - [Alembic Integration](https://github.com/astrapi69/pluginforge/wiki/Alembic-Integration)
166
173
  - [i18n](https://github.com/astrapi69/pluginforge/wiki/i18n)
174
+ - [Security](https://github.com/astrapi69/pluginforge/wiki/Security)
167
175
  - [Examples](https://github.com/astrapi69/pluginforge/wiki/Examples)
176
+ - [Changelog](https://github.com/astrapi69/pluginforge/wiki/Changelog)
168
177
 
169
178
  ## Development
170
179
 
@@ -80,18 +80,24 @@ for plugin in pm.get_active_plugins():
80
80
  # Mount FastAPI routes
81
81
  from fastapi import FastAPI
82
82
  app = FastAPI()
83
- pm.mount_routes(app) # Routes at /api/plugins/{name}/
83
+ pm.mount_routes(app) # Routes under /api/ (configurable prefix)
84
84
  ```
85
85
 
86
86
  ## Features
87
87
 
88
88
  - **YAML Configuration** - App config, per-plugin config, and i18n strings
89
89
  - **Plugin Lifecycle** - init, activate, deactivate with error handling
90
+ - **Hot-Reload** - Swap plugins at runtime without app restart
90
91
  - **Enable/Disable** - Control plugins via config lists
91
92
  - **Dependency Resolution** - Topological sorting with circular dependency detection
92
- - **FastAPI Integration** - Auto-mount plugin routes under `/api/plugins/{name}/`
93
+ - **Extension Points** - Query plugins by interface with `get_extensions(type)`
94
+ - **Config Schema Validation** - Declare expected config types per plugin
95
+ - **Health Checks** - Monitor plugin status via `health_check()`
96
+ - **Pre-Activate Hooks** - Reject plugins before activation (license checks, etc.)
97
+ - **FastAPI Integration** - Mount plugin routes with configurable prefix
93
98
  - **Alembic Support** - Collect migration directories from plugins
94
99
  - **i18n** - Multi-language strings from YAML with fallback
100
+ - **Security** - Plugin name validation and path traversal prevention
95
101
 
96
102
  For detailed documentation, see the [Wiki](https://github.com/astrapi69/pluginforge/wiki).
97
103
 
@@ -136,10 +142,13 @@ The full documentation is available in the [Wiki](https://github.com/astrapi69/p
136
142
  - [Discovery and Dependencies](https://github.com/astrapi69/pluginforge/wiki/Discovery-and-Dependencies)
137
143
  - [Lifecycle](https://github.com/astrapi69/pluginforge/wiki/Lifecycle)
138
144
  - [Hooks](https://github.com/astrapi69/pluginforge/wiki/Hooks)
145
+ - [Extensions](https://github.com/astrapi69/pluginforge/wiki/Extensions)
139
146
  - [FastAPI Integration](https://github.com/astrapi69/pluginforge/wiki/FastAPI-Integration)
140
147
  - [Alembic Integration](https://github.com/astrapi69/pluginforge/wiki/Alembic-Integration)
141
148
  - [i18n](https://github.com/astrapi69/pluginforge/wiki/i18n)
149
+ - [Security](https://github.com/astrapi69/pluginforge/wiki/Security)
142
150
  - [Examples](https://github.com/astrapi69/pluginforge/wiki/Examples)
151
+ - [Changelog](https://github.com/astrapi69/pluginforge/wiki/Changelog)
143
152
 
144
153
  ## Development
145
154
 
@@ -5,7 +5,7 @@ from pluginforge.discovery import CircularDependencyError
5
5
  from pluginforge.manager import PluginManager
6
6
  from pluginforge.security import InvalidPluginNameError
7
7
 
8
- __version__ = "0.3.0"
8
+ __version__ = "0.5.0"
9
9
  __all__ = [
10
10
  "BasePlugin",
11
11
  "CircularDependencyError",
@@ -305,18 +305,90 @@ class PluginManager:
305
305
  def call_hook(self, hook_name: str, **kwargs: Any) -> list[Any]:
306
306
  """Call a named hook on all registered plugins.
307
307
 
308
+ Individual hook implementations that raise exceptions are caught and
309
+ logged. Other implementations still execute (graceful degradation).
310
+
311
+ Args:
312
+ hook_name: Name of the hook to call.
313
+ **kwargs: Arguments to pass to hook implementations.
314
+
315
+ Returns:
316
+ List of results from all hook implementations (failed ones are skipped).
317
+ """
318
+ hook = getattr(self._pm.hook, hook_name, None)
319
+ if hook is None:
320
+ logger.warning("Hook '%s' not found", hook_name)
321
+ return []
322
+ try:
323
+ return hook(**kwargs)
324
+ except Exception as e:
325
+ logger.error("Hook '%s' raised an exception: %s", hook_name, e)
326
+ return []
327
+
328
+ def call_hook_safe(self, hook_name: str, **kwargs: Any) -> list[Any]:
329
+ """Call a hook, collecting results and skipping failed implementations.
330
+
331
+ Unlike call_hook(), this calls each implementation individually so one
332
+ failure does not prevent others from executing.
333
+
308
334
  Args:
309
335
  hook_name: Name of the hook to call.
310
336
  **kwargs: Arguments to pass to hook implementations.
311
337
 
312
338
  Returns:
313
- List of results from all hook implementations.
339
+ List of successful results (failed implementations are logged and skipped).
314
340
  """
315
341
  hook = getattr(self._pm.hook, hook_name, None)
316
342
  if hook is None:
317
343
  logger.warning("Hook '%s' not found", hook_name)
318
344
  return []
319
- return hook(**kwargs)
345
+ results: list[Any] = []
346
+ callers = hook.get_hookimpls()
347
+ for impl in callers:
348
+ try:
349
+ result = impl.function(**kwargs)
350
+ results.append(result)
351
+ except Exception as e:
352
+ plugin_name = impl.plugin_name or "unknown"
353
+ logger.error(
354
+ "Hook '%s' implementation from '%s' failed: %s",
355
+ hook_name,
356
+ plugin_name,
357
+ e,
358
+ )
359
+ return results
360
+
361
+ def get_plugin_hooks(self, name: str) -> list[str]:
362
+ """Return hook names implemented by a specific plugin.
363
+
364
+ Useful for debugging, settings UIs, and plugin marketplaces.
365
+
366
+ Args:
367
+ name: Plugin name.
368
+
369
+ Returns:
370
+ List of hook names the plugin implements, or empty list if not found.
371
+ """
372
+ plugin = self._lifecycle.get_plugin(name)
373
+ if plugin is None:
374
+ return []
375
+ impl_marker = f"{self._entry_point_group}_impl"
376
+ hooks: list[str] = []
377
+ for attr_name in dir(plugin):
378
+ if attr_name.startswith("_"):
379
+ continue
380
+ method = getattr(plugin, attr_name, None)
381
+ if callable(method) and hasattr(method, impl_marker):
382
+ hooks.append(attr_name)
383
+ return hooks
384
+
385
+ def get_all_hook_names(self) -> list[str]:
386
+ """Return all registered hook spec names.
387
+
388
+ Returns:
389
+ List of hook names from all registered specs.
390
+ """
391
+ return [name for name in dir(self._pm.hook) if not name.startswith("_")]
320
392
 
321
393
  def mount_routes(self, app: object, prefix: str = "/api") -> None:
322
394
  """Mount FastAPI routes from all active plugins.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pluginforge"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Application-agnostic plugin framework built on pluggy"
5
5
  authors = ["Asterios Raptis"]
6
6
  license = "MIT"
File without changes