From 7f840f44781e6ba3b69b74a397ac8381521bf996 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 7 Apr 2026 11:09:59 +0100 Subject: [PATCH 1/2] MPT-19903: add /public/v1/integration/extensions/{extensionId}/instances endpoint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>  Conflicts:  mpt_api_client/resources/integration/extensions.py --- .../integration/extension_instances.py | 63 ++++++++ .../resources/integration/extensions.py | 16 ++ .../integration/test_extension_instances.py | 141 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 mpt_api_client/resources/integration/extension_instances.py create mode 100644 tests/unit/resources/integration/test_extension_instances.py 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 f3881b07..5d3c7c2a 100644 --- a/mpt_api_client/resources/integration/extensions.py +++ b/mpt_api_client/resources/integration/extensions.py @@ -17,6 +17,10 @@ AsyncExtensionDocumentsService, ExtensionDocumentsService, ) +from mpt_api_client.resources.integration.extension_instances import ( + AsyncExtensionInstancesService, + ExtensionInstancesService, +) from mpt_api_client.resources.integration.extension_installations import ( AsyncExtensionInstallationsService, ExtensionInstallationsService, @@ -133,6 +137,12 @@ def installations(self, extension_id: str) -> ExtensionInstallationsService: http_client=self.http_client, endpoint_params={"extension_id": extension_id} ) + 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], @@ -183,3 +193,9 @@ def installations(self, extension_id: str) -> AsyncExtensionInstallationsService return AsyncExtensionInstallationsService( http_client=self.http_client, endpoint_params={"extension_id": extension_id} ) + + 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/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 From ed5bcd763956bb4bf0fb7036c773443c8b4e07c8 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 7 Apr 2026 11:17:12 +0100 Subject: [PATCH 2/2] MPT-19903: add e2e tests for /public/v1/integration/extensions/{extensionId}/instances --- e2e_config.test.json | 1 + .../resources/integration/extensions.py | 8 +++--- .../extension_instances/__init__.py | 0 .../extension_instances/conftest.py | 24 +++++++++++++++++ .../test_async_extension_instances.py | 26 +++++++++++++++++++ .../test_sync_extension_instances.py | 24 +++++++++++++++++ 6 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/integration/extension_instances/__init__.py create mode 100644 tests/e2e/integration/extension_instances/conftest.py create mode 100644 tests/e2e/integration/extension_instances/test_async_extension_instances.py create mode 100644 tests/e2e/integration/extension_instances/test_sync_extension_instances.py diff --git a/e2e_config.test.json b/e2e_config.test.json index 250bdeb1..22fb74d4 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -69,6 +69,7 @@ "notifications.message.id": "MSG-0000-6215-1019-0139", "notifications.subscriber.id": "NTS-0829-7123-7123", "integration.extension.id": "EXT-6587-4477", + "integration.extension.instance.id": "INS-6587-4477-5207-8032", "integration.installation.id": "EXI-9262-9354-9841", "integration.term.id": "ETC-6587-4477-0062", "program.certificate.id": "CER-9646-2171-8417", diff --git a/mpt_api_client/resources/integration/extensions.py b/mpt_api_client/resources/integration/extensions.py index 5d3c7c2a..908a2122 100644 --- a/mpt_api_client/resources/integration/extensions.py +++ b/mpt_api_client/resources/integration/extensions.py @@ -17,14 +17,14 @@ AsyncExtensionDocumentsService, ExtensionDocumentsService, ) -from mpt_api_client.resources.integration.extension_instances import ( - AsyncExtensionInstancesService, - ExtensionInstancesService, -) from mpt_api_client.resources.integration.extension_installations import ( AsyncExtensionInstallationsService, ExtensionInstallationsService, ) +from mpt_api_client.resources.integration.extension_instances import ( + AsyncExtensionInstancesService, + ExtensionInstancesService, +) from mpt_api_client.resources.integration.extension_media import ( AsyncExtensionMediaService, ExtensionMediaService, 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..e459feff --- /dev/null +++ b/tests/e2e/integration/extension_instances/conftest.py @@ -0,0 +1,24 @@ +import pytest + + +@pytest.fixture(scope="session") +def instance_id(e2e_config): + return e2e_config["integration.extension.instance.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-{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..4658dbdf --- /dev/null +++ b/tests/e2e/integration/extension_instances/test_async_extension_instances.py @@ -0,0 +1,26 @@ +import pytest + +from tests.e2e.helper import assert_async_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +@pytest.mark.skip(reason="returns 500 error") +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, instance_id): + await assert_async_service_filter_with_iterate( + async_extension_instances_service, instance_id, None + ) # act + + +async def test_get_extension_instance(async_extension_instances_service, instance_id): + result = await async_extension_instances_service.get(instance_id) + + assert result.id == instance_id 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..9137467b --- /dev/null +++ b/tests/e2e/integration/extension_instances/test_sync_extension_instances.py @@ -0,0 +1,24 @@ +import pytest + +from tests.e2e.helper import assert_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +@pytest.mark.skip(reason="returns 500 error") +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, instance_id): + assert_service_filter_with_iterate(extension_instances_service, instance_id, None) # act + + +def test_get_extension_instance(extension_instances_service, instance_id): + result = extension_instances_service.get(instance_id) + + assert result.id == instance_id