diff --git a/mpt_api_client/resources/integration/extension_instances.py b/mpt_api_client/resources/integration/extension_instances.py new file mode 100644 index 00000000..ef9c895f --- /dev/null +++ b/mpt_api_client/resources/integration/extension_instances.py @@ -0,0 +1,63 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncCreateMixin, + AsyncGetMixin, + CollectionMixin, + CreateMixin, + GetMixin, +) +from mpt_api_client.models import Model +from mpt_api_client.models.model import BaseModel + + +class ExtensionInstance(Model): + """Extension Instance resource. + + Attributes: + name: Instance name. + revision: Revision number. + extension: Reference to the extension. + meta: Extension metadata reference. + external_id: External identifier for the instance. + status: Instance status (Connecting, Disconnected, Running, Deleted). + channel: Channel configuration. + audit: Audit information (created, updated, connecting, running, disconnected). + """ + + name: str | None + revision: int | None + extension: BaseModel | None + meta: BaseModel | None + external_id: str | None + status: str | None + channel: BaseModel | None + audit: BaseModel | None + + +class ExtensionInstancesServiceConfig: + """Extension Instances service configuration.""" + + _endpoint = "/public/v1/integration/extensions/{extension_id}/instances" + _model_class = ExtensionInstance + _collection_key = "data" + + +class ExtensionInstancesService( + CreateMixin[ExtensionInstance], + GetMixin[ExtensionInstance], + CollectionMixin[ExtensionInstance], + Service[ExtensionInstance], + ExtensionInstancesServiceConfig, +): + """Sync service for /public/v1/integration/extensions/{extensionId}/instances endpoint.""" + + +class AsyncExtensionInstancesService( + AsyncCreateMixin[ExtensionInstance], + AsyncGetMixin[ExtensionInstance], + AsyncCollectionMixin[ExtensionInstance], + AsyncService[ExtensionInstance], + ExtensionInstancesServiceConfig, +): + """Async service for /public/v1/integration/extensions/{extensionId}/instances endpoint.""" diff --git a/mpt_api_client/resources/integration/extensions.py b/mpt_api_client/resources/integration/extensions.py index 4636d717..f35c4f7c 100644 --- a/mpt_api_client/resources/integration/extensions.py +++ b/mpt_api_client/resources/integration/extensions.py @@ -13,6 +13,10 @@ ) from mpt_api_client.models import Model from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.extension_instances import ( + AsyncExtensionInstancesService, + ExtensionInstancesService, +) from mpt_api_client.resources.integration.mixins import ( AsyncExtensionMixin, ExtensionMixin, @@ -79,6 +83,12 @@ class ExtensionsService( ): """Sync service for the /public/v1/integration/extensions endpoint.""" + def instances(self, extension_id: str) -> ExtensionInstancesService: + """Return extension instances service.""" + return ExtensionInstancesService( + http_client=self.http_client, endpoint_params={"extension_id": extension_id} + ) + class AsyncExtensionsService( AsyncExtensionMixin[Extension], @@ -91,3 +101,9 @@ class AsyncExtensionsService( ExtensionsServiceConfig, ): """Async service for the /public/v1/integration/extensions endpoint.""" + + def instances(self, extension_id: str) -> AsyncExtensionInstancesService: + """Return extension instances service.""" + return AsyncExtensionInstancesService( + http_client=self.http_client, endpoint_params={"extension_id": extension_id} + ) diff --git a/tests/e2e/integration/extension_instances/__init__.py b/tests/e2e/integration/extension_instances/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/integration/extension_instances/conftest.py b/tests/e2e/integration/extension_instances/conftest.py new file mode 100644 index 00000000..7356d56f --- /dev/null +++ b/tests/e2e/integration/extension_instances/conftest.py @@ -0,0 +1,24 @@ +import pytest + + +@pytest.fixture(scope="session") +def extension_id(e2e_config): + return e2e_config["integration.extension.id"] + + +@pytest.fixture +def extension_instances_service(mpt_ops, extension_id): + return mpt_ops.integration.extensions.instances(extension_id) + + +@pytest.fixture +def async_extension_instances_service(async_mpt_ops, extension_id): + return async_mpt_ops.integration.extensions.instances(extension_id) + + +@pytest.fixture +def instance_data(short_uuid): + return { + "externalId": f"e2e-instance-{short_uuid}", + "version": "1.0.0", + } diff --git a/tests/e2e/integration/extension_instances/test_async_extension_instances.py b/tests/e2e/integration/extension_instances/test_async_extension_instances.py new file mode 100644 index 00000000..bfc04445 --- /dev/null +++ b/tests/e2e/integration/extension_instances/test_async_extension_instances.py @@ -0,0 +1,20 @@ +import pytest + +from tests.e2e.helper import assert_async_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +@pytest.mark.skip(reason="creates real resources; run manually only") +async def test_create_extension_instance(async_extension_instances_service, instance_data): + result = await async_extension_instances_service.create(instance_data) + + assert result.external_id == instance_data["externalId"] + + +async def test_filter_extension_instances(async_extension_instances_service, extension_id): + await assert_async_service_filter_with_iterate( + async_extension_instances_service, extension_id, None + ) # act diff --git a/tests/e2e/integration/extension_instances/test_sync_extension_instances.py b/tests/e2e/integration/extension_instances/test_sync_extension_instances.py new file mode 100644 index 00000000..dedda256 --- /dev/null +++ b/tests/e2e/integration/extension_instances/test_sync_extension_instances.py @@ -0,0 +1,18 @@ +import pytest + +from tests.e2e.helper import assert_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +@pytest.mark.skip(reason="creates real resources; run manually only") +def test_create_extension_instance(extension_instances_service, instance_data): + result = extension_instances_service.create(instance_data) + + assert result.external_id == instance_data["externalId"] + + +def test_filter_extension_instances(extension_instances_service, extension_id): + assert_service_filter_with_iterate(extension_instances_service, extension_id, None) # act diff --git a/tests/unit/resources/integration/test_extension_instances.py b/tests/unit/resources/integration/test_extension_instances.py new file mode 100644 index 00000000..de358d4f --- /dev/null +++ b/tests/unit/resources/integration/test_extension_instances.py @@ -0,0 +1,141 @@ +import httpx +import pytest +import respx + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.extension_instances import ( + AsyncExtensionInstancesService, + ExtensionInstance, + ExtensionInstancesService, +) +from mpt_api_client.resources.integration.extensions import ( + AsyncExtensionsService, + ExtensionsService, +) + + +@pytest.fixture +def extension_instances_service(http_client): + return ExtensionInstancesService( + http_client=http_client, endpoint_params={"extension_id": "EXT-001"} + ) + + +@pytest.fixture +def async_extension_instances_service(async_http_client): + return AsyncExtensionInstancesService( + http_client=async_http_client, endpoint_params={"extension_id": "EXT-001"} + ) + + +@pytest.fixture +def extensions_service(http_client): + return ExtensionsService(http_client=http_client) + + +@pytest.fixture +def async_extensions_service(async_http_client): + return AsyncExtensionsService(http_client=async_http_client) + + +@pytest.mark.parametrize( + "method", + [ + "get", + "create", + "iterate", + ], +) +def test_mixins_present(extension_instances_service, method): + result = hasattr(extension_instances_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + [ + "get", + "create", + "iterate", + ], +) +def test_async_mixins_present(async_extension_instances_service, method): + result = hasattr(async_extension_instances_service, method) + + assert result is True + + +def test_extension_instance_primitive_fields(): + instance_data = { + "id": "INS-001", + "name": "My Instance", + "revision": 2, + "externalId": "ext-123", + "status": "Running", + "extension": {"id": "EXT-001"}, + "meta": {"id": "META-001"}, + "channel": {"type": "grpc"}, + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + result = ExtensionInstance(instance_data) + + assert result.id == "INS-001" + assert result.name == "My Instance" + assert result.revision == 2 + assert result.external_id == "ext-123" + assert result.status == "Running" + assert isinstance(result.extension, BaseModel) + assert isinstance(result.meta, BaseModel) + assert isinstance(result.channel, BaseModel) + assert isinstance(result.audit, BaseModel) + + +def test_extension_instance_create(extension_instances_service): + payload = {"externalId": "ext-123", "version": "1.0.0", "channel": {"type": "grpc"}} + expected_response = {"id": "INS-001", "name": "My Instance", "status": "Connecting"} + with respx.mock: + mock_route = respx.post( + "https://api.example.com/public/v1/integration/extensions/EXT-001/instances" + ).mock(return_value=httpx.Response(httpx.codes.CREATED, json=expected_response)) + + result = extension_instances_service.create(payload) + + assert mock_route.call_count == 1 + assert mock_route.calls[0].request.method == "POST" + assert result.to_dict() == expected_response + + +def test_extension_instances_list(extension_instances_service): + expected_response = { + "data": [ + {"id": "INS-001", "name": "Instance 1", "status": "Running"}, + {"id": "INS-002", "name": "Instance 2", "status": "Disconnected"}, + ] + } + with respx.mock: + mock_route = respx.get( + "https://api.example.com/public/v1/integration/extensions/EXT-001/instances" + ).mock(return_value=httpx.Response(httpx.codes.OK, json=expected_response)) + + result = list(extension_instances_service.iterate()) + + assert mock_route.call_count == 1 + assert len(result) == 2 + assert result[0].id == "INS-001" + assert result[1].id == "INS-002" + + +def test_extensions_instances_accessor(extensions_service, http_client): + result = extensions_service.instances("EXT-001") + + assert isinstance(result, ExtensionInstancesService) + assert result.http_client is http_client + + +def test_async_extensions_instances_accessor(async_extensions_service, async_http_client): + result = async_extensions_service.instances("EXT-001") + + assert isinstance(result, AsyncExtensionInstancesService) + assert result.http_client is async_http_client