diff --git a/CHANGELOG.md b/CHANGELOG.md index 0935497..bbbd0ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ Release tags use the `python-vX.Y.Z` prefix. ## [Unreleased] +## [0.1.5] - 2026-07-10 + +### Added +- **`spawn.launch()` now exposes the remaining launch parameters** (#6): + `ami` (custom AMI), `key_name` (SSH key pair), `pre_stop` (pre-stop hook), and + `completion_file` (path spored watches). These were accepted by the REST launch + endpoint but not surfaced by the SDK. With this, the SDK covers the full launch + body. Additive; unset params are omitted. + ## [0.1.4] - 2026-07-10 ### Added @@ -62,7 +71,8 @@ Baseline. Earlier history is in the --- -[Unreleased]: https://github.com/spore-host/python-sdk/compare/python-v0.1.4...HEAD +[Unreleased]: https://github.com/spore-host/python-sdk/compare/python-v0.1.5...HEAD +[0.1.5]: https://github.com/spore-host/python-sdk/compare/python-v0.1.4...python-v0.1.5 [0.1.4]: https://github.com/spore-host/python-sdk/compare/python-v0.1.3...python-v0.1.4 [0.1.3]: https://github.com/spore-host/python-sdk/compare/python-v0.1.2...python-v0.1.3 [0.1.2]: https://github.com/spore-host/python-sdk/releases/tag/python-v0.1.2 diff --git a/pyproject.toml b/pyproject.toml index 55d0d43..a016297 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "spore-host" -version = "0.1.4" +version = "0.1.5" description = "Python SDK for spore.host — ephemeral EC2 compute for researchers" readme = "README.md" requires-python = ">=3.9" diff --git a/spore/__init__.py b/spore/__init__.py index 21c1f52..c6caf91 100644 --- a/spore/__init__.py +++ b/spore/__init__.py @@ -66,7 +66,7 @@ def __repr__(self) -> str: spawn = _LazySubClient("spawn") notifications = _LazySubClient("notifications") -__version__ = "0.1.4" +__version__ = "0.1.5" __all__ = [ "Client", "truffle", diff --git a/spore/_spawn.py b/spore/_spawn.py index 340452a..e30caae 100644 --- a/spore/_spawn.py +++ b/spore/_spawn.py @@ -151,6 +151,10 @@ def launch( on_complete: str = "terminate", slack_workspace: Optional[str] = None, active_processes: Optional[List[str]] = None, + ami: Optional[str] = None, + key_name: Optional[str] = None, + pre_stop: Optional[str] = None, + completion_file: Optional[str] = None, wait: bool = False, ) -> Instance: """ @@ -166,6 +170,11 @@ def launch( on_complete: Action on SPAWN_COMPLETE: "terminate", "stop", "hibernate". slack_workspace: Slack workspace ID for lifecycle notifications. active_processes: Process names that indicate active work (e.g. ["rsession"]). + ami: Custom AMI id (default: spore.host's recommended AMI). + key_name: EC2 key pair name for SSH access. + pre_stop: Shell command run on the instance before stop/terminate. + completion_file: Path spored watches for the completion signal + (default: /tmp/SPAWN_COMPLETE). wait: If True, block until instance is running. For SMS notifications, register your number separately via @@ -199,6 +208,14 @@ def launch( body["slack_workspace"] = slack_workspace if active_processes: body["active_processes"] = ",".join(active_processes) + if ami: + body["ami"] = ami + if key_name: + body["key_name"] = key_name + if pre_stop: + body["pre_stop"] = pre_stop + if completion_file: + body["completion_file"] = completion_file data = self._c.post("/v1/instances", body) # Build via _parse (single source of truth for API→Instance mapping). The diff --git a/tests/test_sdk.py b/tests/test_sdk.py index 89df545..8285407 100644 --- a/tests/test_sdk.py +++ b/tests/test_sdk.py @@ -158,6 +158,32 @@ def test_spawn_launch_returns_instance_with_fields(): assert body["ttl"] == "4h" +def test_spawn_launch_forwards_advanced_params(): + # #6: ami/key_name/pre_stop/completion_file must reach the request body with + # the API's real keys (instances.go launch body). + fake = FakeClient(post_return=_launch_response()) + SpawnClient(fake).launch( + "c7i.2xlarge", + ami="ami-0abc", + key_name="my-key", + pre_stop="aws s3 sync /out s3://bucket/", + completion_file="/tmp/DONE", + ) + _, body = fake.post_calls[0] + assert body["ami"] == "ami-0abc" + assert body["key_name"] == "my-key" + assert body["pre_stop"] == "aws s3 sync /out s3://bucket/" + assert body["completion_file"] == "/tmp/DONE" + + +def test_spawn_launch_omits_unset_advanced_params(): + fake = FakeClient(post_return=_launch_response()) + SpawnClient(fake).launch("c7i.2xlarge") + _, body = fake.post_calls[0] + for k in ("ami", "key_name", "pre_stop", "completion_file"): + assert k not in body + + # ── spawn status/list parsing against real instances.go keys ──────────────── def test_spawn_status_parses_instance():