Python Client
The Python client library provides an async gRPC interface to the WorkManager service.
Installation
From Gitea PyPI Registry
Set up authentication with a Gitea personal access token (scope: read:packages):
export PIP_USERNAME=<your-gitea-username>
export PIP_PASSWORD=<your-gitea-token>
pip install virtufin-workmanager \
--index-url https://pypi.haenerconsulting.com/api/packages/virtufin/pypi/simple/ \
--extra-index-url https://pypi.org/simple
From Local Source
pip install grpcio grpcio-reflection
export PYTHONPATH=/path/to/virtufin-workmanager/src/python
Quick Start
import asyncio
from virtufin import WorkManagerClient
async def main():
async with WorkManagerClient("localhost", 5102) as client:
# Create a worker
worker_id = await client.create_worker(
code_source={"content": "def Process(event):\n return {\"type\": \"com.example.output\", \"source\": \"test\", \"data\": event[\"data\"]}"},
mime_type="text/x-python",
topics=["my-topic"]
)
print(f"Created worker: {worker_id}")
# List workers
workers = await client.list_workers()
for w in workers:
print(f" {w['id']}: {w['status']}")
# Delete worker
await client.delete_worker(worker_id)
asyncio.run(main())
WorkManagerClient
The main client class for interacting with the WorkManager service.
Constructor
def __init__(self, host: str = "localhost", port: int = 5002)
| Parameter | Type | Default | Description |
|---|---|---|---|
host |
str |
"localhost" |
gRPC server hostname |
port |
int |
5102 |
gRPC server port |
Context Manager
async with WorkManagerClient() as client:
# client is connected
pass
# Automatically closed
connect()
Manually establish connection.
await client.connect()
close()
Close the connection.
await client.close()
Worker Operations
create_worker
Creates a new worker.
worker_id: str = await client.create_worker(
code_source: Dict[str, Any],
mime_type: str,
topics: List[str],
)
| Parameter | Type | Description |
|---|---|---|
code_source |
Dict |
Either {"url": "..."} or {"content": "base64_or_raw_string"} |
mime_type |
str |
MIME type like "text/x-python" |
topics |
List[str] |
Pub/sub topics to subscribe to (one or more) |
Returns: Worker ID string.
Example:
# Create with inline code
worker_id = await client.create_worker(
code_source={"content": "def Process(e): return {\"type\": \"output\", \"source\": \"w\", \"data\": e[\"data\"]}"},
mime_type="text/x-python",
topics=["events"]
)
# Create from URL
worker_id = await client.create_worker(
code_source={"url": "https://example.com/worker.py"},
mime_type="text/x-python",
topics=["events"]
)
load_code
Update worker code (consolidated: URL or content via CodeSource).
await client.load_code(id: str, code_source: CodeSource)
| Parameter | Type | Description |
|---|---|---|
id |
str |
Worker ID |
code_source |
CodeSource |
CodeSource(url=...) or CodeSource(content=...) |
set_tag
Set a single tag on a worker (resource metadata, not injected into engine).
await client.set_tag(id: str, key: str, value: str)
get_tag
Get a single tag value.
value: str | None = await client.get_tag(id: str, key: str)
set_tags
Replace all tags on a worker.
await client.set_tags(id: str, tags: dict[str, str])
get_tags
Get all tags for a worker.
tags: dict[str, str] = await client.get_tags(id: str)
delete_worker
Delete a worker.
await client.delete_worker(id: str)
| Parameter | Type | Description |
|---|---|---|
id |
str |
Worker ID to delete |
start_worker
Start a stopped worker.
await client.start_worker(id: str)
| Parameter | Type | Description |
|---|---|---|
id |
str |
Worker ID to start |
stop_worker
Stop a running worker.
await client.stop_worker(id: str)
| Parameter | Type | Description |
|---|---|---|
id |
str |
Worker ID to stop |
list_workers
List all workers.
workers: List[Dict[str, Any]] = await client.list_workers()
Returns: List of worker info dictionaries with keys:
- id - Worker ID
- code_source - {"url": ...} or {"content": ...}
- mime_type - MIME type string
- language - Language name
- topics - Subscribed topics
- created_at - ISO timestamp
- status - "STOPPED" or "RUNNING"
get_worker_history
Get code change history for a worker.
history: List[Dict[str, Any]] = await client.get_worker_history(id: str)
| Parameter | Type | Description |
|---|---|---|
id |
str |
Worker ID |
Returns: List of history entries with:
- code_source - Code source at that point
- created_at - ISO timestamp
recover_workers
Recover workers from persistent state.
count: int = await client.recover_workers()
Returns: Number of workers recovered.
create_worker_from_file
Convenience method to create a worker from a source file.
worker_id: Optional[str] = await client.create_worker_from_file(
file_path: Path,
mime_type: str = "text/x-python",
topics: List[str] = ("time-worker-topic",),
)
| Parameter | Type | Default | Description |
|---|---|---|---|
file_path |
Path |
- | Path to worker source file |
mime_type |
str |
"text/x-python" |
MIME type |
topics |
List[str] |
("time-worker-topic",) |
Topic names |
Via the API Gateway
The WorkManagerClient connects directly to the workmanager's gRPC port
(5102 by default). Alternatively, you can invoke workmanager methods
through the Virtufin API gateway
using the gateway's dynamic-dispatch pattern:
from virtufin.api.client import ApiClient
async with ApiClient() as client:
workmanager = client.gateway.workmanager
# ListWorkers — no request fields
workers = await workmanager.ListWorkers()
for w in workers["workers"]:
print(f" {w['id']}: {w['status']} ({w['language']})")
# CreateWorker — CodeSource oneof (url | content), mime_type, topics
create_result = await workmanager.CreateWorker({
"code_source": {"url": "https://example.com/worker.py"},
"mime_type": "text/x-python",
"topics": ["worker-commands"],
})
worker_id = create_result["id"]
# StartWorker — id field
await workmanager.StartWorker({"id": worker_id})
The gateway pattern is useful when: - The workmanager's gRPC port is not directly reachable (e.g., behind a service mesh, or only the gateway is exposed) - You want service discovery without configuring each backend's address explicitly - You're writing a tool that needs to talk to any of multiple backend services (workmanager, websocketmanager, custom) through one entry point
See the virtufin-api Python client docs for the full gateway client reference, and the proto-to-client-mapping spec §Layer 4 for the dynamic-dispatch call chain (gRPC reflection, JSON marshaling).
Complete Example
#!/usr/bin/env python3
"""Example: Complete worker lifecycle with the Python client."""
import asyncio
import base64
from pathlib import Path
from virtufin import WorkManagerClient
# Worker code as a string
WORKER_CODE = '''
from datetime import datetime
def Process(event):
return {
"type": "com.example.time-response",
"source": "python-worker",
"data": {
"current_time": datetime.now().isoformat(),
"received_event_id": event.get("id", "unknown")
}
}
'''
async def main():
async with WorkManagerClient("localhost", 5102) as client:
print("Creating worker...")
worker_id = await client.create_worker(
code_source={"content": WORKER_CODE},
mime_type="text/x-python",
topics=["time-worker-topic"]
)
print(f"Created: {worker_id}")
print("\nListing workers...")
workers = await client.list_workers()
for w in workers:
print(f" {w['id']} - {w['status']} - topics={w['topics']}")
print("\nUpdating worker code...")
new_code = '''
from datetime import datetime
def Process(event):
return {
"type": "com.example.time-response-v2",
"source": "python-worker-v2",
"data": {
"current_time": datetime.now().isoformat(),
"message": "Updated worker!"
}
}
'''
await client.load_code(worker_id, {"content": new_code.encode()})
print("Code updated")
print("\nGetting history...")
history = await client.get_worker_history(worker_id)
for i, entry in enumerate(history):
print(f" Version {i}: {entry['created_at']}")
print("\nStopping worker...")
await client.stop_worker(worker_id)
workers = await client.list_workers()
for w in workers:
if w['id'] == worker_id:
print(f" Status after stop: {w['status']}")
print("\nStarting worker...")
await client.start_worker(worker_id)
print("\nDeleting worker...")
await client.delete_worker(worker_id)
workers = await client.list_workers()
found = any(w['id'] == worker_id for w in workers)
print(f" Worker still listed: {found}")
if __name__ == "__main__":
asyncio.run(main())
Error Handling
All methods raise grpc.aio.AioRpcError on connection or RPC errors:
try:
await client.create_worker(...)
except grpc.aio.AioRpcError as e:
print(f"gRPC error: {e.code()} - {e.details()}")
Common gRPC status codes:
- StatusCode.NOT_FOUND - Worker doesn't exist
- StatusCode.INVALID_ARGUMENT - Invalid parameters
- StatusCode.UNAVAILABLE - Server not reachable
Direct gRPC Usage
If you need lower-level access, you can use grpcio-reflection directly. See examples/run_worker_grpc.py for a complete implementation using dynamic method invocation without generated stubs.