Metadata-Version: 2.4
Name: vcti-plugin-catalog
Version: 1.0.2
Summary: Generic plugin registry with typed descriptors and attribute-based filtering for Python
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-plugin-catalog
Project-URL: Changelog, https://github.com/vcollab/vcti-python-plugin-catalog/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: vcti-lookup>=1.0.1
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# Plugin Catalog

Generic plugin registry with typed descriptors and attribute-based filtering for Python.

## Overview

`vcti-plugin-catalog` is the registry layer for plugin-style architectures —
anywhere an application keeps a set of interchangeable components (parsers,
exporters, validators, loaders) and must pick the right one at runtime. A
`Descriptor[T]` pairs a component instance with its metadata: a unique id, a
display name, and a free-form `attributes` dict. A `Registry[D]` holds those
descriptors, retrieves them by id in constant time, and filters them by
attribute rules via [vcti-lookup](https://pypi.org/project/vcti-lookup/). The
registry does no discovery — it never imports, constructs, or invokes a plugin,
so the wiring and trust decisions stay in your code. Both classes are generic
and built to be subclassed into a typed, domain-specific catalog.

## Installation

```bash
pip install vcti-plugin-catalog>=1.0.2
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-plugin-catalog>=1.0.2",
]
```

---

## Quick Start

```python
from vcti.lookup import Rule
from vcti.plugincatalog import Descriptor, Registry

# Define your plugin type
class Parser:
    def __init__(self, extensions: list[str]):
        self.extensions = extensions

# Create descriptors with metadata
csv_parser = Descriptor(
    id="csv",
    name="CSV Parser",
    instance=Parser([".csv"]),
    attributes={"file_type": "text", "streaming": True},
)

json_parser = Descriptor(
    id="json",
    name="JSON Parser",
    instance=Parser([".json"]),
    attributes={"file_type": "text", "streaming": False},
)

binary_parser = Descriptor(
    id="hdf5",
    name="HDF5 Parser",
    instance=Parser([".h5", ".hdf5"]),
    attributes={"file_type": "binary", "streaming": False},
)

# Build a registry
registry = Registry()
registry.register_many([csv_parser, json_parser, binary_parser])

# Lookup by ID
parser = registry.get("csv").instance

# Filter by attributes
text_parsers = registry.lookup.filter([Rule("file_type", "==", "text")])
# [csv_parser_desc, json_parser_desc]

# Multiple rules (AND)
streaming = registry.lookup.filter([
    Rule("file_type", "==", "text"),
    Rule("streaming", "==", True),
])
# [csv_parser_desc]

# OR filtering
subset = registry.lookup.filter_any([
    Rule("file_type", "==", "binary"),
    Rule("streaming", "==", True),
])
# [csv_parser_desc, hdf5_parser_desc]

# First match
first = registry.lookup.first([Rule("file_type", "==", "text")])
```

---

## Core API

### Descriptor[T]

Generic container holding plugin metadata and instance:

| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | `str` | Unique identifier |
| `name` | `str` | Human-readable name |
| `instance` | `T` | The plugin instance |
| `description` | `str \| None` | Optional description |
| `attributes` | `dict[str, Any]` | Filterable metadata |

### Registry[D]

Collection manager for descriptors:

| Method | Description |
|--------|-------------|
| `register(descriptor)` | Add a descriptor |
| `register_many(descriptors)` | Add multiple descriptors |
| `unregister(id)` | Remove a descriptor |
| `unregister_many(ids)` | Remove multiple descriptors |
| `get(id)` | Retrieve by ID (raises `EntryNotFoundError`) |
| `find(id)` | Retrieve by ID or return `None` |
| `all()` | List all descriptors |
| `ids()` | List all IDs |
| `lookup` | Cached `Lookup` for attribute filtering |
| `create_lookup()` | New independent `Lookup` instance |
| `copy()` | Shallow copy of the registry |
| `clear()` | Remove all descriptors |
| `isolated(preload=)` | Context manager for test isolation |

The registry also supports `in`, `len()`, and `for ... in` iteration.

### Filtering

The `registry.lookup` property returns a `vcti.lookup.Lookup` instance
that filters against descriptor `.attributes`:

```python
from vcti.lookup import Rule

# AND — all rules must match
registry.lookup.filter([Rule("type", "==", "text"), Rule("active", "==", True)])

# OR — any rule matches
registry.lookup.filter_any([Rule("type", "==", "text"), Rule("type", "==", "csv")])

# NOT — exclude matches
registry.lookup.exclude([Rule("deprecated", "==", True)])

# First match
registry.lookup.first([Rule("type", "==", "text")])
```

See [vcti-lookup](https://pypi.org/project/vcti-lookup/) for the full
filtering API including operators, modifiers, and more.

### Lifecycle

```python
# Safe lookup (returns None instead of raising)
desc = registry.find("maybe-missing")

# Remove a plugin
registry.unregister("hdf5")

# Remove several at once
registry.unregister_many(["csv", "json"])

# Iterate over all registered descriptors
for desc in registry:
    print(desc.id, desc.name)
```

### Test Isolation

```python
with Registry.isolated(preload=[csv_parser, json_parser]) as test_reg:
    assert len(test_reg) == 2
    test_reg.unregister("csv")
    assert len(test_reg) == 1
# test_reg is discarded — the original registry is untouched
```

---

## Dependencies

- [vcti-lookup](https://pypi.org/project/vcti-lookup/) (>=1.0.1) — attribute-based filtering

---

## Documentation

| If you want to… | Read |
|---|---|
| Get started using the package | Quick Start above |
| See practical, real-world usage | [docs/patterns.md](docs/patterns.md) |
| Extend the library (new descriptor types, registry subclasses) | [docs/extending.md](docs/extending.md) |
| Understand the architecture and design decisions | [docs/design.md](docs/design.md) |
| Navigate and understand the source | [docs/source-guide.md](docs/source-guide.md) |
| Troubleshoot or check a common question | [docs/faq.md](docs/faq.md) |
