From 9f88f293ccbcb8d0c8fa9583dc2cfb0a4a8b3805 Mon Sep 17 00:00:00 2001 From: cezara98t Date: Tue, 14 Jul 2026 13:35:35 +0300 Subject: [PATCH 1/4] feat(platform): add IXP designtime transport foundation First increment of the IXP design-time Python SDK (epic ACTV-89216, spike ACTV-89217): the shared transport base every IXP service will build on. Adds platform/ixp/IxpDesigntimeService(BaseService) encoding the du_/api/designtimeapi conventions: - du_/api/designtimeapi base path, org/tenant scoped, mandatory api-version=1.0 on every call - strict path-segment percent-encoding (quote(safe="")) so reserved chars survive (/ -> %2F, space -> %20) - NO retry on non-idempotent writes (POST/PUT/PATCH/upload) or DELETE, matching the CLI SDK's zero-retry contract to avoid double-create/confirm on transient 5xx; idempotent GETs keep the platform retry policy - empty write body sent as {}; multipart `file` upload; binary download - sync + async twins; errors via EnrichedException; no FolderContext (IXP is org/tenant scoped only) Additive only: a new platform/ixp/ subpackage nothing else imports yet. 13 pytest-httpx tests; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/uipath/platform/ixp/__init__.py | 18 + .../src/uipath/platform/ixp/_transport.py | 335 ++++++++++++++++++ .../tests/services/test_ixp_transport.py | 208 +++++++++++ 3 files changed, 561 insertions(+) create mode 100644 packages/uipath-platform/src/uipath/platform/ixp/__init__.py create mode 100644 packages/uipath-platform/src/uipath/platform/ixp/_transport.py create mode 100644 packages/uipath-platform/tests/services/test_ixp_transport.py diff --git a/packages/uipath-platform/src/uipath/platform/ixp/__init__.py b/packages/uipath-platform/src/uipath/platform/ixp/__init__.py new file mode 100644 index 000000000..41626f234 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/ixp/__init__.py @@ -0,0 +1,18 @@ +"""UiPath IXP design-time SDK (``du_/api/designtimeapi``). + +This package hosts the IXP design-time services (projects, taxonomy, labellings, +documents, models). It currently provides the shared transport foundation; +resource services are added on top of :class:`IxpDesigntimeService`. +""" + +from ._transport import ( + DESIGNTIME_API_BASE, + DESIGNTIME_API_VERSION, + IxpDesigntimeService, +) + +__all__ = [ + "IxpDesigntimeService", + "DESIGNTIME_API_BASE", + "DESIGNTIME_API_VERSION", +] diff --git a/packages/uipath-platform/src/uipath/platform/ixp/_transport.py b/packages/uipath-platform/src/uipath/platform/ixp/_transport.py new file mode 100644 index 000000000..6dc0c367d --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/ixp/_transport.py @@ -0,0 +1,335 @@ +"""Transport foundation for the IXP design-time API. + +All IXP design-time endpoints live under ``du_/api/designtimeapi`` and share a +small set of transport conventions that differ from the rest of the platform +SDK. This module centralises them in :class:`IxpDesigntimeService`, the base +class every IXP service (projects, taxonomy, labellings, documents, models) +builds on: + +* **Base path** — every request is prefixed with ``du_/api/designtimeapi`` and + scoped to ``{org}/{tenant}`` by :class:`BaseService`. +* **Mandatory api-version** — the gateway requires an explicit + ``api-version=1.0`` query parameter on *every* call. +* **Strict path encoding** — dynamic path segments (project name, field name, + tag, ...) may contain reserved characters, so each is percent-encoded with + ``quote(safe="")`` (``/`` -> ``%2F``, space -> ``%20``). +* **No retry on writes** — unlike the platform default, non-idempotent verbs + (POST/PUT/PATCH, including multipart upload) and DELETE are **not** retried, + matching the CLI SDK's zero-retry contract so a transient 5xx cannot + double-create or double-confirm. Only idempotent GETs keep the platform + retry policy. +* **Multipart upload / binary download** — helpers for the ``file`` multipart + field and for reading raw bytes + content-type back. + +Errors surface as :class:`~uipath.platform.errors.EnrichedException`, which +already carries the status code and (truncated) response body as structured +fields. + +Note: IXP is org/tenant-scoped only — it has no Orchestrator folder concept — +so this base deliberately does **not** inherit ``FolderContext``. +""" + +from typing import Any, Optional, Union +from urllib.parse import quote + +from httpx import HTTPStatusError, Response +from tenacity import ( + retry, + retry_if_exception, + stop_after_attempt, +) + +from uipath.platform.constants import HEADER_USER_AGENT + +from ..common._base_service import BaseService, _inject_trace_context +from ..common._models import Endpoint +from ..common._user_agent import user_agent_value +from ..common.retry import ( + MAX_RETRY_ATTEMPTS, + is_retryable_platform_exception, + platform_wait_strategy, +) +from ..errors import EnrichedException + +#: API segment every design-time request is prefixed with. +DESIGNTIME_API_BASE = "du_/api/designtimeapi" + +#: The only api-version the gateway currently accepts; sent on every call. +DESIGNTIME_API_VERSION = "1.0" + + +class IxpDesigntimeService(BaseService): + """Base class for IXP design-time services (``du_/api/designtimeapi``). + + Subclasses build endpoints with :meth:`_endpoint` and issue requests with + the ``_get`` / ``_post`` / ``_put`` / ``_patch`` / ``_delete`` / ``_upload`` + / ``_download`` helpers (each has an ``_async`` twin). Every helper appends + the mandatory ``api-version`` query parameter; GETs are retried per the + platform policy while writes and deletes are not (see module docstring). + """ + + def _endpoint(self, template: str, **segments: Any) -> Endpoint: + """Build a design-time endpoint, percent-encoding dynamic segments. + + Args: + template: Path template rooted at the API (e.g. + ``"/api/projects/{name}/models/{version}/metrics"``). Fixed + segments are kept verbatim; ``{placeholder}`` values are filled + from ``segments``. + **segments: Values substituted into the template, each encoded with + ``quote(safe="")`` so reserved characters (``/``, spaces, ...) + survive as ``%2F`` / ``%20``. + + Returns: + The normalized ``Endpoint`` under ``du_/api/designtimeapi``. + """ + encoded = {key: quote(str(value), safe="") for key, value in segments.items()} + return Endpoint(f"/{DESIGNTIME_API_BASE}{template.format(**encoded)}") + + @staticmethod + def _params(params: Optional[dict[str, Any]]) -> dict[str, Any]: + """Merge the mandatory ``api-version`` into a request's query params.""" + return {"api-version": DESIGNTIME_API_VERSION, **(params or {})} + + @staticmethod + def _raise_for_status(response: Response) -> Response: + """Raise :class:`EnrichedException` on a non-2xx response.""" + try: + response.raise_for_status() + except HTTPStatusError as error: + raise EnrichedException(error) from error + return response + + def _headers(self) -> dict[str, str]: + """Per-request headers: user-agent + trace context (auth is on the client).""" + headers: dict[str, str] = { + HEADER_USER_AGENT: user_agent_value(self._specific_component) + } + _inject_trace_context(headers) + return headers + + # ── low-level send (no retry) ──────────────────────────────────────────── + + def _send( + self, + method: str, + endpoint: Union[Endpoint, str], + *, + params: Optional[dict[str, Any]] = None, + json: Optional[Any] = None, + files: Optional[dict[str, Any]] = None, + ) -> Response: + scoped_url = self._url.scope_url(str(endpoint), "tenant") + response = self._client.request( + method, + scoped_url, + params=self._params(params), + json=json, + files=files, + headers=self._headers(), + ) + return self._raise_for_status(response) + + async def _send_async( + self, + method: str, + endpoint: Union[Endpoint, str], + *, + params: Optional[dict[str, Any]] = None, + json: Optional[Any] = None, + files: Optional[dict[str, Any]] = None, + ) -> Response: + scoped_url = self._url.scope_url(str(endpoint), "tenant") + response = await self._client_async.request( + method, + scoped_url, + params=self._params(params), + json=json, + files=files, + headers=self._headers(), + ) + return self._raise_for_status(response) + + # ── retrying send (idempotent GET only) ────────────────────────────────── + + @retry( + retry=retry_if_exception(is_retryable_platform_exception), + wait=platform_wait_strategy, + stop=stop_after_attempt(MAX_RETRY_ATTEMPTS), + reraise=True, + ) + def _send_retrying( + self, + method: str, + endpoint: Union[Endpoint, str], + *, + params: Optional[dict[str, Any]] = None, + ) -> Response: + return self._send(method, endpoint, params=params) + + @retry( + retry=retry_if_exception(is_retryable_platform_exception), + wait=platform_wait_strategy, + stop=stop_after_attempt(MAX_RETRY_ATTEMPTS), + reraise=True, + ) + async def _send_retrying_async( + self, + method: str, + endpoint: Union[Endpoint, str], + *, + params: Optional[dict[str, Any]] = None, + ) -> Response: + return await self._send_async(method, endpoint, params=params) + + # ── verb helpers ───────────────────────────────────────────────────────── + + def _get( + self, endpoint: Union[Endpoint, str], *, params: Optional[dict[str, Any]] = None + ) -> Response: + """GET (idempotent — retried per the platform policy).""" + return self._send_retrying("GET", endpoint, params=params) + + async def _get_async( + self, endpoint: Union[Endpoint, str], *, params: Optional[dict[str, Any]] = None + ) -> Response: + """Async GET (idempotent — retried per the platform policy).""" + return await self._send_retrying_async("GET", endpoint, params=params) + + def _post( + self, + endpoint: Union[Endpoint, str], + *, + body: Optional[Any] = None, + params: Optional[dict[str, Any]] = None, + ) -> Response: + """POST (not retried). A ``None`` body is sent as ``{}`` (matching the CLI SDK).""" + return self._send( + "POST", endpoint, params=params, json=body if body is not None else {} + ) + + async def _post_async( + self, + endpoint: Union[Endpoint, str], + *, + body: Optional[Any] = None, + params: Optional[dict[str, Any]] = None, + ) -> Response: + """Async POST (not retried). A ``None`` body is sent as ``{}``.""" + return await self._send_async( + "POST", endpoint, params=params, json=body if body is not None else {} + ) + + def _put( + self, + endpoint: Union[Endpoint, str], + *, + body: Optional[Any] = None, + params: Optional[dict[str, Any]] = None, + ) -> Response: + """PUT (not retried). A ``None`` body is sent as ``{}``.""" + return self._send( + "PUT", endpoint, params=params, json=body if body is not None else {} + ) + + async def _put_async( + self, + endpoint: Union[Endpoint, str], + *, + body: Optional[Any] = None, + params: Optional[dict[str, Any]] = None, + ) -> Response: + """Async PUT (not retried). A ``None`` body is sent as ``{}``.""" + return await self._send_async( + "PUT", endpoint, params=params, json=body if body is not None else {} + ) + + def _patch( + self, + endpoint: Union[Endpoint, str], + *, + body: Optional[Any] = None, + params: Optional[dict[str, Any]] = None, + ) -> Response: + """PATCH (not retried). A ``None`` body is sent as ``{}``.""" + return self._send( + "PATCH", endpoint, params=params, json=body if body is not None else {} + ) + + async def _patch_async( + self, + endpoint: Union[Endpoint, str], + *, + body: Optional[Any] = None, + params: Optional[dict[str, Any]] = None, + ) -> Response: + """Async PATCH (not retried). A ``None`` body is sent as ``{}``.""" + return await self._send_async( + "PATCH", endpoint, params=params, json=body if body is not None else {} + ) + + def _delete( + self, endpoint: Union[Endpoint, str], *, params: Optional[dict[str, Any]] = None + ) -> Response: + """DELETE (not retried — avoids a spurious 404 from a retried delete).""" + return self._send("DELETE", endpoint, params=params) + + async def _delete_async( + self, endpoint: Union[Endpoint, str], *, params: Optional[dict[str, Any]] = None + ) -> Response: + """Async DELETE (not retried).""" + return await self._send_async("DELETE", endpoint, params=params) + + def _upload( + self, + endpoint: Union[Endpoint, str], + *, + filename: str, + content: Any, + content_type: str = "application/octet-stream", + params: Optional[dict[str, Any]] = None, + ) -> Response: + """Multipart upload of a single file under the ``file`` field (not retried).""" + return self._send( + "POST", + endpoint, + params=params, + files={"file": (filename, content, content_type)}, + ) + + async def _upload_async( + self, + endpoint: Union[Endpoint, str], + *, + filename: str, + content: Any, + content_type: str = "application/octet-stream", + params: Optional[dict[str, Any]] = None, + ) -> Response: + """Async multipart upload under the ``file`` field (not retried).""" + return await self._send_async( + "POST", + endpoint, + params=params, + files={"file": (filename, content, content_type)}, + ) + + def _download( + self, endpoint: Union[Endpoint, str], *, params: Optional[dict[str, Any]] = None + ) -> tuple[bytes, str]: + """GET raw bytes, returning ``(content, content_type)``.""" + response = self._send_retrying("GET", endpoint, params=params) + content_type = ( + response.headers.get("content-type") or "application/octet-stream" + ) + return response.content, content_type + + async def _download_async( + self, endpoint: Union[Endpoint, str], *, params: Optional[dict[str, Any]] = None + ) -> tuple[bytes, str]: + """Async GET raw bytes, returning ``(content, content_type)``.""" + response = await self._send_retrying_async("GET", endpoint, params=params) + content_type = ( + response.headers.get("content-type") or "application/octet-stream" + ) + return response.content, content_type diff --git a/packages/uipath-platform/tests/services/test_ixp_transport.py b/packages/uipath-platform/tests/services/test_ixp_transport.py new file mode 100644 index 000000000..ea20ec684 --- /dev/null +++ b/packages/uipath-platform/tests/services/test_ixp_transport.py @@ -0,0 +1,208 @@ +"""Tests for the IXP design-time transport foundation (``platform/ixp/_transport``). + +Cover the conventions the CLI SDK established and this layer must preserve: +the ``du_/api/designtimeapi`` base path, the mandatory ``api-version=1.0`` query +param, strict path-segment percent-encoding, ``{}`` as the empty write body, +multipart ``file`` upload, binary download, and — the load-bearing one — that +writes are NOT retried while idempotent GETs are. +""" + +import pytest +from pytest_httpx import HTTPXMock + +from uipath.platform import UiPathApiConfig, UiPathExecutionContext +from uipath.platform.errors import EnrichedException +from uipath.platform.ixp import IxpDesigntimeService + + +@pytest.fixture +def ixp_service( + config: UiPathApiConfig, execution_context: UiPathExecutionContext +) -> IxpDesigntimeService: + return IxpDesigntimeService(config=config, execution_context=execution_context) + + +class TestDesigntimeUrl: + def test_get_builds_designtime_url_with_api_version( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + httpx_mock.add_response(json={"value": []}) + + ixp_service._get(ixp_service._endpoint("/api/projects")) + + request = httpx_mock.get_request() + assert request is not None + assert request.method == "GET" + assert request.url.path == "/org/tenant/du_/api/designtimeapi/api/projects" + assert request.url.params.get("api-version") == "1.0" + + def test_path_segments_are_percent_encoded( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + httpx_mock.add_response(json={}) + + # A project name with a space and a slash must survive as %20 / %2F — + # not split into extra path segments, not double-encoded. + ixp_service._get(ixp_service._endpoint("/api/projects/{name}", name="a b/c")) + + request = httpx_mock.get_request() + assert request is not None + assert b"/api/projects/a%20b%2Fc" in request.url.raw_path + + +class TestWriteBody: + def test_post_sends_empty_object_when_no_body( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + httpx_mock.add_response(json={}) + + ixp_service._post( + ixp_service._endpoint("/api/projects/{name}/prompt", name="p") + ) + + request = httpx_mock.get_request() + assert request is not None + assert request.method == "POST" + assert request.content == b"{}" + assert request.headers["content-type"] == "application/json" + + +class TestRetryPolicy: + def test_post_not_retried_on_transient_error( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + # 502 is a normally-retryable status, but writes must not be retried. + httpx_mock.add_response(status_code=502) + + with pytest.raises(EnrichedException) as exc_info: + ixp_service._post( + ixp_service._endpoint("/api/projects"), body={"Name": "p"} + ) + + assert exc_info.value.status_code == 502 + assert len(httpx_mock.get_requests()) == 1 + + def test_delete_not_retried_on_transient_error( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + httpx_mock.add_response(status_code=502) + + with pytest.raises(EnrichedException) as exc_info: + ixp_service._delete(ixp_service._endpoint("/api/projects/{name}", name="p")) + + assert exc_info.value.status_code == 502 + assert len(httpx_mock.get_requests()) == 1 + + def test_get_retried_on_transient_error( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + # retry-after: 0 keeps the test fast while still exercising the retry. + httpx_mock.add_response(status_code=429, headers={"retry-after": "0"}) + httpx_mock.add_response(status_code=200, json={"value": []}) + + response = ixp_service._get(ixp_service._endpoint("/api/projects")) + + assert response.status_code == 200 + assert len(httpx_mock.get_requests()) == 2 + + +class TestUploadDownload: + def test_upload_uses_file_multipart_field( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + httpx_mock.add_response(json={"DocumentId": "d", "AttachmentRef": "r"}) + + ixp_service._upload( + ixp_service._endpoint("/api/projects/{name}/documents", name="p"), + filename="invoice.pdf", + content=b"%PDF-1.7 data", + content_type="application/pdf", + ) + + request = httpx_mock.get_request() + assert request is not None + assert request.method == "POST" + assert request.headers["content-type"].startswith("multipart/form-data") + assert b'name="file"' in request.content + assert b'filename="invoice.pdf"' in request.content + assert b"%PDF-1.7 data" in request.content + + def test_download_returns_content_and_content_type( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + httpx_mock.add_response( + content=b"\x89PNG\r\n", headers={"content-type": "image/png"} + ) + + content, content_type = ixp_service._download( + ixp_service._endpoint( + "/api/projects/{name}/documents/{doc}", name="p", doc="d" + ) + ) + + assert content == b"\x89PNG\r\n" + assert content_type == "image/png" + + def test_download_falls_back_to_octet_stream( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + httpx_mock.add_response(content=b"bytes", headers={}) + + _content, content_type = ixp_service._download( + ixp_service._endpoint( + "/api/projects/{name}/documents/{doc}", name="p", doc="d" + ) + ) + + assert content_type == "application/octet-stream" + + +class TestAsync: + @pytest.mark.anyio + async def test_get_async_builds_designtime_url( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + httpx_mock.add_response(json={"value": []}) + + await ixp_service._get_async(ixp_service._endpoint("/api/projects")) + + request = httpx_mock.get_request() + assert request is not None + assert request.url.path == "/org/tenant/du_/api/designtimeapi/api/projects" + assert request.url.params.get("api-version") == "1.0" + + @pytest.mark.anyio + async def test_post_async_not_retried_on_transient_error( + self, + httpx_mock: HTTPXMock, + ixp_service: IxpDesigntimeService, + ) -> None: + httpx_mock.add_response(status_code=502) + + with pytest.raises(EnrichedException) as exc_info: + await ixp_service._post_async( + ixp_service._endpoint("/api/projects"), body={"Name": "p"} + ) + + assert exc_info.value.status_code == 502 + assert len(httpx_mock.get_requests()) == 1 From 26ccf1137f6c311418dc3d5f6bd0664aa75ccfa2 Mon Sep 17 00:00:00 2001 From: cezara98t Date: Tue, 14 Jul 2026 16:16:56 +0300 Subject: [PATCH 2/4] refactor(platform): rename ixp/ package to document_projects/ Name the design-time service folder after its domain entity rather than the product acronym, matching the entity-based convention used elsewhere in platform/ (connections, entities, documents, ...). Pairs cleanly with the runtime sibling: documents/ (du_/api/framework) is DU runtime, document_projects/ (du_/api/designtimeapi) is DU design-time. The IxpDesigntimeService class name is unchanged (it names the API, not the folder). Only the directory, the import path, and doc references move. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/uipath-platform/CLAUDE.md | 3 ++- .../uipath/platform/{ixp => document_projects}/__init__.py | 0 .../uipath/platform/{ixp => document_projects}/_transport.py | 0 ...t_ixp_transport.py => test_document_projects_transport.py} | 4 ++-- 4 files changed, 4 insertions(+), 3 deletions(-) rename packages/uipath-platform/src/uipath/platform/{ixp => document_projects}/__init__.py (100%) rename packages/uipath-platform/src/uipath/platform/{ixp => document_projects}/_transport.py (100%) rename packages/uipath-platform/tests/services/{test_ixp_transport.py => test_document_projects_transport.py} (98%) diff --git a/packages/uipath-platform/CLAUDE.md b/packages/uipath-platform/CLAUDE.md index 21896676f..b6d121bc2 100644 --- a/packages/uipath-platform/CLAUDE.md +++ b/packages/uipath-platform/CLAUDE.md @@ -98,7 +98,8 @@ Services provide both sync and async variants (e.g., `.invoke()` and `.invoke_as | `chat/` | LLM gateway, conversations, throttling | | `connections/` | External connection management | | `context_grounding/` | RAG services (DeepRAG, batch RAG, ephemeral indexes) | -| `documents/` | Document Understanding (IXP) | +| `document_projects/` | Document Understanding (IXP) **design-time** — projects, fields, taxonomy, labelling, deployments (`du_/api/designtimeapi`) | +| `documents/` | Document Understanding (IXP) **runtime** — extraction/classification (`du_/api/framework`) | | `entities/` | Data Service entity management | | `guardrails/` | LLM output guardrails | | `resource_catalog/` | Resource discovery and metadata | diff --git a/packages/uipath-platform/src/uipath/platform/ixp/__init__.py b/packages/uipath-platform/src/uipath/platform/document_projects/__init__.py similarity index 100% rename from packages/uipath-platform/src/uipath/platform/ixp/__init__.py rename to packages/uipath-platform/src/uipath/platform/document_projects/__init__.py diff --git a/packages/uipath-platform/src/uipath/platform/ixp/_transport.py b/packages/uipath-platform/src/uipath/platform/document_projects/_transport.py similarity index 100% rename from packages/uipath-platform/src/uipath/platform/ixp/_transport.py rename to packages/uipath-platform/src/uipath/platform/document_projects/_transport.py diff --git a/packages/uipath-platform/tests/services/test_ixp_transport.py b/packages/uipath-platform/tests/services/test_document_projects_transport.py similarity index 98% rename from packages/uipath-platform/tests/services/test_ixp_transport.py rename to packages/uipath-platform/tests/services/test_document_projects_transport.py index ea20ec684..18d230102 100644 --- a/packages/uipath-platform/tests/services/test_ixp_transport.py +++ b/packages/uipath-platform/tests/services/test_document_projects_transport.py @@ -1,4 +1,4 @@ -"""Tests for the IXP design-time transport foundation (``platform/ixp/_transport``). +"""Tests for the IXP design-time transport foundation (``platform/document_projects/_transport``). Cover the conventions the CLI SDK established and this layer must preserve: the ``du_/api/designtimeapi`` base path, the mandatory ``api-version=1.0`` query @@ -11,8 +11,8 @@ from pytest_httpx import HTTPXMock from uipath.platform import UiPathApiConfig, UiPathExecutionContext +from uipath.platform.document_projects import IxpDesigntimeService from uipath.platform.errors import EnrichedException -from uipath.platform.ixp import IxpDesigntimeService @pytest.fixture From 098dfb8b38ab9782dba05ef5ed7debe1e0b3afcb Mon Sep 17 00:00:00 2001 From: cezara98t Date: Wed, 15 Jul 2026 16:08:39 +0300 Subject: [PATCH 3/4] docs(platform): justify IXP transport on its own terms, not the CLI The IXP design-time SDK is an independent client of du_/api/designtimeapi; its implementation should not reference the CLI. Reword the no-retry-on-writes rationale to stand on its own (a transient 5xx must not double-create/confirm a design-time mutation) and drop the "matching the CLI SDK" asides from the transport docstrings and the test docstring. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/uipath/platform/document_projects/_transport.py | 7 +++---- .../tests/services/test_document_projects_transport.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/document_projects/_transport.py b/packages/uipath-platform/src/uipath/platform/document_projects/_transport.py index 6dc0c367d..7bc0c6277 100644 --- a/packages/uipath-platform/src/uipath/platform/document_projects/_transport.py +++ b/packages/uipath-platform/src/uipath/platform/document_projects/_transport.py @@ -15,9 +15,8 @@ class every IXP service (projects, taxonomy, labellings, documents, models) ``quote(safe="")`` (``/`` -> ``%2F``, space -> ``%20``). * **No retry on writes** — unlike the platform default, non-idempotent verbs (POST/PUT/PATCH, including multipart upload) and DELETE are **not** retried, - matching the CLI SDK's zero-retry contract so a transient 5xx cannot - double-create or double-confirm. Only idempotent GETs keep the platform - retry policy. + so a transient 5xx cannot double-create or double-confirm a design-time + mutation. Only idempotent GETs keep the platform retry policy. * **Multipart upload / binary download** — helpers for the ``file`` multipart field and for reading raw bytes + content-type back. @@ -203,7 +202,7 @@ def _post( body: Optional[Any] = None, params: Optional[dict[str, Any]] = None, ) -> Response: - """POST (not retried). A ``None`` body is sent as ``{}`` (matching the CLI SDK).""" + """POST (not retried). A ``None`` body is sent as ``{}`` (an empty JSON object).""" return self._send( "POST", endpoint, params=params, json=body if body is not None else {} ) diff --git a/packages/uipath-platform/tests/services/test_document_projects_transport.py b/packages/uipath-platform/tests/services/test_document_projects_transport.py index 18d230102..ebb6bf555 100644 --- a/packages/uipath-platform/tests/services/test_document_projects_transport.py +++ b/packages/uipath-platform/tests/services/test_document_projects_transport.py @@ -1,6 +1,6 @@ """Tests for the IXP design-time transport foundation (``platform/document_projects/_transport``). -Cover the conventions the CLI SDK established and this layer must preserve: +Cover the design-time transport conventions this layer enforces: the ``du_/api/designtimeapi`` base path, the mandatory ``api-version=1.0`` query param, strict path-segment percent-encoding, ``{}`` as the empty write body, multipart ``file`` upload, binary download, and — the load-bearing one — that From 6c8ec8595eeb23853e0b527cf7f1df7c598e7266 Mon Sep 17 00:00:00 2001 From: cezara98t Date: Wed, 15 Jul 2026 18:40:55 +0300 Subject: [PATCH 4/4] refactor(platform): rename IXP _transport.py to _base_service.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the platform's file-naming convention: every service module is `__service.py`, and the shared HTTP base is `common/_base_service.py` (class BaseService). This module plays the same role one level down — the base service every IXP design-time service extends — so name it `_base_service.py` (class IxpDesigntimeService is unchanged). Renames the matching test file and drops the "transport" framing from the docstrings. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/uipath/platform/document_projects/__init__.py | 4 ++-- .../document_projects/{_transport.py => _base_service.py} | 4 ++-- ...ts_transport.py => test_document_projects_base_service.py} | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename packages/uipath-platform/src/uipath/platform/document_projects/{_transport.py => _base_service.py} (98%) rename packages/uipath-platform/tests/services/{test_document_projects_transport.py => test_document_projects_base_service.py} (97%) diff --git a/packages/uipath-platform/src/uipath/platform/document_projects/__init__.py b/packages/uipath-platform/src/uipath/platform/document_projects/__init__.py index 41626f234..d3f563b21 100644 --- a/packages/uipath-platform/src/uipath/platform/document_projects/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/document_projects/__init__.py @@ -1,11 +1,11 @@ """UiPath IXP design-time SDK (``du_/api/designtimeapi``). This package hosts the IXP design-time services (projects, taxonomy, labellings, -documents, models). It currently provides the shared transport foundation; +documents, models). It currently provides the shared base service; resource services are added on top of :class:`IxpDesigntimeService`. """ -from ._transport import ( +from ._base_service import ( DESIGNTIME_API_BASE, DESIGNTIME_API_VERSION, IxpDesigntimeService, diff --git a/packages/uipath-platform/src/uipath/platform/document_projects/_transport.py b/packages/uipath-platform/src/uipath/platform/document_projects/_base_service.py similarity index 98% rename from packages/uipath-platform/src/uipath/platform/document_projects/_transport.py rename to packages/uipath-platform/src/uipath/platform/document_projects/_base_service.py index 7bc0c6277..e76ec714d 100644 --- a/packages/uipath-platform/src/uipath/platform/document_projects/_transport.py +++ b/packages/uipath-platform/src/uipath/platform/document_projects/_base_service.py @@ -1,7 +1,7 @@ -"""Transport foundation for the IXP design-time API. +"""Base service for the IXP design-time API. All IXP design-time endpoints live under ``du_/api/designtimeapi`` and share a -small set of transport conventions that differ from the rest of the platform +small set of HTTP conventions that differ from the rest of the platform SDK. This module centralises them in :class:`IxpDesigntimeService`, the base class every IXP service (projects, taxonomy, labellings, documents, models) builds on: diff --git a/packages/uipath-platform/tests/services/test_document_projects_transport.py b/packages/uipath-platform/tests/services/test_document_projects_base_service.py similarity index 97% rename from packages/uipath-platform/tests/services/test_document_projects_transport.py rename to packages/uipath-platform/tests/services/test_document_projects_base_service.py index ebb6bf555..fe83fb530 100644 --- a/packages/uipath-platform/tests/services/test_document_projects_transport.py +++ b/packages/uipath-platform/tests/services/test_document_projects_base_service.py @@ -1,6 +1,6 @@ -"""Tests for the IXP design-time transport foundation (``platform/document_projects/_transport``). +"""Tests for the IXP design-time base service (``platform/document_projects/_base_service``). -Cover the design-time transport conventions this layer enforces: +Cover the design-time HTTP conventions this layer enforces: the ``du_/api/designtimeapi`` base path, the mandatory ``api-version=1.0`` query param, strict path-segment percent-encoding, ``{}`` as the empty write body, multipart ``file`` upload, binary download, and — the load-bearing one — that