Metadata-Version: 2.4
Name: osp-provider-runtime
Version: 0.3.13
Summary: Thin runtime harness for OSP providers (RabbitMQ transport + contract execution).
Author: OSP Team
License: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: loguru<1,>=0.7
Requires-Dist: osp-provider-contracts<0.4,>=0.3.3
Requires-Dist: pika<2,>=1.3
Provides-Extra: dev
Requires-Dist: build<2,>=1.2; extra == 'dev'
Requires-Dist: hatch<2,>=1.14; extra == 'dev'
Requires-Dist: pytest<9,>=8.3; extra == 'dev'
Requires-Dist: ruff>=0.15; extra == 'dev'
Requires-Dist: twine<7,>=6; extra == 'dev'
Requires-Dist: ty>=0.0.18; extra == 'dev'
Description-Content-Type: text/markdown

# osp-provider-runtime

Thin, boring runtime harness for OSP providers.

This package handles RabbitMQ message plumbing so provider implementations can
focus on business logic.

## What it does (v0.1)

- Parses a versioned request envelope.
- Builds provider `RequestContext`/`ProviderRequest` and calls `execute(...)`.
- Emits canonical asynchronous task updates and synchronous control-RPC replies.
- Applies explicit ack/requeue/dead-letter decisions.
- Emits structured logs for delivery decisions.
- Supports explicit runtime knobs for prefetch/concurrency/retries/transport timeouts/DLQ.
- Accepts contract_v1 request envelopes.
- Provides `ProviderIdentity` so providers derive the same RabbitMQ
  queue/binding, routing prefix, and lane name from `PROVIDER`,
  `PROVIDER_INSTANCE`, and optional `PROVIDER_INSTANCE_ID`.

## Provider identity

Use a provider family for permissions and API requests, then add an instance
only when you need a separate runtime lane:

```python
from osp_provider_runtime import ProviderIdentity

identity = ProviderIdentity(
    provider="nrec",
    instance="pr",
    instance_id=37,
)

identity.routing_prefix   # "nrec.pr.37"
identity.request_queue    # "provider.nrec.pr.37"
identity.request_binding  # "nrec.pr.37.#"
identity.provider_lane    # "nrec_pr"
```

The common case stays small: `ProviderIdentity("vmware", "dev")` gives the
`vmware.dev` routing prefix and `vmware_dev` lane name. Instance IDs refine
routing within a lane without changing its attribution name.

## Result payload conventions

Provider results should keep `ProviderResult.data` focused on the **resolved**
values for the task. The runtime builds the full update payload envelope:

- `success`: provider outcome flag; `false` still represents a completed no-op
  or degraded outcome
- `resolved`: your `ProviderResult.data` (after runtime normalization)
- `provenance`: optional; extracted from `ProviderResult.data["provenance"]`
- `dry_run`: derived from the request payload

Raise a `ProviderError` when the task should enter a failed lifecycle state.
`ProviderResult(success=False)` does not turn a completed outcome into a task
failure.

Special keys in `ProviderResult.data`:

- `progress_events` (list): lifted into the update payload and removed from
  `resolved` to avoid duplicate TaskEvent rows.
- `provenance` (dict): lifted into the top-level `provenance` field.

Providers may opt into live updates by accepting a keyword-only `progress`
argument on `execute`. Use `ProgressReporter` so the same action also works
without live delivery:

```python
from osp_provider_contracts import ProviderResult
from osp_provider_runtime import ProgressReporter


def execute(self, action, request, context, *, progress=None):
    reporter = ProgressReporter(progress)
    reporter.report("Creating virtual machine", {"stage": "openstack_create"})
    vm = create_virtual_machine(request)
    return ProviderResult(
        success=True,
        message="Virtual machine created",
        external_id=vm.id,
        data={"progress_events": reporter.events},
    )
```

With a callback, the event is published live and `reporter.events` stays
empty. Without one, the event is returned through `progress_events` and the
runtime expands it from the terminal update. This keeps each event on one
delivery path. The callback is best-effort and transport-owned. Providers
without the argument keep the original three-argument contract unchanged.

## Task logging

The runtime binds the task context once around `execute()` and emits one
`task.summary` event when the delivery finishes. Provider log sinks must use
JSON serialization so these fields are retained:

- `task_id`: cross-service join key
- `dispatch_id`: one delivery attempt
- `provider` and `task_type`
- `step`, `outcome`, and `duration_ms`
- `error.type`, `error.message`, and `error.code` when applicable

Use `task_step("external.operation")` around meaningful external operations.
It records the step in the summary trail and logs a full traceback before
re-raising failures. Task code must not bind correlation fields itself.

```python
from osp_provider_runtime import task_step

with task_step("openstack.create"):
    server = openstack.create_server(...)
```

Avoid embedding envelope-shaped keys (`requested`, `resolved`,
`request_input`, `request_defaults`) inside `ProviderResult.data`. The runtime
will drop them to keep the stored result compact and predictable.

The orchestrator already owns the original task request. Provider updates do
not echo it; HTTP and CLI read models compose request and result when needed.

## What it does not do

- No provider framework.
- No plugin system.
- No workflow orchestration.

## Install

```bash
pip install osp-provider-runtime
```

## Development

```bash
env -u VIRTUAL_ENV uv sync --extra dev
hatch shell
hatch run dev:check
hatch run dev:build
hatch run dev:verify
```

## Runtime Knobs

Set these via `RuntimeConfig` in your provider `runtime_app.py`:

- `prefetch_count` (default `1`)
- `concurrency` (default `1`)
- `max_attempts` (default `5`)
- `idempotency_cache_max_entries` (default `1024`)
- `dead_letter_exchange` (optional)
- `dead_letter_routing_key` (optional)
- `heartbeat_seconds` (default `60`)
- `blocked_connection_timeout_seconds` (default `30`)

## Update Emission

Use `TaskReporter` for provider task lifecycle updates. The runtime keeps
transport details compatible with orchestrator consumers.

Docs:
- `docs/runtime-contract.md`
- `docs/provider-updates.md`
- `docs/migration-task-reporter.md`
- `docs/runtime-upgrade-checklist.md`
- `docs/release-notes-task-reporter.md`

Tag and push:

```bash
git tag v0.2.0
git push origin v0.2.0
```
