Metadata-Version: 2.4
Name: pyactuator
Version: 0.0.8
Summary: Multi-domain actuation core: trading, Google Workspace, comms, documents, database — plus PyStator and PyGubernator bridges
Author-email: Optophi <contact@optophi.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/optophi/pyactuator
Project-URL: Documentation, https://optophi.github.io/pyactuator/
Project-URL: Repository, https://github.com/optophi/pyactuator
Project-URL: Issues, https://github.com/optophi/pyactuator/issues
Project-URL: Changelog, https://github.com/optophi/pyactuator/blob/main/CHANGELOG.md
Project-URL: Contributing, https://github.com/optophi/pyactuator/blob/main/CONTRIBUTING.md
Keywords: actuation,actuator,automation,trading,execution,broker,alpaca,google,workspace,drive,sheets,docs,slack,email,smtp,documents,pdf,markdown,pystator,pygubernator
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Classifier: Topic :: Office/Business
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: duckdb<2,>=1.0
Requires-Dist: pyarrow<25,>=14
Provides-Extra: trading-alpaca
Requires-Dist: alpaca-py>=0.14.0; extra == "trading-alpaca"
Provides-Extra: google
Requires-Dist: httpx>=0.24.0; extra == "google"
Requires-Dist: google-auth>=2.52.0; extra == "google"
Provides-Extra: comms-slack
Requires-Dist: httpx>=0.24.0; extra == "comms-slack"
Provides-Extra: comms-email
Provides-Extra: comms-twilio
Requires-Dist: httpx>=0.24.0; extra == "comms-twilio"
Provides-Extra: documents-pdf
Requires-Dist: reportlab>=4.0.0; extra == "documents-pdf"
Provides-Extra: documents-md
Requires-Dist: jinja2>=3.0.0; extra == "documents-md"
Requires-Dist: markdown-it-py>=3.0.0; extra == "documents-md"
Provides-Extra: documents-read
Requires-Dist: pypdf<7,>=5.0.0; extra == "documents-read"
Requires-Dist: pillow<13,>=10.0.0; extra == "documents-read"
Provides-Extra: research-tavily
Requires-Dist: httpx>=0.24.0; extra == "research-tavily"
Provides-Extra: research-brave
Requires-Dist: httpx>=0.24.0; extra == "research-brave"
Provides-Extra: fmp
Requires-Dist: httpx>=0.24.0; extra == "fmp"
Provides-Extra: massive
Requires-Dist: httpx>=0.24.0; extra == "massive"
Provides-Extra: market-data
Requires-Dist: httpx>=0.24.0; extra == "market-data"
Provides-Extra: massive-stream
Requires-Dist: websockets<17,>=13; extra == "massive-stream"
Provides-Extra: bridges-pystator
Provides-Extra: bridges-pygubernator
Provides-Extra: alpaca
Requires-Dist: alpaca-py>=0.14.0; extra == "alpaca"
Provides-Extra: api
Requires-Dist: fastapi>=0.136.1; extra == "api"
Requires-Dist: uvicorn[standard]>=0.24.0; extra == "api"
Requires-Dist: pydantic>=2.13.4; extra == "api"
Requires-Dist: pydantic-settings>=2.0.0; extra == "api"
Requires-Dist: httpx>=0.24.0; extra == "api"
Requires-Dist: python-multipart>=0.0.6; extra == "api"
Requires-Dist: PyJWT>=2.8.0; extra == "api"
Requires-Dist: sqlalchemy<3.0,>=2.0.0; extra == "api"
Requires-Dist: alembic<2.0,>=1.13.0; extra == "api"
Provides-Extra: otel
Requires-Dist: opentelemetry-api<2,>=1.24.0; extra == "otel"
Requires-Dist: opentelemetry-sdk<2,>=1.24.0; extra == "otel"
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.49b0; extra == "otel"
Requires-Dist: opentelemetry-instrumentation-httpx>=0.62b1; extra == "otel"
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.24.0; extra == "otel"
Provides-Extra: store-postgres
Requires-Dist: psycopg[binary]<4,>=3.1; extra == "store-postgres"
Provides-Extra: store-mongo
Requires-Dist: pymongo<5,>=4.6; extra == "store-mongo"
Provides-Extra: database-postgres
Requires-Dist: psycopg[binary]<4,>=3.1; extra == "database-postgres"
Provides-Extra: database-sqlite
Provides-Extra: database-mongo
Requires-Dist: pymongo<5,>=4.6; extra == "database-mongo"
Provides-Extra: database-redis
Requires-Dist: redis[hiredis]<9,>=5; extra == "database-redis"
Provides-Extra: data-sources-postgres
Requires-Dist: psycopg[binary]<4,>=3.1; extra == "data-sources-postgres"
Provides-Extra: data-sources-bigquery
Requires-Dist: google-cloud-bigquery<4,>=3.0; extra == "data-sources-bigquery"
Provides-Extra: data-sources-snowflake
Requires-Dist: snowflake-connector-python<5,>=3.0; extra == "data-sources-snowflake"
Provides-Extra: data-sources-redshift
Requires-Dist: redshift-connector<3,>=2.0; extra == "data-sources-redshift"
Provides-Extra: data-sources-files
Provides-Extra: ui
Requires-Dist: fastapi>=0.136.1; extra == "ui"
Requires-Dist: uvicorn[standard]>=0.24.0; extra == "ui"
Requires-Dist: httpx>=0.24.0; extra == "ui"
Requires-Dist: aiofiles>=23.0.0; extra == "ui"
Provides-Extra: all
Requires-Dist: alpaca-py>=0.14.0; extra == "all"
Requires-Dist: httpx>=0.24.0; extra == "all"
Requires-Dist: google-auth>=2.52.0; extra == "all"
Requires-Dist: reportlab>=4.0.0; extra == "all"
Requires-Dist: jinja2>=3.0.0; extra == "all"
Requires-Dist: markdown-it-py>=3.0.0; extra == "all"
Requires-Dist: pypdf<7,>=5.0.0; extra == "all"
Requires-Dist: pillow<13,>=10.0.0; extra == "all"
Requires-Dist: fastapi>=0.136.1; extra == "all"
Requires-Dist: uvicorn[standard]>=0.24.0; extra == "all"
Requires-Dist: pydantic>=2.13.4; extra == "all"
Requires-Dist: pydantic-settings>=2.0.0; extra == "all"
Requires-Dist: python-multipart>=0.0.6; extra == "all"
Requires-Dist: PyJWT>=2.8.0; extra == "all"
Requires-Dist: aiofiles>=23.0.0; extra == "all"
Requires-Dist: sqlalchemy<3.0,>=2.0.0; extra == "all"
Requires-Dist: alembic<2.0,>=1.13.0; extra == "all"
Requires-Dist: websockets<17,>=13; extra == "all"
Requires-Dist: opentelemetry-api<2,>=1.24.0; extra == "all"
Requires-Dist: opentelemetry-sdk<2,>=1.24.0; extra == "all"
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.49b0; extra == "all"
Requires-Dist: opentelemetry-instrumentation-httpx>=0.62b1; extra == "all"
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.24.0; extra == "all"
Requires-Dist: psycopg[binary]<4,>=3.1; extra == "all"
Requires-Dist: pymongo<5,>=4.6; extra == "all"
Requires-Dist: redis[hiredis]<9,>=5; extra == "all"
Provides-Extra: docs
Requires-Dist: mkdocs<2,>=1.6.1; extra == "docs"
Requires-Dist: mkdocs-material>=9.7.6; extra == "docs"
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "docs"
Requires-Dist: pymdown-extensions>=10.0.0; extra == "docs"
Requires-Dist: mike>=2.2.0; extra == "docs"
Provides-Extra: ci
Requires-Dist: pyactuator[api,documents-read]; extra == "ci"
Requires-Dist: pytest>=7.0.0; extra == "ci"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "ci"
Requires-Dist: pytest-cov>=4.0.0; extra == "ci"
Requires-Dist: pytest-timeout>=2.1.0; extra == "ci"
Requires-Dist: pytest-xdist>=3.5.0; extra == "ci"
Requires-Dist: ruff>=0.9.0; extra == "ci"
Requires-Dist: mypy>=1.0.0; extra == "ci"
Requires-Dist: pre-commit>=3.0.0; extra == "ci"
Requires-Dist: mkdocs<2,>=1.6.1; extra == "ci"
Requires-Dist: mkdocs-material>=9.7.6; extra == "ci"
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "ci"
Requires-Dist: pymdown-extensions>=10.0.0; extra == "ci"
Provides-Extra: dev
Requires-Dist: pyactuator[api]; extra == "dev"
Requires-Dist: pyactuator[documents-read]; extra == "dev"
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: pytest-timeout>=2.1.0; extra == "dev"
Requires-Dist: pytest-xdist>=3.5.0; extra == "dev"
Requires-Dist: ruff>=0.9.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
Requires-Dist: build>=0.10.0; extra == "dev"
Requires-Dist: twine>=6.2.0; extra == "dev"
Requires-Dist: requests>=2.32.3; extra == "dev"
Requires-Dist: mkdocs<2,>=1.6.1; extra == "dev"
Requires-Dist: mkdocs-material>=9.7.6; extra == "dev"
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "dev"
Requires-Dist: pymdown-extensions>=10.0.0; extra == "dev"
Requires-Dist: mike>=2.2.0; extra == "dev"
Dynamic: license-file

# 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** — `pyactuator.domains.{trading,google,comms,documents,database}`,
  each with the same shape: `types`, `protocols`, `errors`, `adapters/`.
- **Stateless core** — `pyactuator.core` ships 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.
- **Bridges** — install pyactuator operations into PyStator state
  machines (`integrations.pystator`) and PyGubernator tool registries
  (`integrations.pygubernator`) with one call.
- **CLI** — `pyactuator run` for unified YAML/JSON configs, `pyactuator tools list`
  for valid operation names, plus `api`, `ui`, `worker`, and `docs` helpers.
- **HTTP API** — domain-scoped FastAPI routers under
  `/api/v1/<domain>/...`, sharing a single `AdapterRegistry`.
- **Optional deps** — `alpaca-py`, `httpx`, `google-auth`, `jinja2`,
  `markdown-it-py`, `reportlab` are all optional extras. The core has
  zero mandatory dependencies.
- **Backwards compatible** — existing `from pyactuator import OrderRequest`
  imports continue to work as re-exports of the trading domain.

## Installation

```bash
# Core only (envelopes, registry, mock adapters)
pip install pyactuator

# Trading with Alpaca broker
pip install pyactuator[trading-alpaca]

# Google Workspace (Drive / Sheets / Docs)
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]

# Everything
pip install pyactuator[all]

# Development
pip install -e ".[dev]"
```

## Documentation

Published docs (Material for MkDocs) live at **[optophi.github.io/pyactuator](https://optophi.github.io/pyactuator/)**.

To build or preview locally:

```bash
pip install pyactuator[docs]   # or: pip install -e ".[dev]" (includes docs tooling)
mkdocs serve
```

The site configuration mirrors [PyStator](https://github.com/optophi/pystator) / [PyCharter](https://github.com/optophi/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: Trading (mock broker)

```python
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)

```python
import asyncio

from pyactuator.domains.google.types import AppendRowsRequest
from pyactuator.domains.google.adapters.mock import MockSheetsClient


async def main() -> None:
    sheets = MockSheetsClient()
    await sheets.create_spreadsheet(spreadsheet_id="sheet-1", title="Daily PnL")

    await sheets.append_rows(AppendRowsRequest(
        spreadsheet_id="sheet-1",
        range="Sheet1!A1",
        values=[["2025-05-07", "AAPL", 1234.56]],
    ))


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.

```python
import asyncio

from pystator.actions import ActionRegistry, ActionExecutor
from pystator.actions.types import 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.execute_action_spec(
        ActionSpec(
            name="pyactuator.trading.place_order",
            params={
                "symbol": "AAPL",
                "side": "buy",
                "quantity": 1,
                "order_type": "market",
                "client_order_id": "coid-1",
            },
        ),
        ctx={},
    )
    print(result["success"], result["result"]["external_order_id"])
    print("audit records:", len(audit.records()))


asyncio.run(main())
```

## Quick start: PyGubernator tool

```python
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`](./ARCHITECTURE.md) for the full layer model and
[`docs/guides/`](./docs/guides/) for the per-topic guides:

- [`domains-overview.md`](./docs/guides/domains-overview.md) — what
  domains ship today and which surface to call them through.
- [`adding-a-domain.md`](./docs/guides/adding-a-domain.md) — step-by-step
  recipe for adding a new domain.
- [`bridges.md`](./docs/guides/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
```

```python
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)`:

```python
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`:

```python
from pyactuator.core.audit import MemoryAuditWriter

audit = MemoryAuditWriter()
register_pyactuator_actions(action_registry, adapters=adapters, audit_writer=audit)

for record in audit.records():
    print(record.kind, record.action, record.success)
```

`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/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.
