Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ pip install 'alissa[tools.github.revloop]' # + a tool, merged into alissa.
pip install 'alissa[all]' # + every tool
```

Beyond the namespace, the core ships typed REST bindings under `alissa.sdk.api`
— a shared client/auth core plus one module per API surface. Today that is
`alissa.sdk.api.bridge`, the Local Bridge queue-mode executor and job endpoints:

```python
from alissa.sdk.api import BridgeClient

bridge = BridgeClient() # token from $ALISSA_API_TOKEN
for executor in bridge.list_executors():
print(executor.executor_id, executor.status)

job = bridge.get_job("j57bridge0001")
print(job.spec.title, job.status, f"attempt {job.attempt}/{job.max_attempts}")
```

See [`alissa/README.md`](./alissa/README.md#api-bindings-alissasdkapi) for the
error model and the rest of the surface.

## How ownership works

`alissa` is the anchor distribution: the thing you `pip install`, the top-level
Expand Down Expand Up @@ -48,6 +66,7 @@ alissa-python-sdk/
│ ├── MANIFEST.in
│ └── src/
│ ├── main/alissa/sdk/ ← owned leaf: SDK surface (+ plain-text `version` file)
│ │ └── api/ ← REST bindings: shared client/auth core + per-surface modules
│ ├── main/alissa/utils/ ← owned leaf: shared helpers (alissa.utils.version)
│ └── test/test_alissa/ ← mirrors main as test_*
├── .github/workflows/ ← style / types / tests / wheel / publish (matrix: alissa)
Expand Down
62 changes: 62 additions & 0 deletions alissa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,68 @@ probing each through the import machinery — so it is correct for both wheel an
editable installs. The curated set lives in `src/main/alissa/sdk/_tools.py`, the
same registry `setup.py` builds the extras from.

## API bindings (`alissa.sdk.api`)

Typed access to the Alissa REST API — stdlib-only, like the rest of the core.
One `ApiClient` owns the base URL, the bearer token, JSON and the error
envelope; each API surface is a binding module on top of it, so there is never a
second HTTP stack in this SDK.

Configuration: `ALISSA_API_TOKEN` (or `token=`) and `ALISSA_BASE` (or
`base_url=`, default `https://api.alissa.app`).

> **On the base-URL variable.** The Node `alissa` CLI spells this knob
> `ALISSA_API_BASE`, not `ALISSA_BASE`. This SDK reads `ALISSA_BASE` first
> (matching its Python siblings) and falls back to `ALISSA_API_BASE`, so
> exporting either one points the CLI *and* the SDK at the same deployment.
> Set both and `ALISSA_BASE` wins.

Requests never follow redirects: the API is a fixed JSON surface with no reason
to issue one, and `urllib` would copy the `Authorization` header onto a
cross-origin `Location`. An unexpected 3xx surfaces as an `ApiError` with code
`HTTP_302` (or whichever status arrived) rather than a silent off-origin call.

### Local Bridge queue mode (`alissa.sdk.api.bridge`)

The `/v1/bridge` executor and job surface: four executor endpoints (register,
list, heartbeat, stop) and seven job endpoints (feed, detail, claim, start,
progress, fulfill, fail).

**Bindings only.** The executor *daemon* — polling, claim state machine, tmux —
is the Node `alissa` CLI's, and none of it lives here. This module is typed
request/response plumbing for observers and tooling.

```python
from alissa.sdk.api import BridgeClient

bridge = BridgeClient() # token from $ALISSA_API_TOKEN

# Which machines are registered, and are they alive?
for executor in bridge.list_executors():
print(executor.executor_id, executor.status, executor.last_heartbeat_at)

# Tail one job's status.
job = bridge.get_job("j57bridge0001")
print(job.spec.title, job.status, f"attempt {job.attempt}/{job.max_attempts}")
print(job.progress_note or job.error or "")
```

Errors are branchable by **code**, never by message text. The queue's four 409s
share one base, because they all mean *re-read the row and retry* rather than
*give up*:

```python
from alissa.sdk.api import RetryAfterReadError, StaleConsumerError

try:
claim = bridge.claim_job(job_id, executor_id=executor_id,
consumer_id=nonce, claim_seq=claim_seq)
except StaleConsumerError:
pass # a newer attempt owns this row — stop
except RetryAfterReadError as err:
print(err.code, err.observed_status) # re-poll, do not spin
```

## Shared utilities (`alissa.utils`)

Helpers the SDK factors out so every `alissa.*` distribution reuses one
Expand Down
64 changes: 64 additions & 0 deletions alissa/src/main/alissa/sdk/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""``alissa.sdk.api`` — typed bindings for the Alissa REST API.

Two layers, deliberately separated:

* **The core** — :class:`~alissa.sdk.api.client.ApiClient` (base URL, bearer
token, JSON, timeouts) and :mod:`alissa.sdk.api.errors` (the failure envelope
as a class per code). Every binding shares these; there is no second HTTP
stack anywhere in this SDK.
* **The bindings** — one subpackage per API surface, owning paths and shapes and
nothing else. Today: :mod:`alissa.sdk.api.bridge`, the Local Bridge queue-mode
executor and job endpoints.

Both are stdlib-only, matching the SDK core's zero-dependency contract.

from alissa.sdk.api import ApiClient, BridgeClient, StaleConsumerError

bridge = BridgeClient(ApiClient(token="alissa_…"))
for executor in bridge.list_executors():
print(executor.executor_id, executor.status)
"""
from .bridge import BridgeClient
from .client import DEFAULT_BASE_URL, ApiClient, HttpRequest, HttpResponse, Transport, UrllibTransport
from .errors import (
AlissaError,
ApiError,
CapacityExceededError,
CasConflictError,
ConflictError,
ForbiddenError,
MissingTokenError,
NotFoundError,
PreconditionFailedError,
RetryAfterReadError,
StaleConsumerError,
StickyViolationError,
TransportError,
UnauthorizedError,
ValidationError,
)

__all__ = [
"ApiClient",
"BridgeClient",
"DEFAULT_BASE_URL",
"HttpRequest",
"HttpResponse",
"Transport",
"UrllibTransport",
"AlissaError",
"ApiError",
"CapacityExceededError",
"CasConflictError",
"ConflictError",
"ForbiddenError",
"MissingTokenError",
"NotFoundError",
"PreconditionFailedError",
"RetryAfterReadError",
"StaleConsumerError",
"StickyViolationError",
"TransportError",
"UnauthorizedError",
"ValidationError",
]
61 changes: 61 additions & 0 deletions alissa/src/main/alissa/sdk/api/bridge/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""``alissa.sdk.api.bridge`` — typed bindings for the Local Bridge queue mode.

The ``/v1/bridge`` executor and job surface of the Alissa API, mirroring
``api/src/schemas/bridge.ts`` (design: ``docs/design/local-bridge-queue-mode.md``
§5). :mod:`~alissa.sdk.api.bridge.client` holds the eleven endpoint methods;
:mod:`~alissa.sdk.api.bridge.models` holds the response structures.

Request/response plumbing only — the executor daemon itself is the Node `alissa`
CLI's, and lives nowhere in this package.
"""
from .client import BRIDGE_PREFIX, EXECUTOR_KIND, BridgeClient
from .models import (
ExecutorCapabilities,
ExecutorHeartbeat,
ExecutorRegistration,
ExecutorStopResult,
ExecutorSummary,
JobAcceptanceCriterion,
JobClaim,
JobDeliverable,
JobDetail,
JobFailAck,
JobFeed,
JobFeedItem,
JobFulfillAck,
JobProgressAck,
JobReference,
JobResult,
JobResultAcceptance,
JobResultLink,
JobSpec,
JobStartAck,
ResumedJob,
)

__all__ = [
"BRIDGE_PREFIX",
"EXECUTOR_KIND",
"BridgeClient",
"ExecutorCapabilities",
"ExecutorHeartbeat",
"ExecutorRegistration",
"ExecutorStopResult",
"ExecutorSummary",
"JobAcceptanceCriterion",
"JobClaim",
"JobDeliverable",
"JobDetail",
"JobFailAck",
"JobFeed",
"JobFeedItem",
"JobFulfillAck",
"JobProgressAck",
"JobReference",
"JobResult",
"JobResultAcceptance",
"JobResultLink",
"JobSpec",
"JobStartAck",
"ResumedJob",
]
Loading
Loading