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
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ The hosted API client and local migration adapter are separate surfaces:
| Import | What runs | Authentication |
|---|---|---|
| `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 |
| `SankaMigrate` or `AsyncSankaMigrate` from `sanka_sdk.migrate` | A local `sanka` subprocess | None |

Install the runtime separately. Installing `sanka-sdk` does not install or
authenticate `sanka-migrate`.
authenticate `sanka`.

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

Use a runtime release that includes the extension marketplace commands and
Expand Down Expand Up @@ -80,15 +80,15 @@ verified = migrate.verify()

`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
already installed and has not been disabled, `sanka` 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
secret values. `sanka` forwards only those named values to the selected
extension. Both options are available on `scan()`, `plan()`, `apply()`,
`test()`, and `verify()`.

Expand All @@ -114,7 +114,7 @@ marketplaces.remove("partner")

The Python methods map directly to these local commands:

| Python method | `sanka-migrate` command |
| Python method | `sanka` command |
|---|---|
| `extensions.list()` | `extension list --json` |
| `extensions.add(id, marketplace=...)` | `extension add ID --marketplace NAME --json` |
Expand All @@ -125,7 +125,7 @@ The Python methods map directly to these local commands:
| `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,
`sanka` 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.
Expand Down Expand Up @@ -161,11 +161,11 @@ Each method maps directly to the local runtime:

| Python method | Runtime command | Purpose |
|---|---|---|
| `scan()` | `sanka-migrate scan ... --json` | Inspect the source and write the scan artifact |
| `plan()` | `sanka-migrate plan ... --json` | Create a reviewable plan and plan hash |
| `apply()` | `sanka-migrate apply ... --json` | Generate only from the supplied reviewed plan hash |
| `test()` | `sanka-migrate test ... --json` | Prepare the generated target environment and run its tests |
| `verify()` | `sanka-migrate verify ... --json` | Verify integrity and configured behavior |
| `scan()` | `sanka scan ... --json` | Inspect the source and write the scan artifact |
| `plan()` | `sanka plan ... --json` | Create a reviewable plan and plan hash |
| `apply()` | `sanka apply ... --json` | Generate only from the supplied reviewed plan hash |
| `test()` | `sanka test ... --json` | Prepare the generated target environment and run its tests |
| `verify()` | `sanka verify ... --json` | Verify integrity and configured behavior |

### Results, failures, and subprocess safety

Expand Down Expand Up @@ -200,7 +200,7 @@ 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
safety remain in `sanka`. 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)
Expand Down
59 changes: 30 additions & 29 deletions handwritten/sanka_sdk/migrate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Local Sanka migration commands for Python applications.

``SankaMigrate`` and ``AsyncSankaMigrate`` expose the same generic lifecycle as
the ``sanka-migrate`` CLI: ``scan -> plan -> apply -> test -> verify``. Both run
the ``sanka`` CLI: ``scan -> plan -> apply -> test -> verify``. Both run
the separately installed CLI in non-interactive JSON mode; neither calls
Sanka's hosted API.
"""
Expand All @@ -21,6 +21,8 @@
JsonValue = Union[None, bool, int, float, str, List["JsonValue"], Dict[str, "JsonValue"]]
SankaMigrateCommand = Literal["scan", "plan", "apply", "test", "verify", "extension"]
CLI_SCHEMA_VERSION = "sanka-cli/v1"
DEFAULT_EXECUTABLE = "sanka"
INSTALL_HINT = "Install it with: uv tool install sanka-cli"

__all__ = [
"ApplyData",
Expand Down Expand Up @@ -51,7 +53,7 @@ class ExtensionEvidence(TypedDict):


class ExtensionRecommendation(TypedDict):
"""One compatible extension recommended by ``sanka-migrate``."""
"""One compatible extension recommended by ``sanka``."""

id: str
version: str
Expand Down Expand Up @@ -162,10 +164,10 @@ class SankaMigrate:
"""Run local Sanka migration commands without a Sanka API token.

Args:
cwd: Working directory used by ``sanka-migrate``. Relative command
cwd: Working directory used by ``sanka``. Relative command
paths and default artifacts resolve from this directory.
executable: CLI executable name or path. Install it separately with
``uv tool install sanka-migrate``.
``uv tool install sanka-cli``.
env: Environment variables merged over the current process environment.

The adapter is non-interactive and always requests one ``sanka-cli/v1``
Expand All @@ -177,7 +179,7 @@ def __init__(
self,
*,
cwd: Optional[PathValue] = None,
executable: PathValue = "sanka-migrate",
executable: PathValue = DEFAULT_EXECUTABLE,
env: Optional[Mapping[str, str]] = None,
) -> None:
executable_value = os.fspath(executable)
Expand All @@ -196,7 +198,7 @@ def scan(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[ScanData]:
"""Inspect a source application with ``sanka-migrate scan``.
"""Inspect a source application with ``sanka scan``.

Args:
root: Source repository root. Omit it to use ``cwd``.
Expand Down Expand Up @@ -239,7 +241,7 @@ def plan(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[PlanData]:
"""Create a reviewable, hash-bound plan with ``sanka-migrate plan``.
"""Create a reviewable, hash-bound plan with ``sanka plan``.

Args:
root: Source repository root. Omit it to use ``cwd``.
Expand Down Expand Up @@ -305,7 +307,7 @@ def apply(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[ApplyData]:
"""Apply exactly one reviewed plan with ``sanka-migrate apply``.
"""Apply exactly one reviewed plan with ``sanka apply``.

Args:
plan_hash: Non-empty hash returned by ``plan()``; writes are bound to it.
Expand Down Expand Up @@ -369,7 +371,7 @@ def test(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[TestData]:
"""Run generated-target tests with ``sanka-migrate test``.
"""Run generated-target tests with ``sanka test``.

Args:
root: Source repository root. Omit it to use ``cwd``.
Expand Down Expand Up @@ -423,7 +425,7 @@ def verify(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[VerifyData]:
"""Verify the selected migration with ``sanka-migrate verify``.
"""Verify the selected migration with ``sanka verify``.

Args:
root: Source repository root. Omit it to use ``cwd``.
Expand Down Expand Up @@ -488,7 +490,7 @@ def _run(self, command: SankaMigrateCommand, args: Sequence[str]) -> SankaMigrat
raise _missing_executable(command) from error
except OSError as error:
raise SankaMigrateError(
"could not execute sanka-migrate: {}".format(error),
"could not execute sanka: {}".format(error),
command=command,
) from error

Expand All @@ -504,7 +506,7 @@ def _prepare(
) -> Tuple[List[str], Dict[str, str]]:
if self.cwd is not None and not os.path.isdir(self.cwd):
raise SankaMigrateError(
"sanka-migrate working directory was not found: {}".format(self.cwd),
"sanka working directory was not found: {}".format(self.cwd),
command=command,
)
environment = os.environ.copy()
Expand All @@ -516,7 +518,7 @@ class AsyncSankaMigrate(SankaMigrate):
"""Run local Sanka migration commands without blocking the event loop.

Args:
cwd: Working directory used by ``sanka-migrate``.
cwd: Working directory used by ``sanka``.
executable: Separately installed CLI executable name or path.
env: Environment variables merged over the current process environment.

Expand All @@ -533,7 +535,7 @@ async def scan(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[ScanData]:
"""Asynchronously inspect a source with ``sanka-migrate scan``.
"""Asynchronously inspect a source with ``sanka scan``.

Args:
root: Source repository root. Omit it to use ``cwd``.
Expand Down Expand Up @@ -576,7 +578,7 @@ async def plan(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[PlanData]:
"""Asynchronously create a hash-bound plan with ``sanka-migrate plan``.
"""Asynchronously create a hash-bound plan with ``sanka plan``.

Args:
root: Source repository root. Omit it to use ``cwd``.
Expand Down Expand Up @@ -641,7 +643,7 @@ async def apply(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[ApplyData]:
"""Asynchronously apply one reviewed plan with ``sanka-migrate apply``.
"""Asynchronously apply one reviewed plan with ``sanka apply``.

Args:
plan_hash: Non-empty hash returned by ``plan``.
Expand Down Expand Up @@ -705,7 +707,7 @@ async def test(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[TestData]:
"""Asynchronously run generated tests with ``sanka-migrate test``.
"""Asynchronously run generated tests with ``sanka test``.

Args:
root: Source repository root. Omit it to use ``cwd``.
Expand Down Expand Up @@ -758,7 +760,7 @@ async def verify(
extension_config: Optional[Mapping[str, JsonValue]] = None,
extension_environment: Sequence[str] = (),
) -> SankaMigrateResult[VerifyData]:
"""Asynchronously verify with ``sanka-migrate verify``.
"""Asynchronously verify with ``sanka verify``.

Args:
root: Source repository root. Omit it to use ``cwd``.
Expand Down Expand Up @@ -823,7 +825,7 @@ async def _run_async(
raise _missing_executable(command) from error
except OSError as error:
raise SankaMigrateError(
"could not execute sanka-migrate: {}".format(error),
"could not execute sanka: {}".format(error),
command=command,
) from error

Expand All @@ -840,7 +842,7 @@ async def _run_async(

exit_code = process.returncode
if exit_code is None:
raise SankaMigrateError("sanka-migrate {} did not exit".format(command), command=command)
raise SankaMigrateError("sanka {} did not exit".format(command), command=command)
return _finish_result(
stdout_bytes.decode("utf-8", errors="replace"),
command=command,
Expand Down Expand Up @@ -1193,7 +1195,7 @@ def _finish_result(
message = (
str(error_data.get("message"))
if error_data and error_data.get("message")
else "sanka-migrate {} failed with exit code {}".format(command, exit_code)
else "sanka {} failed with exit code {}".format(command, exit_code)
)
raise SankaMigrateError(
message,
Expand All @@ -1208,8 +1210,7 @@ def _finish_result(

def _missing_executable(command: SankaMigrateCommand) -> SankaMigrateError:
return SankaMigrateError(
"sanka-migrate executable was not found; install it with "
"`uv tool install sanka-migrate` or pass executable=...",
"{} executable was not found. {} or pass executable=...".format(DEFAULT_EXECUTABLE, INSTALL_HINT),
command=command,
)

Expand All @@ -1225,14 +1226,14 @@ def _decode_result(
payload = json.loads(stdout)
except (json.JSONDecodeError, TypeError) as error:
raise SankaMigrateError(
"sanka-migrate {} did not return one valid JSON document".format(command),
"sanka {} did not return one valid JSON document".format(command),
command=command,
exit_code=exit_code,
stderr=stderr,
) from error
if not isinstance(payload, dict):
raise SankaMigrateError(
"sanka-migrate {} returned a non-object JSON document".format(command),
"sanka {} returned a non-object JSON document".format(command),
command=command,
exit_code=exit_code,
stderr=stderr,
Expand All @@ -1242,14 +1243,14 @@ def _decode_result(
payload_command = payload.get("command")
if schema_version != CLI_SCHEMA_VERSION:
raise SankaMigrateError(
"unsupported sanka-migrate protocol: {!r}".format(schema_version),
"unsupported sanka protocol: {!r}".format(schema_version),
command=command,
exit_code=exit_code,
stderr=stderr,
)
if payload_command != command:
raise SankaMigrateError(
"sanka-migrate returned command {!r}, expected {!r}".format(payload_command, command),
"sanka returned command {!r}, expected {!r}".format(payload_command, command),
command=command,
exit_code=exit_code,
stderr=stderr,
Expand All @@ -1260,14 +1261,14 @@ def _decode_result(
raise _invalid_field(command, exit_code, stderr, "outcome", "'success' or 'error'")
if exit_code not in (0, 1, 2):
raise SankaMigrateError(
"invalid sanka-migrate exit code {}; expected 0, 1, or 2".format(exit_code),
"invalid sanka exit code {}; expected 0, 1, or 2".format(exit_code),
command=command,
exit_code=exit_code,
stderr=stderr,
)
if (outcome == "success") != (exit_code == 0):
raise SankaMigrateError(
"sanka-migrate outcome {!r} is inconsistent with exit code {}".format(outcome, exit_code),
"sanka outcome {!r} is inconsistent with exit code {}".format(outcome, exit_code),
command=command,
exit_code=exit_code,
stderr=stderr,
Expand Down
Loading