Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
b8705fb
feat(runtime): Add generic handler factory for serverless execution
deanq Jan 3, 2026
8c0b62a
feat(cli): Add handler generator, manifest builder, and scanner for b…
deanq Jan 3, 2026
c14ed9f
test(runtime): Add comprehensive tests for generic handler
deanq Jan 3, 2026
8c84c34
test(cli): Add tests for handler generation, manifest building, and s…
deanq Jan 3, 2026
cc77fa5
docs(runtime): Document generic handler factory architecture
deanq Jan 3, 2026
72ff4a1
docs(cli): Add flash build command documentation
deanq Jan 3, 2026
e761d48
docs: Add build process and handler generation section to README
deanq Jan 3, 2026
9af1505
feat(cli): Integrate build utilities into flash build command
deanq Jan 3, 2026
b1968d6
refactor(build): Fix directory structure and add comprehensive error …
deanq Jan 3, 2026
8717dc3
feat(resources): Add LoadBalancerSlsResource for LB endpoints
deanq Jan 4, 2026
3cdb565
fix(test): Fix LoadBalancerSlsResource deployment test mocks
deanq Jan 4, 2026
daa1375
feat(resources): Phase 1 - Core infrastructure for @remote on LB endp…
deanq Jan 4, 2026
d02082b
feat(build): Phase 2.1 - Enhanced scanner for HTTP routing extraction
deanq Jan 4, 2026
e83c4f0
feat(build): Phase 2.2 - Updated manifest schema for HTTP routing
deanq Jan 4, 2026
3b41ca4
feat(cli): Add LB handler generator for FastAPI app creation
deanq Jan 4, 2026
6cc2888
feat(runtime): Implement LB handler factory for FastAPI app creation
deanq Jan 4, 2026
babfe12
feat(cli): Route build command to separate handlers for LB endpoints
deanq Jan 4, 2026
c9a160b
feat(resources): Add LiveLoadBalancer for local LB endpoint testing
deanq Jan 4, 2026
7f1961b
test(stubs): Add comprehensive unit tests for LoadBalancerSlsStub
deanq Jan 4, 2026
bc8f733
fix(test): Correct LB endpoint test decorator to match assertions
deanq Jan 4, 2026
79e8f88
docs: Add comprehensive documentation for @remote with LoadBalancer e…
deanq Jan 4, 2026
47d73f8
security: Remove /execute from deployed LoadBalancer endpoints
deanq Jan 4, 2026
2353c69
feat(build): Phase 4 - Fix LiveLoadBalancer handler generation to inc…
deanq Jan 4, 2026
d86b58c
fix(scanner): Discover LoadBalancer resources in addition to Serverle…
deanq Jan 4, 2026
db28ae0
chore: Format code for line length and remove unused imports
deanq Jan 4, 2026
7304d17
fix: Address PR #131 review feedback
deanq Jan 4, 2026
0218995
style: Format datetime chaining for line length
deanq Jan 4, 2026
483536b
fix: LiveLoadBalancer template not serialized to RunPod GraphQL
deanq Jan 4, 2026
ca8cd7e
fix: LoadBalancer endpoint URL and add CPU support
deanq Jan 4, 2026
17bf287
fix: Export CpuLiveLoadBalancer and CpuLoadBalancerSlsResource from t…
deanq Jan 4, 2026
a5368b7
fix: Add API key authentication to LoadBalancer health check
deanq Jan 4, 2026
8cd129a
fix(lb): Exclude flashboot from CpuLoadBalancerSlsResource GraphQL pa…
deanq Jan 4, 2026
cc73b94
fix(lb): Expand CpuInstanceType.ANY to all CPU flavors in CpuLoadBala…
deanq Jan 4, 2026
8bf1739
refactor(cpu): Move instanceIds validator to CpuEndpointMixin
deanq Jan 4, 2026
8f31e03
test: Update CPU instance test to reflect validator expansion
deanq Jan 4, 2026
5da2441
fix(lb): Increase health check timeout from 5s to 15s
deanq Jan 4, 2026
586286d
fix(lb): Fix CPU load balancer template deployment error
deanq Jan 4, 2026
027965c
fix(drift): Exclude runtime fields from config hash to prevent false …
deanq Jan 4, 2026
1b55718
fix(http): Standardize RunPod HTTP client authentication across codebase
deanq Jan 5, 2026
8b97197
feat(http): Extend HTTP utilities to cover both sync and async authen…
deanq Jan 5, 2026
9f4e19a
fix: Address PR feedback on HTTP utilities implementation
deanq Jan 5, 2026
462654b
Merge branch 'deanq/ae-1102-load-balancer-sls-resource' into deanq/ae…
deanq Jan 5, 2026
b57748f
refactor(drift): Extract runtime field constants and improve maintain…
deanq Jan 5, 2026
915f574
docs: Improve LoadBalancer documentation accuracy and completeness
deanq Jan 5, 2026
1c6d99d
docs: add resource config drift detection documentation
deanq Jan 5, 2026
f719c73
docs: proper name for the file
deanq Jan 5, 2026
2a2a21d
test(build): Add comprehensive test coverage for scanner and handler …
deanq Jan 6, 2026
17d338a
Merge branch 'deanq/ae-1251-handler-mapper' into deanq/ae-1102-load-b…
deanq Jan 6, 2026
5ead8e7
Merge branch 'deanq/ae-1102-load-balancer-sls-resource' into deanq/ae…
deanq Jan 6, 2026
6d3ff3b
test(scanner): Fix resource type assertions to match scanner behavior
deanq Jan 6, 2026
8fe3d67
Merge branch 'deanq/ae-1102-load-balancer-sls-resource' into deanq/ae…
deanq Jan 6, 2026
2640b98
Merge branch 'main' into deanq/ae-1196-absolute-drift-detection
deanq Jan 8, 2026
6431b62
chore: merge correction
deanq Jan 8, 2026
1c31455
fix(drift): Remove manual undeploy/deploy from update() method
deanq Jan 9, 2026
426ba16
docs(drift): Clarify _has_structural_changes detects version-triggeri…
deanq Jan 9, 2026
42382af
feat(drift): Enable environment variable drift detection
deanq Jan 9, 2026
d02d8c8
test(drift): Update tests for environment variable drift detection
deanq Jan 9, 2026
9ea43f2
Merge branch 'main' into deanq/ae-1196-absolute-drift-detection
deanq Jan 9, 2026
c8bab65
fix: Address Copilot review feedback on type hints and documentation
deanq Jan 9, 2026
8464d14
chore: Update Python version compatibility to 3.10-3.14
deanq Jan 11, 2026
27aa3e7
chore: Increase code coverage requirement to 65%
deanq Jan 11, 2026
9b27521
Merge branch 'main' into deanq/ae-1679-python-310-314-compatibility
deanq Jan 12, 2026
a1a5154
Merge branch 'main' into deanq/ae-1679-python-310-314-compatibility
deanq Jan 12, 2026
ceffe7d
Merge branch 'main' into deanq/ae-1679-python-310-314-compatibility
deanq Jan 14, 2026
06f5dc3
Merge remote-tracking branch 'origin/main' into deanq/ae-1679-python-…
deanq Jan 15, 2026
d88c173
Merge branch 'main' into deanq/ae-1679-python-310-314-compatibility
deanq Jan 20, 2026
d4c6a81
refactor: remove dead code and add serialization tests
deanq Jan 20, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
timeout-minutes: 15

steps:
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
requires-python = ">=3.9,<3.14"
requires-python = ">=3.10,<3.15"

dependencies = [
"cloudpickle>=3.1.1",
Expand Down Expand Up @@ -68,7 +68,7 @@ addopts = [
"--tb=short",
"--cov=tetra_rp",
"--cov-report=term-missing",
"--cov-fail-under=35"
"--cov-fail-under=65"
]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
Expand All @@ -92,7 +92,7 @@ exclude = [

[tool.mypy]
# Basic configuration
python_version = "3.9"
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false # Start lenient, can be stricter later
Expand Down
71 changes: 0 additions & 71 deletions src/tetra_rp/core/resources/template.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import warnings
from typing import Dict, List, Optional, Any
from pydantic import BaseModel, model_validator
from tetra_rp.core.utils.http import get_authenticated_requests_session
from .base import BaseResource


Expand Down Expand Up @@ -36,72 +34,3 @@ class PodTemplate(BaseResource):
def sync_input_fields(self):
self.name = f"{self.name}__{self.resource_id}"
return self


def update_system_dependencies(
template_id, token=None, system_dependencies=None, base_entry_cmd=None
):
"""
Updates Runpod template with system dependencies installed via apt-get,
and appends the app start command.

Args:
template_id (str): Runpod template ID.
token (str): [DEPRECATED] Runpod API token. Ignored; uses RUNPOD_API_KEY env var instead.
system_dependencies (List[str]): List of apt packages to install.
base_entry_cmd (List[str]): The default command to run the app, e.g. ["uv", "run", "handler.py"]
Returns:
dict: API response JSON or error info.
"""
# Warn if deprecated token parameter is used
if token is not None:
warnings.warn(
"The 'token' parameter is deprecated and ignored. "
"Authentication now uses RUNPOD_API_KEY environment variable.",
DeprecationWarning,
stacklevel=2,
)

# Compose apt-get install command if any packages specified
apt_cmd = ""
if system_dependencies:
joined_pkgs = " ".join(system_dependencies)
apt_cmd = f"apt-get update && apt-get install -y {joined_pkgs} && "

# Default start command if not provided
app_cmd = base_entry_cmd or ["uv", "run", "handler.py"]
app_cmd_str = " ".join(app_cmd)

# Full command to run in entrypoint shell
full_cmd = f"{apt_cmd}exec {app_cmd_str}"

payload = {
# other required fields like disk, env, image, etc, should be fetched or passed in real usage
"dockerEntrypoint": ["/bin/bash", "-c", full_cmd],
"dockerStartCmd": [],
# placeholder values, replace as needed or fetch from current template state
"containerDiskInGb": 50,
"containerRegistryAuthId": "",
"env": {},
"imageName": "your-image-name",
"isPublic": False,
"name": "your-template-name",
"ports": ["8888/http", "22/tcp"],
"readme": "",
"volumeInGb": 20,
"volumeMountPath": "/workspace",
}

url = f"https://rest.runpod.io/v1/templates/{template_id}/update"

# Use centralized auth utility instead of manual header setup
# Note: token parameter is deprecated; uses RUNPOD_API_KEY environment variable
session = get_authenticated_requests_session()
try:
response = session.post(url, json=payload)
response.raise_for_status()
return response.json()
except Exception as e:
return {"error": "Failed to update template", "details": str(e)}
finally:
session.close()
50 changes: 0 additions & 50 deletions src/tetra_rp/core/resources/utils.py

This file was deleted.

33 changes: 0 additions & 33 deletions src/tetra_rp/core/utils/json.py

This file was deleted.

183 changes: 183 additions & 0 deletions tests/unit/runtime/test_serialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""Tests for serialization utilities."""

from unittest.mock import patch

import pytest

from tetra_rp.runtime.exceptions import SerializationError
from tetra_rp.runtime.serialization import (
deserialize_arg,
deserialize_args,
deserialize_kwargs,
serialize_arg,
serialize_args,
serialize_kwargs,
)


class TestSerializeArg:
"""Test serialize_arg function."""

def test_serialize_simple_arg(self):
"""Test serializing a simple argument."""
result = serialize_arg(42)
assert isinstance(result, str)
# Verify it's valid base64
import base64

decoded = base64.b64decode(result)
assert len(decoded) > 0

def test_serialize_raises_on_cloudpickle_error(self):
"""Test serialize_arg handles cloudpickle errors."""
with patch("cloudpickle.dumps") as mock_dumps:
mock_dumps.side_effect = RuntimeError("Unexpected cloudpickle error")
with pytest.raises(
SerializationError, match="Failed to serialize argument"
):
serialize_arg(42)


class TestSerializeArgs:
"""Test serialize_args function."""

def test_serialize_multiple_args(self):
"""Test serializing multiple arguments."""
result = serialize_args((1, "test", [1, 2, 3]))
assert len(result) == 3
assert all(isinstance(item, str) for item in result)

def test_serialize_empty_args(self):
"""Test serializing empty args tuple."""
result = serialize_args(())
assert result == []

def test_serialize_args_propagates_serialization_error(self):
"""Test serialize_args propagates SerializationError."""
with patch("tetra_rp.runtime.serialization.serialize_arg") as mock_serialize:
mock_serialize.side_effect = SerializationError("Known error")
with pytest.raises(SerializationError, match="Known error"):
serialize_args((1, 2))

def test_serialize_args_unexpected_error(self):
"""Test serialize_args handles unexpected exceptions."""
with patch("tetra_rp.runtime.serialization.serialize_arg") as mock_serialize:
mock_serialize.side_effect = RuntimeError("Unexpected error")
with pytest.raises(SerializationError, match="Failed to serialize args"):
serialize_args((1, 2))


class TestSerializeKwargs:
"""Test serialize_kwargs function."""

def test_serialize_kwargs(self):
"""Test serializing keyword arguments."""
result = serialize_kwargs({"key1": 42, "key2": "test"})
assert len(result) == 2
assert "key1" in result
assert "key2" in result
assert all(isinstance(v, str) for v in result.values())

def test_serialize_empty_kwargs(self):
"""Test serializing empty kwargs dict."""
result = serialize_kwargs({})
assert result == {}

def test_serialize_kwargs_propagates_serialization_error(self):
"""Test serialize_kwargs propagates SerializationError."""
with patch("tetra_rp.runtime.serialization.serialize_arg") as mock_serialize:
mock_serialize.side_effect = SerializationError("Known error")
with pytest.raises(SerializationError, match="Known error"):
serialize_kwargs({"key": 42})

def test_serialize_kwargs_unexpected_error(self):
"""Test serialize_kwargs handles unexpected exceptions."""
with patch("tetra_rp.runtime.serialization.serialize_arg") as mock_serialize:
mock_serialize.side_effect = RuntimeError("Unexpected error")
with pytest.raises(SerializationError, match="Failed to serialize kwargs"):
serialize_kwargs({"key": 42})


class TestDeserializeArg:
"""Test deserialize_arg function."""

def test_deserialize_simple_arg(self):
"""Test deserializing a simple argument."""
# First serialize something
serialized = serialize_arg(42)
# Then deserialize it
result = deserialize_arg(serialized)
assert result == 42

def test_deserialize_raises_on_invalid_base64(self):
"""Test deserialize_arg raises on invalid base64."""
with pytest.raises(SerializationError, match="Failed to deserialize argument"):
deserialize_arg("not-valid-base64!!!")


class TestDeserializeArgs:
"""Test deserialize_args function."""

def test_deserialize_multiple_args(self):
"""Test deserializing multiple arguments."""
serialized = serialize_args((1, "test", [1, 2, 3]))
result = deserialize_args(serialized)
assert result == [1, "test", [1, 2, 3]]

def test_deserialize_empty_args(self):
"""Test deserializing empty args list."""
result = deserialize_args([])
assert result == []

def test_deserialize_args_propagates_serialization_error(self):
"""Test deserialize_args propagates SerializationError."""
with patch(
"tetra_rp.runtime.serialization.deserialize_arg"
) as mock_deserialize:
mock_deserialize.side_effect = SerializationError("Known error")
with pytest.raises(SerializationError, match="Known error"):
deserialize_args(["arg1", "arg2"])

def test_deserialize_args_unexpected_error(self):
"""Test deserialize_args handles unexpected exceptions."""
with patch(
"tetra_rp.runtime.serialization.deserialize_arg"
) as mock_deserialize:
mock_deserialize.side_effect = RuntimeError("Unexpected error")
with pytest.raises(SerializationError, match="Failed to deserialize args"):
deserialize_args(["arg1", "arg2"])


class TestDeserializeKwargs:
"""Test deserialize_kwargs function."""

def test_deserialize_kwargs(self):
"""Test deserializing keyword arguments."""
serialized = serialize_kwargs({"key1": 42, "key2": "test"})
result = deserialize_kwargs(serialized)
assert result == {"key1": 42, "key2": "test"}

def test_deserialize_empty_kwargs(self):
"""Test deserializing empty kwargs dict."""
result = deserialize_kwargs({})
assert result == {}

def test_deserialize_kwargs_propagates_serialization_error(self):
"""Test deserialize_kwargs propagates SerializationError."""
with patch(
"tetra_rp.runtime.serialization.deserialize_arg"
) as mock_deserialize:
mock_deserialize.side_effect = SerializationError("Known error")
with pytest.raises(SerializationError, match="Known error"):
deserialize_kwargs({"key": "value"})

def test_deserialize_kwargs_unexpected_error(self):
"""Test deserialize_kwargs handles unexpected exceptions."""
with patch(
"tetra_rp.runtime.serialization.deserialize_arg"
) as mock_deserialize:
mock_deserialize.side_effect = RuntimeError("Unexpected error")
with pytest.raises(
SerializationError, match="Failed to deserialize kwargs"
):
deserialize_kwargs({"key": "value"})
Loading
Loading