From 9507d454fc3953af2c6efb3b64c13944d6bf7298 Mon Sep 17 00:00:00 2001 From: Alexandru Jircan Date: Wed, 7 Oct 2026 12:43:40 +0300 Subject: [PATCH 1/2] feat(documents): move the documents service to DU Framework v2 - send api-version=2.0 and use the v2 tags/{tag} routes - read PascalCase responses, which v2 returns for orgs with du-enable-v2-pascal-case-responses on - percent-encode URL path segments, so a tag cannot change the route - send allowChangeOfDocumentType inside the validation configuration - add the FieldGroup and MonetaryQuantity field types returned by IXP v2 Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/documents/_documents_service.py | 634 +++++++++++------- .../uipath/platform/documents/documents.py | 11 +- .../tests/services/test_documents_service.py | 498 ++++++++++---- .../ixp_extraction_response_v2.json | 84 +++ packages/uipath-platform/uv.lock | 2 +- 6 files changed, 878 insertions(+), 353 deletions(-) create mode 100644 packages/uipath-platform/tests/services/tests_data/documents_service/ixp_extraction_response_v2.json diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 091bd2284..be322ea1e 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.34" +version = "0.2.35" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py b/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py index 2dd865cef..7e285795f 100644 --- a/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py +++ b/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py @@ -4,6 +4,7 @@ from contextlib import nullcontext from pathlib import Path from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Tuple, Union +from urllib.parse import quote from uuid import UUID from uipath.core.tracing import traced @@ -32,6 +33,44 @@ POLLING_INTERVAL = 2 # seconds POLLING_TIMEOUT = 300 # seconds +API_VERSION = "2.0" + +# Response values returned as-is: Document Processing contract payloads are +# PascalCase in every API version, and the digitization DOM is large and unused. +_UNNORMALIZED_KEYS = frozenset( + { + "extractionResult", + "classificationResults", + "validatedExtractionResults", + "validatedClassificationResults", + "documentObjectModel", + } +) + + +def _framework_url(*segments: Any) -> Endpoint: + path = "/".join(quote(str(segment), safe="") for segment in segments) + return Endpoint(f"/du_/api/framework/projects/{path}") + + +def _camel_case_keys(value: Any) -> Any: + """Normalizes response keys to camelCase. + + DU Framework v2 returns PascalCase keys for organizations that have the + `du-enable-v2-pascal-case-responses` feature flag on, camelCase otherwise. + """ + if isinstance(value, list): + return [_camel_case_keys(item) for item in value] + + if not isinstance(value, dict): + return value + + normalized = {} + for key, item in value.items(): + key = key[:1].lower() + key[1:] + normalized[key] = item if key in _UNNORMALIZED_KEYS else _camel_case_keys(item) + return normalized + def _must_not_be_provided(**kwargs: Any) -> None: for name, value in kwargs.items(): @@ -147,6 +186,15 @@ def _get_common_headers(self) -> Dict[str, str]: "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", } + def _request_json(self, method: str, url: Endpoint, **kwargs: Any) -> Any: + return _camel_case_keys(self.request(method, url=url, **kwargs).json()) + + async def _request_json_async( + self, method: str, url: Endpoint, **kwargs: Any + ) -> Any: + response = await self.request_async(method, url=url, **kwargs) + return _camel_case_keys(response.json()) + def _get_classifier_id( self, project_type: ProjectType, project_id: str, version: Optional[int] ) -> Optional[str]: @@ -156,17 +204,17 @@ def _get_classifier_id( if version is None: return None - response = self.request( + response = self._request_json( "GET", - url=Endpoint(f"/du_/api/framework/projects/{project_id}/classifiers"), - params={"api-version": 1.1}, + url=_framework_url(project_id, "classifiers"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) try: return next( classifier["id"] - for classifier in response.json().get("classifiers", []) + for classifier in response.get("classifiers", []) if classifier["projectVersion"] == version ) except StopIteration: @@ -181,17 +229,17 @@ async def _get_classifier_id_async( if version is None: return None - response = await self.request_async( + response = await self._request_json_async( "GET", - url=Endpoint(f"/du_/api/framework/projects/{project_id}/classifiers"), - params={"api-version": 1.1}, + url=_framework_url(project_id, "classifiers"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) try: return next( classifier["id"] - for classifier in response.json().get("classifiers", []) + for classifier in response.get("classifiers", []) if classifier["projectVersion"] == version ) except StopIteration: @@ -210,17 +258,17 @@ def _get_extractor_id( if version is None: return None - response = self.request( + response = self._request_json( "GET", - url=Endpoint(f"/du_/api/framework/projects/{project_id}/extractors"), - params={"api-version": 1.1}, + url=_framework_url(project_id, "extractors"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) try: return next( extractor["id"] - for extractor in response.json().get("extractors", []) + for extractor in response.get("extractors", []) if extractor["projectVersion"] == version and extractor["documentTypeId"] == document_type_id ) @@ -242,17 +290,17 @@ async def _get_extractor_id_async( if version is None: return None - response = await self.request_async( + response = await self._request_json_async( "GET", - url=Endpoint(f"/du_/api/framework/projects/{project_id}/extractors"), - params={"api-version": 1.1}, + url=_framework_url(project_id, "extractors"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) try: return next( extractor["id"] - for extractor in response.json().get("extractors", []) + for extractor in response.get("extractors", []) if extractor["projectVersion"] == version and extractor["documentTypeId"] == document_type_id ) @@ -273,17 +321,17 @@ def _get_project_id( if classification_result is not None: return classification_result.project_id - response = self.request( + response = self._request_json( "GET", - url=Endpoint("/du_/api/framework/projects"), - params={"api-version": 1.1, "type": project_type.value}, + url=_framework_url(), + params={"api-version": API_VERSION, "type": project_type.value}, headers=self._get_common_headers(), ) try: return next( project["id"] - for project in response.json()["projects"] + for project in response["projects"] if project["name"] == project_name ) except StopIteration: @@ -301,39 +349,39 @@ async def _get_project_id_async( if classification_result is not None: return classification_result.project_id - response = await self.request_async( + response = await self._request_json_async( "GET", - url=Endpoint("/du_/api/framework/projects"), - params={"api-version": 1.1, "type": project_type.value}, + url=_framework_url(), + params={"api-version": API_VERSION, "type": project_type.value}, headers=self._get_common_headers(), ) try: return next( project["id"] - for project in response.json()["projects"] + for project in response["projects"] if project["name"] == project_name ) except StopIteration: raise ValueError(f"Project '{project_name}' not found.") from None def _get_project_tags(self, project_id: str) -> Set[str]: - response = self.request( + response = self._request_json( "GET", - url=Endpoint(f"/du_/api/framework/projects/{project_id}/tags"), - params={"api-version": 1.1}, + url=_framework_url(project_id, "tags"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) - return {tag["name"] for tag in response.json().get("tags", [])} + return {tag["name"] for tag in response.get("tags", [])} async def _get_project_tags_async(self, project_id: str) -> Set[str]: - response = await self.request_async( + response = await self._request_json_async( "GET", - url=Endpoint(f"/du_/api/framework/projects/{project_id}/tags"), - params={"api-version": 1.1}, + url=_framework_url(project_id, "tags"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) - return {tag["name"] for tag in response.json().get("tags", [])} + return {tag["name"] for tag in response.get("tags", [])} def _get_document_id( self, @@ -394,14 +442,16 @@ def _get_version( if classification_result is None or classification_result.classifier_id is None: return None - return self.request( + return self._request_json( "GET", - url=Endpoint( - f"/du_/api/framework/projects/{classification_result.project_id}/classifiers/{classification_result.classifier_id}" + url=_framework_url( + classification_result.project_id, + "classifiers", + classification_result.classifier_id, ), - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), - ).json()["projectVersion"] + )["projectVersion"] async def _get_version_async( self, @@ -419,15 +469,17 @@ async def _get_version_async( return None return ( - await self.request_async( + await self._request_json_async( "GET", - url=Endpoint( - f"/du_/api/framework/projects/{classification_result.project_id}/classifiers/{classification_result.classifier_id}" + url=_framework_url( + classification_result.project_id, + "classifiers", + classification_result.classifier_id, ), - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) - ).json()["projectVersion"] + )["projectVersion"] def _get_tag( self, @@ -488,15 +540,13 @@ def _start_digitization( file_path: Optional[str] = None, ) -> str: with open(Path(file_path), "rb") if file_path else nullcontext(file) as handle: - return self.request( + return self._request_json( "POST", - url=Endpoint( - f"/du_/api/framework/projects/{project_id}/digitization/start" - ), - params={"api-version": 1.1}, + url=_framework_url(project_id, "digitization", "start"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), files={"File": handle}, - ).json()["documentId"] + )["documentId"] async def _start_digitization_async( self, @@ -506,27 +556,23 @@ async def _start_digitization_async( ) -> str: with open(Path(file_path), "rb") if file_path else nullcontext(file) as handle: return ( - await self.request_async( + await self._request_json_async( "POST", - url=Endpoint( - f"/du_/api/framework/projects/{project_id}/digitization/start" - ), - params={"api-version": 1.1}, + url=_framework_url(project_id, "digitization", "start"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), files={"File": handle}, ) - ).json()["documentId"] + )["documentId"] def _wait_for_digitization(self, project_id: str, document_id: str) -> None: def result_getter() -> Tuple[str, Optional[str], Optional[str]]: - result = self.request( + result = self._request_json( method="GET", - url=Endpoint( - f"/du_/api/framework/projects/{project_id}/digitization/result/{document_id}" - ), - params={"api-version": 1.1}, + url=_framework_url(project_id, "digitization", "result", document_id), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), - ).json() + ) return ( result["status"], result.get("error", None), @@ -543,16 +589,12 @@ async def _wait_for_digitization_async( self, project_id: str, document_id: str ) -> None: async def result_getter() -> Tuple[str, Optional[str], Optional[str]]: - result = ( - await self.request_async( - method="GET", - url=Endpoint( - f"/du_/api/framework/projects/{project_id}/digitization/result/{document_id}" - ), - params={"api-version": 1.1}, - headers=self._get_common_headers(), - ) - ).json() + result = await self._request_json_async( + method="GET", + url=_framework_url(project_id, "digitization", "result", document_id), + params={"api-version": API_VERSION}, + headers=self._get_common_headers(), + ) return ( result["status"], result.get("error", None), @@ -578,17 +620,17 @@ def _get_document_type_id( if classification_result is not None: return classification_result.document_type_id - response = self.request( + response = self._request_json( "GET", - url=Endpoint(f"/du_/api/framework/projects/{project_id}/document-types"), - params={"api-version": 1.1}, + url=_framework_url(project_id, "document-types"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) try: return next( extractor["id"] - for extractor in response.json().get("documentTypes", []) + for extractor in response.get("documentTypes", []) if extractor["name"].lower() == document_type_name.lower() ) except StopIteration: @@ -609,17 +651,17 @@ async def _get_document_type_id_async( if classification_result is not None: return classification_result.document_type_id - response = await self.request_async( + response = await self._request_json_async( "GET", - url=Endpoint(f"/du_/api/framework/projects/{project_id}/document-types"), - params={"api-version": 1.1}, + url=_framework_url(project_id, "document-types"), + params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) try: return next( extractor["id"] - for extractor in response.json().get("documentTypes", []) + for extractor in response.get("documentTypes", []) if extractor["name"].lower() == document_type_name.lower() ) except StopIteration: @@ -636,21 +678,27 @@ def _start_extraction( document_id: str, ) -> StartExtractionResponse: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start" + url = _framework_url( + project_id, "extractors", extractor_id, "extraction", "start" ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/start" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "extraction", + "start", ) - operation_id = self.request( + operation_id = self._request_json( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), json={"documentId": document_id}, - ).json()["operationId"] + )["operationId"] return StartExtractionResponse( operation_id=operation_id, @@ -668,23 +716,29 @@ async def _start_extraction_async( document_id: str, ) -> StartExtractionResponse: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start" + url = _framework_url( + project_id, "extractors", extractor_id, "extraction", "start" ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/start" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "extraction", + "start", ) operation_id = ( - await self.request_async( + await self._request_json_async( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), json={"documentId": document_id}, ) - ).json()["operationId"] + )["operationId"] return StartExtractionResponse( operation_id=operation_id, @@ -758,20 +812,32 @@ def _wait_for_extraction( ) -> Union[ExtractionResponse, ExtractionResponseIXP]: def result_getter() -> Tuple[str, str, Any]: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}" + url = _framework_url( + project_id, + "extractors", + extractor_id, + "extraction", + "result", + operation_id, ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/result/{operation_id}" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "extraction", + "result", + operation_id, ) - result = self.request( + result = self._request_json( method="GET", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), - ).json() + ) return ( result["status"], result.get("error", None), @@ -806,22 +872,32 @@ async def _wait_for_extraction_async( ) -> Union[ExtractionResponse, ExtractionResponseIXP]: async def result_getter() -> Tuple[str, str, Any]: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}" + url = _framework_url( + project_id, + "extractors", + extractor_id, + "extraction", + "result", + operation_id, ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/result/{operation_id}" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "extraction", + "result", + operation_id, ) - result = ( - await self.request_async( - method="GET", - url=url, - params={"api-version": 1.1}, - headers=self._get_common_headers(), - ) - ).json() + result = await self._request_json_async( + method="GET", + url=url, + params={"api-version": API_VERSION}, + headers=self._get_common_headers(), + ) return ( result["status"], result.get("error", None), @@ -853,21 +929,19 @@ def _start_classification( document_id: str, ) -> str: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/start" + url = _framework_url( + project_id, "classifiers", classifier_id, "classification", "start" ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classification/start" - ) + url = _framework_url(project_id, "tags", tag, "classification", "start") - return self.request( + return self._request_json( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), json={"documentId": document_id}, - ).json()["operationId"] + )["operationId"] async def _start_classification_async( self, @@ -877,23 +951,21 @@ async def _start_classification_async( document_id: str, ) -> str: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/start" + url = _framework_url( + project_id, "classifiers", classifier_id, "classification", "start" ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classification/start" - ) + url = _framework_url(project_id, "tags", tag, "classification", "start") return ( - await self.request_async( + await self._request_json_async( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), json={"documentId": document_id}, ) - ).json()["operationId"] + )["operationId"] def _wait_for_classification( self, @@ -905,20 +977,25 @@ def _wait_for_classification( ) -> List[ClassificationResult]: def result_getter() -> Tuple[str, Optional[str], Optional[str]]: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/result/{operation_id}" + url = _framework_url( + project_id, + "classifiers", + classifier_id, + "classification", + "result", + operation_id, ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classification/result/{operation_id}" + url = _framework_url( + project_id, "tags", tag, "classification", "result", operation_id ) - result = self.request( + result = self._request_json( method="GET", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), - ).json() + ) return ( result["status"], result.get("error", None), @@ -950,22 +1027,25 @@ async def _wait_for_classification_async( ) -> List[ClassificationResult]: async def result_getter() -> Tuple[str, Optional[str], Optional[str]]: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/result/{operation_id}" + url = _framework_url( + project_id, + "classifiers", + classifier_id, + "classification", + "result", + operation_id, ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classification/result/{operation_id}" + url = _framework_url( + project_id, "tags", tag, "classification", "result", operation_id ) - result = ( - await self.request_async( - method="GET", - url=url, - params={"api-version": 1.1}, - headers=self._get_common_headers(), - ) - ).json() + result = await self._request_json_async( + method="GET", + url=url, + params={"api-version": API_VERSION}, + headers=self._get_common_headers(), + ) return ( result["status"], result.get("error", None), @@ -1235,12 +1315,12 @@ def _retrieve_operation_result( operation_id: str, operation_name: str, ) -> Dict: - response = self.request( + response = self._request_json( method="GET", url=url, - params={"api-version": "1.1"}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), - ).json() + ) status = response.get("status") if status in ["NotStarted", "Running"]: @@ -1265,14 +1345,12 @@ async def _retrieve_operation_result_async( operation_id: str, operation_name: str, ) -> Dict: - response = ( - await self.request_async( - method="GET", - url=url, - params={"api-version": "1.1"}, - headers=self._get_common_headers(), - ) - ).json() + response = await self._request_json_async( + method="GET", + url=url, + params={"api-version": API_VERSION}, + headers=self._get_common_headers(), + ) status = response.get("status") if status in ["NotStarted", "Running"]: @@ -1328,8 +1406,15 @@ def retrieve_ixp_extraction_result( """ document_type_id = str(UUID(int=0)) - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/result/{operation_id}" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "extraction", + "result", + operation_id, ) extraction_response = self._retrieve_operation_result( @@ -1356,8 +1441,15 @@ async def retrieve_ixp_extraction_result_async( """Asynchronous version of the [`retrieve_ixp_extraction_result`][uipath.platform.documents._documents_service.DocumentsService.retrieve_ixp_extraction_result] method.""" document_type_id = str(UUID(int=0)) - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/result/{operation_id}" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "extraction", + "result", + operation_id, ) extraction_response = await self._retrieve_operation_result_async( @@ -1610,18 +1702,18 @@ def _start_classification_validation( storage_bucket_directory_path: Optional[str] = None, ) -> str: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/start" + url = _framework_url( + project_id, "classifiers", classifier_id, "validation", "start" ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/start" + url = _framework_url( + project_id, "tags", tag, "classifiers", "validation", "start" ) - return self.request( + return self._request_json( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), json={ "classificationResults": [ @@ -1635,7 +1727,7 @@ def _start_classification_validation( "storageBucketName": storage_bucket_name, "storageBucketDirectoryPath": storage_bucket_directory_path, }, - ).json()["operationId"] + )["operationId"] async def _start_classification_validation_async( self, @@ -1651,19 +1743,19 @@ async def _start_classification_validation_async( storage_bucket_directory_path: Optional[str] = None, ) -> str: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/start" + url = _framework_url( + project_id, "classifiers", classifier_id, "validation", "start" ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/start" + url = _framework_url( + project_id, "tags", tag, "classifiers", "validation", "start" ) return ( - await self.request_async( + await self._request_json_async( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), json={ "classificationResults": [ @@ -1678,7 +1770,7 @@ async def _start_classification_validation_async( "storageBucketDirectoryPath": storage_bucket_directory_path, }, ) - ).json()["operationId"] + )["operationId"] def _start_extraction_validation( self, @@ -1695,18 +1787,24 @@ def _start_extraction_validation( extraction_response: ExtractionResponse, ) -> StartExtractionValidationResponse: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/start" + url = _framework_url( + project_id, "extractors", extractor_id, "validation", "start" ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/start" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "validation", + "start", ) - operation_id = self.request( + operation_id = self._request_json( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), json={ "extractionResult": extraction_response.extraction_result.model_dump(), @@ -1716,10 +1814,10 @@ def _start_extraction_validation( "actionCatalog": action_catalog, "actionFolder": action_folder, "storageBucketName": storage_bucket_name, - "allowChangeOfDocumentType": True, + "configuration": {"allowChangeOfDocumentType": True}, "storageBucketDirectoryPath": storage_bucket_directory_path, }, - ).json()["operationId"] + )["operationId"] return StartExtractionValidationResponse( operation_id=operation_id, @@ -1743,19 +1841,25 @@ async def _start_extraction_validation_async( extraction_response: ExtractionResponse, ) -> StartExtractionValidationResponse: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/start" + url = _framework_url( + project_id, "extractors", extractor_id, "validation", "start" ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/start" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "validation", + "start", ) operation_id = ( - await self.request_async( + await self._request_json_async( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), json={ "extractionResult": extraction_response.extraction_result.model_dump(), @@ -1765,11 +1869,11 @@ async def _start_extraction_validation_async( "actionCatalog": action_catalog, "actionFolder": action_folder, "storageBucketName": storage_bucket_name, - "allowChangeOfDocumentType": True, + "configuration": {"allowChangeOfDocumentType": True}, "storageBucketDirectoryPath": storage_bucket_directory_path, }, ) - ).json()["operationId"] + )["operationId"] return StartExtractionValidationResponse( operation_id=operation_id, @@ -1910,8 +2014,15 @@ def retrieve_ixp_extraction_validation_result( """ document_type_id = str(UUID(int=0)) - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/result/{operation_id}" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "validation", + "result", + operation_id, ) result = self._retrieve_operation_result( @@ -1942,8 +2053,15 @@ async def retrieve_ixp_extraction_validation_result_async( """Asynchronous version of the [`retrieve_ixp_extraction_validation_result`][uipath.platform.documents._documents_service.DocumentsService.retrieve_ixp_extraction_validation_result] method.""" document_type_id = str(UUID(int=0)) - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/result/{operation_id}" + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "validation", + "result", + operation_id, ) result = await self._retrieve_operation_result_async( @@ -1969,20 +2087,31 @@ def _get_classification_validation_result( operation_id: str, ) -> Dict: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/result/{operation_id}" + url = _framework_url( + project_id, + "classifiers", + classifier_id, + "validation", + "result", + operation_id, ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/result/{operation_id}" + url = _framework_url( + project_id, + "tags", + tag, + "classifiers", + "validation", + "result", + operation_id, ) - return self.request( + return self._request_json( method="GET", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), - ).json() + ) async def _get_classification_validation_result_async( self, @@ -1992,22 +2121,31 @@ async def _get_classification_validation_result_async( operation_id: str, ) -> Dict: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/result/{operation_id}" + url = _framework_url( + project_id, + "classifiers", + classifier_id, + "validation", + "result", + operation_id, ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/result/{operation_id}" + url = _framework_url( + project_id, + "tags", + tag, + "classifiers", + "validation", + "result", + operation_id, ) - return ( - await self.request_async( - method="GET", - url=url, - params={"api-version": 1.1}, - headers=self._get_common_headers(), - ) - ).json() + return await self._request_json_async( + method="GET", + url=url, + params={"api-version": API_VERSION}, + headers=self._get_common_headers(), + ) def _get_extraction_validation_result( self, @@ -2018,20 +2156,32 @@ def _get_extraction_validation_result( operation_id: str, ) -> Dict: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/result/{operation_id}" + url = _framework_url( + project_id, + "extractors", + extractor_id, + "validation", + "result", + operation_id, ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/result/{operation_id}" - ) - - return self.request( + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "validation", + "result", + operation_id, + ) + + return self._request_json( method="GET", url=url, - params={"api-version": 1.1}, + params={"api-version": API_VERSION}, headers=self._get_common_headers(), - ).json() + ) async def _get_extraction_validation_result_async( self, @@ -2042,22 +2192,32 @@ async def _get_extraction_validation_result_async( operation_id: str, ) -> Dict: if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/result/{operation_id}" + url = _framework_url( + project_id, + "extractors", + extractor_id, + "validation", + "result", + operation_id, ) else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/result/{operation_id}" - ) - - return ( - await self.request_async( - method="GET", - url=url, - params={"api-version": 1.1}, - headers=self._get_common_headers(), - ) - ).json() + url = _framework_url( + project_id, + "tags", + tag, + "document-types", + document_type_id, + "validation", + "result", + operation_id, + ) + + return await self._request_json_async( + method="GET", + url=url, + params={"api-version": API_VERSION}, + headers=self._get_common_headers(), + ) def _wait_for_create_validate_classification_action( self, diff --git a/packages/uipath-platform/src/uipath/platform/documents/documents.py b/packages/uipath-platform/src/uipath/platform/documents/documents.py index 7870635f9..d47415b28 100644 --- a/packages/uipath-platform/src/uipath/platform/documents/documents.py +++ b/packages/uipath-platform/src/uipath/platform/documents/documents.py @@ -23,6 +23,8 @@ class FieldType(str, Enum): BOOLEAN = "Boolean" TABLE = "Table" INTERNAL = "Internal" + FIELD_GROUP = "FieldGroup" + MONETARY_QUANTITY = "MonetaryQuantity" class ActionPriority(str, Enum): @@ -60,7 +62,12 @@ class ProjectType(str, Enum): class FieldValueProjection(BaseModel): - """A model representing a projection of a field value in a document extraction result.""" + """A model representing a projection of a field value in a document extraction result. + + Attributes: + id (str): The field ID, namespaced by its field group (e.g. `Default.Invoice.Total`). + name (str): The field display name. + """ model_config = ConfigDict( serialize_by_alias=True, @@ -138,7 +145,7 @@ class ExtractionResponseIXP(ExtractionResponse): """A model representing the response from a document extraction process for IXP projects. Attributes: - data_projection (List[FieldGroupValueProjection]): A simplified projection of the extracted data. + data_projection (List[FieldGroupValueProjection]): A simplified projection of the extracted data, with one entry per field group. """ data_projection: Optional[List[FieldGroupValueProjection]] = Field( diff --git a/packages/uipath-platform/tests/services/test_documents_service.py b/packages/uipath-platform/tests/services/test_documents_service.py index 1a6543afb..b1b560e3c 100644 --- a/packages/uipath-platform/tests/services/test_documents_service.py +++ b/packages/uipath-platform/tests/services/test_documents_service.py @@ -13,6 +13,7 @@ ClassificationResult, DocumentsService, ExtractionResponse, + FieldType, ProjectType, ValidateClassificationAction, ValidateExtractionAction, @@ -36,6 +37,29 @@ def service( ) +# Document Processing contract payloads are PascalCase in every API version. +CONTRACT_PAYLOAD_KEYS = { + "extractionResult", + "classificationResults", + "validatedExtractionResults", + "validatedClassificationResults", +} + + +def to_pascal_case(value: Any) -> Any: + """Mimics DU Framework v2 responses for orgs with PascalCase responses enabled.""" + if isinstance(value, list): + return [to_pascal_case(item) for item in value] + if not isinstance(value, dict): + return value + return { + key[:1].upper() + key[1:]: ( + item if key in CONTRACT_PAYLOAD_KEYS else to_pascal_case(item) + ) + for key, item in value.items() + } + + @pytest.fixture def documents_tests_data_path(tests_data_path: Path) -> Path: return tests_data_path / "documents_service" @@ -53,6 +77,12 @@ def ixp_extraction_response(documents_tests_data_path: Path) -> dict: # type: i return json.load(f) +@pytest.fixture +def ixp_extraction_response_v2(documents_tests_data_path: Path) -> dict: # type: ignore + with open(documents_tests_data_path / "ixp_extraction_response_v2.json", "r") as f: + return json.load(f) + + @pytest.fixture def modern_extraction_response(documents_tests_data_path: Path) -> dict: # type: ignore with open(documents_tests_data_path / "modern_extraction_response.json", "r") as f: @@ -214,7 +244,7 @@ async def test_extract_with_classification_result_predefined( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -223,7 +253,7 @@ async def test_extract_with_classification_result_predefined( json={"operationId": operation_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -286,7 +316,7 @@ async def test_extract_with_classification_result_modern_with_version( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -298,7 +328,7 @@ async def test_extract_with_classification_result_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -325,7 +355,7 @@ async def test_extract_with_classification_result_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -337,7 +367,7 @@ async def test_extract_with_classification_result_modern_with_version( statuses = ["NotStarted", "Running", "Succeeded"] for status in statuses: httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -394,7 +424,7 @@ async def test_extract_with_classification_result_modern_with_tag( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/Production/document-types/{document_type_id}/extraction/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/Production/document-types/{document_type_id}/extraction/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -403,7 +433,7 @@ async def test_extract_with_classification_result_modern_with_tag( json={"operationId": operation_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/Production/document-types/{document_type_id}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/Production/document-types/{document_type_id}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -444,7 +474,7 @@ async def test_classify_predefined( document_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -453,7 +483,7 @@ async def test_classify_predefined( json={"documentId": document_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -463,7 +493,7 @@ async def test_classify_predefined( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -472,7 +502,7 @@ async def test_classify_predefined( json={"operationId": operation_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -523,7 +553,7 @@ async def test_classify_with_version_classifier_not_found( version = 5 httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=Modern", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -538,7 +568,7 @@ async def test_classify_with_version_classifier_not_found( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -548,7 +578,7 @@ async def test_classify_with_version_classifier_not_found( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -557,7 +587,7 @@ async def test_classify_with_version_classifier_not_found( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -611,7 +641,7 @@ async def test_classify_modern_with_version( classifier_id = "classifier_2" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=Modern", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -626,7 +656,7 @@ async def test_classify_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -635,7 +665,7 @@ async def test_classify_modern_with_version( json={"documentId": document_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -644,7 +674,7 @@ async def test_classify_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -659,7 +689,7 @@ async def test_classify_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -669,7 +699,7 @@ async def test_classify_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -724,7 +754,7 @@ async def test_classify_modern_with_tag( document_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=Modern", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -739,7 +769,7 @@ async def test_classify_modern_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -752,7 +782,7 @@ async def test_classify_modern_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -761,7 +791,7 @@ async def test_classify_modern_with_tag( json={"documentId": document_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -771,7 +801,7 @@ async def test_classify_modern_with_tag( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/Production/classification/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/Production/classification/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -780,7 +810,7 @@ async def test_classify_modern_with_tag( json={"operationId": operation_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/Production/classification/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/Production/classification/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -980,7 +1010,7 @@ async def test_extract_ixp_with_version( version = 3 httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=IXP", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=IXP", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -995,7 +1025,7 @@ async def test_extract_ixp_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1005,7 +1035,7 @@ async def test_extract_ixp_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1014,7 +1044,7 @@ async def test_extract_ixp_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1041,7 +1071,7 @@ async def test_extract_ixp_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1051,7 +1081,7 @@ async def test_extract_ixp_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1102,7 +1132,7 @@ async def test_extract_ixp_with_tag( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=IXP", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=IXP", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1116,7 +1146,7 @@ async def test_extract_ixp_with_tag( }, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1130,7 +1160,7 @@ async def test_extract_ixp_with_tag( }, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1139,7 +1169,7 @@ async def test_extract_ixp_with_tag( json={"documentId": document_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1147,7 +1177,7 @@ async def test_extract_ixp_with_tag( json={"status": "Succeeded", "result": {}}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/live/document-types/{UUID(int=0)}/extraction/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/live/document-types/{UUID(int=0)}/extraction/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1157,7 +1187,7 @@ async def test_extract_ixp_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/live/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/live/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1165,7 +1195,7 @@ async def test_extract_ixp_with_tag( json={"status": "NotStarted", "result": ixp_extraction_response}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/live/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/live/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1173,7 +1203,7 @@ async def test_extract_ixp_with_tag( json={"status": "Running", "result": ixp_extraction_response}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/live/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/live/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1225,7 +1255,7 @@ async def test_extract_predefined( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1234,7 +1264,7 @@ async def test_extract_predefined( json={"documentId": document_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1243,7 +1273,7 @@ async def test_extract_predefined( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/document-types?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/document-types?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1258,7 +1288,7 @@ async def test_extract_predefined( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1270,7 +1300,7 @@ async def test_extract_predefined( statuses = ["NotStarted", "Running", "Succeeded"] for status in statuses: httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1319,7 +1349,7 @@ async def test_extract_modern_with_version_extractor_not_found( document_type_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=Modern", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1334,7 +1364,7 @@ async def test_extract_modern_with_version_extractor_not_found( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1344,7 +1374,7 @@ async def test_extract_modern_with_version_extractor_not_found( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1353,7 +1383,7 @@ async def test_extract_modern_with_version_extractor_not_found( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/document-types?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/document-types?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1368,7 +1398,7 @@ async def test_extract_modern_with_version_extractor_not_found( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1437,7 +1467,7 @@ async def test_extract_modern_with_version( version = 2 httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=Modern", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1452,7 +1482,7 @@ async def test_extract_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1461,7 +1491,7 @@ async def test_extract_modern_with_version( json={"documentId": document_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1470,7 +1500,7 @@ async def test_extract_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/document-types?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/document-types?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1485,7 +1515,7 @@ async def test_extract_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1512,7 +1542,7 @@ async def test_extract_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1524,7 +1554,7 @@ async def test_extract_modern_with_version( statuses = ["NotStarted", "Running", "Succeeded"] for status in statuses: httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1578,7 +1608,7 @@ async def test_extract_modern_with_tag( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=Modern", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1593,7 +1623,7 @@ async def test_extract_modern_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1608,7 +1638,7 @@ async def test_extract_modern_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1617,7 +1647,7 @@ async def test_extract_modern_with_tag( json={"documentId": document_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/result/{document_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1626,7 +1656,7 @@ async def test_extract_modern_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/document-types?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/document-types?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1641,7 +1671,7 @@ async def test_extract_modern_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/Production/document-types/{document_type_id}/extraction/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/Production/document-types/{document_type_id}/extraction/start?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1653,7 +1683,7 @@ async def test_extract_modern_with_tag( statuses = ["NotStarted", "Running", "Succeeded"] for status in statuses: httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/Production/document-types/{document_type_id}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/Production/document-types/{document_type_id}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -1726,7 +1756,7 @@ async def test_get_document_type_id_not_found( ): # ARRANGE httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/dummy_project_id/document-types?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/dummy_project_id/document-types?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={ @@ -1800,7 +1830,7 @@ async def test_extract_with_wrong_project_name( ): # ARRANGE httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=IXP", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=IXP", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={ @@ -1843,7 +1873,7 @@ async def test_extract_with_wrong_tag( # ARRANGE project_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=IXP", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=IXP", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={ @@ -1855,7 +1885,7 @@ async def test_extract_with_wrong_tag( }, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"tags": [{"name": "staging"}]}, @@ -1912,7 +1942,7 @@ async def test_create_validate_classification_action_pretrained( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -1931,7 +1961,7 @@ async def test_create_validate_classification_action_pretrained( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2001,7 +2031,7 @@ async def test_create_validate_classification_action_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -2020,7 +2050,7 @@ async def test_create_validate_classification_action_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2090,7 +2120,7 @@ async def test_create_validate_classification_action_modern_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/classifiers/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -2109,7 +2139,7 @@ async def test_create_validate_classification_action_modern_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/classifiers/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2207,7 +2237,7 @@ async def test_create_validate_classification_action_with_optional_params_omitte ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -2226,7 +2256,7 @@ async def test_create_validate_classification_action_with_optional_params_omitte ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2278,7 +2308,7 @@ async def test_create_validate_extraction_action_pretrained( storage_bucket_directory_path = "Test/Directory/Path" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -2291,14 +2321,14 @@ async def test_create_validate_extraction_action_pretrained( "actionCatalog": action_catalog, "actionFolder": action_folder, "storageBucketName": storage_bucket_name, - "allowChangeOfDocumentType": True, + "configuration": {"allowChangeOfDocumentType": True}, "storageBucketDirectoryPath": storage_bucket_directory_path, }, json={"operationId": operation_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2374,7 +2404,7 @@ async def test_create_validate_extraction_action_ixp_with_version( storage_bucket_directory_path = "Test/Directory/Path" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -2385,13 +2415,13 @@ async def test_create_validate_extraction_action_ixp_with_version( "actionCatalog": action_catalog, "actionFolder": action_folder, "storageBucketName": storage_bucket_name, - "allowChangeOfDocumentType": True, + "configuration": {"allowChangeOfDocumentType": True}, "storageBucketDirectoryPath": storage_bucket_directory_path, }, json={"operationId": operation_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2466,7 +2496,7 @@ async def test_create_validate_extraction_action_ixp_with_tag( storage_bucket_directory_path = "Test/Directory/Path" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -2477,27 +2507,27 @@ async def test_create_validate_extraction_action_ixp_with_tag( "actionCatalog": action_catalog, "actionFolder": action_folder, "storageBucketName": storage_bucket_name, - "allowChangeOfDocumentType": True, + "configuration": {"allowChangeOfDocumentType": True}, "storageBucketDirectoryPath": storage_bucket_directory_path, }, json={"operationId": operation_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "NotStarted"}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Running"}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2567,7 +2597,7 @@ async def test_create_validate_extraction_action_with_optional_params_omitted( action_title = "TestAction" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -2580,14 +2610,14 @@ async def test_create_validate_extraction_action_with_optional_params_omitted( "actionCatalog": None, "actionFolder": None, "storageBucketName": None, - "allowChangeOfDocumentType": True, + "configuration": {"allowChangeOfDocumentType": True}, "storageBucketDirectoryPath": None, }, json={"operationId": operation_id}, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2649,7 +2679,7 @@ async def test_get_validate_classification_result_pretrained( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2713,7 +2743,7 @@ async def test_get_validate_classification_result_modern_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2776,7 +2806,7 @@ async def test_get_validate_classification_result_modern_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/Production/classifiers/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/Production/classifiers/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2847,7 +2877,7 @@ async def test_get_validate_extraction_result_pretrained( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -2926,7 +2956,7 @@ async def test_get_validate_extraction_result_with_version( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -3005,7 +3035,7 @@ async def test_get_validate_extraction_result_with_tag( ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{document_type_id}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -3115,7 +3145,7 @@ async def test_start_ixp_extraction( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=IXP", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=2.0&type=IXP", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={ @@ -3125,7 +3155,7 @@ async def test_start_ixp_extraction( }, ) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_files={"File": b"test content"}, @@ -3134,7 +3164,7 @@ async def test_start_ixp_extraction( httpx_mock.add_response( method="POST", - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/staging/document-types/{UUID(int=0)}/extraction/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/staging/document-types/{UUID(int=0)}/extraction/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={"documentId": document_id}, @@ -3201,7 +3231,7 @@ async def test_retrieve_ixp_extraction_result_success( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/staging/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/staging/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": ixp_extraction_response}, @@ -3241,7 +3271,7 @@ async def test_retrieve_ixp_extraction_result_not_complete( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/staging/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/staging/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Running"}, @@ -3284,7 +3314,7 @@ async def test_retrieve_ixp_extraction_result_failed( operation_id = str(uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/staging/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/staging/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Failed", "error": "Dummy extraction error"}, @@ -3332,7 +3362,7 @@ async def test_start_ixp_extraction_validation( storage_bucket_directory_path = "Test/Directory/Path" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -3343,7 +3373,7 @@ async def test_start_ixp_extraction_validation( "actionCatalog": action_catalog, "actionFolder": action_folder, "storageBucketName": storage_bucket_name, - "allowChangeOfDocumentType": True, + "configuration": {"allowChangeOfDocumentType": True}, "storageBucketDirectoryPath": storage_bucket_directory_path, }, json={"operationId": operation_id}, @@ -3408,7 +3438,7 @@ async def test_start_ixp_extraction_validation_with_optional_params_omitted( action_title = "TestAction" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/start?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/start?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -3419,7 +3449,7 @@ async def test_start_ixp_extraction_validation_with_optional_params_omitted( "actionCatalog": None, "actionFolder": None, "storageBucketName": None, - "allowChangeOfDocumentType": True, + "configuration": {"allowChangeOfDocumentType": True}, "storageBucketDirectoryPath": None, }, json={"operationId": operation_id}, @@ -3473,7 +3503,7 @@ async def test_retrieve_ixp_extraction_validation_result_unassigned( tag = "live" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -3522,7 +3552,7 @@ async def test_retrieve_ixp_extraction_validation_result_completed( tag = "live" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={ @@ -3575,7 +3605,7 @@ async def test_retrieve_ixp_extraction_validation_result_not_complete( tag = "live" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Running"}, @@ -3619,7 +3649,7 @@ async def test_retrieve_ixp_extraction_validation_result_failed( tag = "live" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=1.1", + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/{tag}/document-types/{UUID(int=0)}/validation/result/{operation_id}?api-version=2.0", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Failed", "error": "Dummy error"}, @@ -3642,3 +3672,247 @@ async def test_retrieve_ixp_extraction_validation_result_failed( tag=tag, operation_id=operation_id, ) + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_classify_modern_with_tag_pascal_case_responses( + self, + httpx_mock: HTTPXMock, + service: DocumentsService, + base_url: str, + org: str, + tenant: str, + mode: str, + classification_response: dict, # type: ignore + ): + # ARRANGE + project_id = str(uuid4()) + document_id = str(uuid4()) + operation_id = str(uuid4()) + base = f"{base_url}{org}{tenant}/du_/api/framework/projects" + + httpx_mock.add_response( + url=f"{base}?api-version=2.0&type=Modern", + json=to_pascal_case( + {"projects": [{"id": project_id, "name": "TestProject"}]} + ), + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/tags?api-version=2.0", + json=to_pascal_case({"tags": [{"name": "Production"}]}), + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/digitization/start?api-version=2.0", + json=to_pascal_case({"documentId": document_id}), + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/digitization/result/{document_id}?api-version=2.0", + json=to_pascal_case( + {"status": "Succeeded", "result": {"documentObjectModel": {}}} + ), + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/tags/Production/classification/start?api-version=2.0", + match_json={"documentId": document_id}, + json=to_pascal_case({"operationId": operation_id}), + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/tags/Production/classification/result/{operation_id}?api-version=2.0", + json=to_pascal_case( + {"status": "Succeeded", "result": classification_response} + ), + ) + + # ACT + if mode == "async": + response = await service.classify_async( + tag="Production", + project_name="TestProject", + project_type=ProjectType.MODERN, + file=b"test content", + ) + else: + response = service.classify( + tag="Production", + project_name="TestProject", + project_type=ProjectType.MODERN, + file=b"test content", + ) + + # ASSERT + expected = classification_response["classificationResults"][0] + expected["ProjectId"] = project_id + expected["ProjectType"] = ProjectType.MODERN.value + expected["ClassifierId"] = None + expected["Tag"] = "Production" + assert response[0].model_dump() == expected + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.parametrize("pascal_case", [False, True]) + @pytest.mark.asyncio + async def test_retrieve_ixp_extraction_result_with_field_groups( + self, + httpx_mock: HTTPXMock, + service: DocumentsService, + base_url: str, + org: str, + tenant: str, + mode: str, + pascal_case: bool, + ixp_extraction_response_v2: dict, # type: ignore + ): + # ARRANGE + project_id = str(uuid4()) + operation_id = str(uuid4()) + body = {"status": "Succeeded", "result": ixp_extraction_response_v2} + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/live/document-types/{UUID(int=0)}/extraction/result/{operation_id}?api-version=2.0", + json=to_pascal_case(body) if pascal_case else body, + ) + + # ACT + if mode == "async": + response = await service.retrieve_ixp_extraction_result_async( + project_id=project_id, + tag="live", + operation_id=operation_id, + ) + else: + response = service.retrieve_ixp_extraction_result( + project_id=project_id, + tag="live", + operation_id=operation_id, + ) + + # ASSERT + assert ( + response.extraction_result.model_dump() + == (ixp_extraction_response_v2["extractionResult"]) + ) + [field_group] = response.data_projection + assert field_group.field_group_name == "Details" + total, due_date = field_group.field_values + assert total.id == "Default.Details.Total" + assert total.type == FieldType.MONETARY_QUANTITY + assert total.value == "66.79 RON" + assert due_date.type == FieldType.DATE + assert due_date.value is None + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_get_validate_extraction_result_pascal_case_responses( + self, + httpx_mock: HTTPXMock, + base_url: str, + org: str, + tenant: str, + service: DocumentsService, + create_validation_action_response: dict, # type: ignore + ixp_extraction_response_v2: dict, # type: ignore + mode: str, + ): + # ARRANGE + project_id = str(uuid4()) + operation_id = str(uuid4()) + document_type_id = str(UUID(int=0)) + url = f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags/live/document-types/{document_type_id}/validation/result/{operation_id}?api-version=2.0" + + validation_action = ValidateExtractionAction( + action_data=create_validation_action_response["actionData"], + action_status="Unassigned", + project_id=project_id, + project_type=ProjectType.IXP, + tag="live", + operation_id=operation_id, + extractor_id=None, + document_type_id=document_type_id, + ) + completed = { + **create_validation_action_response, + "actionStatus": "Completed", + "validatedExtractionResults": ixp_extraction_response_v2[ + "extractionResult" + ], + "dataProjection": ixp_extraction_response_v2["dataProjection"], + } + + httpx_mock.add_response( + url=url, + json=to_pascal_case( + {"status": "Succeeded", "result": create_validation_action_response} + ), + ) + httpx_mock.add_response( + url=url, + json=to_pascal_case({"status": "Succeeded", "result": completed}), + ) + + # ACT + if mode == "async": + response = await service.get_validate_extraction_result_async( + validation_action=validation_action + ) + else: + response = service.get_validate_extraction_result( + validation_action=validation_action + ) + + # ASSERT + assert ( + response.extraction_result.model_dump() + == (ixp_extraction_response_v2["extractionResult"]) + ) + assert response.data_projection[0].field_values[0].id == ( + "Default.Details.Total" + ) + assert response.tag == "live" + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_start_ixp_extraction_encodes_tag_in_url( + self, + httpx_mock: HTTPXMock, + service: DocumentsService, + base_url: str, + org: str, + tenant: str, + mode: str, + ): + # ARRANGE + project_id = str(uuid4()) + document_id = str(uuid4()) + operation_id = str(uuid4()) + base = f"{base_url}{org}{tenant}/du_/api/framework/projects" + + httpx_mock.add_response( + url=f"{base}?api-version=2.0&type=IXP", + json={"projects": [{"id": project_id, "name": "TestProjectIXP"}]}, + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/digitization/start?api-version=2.0", + json={"documentId": document_id}, + ) + httpx_mock.add_response( + method="POST", + url=f"{base}/{project_id}/tags/my%20tag%2F..%2Fv2/document-types/{UUID(int=0)}/extraction/start?api-version=2.0", + json={"operationId": operation_id}, + ) + + # ACT + if mode == "async": + response = await service.start_ixp_extraction_async( + project_name="TestProjectIXP", + tag="my tag/../v2", + file=b"test content", + ) + else: + response = service.start_ixp_extraction( + project_name="TestProjectIXP", + tag="my tag/../v2", + file=b"test content", + ) + + # ASSERT + assert response.operation_id == operation_id + assert response.tag == "my tag/../v2" diff --git a/packages/uipath-platform/tests/services/tests_data/documents_service/ixp_extraction_response_v2.json b/packages/uipath-platform/tests/services/tests_data/documents_service/ixp_extraction_response_v2.json new file mode 100644 index 000000000..5e421cac3 --- /dev/null +++ b/packages/uipath-platform/tests/services/tests_data/documents_service/ixp_extraction_response_v2.json @@ -0,0 +1,84 @@ +{ + "extractionResult": { + "DocumentId": "9b1d3c2a-6f1e-4c55-8d0b-2f8a7a1e4c10", + "ResultsVersion": 0, + "ResultsDocument": { + "DocumentTypeId": "00000000-0000-0000-0000-000000000000", + "DocumentTypeName": "Default", + "DocumentTypeDataVersion": 0, + "DataVersion": 0, + "DocumentTypeSource": "Automatic", + "Fields": [ + { + "FieldId": "Default.Details", + "FieldName": "Details", + "FieldType": "FieldGroup", + "IsMissing": false, + "DataSource": "Automatic", + "Values": [ + { + "Components": [ + { + "FieldId": "Default.Details.Total", + "FieldName": "Total", + "FieldType": "MonetaryQuantity", + "IsMissing": false, + "DataSource": "Automatic", + "Values": [ + { + "Components": [], + "Value": "66.79 RON", + "UnformattedValue": "66.79 RON", + "Confidence": 0.9975757, + "OcrConfidence": 1.0 + } + ] + }, + { + "FieldId": "Default.Details.DueDate", + "FieldName": "Due Date", + "FieldType": "Date", + "IsMissing": true, + "DataSource": "Automatic", + "Values": [] + } + ], + "Value": "", + "UnformattedValue": "", + "Confidence": 0.9975757, + "OcrConfidence": 1.0 + } + ] + } + ], + "Tables": [] + }, + "ExtractorPayloads": null, + "BusinessRulesResults": null + }, + "dataProjection": [ + { + "fieldGroupName": "Details", + "fieldValues": [ + { + "id": "Default.Details.Total", + "name": "Total", + "value": "66.79 RON", + "unformattedValue": "66.79 RON", + "confidence": 0.9975757, + "ocrConfidence": 1.0, + "type": "MonetaryQuantity" + }, + { + "id": "Default.Details.DueDate", + "name": "Due Date", + "value": null, + "unformattedValue": null, + "confidence": null, + "ocrConfidence": null, + "type": "Date" + } + ] + } + ] +} diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 8d7cccdcc..b02fd2c40 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.34" +version = "0.2.35" source = { editable = "." } dependencies = [ { name = "anyio" }, From 226ee4432b557ee75e541e9f5c872f505a22800d Mon Sep 17 00:00:00 2001 From: Alexandru Jircan Date: Wed, 7 Oct 2026 14:24:45 +0300 Subject: [PATCH 2/2] refactor(documents): share framework route selection between sync and async - pick versioned or tagged routes in one helper per route family instead of repeating the branch in every sync and async method - share the operation status check between the sync and async retrieve - regenerate the uipath lockfile for uipath-platform 0.2.35 - document that field IDs are prefixed by document type and field group Co-Authored-By: Claude Opus 5.5 (1M context) --- .../platform/documents/_documents_service.py | 383 ++++++------------ .../uipath/platform/documents/documents.py | 2 +- packages/uipath/uv.lock | 2 +- 3 files changed, 123 insertions(+), 264 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py b/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py index 7e285795f..f5b73dfcf 100644 --- a/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py +++ b/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py @@ -53,6 +53,61 @@ def _framework_url(*segments: Any) -> Endpoint: return Endpoint(f"/du_/api/framework/projects/{path}") +def _extractor_url( + project_id: str, + extractor_id: Optional[str], + tag: Optional[str], + document_type_id: str, + *segments: Any, +) -> Endpoint: + if tag is None: + return _framework_url(project_id, "extractors", extractor_id, *segments) + return _framework_url( + project_id, "tags", tag, "document-types", document_type_id, *segments + ) + + +def _classifier_url( + project_id: str, classifier_id: Optional[str], tag: Optional[str], *segments: Any +) -> Endpoint: + if tag is None: + return _framework_url(project_id, "classifiers", classifier_id, *segments) + return _framework_url(project_id, "tags", tag, *segments) + + +def _classification_validation_url( + project_id: str, classifier_id: Optional[str], tag: Optional[str], *segments: Any +) -> Endpoint: + if tag is None: + return _framework_url( + project_id, "classifiers", classifier_id, "validation", *segments + ) + return _framework_url( + project_id, "tags", tag, "classifiers", "validation", *segments + ) + + +def _operation_result( + response: Dict[str, Any], operation_id: str, operation_name: str +) -> Dict[str, Any]: + status = response.get("status") + if status in ["NotStarted", "Running"]: + raise OperationNotCompleteException( + operation_id=operation_id, + status=status, + operation_name=operation_name, + ) + + if status != "Succeeded": + raise OperationFailedException( + operation_id=operation_id, + status=status, + error=response.get("error"), + operation_name=operation_name, + ) + return response.get("result") + + def _camel_case_keys(value: Any) -> Any: """Normalizes response keys to camelCase. @@ -677,20 +732,9 @@ def _start_extraction( document_type_id: str, document_id: str, ) -> StartExtractionResponse: - if tag is None: - url = _framework_url( - project_id, "extractors", extractor_id, "extraction", "start" - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "document-types", - document_type_id, - "extraction", - "start", - ) + url = _extractor_url( + project_id, extractor_id, tag, document_type_id, "extraction", "start" + ) operation_id = self._request_json( "POST", @@ -715,20 +759,9 @@ async def _start_extraction_async( document_type_id: str, document_id: str, ) -> StartExtractionResponse: - if tag is None: - url = _framework_url( - project_id, "extractors", extractor_id, "extraction", "start" - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "document-types", - document_type_id, - "extraction", - "start", - ) + url = _extractor_url( + project_id, extractor_id, tag, document_type_id, "extraction", "start" + ) operation_id = ( await self._request_json_async( @@ -811,26 +844,15 @@ def _wait_for_extraction( project_type: ProjectType, ) -> Union[ExtractionResponse, ExtractionResponseIXP]: def result_getter() -> Tuple[str, str, Any]: - if tag is None: - url = _framework_url( - project_id, - "extractors", - extractor_id, - "extraction", - "result", - operation_id, - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "document-types", - document_type_id, - "extraction", - "result", - operation_id, - ) + url = _extractor_url( + project_id, + extractor_id, + tag, + document_type_id, + "extraction", + "result", + operation_id, + ) result = self._request_json( method="GET", @@ -871,26 +893,15 @@ async def _wait_for_extraction_async( project_type: ProjectType, ) -> Union[ExtractionResponse, ExtractionResponseIXP]: async def result_getter() -> Tuple[str, str, Any]: - if tag is None: - url = _framework_url( - project_id, - "extractors", - extractor_id, - "extraction", - "result", - operation_id, - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "document-types", - document_type_id, - "extraction", - "result", - operation_id, - ) + url = _extractor_url( + project_id, + extractor_id, + tag, + document_type_id, + "extraction", + "result", + operation_id, + ) result = await self._request_json_async( method="GET", @@ -928,12 +939,7 @@ def _start_classification( classifier_id: Optional[str], document_id: str, ) -> str: - if tag is None: - url = _framework_url( - project_id, "classifiers", classifier_id, "classification", "start" - ) - else: - url = _framework_url(project_id, "tags", tag, "classification", "start") + url = _classifier_url(project_id, classifier_id, tag, "classification", "start") return self._request_json( "POST", @@ -950,12 +956,7 @@ async def _start_classification_async( classifier_id: Optional[str], document_id: str, ) -> str: - if tag is None: - url = _framework_url( - project_id, "classifiers", classifier_id, "classification", "start" - ) - else: - url = _framework_url(project_id, "tags", tag, "classification", "start") + url = _classifier_url(project_id, classifier_id, tag, "classification", "start") return ( await self._request_json_async( @@ -976,19 +977,9 @@ def _wait_for_classification( operation_id: str, ) -> List[ClassificationResult]: def result_getter() -> Tuple[str, Optional[str], Optional[str]]: - if tag is None: - url = _framework_url( - project_id, - "classifiers", - classifier_id, - "classification", - "result", - operation_id, - ) - else: - url = _framework_url( - project_id, "tags", tag, "classification", "result", operation_id - ) + url = _classifier_url( + project_id, classifier_id, tag, "classification", "result", operation_id + ) result = self._request_json( method="GET", @@ -1026,19 +1017,9 @@ async def _wait_for_classification_async( operation_id: str, ) -> List[ClassificationResult]: async def result_getter() -> Tuple[str, Optional[str], Optional[str]]: - if tag is None: - url = _framework_url( - project_id, - "classifiers", - classifier_id, - "classification", - "result", - operation_id, - ) - else: - url = _framework_url( - project_id, "tags", tag, "classification", "result", operation_id - ) + url = _classifier_url( + project_id, classifier_id, tag, "classification", "result", operation_id + ) result = await self._request_json_async( method="GET", @@ -1321,23 +1302,7 @@ def _retrieve_operation_result( params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) - - status = response.get("status") - if status in ["NotStarted", "Running"]: - raise OperationNotCompleteException( - operation_id=operation_id, - status=response.get("status"), - operation_name=operation_name, - ) - - if status != "Succeeded": - raise OperationFailedException( - operation_id=operation_id, - status=status, - error=response.get("error"), - operation_name=operation_name, - ) - return response.get("result") + return _operation_result(response, operation_id, operation_name) async def _retrieve_operation_result_async( self, @@ -1351,23 +1316,7 @@ async def _retrieve_operation_result_async( params={"api-version": API_VERSION}, headers=self._get_common_headers(), ) - - status = response.get("status") - if status in ["NotStarted", "Running"]: - raise OperationNotCompleteException( - operation_id=operation_id, - status=response.get("status"), - operation_name=operation_name, - ) - - if status != "Succeeded": - raise OperationFailedException( - operation_id=operation_id, - status=status, - error=response.get("error"), - operation_name=operation_name, - ) - return response.get("result") + return _operation_result(response, operation_id, operation_name) @traced(name="documents_retrieve_ixp_extraction_result", run_type="uipath") def retrieve_ixp_extraction_result( @@ -1701,14 +1650,7 @@ def _start_classification_validation( storage_bucket_name: Optional[str] = None, storage_bucket_directory_path: Optional[str] = None, ) -> str: - if tag is None: - url = _framework_url( - project_id, "classifiers", classifier_id, "validation", "start" - ) - else: - url = _framework_url( - project_id, "tags", tag, "classifiers", "validation", "start" - ) + url = _classification_validation_url(project_id, classifier_id, tag, "start") return self._request_json( "POST", @@ -1742,14 +1684,7 @@ async def _start_classification_validation_async( storage_bucket_name: Optional[str] = None, storage_bucket_directory_path: Optional[str] = None, ) -> str: - if tag is None: - url = _framework_url( - project_id, "classifiers", classifier_id, "validation", "start" - ) - else: - url = _framework_url( - project_id, "tags", tag, "classifiers", "validation", "start" - ) + url = _classification_validation_url(project_id, classifier_id, tag, "start") return ( await self._request_json_async( @@ -1786,20 +1721,9 @@ def _start_extraction_validation( storage_bucket_directory_path: Optional[str], extraction_response: ExtractionResponse, ) -> StartExtractionValidationResponse: - if tag is None: - url = _framework_url( - project_id, "extractors", extractor_id, "validation", "start" - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "document-types", - document_type_id, - "validation", - "start", - ) + url = _extractor_url( + project_id, extractor_id, tag, document_type_id, "validation", "start" + ) operation_id = self._request_json( "POST", @@ -1840,20 +1764,9 @@ async def _start_extraction_validation_async( storage_bucket_directory_path: Optional[str], extraction_response: ExtractionResponse, ) -> StartExtractionValidationResponse: - if tag is None: - url = _framework_url( - project_id, "extractors", extractor_id, "validation", "start" - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "document-types", - document_type_id, - "validation", - "start", - ) + url = _extractor_url( + project_id, extractor_id, tag, document_type_id, "validation", "start" + ) operation_id = ( await self._request_json_async( @@ -2086,25 +1999,9 @@ def _get_classification_validation_result( tag: Optional[str], operation_id: str, ) -> Dict: - if tag is None: - url = _framework_url( - project_id, - "classifiers", - classifier_id, - "validation", - "result", - operation_id, - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "classifiers", - "validation", - "result", - operation_id, - ) + url = _classification_validation_url( + project_id, classifier_id, tag, "result", operation_id + ) return self._request_json( method="GET", @@ -2120,25 +2017,9 @@ async def _get_classification_validation_result_async( tag: Optional[str], operation_id: str, ) -> Dict: - if tag is None: - url = _framework_url( - project_id, - "classifiers", - classifier_id, - "validation", - "result", - operation_id, - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "classifiers", - "validation", - "result", - operation_id, - ) + url = _classification_validation_url( + project_id, classifier_id, tag, "result", operation_id + ) return await self._request_json_async( method="GET", @@ -2155,26 +2036,15 @@ def _get_extraction_validation_result( document_type_id: str, operation_id: str, ) -> Dict: - if tag is None: - url = _framework_url( - project_id, - "extractors", - extractor_id, - "validation", - "result", - operation_id, - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "document-types", - document_type_id, - "validation", - "result", - operation_id, - ) + url = _extractor_url( + project_id, + extractor_id, + tag, + document_type_id, + "validation", + "result", + operation_id, + ) return self._request_json( method="GET", @@ -2191,26 +2061,15 @@ async def _get_extraction_validation_result_async( document_type_id: str, operation_id: str, ) -> Dict: - if tag is None: - url = _framework_url( - project_id, - "extractors", - extractor_id, - "validation", - "result", - operation_id, - ) - else: - url = _framework_url( - project_id, - "tags", - tag, - "document-types", - document_type_id, - "validation", - "result", - operation_id, - ) + url = _extractor_url( + project_id, + extractor_id, + tag, + document_type_id, + "validation", + "result", + operation_id, + ) return await self._request_json_async( method="GET", diff --git a/packages/uipath-platform/src/uipath/platform/documents/documents.py b/packages/uipath-platform/src/uipath/platform/documents/documents.py index d47415b28..42b48a99e 100644 --- a/packages/uipath-platform/src/uipath/platform/documents/documents.py +++ b/packages/uipath-platform/src/uipath/platform/documents/documents.py @@ -65,7 +65,7 @@ class FieldValueProjection(BaseModel): """A model representing a projection of a field value in a document extraction result. Attributes: - id (str): The field ID, namespaced by its field group (e.g. `Default.Invoice.Total`). + id (str): The field ID, prefixed by its document type and field group, if any (e.g. `Invoices.name`, `Default.Invoice.Total`). name (str): The field display name. """ diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 9a1aa77c6..72e4bda1b 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2762,7 +2762,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.34" +version = "0.2.35" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" },