From 93c040c49105b59135ec829fe3c4ae73de533484 Mon Sep 17 00:00:00 2001 From: scttfrdmn <3011922+scttfrdmn@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:14:42 -0700 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20expose=20remaining=20launch=20para?= =?UTF-8?q?ms=20=E2=80=94=20ami/key=5Fname/pre=5Fstop/completion=5Ffile=20?= =?UTF-8?q?(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The REST launch endpoint accepts ami, key_name, pre_stop, completion_file (instances.go), but launch() never surfaced them. Add as optional kwargs, forwarded to the body with the real API keys when set (omitted otherwise). SDK now covers the full launch body — API-complete. Tests assert forwarding + omission. Bump 0.1.4 -> 0.1.5. Closes #6 --- CHANGELOG.md | 12 +++++++++++- pyproject.toml | 2 +- spore/__init__.py | 2 +- spore/_spawn.py | 17 +++++++++++++++++ tests/test_sdk.py | 26 ++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) 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():