-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add warmup CLI for binary pre-caching #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||||||||||||||||
| """Pre-download and cache the capiscio-core binary. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Usage: | ||||||||||||||||||||
| python -m capiscio_sdk.warmup | ||||||||||||||||||||
|
|
||||||||||||||||||||
| This is intended for Docker builds, CI pipelines, and serverless deployments | ||||||||||||||||||||
| where you want to cache the binary at build time rather than downloading it | ||||||||||||||||||||
| on first request. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| See: https://github.com/capiscio/capiscio-sdk-python/issues/41 | ||||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import subprocess | ||||||||||||||||||||
| import sys | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from capiscio_sdk._rpc.process import CORE_VERSION, ProcessManager | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def main() -> int: | ||||||||||||||||||||
| """Download and verify the capiscio-core binary, then exit.""" | ||||||||||||||||||||
| manager = ProcessManager() | ||||||||||||||||||||
|
|
||||||||||||||||||||
| print(f"capiscio-core v{CORE_VERSION}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Ensure binary is in the versioned cache (~/.capiscio/bin/<version>/) | ||||||||||||||||||||
| binary = manager.ensure_cached() | ||||||||||||||||||||
| print(f" Binary path: {binary}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Verify the binary is executable | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| result = subprocess.run( | ||||||||||||||||||||
| [str(binary), "--version"], | ||||||||||||||||||||
| capture_output=True, | ||||||||||||||||||||
| text=True, | ||||||||||||||||||||
| timeout=10, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| if result.returncode == 0: | ||||||||||||||||||||
| version_str = result.stdout.strip() or result.stderr.strip() | ||||||||||||||||||||
| print(f" Binary OK \u2713 ({version_str})" if version_str else " Binary OK \u2713") | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| print(f" Warning: binary exited with code {result.returncode}", file=sys.stderr) | ||||||||||||||||||||
| return 1 | ||||||||||||||||||||
| except FileNotFoundError: | ||||||||||||||||||||
| print(f" Error: binary not found at {binary}", file=sys.stderr) | ||||||||||||||||||||
| return 1 | ||||||||||||||||||||
| except subprocess.TimeoutExpired: | ||||||||||||||||||||
| # --version hanging likely means the binary exists but has different CLI | ||||||||||||||||||||
| print(" Binary OK \u2713 (version check timed out)") | ||||||||||||||||||||
|
||||||||||||||||||||
| print(" Binary OK \u2713 (version check timed out)") | |
| print(" Binary OK \u2713 (version check timed out)") | |
| except PermissionError as exc: | |
| print(f" Error: failed to execute binary at {binary}: {exc}", file=sys.stderr) | |
| return 1 | |
| except Exception as exc: | |
| # Catch-all to avoid uncaught tracebacks in CLI usage | |
| print(f" Error: unexpected failure while running binary: {exc}", file=sys.stderr) | |
| return 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed — added PermissionError handler and a catch-all Exception handler. Both emit a clean error message to stderr and return non-zero. Added test_permission_error test case.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,106 @@ | ||||||||||||||||||||||||||||||||||
| """Tests for the warmup CLI module.""" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| from unittest.mock import MagicMock, patch | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| from capiscio_sdk.warmup import main | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| class TestWarmup: | ||||||||||||||||||||||||||||||||||
| def test_already_cached(self, tmp_path): | ||||||||||||||||||||||||||||||||||
| """When binary is already cached, returns 0.""" | ||||||||||||||||||||||||||||||||||
| fake_binary = tmp_path / "capiscio" | ||||||||||||||||||||||||||||||||||
| fake_binary.write_text("#!/bin/sh\necho v2.4.0") | ||||||||||||||||||||||||||||||||||
| fake_binary.chmod(0o755) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| with patch("capiscio_sdk.warmup.ProcessManager") as MockPM: | ||||||||||||||||||||||||||||||||||
| instance = MockPM.return_value | ||||||||||||||||||||||||||||||||||
| instance.ensure_cached.return_value = fake_binary | ||||||||||||||||||||||||||||||||||
| with patch("capiscio_sdk.warmup.subprocess") as mock_sub: | ||||||||||||||||||||||||||||||||||
| mock_sub.run.return_value = MagicMock(returncode=0, stdout="v2.4.0", stderr="") | ||||||||||||||||||||||||||||||||||
| mock_sub.TimeoutExpired = TimeoutError | ||||||||||||||||||||||||||||||||||
| result = main() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| assert result == 0 | ||||||||||||||||||||||||||||||||||
| instance.ensure_cached.assert_called_once() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def test_download_needed(self, tmp_path): | ||||||||||||||||||||||||||||||||||
| """When binary is not cached, downloads it and returns 0.""" | ||||||||||||||||||||||||||||||||||
| fake_binary = tmp_path / "capiscio" | ||||||||||||||||||||||||||||||||||
| fake_binary.write_text("#!/bin/sh\necho v2.4.0") | ||||||||||||||||||||||||||||||||||
| fake_binary.chmod(0o755) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| with patch("capiscio_sdk.warmup.ProcessManager") as MockPM: | ||||||||||||||||||||||||||||||||||
| instance = MockPM.return_value | ||||||||||||||||||||||||||||||||||
| instance.ensure_cached.return_value = fake_binary | ||||||||||||||||||||||||||||||||||
| with patch("capiscio_sdk.warmup.subprocess") as mock_sub: | ||||||||||||||||||||||||||||||||||
| mock_sub.run.return_value = MagicMock(returncode=0, stdout="v2.4.0", stderr="") | ||||||||||||||||||||||||||||||||||
| mock_sub.TimeoutExpired = TimeoutError | ||||||||||||||||||||||||||||||||||
| result = main() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| assert result == 0 | ||||||||||||||||||||||||||||||||||
| instance.ensure_cached.assert_called_once() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+44
|
||||||||||||||||||||||||||||||||||
| def test_download_needed(self, tmp_path): | |
| """When binary is not cached, downloads it and returns 0.""" | |
| fake_binary = tmp_path / "capiscio" | |
| fake_binary.write_text("#!/bin/sh\necho v2.4.0") | |
| fake_binary.chmod(0o755) | |
| with patch("capiscio_sdk.warmup.ProcessManager") as MockPM: | |
| instance = MockPM.return_value | |
| instance.ensure_cached.return_value = fake_binary | |
| with patch("capiscio_sdk.warmup.subprocess") as mock_sub: | |
| mock_sub.run.return_value = MagicMock(returncode=0, stdout="v2.4.0", stderr="") | |
| mock_sub.TimeoutExpired = TimeoutError | |
| result = main() | |
| assert result == 0 | |
| instance.ensure_cached.assert_called_once() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The success output uses a Unicode checkmark ("✓"). On some Windows consoles or ASCII-only stdout encodings (common in minimal Docker/CI environments), this can raise
UnicodeEncodeErrorand cause the warmup command to fail even though the binary is OK. Prefer ASCII-only output (e.g., "OK") or gate the symbol onsys.stdout.encodingcapability.