diff --git a/src/runloop_api_client/sdk/__init__.py b/src/runloop_api_client/sdk/__init__.py index 610017b79..15f18f8b2 100644 --- a/src/runloop_api_client/sdk/__init__.py +++ b/src/runloop_api_client/sdk/__init__.py @@ -14,6 +14,7 @@ SnapshotOps, BenchmarkOps, BlueprintOps, + NetworkPolicyOps, StorageObjectOps, ) from .agent import Agent @@ -27,6 +28,7 @@ AsyncSnapshotOps, AsyncBenchmarkOps, AsyncBlueprintOps, + AsyncNetworkPolicyOps, AsyncStorageObjectOps, ) from .devbox import Devbox, NamedShell @@ -43,6 +45,7 @@ from .benchmark_run import BenchmarkRun from .async_scenario import AsyncScenario from .async_snapshot import AsyncSnapshot +from .network_policy import NetworkPolicy from .storage_object import StorageObject from .async_benchmark import AsyncBenchmark from .async_blueprint import AsyncBlueprint @@ -51,6 +54,7 @@ from .scenario_builder import ScenarioBuilder from .async_scenario_run import AsyncScenarioRun from .async_benchmark_run import AsyncBenchmarkRun +from .async_network_policy import AsyncNetworkPolicy from .async_storage_object import AsyncStorageObject 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..1b5f984ac 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -19,10 +19,13 @@ BenchmarkUpdateParams, BlueprintCreateParams, DevboxUploadFileParams, + NetworkPolicyListParams, DevboxCreateTunnelParams, DevboxDownloadFileParams, DevboxRemoveTunnelParams, DevboxSnapshotDiskParams, + NetworkPolicyCreateParams, + NetworkPolicyUpdateParams, DevboxReadFileContentsParams, DevboxWriteFileContentsParams, ) @@ -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..4e509a4af 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 @@ -39,6 +41,7 @@ from .async_benchmark import AsyncBenchmark from .async_blueprint import AsyncBlueprint from ..lib.context_loader import TarFilter, build_directory_tar +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 @@ -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..b9fc4ff38 --- /dev/null +++ b/src/runloop_api_client/sdk/async_network_policy.py @@ -0,0 +1,80 @@ +"""NetworkPolicy resource class for asynchronous operations.""" + +from __future__ import annotations + +from typing_extensions import Unpack, override + +from ._types import BaseRequestOptions, LongRequestOptions, SDKNetworkPolicyUpdateParams +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, **params: Unpack[SDKNetworkPolicyUpdateParams]) -> NetworkPolicyView: + """Update the network policy. + + :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, **params) + + 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/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 new file mode 100644 index 000000000..d3e6a6376 --- /dev/null +++ b/src/runloop_api_client/sdk/network_policy.py @@ -0,0 +1,80 @@ +"""NetworkPolicy resource class for synchronous operations.""" + +from __future__ import annotations + +from typing_extensions import Unpack, override + +from ._types import BaseRequestOptions, LongRequestOptions, SDKNetworkPolicyUpdateParams +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, **params: Unpack[SDKNetworkPolicyUpdateParams]) -> NetworkPolicyView: + """Update the network policy. + + :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, **params) + + 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..a5127e309 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 @@ -37,6 +39,7 @@ from .snapshot import Snapshot from .benchmark import Benchmark from .blueprint import Blueprint +from .network_policy import NetworkPolicy from .storage_object import StorageObject from .scenario_builder import ScenarioBuilder from ..lib.context_loader import TarFilter, build_directory_tar @@ -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..263e2bd3c 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"}, @@ -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"}, diff --git a/tests/sdk/conftest.py b/tests/sdk/conftest.py index ac36e6837..29085ed45 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": "np_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..bc6d804b5 --- /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, "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, "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, "np_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, "np_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, "np_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, "np_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..4fefaf444 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 ( @@ -37,7 +38,9 @@ AsyncSnapshotOps, AsyncBenchmarkOps, AsyncBlueprintOps, + AsyncNetworkPolicy, AsyncStorageObject, + AsyncNetworkPolicyOps, AsyncStorageObjectOps, ) from runloop_api_client.lib.polling import PollingConfig @@ -1259,6 +1262,88 @@ 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 == "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("np_123") + + assert isinstance(network_policy, AsyncNetworkPolicy) + assert network_policy.id == "np_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="np_000", + ) + + assert len(network_policies) == 1 + assert isinstance(network_policies[0], AsyncNetworkPolicy) + 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="np_001", name="policy-1") + network_policy_view2 = MockNetworkPolicyView(id="np_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 == "np_001" + assert network_policies[1].id == "np_002" + + class TestAsyncRunloopSDK: """Tests for AsyncRunloopSDK class.""" @@ -1269,6 +1354,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..b5f41c01c --- /dev/null +++ b/tests/sdk/test_network_policy.py @@ -0,0 +1,106 @@ +"""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, "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, "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, "np_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( + "np_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, "np_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( + "np_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, "np_123") + result = network_policy.update( + name="renamed-policy", + ) + + assert result == network_policy_view + mock_client.network_policies.update.assert_called_once_with( + "np_123", + 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, "np_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( + "np_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..a99ed96fe 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 ( @@ -37,7 +38,9 @@ SnapshotOps, BenchmarkOps, BlueprintOps, + NetworkPolicy, StorageObject, + NetworkPolicyOps, StorageObjectOps, ) 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 == "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("np_123") + + assert isinstance(network_policy, NetworkPolicy) + assert network_policy.id == "np_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="np_000", + ) + + assert len(network_policies) == 1 + assert isinstance(network_policies[0], NetworkPolicy) + 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="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) + 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 == "np_001" + assert network_policies[1].id == "np_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) 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()