# Ursly Python SDK

Public documentation: [developer.ursly.io](https://developer.ursly.io/).

A typed, synchronous v1 intent client with zero runtime dependencies for Python 3.11+. Install the versioned wheel from the website; it needs no build backend. The source archive (`ursly_sdk-<version>.tar.gz`) also installs, but pip then fetches setuptools to build it. Registry publication is separate.

```sh
python -m pip install ./ursly_sdk-<version>-py3-none-any.whl
```

`<version>` is `release.version` in `/downloads/manifest.json`. The public API host is `https://ursly.io`; pass it explicitly as `base_url`, because the client has no default destination.

```python
from ursly import UrslyClient

client = UrslyClient(base_url="https://YOUR-API-HOST", timeout=5)
client.register_handler(
    "navigation.open", lambda action, intent: print(action["target"])
)
request = {
    "input": {"sense": "voice", "transcript": "open home"},
    "rules": [
        {
            "id": "home",
            "match": "open home",
            "action": {"type": "navigation.open", "target": "/"},
        }
    ],
}
resolution = client.resolve(request)  # Returns intents and performs no action.
executed = client.resolve_and_dispatch(request)  # Explicit application handlers.
```

Methods are `capabilities()`, `actions()`, `templates()`, `validate(request)`, and `resolve(request)`. `templates()` supplies editable requests. `register_handler(type, fn)` and `unregister_handler(type)` configure application-owned action behavior. `dispatch(resolution)` is explicit. Handlers are synchronous and receive the action and intent. Missing handlers are checked before any dispatch; handler failure stops later handlers without retry or rollback of completed effects.

## Transport and cancellation

`base_url` is required; an HTTP(S) proxy prefix is supported. Embedded credentials, queries and fragments are rejected. Default timeout is 10 seconds. Requests identify the SDK with its installed package version; uninstalled source uses an unversioned Ursly SDK identifier. A transport can be injected with the signature `(method, url, JSON_body_or_None, timeout_seconds) -> (HTTP_status, decoded_JSON)`.

Optional method argument `cancel_event` accepts a `threading.Event`. The client checks it before sending and while waiting, then prevents handler dispatch after cancellation. The default urllib adapter performs bounded I/O in a daemon worker. A deadline or cancellation stops client waiting; an already-started HTTP request may finish in that worker. It cannot be unsent, and no handler executes from an abandoned response. Injected transports must likewise bound their own I/O. No automatic retry or redirect is performed.

Typed exceptions distinguish API rejection (`status`, server `code`), timeout, cancellation, transport/invalid response, missing handler and handler failure. The client does not print request bodies, secrets or credential-bearing URLs.

## Response validation

Catalog entries and template requests are validated before being returned. Invalid action payloads are rejected before registered handlers execute, including external navigation (a path whose `//`, `.` or `..` segments, `%2e` included, could resolve to another host), invalid identifiers, unknown action fields and oversized UTF-8 payloads. Optional `confidence` and `minConfidence` accept both omission and JSON `null` (`None`), matching the generated API and templates. Applications still own authorization and the safety of handler side effects.

## Declarative actions

Supported types are `navigation.open`, `text.insert`, `media.play`, `media.pause`, `scene.focus`, and `workflow.trigger`. Workflow parameters are bounded string maps. Configure optional `enabled`/`minConfidence` on rules and adapter-supplied `confidence` on inputs. Replace voice input with `{"sense":"motion","gesture":"swipe_left"}` for normalized motion rules. No microphone/camera acquisition, recognition, shell execution or server HTTP relay is built in.

Voice production and motion in development describe founder-reported product stages, not certified hardware or recognition performance. Remaining senses explicitly return HTTP 422 `sense_unavailable`. Tests of this client do not validate a physical sensor or commercial integration.

## Changes since 0.1.0

Breaking:

- Responses are validated against the v1 contract before any handler runs: malformed actions, catalog entries and templates raise `InvalidResponseError`. `navigation.open` targets must be origin-relative paths with no `//`, no `.` or `..` segment (also spelled `%2e`), backslash, line break or whitespace.
- A base URL containing `?` or `#`, even with nothing after it, raises `ConfigurationError`.
- An error body whose `error.code` or `error.message` is not a string gives `ApiError` code `http_error` or message "API request rejected", instead of that value's `str()`.

Also new: the `confidence` and `minConfidence` types accept `None` (JSON `null`), as the API does.

## Development

```sh
PYTHONPATH=sdk/python/src python3 -m unittest discover -s sdk/python/tests
python3 -m compileall -q sdk/python/src
cd sdk/python
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/ruff check .
.venv/bin/ruff format --check .
.venv/bin/python -m build --no-isolation
```

The project is `ursly-sdk`, versioned in `pyproject.toml`; the `dev` extra pins the setuptools backend so `--no-isolation` builds work. Import package `ursly` includes `py.typed`. Framework-independent typed contracts/handler dispatch use an injected transport; urllib is the default adapter. The shared Rust API generates authoritative OpenAPI and catalogs. [SOURCES.md](SOURCES.md) records versions, official references and evidence boundaries.

## License

License terms have not yet been published. Public downloads do not grant an open-source license; the source repository remains private.
