# Plugin Catalog

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

## Installation

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

### In `pyproject.toml` dependencies

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

---

## 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
