diff --git a/scripts/lint b/scripts/lint index 36ff12baf..b6390e3ae 100755 --- a/scripts/lint +++ b/scripts/lint @@ -13,5 +13,8 @@ uv run mypy . echo "==> Running lints" uv run ruff check . "$@" +echo "==> Running format check" +uv run ruff format --check . + echo "==> Making sure it imports" uv run python -c 'import runloop_api_client' diff --git a/src/runloop_api_client/sdk/__init__.py b/src/runloop_api_client/sdk/__init__.py index 15f18f8b2..edfe3f148 100644 --- a/src/runloop_api_client/sdk/__init__.py +++ b/src/runloop_api_client/sdk/__init__.py @@ -14,6 +14,7 @@ SnapshotOps, BenchmarkOps, BlueprintOps, + GatewayConfigOps, NetworkPolicyOps, StorageObjectOps, ) @@ -28,6 +29,7 @@ AsyncSnapshotOps, AsyncBenchmarkOps, AsyncBlueprintOps, + AsyncGatewayConfigOps, AsyncNetworkPolicyOps, AsyncStorageObjectOps, ) @@ -45,6 +47,7 @@ from .benchmark_run import BenchmarkRun from .async_scenario import AsyncScenario from .async_snapshot import AsyncSnapshot +from .gateway_config import GatewayConfig from .network_policy import NetworkPolicy from .storage_object import StorageObject from .async_benchmark import AsyncBenchmark @@ -54,6 +57,7 @@ from .scenario_builder import ScenarioBuilder from .async_scenario_run import AsyncScenarioRun from .async_benchmark_run import AsyncBenchmarkRun +from .async_gateway_config import AsyncGatewayConfig from .async_network_policy import AsyncNetworkPolicy from .async_storage_object import AsyncStorageObject from .async_execution_result import AsyncExecutionResult @@ -82,6 +86,8 @@ "AsyncStorageObjectOps", "NetworkPolicyOps", "AsyncNetworkPolicyOps", + "GatewayConfigOps", + "AsyncGatewayConfigOps", # Resource classes "Agent", "AsyncAgent", @@ -112,6 +118,8 @@ "AsyncStorageObject", "NetworkPolicy", "AsyncNetworkPolicy", + "GatewayConfig", + "AsyncGatewayConfig", "NamedShell", "AsyncNamedShell", ] diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index b7048a536..9eb0526bc 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -19,12 +19,15 @@ BenchmarkUpdateParams, BlueprintCreateParams, DevboxUploadFileParams, + GatewayConfigListParams, NetworkPolicyListParams, DevboxCreateTunnelParams, DevboxDownloadFileParams, DevboxEnableTunnelParams, DevboxRemoveTunnelParams, DevboxSnapshotDiskParams, + GatewayConfigCreateParams, + GatewayConfigUpdateParams, NetworkPolicyCreateParams, NetworkPolicyUpdateParams, DevboxReadFileContentsParams, @@ -262,3 +265,15 @@ class SDKNetworkPolicyListParams(NetworkPolicyListParams, BaseRequestOptions): class SDKNetworkPolicyUpdateParams(NetworkPolicyUpdateParams, LongRequestOptions): pass + + +class SDKGatewayConfigCreateParams(GatewayConfigCreateParams, LongRequestOptions): + pass + + +class SDKGatewayConfigListParams(GatewayConfigListParams, BaseRequestOptions): + pass + + +class SDKGatewayConfigUpdateParams(GatewayConfigUpdateParams, LongRequestOptions): + pass diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index 98519f36f..c35ae98ae 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -26,7 +26,9 @@ SDKBenchmarkCreateParams, SDKBlueprintCreateParams, SDKDiskSnapshotListParams, + SDKGatewayConfigListParams, SDKNetworkPolicyListParams, + SDKGatewayConfigCreateParams, SDKNetworkPolicyCreateParams, SDKDevboxCreateFromImageParams, ) @@ -41,6 +43,7 @@ from .async_benchmark import AsyncBenchmark from .async_blueprint import AsyncBlueprint from ..lib.context_loader import TarFilter, build_directory_tar +from .async_gateway_config import AsyncGatewayConfig from .async_network_policy import AsyncNetworkPolicy from .async_storage_object import AsyncStorageObject from .async_scenario_builder import AsyncScenarioBuilder @@ -924,6 +927,90 @@ async def list(self, **params: Unpack[SDKNetworkPolicyListParams]) -> list[Async return [AsyncNetworkPolicy(self._client, item.id) for item in page.network_policies] +class AsyncGatewayConfigOps: + """High-level async manager for creating and managing gateway configurations. + + Accessed via ``runloop.gateway_config`` from :class:`AsyncRunloopSDK`, provides + coroutines to create, retrieve, update, delete, and list gateway configs. Gateway configs + define how to proxy API requests through the credential gateway, enabling secure API + proxying without exposing API keys. + + Example: + >>> runloop = AsyncRunloopSDK() + >>> gateway_config = await runloop.gateway_config.create( + ... name="my-api-gateway", + ... endpoint="https://api.example.com", + ... auth_mechanism={"type": "bearer"}, + ... ) + >>> # Use with a devbox + >>> devbox = await runloop.devbox.create( + ... name="my-devbox", + ... gateways={ + ... "MY_API": { + ... "gateway": gateway_config.id, + ... "secret": "my-api-key-secret", + ... }, + ... }, + ... ) + """ + + def __init__(self, client: AsyncRunloop) -> None: + """Initialize AsyncGatewayConfigOps. + + :param client: AsyncRunloop client instance + :type client: AsyncRunloop + """ + self._client = client + + async def create(self, **params: Unpack[SDKGatewayConfigCreateParams]) -> AsyncGatewayConfig: + """Create a new gateway config. + + Example: + >>> gateway_config = await runloop.gateway_config.create( + ... name="my-gateway", + ... endpoint="https://api.example.com", + ... auth_mechanism={"type": "header", "key": "x-api-key"}, + ... description="Gateway for My API", + ... ) + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKGatewayConfigCreateParams` for available parameters + :return: The newly created gateway config + :rtype: AsyncGatewayConfig + """ + response = await self._client.gateway_configs.create(**params) + return AsyncGatewayConfig(self._client, response.id) + + def from_id(self, gateway_config_id: str) -> AsyncGatewayConfig: + """Get an AsyncGatewayConfig instance for an existing gateway config ID. + + Example: + >>> gateway_config = runloop.gateway_config.from_id("gwc_1234567890") + >>> info = await gateway_config.get_info() + >>> print(f"Gateway Config name: {info.name}") + + :param gateway_config_id: ID of the gateway config + :type gateway_config_id: str + :return: AsyncGatewayConfig instance for the given ID + :rtype: AsyncGatewayConfig + """ + return AsyncGatewayConfig(self._client, gateway_config_id) + + async def list(self, **params: Unpack[SDKGatewayConfigListParams]) -> list[AsyncGatewayConfig]: + """List all gateway configs, optionally filtered by parameters. + + Example: + >>> configs = await runloop.gateway_config.list(limit=10) + >>> for config in configs: + ... print(config.id) + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKGatewayConfigListParams` for available parameters + :return: List of gateway configs + :rtype: list[AsyncGatewayConfig] + """ + page = await self._client.gateway_configs.list(**params) + return [AsyncGatewayConfig(self._client, item.id) for item in page.gateway_configs] + + class AsyncRunloopSDK: """High-level asynchronous entry point for the Runloop SDK. @@ -951,6 +1038,8 @@ class AsyncRunloopSDK: :vartype storage_object: AsyncStorageObjectOps :ivar network_policy: High-level async interface for network policy management :vartype network_policy: AsyncNetworkPolicyOps + :ivar gateway_config: High-level async interface for gateway config management + :vartype gateway_config: AsyncGatewayConfigOps Example: >>> runloop = AsyncRunloopSDK() # Uses RUNLOOP_API_KEY env var @@ -965,6 +1054,7 @@ class AsyncRunloopSDK: benchmark: AsyncBenchmarkOps devbox: AsyncDevboxOps blueprint: AsyncBlueprintOps + gateway_config: AsyncGatewayConfigOps network_policy: AsyncNetworkPolicyOps scenario: AsyncScenarioOps scorer: AsyncScorerOps @@ -1013,6 +1103,7 @@ def __init__( self.benchmark = AsyncBenchmarkOps(self.api) self.devbox = AsyncDevboxOps(self.api) self.blueprint = AsyncBlueprintOps(self.api) + self.gateway_config = AsyncGatewayConfigOps(self.api) self.network_policy = AsyncNetworkPolicyOps(self.api) self.scenario = AsyncScenarioOps(self.api) self.scorer = AsyncScorerOps(self.api) diff --git a/src/runloop_api_client/sdk/async_gateway_config.py b/src/runloop_api_client/sdk/async_gateway_config.py new file mode 100644 index 000000000..423e8c3be --- /dev/null +++ b/src/runloop_api_client/sdk/async_gateway_config.py @@ -0,0 +1,108 @@ +"""GatewayConfig resource class for asynchronous operations.""" + +from __future__ import annotations + +from typing_extensions import Unpack, override + +from ._types import BaseRequestOptions, LongRequestOptions, SDKGatewayConfigUpdateParams +from .._client import AsyncRunloop +from ..types.gateway_config_view import GatewayConfigView + + +class AsyncGatewayConfig: + """Asynchronous wrapper around a gateway config resource. + + Gateway configs define how to proxy API requests through the credential gateway. + They specify the target endpoint and how credentials should be applied. Use with + devboxes to securely proxy requests to external APIs without exposing API keys. + + Example: + >>> runloop = AsyncRunloopSDK() + >>> gateway_config = await runloop.gateway_config.create( + ... name="my-api-gateway", + ... endpoint="https://api.example.com", + ... auth_mechanism={"type": "bearer"}, + ... ) + >>> info = await gateway_config.get_info() + >>> print(f"Gateway Config: {info.name}") + """ + + def __init__( + self, + client: AsyncRunloop, + gateway_config_id: str, + ) -> None: + """Initialize the wrapper. + + :param client: Generated AsyncRunloop client + :type client: AsyncRunloop + :param gateway_config_id: GatewayConfig ID returned by the API + :type gateway_config_id: str + """ + self._client = client + self._id = gateway_config_id + + @override + def __repr__(self) -> str: + return f"" + + @property + def id(self) -> str: + """Return the gateway config ID. + + :return: Unique gateway config ID + :rtype: str + """ + return self._id + + async def get_info( + self, + **options: Unpack[BaseRequestOptions], + ) -> GatewayConfigView: + """Retrieve the latest gateway config details. + + Example: + >>> info = await gateway_config.get_info() + >>> print(f"Gateway Config: {info.name}, endpoint: {info.endpoint}") + + :param options: Optional request configuration + :return: API response describing the gateway config + :rtype: GatewayConfigView + """ + return await self._client.gateway_configs.retrieve( + self._id, + **options, + ) + + async def update(self, **params: Unpack[SDKGatewayConfigUpdateParams]) -> GatewayConfigView: + """Update the gateway config. + + Example: + >>> updated = await gateway_config.update( + ... name="updated-gateway-name", + ... description="Updated description", + ... ) + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKGatewayConfigUpdateParams` for available parameters + :return: Updated gateway config view + :rtype: GatewayConfigView + """ + return await self._client.gateway_configs.update(self._id, **params) + + async def delete( + self, + **options: Unpack[LongRequestOptions], + ) -> GatewayConfigView: + """Delete the gateway config. This action is irreversible. + + Example: + >>> await gateway_config.delete() + + :param options: Optional long-running request configuration + :return: API response acknowledging deletion + :rtype: GatewayConfigView + """ + return await self._client.gateway_configs.delete( + self._id, + **options, + ) diff --git a/src/runloop_api_client/sdk/gateway_config.py b/src/runloop_api_client/sdk/gateway_config.py new file mode 100644 index 000000000..0a067fb48 --- /dev/null +++ b/src/runloop_api_client/sdk/gateway_config.py @@ -0,0 +1,108 @@ +"""GatewayConfig resource class for synchronous operations.""" + +from __future__ import annotations + +from typing_extensions import Unpack, override + +from ._types import BaseRequestOptions, LongRequestOptions, SDKGatewayConfigUpdateParams +from .._client import Runloop +from ..types.gateway_config_view import GatewayConfigView + + +class GatewayConfig: + """Synchronous wrapper around a gateway config resource. + + Gateway configs define how to proxy API requests through the credential gateway. + They specify the target endpoint and how credentials should be applied. Use with + devboxes to securely proxy requests to external APIs without exposing API keys. + + Example: + >>> runloop = RunloopSDK() + >>> gateway_config = runloop.gateway_config.create( + ... name="my-api-gateway", + ... endpoint="https://api.example.com", + ... auth_mechanism={"type": "bearer"}, + ... ) + >>> info = gateway_config.get_info() + >>> print(f"Gateway Config: {info.name}") + """ + + def __init__( + self, + client: Runloop, + gateway_config_id: str, + ) -> None: + """Initialize the wrapper. + + :param client: Generated Runloop client + :type client: Runloop + :param gateway_config_id: GatewayConfig ID returned by the API + :type gateway_config_id: str + """ + self._client = client + self._id = gateway_config_id + + @override + def __repr__(self) -> str: + return f"" + + @property + def id(self) -> str: + """Return the gateway config ID. + + :return: Unique gateway config ID + :rtype: str + """ + return self._id + + def get_info( + self, + **options: Unpack[BaseRequestOptions], + ) -> GatewayConfigView: + """Retrieve the latest gateway config details. + + Example: + >>> info = gateway_config.get_info() + >>> print(f"Gateway Config: {info.name}, endpoint: {info.endpoint}") + + :param options: Optional request configuration + :return: API response describing the gateway config + :rtype: GatewayConfigView + """ + return self._client.gateway_configs.retrieve( + self._id, + **options, + ) + + def update(self, **params: Unpack[SDKGatewayConfigUpdateParams]) -> GatewayConfigView: + """Update the gateway config. + + Example: + >>> updated = gateway_config.update( + ... name="updated-gateway-name", + ... description="Updated description", + ... ) + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKGatewayConfigUpdateParams` for available parameters + :return: Updated gateway config view + :rtype: GatewayConfigView + """ + return self._client.gateway_configs.update(self._id, **params) + + def delete( + self, + **options: Unpack[LongRequestOptions], + ) -> GatewayConfigView: + """Delete the gateway config. This action is irreversible. + + Example: + >>> gateway_config.delete() + + :param options: Optional long-running request configuration + :return: API response acknowledging deletion + :rtype: GatewayConfigView + """ + return self._client.gateway_configs.delete( + self._id, + **options, + ) diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index aa5fa27fc..938576508 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -26,7 +26,9 @@ SDKBenchmarkCreateParams, SDKBlueprintCreateParams, SDKDiskSnapshotListParams, + SDKGatewayConfigListParams, SDKNetworkPolicyListParams, + SDKGatewayConfigCreateParams, SDKNetworkPolicyCreateParams, SDKDevboxCreateFromImageParams, ) @@ -39,6 +41,7 @@ from .snapshot import Snapshot from .benchmark import Benchmark from .blueprint import Blueprint +from .gateway_config import GatewayConfig from .network_policy import NetworkPolicy from .storage_object import StorageObject from .scenario_builder import ScenarioBuilder @@ -949,6 +952,90 @@ def list(self, **params: Unpack[SDKNetworkPolicyListParams]) -> list[NetworkPoli return [NetworkPolicy(self._client, item.id) for item in page.network_policies] +class GatewayConfigOps: + """High-level manager for creating and managing gateway configurations. + + Accessed via ``runloop.gateway_config`` from :class:`RunloopSDK`, provides methods + to create, retrieve, update, delete, and list gateway configs. Gateway configs define + how to proxy API requests through the credential gateway, enabling secure API + proxying without exposing API keys. + + Example: + >>> runloop = RunloopSDK() + >>> gateway_config = runloop.gateway_config.create( + ... name="my-api-gateway", + ... endpoint="https://api.example.com", + ... auth_mechanism={"type": "bearer"}, + ... ) + >>> # Use with a devbox + >>> devbox = runloop.devbox.create( + ... name="my-devbox", + ... gateways={ + ... "MY_API": { + ... "gateway": gateway_config.id, + ... "secret": "my-api-key-secret", + ... }, + ... }, + ... ) + """ + + def __init__(self, client: Runloop) -> None: + """Initialize GatewayConfigOps. + + :param client: Runloop client instance + :type client: Runloop + """ + self._client = client + + def create(self, **params: Unpack[SDKGatewayConfigCreateParams]) -> GatewayConfig: + """Create a new gateway config. + + Example: + >>> gateway_config = runloop.gateway_config.create( + ... name="my-gateway", + ... endpoint="https://api.example.com", + ... auth_mechanism={"type": "header", "key": "x-api-key"}, + ... description="Gateway for My API", + ... ) + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKGatewayConfigCreateParams` for available parameters + :return: The newly created gateway config + :rtype: GatewayConfig + """ + response = self._client.gateway_configs.create(**params) + return GatewayConfig(self._client, response.id) + + def from_id(self, gateway_config_id: str) -> GatewayConfig: + """Get a GatewayConfig instance for an existing gateway config ID. + + Example: + >>> gateway_config = runloop.gateway_config.from_id("gwc_1234567890") + >>> info = gateway_config.get_info() + >>> print(f"Gateway Config name: {info.name}") + + :param gateway_config_id: ID of the gateway config + :type gateway_config_id: str + :return: GatewayConfig instance for the given ID + :rtype: GatewayConfig + """ + return GatewayConfig(self._client, gateway_config_id) + + def list(self, **params: Unpack[SDKGatewayConfigListParams]) -> list[GatewayConfig]: + """List all gateway configs, optionally filtered by parameters. + + Example: + >>> configs = runloop.gateway_config.list(limit=10) + >>> for config in configs: + ... print(config.id) + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKGatewayConfigListParams` for available parameters + :return: List of gateway configs + :rtype: list[GatewayConfig] + """ + page = self._client.gateway_configs.list(**params) + return [GatewayConfig(self._client, item.id) for item in page.gateway_configs] + + class RunloopSDK: """High-level synchronous entry point for the Runloop SDK. @@ -976,6 +1063,8 @@ class RunloopSDK: :vartype storage_object: StorageObjectOps :ivar network_policy: High-level interface for network policy management :vartype network_policy: NetworkPolicyOps + :ivar gateway_config: High-level interface for gateway config management + :vartype gateway_config: GatewayConfigOps Example: >>> runloop = RunloopSDK() # Uses RUNLOOP_API_KEY env var @@ -990,6 +1079,7 @@ class RunloopSDK: benchmark: BenchmarkOps devbox: DevboxOps blueprint: BlueprintOps + gateway_config: GatewayConfigOps network_policy: NetworkPolicyOps scenario: ScenarioOps scorer: ScorerOps @@ -1038,6 +1128,7 @@ def __init__( self.benchmark = BenchmarkOps(self.api) self.devbox = DevboxOps(self.api) self.blueprint = BlueprintOps(self.api) + self.gateway_config = GatewayConfigOps(self.api) self.network_policy = NetworkPolicyOps(self.api) self.scenario = ScenarioOps(self.api) self.scorer = ScorerOps(self.api) diff --git a/tests/sdk/conftest.py b/tests/sdk/conftest.py index 29085ed45..a67f25b27 100644 --- a/tests/sdk/conftest.py +++ b/tests/sdk/conftest.py @@ -28,6 +28,7 @@ "benchmark": "bmd_123", "benchmark_run": "bmr_123", "network_policy": "np_123", + "gateway_config": "gwc_123", } # Test URL constants @@ -179,6 +180,26 @@ class MockNetworkPolicyView: egress: MockEgress = field(default_factory=MockEgress) +@dataclass +class MockAuthMechanism: + """Mock AuthMechanism for testing.""" + + type: str = "bearer" + key: str | None = None + + +@dataclass +class MockGatewayConfigView: + """Mock GatewayConfigView for testing.""" + + id: str = TEST_IDS["gateway_config"] + name: str = "test-gateway-config" + endpoint: str = "https://api.example.com" + description: str | None = "Test gateway config description" + create_time_ms: int = 1234567890000 + auth_mechanism: MockAuthMechanism = field(default_factory=MockAuthMechanism) + + class AsyncIterableMock: """A simple async iterable mock for testing paginated responses.""" @@ -316,6 +337,12 @@ def network_policy_view() -> MockNetworkPolicyView: return MockNetworkPolicyView() +@pytest.fixture +def gateway_config_view() -> MockGatewayConfigView: + """Create a mock GatewayConfigView.""" + return MockGatewayConfigView() + + @pytest.fixture def mock_httpx_response() -> Mock: """Create a mock httpx.Response.""" diff --git a/tests/sdk/test_async_gateway_config.py b/tests/sdk/test_async_gateway_config.py new file mode 100644 index 000000000..8c93c88a2 --- /dev/null +++ b/tests/sdk/test_async_gateway_config.py @@ -0,0 +1,89 @@ +"""Comprehensive tests for async GatewayConfig class.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest + +from tests.sdk.conftest import MockGatewayConfigView +from runloop_api_client.sdk import AsyncGatewayConfig + + +class TestAsyncGatewayConfig: + """Tests for AsyncGatewayConfig class.""" + + def test_init(self, mock_async_client: AsyncMock) -> None: + """Test AsyncGatewayConfig initialization.""" + gateway_config = AsyncGatewayConfig(mock_async_client, "gwc_123") + assert gateway_config.id == "gwc_123" + + def test_repr(self, mock_async_client: AsyncMock) -> None: + """Test AsyncGatewayConfig string representation.""" + gateway_config = AsyncGatewayConfig(mock_async_client, "gwc_123") + assert repr(gateway_config) == "" + + @pytest.mark.asyncio + async def test_get_info(self, mock_async_client: AsyncMock, gateway_config_view: MockGatewayConfigView) -> None: + """Test get_info method.""" + mock_async_client.gateway_configs.retrieve = AsyncMock(return_value=gateway_config_view) + + gateway_config = AsyncGatewayConfig(mock_async_client, "gwc_123") + result = await gateway_config.get_info( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == gateway_config_view + mock_async_client.gateway_configs.retrieve.assert_awaited_once() + + @pytest.mark.asyncio + async def test_update(self, mock_async_client: AsyncMock, gateway_config_view: MockGatewayConfigView) -> None: + """Test update method.""" + mock_async_client.gateway_configs.update = AsyncMock(return_value=gateway_config_view) + + gateway_config = AsyncGatewayConfig(mock_async_client, "gwc_123") + result = await gateway_config.update( + name="updated-gateway", + description="Updated description", + endpoint="https://api.updated.com", + auth_mechanism={"type": "header", "key": "x-api-key"}, + extra_headers={"X-Custom": "value"}, + timeout=30.0, + ) + + assert result == gateway_config_view + mock_async_client.gateway_configs.update.assert_awaited_once() + + @pytest.mark.asyncio + async def test_update_partial( + self, mock_async_client: AsyncMock, gateway_config_view: MockGatewayConfigView + ) -> None: + """Test update method with partial fields.""" + mock_async_client.gateway_configs.update = AsyncMock(return_value=gateway_config_view) + + gateway_config = AsyncGatewayConfig(mock_async_client, "gwc_123") + result = await gateway_config.update( + name="renamed-gateway", + ) + + assert result == gateway_config_view + mock_async_client.gateway_configs.update.assert_awaited_once() + + @pytest.mark.asyncio + async def test_delete(self, mock_async_client: AsyncMock, gateway_config_view: MockGatewayConfigView) -> None: + """Test delete method.""" + mock_async_client.gateway_configs.delete = AsyncMock(return_value=gateway_config_view) + + gateway_config = AsyncGatewayConfig(mock_async_client, "gwc_123") + result = await gateway_config.delete( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == gateway_config_view + mock_async_client.gateway_configs.delete.assert_awaited_once() diff --git a/tests/sdk/test_async_ops.py b/tests/sdk/test_async_ops.py index 432cb706d..b3f02ab5c 100644 --- a/tests/sdk/test_async_ops.py +++ b/tests/sdk/test_async_ops.py @@ -19,6 +19,7 @@ MockSnapshotView, MockBenchmarkView, MockBlueprintView, + MockGatewayConfigView, MockNetworkPolicyView, create_mock_httpx_response, ) @@ -38,8 +39,10 @@ AsyncSnapshotOps, AsyncBenchmarkOps, AsyncBlueprintOps, + AsyncGatewayConfig, AsyncNetworkPolicy, AsyncStorageObject, + AsyncGatewayConfigOps, AsyncNetworkPolicyOps, AsyncStorageObjectOps, ) @@ -1313,6 +1316,81 @@ async def test_list_multiple(self, mock_async_client: AsyncMock) -> None: mock_async_client.network_policies.list.assert_awaited_once() +class TestAsyncGatewayConfigOps: + """Tests for AsyncGatewayConfigOps class.""" + + @pytest.mark.asyncio + async def test_create(self, mock_async_client: AsyncMock, gateway_config_view: MockGatewayConfigView) -> None: + """Test create method.""" + mock_async_client.gateway_configs.create = AsyncMock(return_value=gateway_config_view) + + ops = AsyncGatewayConfigOps(mock_async_client) + gateway_config = await ops.create( + name="test-gateway-config", + endpoint="https://api.example.com", + auth_mechanism={"type": "bearer"}, + ) + + assert isinstance(gateway_config, AsyncGatewayConfig) + assert gateway_config.id == "gwc_123" + mock_async_client.gateway_configs.create.assert_awaited_once() + + def test_from_id(self, mock_async_client: AsyncMock) -> None: + """Test from_id method.""" + ops = AsyncGatewayConfigOps(mock_async_client) + gateway_config = ops.from_id("gwc_123") + + assert isinstance(gateway_config, AsyncGatewayConfig) + assert gateway_config.id == "gwc_123" + + @pytest.mark.asyncio + async def test_list_empty(self, mock_async_client: AsyncMock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(gateway_configs=[]) + mock_async_client.gateway_configs.list = AsyncMock(return_value=page) + + ops = AsyncGatewayConfigOps(mock_async_client) + gateway_configs = await ops.list(limit=10) + + assert len(gateway_configs) == 0 + mock_async_client.gateway_configs.list.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_single(self, mock_async_client: AsyncMock, gateway_config_view: MockGatewayConfigView) -> None: + """Test list method with single result.""" + page = SimpleNamespace(gateway_configs=[gateway_config_view]) + mock_async_client.gateway_configs.list = AsyncMock(return_value=page) + + ops = AsyncGatewayConfigOps(mock_async_client) + gateway_configs = await ops.list( + limit=10, + starting_after="gwc_000", + ) + + assert len(gateway_configs) == 1 + assert isinstance(gateway_configs[0], AsyncGatewayConfig) + assert gateway_configs[0].id == "gwc_123" + mock_async_client.gateway_configs.list.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_multiple(self, mock_async_client: AsyncMock) -> None: + """Test list method with multiple results.""" + gateway_config_view1 = MockGatewayConfigView(id="gwc_001", name="gateway-1") + gateway_config_view2 = MockGatewayConfigView(id="gwc_002", name="gateway-2") + page = SimpleNamespace(gateway_configs=[gateway_config_view1, gateway_config_view2]) + mock_async_client.gateway_configs.list = AsyncMock(return_value=page) + + ops = AsyncGatewayConfigOps(mock_async_client) + gateway_configs = await ops.list(limit=10) + + assert len(gateway_configs) == 2 + assert isinstance(gateway_configs[0], AsyncGatewayConfig) + assert isinstance(gateway_configs[1], AsyncGatewayConfig) + assert gateway_configs[0].id == "gwc_001" + assert gateway_configs[1].id == "gwc_002" + mock_async_client.gateway_configs.list.assert_awaited_once() + + class TestAsyncRunloopSDK: """Tests for AsyncRunloopSDK class.""" @@ -1323,6 +1401,7 @@ def test_init(self) -> None: assert isinstance(runloop.agent, AsyncAgentOps) assert isinstance(runloop.benchmark, AsyncBenchmarkOps) assert isinstance(runloop.devbox, AsyncDevboxOps) + assert isinstance(runloop.gateway_config, AsyncGatewayConfigOps) assert isinstance(runloop.network_policy, AsyncNetworkPolicyOps) assert isinstance(runloop.scorer, AsyncScorerOps) assert isinstance(runloop.snapshot, AsyncSnapshotOps) diff --git a/tests/sdk/test_gateway_config.py b/tests/sdk/test_gateway_config.py new file mode 100644 index 000000000..5f515fe97 --- /dev/null +++ b/tests/sdk/test_gateway_config.py @@ -0,0 +1,104 @@ +"""Comprehensive tests for sync GatewayConfig class.""" + +from __future__ import annotations + +from unittest.mock import Mock + +from tests.sdk.conftest import MockGatewayConfigView +from runloop_api_client.sdk import GatewayConfig + + +class TestGatewayConfig: + """Tests for GatewayConfig class.""" + + def test_init(self, mock_client: Mock) -> None: + """Test GatewayConfig initialization.""" + gateway_config = GatewayConfig(mock_client, "gwc_123") + assert gateway_config.id == "gwc_123" + + def test_repr(self, mock_client: Mock) -> None: + """Test GatewayConfig string representation.""" + gateway_config = GatewayConfig(mock_client, "gwc_123") + assert repr(gateway_config) == "" + + def test_get_info(self, mock_client: Mock, gateway_config_view: MockGatewayConfigView) -> None: + """Test get_info method.""" + mock_client.gateway_configs.retrieve.return_value = gateway_config_view + + gateway_config = GatewayConfig(mock_client, "gwc_123") + result = gateway_config.get_info( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == gateway_config_view + mock_client.gateway_configs.retrieve.assert_called_once_with( + "gwc_123", + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + def test_update(self, mock_client: Mock, gateway_config_view: MockGatewayConfigView) -> None: + """Test update method.""" + mock_client.gateway_configs.update.return_value = gateway_config_view + + gateway_config = GatewayConfig(mock_client, "gwc_123") + result = gateway_config.update( + name="updated-gateway", + description="Updated description", + endpoint="https://api.updated.com", + auth_mechanism={"type": "header", "key": "x-api-key"}, + extra_headers={"X-Custom": "value"}, + timeout=30.0, + ) + + assert result == gateway_config_view + mock_client.gateway_configs.update.assert_called_once_with( + "gwc_123", + name="updated-gateway", + description="Updated description", + endpoint="https://api.updated.com", + auth_mechanism={"type": "header", "key": "x-api-key"}, + extra_headers={"X-Custom": "value"}, + timeout=30.0, + ) + + def test_update_partial(self, mock_client: Mock, gateway_config_view: MockGatewayConfigView) -> None: + """Test update method with partial fields.""" + mock_client.gateway_configs.update.return_value = gateway_config_view + + gateway_config = GatewayConfig(mock_client, "gwc_123") + result = gateway_config.update( + name="renamed-gateway", + ) + + assert result == gateway_config_view + mock_client.gateway_configs.update.assert_called_once_with( + "gwc_123", + name="renamed-gateway", + ) + + def test_delete(self, mock_client: Mock, gateway_config_view: MockGatewayConfigView) -> None: + """Test delete method.""" + mock_client.gateway_configs.delete.return_value = gateway_config_view + + gateway_config = GatewayConfig(mock_client, "gwc_123") + result = gateway_config.delete( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == gateway_config_view + mock_client.gateway_configs.delete.assert_called_once_with( + "gwc_123", + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) diff --git a/tests/sdk/test_ops.py b/tests/sdk/test_ops.py index ff4075e77..3ff648adc 100644 --- a/tests/sdk/test_ops.py +++ b/tests/sdk/test_ops.py @@ -19,6 +19,7 @@ MockSnapshotView, MockBenchmarkView, MockBlueprintView, + MockGatewayConfigView, MockNetworkPolicyView, create_mock_httpx_response, ) @@ -38,8 +39,10 @@ SnapshotOps, BenchmarkOps, BlueprintOps, + GatewayConfig, NetworkPolicy, StorageObject, + GatewayConfigOps, NetworkPolicyOps, StorageObjectOps, ) @@ -1228,6 +1231,77 @@ def test_list_multiple(self, mock_client: Mock) -> None: mock_client.network_policies.list.assert_called_once() +class TestGatewayConfigOps: + """Tests for GatewayConfigOps class.""" + + def test_create(self, mock_client: Mock, gateway_config_view: MockGatewayConfigView) -> None: + """Test create method.""" + mock_client.gateway_configs.create.return_value = gateway_config_view + + ops = GatewayConfigOps(mock_client) + gateway_config = ops.create( + name="test-gateway-config", + endpoint="https://api.example.com", + auth_mechanism={"type": "bearer"}, + ) + + assert isinstance(gateway_config, GatewayConfig) + assert gateway_config.id == "gwc_123" + mock_client.gateway_configs.create.assert_called_once() + + def test_from_id(self, mock_client: Mock) -> None: + """Test from_id method.""" + ops = GatewayConfigOps(mock_client) + gateway_config = ops.from_id("gwc_123") + + assert isinstance(gateway_config, GatewayConfig) + assert gateway_config.id == "gwc_123" + + def test_list_empty(self, mock_client: Mock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(gateway_configs=[]) + mock_client.gateway_configs.list.return_value = page + + ops = GatewayConfigOps(mock_client) + gateway_configs = ops.list(limit=10) + + assert len(gateway_configs) == 0 + mock_client.gateway_configs.list.assert_called_once() + + def test_list_single(self, mock_client: Mock, gateway_config_view: MockGatewayConfigView) -> None: + """Test list method with single result.""" + page = SimpleNamespace(gateway_configs=[gateway_config_view]) + mock_client.gateway_configs.list.return_value = page + + ops = GatewayConfigOps(mock_client) + gateway_configs = ops.list( + limit=10, + starting_after="gwc_000", + ) + + assert len(gateway_configs) == 1 + assert isinstance(gateway_configs[0], GatewayConfig) + assert gateway_configs[0].id == "gwc_123" + mock_client.gateway_configs.list.assert_called_once() + + def test_list_multiple(self, mock_client: Mock) -> None: + """Test list method with multiple results.""" + gateway_config_view1 = MockGatewayConfigView(id="gwc_001", name="gateway-1") + gateway_config_view2 = MockGatewayConfigView(id="gwc_002", name="gateway-2") + page = SimpleNamespace(gateway_configs=[gateway_config_view1, gateway_config_view2]) + mock_client.gateway_configs.list.return_value = page + + ops = GatewayConfigOps(mock_client) + gateway_configs = ops.list(limit=10) + + assert len(gateway_configs) == 2 + assert isinstance(gateway_configs[0], GatewayConfig) + assert isinstance(gateway_configs[1], GatewayConfig) + assert gateway_configs[0].id == "gwc_001" + assert gateway_configs[1].id == "gwc_002" + mock_client.gateway_configs.list.assert_called_once() + + class TestRunloopSDK: """Tests for RunloopSDK class.""" @@ -1238,6 +1312,7 @@ def test_init(self) -> None: assert isinstance(runloop.agent, AgentOps) assert isinstance(runloop.benchmark, BenchmarkOps) assert isinstance(runloop.devbox, DevboxOps) + assert isinstance(runloop.gateway_config, GatewayConfigOps) assert isinstance(runloop.network_policy, NetworkPolicyOps) assert isinstance(runloop.scorer, ScorerOps) assert isinstance(runloop.snapshot, SnapshotOps) diff --git a/tests/smoketests/sdk/test_async_gateway_config.py b/tests/smoketests/sdk/test_async_gateway_config.py new file mode 100644 index 000000000..1ff4959d1 --- /dev/null +++ b/tests/smoketests/sdk/test_async_gateway_config.py @@ -0,0 +1,112 @@ +"""Asynchronous SDK smoke tests for Gateway Config operations.""" + +from __future__ import annotations + +import pytest + +from runloop_api_client.sdk import AsyncRunloopSDK +from tests.smoketests.utils import unique_name + +pytestmark = [pytest.mark.smoketest, pytest.mark.asyncio] + +THIRTY_SECOND_TIMEOUT = 30 + + +class TestAsyncGatewayConfigLifecycle: + """Test async gateway config lifecycle operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_gateway_config_full_lifecycle(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test complete gateway config lifecycle: create, get_info, update, delete.""" + # Create + name = unique_name("sdk-async-gateway-config") + gateway_config = await async_sdk_client.gateway_config.create( + name=name, + endpoint="https://api.example.com", + auth_mechanism={"type": "bearer"}, + description="SDK async smoke test gateway config", + ) + + try: + assert gateway_config is not None + assert gateway_config.id is not None + assert len(gateway_config.id) > 0 + + # Get info + info = await gateway_config.get_info() + assert info.id == gateway_config.id + assert info.name == name + assert info.endpoint == "https://api.example.com" + assert info.description == "SDK async smoke test gateway config" + assert info.auth_mechanism is not None + assert info.auth_mechanism.type == "bearer" + + # Update name and description + updated_name = unique_name("sdk-async-gateway-config-updated") + result = await gateway_config.update( + name=updated_name, + description="Updated async description", + ) + assert result.name == updated_name + assert result.description == "Updated async description" + + # Update endpoint + result = await gateway_config.update( + endpoint="https://api.updated-example.com", + ) + assert result.endpoint == "https://api.updated-example.com" + + # Update auth mechanism + result = await gateway_config.update( + auth_mechanism={"type": "header", "key": "x-api-key"}, + ) + assert result.auth_mechanism.type == "header" + assert result.auth_mechanism.key == "x-api-key" + + # Verify all updates persisted + info = await gateway_config.get_info() + assert info.name == updated_name + assert info.description == "Updated async description" + assert info.endpoint == "https://api.updated-example.com" + assert info.auth_mechanism.type == "header" + assert info.auth_mechanism.key == "x-api-key" + finally: + # Delete + result = await gateway_config.delete() + assert result is not None + + +class TestAsyncGatewayConfigListing: + """Test async gateway config listing and retrieval operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_gateway_config_list_and_retrieve(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test listing gateway configs and retrieving by ID.""" + # Create two gateway configs + config1 = await async_sdk_client.gateway_config.create( + name=unique_name("sdk-async-gateway-config-list-1"), + endpoint="https://api.list-test-1.com", + auth_mechanism={"type": "bearer"}, + ) + config2 = await async_sdk_client.gateway_config.create( + name=unique_name("sdk-async-gateway-config-list-2"), + endpoint="https://api.list-test-2.com", + auth_mechanism={"type": "header", "key": "x-api-key"}, + ) + + try: + # List gateway configs + configs = await async_sdk_client.gateway_config.list(limit=100) + assert isinstance(configs, list) + config_ids = [c.id for c in configs] + assert config1.id in config_ids + assert config2.id in config_ids + + # Retrieve by ID + retrieved = async_sdk_client.gateway_config.from_id(config1.id) + assert retrieved.id == config1.id + info = await retrieved.get_info() + assert info.id == config1.id + finally: + await config1.delete() + await config2.delete() diff --git a/tests/smoketests/sdk/test_gateway_config.py b/tests/smoketests/sdk/test_gateway_config.py new file mode 100644 index 000000000..eac45a122 --- /dev/null +++ b/tests/smoketests/sdk/test_gateway_config.py @@ -0,0 +1,112 @@ +"""Synchronous SDK smoke tests for Gateway Config operations.""" + +from __future__ import annotations + +import pytest + +from runloop_api_client.sdk import RunloopSDK +from tests.smoketests.utils import unique_name + +pytestmark = [pytest.mark.smoketest] + +THIRTY_SECOND_TIMEOUT = 30 + + +class TestGatewayConfigLifecycle: + """Test gateway config lifecycle operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_gateway_config_full_lifecycle(self, sdk_client: RunloopSDK) -> None: + """Test complete gateway config lifecycle: create, get_info, update, delete.""" + # Create + name = unique_name("sdk-gateway-config") + gateway_config = sdk_client.gateway_config.create( + name=name, + endpoint="https://api.example.com", + auth_mechanism={"type": "bearer"}, + description="SDK smoke test gateway config", + ) + + try: + assert gateway_config is not None + assert gateway_config.id is not None + assert len(gateway_config.id) > 0 + + # Get info + info = gateway_config.get_info() + assert info.id == gateway_config.id + assert info.name == name + assert info.endpoint == "https://api.example.com" + assert info.description == "SDK smoke test gateway config" + assert info.auth_mechanism is not None + assert info.auth_mechanism.type == "bearer" + + # Update name and description + updated_name = unique_name("sdk-gateway-config-updated") + result = gateway_config.update( + name=updated_name, + description="Updated description", + ) + assert result.name == updated_name + assert result.description == "Updated description" + + # Update endpoint + result = gateway_config.update( + endpoint="https://api.updated-example.com", + ) + assert result.endpoint == "https://api.updated-example.com" + + # Update auth mechanism + result = gateway_config.update( + auth_mechanism={"type": "header", "key": "x-api-key"}, + ) + assert result.auth_mechanism.type == "header" + assert result.auth_mechanism.key == "x-api-key" + + # Verify all updates persisted + info = gateway_config.get_info() + assert info.name == updated_name + assert info.description == "Updated description" + assert info.endpoint == "https://api.updated-example.com" + assert info.auth_mechanism.type == "header" + assert info.auth_mechanism.key == "x-api-key" + finally: + # Delete + result = gateway_config.delete() + assert result is not None + + +class TestGatewayConfigListing: + """Test gateway config listing and retrieval operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_gateway_config_list_and_retrieve(self, sdk_client: RunloopSDK) -> None: + """Test listing gateway configs and retrieving by ID.""" + # Create two gateway configs + config1 = sdk_client.gateway_config.create( + name=unique_name("sdk-gateway-config-list-1"), + endpoint="https://api.list-test-1.com", + auth_mechanism={"type": "bearer"}, + ) + config2 = sdk_client.gateway_config.create( + name=unique_name("sdk-gateway-config-list-2"), + endpoint="https://api.list-test-2.com", + auth_mechanism={"type": "header", "key": "x-api-key"}, + ) + + try: + # List gateway configs + configs = sdk_client.gateway_config.list(limit=100) + assert isinstance(configs, list) + config_ids = [c.id for c in configs] + assert config1.id in config_ids + assert config2.id in config_ids + + # Retrieve by ID + retrieved = sdk_client.gateway_config.from_id(config1.id) + assert retrieved.id == config1.id + info = retrieved.get_info() + assert info.id == config1.id + finally: + config1.delete() + config2.delete()