From d0e5d38314ec355cc77a5e7e4910b5c2e4421ce6 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 7 Apr 2026 11:12:48 +0100 Subject: [PATCH 1/4] MPT-19904: add /public/v1/integration/extensions/{extensionId}/documents endpoint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../integration/extension_documents.py | 79 ++++++++++ .../resources/integration/extensions.py | 30 ++++ .../integration/test_extension_documents.py | 147 ++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 mpt_api_client/resources/integration/extension_documents.py create mode 100644 tests/unit/resources/integration/test_extension_documents.py diff --git a/mpt_api_client/resources/integration/extension_documents.py b/mpt_api_client/resources/integration/extension_documents.py new file mode 100644 index 00000000..1445eceb --- /dev/null +++ b/mpt_api_client/resources/integration/extension_documents.py @@ -0,0 +1,79 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncCreateFileMixin, + AsyncModifiableResourceMixin, + CollectionMixin, + CreateFileMixin, + ModifiableResourceMixin, +) +from mpt_api_client.models import Model +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.mixins import ( + AsyncPublishableMixin, + PublishableMixin, +) + + +class ExtensionDocument(Model): + """Extension Document resource. + + Attributes: + name: Document name. + revision: Revision number. + type: Document type (Online or File). + description: Document description. + status: Document status (Draft, Published, Unpublished, Deleted). + filename: Original file name. + size: File size in bytes. + content_type: MIME content type. + url: URL to access the document. + language: Language code. + extension: Reference to the extension. + audit: Audit information (created, updated, published, unpublished). + """ + + name: str | None + revision: int | None + type: str | None + description: str | None + status: str | None + filename: str | None + size: int | None + content_type: str | None + url: str | None + language: str | None + extension: BaseModel | None + audit: BaseModel | None + + +class ExtensionDocumentsServiceConfig: + """Extension Documents service configuration.""" + + _endpoint = "/public/v1/integration/extensions/{extension_id}/documents" + _model_class = ExtensionDocument + _collection_key = "data" + _upload_file_key = "file" + _upload_data_key = "document" + + +class ExtensionDocumentsService( + PublishableMixin[ExtensionDocument], + CreateFileMixin[ExtensionDocument], + ModifiableResourceMixin[ExtensionDocument], + CollectionMixin[ExtensionDocument], + Service[ExtensionDocument], + ExtensionDocumentsServiceConfig, +): + """Sync service for /public/v1/integration/extensions/{extensionId}/documents endpoint.""" + + +class AsyncExtensionDocumentsService( + AsyncPublishableMixin[ExtensionDocument], + AsyncCreateFileMixin[ExtensionDocument], + AsyncModifiableResourceMixin[ExtensionDocument], + AsyncCollectionMixin[ExtensionDocument], + AsyncService[ExtensionDocument], + ExtensionDocumentsServiceConfig, +): + """Async service for /public/v1/integration/extensions/{extensionId}/documents endpoint.""" diff --git a/mpt_api_client/resources/integration/extensions.py b/mpt_api_client/resources/integration/extensions.py index f24e2074..26ab556a 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_documents import ( + AsyncExtensionDocumentsService, + ExtensionDocumentsService, +) from mpt_api_client.resources.integration.extension_media import ( AsyncExtensionMediaService, ExtensionMediaService, @@ -106,6 +110,19 @@ def media(self, extension_id: str) -> ExtensionMediaService: http_client=self.http_client, endpoint_params={"extension_id": extension_id} ) + def documents(self, extension_id: str) -> ExtensionDocumentsService: + """Return extension documents service. + + Args: + extension_id: Extension ID. + + Returns: + ExtensionDocumentsService instance. + """ + return ExtensionDocumentsService( + http_client=self.http_client, endpoint_params={"extension_id": extension_id} + ) + class AsyncExtensionsService( AsyncExtensionMixin[Extension], @@ -137,3 +154,16 @@ def media(self, extension_id: str) -> AsyncExtensionMediaService: return AsyncExtensionMediaService( http_client=self.http_client, endpoint_params={"extension_id": extension_id} ) + + def documents(self, extension_id: str) -> AsyncExtensionDocumentsService: + """Return async extension documents service. + + Args: + extension_id: Extension ID. + + Returns: + AsyncExtensionDocumentsService instance. + """ + return AsyncExtensionDocumentsService( + http_client=self.http_client, endpoint_params={"extension_id": extension_id} + ) diff --git a/tests/unit/resources/integration/test_extension_documents.py b/tests/unit/resources/integration/test_extension_documents.py new file mode 100644 index 00000000..3d2a727c --- /dev/null +++ b/tests/unit/resources/integration/test_extension_documents.py @@ -0,0 +1,147 @@ +import httpx +import pytest +import respx + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.extension_documents import ( + AsyncExtensionDocumentsService, + ExtensionDocument, + ExtensionDocumentsService, +) +from mpt_api_client.resources.integration.extensions import ( + AsyncExtensionsService, + ExtensionsService, +) + + +@pytest.fixture +def extension_documents_service(http_client) -> ExtensionDocumentsService: + return ExtensionDocumentsService( + http_client=http_client, endpoint_params={"extension_id": "EXT-001"} + ) + + +@pytest.fixture +def async_extension_documents_service(async_http_client) -> AsyncExtensionDocumentsService: + return AsyncExtensionDocumentsService( + http_client=async_http_client, endpoint_params={"extension_id": "EXT-001"} + ) + + +@pytest.fixture +def extensions_service(http_client) -> ExtensionsService: + return ExtensionsService(http_client=http_client) + + +@pytest.fixture +def async_extensions_service(async_http_client) -> AsyncExtensionsService: + return AsyncExtensionsService(http_client=async_http_client) + + +@pytest.fixture +def document_data(): + return { + "id": "DOC-001", + "name": "User Guide", + "revision": 1, + "type": "File", + "description": "Extension user guide", + "status": "Draft", + "filename": "guide.pdf", + "size": 4096, + "contentType": "application/pdf", + "url": "https://example.com/guide.pdf", + "language": "en", + "extension": {"id": "EXT-001", "name": "My Extension"}, + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +def test_endpoint(extension_documents_service) -> None: + result = ( + extension_documents_service.path == "/public/v1/integration/extensions/EXT-001/documents" + ) + + assert result is True + + +def test_async_endpoint(async_extension_documents_service) -> None: + result = ( + async_extension_documents_service.path + == "/public/v1/integration/extensions/EXT-001/documents" + ) + + assert result is True + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "publish", "unpublish", "iterate"], +) +def test_mixins_present(extension_documents_service, method: str) -> None: + result = hasattr(extension_documents_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "publish", "unpublish", "iterate"], +) +def test_async_mixins_present(async_extension_documents_service, method: str) -> None: + result = hasattr(async_extension_documents_service, method) + + assert result is True + + +def test_extension_document_primitive_fields(document_data): + result = ExtensionDocument(document_data) + + assert result.to_dict() == document_data + + +def test_extension_document_nested_fields(document_data): + result = ExtensionDocument(document_data) + + assert isinstance(result.extension, BaseModel) + assert isinstance(result.audit, BaseModel) + + +def test_extension_document_optional_absent(): + result = ExtensionDocument({"id": "DOC-001"}) + + assert result.id == "DOC-001" + assert not hasattr(result, "name") + assert not hasattr(result, "status") + assert not hasattr(result, "audit") + + +def test_extension_document_create(extension_documents_service, tmp_path): + document_data = {"Name": "User Guide", "Description": "Guide", "Language": "en"} + expected_response = {"id": "DOC-001", "name": "User Guide"} + file_path = tmp_path / "guide.pdf" + file_path.write_bytes(b"fake pdf data") + with file_path.open("rb") as doc_file, respx.mock: + mock_route = respx.post( + "https://api.example.com/public/v1/integration/extensions/EXT-001/documents" + ).mock(return_value=httpx.Response(httpx.codes.CREATED, json=expected_response)) + + result = extension_documents_service.create(document_data, file=doc_file) + + assert mock_route.call_count == 1 + assert mock_route.calls[0].request.method == "POST" + assert result.to_dict() == expected_response + + +def test_extensions_documents_accessor(extensions_service): + result = extensions_service.documents("EXT-001") + + assert isinstance(result, ExtensionDocumentsService) + assert result.path == "/public/v1/integration/extensions/EXT-001/documents" + + +def test_async_extensions_documents_accessor(async_extensions_service): + result = async_extensions_service.documents("EXT-001") + + assert isinstance(result, AsyncExtensionDocumentsService) + assert result.path == "/public/v1/integration/extensions/EXT-001/documents" From aa8b591046bb80290e841ea9c78f8270c1963e56 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 7 Apr 2026 11:17:01 +0100 Subject: [PATCH 2/4] MPT-19904: add e2e tests for /public/v1/integration/extensions/{extensionId}/documents Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../extension_documents/__init__.py | 0 .../extension_documents/conftest.py | 53 +++++++++++++++++++ .../test_async_extension_documents.py | 53 +++++++++++++++++++ .../test_sync_extension_documents.py | 45 ++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 tests/e2e/integration/extension_documents/__init__.py create mode 100644 tests/e2e/integration/extension_documents/conftest.py create mode 100644 tests/e2e/integration/extension_documents/test_async_extension_documents.py create mode 100644 tests/e2e/integration/extension_documents/test_sync_extension_documents.py diff --git a/tests/e2e/integration/extension_documents/__init__.py b/tests/e2e/integration/extension_documents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/integration/extension_documents/conftest.py b/tests/e2e/integration/extension_documents/conftest.py new file mode 100644 index 00000000..f11496ed --- /dev/null +++ b/tests/e2e/integration/extension_documents/conftest.py @@ -0,0 +1,53 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError + + +@pytest.fixture(scope="session") +def extension_id(e2e_config): + return e2e_config["integration.extension.id"] + + +@pytest.fixture +def extension_documents_service(mpt_vendor, extension_id): + return mpt_vendor.integration.extensions.documents(extension_id) + + +@pytest.fixture +def async_extension_documents_service(async_mpt_vendor, extension_id): + return async_mpt_vendor.integration.extensions.documents(extension_id) + + +@pytest.fixture +def document_data(short_uuid): + return { + "name": f"e2e - please delete {short_uuid}", + "description": "Created by automated E2E tests. Safe to delete.", + "language": "en-US", + "documentType": "Online", + "url": "https://example.com/terms", + } + + +@pytest.fixture +def created_document(extension_documents_service, document_data): + document = extension_documents_service.create(document_data) + + yield document + + try: + extension_documents_service.delete(document.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete document {document.id}: {error.title}") # noqa: WPS421 + + +@pytest.fixture +async def async_created_document(async_extension_documents_service, document_data): + document = await async_extension_documents_service.create(document_data) + + yield document + + try: + await async_extension_documents_service.delete(document.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete document {document.id}: {error.title}") # noqa: WPS421 diff --git a/tests/e2e/integration/extension_documents/test_async_extension_documents.py b/tests/e2e/integration/extension_documents/test_async_extension_documents.py new file mode 100644 index 00000000..5f03d5d3 --- /dev/null +++ b/tests/e2e/integration/extension_documents/test_async_extension_documents.py @@ -0,0 +1,53 @@ +import pytest + +from tests.e2e.helper import assert_async_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +def test_create_extension_document(async_created_document, document_data): + result = async_created_document.name + + assert result == document_data["name"] + + +async def test_filter_extension_documents( + async_extension_documents_service, async_created_document +): + await assert_async_service_filter_with_iterate( + async_extension_documents_service, async_created_document.id, None + ) # act + + +async def test_update_extension_document( + async_extension_documents_service, async_created_document, short_uuid +): + update_data = {"name": f"e2e updated {short_uuid}"} + + result = await async_extension_documents_service.update(async_created_document.id, update_data) + + assert result.name == update_data["name"] + + +async def test_publish_extension_document( + async_extension_documents_service, async_created_document +): + result = await async_extension_documents_service.publish(async_created_document.id) + + assert result.status == "Published" + + +async def test_unpublish_extension_document( + async_extension_documents_service, async_created_document +): + await async_extension_documents_service.publish(async_created_document.id) + + result = await async_extension_documents_service.unpublish(async_created_document.id) + + assert result.status == "Unpublished" + + +async def test_delete_extension_document(async_extension_documents_service, async_created_document): + await async_extension_documents_service.delete(async_created_document.id) # act diff --git a/tests/e2e/integration/extension_documents/test_sync_extension_documents.py b/tests/e2e/integration/extension_documents/test_sync_extension_documents.py new file mode 100644 index 00000000..c8fd421c --- /dev/null +++ b/tests/e2e/integration/extension_documents/test_sync_extension_documents.py @@ -0,0 +1,45 @@ +import pytest + +from tests.e2e.helper import assert_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +def test_create_extension_document(created_document, document_data): + result = created_document.name + + assert result == document_data["name"] + + +def test_filter_extension_documents(extension_documents_service, created_document): + assert_service_filter_with_iterate( + extension_documents_service, created_document.id, None + ) # act + + +def test_update_extension_document(extension_documents_service, created_document, short_uuid): + update_data = {"name": f"e2e updated {short_uuid}"} + + result = extension_documents_service.update(created_document.id, update_data) + + assert result.name == update_data["name"] + + +def test_publish_extension_document(extension_documents_service, created_document): + result = extension_documents_service.publish(created_document.id) + + assert result.status == "Published" + + +def test_unpublish_extension_document(extension_documents_service, created_document): + extension_documents_service.publish(created_document.id) + + result = extension_documents_service.unpublish(created_document.id) + + assert result.status == "Unpublished" + + +def test_delete_extension_document(extension_documents_service, created_document): + extension_documents_service.delete(created_document.id) # act From a16b0acfd252e3f54d55b014a6894dc23d0433b9 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Fri, 10 Apr 2026 09:19:38 +0100 Subject: [PATCH 3/4] MPT-19904: update documentType to File for pdf upload in e2e tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/e2e/integration/extension_documents/conftest.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/e2e/integration/extension_documents/conftest.py b/tests/e2e/integration/extension_documents/conftest.py index f11496ed..cf045364 100644 --- a/tests/e2e/integration/extension_documents/conftest.py +++ b/tests/e2e/integration/extension_documents/conftest.py @@ -24,14 +24,13 @@ def document_data(short_uuid): "name": f"e2e - please delete {short_uuid}", "description": "Created by automated E2E tests. Safe to delete.", "language": "en-US", - "documentType": "Online", - "url": "https://example.com/terms", + "documentType": "File", } @pytest.fixture -def created_document(extension_documents_service, document_data): - document = extension_documents_service.create(document_data) +def created_document(extension_documents_service, document_data, pdf_fd): + document = extension_documents_service.create(document_data, file=pdf_fd) yield document @@ -42,8 +41,8 @@ def created_document(extension_documents_service, document_data): @pytest.fixture -async def async_created_document(async_extension_documents_service, document_data): - document = await async_extension_documents_service.create(document_data) +async def async_created_document(async_extension_documents_service, document_data, pdf_fd): + document = await async_extension_documents_service.create(document_data, file=pdf_fd) yield document From 74b9933f3427e014eb36b21fe9e363714191f070 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Fri, 10 Apr 2026 09:28:49 +0100 Subject: [PATCH 4/4] MPT-19904: add download support and tests for extension documents Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../resources/integration/extension_documents.py | 4 ++++ .../extension_documents/test_async_extension_documents.py | 8 ++++++++ .../extension_documents/test_sync_extension_documents.py | 6 ++++++ .../resources/integration/test_extension_documents.py | 4 ++-- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/mpt_api_client/resources/integration/extension_documents.py b/mpt_api_client/resources/integration/extension_documents.py index 1445eceb..5f8d355a 100644 --- a/mpt_api_client/resources/integration/extension_documents.py +++ b/mpt_api_client/resources/integration/extension_documents.py @@ -2,9 +2,11 @@ from mpt_api_client.http.mixins import ( AsyncCollectionMixin, AsyncCreateFileMixin, + AsyncDownloadFileMixin, AsyncModifiableResourceMixin, CollectionMixin, CreateFileMixin, + DownloadFileMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model @@ -59,6 +61,7 @@ class ExtensionDocumentsServiceConfig: class ExtensionDocumentsService( PublishableMixin[ExtensionDocument], + DownloadFileMixin[ExtensionDocument], CreateFileMixin[ExtensionDocument], ModifiableResourceMixin[ExtensionDocument], CollectionMixin[ExtensionDocument], @@ -70,6 +73,7 @@ class ExtensionDocumentsService( class AsyncExtensionDocumentsService( AsyncPublishableMixin[ExtensionDocument], + AsyncDownloadFileMixin[ExtensionDocument], AsyncCreateFileMixin[ExtensionDocument], AsyncModifiableResourceMixin[ExtensionDocument], AsyncCollectionMixin[ExtensionDocument], diff --git a/tests/e2e/integration/extension_documents/test_async_extension_documents.py b/tests/e2e/integration/extension_documents/test_async_extension_documents.py index 5f03d5d3..ad1f8f7f 100644 --- a/tests/e2e/integration/extension_documents/test_async_extension_documents.py +++ b/tests/e2e/integration/extension_documents/test_async_extension_documents.py @@ -39,6 +39,14 @@ async def test_publish_extension_document( assert result.status == "Published" +async def test_download_extension_document( + async_extension_documents_service, async_created_document +): + result = await async_extension_documents_service.download(async_created_document.id) + + assert result.file_contents is not None + + async def test_unpublish_extension_document( async_extension_documents_service, async_created_document ): diff --git a/tests/e2e/integration/extension_documents/test_sync_extension_documents.py b/tests/e2e/integration/extension_documents/test_sync_extension_documents.py index c8fd421c..3b20cdd5 100644 --- a/tests/e2e/integration/extension_documents/test_sync_extension_documents.py +++ b/tests/e2e/integration/extension_documents/test_sync_extension_documents.py @@ -33,6 +33,12 @@ def test_publish_extension_document(extension_documents_service, created_documen assert result.status == "Published" +def test_download_extension_document(extension_documents_service, created_document): + result = extension_documents_service.download(created_document.id) + + assert result.file_contents is not None + + def test_unpublish_extension_document(extension_documents_service, created_document): extension_documents_service.publish(created_document.id) diff --git a/tests/unit/resources/integration/test_extension_documents.py b/tests/unit/resources/integration/test_extension_documents.py index 3d2a727c..bbef452a 100644 --- a/tests/unit/resources/integration/test_extension_documents.py +++ b/tests/unit/resources/integration/test_extension_documents.py @@ -76,7 +76,7 @@ def test_async_endpoint(async_extension_documents_service) -> None: @pytest.mark.parametrize( "method", - ["get", "create", "update", "delete", "publish", "unpublish", "iterate"], + ["get", "create", "update", "delete", "download", "publish", "unpublish", "iterate"], ) def test_mixins_present(extension_documents_service, method: str) -> None: result = hasattr(extension_documents_service, method) @@ -86,7 +86,7 @@ def test_mixins_present(extension_documents_service, method: str) -> None: @pytest.mark.parametrize( "method", - ["get", "create", "update", "delete", "publish", "unpublish", "iterate"], + ["get", "create", "update", "delete", "download", "publish", "unpublish", "iterate"], ) def test_async_mixins_present(async_extension_documents_service, method: str) -> None: result = hasattr(async_extension_documents_service, method)