Metadata-Version: 2.4
Name: outerproduct-sdk
Version: 0.1.4
Requires-Dist: adbc-driver-flightsql>=1.8,<2
Requires-Dist: cloudpickle==3.1.2
Requires-Dist: obstore>=0.11,<1
Requires-Dist: pydantic>=2.13.4,<3
Summary: High-level OuterProduct SDK for workflows, data, environments, and Unity Catalog.
Requires-Python: >=3.12
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# OuterProduct SDK

`outerproduct-sdk` owns both the caller-facing client and the
transport-independent capability contract used by managed compute. `Client`
exposes the generated `ExecutionClient` as `client.execution` alongside Flight
SQL, Unity Catalog, and temporary-credential vending. It can be passed directly
to the SDK's `store_from_*` functions.

`download_s3_prefix(client, s3_prefix, target_path)` copies a UC-governed S3
prefix into a local directory with vended credentials. No Files RPC or
provider-native bucket mount is involved.

The published wheel is self-contained with respect to this monorepo. One native
extension registers the low-level `control_plane_client`,
`file_service_client`, and `uc_client` surfaces. The wheel also vendors the
SDK's internal serialization package; only third-party libraries are installed
as separate dependencies.

Unity Catalog and Flight SQL are direct client capabilities:

```python
async with op.init() as client:
    catalog = client.uc
    with client.sql.cursor() as cursor:
        cursor.execute("SELECT 1")
        row = cursor.fetchone()
```

## UC-governed object storage

Use the SDK's `store_from_*` functions to create refresh-aware stores for UC
volumes, UC tables, or raw cloud paths:

```python
import outerproduct_sdk as op

client = op.init()

volume_store = op.store_from_volume(
    client, "main.default.files", sub_path="incoming"
)
table_store = op.store_from_table(client, "main.default.events")
s3_store = op.store_from_path(
    client,
    "s3://example-bucket/datasets/events",
    region="us-east-1",
)
```

Libraries with their own S3 reader can use the client's temporary-credential
methods directly. For Polars, vend credentials from its callback so a lazy
scan can refresh an expiring lease:

```python
import polars as pl

s3_prefix = "s3://example-bucket/datasets/events"


def credentials() -> pl.CredentialProviderFunctionReturn:
    credential, _ = client.temporary_path_credential(s3_prefix, "read")
    aws = credential.aws_temp_credentials
    if aws is None:
        raise RuntimeError("Unity Catalog did not vend AWS credentials")

    return {
        "aws_access_key_id": aws.access_key_id,
        "aws_secret_access_key": aws.secret_access_key,
        "aws_session_token": aws.session_token,
    }, credential.expiration_time // 1_000


frame = pl.scan_parquet(
    f"{s3_prefix}/*.parquet",
    credential_provider=credentials,
    storage_options={"aws_region": "us-east-1"},
).collect()
```

Unity Catalog reports expiration in milliseconds; Polars expects Unix
seconds. The complete runnable version is
[`examples/uc_storage.py`](examples/uc_storage.py).

## Environment volumes

An environment mounts no storage implicitly. Pass created Unity Catalog
volume resources by absolute container path when building an environment:

```python
import outerproduct_sdk as op
from uc_client import VolumeType

volume = client.uc.create_volume(
    "main",
    "default",
    "models",
    VolumeType.MANAGED,
)
environment = client.execution.build_environment(
    op.Environment(
        spec=op.EnvironmentSpec(
            base_image=cpu_image.name,
            compute=op.ComputeSpec(cpu=2),
            volumes={"/mnt/models": volume.full_name},
        )
    )
)
```

At each workflow invocation, the runtime vends a fresh read credential through
Unity Catalog and materializes the requested volume prefix at that path.
Environment specs retain only the path and three-level volume name—never cloud
storage locations or temporary credentials.

Run functions in an existing environment:

```python
import outerproduct_sdk as op


async def main() -> None:
    client = op.init()
    environment = client.execution.environment(
        "environments/00000000-0000-0000-0000-000000000000"
    ).get()

    @environment.fn
    def child(value: int, *, client):
        return value * 2

    @environment.fn
    async def parent(value: int, *, client):
        return await client.run(child, value)

    result = await client.run(parent, 21)
```

