Metadata-Version: 2.5
Name: service-registrar
Version: 0.2.0
Summary: plugin mechanism for service registration and discovery
Author-email: Louis Zhuang <yzhuang@outlook.com>
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# Service Registrar

A small provider registry for Python libraries that depend on services through
ports (interfaces) while allowing adapters to be supplied by independently
installed packages.

## Define a port and inject an adapter

Keep the port in the library that needs the service. Construct the library with
the selected adapter rather than having it discover or instantiate providers:

```python
from typing import Protocol


class ObjectStore(Protocol):
    def read(self, key: str) -> bytes: ...


class Catalog:
    def __init__(self, store: ObjectStore) -> None:
        self._store = store


# The composition root chooses the implementation and injects it.
catalog = Catalog(store=registry.create(ObjectStore, provider="filesystem"))
```

## Publish a provider plugin

A provider package exports a `Provider` registration whose service is the
consumer's port and whose factory creates the adapter:

```python
from collections.abc import Mapping

from service_registrar import Provider, ProviderRegistry
from my_library.ports import ObjectStore


class FilesystemStore:
    def __init__(
        self,
        *,
        config: Mapping[str, object],
        registry: ProviderRegistry,
    ) -> None:
        self._root = str(config["root"])
        self._registry = registry

    def read(self, key: str) -> bytes:
        ...


FILESYSTEM_STORE = Provider(service=ObjectStore, factory=FilesystemStore)
```

Declare that registration in the provider package's `pyproject.toml`:

```toml
[project.entry-points."service_registrar.providers"]
filesystem = "my_store_plugin:FILESYSTEM_STORE"
```

The application discovers installed plugins and explicitly selects a provider
when composing the library:

```python
from service_registrar import get_registry

registry = get_registry()
catalog = Catalog(store=registry.create(ObjectStore, provider="filesystem"))
```

`get_registry()` returns a process-wide singleton. Plugin discovery is lazy:
the first `create()` or `dump()` call discovers installed entry points once.
Use `ProviderRegistry(...)` directly when an isolated registry or custom
in-memory configuration is needed.

By default, `ProviderRegistry` reads `service-registrar.toml` from the current
working directory. Each top-level TOML table is a provider's configuration
section, and `create()` passes the selected provider's section to its factory.
For example:

```python
[filesystem]
root = "/var/lib/catalog"
```

When the file is absent or a provider has no section, the factory receives an
empty mapping. Pass `config=...` to `ProviderRegistry` to override file loading
with an in-memory mapping.

Factories are called with named `config` and `registry` arguments. The injected
registry can be used to recursively construct other services:

```python
class Catalog:
    def __init__(self, *, registry: ProviderRegistry) -> None:
        self._store = registry.create(ObjectStore, provider="filesystem")


def create_catalog(
    *,
    config: Mapping[str, object],
    registry: ProviderRegistry,
) -> Catalog:
    return Catalog(registry=registry)
```

`ProviderRegistry.create()` may omit the provider name only when exactly one
provider is registered for the port. It raises a clear error for missing or
ambiguous providers. Factories are called when `create()` is called; lifecycle
and caching remain under the application's control.

The entry-point name is the provider's registration name, so it is only
declared once for plugins. For manual registration, pass the name explicitly:

```python
registry.register("filesystem", FILESYSTEM_STORE)
```

Run `service-registrar` to discover installed providers and print the available
services and their provider names.
