Metadata-Version: 2.4
Name: pluginforge
Version: 0.10.0
Summary: Application-agnostic plugin framework built on pluggy
License-Expression: MIT
License-File: LICENSE
Keywords: plugin,framework,pluggy,hooks,yaml
Author: Asterios Raptis
Author-email: aster.raptis@gmail.com
Requires-Python: >=3.11,<4.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Dist: packaging (>=21.0)
Requires-Dist: pluggy (>=1.5.0,<2.0.0)
Requires-Dist: pyyaml (>=6.0,<7.0.0)
Project-URL: Changelog, https://github.com/astrapi69/pluginforge/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/astrapi69/pluginforge/wiki
Project-URL: Homepage, https://github.com/astrapi69/pluginforge
Project-URL: Issue Tracker, https://github.com/astrapi69/pluginforge/issues
Project-URL: Repository, https://github.com/astrapi69/pluginforge
Description-Content-Type: text/markdown

# PluginForge

Application-agnostic Python plugin framework built on [pluggy](https://pluggy.readthedocs.io/).

PluginForge adds the layers that pluggy is missing: YAML configuration, plugin lifecycle management, enable/disable per config, dependency resolution, FastAPI integration, and i18n support.

## Installation

```bash
pip install pluginforge
```

FastAPI integration is built in; install FastAPI alongside PluginForge if your application uses it:

```bash
pip install pluginforge fastapi
```

(The `[fastapi]` extra existed in pre-0.6.0 releases and was a non-functional shim; it was removed in v0.6.0. Install FastAPI as a normal dependency of your consuming application.)

## Quickstart

### 1. Create a plugin

```python
from pluginforge import BasePlugin

class HelloPlugin(BasePlugin):
    name = "hello"
    version = "1.0.0"
    description = "A hello world plugin"

    def activate(self):
        print(f"Hello plugin activated with config: {self.config}")

    def get_routes(self):
        from fastapi import APIRouter
        router = APIRouter()

        @router.get("/hello")
        def hello():
            return {"message": self.config.get("greeting", "Hello!")}

        return [router]
```

### 2. Configure your app

```yaml
# config/app.yaml
app:
  name: "MyApp"
  version: "1.0.0"
  default_language: "en"

plugins:
  entry_point_group: "myapp.plugins"
  enabled:
    - "hello"
  disabled: []
```

```yaml
# config/plugins/hello.yaml
greeting: "Hello from PluginForge!"
```

### 3. Use PluginManager

```python
from pluginforge import PluginManager

pm = PluginManager("config/app.yaml")

# Register plugins directly (or use entry points for auto-discovery)
result = pm.register_plugins([HelloPlugin])
print(f"Activated: {result.activated}")          # ['hello']
print(f"Filtered:  {result.filtered_out()}")     # {} when all activated

# Access plugins
for plugin in pm.get_active_plugins():
    print(f"Active: {plugin.name} v{plugin.version}")

# Mount FastAPI routes
from fastapi import FastAPI
app = FastAPI()
pm.mount_routes(app)  # Routes under /api/ (configurable prefix)
```

## Features

- **YAML Configuration** - App config, per-plugin config, and i18n strings
- **Plugin Lifecycle** - init, activate, deactivate with error handling
- **Structured Diagnostics** - `DiscoveryResult` surfaces per-plugin `PluginState`; structured `PluginError`s carry the cause exception, lifecycle phase, and severity. `DiscoveryResult.by_filter_reason(reason)` and (v0.10.0) `DiscoveryDiff.by_filter_reason(reason)` group plugins by filter outcome in one call
- **Hot-Reload** - Reload an active plugin's module via `reload_plugin(name)` without restarting the app
- **Entry Point Rediscovery** - Pick up newly-installed plugins at runtime via `rediscover()`, no process restart required
- **Enable/Disable** - Control plugins via config lists
- **Live Config Refresh** - `refresh_config()` replaces the app-config snapshot and notifies active plugins through the `on_config_changed` hook. v0.10.0 adds `merge_app_config(overlay, *, notify=True)` for deep-merge overlay semantics (replaces the `_app_config = ...` hack consumers were using) plus a `notify=False` kwarg on `refresh_config` for the no-active-plugins startup path
- **Dependency Resolution** - Topological sorting with circular dependency detection
- **Extension Points** - Query plugins by interface with `get_extensions(type)`
- **Config Schema Validation** - Declare expected config types per plugin
- **Health Checks** - Monitor plugin status via `health_check()`
- **Pre-Activate Hooks** - Reject plugins before activation (license checks, etc.)
- **Version Gating** - Enforce `api_version` and `min_app_version` with configurable severity
- **Application Identity Gating** - Declare `target_application` on plugins and `app_id` on the host. Plugins whose `target_application` mismatches the host's `app_id`, or whose `target_application` is not declared at all, refuse to activate. Permissive: hosts without `app_id` see no validation
- **Lifecycle Visibility** - `PluginState` carries `activated_at` / `last_config_change` / `source` timestamps; `inspect_plugin(name)` aggregates state, config, health, hooks, routes, and identity into one snapshot; `on_plugin_activated` / `on_plugin_deactivated` / `on_config_refreshed` event hooks notify subscribers after lifecycle transitions
- **FastAPI Integration** - Mount plugin routes with configurable prefix
- **Idempotent Route Mounting (v0.8.0)** - `mount_routes` is safe to re-call; no route-table accumulation across `TestClient` lifespans (closes the v0.7.0 recursion-cascade reported by downstream consumers)
- **Test Helpers (v0.8.0)** - `pluginforge.testing.IsolatedPluginManager` and `MockPlugin` for consumer-app test wiring
- **Single-Router Convention (v0.8.0)** - one router per `get_routes()` is recommended; multi-router plugins emit a `DeprecationWarning`
- **Type Annotations (PEP 561)** - `py.typed` marker shipped; `mypy` / `pyright` consume PluginForge's full type information
- **Alembic Support** - Collect migration directories from plugins
- **i18n** - Multi-language strings from YAML with fallback
- **Security** - Plugin name validation and path traversal prevention

For detailed documentation, see the [Wiki](https://github.com/astrapi69/pluginforge/wiki).

## Entry Point Discovery

Register plugins as entry points in your `pyproject.toml`:

```toml
[project.entry-points."myapp.plugins"]
hello = "myapp.plugins.hello:HelloPlugin"
```

Then use `discover_plugins()` instead of `register_plugins()`:

```python
pm = PluginManager("config/app.yaml")
result = pm.discover_plugins()  # Auto-discovers from entry points

# Later, after a new plugin is installed (e.g. poetry install in another shell):
diff = pm.rediscover()
print(f"Newly activated: {diff.added}")
print(f"Removed:         {diff.removed}")
```

## i18n

```yaml
# config/i18n/en.yaml
common:
  save: "Save"
  cancel: "Cancel"
```

```python
pm.get_text("common.save", "en")  # "Save"
pm.get_text("common.save", "de")  # "Speichern"
```

## Documentation

The full documentation is available in the [Wiki](https://github.com/astrapi69/pluginforge/wiki) and the in-repo guides:

- [Consumer Integration Guide](docs/guides/consumer-integration.md) - wire PluginForge into your FastAPI app from scratch (covers lifespan, mounting, hot reload, testing)
- [Plugin Author Guide](docs/guides/plugin-author.md) - write a PluginForge plugin (BasePlugin contract, identity gating, single-router convention, lifecycle hooks)
- [Getting Started](https://github.com/astrapi69/pluginforge/wiki/Getting-Started)
- [BasePlugin](https://github.com/astrapi69/pluginforge/wiki/BasePlugin)
- [PluginManager](https://github.com/astrapi69/pluginforge/wiki/PluginManager)
- [Configuration](https://github.com/astrapi69/pluginforge/wiki/Configuration)
- [Discovery and Dependencies](https://github.com/astrapi69/pluginforge/wiki/Discovery-and-Dependencies)
- [Lifecycle](https://github.com/astrapi69/pluginforge/wiki/Lifecycle)
- [Hooks](https://github.com/astrapi69/pluginforge/wiki/Hooks)
- [Extensions](https://github.com/astrapi69/pluginforge/wiki/Extensions)
- [FastAPI Integration](https://github.com/astrapi69/pluginforge/wiki/FastAPI-Integration)
- [Alembic Integration](https://github.com/astrapi69/pluginforge/wiki/Alembic-Integration)
- [i18n](https://github.com/astrapi69/pluginforge/wiki/i18n)
- [Security](https://github.com/astrapi69/pluginforge/wiki/Security)
- [Examples](https://github.com/astrapi69/pluginforge/wiki/Examples)
- [Changelog](https://github.com/astrapi69/pluginforge/wiki/Changelog)
- [Roadmap](https://github.com/astrapi69/pluginforge/wiki/Roadmap)

## Development

```bash
make install-dev   # Install with dev dependencies
make test          # Run tests
make lint          # Run ruff linter
make format        # Format code
make ci            # Full CI pipeline (lint + format-check + test)
make help          # Show all available targets
```

### Pre-commit hooks

This project ships a [pre-commit](https://pre-commit.com/) configuration that runs `ruff` and `ruff-format` on every commit. After `make install-dev`, register the git hook once per checkout:

```bash
poetry run pre-commit install
```

From that point, every `git commit` runs the hooks; if `ruff-format` rewrites a file, the commit is aborted so you can re-stage and try again. To run the hooks against the entire repo on demand:

```bash
poetry run pre-commit run --all-files
```

The ruff version pinned in `.pre-commit-config.yaml` matches the `ruff` dev dependency in `pyproject.toml`. Bump both together when upgrading.

## License

MIT

