From a74dcd48df76b5e9cd26395f22a888a8242508d1 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 20 Jan 2026 16:21:22 -0800 Subject: [PATCH 1/7] cp dines --- src/runloop_api_client/sdk/__init__.py | 8 ++ src/runloop_api_client/sdk/_types.py | 15 +++ src/runloop_api_client/sdk/async_.py | 61 ++++++++++ .../sdk/async_network_policy.py | 110 ++++++++++++++++++ src/runloop_api_client/sdk/network_policy.py | 110 ++++++++++++++++++ src/runloop_api_client/sdk/sync.py | 61 ++++++++++ tests/sdk/async_devbox/test_core.py | 2 +- tests/sdk/conftest.py | 28 +++++ tests/sdk/devbox/test_core.py | 4 +- tests/sdk/test_async_network_policy.py | 90 ++++++++++++++ tests/sdk/test_async_ops.py | 88 ++++++++++++++ tests/sdk/test_network_policy.py | 110 ++++++++++++++++++ tests/sdk/test_ops.py | 71 +++++++++++ 13 files changed, 755 insertions(+), 3 deletions(-) create mode 100644 src/runloop_api_client/sdk/async_network_policy.py create mode 100644 src/runloop_api_client/sdk/network_policy.py create mode 100644 tests/sdk/test_async_network_policy.py create mode 100644 tests/sdk/test_network_policy.py diff --git a/src/runloop_api_client/sdk/__init__.py b/src/runloop_api_client/sdk/__init__.py index 610017b79..01b655e6e 100644 --- a/src/runloop_api_client/sdk/__init__.py +++ b/src/runloop_api_client/sdk/__init__.py @@ -15,6 +15,7 @@ BenchmarkOps, BlueprintOps, StorageObjectOps, + NetworkPolicyOps, ) from .agent import Agent from ._types import ScenarioPreview @@ -28,6 +29,7 @@ AsyncBenchmarkOps, AsyncBlueprintOps, AsyncStorageObjectOps, + AsyncNetworkPolicyOps, ) from .devbox import Devbox, NamedShell from .scorer import Scorer @@ -44,6 +46,7 @@ from .async_scenario import AsyncScenario from .async_snapshot import AsyncSnapshot from .storage_object import StorageObject +from .network_policy import NetworkPolicy from .async_benchmark import AsyncBenchmark from .async_blueprint import AsyncBlueprint from .async_execution import AsyncExecution @@ -52,6 +55,7 @@ from .async_scenario_run import AsyncScenarioRun from .async_benchmark_run import AsyncBenchmarkRun from .async_storage_object import AsyncStorageObject +from .async_network_policy import AsyncNetworkPolicy from .async_execution_result import AsyncExecutionResult from .async_scenario_builder import AsyncScenarioBuilder @@ -76,6 +80,8 @@ "AsyncSnapshotOps", "StorageObjectOps", "AsyncStorageObjectOps", + "NetworkPolicyOps", + "AsyncNetworkPolicyOps", # Resource classes "Agent", "AsyncAgent", @@ -104,6 +110,8 @@ "AsyncSnapshot", "StorageObject", "AsyncStorageObject", + "NetworkPolicy", + "AsyncNetworkPolicy", "NamedShell", "AsyncNamedShell", ] diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index 0fd342bb8..2a0a38652 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -23,7 +23,10 @@ DevboxDownloadFileParams, DevboxRemoveTunnelParams, DevboxSnapshotDiskParams, + NetworkPolicyListParams, DevboxReadFileContentsParams, + NetworkPolicyCreateParams, + NetworkPolicyUpdateParams, DevboxWriteFileContentsParams, ) from .._types import Body, Query, Headers, Timeout, NotGiven @@ -236,3 +239,15 @@ class SDKBenchmarkListRunsParams(RunSelfListParams, BaseRequestOptions): class SDKBenchmarkRunListScenarioRunsParams(RunListScenarioRunsParams, BaseRequestOptions): pass + + +class SDKNetworkPolicyCreateParams(NetworkPolicyCreateParams, LongRequestOptions): + pass + + +class SDKNetworkPolicyListParams(NetworkPolicyListParams, BaseRequestOptions): + pass + + +class SDKNetworkPolicyUpdateParams(NetworkPolicyUpdateParams, LongRequestOptions): + pass diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index 6e6e828ff..35fb6f3dc 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -26,6 +26,8 @@ SDKBenchmarkCreateParams, SDKBlueprintCreateParams, SDKDiskSnapshotListParams, + SDKNetworkPolicyListParams, + SDKNetworkPolicyCreateParams, SDKDevboxCreateFromImageParams, ) from .._types import Timeout, NotGiven, not_given @@ -40,6 +42,7 @@ from .async_blueprint import AsyncBlueprint from ..lib.context_loader import TarFilter, build_directory_tar from .async_storage_object import AsyncStorageObject +from .async_network_policy import AsyncNetworkPolicy from .async_scenario_builder import AsyncScenarioBuilder from ..types.object_create_params import ContentType from ..types.shared_params.agent_source import Git, Npm, Pip, Object @@ -867,6 +870,60 @@ async def list(self, **params: Unpack[SDKBenchmarkListParams]) -> list[AsyncBenc return [AsyncBenchmark(self._client, item.id) for item in page.benchmarks] +class AsyncNetworkPolicyOps: + """High-level async manager for creating and managing network policies. + + Accessed via ``runloop.network_policy`` from :class:`AsyncRunloopSDK`, provides + coroutines to create, retrieve, update, delete, and list network policies. + + Example: + >>> runloop = AsyncRunloopSDK() + >>> policy = await runloop.network_policy.create( + ... name="my-policy", + ... allowed_hostnames=["github.com", "*.npmjs.org"], + ... ) + >>> policies = await runloop.network_policy.list() + """ + + def __init__(self, client: AsyncRunloop) -> None: + """Initialize AsyncNetworkPolicyOps. + + :param client: AsyncRunloop client instance + :type client: AsyncRunloop + """ + self._client = client + + async def create(self, **params: Unpack[SDKNetworkPolicyCreateParams]) -> AsyncNetworkPolicy: + """Create a new network policy. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKNetworkPolicyCreateParams` for available parameters + :return: The newly created network policy + :rtype: AsyncNetworkPolicy + """ + response = await self._client.network_policies.create(**params) + return AsyncNetworkPolicy(self._client, response.id) + + def from_id(self, network_policy_id: str) -> AsyncNetworkPolicy: + """Get an AsyncNetworkPolicy instance for an existing network policy ID. + + :param network_policy_id: ID of the network policy + :type network_policy_id: str + :return: AsyncNetworkPolicy instance for the given ID + :rtype: AsyncNetworkPolicy + """ + return AsyncNetworkPolicy(self._client, network_policy_id) + + async def list(self, **params: Unpack[SDKNetworkPolicyListParams]) -> list[AsyncNetworkPolicy]: + """List all network policies, optionally filtered by parameters. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKNetworkPolicyListParams` for available parameters + :return: List of network policies + :rtype: list[AsyncNetworkPolicy] + """ + page = self._client.network_policies.list(**params) + return [AsyncNetworkPolicy(self._client, item.id) async for item in page] + + class AsyncRunloopSDK: """High-level asynchronous entry point for the Runloop SDK. @@ -892,6 +949,8 @@ class AsyncRunloopSDK: :vartype snapshot: AsyncSnapshotOps :ivar storage_object: High-level async interface for storage object management :vartype storage_object: AsyncStorageObjectOps + :ivar network_policy: High-level async interface for network policy management + :vartype network_policy: AsyncNetworkPolicyOps Example: >>> runloop = AsyncRunloopSDK() # Uses RUNLOOP_API_KEY env var @@ -906,6 +965,7 @@ class AsyncRunloopSDK: benchmark: AsyncBenchmarkOps devbox: AsyncDevboxOps blueprint: AsyncBlueprintOps + network_policy: AsyncNetworkPolicyOps scenario: AsyncScenarioOps scorer: AsyncScorerOps snapshot: AsyncSnapshotOps @@ -953,6 +1013,7 @@ def __init__( self.benchmark = AsyncBenchmarkOps(self.api) self.devbox = AsyncDevboxOps(self.api) self.blueprint = AsyncBlueprintOps(self.api) + self.network_policy = AsyncNetworkPolicyOps(self.api) self.scenario = AsyncScenarioOps(self.api) self.scorer = AsyncScorerOps(self.api) self.snapshot = AsyncSnapshotOps(self.api) diff --git a/src/runloop_api_client/sdk/async_network_policy.py b/src/runloop_api_client/sdk/async_network_policy.py new file mode 100644 index 000000000..c4bf06548 --- /dev/null +++ b/src/runloop_api_client/sdk/async_network_policy.py @@ -0,0 +1,110 @@ +"""NetworkPolicy resource class for asynchronous operations.""" + +from __future__ import annotations + +from typing import Optional + +from typing_extensions import Unpack, override + +from .._types import SequenceNotStr +from ._types import BaseRequestOptions, LongRequestOptions +from .._client import AsyncRunloop +from ..types.network_policy_view import NetworkPolicyView + + +class AsyncNetworkPolicy: + """Asynchronous wrapper around a network policy resource.""" + + def __init__( + self, + client: AsyncRunloop, + network_policy_id: str, + ) -> None: + """Initialize the wrapper. + + :param client: Generated AsyncRunloop client + :type client: AsyncRunloop + :param network_policy_id: NetworkPolicy ID returned by the API + :type network_policy_id: str + """ + self._client = client + self._id = network_policy_id + + @override + def __repr__(self) -> str: + return f"" + + @property + def id(self) -> str: + """Return the network policy ID. + + :return: Unique network policy ID + :rtype: str + """ + return self._id + + async def get_info( + self, + **options: Unpack[BaseRequestOptions], + ) -> NetworkPolicyView: + """Retrieve the latest network policy details. + + :param options: Optional request configuration + :return: API response describing the network policy + :rtype: NetworkPolicyView + """ + return await self._client.network_policies.retrieve( + self._id, + **options, + ) + + async def update( + self, + *, + allow_all: Optional[bool] = None, + allow_devbox_to_devbox: Optional[bool] = None, + allowed_hostnames: Optional[SequenceNotStr[str]] = None, + description: Optional[str] = None, + name: Optional[str] = None, + **options: Unpack[LongRequestOptions], + ) -> NetworkPolicyView: + """Update the network policy. + + :param allow_all: If true, all egress traffic is allowed (ALLOW_ALL policy) + :type allow_all: Optional[bool] + :param allow_devbox_to_devbox: If true, allows traffic between devboxes via tunnels + :type allow_devbox_to_devbox: Optional[bool] + :param allowed_hostnames: DNS-based allow list with wildcard support + :type allowed_hostnames: Optional[SequenceNotStr[str]] + :param description: Updated description for the NetworkPolicy + :type description: Optional[str] + :param name: Updated human-readable name for the NetworkPolicy + :type name: Optional[str] + :param options: Optional long-running request configuration + :return: Updated network policy view + :rtype: NetworkPolicyView + """ + return await self._client.network_policies.update( + self._id, + allow_all=allow_all, + allow_devbox_to_devbox=allow_devbox_to_devbox, + allowed_hostnames=allowed_hostnames, + description=description, + name=name, + **options, + ) + + async def delete( + self, + **options: Unpack[LongRequestOptions], + ) -> NetworkPolicyView: + """Delete the network policy. + + :param options: Optional long-running request configuration + :return: API response acknowledging deletion + :rtype: NetworkPolicyView + """ + return await self._client.network_policies.delete( + self._id, + **options, + ) diff --git a/src/runloop_api_client/sdk/network_policy.py b/src/runloop_api_client/sdk/network_policy.py new file mode 100644 index 000000000..c503c0d2e --- /dev/null +++ b/src/runloop_api_client/sdk/network_policy.py @@ -0,0 +1,110 @@ +"""NetworkPolicy resource class for synchronous operations.""" + +from __future__ import annotations + +from typing import Optional + +from typing_extensions import Unpack, override + +from .._types import SequenceNotStr +from ._types import BaseRequestOptions, LongRequestOptions +from .._client import Runloop +from ..types.network_policy_view import NetworkPolicyView + + +class NetworkPolicy: + """Synchronous wrapper around a network policy resource.""" + + def __init__( + self, + client: Runloop, + network_policy_id: str, + ) -> None: + """Initialize the wrapper. + + :param client: Generated Runloop client + :type client: Runloop + :param network_policy_id: NetworkPolicy ID returned by the API + :type network_policy_id: str + """ + self._client = client + self._id = network_policy_id + + @override + def __repr__(self) -> str: + return f"" + + @property + def id(self) -> str: + """Return the network policy ID. + + :return: Unique network policy ID + :rtype: str + """ + return self._id + + def get_info( + self, + **options: Unpack[BaseRequestOptions], + ) -> NetworkPolicyView: + """Retrieve the latest network policy details. + + :param options: Optional request configuration + :return: API response describing the network policy + :rtype: NetworkPolicyView + """ + return self._client.network_policies.retrieve( + self._id, + **options, + ) + + def update( + self, + *, + allow_all: Optional[bool] = None, + allow_devbox_to_devbox: Optional[bool] = None, + allowed_hostnames: Optional[SequenceNotStr[str]] = None, + description: Optional[str] = None, + name: Optional[str] = None, + **options: Unpack[LongRequestOptions], + ) -> NetworkPolicyView: + """Update the network policy. + + :param allow_all: If true, all egress traffic is allowed (ALLOW_ALL policy) + :type allow_all: Optional[bool] + :param allow_devbox_to_devbox: If true, allows traffic between devboxes via tunnels + :type allow_devbox_to_devbox: Optional[bool] + :param allowed_hostnames: DNS-based allow list with wildcard support + :type allowed_hostnames: Optional[SequenceNotStr[str]] + :param description: Updated description for the NetworkPolicy + :type description: Optional[str] + :param name: Updated human-readable name for the NetworkPolicy + :type name: Optional[str] + :param options: Optional long-running request configuration + :return: Updated network policy view + :rtype: NetworkPolicyView + """ + return self._client.network_policies.update( + self._id, + allow_all=allow_all, + allow_devbox_to_devbox=allow_devbox_to_devbox, + allowed_hostnames=allowed_hostnames, + description=description, + name=name, + **options, + ) + + def delete( + self, + **options: Unpack[LongRequestOptions], + ) -> NetworkPolicyView: + """Delete the network policy. + + :param options: Optional long-running request configuration + :return: API response acknowledging deletion + :rtype: NetworkPolicyView + """ + return self._client.network_policies.delete( + self._id, + **options, + ) diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index d83eb5a6e..933ed0c91 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -26,6 +26,8 @@ SDKBenchmarkCreateParams, SDKBlueprintCreateParams, SDKDiskSnapshotListParams, + SDKNetworkPolicyListParams, + SDKNetworkPolicyCreateParams, SDKDevboxCreateFromImageParams, ) from .devbox import Devbox @@ -38,6 +40,7 @@ from .benchmark import Benchmark from .blueprint import Blueprint from .storage_object import StorageObject +from .network_policy import NetworkPolicy from .scenario_builder import ScenarioBuilder from ..lib.context_loader import TarFilter, build_directory_tar from ..types.object_create_params import ContentType @@ -892,6 +895,60 @@ def list(self, **params: Unpack[SDKBenchmarkListParams]) -> list[Benchmark]: return [Benchmark(self._client, item.id) for item in page.benchmarks] +class NetworkPolicyOps: + """High-level manager for creating and managing network policies. + + Accessed via ``runloop.network_policy`` from :class:`RunloopSDK`, provides methods + to create, retrieve, update, delete, and list network policies. + + Example: + >>> runloop = RunloopSDK() + >>> policy = runloop.network_policy.create( + ... name="my-policy", + ... allowed_hostnames=["github.com", "*.npmjs.org"], + ... ) + >>> policies = runloop.network_policy.list() + """ + + def __init__(self, client: Runloop) -> None: + """Initialize NetworkPolicyOps. + + :param client: Runloop client instance + :type client: Runloop + """ + self._client = client + + def create(self, **params: Unpack[SDKNetworkPolicyCreateParams]) -> NetworkPolicy: + """Create a new network policy. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKNetworkPolicyCreateParams` for available parameters + :return: The newly created network policy + :rtype: NetworkPolicy + """ + response = self._client.network_policies.create(**params) + return NetworkPolicy(self._client, response.id) + + def from_id(self, network_policy_id: str) -> NetworkPolicy: + """Get a NetworkPolicy instance for an existing network policy ID. + + :param network_policy_id: ID of the network policy + :type network_policy_id: str + :return: NetworkPolicy instance for the given ID + :rtype: NetworkPolicy + """ + return NetworkPolicy(self._client, network_policy_id) + + def list(self, **params: Unpack[SDKNetworkPolicyListParams]) -> list[NetworkPolicy]: + """List all network policies, optionally filtered by parameters. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKNetworkPolicyListParams` for available parameters + :return: List of network policies + :rtype: list[NetworkPolicy] + """ + page = self._client.network_policies.list(**params) + return [NetworkPolicy(self._client, item.id) for item in page] + + class RunloopSDK: """High-level synchronous entry point for the Runloop SDK. @@ -917,6 +974,8 @@ class RunloopSDK: :vartype snapshot: SnapshotOps :ivar storage_object: High-level interface for storage object management :vartype storage_object: StorageObjectOps + :ivar network_policy: High-level interface for network policy management + :vartype network_policy: NetworkPolicyOps Example: >>> runloop = RunloopSDK() # Uses RUNLOOP_API_KEY env var @@ -931,6 +990,7 @@ class RunloopSDK: benchmark: BenchmarkOps devbox: DevboxOps blueprint: BlueprintOps + network_policy: NetworkPolicyOps scenario: ScenarioOps scorer: ScorerOps snapshot: SnapshotOps @@ -978,6 +1038,7 @@ def __init__( self.benchmark = BenchmarkOps(self.api) self.devbox = DevboxOps(self.api) self.blueprint = BlueprintOps(self.api) + self.network_policy = NetworkPolicyOps(self.api) self.scenario = ScenarioOps(self.api) self.scorer = ScorerOps(self.api) self.snapshot = SnapshotOps(self.api) diff --git a/tests/sdk/async_devbox/test_core.py b/tests/sdk/async_devbox/test_core.py index f9b4fee11..c5102d477 100644 --- a/tests/sdk/async_devbox/test_core.py +++ b/tests/sdk/async_devbox/test_core.py @@ -193,7 +193,7 @@ async def test_resume_async(self, mock_async_client: AsyncMock, devbox_view: Moc """Test resume_async method.""" mock_async_client.devboxes.resume = AsyncMock(return_value=devbox_view) - devbox = AsyncDevbox(mock_async_client, "dev_123") + devbox = AsyncDevbox(mock_async_client, "dbx_123") result = await devbox.resume_async( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, diff --git a/tests/sdk/conftest.py b/tests/sdk/conftest.py index ac36e6837..f4279c2f5 100644 --- a/tests/sdk/conftest.py +++ b/tests/sdk/conftest.py @@ -27,6 +27,7 @@ "scenario_run": "scr_123", "benchmark": "bmd_123", "benchmark_run": "bmr_123", + "network_policy": "npo_123", } # Test URL constants @@ -157,6 +158,27 @@ class MockBenchmarkRunView: score: float | None = None +@dataclass +class MockEgress: + """Mock Egress for testing.""" + + allow_all: bool = False + allow_devbox_to_devbox: bool = False + allowed_hostnames: list[str] = field(default_factory=lambda: ["github.com", "*.npmjs.org"]) + + +@dataclass +class MockNetworkPolicyView: + """Mock NetworkPolicyView for testing.""" + + id: str = TEST_IDS["network_policy"] + name: str = "test-network-policy" + description: str | None = "Test network policy description" + create_time_ms: int = 1234567890000 + update_time_ms: int = 1234567890000 + egress: MockEgress = field(default_factory=MockEgress) + + class AsyncIterableMock: """A simple async iterable mock for testing paginated responses.""" @@ -288,6 +310,12 @@ def benchmark_run_view() -> MockBenchmarkRunView: return MockBenchmarkRunView() +@pytest.fixture +def network_policy_view() -> MockNetworkPolicyView: + """Create a mock NetworkPolicyView.""" + return MockNetworkPolicyView() + + @pytest.fixture def mock_httpx_response() -> Mock: """Create a mock httpx.Response.""" diff --git a/tests/sdk/devbox/test_core.py b/tests/sdk/devbox/test_core.py index b73c87517..c131ca489 100644 --- a/tests/sdk/devbox/test_core.py +++ b/tests/sdk/devbox/test_core.py @@ -195,7 +195,7 @@ def test_resume_async(self, mock_client: Mock, devbox_view: MockDevboxView) -> N mock_client.devboxes.resume.return_value = devbox_view mock_client.devboxes.await_running = Mock() - devbox = Devbox(mock_client, "dev_123") + devbox = Devbox(mock_client, "dbx_123") result = devbox.resume_async( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -206,7 +206,7 @@ def test_resume_async(self, mock_client: Mock, devbox_view: MockDevboxView) -> N assert result == devbox_view mock_client.devboxes.resume.assert_called_once_with( - "dev_123", + "dbx_123", extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, extra_body={"key": "value"}, diff --git a/tests/sdk/test_async_network_policy.py b/tests/sdk/test_async_network_policy.py new file mode 100644 index 000000000..8d392734f --- /dev/null +++ b/tests/sdk/test_async_network_policy.py @@ -0,0 +1,90 @@ +"""Comprehensive tests for async NetworkPolicy class.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest + +from tests.sdk.conftest import MockNetworkPolicyView +from runloop_api_client.sdk import AsyncNetworkPolicy + + +class TestAsyncNetworkPolicy: + """Tests for AsyncNetworkPolicy class.""" + + def test_init(self, mock_async_client: AsyncMock) -> None: + """Test AsyncNetworkPolicy initialization.""" + network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + assert network_policy.id == "npo_123" + + def test_repr(self, mock_async_client: AsyncMock) -> None: + """Test AsyncNetworkPolicy string representation.""" + network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + assert repr(network_policy) == "" + + @pytest.mark.asyncio + async def test_get_info(self, mock_async_client: AsyncMock, network_policy_view: MockNetworkPolicyView) -> None: + """Test get_info method.""" + mock_async_client.network_policies.retrieve = AsyncMock(return_value=network_policy_view) + + network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + result = await network_policy.get_info( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == network_policy_view + mock_async_client.network_policies.retrieve.assert_awaited_once() + + @pytest.mark.asyncio + async def test_update(self, mock_async_client: AsyncMock, network_policy_view: MockNetworkPolicyView) -> None: + """Test update method.""" + mock_async_client.network_policies.update = AsyncMock(return_value=network_policy_view) + + network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + result = await network_policy.update( + name="updated-policy", + description="Updated description", + allowed_hostnames=["api.openai.com"], + allow_all=False, + allow_devbox_to_devbox=True, + extra_headers={"X-Custom": "value"}, + timeout=30.0, + ) + + assert result == network_policy_view + mock_async_client.network_policies.update.assert_awaited_once() + + @pytest.mark.asyncio + async def test_update_partial( + self, mock_async_client: AsyncMock, network_policy_view: MockNetworkPolicyView + ) -> None: + """Test update method with partial fields.""" + mock_async_client.network_policies.update = AsyncMock(return_value=network_policy_view) + + network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + result = await network_policy.update( + name="renamed-policy", + ) + + assert result == network_policy_view + mock_async_client.network_policies.update.assert_awaited_once() + + @pytest.mark.asyncio + async def test_delete(self, mock_async_client: AsyncMock, network_policy_view: MockNetworkPolicyView) -> None: + """Test delete method.""" + mock_async_client.network_policies.delete = AsyncMock(return_value=network_policy_view) + + network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + result = await network_policy.delete( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == network_policy_view + mock_async_client.network_policies.delete.assert_awaited_once() diff --git a/tests/sdk/test_async_ops.py b/tests/sdk/test_async_ops.py index 7e36e938d..8f360ddc6 100644 --- a/tests/sdk/test_async_ops.py +++ b/tests/sdk/test_async_ops.py @@ -19,6 +19,7 @@ MockSnapshotView, MockBenchmarkView, MockBlueprintView, + MockNetworkPolicyView, create_mock_httpx_response, ) from runloop_api_client.sdk import ( @@ -38,7 +39,9 @@ AsyncBenchmarkOps, AsyncBlueprintOps, AsyncStorageObject, + AsyncNetworkPolicy, AsyncStorageObjectOps, + AsyncNetworkPolicyOps, ) from runloop_api_client.lib.polling import PollingConfig @@ -1259,6 +1262,90 @@ async def test_list_with_name_filter(self, mock_async_client: AsyncMock, benchma mock_async_client.benchmarks.list.assert_awaited_once_with(name="test-benchmark", limit=10) +class TestAsyncNetworkPolicyOps: + """Tests for AsyncNetworkPolicyOps class.""" + + @pytest.mark.asyncio + async def test_create(self, mock_async_client: AsyncMock, network_policy_view: MockNetworkPolicyView) -> None: + """Test create method.""" + mock_async_client.network_policies.create = AsyncMock(return_value=network_policy_view) + + ops = AsyncNetworkPolicyOps(mock_async_client) + network_policy = await ops.create( + name="test-network-policy", + allowed_hostnames=["github.com", "*.npmjs.org"], + ) + + assert isinstance(network_policy, AsyncNetworkPolicy) + assert network_policy.id == "npo_123" + mock_async_client.network_policies.create.assert_awaited_once() + + def test_from_id(self, mock_async_client: AsyncMock) -> None: + """Test from_id method.""" + ops = AsyncNetworkPolicyOps(mock_async_client) + network_policy = ops.from_id("npo_123") + + assert isinstance(network_policy, AsyncNetworkPolicy) + assert network_policy.id == "npo_123" + + @pytest.mark.asyncio + async def test_list_empty(self, mock_async_client: AsyncMock) -> None: + """Test list method with empty results.""" + + async def async_iter(): + return + yield # Make this a generator + + mock_async_client.network_policies.list.return_value = async_iter() + + ops = AsyncNetworkPolicyOps(mock_async_client) + network_policies = await ops.list(limit=10) + + assert len(network_policies) == 0 + + @pytest.mark.asyncio + async def test_list_single( + self, mock_async_client: AsyncMock, network_policy_view: MockNetworkPolicyView + ) -> None: + """Test list method with single result.""" + + async def async_iter(): + yield network_policy_view + + mock_async_client.network_policies.list.return_value = async_iter() + + ops = AsyncNetworkPolicyOps(mock_async_client) + network_policies = await ops.list( + limit=10, + starting_after="npo_000", + ) + + assert len(network_policies) == 1 + assert isinstance(network_policies[0], AsyncNetworkPolicy) + assert network_policies[0].id == "npo_123" + + @pytest.mark.asyncio + async def test_list_multiple(self, mock_async_client: AsyncMock) -> None: + """Test list method with multiple results.""" + network_policy_view1 = MockNetworkPolicyView(id="npo_001", name="policy-1") + network_policy_view2 = MockNetworkPolicyView(id="npo_002", name="policy-2") + + async def async_iter(): + yield network_policy_view1 + yield network_policy_view2 + + mock_async_client.network_policies.list.return_value = async_iter() + + ops = AsyncNetworkPolicyOps(mock_async_client) + network_policies = await ops.list(limit=10) + + assert len(network_policies) == 2 + assert isinstance(network_policies[0], AsyncNetworkPolicy) + assert isinstance(network_policies[1], AsyncNetworkPolicy) + assert network_policies[0].id == "npo_001" + assert network_policies[1].id == "npo_002" + + class TestAsyncRunloopSDK: """Tests for AsyncRunloopSDK class.""" @@ -1269,6 +1356,7 @@ def test_init(self) -> None: assert isinstance(runloop.agent, AsyncAgentOps) assert isinstance(runloop.benchmark, AsyncBenchmarkOps) assert isinstance(runloop.devbox, AsyncDevboxOps) + assert isinstance(runloop.network_policy, AsyncNetworkPolicyOps) assert isinstance(runloop.scorer, AsyncScorerOps) assert isinstance(runloop.snapshot, AsyncSnapshotOps) assert isinstance(runloop.blueprint, AsyncBlueprintOps) diff --git a/tests/sdk/test_network_policy.py b/tests/sdk/test_network_policy.py new file mode 100644 index 000000000..d5150f39d --- /dev/null +++ b/tests/sdk/test_network_policy.py @@ -0,0 +1,110 @@ +"""Comprehensive tests for sync NetworkPolicy class.""" + +from __future__ import annotations + +from unittest.mock import Mock + +from tests.sdk.conftest import MockNetworkPolicyView +from runloop_api_client.sdk import NetworkPolicy + + +class TestNetworkPolicy: + """Tests for NetworkPolicy class.""" + + def test_init(self, mock_client: Mock) -> None: + """Test NetworkPolicy initialization.""" + network_policy = NetworkPolicy(mock_client, "npo_123") + assert network_policy.id == "npo_123" + + def test_repr(self, mock_client: Mock) -> None: + """Test NetworkPolicy string representation.""" + network_policy = NetworkPolicy(mock_client, "npo_123") + assert repr(network_policy) == "" + + def test_get_info(self, mock_client: Mock, network_policy_view: MockNetworkPolicyView) -> None: + """Test get_info method.""" + mock_client.network_policies.retrieve.return_value = network_policy_view + + network_policy = NetworkPolicy(mock_client, "npo_123") + result = network_policy.get_info( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == network_policy_view + mock_client.network_policies.retrieve.assert_called_once_with( + "npo_123", + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + def test_update(self, mock_client: Mock, network_policy_view: MockNetworkPolicyView) -> None: + """Test update method.""" + mock_client.network_policies.update.return_value = network_policy_view + + network_policy = NetworkPolicy(mock_client, "npo_123") + result = network_policy.update( + name="updated-policy", + description="Updated description", + allowed_hostnames=["api.openai.com"], + allow_all=False, + allow_devbox_to_devbox=True, + extra_headers={"X-Custom": "value"}, + timeout=30.0, + ) + + assert result == network_policy_view + mock_client.network_policies.update.assert_called_once_with( + "npo_123", + allow_all=False, + allow_devbox_to_devbox=True, + allowed_hostnames=["api.openai.com"], + description="Updated description", + name="updated-policy", + extra_headers={"X-Custom": "value"}, + timeout=30.0, + ) + + def test_update_partial(self, mock_client: Mock, network_policy_view: MockNetworkPolicyView) -> None: + """Test update method with partial fields.""" + mock_client.network_policies.update.return_value = network_policy_view + + network_policy = NetworkPolicy(mock_client, "npo_123") + result = network_policy.update( + name="renamed-policy", + ) + + assert result == network_policy_view + mock_client.network_policies.update.assert_called_once_with( + "npo_123", + allow_all=None, + allow_devbox_to_devbox=None, + allowed_hostnames=None, + description=None, + name="renamed-policy", + ) + + def test_delete(self, mock_client: Mock, network_policy_view: MockNetworkPolicyView) -> None: + """Test delete method.""" + mock_client.network_policies.delete.return_value = network_policy_view + + network_policy = NetworkPolicy(mock_client, "npo_123") + result = network_policy.delete( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == network_policy_view + mock_client.network_policies.delete.assert_called_once_with( + "npo_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 0cce98ab9..4c9d54ee8 100644 --- a/tests/sdk/test_ops.py +++ b/tests/sdk/test_ops.py @@ -19,6 +19,7 @@ MockSnapshotView, MockBenchmarkView, MockBlueprintView, + MockNetworkPolicyView, create_mock_httpx_response, ) from runloop_api_client.sdk import ( @@ -38,7 +39,9 @@ BenchmarkOps, BlueprintOps, StorageObject, + NetworkPolicy, StorageObjectOps, + NetworkPolicyOps, ) from runloop_api_client.lib.polling import PollingConfig @@ -1152,6 +1155,73 @@ def test_list_with_name_filter(self, mock_client: Mock, benchmark_view: MockBenc mock_client.benchmarks.list.assert_called_once_with(name="test-benchmark", limit=10) +class TestNetworkPolicyOps: + """Tests for NetworkPolicyOps class.""" + + def test_create(self, mock_client: Mock, network_policy_view: MockNetworkPolicyView) -> None: + """Test create method.""" + mock_client.network_policies.create.return_value = network_policy_view + + ops = NetworkPolicyOps(mock_client) + network_policy = ops.create( + name="test-network-policy", + allowed_hostnames=["github.com", "*.npmjs.org"], + ) + + assert isinstance(network_policy, NetworkPolicy) + assert network_policy.id == "npo_123" + mock_client.network_policies.create.assert_called_once() + + def test_from_id(self, mock_client: Mock) -> None: + """Test from_id method.""" + ops = NetworkPolicyOps(mock_client) + network_policy = ops.from_id("npo_123") + + assert isinstance(network_policy, NetworkPolicy) + assert network_policy.id == "npo_123" + + def test_list_empty(self, mock_client: Mock) -> None: + """Test list method with empty results.""" + mock_client.network_policies.list.return_value = [] + + ops = NetworkPolicyOps(mock_client) + network_policies = ops.list(limit=10) + + assert len(network_policies) == 0 + mock_client.network_policies.list.assert_called_once() + + def test_list_single(self, mock_client: Mock, network_policy_view: MockNetworkPolicyView) -> None: + """Test list method with single result.""" + mock_client.network_policies.list.return_value = [network_policy_view] + + ops = NetworkPolicyOps(mock_client) + network_policies = ops.list( + limit=10, + starting_after="npo_000", + ) + + assert len(network_policies) == 1 + assert isinstance(network_policies[0], NetworkPolicy) + assert network_policies[0].id == "npo_123" + mock_client.network_policies.list.assert_called_once() + + def test_list_multiple(self, mock_client: Mock) -> None: + """Test list method with multiple results.""" + network_policy_view1 = MockNetworkPolicyView(id="npo_001", name="policy-1") + network_policy_view2 = MockNetworkPolicyView(id="npo_002", name="policy-2") + mock_client.network_policies.list.return_value = [network_policy_view1, network_policy_view2] + + ops = NetworkPolicyOps(mock_client) + network_policies = ops.list(limit=10) + + assert len(network_policies) == 2 + assert isinstance(network_policies[0], NetworkPolicy) + assert isinstance(network_policies[1], NetworkPolicy) + assert network_policies[0].id == "npo_001" + assert network_policies[1].id == "npo_002" + mock_client.network_policies.list.assert_called_once() + + class TestRunloopSDK: """Tests for RunloopSDK class.""" @@ -1162,6 +1232,7 @@ def test_init(self) -> None: assert isinstance(runloop.agent, AgentOps) assert isinstance(runloop.benchmark, BenchmarkOps) assert isinstance(runloop.devbox, DevboxOps) + assert isinstance(runloop.network_policy, NetworkPolicyOps) assert isinstance(runloop.scorer, ScorerOps) assert isinstance(runloop.snapshot, SnapshotOps) assert isinstance(runloop.blueprint, BlueprintOps) From 284aa48d4f208cccaf1fd8bfd80040132243a2d3 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 20 Jan 2026 16:25:39 -0800 Subject: [PATCH 2/7] cp dines --- src/runloop_api_client/sdk/__init__.py | 8 ++++---- src/runloop_api_client/sdk/_types.py | 4 ++-- src/runloop_api_client/sdk/async_.py | 2 +- src/runloop_api_client/sdk/async_network_policy.py | 3 +-- src/runloop_api_client/sdk/benchmark.py | 4 +--- src/runloop_api_client/sdk/network_policy.py | 3 +-- src/runloop_api_client/sdk/sync.py | 2 +- tests/sdk/test_async_ops.py | 8 +++----- tests/sdk/test_ops.py | 4 ++-- 9 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/runloop_api_client/sdk/__init__.py b/src/runloop_api_client/sdk/__init__.py index 01b655e6e..15f18f8b2 100644 --- a/src/runloop_api_client/sdk/__init__.py +++ b/src/runloop_api_client/sdk/__init__.py @@ -14,8 +14,8 @@ SnapshotOps, BenchmarkOps, BlueprintOps, - StorageObjectOps, NetworkPolicyOps, + StorageObjectOps, ) from .agent import Agent from ._types import ScenarioPreview @@ -28,8 +28,8 @@ AsyncSnapshotOps, AsyncBenchmarkOps, AsyncBlueprintOps, - AsyncStorageObjectOps, AsyncNetworkPolicyOps, + AsyncStorageObjectOps, ) from .devbox import Devbox, NamedShell from .scorer import Scorer @@ -45,8 +45,8 @@ from .benchmark_run import BenchmarkRun from .async_scenario import AsyncScenario from .async_snapshot import AsyncSnapshot -from .storage_object import StorageObject from .network_policy import NetworkPolicy +from .storage_object import StorageObject from .async_benchmark import AsyncBenchmark from .async_blueprint import AsyncBlueprint from .async_execution import AsyncExecution @@ -54,8 +54,8 @@ from .scenario_builder import ScenarioBuilder from .async_scenario_run import AsyncScenarioRun from .async_benchmark_run import AsyncBenchmarkRun -from .async_storage_object import AsyncStorageObject from .async_network_policy import AsyncNetworkPolicy +from .async_storage_object import AsyncStorageObject from .async_execution_result import AsyncExecutionResult from .async_scenario_builder import AsyncScenarioBuilder diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index 2a0a38652..1b5f984ac 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -19,14 +19,14 @@ BenchmarkUpdateParams, BlueprintCreateParams, DevboxUploadFileParams, + NetworkPolicyListParams, DevboxCreateTunnelParams, DevboxDownloadFileParams, DevboxRemoveTunnelParams, DevboxSnapshotDiskParams, - NetworkPolicyListParams, - DevboxReadFileContentsParams, NetworkPolicyCreateParams, NetworkPolicyUpdateParams, + DevboxReadFileContentsParams, DevboxWriteFileContentsParams, ) from .._types import Body, Query, Headers, Timeout, NotGiven diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index 35fb6f3dc..4e509a4af 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -41,8 +41,8 @@ from .async_benchmark import AsyncBenchmark from .async_blueprint import AsyncBlueprint from ..lib.context_loader import TarFilter, build_directory_tar -from .async_storage_object import AsyncStorageObject from .async_network_policy import AsyncNetworkPolicy +from .async_storage_object import AsyncStorageObject from .async_scenario_builder import AsyncScenarioBuilder from ..types.object_create_params import ContentType from ..types.shared_params.agent_source import Git, Npm, Pip, Object diff --git a/src/runloop_api_client/sdk/async_network_policy.py b/src/runloop_api_client/sdk/async_network_policy.py index c4bf06548..d53f1ca5e 100644 --- a/src/runloop_api_client/sdk/async_network_policy.py +++ b/src/runloop_api_client/sdk/async_network_policy.py @@ -3,11 +3,10 @@ from __future__ import annotations from typing import Optional - from typing_extensions import Unpack, override -from .._types import SequenceNotStr from ._types import BaseRequestOptions, LongRequestOptions +from .._types import SequenceNotStr from .._client import AsyncRunloop from ..types.network_policy_view import NetworkPolicyView diff --git a/src/runloop_api_client/sdk/benchmark.py b/src/runloop_api_client/sdk/benchmark.py index 6473ccb9e..aee49e543 100644 --- a/src/runloop_api_client/sdk/benchmark.py +++ b/src/runloop_api_client/sdk/benchmark.py @@ -163,7 +163,5 @@ def list_runs( **params, ) return [ - BenchmarkRun(self._client, run.id, run.benchmark_id) - for run in page.runs - if run.benchmark_id is not None + BenchmarkRun(self._client, run.id, run.benchmark_id) for run in page.runs if run.benchmark_id is not None ] diff --git a/src/runloop_api_client/sdk/network_policy.py b/src/runloop_api_client/sdk/network_policy.py index c503c0d2e..6a7d7f1ee 100644 --- a/src/runloop_api_client/sdk/network_policy.py +++ b/src/runloop_api_client/sdk/network_policy.py @@ -3,11 +3,10 @@ from __future__ import annotations from typing import Optional - from typing_extensions import Unpack, override -from .._types import SequenceNotStr from ._types import BaseRequestOptions, LongRequestOptions +from .._types import SequenceNotStr from .._client import Runloop from ..types.network_policy_view import NetworkPolicyView diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index 933ed0c91..a5127e309 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -39,8 +39,8 @@ from .snapshot import Snapshot from .benchmark import Benchmark from .blueprint import Blueprint -from .storage_object import StorageObject from .network_policy import NetworkPolicy +from .storage_object import StorageObject from .scenario_builder import ScenarioBuilder from ..lib.context_loader import TarFilter, build_directory_tar from ..types.object_create_params import ContentType diff --git a/tests/sdk/test_async_ops.py b/tests/sdk/test_async_ops.py index 8f360ddc6..b83a8b9c4 100644 --- a/tests/sdk/test_async_ops.py +++ b/tests/sdk/test_async_ops.py @@ -38,10 +38,10 @@ AsyncSnapshotOps, AsyncBenchmarkOps, AsyncBlueprintOps, - AsyncStorageObject, AsyncNetworkPolicy, - AsyncStorageObjectOps, + AsyncStorageObject, AsyncNetworkPolicyOps, + AsyncStorageObjectOps, ) from runloop_api_client.lib.polling import PollingConfig @@ -1304,9 +1304,7 @@ async def async_iter(): assert len(network_policies) == 0 @pytest.mark.asyncio - async def test_list_single( - self, mock_async_client: AsyncMock, network_policy_view: MockNetworkPolicyView - ) -> None: + async def test_list_single(self, mock_async_client: AsyncMock, network_policy_view: MockNetworkPolicyView) -> None: """Test list method with single result.""" async def async_iter(): diff --git a/tests/sdk/test_ops.py b/tests/sdk/test_ops.py index 4c9d54ee8..f21de2b59 100644 --- a/tests/sdk/test_ops.py +++ b/tests/sdk/test_ops.py @@ -38,10 +38,10 @@ SnapshotOps, BenchmarkOps, BlueprintOps, - StorageObject, NetworkPolicy, - StorageObjectOps, + StorageObject, NetworkPolicyOps, + StorageObjectOps, ) from runloop_api_client.lib.polling import PollingConfig From dc3a24465ca5377529a717b13ee9bdadc7a07df3 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 20 Jan 2026 16:34:14 -0800 Subject: [PATCH 3/7] cp dines --- tests/sdk/async_devbox/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sdk/async_devbox/test_core.py b/tests/sdk/async_devbox/test_core.py index c5102d477..263e2bd3c 100644 --- a/tests/sdk/async_devbox/test_core.py +++ b/tests/sdk/async_devbox/test_core.py @@ -204,7 +204,7 @@ async def test_resume_async(self, mock_async_client: AsyncMock, devbox_view: Moc assert result == devbox_view mock_async_client.devboxes.resume.assert_called_once_with( - "dev_123", + "dbx_123", extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, extra_body={"key": "value"}, From 46d7c503f11a0e3b2d73772742f49e8369fe48b3 Mon Sep 17 00:00:00 2001 From: Claude Code Review Bot Date: Wed, 21 Jan 2026 00:45:37 +0000 Subject: [PATCH 4/7] fix: address PR review feedback (conventions, KISS, duplication) Co-Authored-By: Claude Sonnet 4.5 --- src/runloop_api_client/sdk/async_benchmark.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/runloop_api_client/sdk/async_benchmark.py b/src/runloop_api_client/sdk/async_benchmark.py index 0a5b3a371..01a35fb6d 100644 --- a/src/runloop_api_client/sdk/async_benchmark.py +++ b/src/runloop_api_client/sdk/async_benchmark.py @@ -163,7 +163,5 @@ async def list_runs( **params, ) return [ - AsyncBenchmarkRun(self._client, run.id, run.benchmark_id) - for run in page.runs - if run.benchmark_id is not None + AsyncBenchmarkRun(self._client, run.id, run.benchmark_id) for run in page.runs if run.benchmark_id is not None ] From 90e389d99dd0996c9557b5dca8eb037b8600818c Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.5" Date: Wed, 21 Jan 2026 01:09:15 +0000 Subject: [PATCH 5/7] fix: format async_benchmark.py to pass ruff checks Co-Authored-By: Claude Sonnet 4.5 --- src/runloop_api_client/sdk/async_benchmark.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/runloop_api_client/sdk/async_benchmark.py b/src/runloop_api_client/sdk/async_benchmark.py index 01a35fb6d..0a5b3a371 100644 --- a/src/runloop_api_client/sdk/async_benchmark.py +++ b/src/runloop_api_client/sdk/async_benchmark.py @@ -163,5 +163,7 @@ async def list_runs( **params, ) return [ - AsyncBenchmarkRun(self._client, run.id, run.benchmark_id) for run in page.runs if run.benchmark_id is not None + AsyncBenchmarkRun(self._client, run.id, run.benchmark_id) + for run in page.runs + if run.benchmark_id is not None ] From e618e48445ad926a7e071e392e93fa5bd48a1700 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.5" Date: Wed, 21 Jan 2026 01:17:23 +0000 Subject: [PATCH 6/7] fix: address PR review feedback - use SDKNetworkPolicyUpdateParams and rename test IDs - Use SDKNetworkPolicyUpdateParams in update methods for both sync and async NetworkPolicy classes - Rename network policy test IDs from npo_123 to np_123 across all test files - Update test expectations to match new parameter unpacking behavior Co-Authored-By: Claude Sonnet 4.5 --- .../sdk/async_network_policy.py | 37 ++----------------- src/runloop_api_client/sdk/network_policy.py | 37 ++----------------- tests/sdk/conftest.py | 2 +- tests/sdk/test_async_network_policy.py | 16 ++++---- tests/sdk/test_async_ops.py | 18 ++++----- tests/sdk/test_network_policy.py | 28 ++++++-------- tests/sdk/test_ops.py | 18 ++++----- 7 files changed, 47 insertions(+), 109 deletions(-) diff --git a/src/runloop_api_client/sdk/async_network_policy.py b/src/runloop_api_client/sdk/async_network_policy.py index d53f1ca5e..b9fc4ff38 100644 --- a/src/runloop_api_client/sdk/async_network_policy.py +++ b/src/runloop_api_client/sdk/async_network_policy.py @@ -2,11 +2,9 @@ from __future__ import annotations -from typing import Optional from typing_extensions import Unpack, override -from ._types import BaseRequestOptions, LongRequestOptions -from .._types import SequenceNotStr +from ._types import BaseRequestOptions, LongRequestOptions, SDKNetworkPolicyUpdateParams from .._client import AsyncRunloop from ..types.network_policy_view import NetworkPolicyView @@ -57,41 +55,14 @@ async def get_info( **options, ) - async def update( - self, - *, - allow_all: Optional[bool] = None, - allow_devbox_to_devbox: Optional[bool] = None, - allowed_hostnames: Optional[SequenceNotStr[str]] = None, - description: Optional[str] = None, - name: Optional[str] = None, - **options: Unpack[LongRequestOptions], - ) -> NetworkPolicyView: + async def update(self, **params: Unpack[SDKNetworkPolicyUpdateParams]) -> NetworkPolicyView: """Update the network policy. - :param allow_all: If true, all egress traffic is allowed (ALLOW_ALL policy) - :type allow_all: Optional[bool] - :param allow_devbox_to_devbox: If true, allows traffic between devboxes via tunnels - :type allow_devbox_to_devbox: Optional[bool] - :param allowed_hostnames: DNS-based allow list with wildcard support - :type allowed_hostnames: Optional[SequenceNotStr[str]] - :param description: Updated description for the NetworkPolicy - :type description: Optional[str] - :param name: Updated human-readable name for the NetworkPolicy - :type name: Optional[str] - :param options: Optional long-running request configuration + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKNetworkPolicyUpdateParams` for available parameters :return: Updated network policy view :rtype: NetworkPolicyView """ - return await self._client.network_policies.update( - self._id, - allow_all=allow_all, - allow_devbox_to_devbox=allow_devbox_to_devbox, - allowed_hostnames=allowed_hostnames, - description=description, - name=name, - **options, - ) + return await self._client.network_policies.update(self._id, **params) async def delete( self, diff --git a/src/runloop_api_client/sdk/network_policy.py b/src/runloop_api_client/sdk/network_policy.py index 6a7d7f1ee..d3e6a6376 100644 --- a/src/runloop_api_client/sdk/network_policy.py +++ b/src/runloop_api_client/sdk/network_policy.py @@ -2,11 +2,9 @@ from __future__ import annotations -from typing import Optional from typing_extensions import Unpack, override -from ._types import BaseRequestOptions, LongRequestOptions -from .._types import SequenceNotStr +from ._types import BaseRequestOptions, LongRequestOptions, SDKNetworkPolicyUpdateParams from .._client import Runloop from ..types.network_policy_view import NetworkPolicyView @@ -57,41 +55,14 @@ def get_info( **options, ) - def update( - self, - *, - allow_all: Optional[bool] = None, - allow_devbox_to_devbox: Optional[bool] = None, - allowed_hostnames: Optional[SequenceNotStr[str]] = None, - description: Optional[str] = None, - name: Optional[str] = None, - **options: Unpack[LongRequestOptions], - ) -> NetworkPolicyView: + def update(self, **params: Unpack[SDKNetworkPolicyUpdateParams]) -> NetworkPolicyView: """Update the network policy. - :param allow_all: If true, all egress traffic is allowed (ALLOW_ALL policy) - :type allow_all: Optional[bool] - :param allow_devbox_to_devbox: If true, allows traffic between devboxes via tunnels - :type allow_devbox_to_devbox: Optional[bool] - :param allowed_hostnames: DNS-based allow list with wildcard support - :type allowed_hostnames: Optional[SequenceNotStr[str]] - :param description: Updated description for the NetworkPolicy - :type description: Optional[str] - :param name: Updated human-readable name for the NetworkPolicy - :type name: Optional[str] - :param options: Optional long-running request configuration + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKNetworkPolicyUpdateParams` for available parameters :return: Updated network policy view :rtype: NetworkPolicyView """ - return self._client.network_policies.update( - self._id, - allow_all=allow_all, - allow_devbox_to_devbox=allow_devbox_to_devbox, - allowed_hostnames=allowed_hostnames, - description=description, - name=name, - **options, - ) + return self._client.network_policies.update(self._id, **params) def delete( self, diff --git a/tests/sdk/conftest.py b/tests/sdk/conftest.py index f4279c2f5..29085ed45 100644 --- a/tests/sdk/conftest.py +++ b/tests/sdk/conftest.py @@ -27,7 +27,7 @@ "scenario_run": "scr_123", "benchmark": "bmd_123", "benchmark_run": "bmr_123", - "network_policy": "npo_123", + "network_policy": "np_123", } # Test URL constants diff --git a/tests/sdk/test_async_network_policy.py b/tests/sdk/test_async_network_policy.py index 8d392734f..bc6d804b5 100644 --- a/tests/sdk/test_async_network_policy.py +++ b/tests/sdk/test_async_network_policy.py @@ -15,20 +15,20 @@ class TestAsyncNetworkPolicy: def test_init(self, mock_async_client: AsyncMock) -> None: """Test AsyncNetworkPolicy initialization.""" - network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") - assert network_policy.id == "npo_123" + network_policy = AsyncNetworkPolicy(mock_async_client, "np_123") + assert network_policy.id == "np_123" def test_repr(self, mock_async_client: AsyncMock) -> None: """Test AsyncNetworkPolicy string representation.""" - network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") - assert repr(network_policy) == "" + network_policy = AsyncNetworkPolicy(mock_async_client, "np_123") + assert repr(network_policy) == "" @pytest.mark.asyncio async def test_get_info(self, mock_async_client: AsyncMock, network_policy_view: MockNetworkPolicyView) -> None: """Test get_info method.""" mock_async_client.network_policies.retrieve = AsyncMock(return_value=network_policy_view) - network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + network_policy = AsyncNetworkPolicy(mock_async_client, "np_123") result = await network_policy.get_info( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -44,7 +44,7 @@ async def test_update(self, mock_async_client: AsyncMock, network_policy_view: M """Test update method.""" mock_async_client.network_policies.update = AsyncMock(return_value=network_policy_view) - network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + network_policy = AsyncNetworkPolicy(mock_async_client, "np_123") result = await network_policy.update( name="updated-policy", description="Updated description", @@ -65,7 +65,7 @@ async def test_update_partial( """Test update method with partial fields.""" mock_async_client.network_policies.update = AsyncMock(return_value=network_policy_view) - network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + network_policy = AsyncNetworkPolicy(mock_async_client, "np_123") result = await network_policy.update( name="renamed-policy", ) @@ -78,7 +78,7 @@ async def test_delete(self, mock_async_client: AsyncMock, network_policy_view: M """Test delete method.""" mock_async_client.network_policies.delete = AsyncMock(return_value=network_policy_view) - network_policy = AsyncNetworkPolicy(mock_async_client, "npo_123") + network_policy = AsyncNetworkPolicy(mock_async_client, "np_123") result = await network_policy.delete( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, diff --git a/tests/sdk/test_async_ops.py b/tests/sdk/test_async_ops.py index b83a8b9c4..4fefaf444 100644 --- a/tests/sdk/test_async_ops.py +++ b/tests/sdk/test_async_ops.py @@ -1277,16 +1277,16 @@ async def test_create(self, mock_async_client: AsyncMock, network_policy_view: M ) assert isinstance(network_policy, AsyncNetworkPolicy) - assert network_policy.id == "npo_123" + assert network_policy.id == "np_123" mock_async_client.network_policies.create.assert_awaited_once() def test_from_id(self, mock_async_client: AsyncMock) -> None: """Test from_id method.""" ops = AsyncNetworkPolicyOps(mock_async_client) - network_policy = ops.from_id("npo_123") + network_policy = ops.from_id("np_123") assert isinstance(network_policy, AsyncNetworkPolicy) - assert network_policy.id == "npo_123" + assert network_policy.id == "np_123" @pytest.mark.asyncio async def test_list_empty(self, mock_async_client: AsyncMock) -> None: @@ -1315,18 +1315,18 @@ async def async_iter(): ops = AsyncNetworkPolicyOps(mock_async_client) network_policies = await ops.list( limit=10, - starting_after="npo_000", + starting_after="np_000", ) assert len(network_policies) == 1 assert isinstance(network_policies[0], AsyncNetworkPolicy) - assert network_policies[0].id == "npo_123" + assert network_policies[0].id == "np_123" @pytest.mark.asyncio async def test_list_multiple(self, mock_async_client: AsyncMock) -> None: """Test list method with multiple results.""" - network_policy_view1 = MockNetworkPolicyView(id="npo_001", name="policy-1") - network_policy_view2 = MockNetworkPolicyView(id="npo_002", name="policy-2") + network_policy_view1 = MockNetworkPolicyView(id="np_001", name="policy-1") + network_policy_view2 = MockNetworkPolicyView(id="np_002", name="policy-2") async def async_iter(): yield network_policy_view1 @@ -1340,8 +1340,8 @@ async def async_iter(): assert len(network_policies) == 2 assert isinstance(network_policies[0], AsyncNetworkPolicy) assert isinstance(network_policies[1], AsyncNetworkPolicy) - assert network_policies[0].id == "npo_001" - assert network_policies[1].id == "npo_002" + assert network_policies[0].id == "np_001" + assert network_policies[1].id == "np_002" class TestAsyncRunloopSDK: diff --git a/tests/sdk/test_network_policy.py b/tests/sdk/test_network_policy.py index d5150f39d..b5f41c01c 100644 --- a/tests/sdk/test_network_policy.py +++ b/tests/sdk/test_network_policy.py @@ -13,19 +13,19 @@ class TestNetworkPolicy: def test_init(self, mock_client: Mock) -> None: """Test NetworkPolicy initialization.""" - network_policy = NetworkPolicy(mock_client, "npo_123") - assert network_policy.id == "npo_123" + network_policy = NetworkPolicy(mock_client, "np_123") + assert network_policy.id == "np_123" def test_repr(self, mock_client: Mock) -> None: """Test NetworkPolicy string representation.""" - network_policy = NetworkPolicy(mock_client, "npo_123") - assert repr(network_policy) == "" + network_policy = NetworkPolicy(mock_client, "np_123") + assert repr(network_policy) == "" def test_get_info(self, mock_client: Mock, network_policy_view: MockNetworkPolicyView) -> None: """Test get_info method.""" mock_client.network_policies.retrieve.return_value = network_policy_view - network_policy = NetworkPolicy(mock_client, "npo_123") + network_policy = NetworkPolicy(mock_client, "np_123") result = network_policy.get_info( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -35,7 +35,7 @@ def test_get_info(self, mock_client: Mock, network_policy_view: MockNetworkPolic assert result == network_policy_view mock_client.network_policies.retrieve.assert_called_once_with( - "npo_123", + "np_123", extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, extra_body={"key": "value"}, @@ -46,7 +46,7 @@ def test_update(self, mock_client: Mock, network_policy_view: MockNetworkPolicyV """Test update method.""" mock_client.network_policies.update.return_value = network_policy_view - network_policy = NetworkPolicy(mock_client, "npo_123") + network_policy = NetworkPolicy(mock_client, "np_123") result = network_policy.update( name="updated-policy", description="Updated description", @@ -59,7 +59,7 @@ def test_update(self, mock_client: Mock, network_policy_view: MockNetworkPolicyV assert result == network_policy_view mock_client.network_policies.update.assert_called_once_with( - "npo_123", + "np_123", allow_all=False, allow_devbox_to_devbox=True, allowed_hostnames=["api.openai.com"], @@ -73,18 +73,14 @@ def test_update_partial(self, mock_client: Mock, network_policy_view: MockNetwor """Test update method with partial fields.""" mock_client.network_policies.update.return_value = network_policy_view - network_policy = NetworkPolicy(mock_client, "npo_123") + network_policy = NetworkPolicy(mock_client, "np_123") result = network_policy.update( name="renamed-policy", ) assert result == network_policy_view mock_client.network_policies.update.assert_called_once_with( - "npo_123", - allow_all=None, - allow_devbox_to_devbox=None, - allowed_hostnames=None, - description=None, + "np_123", name="renamed-policy", ) @@ -92,7 +88,7 @@ def test_delete(self, mock_client: Mock, network_policy_view: MockNetworkPolicyV """Test delete method.""" mock_client.network_policies.delete.return_value = network_policy_view - network_policy = NetworkPolicy(mock_client, "npo_123") + network_policy = NetworkPolicy(mock_client, "np_123") result = network_policy.delete( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -102,7 +98,7 @@ def test_delete(self, mock_client: Mock, network_policy_view: MockNetworkPolicyV assert result == network_policy_view mock_client.network_policies.delete.assert_called_once_with( - "npo_123", + "np_123", extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, extra_body={"key": "value"}, diff --git a/tests/sdk/test_ops.py b/tests/sdk/test_ops.py index f21de2b59..a99ed96fe 100644 --- a/tests/sdk/test_ops.py +++ b/tests/sdk/test_ops.py @@ -1169,16 +1169,16 @@ def test_create(self, mock_client: Mock, network_policy_view: MockNetworkPolicyV ) assert isinstance(network_policy, NetworkPolicy) - assert network_policy.id == "npo_123" + assert network_policy.id == "np_123" mock_client.network_policies.create.assert_called_once() def test_from_id(self, mock_client: Mock) -> None: """Test from_id method.""" ops = NetworkPolicyOps(mock_client) - network_policy = ops.from_id("npo_123") + network_policy = ops.from_id("np_123") assert isinstance(network_policy, NetworkPolicy) - assert network_policy.id == "npo_123" + assert network_policy.id == "np_123" def test_list_empty(self, mock_client: Mock) -> None: """Test list method with empty results.""" @@ -1197,18 +1197,18 @@ def test_list_single(self, mock_client: Mock, network_policy_view: MockNetworkPo ops = NetworkPolicyOps(mock_client) network_policies = ops.list( limit=10, - starting_after="npo_000", + starting_after="np_000", ) assert len(network_policies) == 1 assert isinstance(network_policies[0], NetworkPolicy) - assert network_policies[0].id == "npo_123" + assert network_policies[0].id == "np_123" mock_client.network_policies.list.assert_called_once() def test_list_multiple(self, mock_client: Mock) -> None: """Test list method with multiple results.""" - network_policy_view1 = MockNetworkPolicyView(id="npo_001", name="policy-1") - network_policy_view2 = MockNetworkPolicyView(id="npo_002", name="policy-2") + network_policy_view1 = MockNetworkPolicyView(id="np_001", name="policy-1") + network_policy_view2 = MockNetworkPolicyView(id="np_002", name="policy-2") mock_client.network_policies.list.return_value = [network_policy_view1, network_policy_view2] ops = NetworkPolicyOps(mock_client) @@ -1217,8 +1217,8 @@ def test_list_multiple(self, mock_client: Mock) -> None: assert len(network_policies) == 2 assert isinstance(network_policies[0], NetworkPolicy) assert isinstance(network_policies[1], NetworkPolicy) - assert network_policies[0].id == "npo_001" - assert network_policies[1].id == "npo_002" + assert network_policies[0].id == "np_001" + assert network_policies[1].id == "np_002" mock_client.network_policies.list.assert_called_once() From c5ca54a926a0fbd30ab29021f18759ff6ed183ba Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 20 Jan 2026 17:25:57 -0800 Subject: [PATCH 7/7] cp dines --- .../sdk/test_async_network_policy.py | 214 ++++++++++++++++++ tests/smoketests/sdk/test_network_policy.py | 214 ++++++++++++++++++ 2 files changed, 428 insertions(+) create mode 100644 tests/smoketests/sdk/test_async_network_policy.py create mode 100644 tests/smoketests/sdk/test_network_policy.py diff --git a/tests/smoketests/sdk/test_async_network_policy.py b/tests/smoketests/sdk/test_async_network_policy.py new file mode 100644 index 000000000..d8fef917f --- /dev/null +++ b/tests/smoketests/sdk/test_async_network_policy.py @@ -0,0 +1,214 @@ +"""Asynchronous SDK smoke tests for Network Policy 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 TestAsyncNetworkPolicyLifecycle: + """Test basic async network policy lifecycle operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_network_policy_create(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating a network policy.""" + name = unique_name("sdk-async-network-policy") + network_policy = await async_sdk_client.network_policy.create( + name=name, + description="SDK async smoke test network policy", + allowed_hostnames=["github.com", "*.npmjs.org"], + ) + + try: + assert network_policy is not None + assert network_policy.id is not None + assert len(network_policy.id) > 0 + finally: + await network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_network_policy_get_info(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test retrieving network policy information.""" + name = unique_name("sdk-async-network-policy-info") + network_policy = await async_sdk_client.network_policy.create( + name=name, + description="Async test policy for get_info", + allowed_hostnames=["example.com"], + ) + + try: + info = await network_policy.get_info() + + assert info.id == network_policy.id + assert info.name == name + assert info.description == "Async test policy for get_info" + finally: + await network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_network_policy_update(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test updating a network policy.""" + network_policy = await async_sdk_client.network_policy.create( + name=unique_name("sdk-async-network-policy-update"), + description="Original async description", + allowed_hostnames=["example.com"], + ) + + try: + # Update the policy + updated_name = unique_name("sdk-async-network-policy-updated") + result = await network_policy.update( + name=updated_name, + description="Updated async description", + allowed_hostnames=["example.com", "api.example.com"], + ) + + assert result is not None + assert result.name == updated_name + assert result.description == "Updated async description" + + # Verify update persisted + info = await network_policy.get_info() + assert info.name == updated_name + assert info.description == "Updated async description" + finally: + await network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_network_policy_delete(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test deleting a network policy.""" + network_policy = await async_sdk_client.network_policy.create( + name=unique_name("sdk-async-network-policy-delete"), + allowed_hostnames=["example.com"], + ) + + policy_id = network_policy.id + result = await network_policy.delete() + + assert result is not None + # Verify it's deleted + info = await async_sdk_client.api.network_policies.retrieve(policy_id) + assert info.id == policy_id + + +class TestAsyncNetworkPolicyCreationVariations: + """Test different async network policy creation scenarios.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_network_policy_with_allow_all(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating a network policy with allow_all enabled.""" + network_policy = await async_sdk_client.network_policy.create( + name=unique_name("sdk-async-network-policy-allow-all"), + description="Allow all egress traffic", + allow_all=True, + ) + + try: + info = await network_policy.get_info() + assert info.egress is not None + assert info.egress.allow_all is True + finally: + await network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_network_policy_with_devbox_to_devbox(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating a network policy with devbox-to-devbox traffic allowed.""" + network_policy = await async_sdk_client.network_policy.create( + name=unique_name("sdk-async-network-policy-d2d"), + description="Allow devbox to devbox traffic", + allow_devbox_to_devbox=True, + allowed_hostnames=["internal.example.com"], + ) + + try: + info = await network_policy.get_info() + assert info.egress is not None + assert info.egress.allow_devbox_to_devbox is True + finally: + await network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_network_policy_with_wildcard_hostnames(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating a network policy with wildcard hostname patterns.""" + network_policy = await async_sdk_client.network_policy.create( + name=unique_name("sdk-async-network-policy-wildcards"), + allowed_hostnames=[ + "*.github.com", + "*.githubusercontent.com", + "registry.npmjs.org", + "*.pypi.org", + ], + ) + + try: + info = await network_policy.get_info() + assert info.egress is not None + assert info.egress.allowed_hostnames is not None + assert len(info.egress.allowed_hostnames) == 4 + finally: + await network_policy.delete() + + +class TestAsyncNetworkPolicyListing: + """Test async network policy listing and retrieval operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_list_network_policies(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test listing network policies.""" + policies = await async_sdk_client.network_policy.list(limit=10) + + assert isinstance(policies, list) + # List might be empty, that's okay + assert len(policies) >= 0 + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_get_network_policy_by_id(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test retrieving network policy by ID.""" + # Create a policy + created = await async_sdk_client.network_policy.create( + name=unique_name("sdk-async-network-policy-retrieve"), + allowed_hostnames=["example.com"], + ) + + try: + # Retrieve it by ID + retrieved = async_sdk_client.network_policy.from_id(created.id) + assert retrieved.id == created.id + + # Verify it's the same policy + info = await retrieved.get_info() + assert info.id == created.id + finally: + await created.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_list_network_policies_with_limit(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test listing network policies with a limit.""" + # Create two policies + policy1 = await async_sdk_client.network_policy.create( + name=unique_name("sdk-async-network-policy-list-1"), + allowed_hostnames=["example1.com"], + ) + policy2 = await async_sdk_client.network_policy.create( + name=unique_name("sdk-async-network-policy-list-2"), + allowed_hostnames=["example2.com"], + ) + + try: + # List with limit + policies = await async_sdk_client.network_policy.list(limit=100) + + assert isinstance(policies, list) + # Should find our policies + policy_ids = [p.id for p in policies] + assert policy1.id in policy_ids + assert policy2.id in policy_ids + finally: + await policy1.delete() + await policy2.delete() diff --git a/tests/smoketests/sdk/test_network_policy.py b/tests/smoketests/sdk/test_network_policy.py new file mode 100644 index 000000000..9457c338a --- /dev/null +++ b/tests/smoketests/sdk/test_network_policy.py @@ -0,0 +1,214 @@ +"""Synchronous SDK smoke tests for Network Policy 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 TestNetworkPolicyLifecycle: + """Test basic network policy lifecycle operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_network_policy_create(self, sdk_client: RunloopSDK) -> None: + """Test creating a network policy.""" + name = unique_name("sdk-network-policy") + network_policy = sdk_client.network_policy.create( + name=name, + description="SDK smoke test network policy", + allowed_hostnames=["github.com", "*.npmjs.org"], + ) + + try: + assert network_policy is not None + assert network_policy.id is not None + assert len(network_policy.id) > 0 + finally: + network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_network_policy_get_info(self, sdk_client: RunloopSDK) -> None: + """Test retrieving network policy information.""" + name = unique_name("sdk-network-policy-info") + network_policy = sdk_client.network_policy.create( + name=name, + description="Test policy for get_info", + allowed_hostnames=["example.com"], + ) + + try: + info = network_policy.get_info() + + assert info.id == network_policy.id + assert info.name == name + assert info.description == "Test policy for get_info" + finally: + network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_network_policy_update(self, sdk_client: RunloopSDK) -> None: + """Test updating a network policy.""" + network_policy = sdk_client.network_policy.create( + name=unique_name("sdk-network-policy-update"), + description="Original description", + allowed_hostnames=["example.com"], + ) + + try: + # Update the policy + updated_name = unique_name("sdk-network-policy-updated") + result = network_policy.update( + name=updated_name, + description="Updated description", + allowed_hostnames=["example.com", "api.example.com"], + ) + + assert result is not None + assert result.name == updated_name + assert result.description == "Updated description" + + # Verify update persisted + info = network_policy.get_info() + assert info.name == updated_name + assert info.description == "Updated description" + finally: + network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_network_policy_delete(self, sdk_client: RunloopSDK) -> None: + """Test deleting a network policy.""" + network_policy = sdk_client.network_policy.create( + name=unique_name("sdk-network-policy-delete"), + allowed_hostnames=["example.com"], + ) + + policy_id = network_policy.id + result = network_policy.delete() + + assert result is not None + # Verify it's deleted + info = sdk_client.api.network_policies.retrieve(policy_id) + assert info.id == policy_id + + +class TestNetworkPolicyCreationVariations: + """Test different network policy creation scenarios.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_network_policy_with_allow_all(self, sdk_client: RunloopSDK) -> None: + """Test creating a network policy with allow_all enabled.""" + network_policy = sdk_client.network_policy.create( + name=unique_name("sdk-network-policy-allow-all"), + description="Allow all egress traffic", + allow_all=True, + ) + + try: + info = network_policy.get_info() + assert info.egress is not None + assert info.egress.allow_all is True + finally: + network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_network_policy_with_devbox_to_devbox(self, sdk_client: RunloopSDK) -> None: + """Test creating a network policy with devbox-to-devbox traffic allowed.""" + network_policy = sdk_client.network_policy.create( + name=unique_name("sdk-network-policy-d2d"), + description="Allow devbox to devbox traffic", + allow_devbox_to_devbox=True, + allowed_hostnames=["internal.example.com"], + ) + + try: + info = network_policy.get_info() + assert info.egress is not None + assert info.egress.allow_devbox_to_devbox is True + finally: + network_policy.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_network_policy_with_wildcard_hostnames(self, sdk_client: RunloopSDK) -> None: + """Test creating a network policy with wildcard hostname patterns.""" + network_policy = sdk_client.network_policy.create( + name=unique_name("sdk-network-policy-wildcards"), + allowed_hostnames=[ + "*.github.com", + "*.githubusercontent.com", + "registry.npmjs.org", + "*.pypi.org", + ], + ) + + try: + info = network_policy.get_info() + assert info.egress is not None + assert info.egress.allowed_hostnames is not None + assert len(info.egress.allowed_hostnames) == 4 + finally: + network_policy.delete() + + +class TestNetworkPolicyListing: + """Test network policy listing and retrieval operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_list_network_policies(self, sdk_client: RunloopSDK) -> None: + """Test listing network policies.""" + policies = sdk_client.network_policy.list(limit=10) + + assert isinstance(policies, list) + # List might be empty, that's okay + assert len(policies) >= 0 + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_get_network_policy_by_id(self, sdk_client: RunloopSDK) -> None: + """Test retrieving network policy by ID.""" + # Create a policy + created = sdk_client.network_policy.create( + name=unique_name("sdk-network-policy-retrieve"), + allowed_hostnames=["example.com"], + ) + + try: + # Retrieve it by ID + retrieved = sdk_client.network_policy.from_id(created.id) + assert retrieved.id == created.id + + # Verify it's the same policy + info = retrieved.get_info() + assert info.id == created.id + finally: + created.delete() + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_list_network_policies_with_limit(self, sdk_client: RunloopSDK) -> None: + """Test listing network policies with a limit.""" + # Create two policies + policy1 = sdk_client.network_policy.create( + name=unique_name("sdk-network-policy-list-1"), + allowed_hostnames=["example1.com"], + ) + policy2 = sdk_client.network_policy.create( + name=unique_name("sdk-network-policy-list-2"), + allowed_hostnames=["example2.com"], + ) + + try: + # List with limit + policies = sdk_client.network_policy.list(limit=100) + + assert isinstance(policies, list) + # Should find our policies + policy_ids = [p.id for p in policies] + assert policy1.id in policy_ids + assert policy2.id in policy_ids + finally: + policy1.delete() + policy2.delete()