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
145 changes: 123 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Python 3.9 or newer is required. CI tests every minor from Python 3.9 through Py
uv add sanka-sdk
```

## Usage
## Hosted API

```python
from sanka_sdk import SankaClient
Expand All @@ -22,7 +22,11 @@ response = client.public_auth.whoami()
print(response)
```

## Local migration
`SankaClient` calls Sanka's hosted HTTP API and requires a token. Extension
management is part of the local migration runtime described below; it is not a
hosted API resource.

## Local migration runtime

The hosted API client and local migration adapter are separate surfaces:

Expand All @@ -31,35 +35,107 @@ The hosted API client and local migration adapter are separate surfaces:
| `from sanka_sdk import SankaClient` | Sanka's hosted HTTP API | API token |
| `SankaMigrate` or `AsyncSankaMigrate` from `sanka_sdk.migrate` | A local `sanka-migrate` subprocess | None |

Install the migration runtime separately, then use the tokenless adapter:
Install the runtime separately. Installing `sanka-sdk` does not install or
authenticate `sanka-migrate`.

```bash
uv tool install sanka-migrate
```

### Synchronous
Use a runtime release that includes the extension marketplace commands and
the published default DRF extension dependency.

### Configure an extension, scan, and plan

Add the official marketplace and lock the extension before the first scan.
Marketplace snapshots are user-scoped; the extension lock belongs to the
project in `cwd`.

```python
from sanka_sdk.migrate import SankaMigrate

migrate = SankaMigrate(cwd="./django-app")

migrate.extensions.marketplaces.add(
"git@github.com:sankaHQ/extensions.git",
name="sanka",
)
migrate.extensions.add("sanka/drf-to-fastapi", marketplace="sanka")

scan = migrate.scan()
plan = migrate.plan(
to="fastapi",
generation="full",
strategy="native",
package_manager="uv",
extension_config={
"generation": "minimal",
"output": "./fastapi-app",
"package_manager": "uv",
"strategy": "native",
},
extension_environment=("DJANGO_SECRET_KEY",),
)
applied = migrate.apply(plan_hash=plan.data["plan_hash"])
tested = migrate.test()
verified = migrate.verify()
```

### Asynchronous
`scan.data["recommendations"]` contains the selected extension, its target,
matching evidence, and install status. When the exact default package is
already installed and has not been disabled, `sanka-migrate` can lock it on
the first scan. Otherwise, if no matching extension is enabled, the command
stops with `SANKA_EXTENSION_REQUIRED`. The error details contain the
recommendations and exact `add_command`; the SDK does not bypass the runtime's
selection and trust checks.

`extension_config` accepts JSON-compatible values and is serialized as stable,
sorted JSON. `extension_environment` accepts environment variable names, not
secret values. `sanka-migrate` forwards only those named values to the selected
extension. Both options are available on `scan()`, `plan()`, `apply()`,
`test()`, and `verify()`.

### Manage extensions and marketplaces

```python
extensions = migrate.extensions

installed = extensions.list()
extensions.add("example/demo", marketplace="partner")
extensions.remove("example/demo")

marketplaces = extensions.marketplaces
marketplaces.add(
"https://github.com/example/sanka-extensions.git",
name="partner",
trust=True,
)
marketplaces.list()
marketplaces.upgrade("partner") # Omit the name to upgrade all marketplaces.
marketplaces.remove("partner")
```

The Python methods map directly to these local commands:

| Python method | `sanka-migrate` command |
|---|---|
| `extensions.list()` | `extension list --json` |
| `extensions.add(id, marketplace=...)` | `extension add ID --marketplace NAME --json` |
| `extensions.remove(id)` | `extension remove ID --json` |
| `extensions.marketplaces.list()` | `extension marketplace list --json` |
| `extensions.marketplaces.add(source, name=..., trust=True)` | `extension marketplace add SOURCE --name NAME --trust --json` |
| `extensions.marketplaces.upgrade(name)` | `extension marketplace upgrade NAME --json` |
| `extensions.marketplaces.remove(name)` | `extension marketplace remove NAME --json` |

`trust=True` is an explicit operator decision. The SDK only passes `--trust`.
`sanka-migrate` owns source identity checks, immutable marketplace snapshots,
artifact verification, project locks, extension installation, upgrades, and
removal safety. An untrusted source fails with
`SANKA_MARKETPLACE_TRUST_REQUIRED`; the SDK does not bypass that check.

### Async adapter

Use `AsyncSankaMigrate` to run the same commands without blocking the event
loop. Cancelling an awaited command also terminates its local CLI process.
loop. Its lifecycle, extension, and marketplace methods have the same arguments
and results as the synchronous adapter. Cancelling an awaited command kills and
reaps its local CLI process.

```python
import asyncio
Expand All @@ -70,8 +146,9 @@ from sanka_sdk.migrate import AsyncSankaMigrate
async def main() -> None:
migrate = AsyncSankaMigrate(cwd="./django-app")

await migrate.scan()
plan = await migrate.plan(to="fastapi", generation="full")
scan = await migrate.scan()
await migrate.extensions.list()
plan = await migrate.plan(to="fastapi")
await migrate.apply(plan_hash=plan.data["plan_hash"])
await migrate.test()
await migrate.verify()
Expand All @@ -90,17 +167,41 @@ Each method maps directly to the local runtime:
| `test()` | `sanka-migrate test ... --json` | Prepare the generated target environment and run its tests |
| `verify()` | `sanka-migrate verify ... --json` | Verify integrity and configured behavior |

Both adapters invoke an argument vector without a shell and never call Sanka's
hosted API. They forward only parameters you provide; defaults, validation,
framework detection, generated-target environments, and plan-hash safety remain
owned by `sanka-migrate`. Every call returns a typed `SankaMigrateResult` with
the `sanka-cli/v1` fields `data`, `artifacts`, `limitations`, and
`next_actions`.

Failures raise `SankaMigrateError`. Its `command`, `exit_code`, `parsed_error`,
and `stderr` attributes distinguish a migration failure (exit `1`), invalid
usage (exit `2`), a missing executable, and an invalid protocol response. The
public classes and methods include docstrings for IDE hover and `help()`.
### Results, failures, and subprocess safety

Both adapters execute an argv list without a shell and never call Sanka's
hosted API. Arguments such as marketplace URLs, paths, and configuration values
are not interpreted as shell commands.

Every successful call returns a typed `SankaMigrateResult`. The validated
`sanka-cli/v1` fields are `schema_version`, `command`, `outcome`,
`migration_state`, `data`, `artifacts`, `limitations`, and `next_actions`.
`ScanData`, `ExtensionRecommendation`, `ExtensionEvidence`, and
`ExtensionFailure` describe the extension-specific data available to type
checkers and IDEs.

```python
from sanka_sdk.migrate import SankaMigrateError

try:
migrate.extensions.marketplaces.add("./third-party", name="third-party")
except SankaMigrateError as error:
print(error.command, error.exit_code)
print(error.parsed_error) # code, message, and optional details
print(error.result) # Complete validated failure envelope, when available.
```

Failures are fail-closed. The SDK rejects missing executables, malformed or
non-object JSON, a schema other than `sanka-cli/v1`, the wrong command, invalid
outcome/exit-code pairs, malformed `data.error`, and non-string artifact or
action lists. A valid CLI failure raises `SankaMigrateError` with its typed
result preserved. Exit `1` is a runtime failure, exit `2` is invalid usage, and
any other exit code is a protocol error.

Defaults, framework detection, marketplace trust, immutable snapshots,
extension subprocess execution, generated-target environments, and plan-hash
safety remain in `sanka-migrate`. The SDK is a typed local adapter, not a second
migration runtime.

See the [CLI execution model](https://github.com/sankaHQ/sanka/blob/main/docs/django-to-fastapi.md#cli-and-sdk-execution-model)
and [Sanka developer documentation](https://sanka.com/docs/developers/).
Expand Down
Loading