PyActuator
The actuation core of the Optophi family. PyActuator is a stateless, multi-domain Python package for performing side-effecting operations on external systems on behalf of strategy, state-machine, and agent components. Trading is one domain among several — Google Workspace, Comms (Slack / Email), Documents (PDF / Markdown / HTML), and Database (Postgres, SQLite, MongoDB, Redis) are first-class siblings.
Features
- Multi-domain — 15 domains under
pyactuator.domains.*, each with the same shape (types,protocols,errors,adapters/): market data (alpaca,fmp,massive,polymarket,finance), execution (trading), workspace/comms (google,comms,documents), data (database,datasets,data_sources,analytics,charts), andresearch. See Domains Overview. - Stateless core —
pyactuator.coreships envelopes, idempotency, retry, audit, policy, registry, and spec primitives. No I/O. - Adapter pattern — every external system is an adapter behind a
Protocol. Mock adapters ship with every domain, andpyactuator.testingwires them into a registry for your own tests. See Testing. - Bridges — install pyactuator operations into PyStator state
machines (
integrations.pystator) and PyGubernator tool registries (integrations.pygubernator) with one call. - MCP server —
pyactuator mcp serveoffers a tool profile's operations to Claude Desktop, Claude Code and any other MCP client, each call validated, governed and audited. See MCP server. - Operation plugins — installed packages can add their own operations to
the catalog, available through
invoke(), MCP and PyStator actions. See Publish your own operation. - Deployment manifest — one
pyactuator.yamldeclares the agents sharing a runtime: each one's tool profile, credentials and policy.pyactuator preflight --strictchecks it before a deploy. See Deployment manifest. - CLI —
pyactuator runfor unified YAML/JSON configs,pyactuator tools listfor valid operation names, plusapi,ui,worker, anddocshelpers. - HTTP API — domain-scoped FastAPI routers under
/api/v1/<domain>/..., sharing a singleAdapterRegistry. - Optional deps —
alpaca-py,httpx,google-auth,jinja2,markdown-it-py, andreportlabare all optional extras, so you install only the vendors you actuate. The base install pullsduckdb+pyarrowfor the analytics, datasets, data-source, and charts domains. - Backwards compatible — existing
from pyactuator import OrderRequestimports continue to work as re-exports of the trading domain.
Installation
# Core only (envelopes, registry, mock adapters) — no mandatory dependencies
pip install pyactuator
# Data engine (analytics / datasets / data sources / charts) — DuckDB + PyArrow
pip install pyactuator[analytics]
# Trading with Alpaca broker
pip install pyactuator[trading-alpaca]
# Google Workspace (Drive / Sheets / Docs / Calendar)
pip install pyactuator[google]
# Slack via webhook or bot token
pip install pyactuator[comms-slack]
# PDF + Markdown rendering
pip install pyactuator[documents-pdf,documents-md]
# Local file read (PDF text extraction + images; pypdf + Pillow)
pip install pyactuator[documents-read]
# MCP server (`pyactuator mcp serve`)
pip install pyactuator[mcp]
# Everything
pip install pyactuator[all]
# Development
pip install -e ".[dev]"
Documentation
Published docs (Material for MkDocs) live at optophi.github.io/pyactuator.
To build or preview locally:
pip install pyactuator[docs] # or: pip install -e ".[dev]" (includes docs tooling)
mkdocs serve
The site configuration mirrors PyStator / PyCharter: mkdocs.yml, mkdocs.optophi.yml for the Optophi.com docs deployment, and docs/ as the documentation root. CI runs mkdocs build --strict when mkdocs.yml is present (see ./scripts/ci.sh).
Quick start: invoke (the config-driven entry point)
pyactuator.sync.invoke / pyactuator.invoke dispatch any catalogued operation
by name against a mock (or real) AdapterRegistry and return the canonical wire
envelope — the recommended entry point when you don't need a specific domain's
typed objects. In async code, await pyactuator.invoke(...); the sync facade is
for synchronous callers and also works from inside a running event loop.
from pyactuator import list_operations
from pyactuator.sync import invoke
# Every operation name, e.g. "pyactuator.trading.place_order",
# "pyactuator.google.sheets_append", "pyactuator.comms.slack_send" ...
print(list_operations())
result = invoke(
"pyactuator.trading.place_order",
{
"symbol": "AAPL",
"side": "buy",
"quantity": "10",
"order_type": "market",
"client_order_id": "my-order-001", # stable idempotency key
},
)
print(result["success"], result["outcome"]) # True SUCCESS
print(result["adapter"]["real"]) # False — a mock served this call
Every envelope carries an adapter block, so a mock success is never mistaken
for a real one. Pass require_real_adapters=True to refuse to run against a mock
(or set PYACTUATOR_STRICT_ACTUATION=1 to make that the deployment default), or
a configured registry= to actuate against real credentials.
Pass a policy_enforcer= to decide before the adapter is called. A denied call
returns error.code: "POLICY_DENIED"; a dry-run decision returns dry_run: true
with success: false, because nothing happened. pyactuator.runner.run,
register_pyactuator_actions, register_pyactuator_tools and the MCP server's
CatalogToolset accept the same argument.
from decimal import Decimal
from pyactuator.core import LimitNotional
from pyactuator.sync import invoke
result = invoke(
"pyactuator.trading.place_order",
{
"symbol": "AAPL",
"side": "buy",
"quantity": "100",
"order_type": "limit",
"limit_price": "250",
"client_order_id": "my-order-002",
},
policy_enforcer=LimitNotional(Decimal("10000")),
)
print(result["success"], result["error"]["code"]) # False POLICY_DENIED
Quick start: Trading (mock broker)
import asyncio
from decimal import Decimal
from pyactuator import OrderRequest, Side, OrderType, TimeInForce
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
async def main() -> None:
client = MockExecutionClient()
response = await client.submit(OrderRequest(
client_order_id="my-order-001",
symbol="AAPL",
side=Side.BUY,
quantity=Decimal("10"),
order_type=OrderType.MARKET,
time_in_force=TimeInForce.DAY,
))
print(response.success, response.external_order_id)
status = await client.get_status(response.external_order_id)
print(status.status) # ExecutionStatus.FILLED
await client.close()
asyncio.run(main())
Quick start: Google Sheets (mock)
import asyncio
from pyactuator.domains.google.types import AppendRowsRequest
from pyactuator.domains.google.adapters.mock import MockSheetsClient
async def main() -> None:
sheets = MockSheetsClient()
# create_spreadsheet takes the title and returns the new spreadsheet id.
spreadsheet_id = await sheets.create_spreadsheet(title="Daily PnL")
response = await sheets.append_rows(AppendRowsRequest(
spreadsheet_id=spreadsheet_id,
range="Sheet1!A1",
values=[["2025-05-07", "AAPL", 1234.56]],
))
print(spreadsheet_id, response.success)
asyncio.run(main())
Quick start: PyStator action
PyActuator's PyStator bridge installs every domain operation as a
kwargs-aware action callable on a pystator.actions.ActionRegistry.
State machines call them by name from YAML, with parameters templated
from the FSM context.
import asyncio
from pystator.actions import ActionExecutor, ActionRegistry, ActionSpec
from pyactuator.core.audit import MemoryAuditWriter
from pyactuator.core.registry import AdapterRegistry
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
from pyactuator.integrations.pystator import register_pyactuator_actions
async def main() -> None:
adapters = AdapterRegistry()
adapters.register("trading", "broker", MockExecutionClient())
audit = MemoryAuditWriter()
registry = ActionRegistry()
register_pyactuator_actions(registry, adapters=adapters, audit_writer=audit)
executor = ActionExecutor(registry)
result = await executor.async_execute_action_spec(
ActionSpec(
name="pyactuator.trading.place_order",
params={
"symbol": "AAPL",
"side": "buy",
"quantity": 1,
"order_type": "market",
"client_order_id": "coid-1",
},
),
context={},
)
# The action's wire envelope is on ``result.result``.
print(result.success, result.result["result"]["external_order_id"])
print("audit records:", len(audit.records))
asyncio.run(main())
Quick start: PyGubernator tool
from pyactuator.core.registry import AdapterRegistry
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
from pyactuator.integrations.pygubernator import (
iter_pyactuator_tool_specs,
register_pyactuator_tools,
)
from pygubernator.tools import ToolRegistry
adapters = AdapterRegistry()
adapters.register("trading", "broker", MockExecutionClient())
tools = ToolRegistry()
register_pyactuator_tools(tools, adapters=adapters)
for spec in iter_pyactuator_tool_specs():
print(spec.name, spec.metadata.side_effects, spec.metadata.idempotent)
Architecture
┌────────────────────────────────────────────────────────────┐
│ Callers │
│ ───────────────────────────────────────────────── │
│ PyStator FSMs PyGubernator agents HTTP clients │
└────────────┬──────────────┬──────────────┬─────────────────┘
│ │ │
actions tools /api/v1/<domain>
│ │ │
┌────────────▼──────────────▼──────────────▼─────────────────┐
│ pyactuator │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ core: envelopes • idempotency • retry • audit │ │
│ │ policy • errors • registry • spec │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌────────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ trading │ │ google │ │ comms │ │ documents │ │
│ │ Alpaca/Mock│ │Drive/Sheets│ │Slack/Email│ │PDF/MD/HTML │ │
│ └────────────┘ └──────────┘ └──────────┘ └─────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ integrations: pystator • pygubernator │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
│
┌────────┴────────┬────────────┬───────────┐
▼ ▼ ▼ ▼
Broker API Google APIs Slack API SMTP / SendGrid
(Alpaca, IB) (Drive/Sheets)
See ARCHITECTURE.md for the full layer model and
docs/guides/ for the per-topic guides:
domains-overview.md— what domains ship today and which surface to call them through.adding-a-domain.md— step-by-step recipe for adding a new domain.bridges.md— PyStator and PyGubernator bridge usage.
Domains shipped today
| Domain | Operations | Adapters |
|---|---|---|
trading |
submit, get_status, cancel, subscribe_fills, get_positions, get_account |
MockExecutionClient, AlpacaExecutionClient |
google |
Sheets append/read, Drive upload, Docs replace text / create | Mock + HTTPX-backed real adapters |
comms |
Slack send (webhook / API), Email send (SMTP / SendGrid) | Mock + real adapters |
documents |
Render templated PDF / Markdown / HTML | MockDocumentRenderer, JinjaMarkdownRenderer, ReportlabPdfRenderer |
Trading: error hierarchy
The trading domain still exposes its full error hierarchy from the
package root (re-exported from pyactuator.domains.trading.errors):
PyActuatorError
├── OrderValidationError # bad OrderRequest fields
├── OrderRejectedError # broker explicitly rejected
├── OrderNotFoundError # get_status / cancel on unknown order
├── BrokerConnectionError # network / timeout (retryable)
└── BrokerAdapterError # adapter misconfiguration
from pyactuator import (
PyActuatorError,
OrderNotFoundError,
BrokerConnectionError,
)
try:
status = await client.get_status("unknown-id")
except OrderNotFoundError:
print("Order does not exist at broker")
except BrokerConnectionError:
print("Transient failure — safe to retry")
except PyActuatorError:
print("Catch-all for any pyactuator error")
Adapter registry
All bridges and the FastAPI app share a single
pyactuator.core.registry.AdapterRegistry keyed on (domain, kind):
from pyactuator.core.registry import AdapterRegistry
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
from pyactuator.domains.comms.adapters.mock import MockSlackClient
adapters = AdapterRegistry()
adapters.register("trading", "broker", MockExecutionClient())
adapters.register("comms", "slack", MockSlackClient())
broker = adapters.resolve("trading", "broker")
slack = adapters.resolve("comms", "slack")
Swapping a real adapter for a mock at startup is a one-liner.
Audit
Every bridge call writes an AuditRecord through an injected
AuditWriter:
from pystator.actions import ActionRegistry
from pyactuator.core.audit import MemoryAuditWriter
from pyactuator.core.registry import AdapterRegistry
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
from pyactuator.integrations.pystator import register_pyactuator_actions
adapters = AdapterRegistry()
adapters.register("trading", "broker", MockExecutionClient())
audit = MemoryAuditWriter()
register_pyactuator_actions(ActionRegistry(), adapters=adapters, audit_writer=audit)
# After actions run, each call has left one record:
for record in audit.records:
print(record.operation, record.success, record.correlation_id)
NullAuditWriter is the default; production deployments wire their own
writer (e.g. backed by a database or a log shipper).
HTTP API
Each domain ships a FastAPI router under /api/v1/<domain>/...:
| Route | Purpose |
|---|---|
POST /api/v1/orders |
Submit an order |
GET /api/v1/orders/{id} |
Order status |
POST /api/v1/google/sheets/append |
Append rows to a sheet |
POST /api/v1/google/drive/upload |
Upload a file to Drive |
POST /api/v1/google/drive/share |
Grant a role on a Drive file to an address |
POST /api/v1/google/docs/replace-text |
Replace text in a Google Doc |
POST /api/v1/comms/slack/send |
Send a Slack message |
POST /api/v1/comms/email/send |
Send an email |
POST /api/v1/documents/render |
Render a templated document |
GET /api/v1/documents/local/roots |
List configured local-read root keys (safe names only) |
POST /api/v1/documents/local/read |
Read txt / csv / pdf / image under allowlisted roots |
POST /api/v1/database/postgres/query |
Postgres read-only SQL |
POST /api/v1/database/sqlite/query |
SQLite read-only SQL |
POST /api/v1/database/mongo/find |
MongoDB find (bounded) |
POST /api/v1/database/redis/string-get |
Redis GET |
Adapters resolve from app.state.adapter_registry. See
pyactuator.api.main for the wiring and pyactuator.api.dependencies.*
for the resolvers.
API reference (trading)
Protocols
| Protocol | Methods |
|---|---|
ExecutionClient |
submit, get_status, cancel, subscribe_fills, close |
BrokerQueryClient |
get_positions, get_position, get_account |
Types
| Type | Purpose |
|---|---|
OrderRequest |
Submit order params (validates on construction) |
OrderResponse |
Submission result (success, external_order_id, status) |
OrderStatus |
Full order status from broker |
Fill |
Execution/fill report |
CancelResponse |
Cancellation result |
Position |
Current position for a symbol |
Account |
Brokerage account info |
Enums
| Enum | Values |
|---|---|
Side |
BUY, SELL |
OrderType |
MARKET, LIMIT, STOP, STOP_LIMIT |
TimeInForce |
DAY, GTC, IOC, FOK, OPG, CLS |
ExecutionStatus |
PENDING_NEW, OPEN, PARTIALLY_FILLED, FILLED, CANCELED, REJECTED, EXPIRED |
Adapters
| Adapter | Install | Protocols |
|---|---|---|
MockExecutionClient |
core | ExecutionClient + BrokerQueryClient |
AlpacaExecutionClient |
pyactuator[trading-alpaca] |
ExecutionClient + BrokerQueryClient |
RetryExecutionClient |
core | wraps any ExecutionClient with retry |
License
MIT.
Metadata
Release files for pyactuator 0.0.17
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pyactuator-0.0.17.tar.gz | 2.2 MB | With provenance | Details for pyactuator-0.0.17.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pyactuator-0.0.17-py3-none-any.whl | Python 3 | none | any | Details for pyactuator-0.0.17-py3-none-any.whl |
Total release size: 4.5 MB
Release files / pyactuator-0.0.17.tar.gz
| Download URL | pyactuator-0.0.17.tar.gz |
|---|---|
| Size | 2.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4f578c282b8c3a5bce41efac13c9891c1c9e2a0369125ee3eebfee1e3669e688
|
|
BLAKE2b-256 checksum How to use checksums |
bf48557415d71c535cea94eea34b2c774e8e1113b40bcd25693c1830d82576d2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / pyactuator-0.0.17-py3-none-any.whl
| Download URL | pyactuator-0.0.17-py3-none-any.whl |
|---|---|
| Size | 2.2 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a9e77b4c929055d8b7668c6ca23ce041bb58b43a01db9c328d9d5eeb71974a48
|
|
BLAKE2b-256 checksum How to use checksums |
d74f6f408dc32454f948cfec1d20f664c911efe411c9fbe96ad5c2fb356eefe0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logSecurity for pyactuator 0.0.17
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pyactuator-0.0.17.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.