# ocx_sdk

*module*

Python SDK for [OCX](https://github.com/ocx-sh/ocx).

`ocx-sdk` drives the ocx binary rather than reimplementing it: ocx owns
resolution, verification, and the identifier grammar, and this package gives
you typed, CWD-independent handles over the commands it exposes.

```python
from ocx_sdk import Ocx, bootstrap

ocx = Ocx(exe=bootstrap.ensure())
project = ocx.project("/srv/build")
project.pull()
project.exec(["task", "verify"])
```

**This module is the API.** Everything listed in `__all__` is the stable
surface; every other module is underscored and package-private, and the one
public submodule is `ocx_sdk.bootstrap`. Reaching into an underscored path
means the next release may move it without notice — pre-1.0, breaking
changes ship without shims.

Start at `Ocx` for the runtime API and `bootstrap.ensure` for provisioning.

[View source](https://github.com/ocx-sh/ocx-sdk-python/blob/main/src/ocx_sdk/__init__.py#L1-L1)

## ocx_sdk.Ocx

*class*

```python
class Ocx
```

Re-exported from: `ocx_sdk._client`

A handle on one ocx binary.

Construction resolves the binary once — the explicit `exe`, then
`OCX_SDK_EXE`, then `PATH`, then ocx's own install symlink — and pins the
result for the handle's lifetime. Every call composes its own argv and
child environment, and the handle exposes no mutators, so it is safe to
share across threads, tasks, and event loops.

The first typed call probes `ocx version` and refuses a binary older than
`MIN_SUPPORTED`; newer than `TESTED_OCX_VERSION` is expected and only
noted at DEBUG.

A long-lived handle keeps running the binary it resolved, so a consumer
shaped like a daemon should reconstruct `Ocx()` periodically to pick up
what an `ocx self update` replaced underneath it.

> **Example**
>
> ```python
> from ocx_sdk import Ocx, OcxConfig
> 
> ocx = Ocx(config=OcxConfig(offline=True))
> project = ocx.project("/srv/build")
> project.pull()
> result = project.exec(["task", "verify"])
> ```

**Attributes**

- `exe` (`Path`) — The resolved binary this handle runs.
- `session_config` (`OcxConfig`) — The `OcxConfig` every call spawns under.

[View source](https://github.com/ocx-sh/ocx-sdk-python/blob/main/src/ocx_sdk/_client.py#L216-L648)

### ocx_sdk.Ocx.config

*property*

```python
config: ConfigCommands
```

The `ocx config` command group — the managed-config tier.

[View source](https://github.com/ocx-sh/ocx-sdk-python/blob/main/src/ocx_sdk/_client.py#L393-L395)

### ocx_sdk.Ocx.exe

*property*

```python
exe: Path
```

The resolved binary path, fixed at construction.

[View source](https://github.com/ocx-sh/ocx-sdk-python/blob/main/src/ocx_sdk/_client.py#L296-L298)

### ocx_sdk.Ocx.package

*property*

```python
package: PackageCommands
```

The `ocx package` command group — machine tier, no project path.

[View source](https://github.com/ocx-sh/ocx-sdk-python/blob/main/src/ocx_sdk/_client.py#L388-L390)

### ocx_sdk.Ocx.spawn

*method*

```python
def spawn(argv: Sequence[str], **popen_kw: Any) -> subprocess.Popen[Any]
```

Start an arbitrary ocx command line and return the live `Popen`.

No timeout and no pumps: waiting, draining pipes, and killing belong
to the caller, exactly as with a bare `Popen`.

**Parameters**

- `argv` (`Sequence[str]`) — The command and its arguments, without the binary.
- `**popen_kw` (`Any`) (default: `{}`) — Forwarded to `Popen`. `args`, `shell`, and `executable` are rejected; the environment is SDK-composed.

**Returns**

- (`subprocess.Popen[Any]`) — The running child.

**Raises**

- `ValueError` — `args`, `shell`, or `executable` was passed.
- `TypeError` — `env` was passed. It is the process layer's own
parameter, so it arrives as a duplicate argument rather than
a rejected one. Put what the child needs into `OcxConfig` or
the `HostEnv` this handle was built with.

[View source](https://github.com/ocx-sh/ocx-sdk-python/blob/main/src/ocx_sdk/_client.py#L610-L631)
