diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index be322ea1e..4699bfeeb 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.35" +version = "0.2.36" 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..a9cca5d25 100644 --- a/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py +++ b/packages/uipath-platform/src/uipath/platform/documents/_documents_service.py @@ -4,8 +4,10 @@ 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.feature_flags import FeatureFlags from uipath.core.tracing import traced from ..common._base_service import BaseService @@ -32,6 +34,66 @@ POLLING_INTERVAL = 2 # seconds POLLING_TIMEOUT = 300 # seconds +# Opts in to DU Framework API v2. Otherwise the service calls API v1.1. +FRAMEWORK_V2_FEATURE_FLAG = "DocumentsFrameworkV2" + +# 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 _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. + + 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(): @@ -126,6 +188,9 @@ class DocumentsService(FolderContext, BaseService): This service provides methods to extract data from documents using UiPath's Document Understanding capabilities. + The service calls DU Framework API v1.1 by default. Set the `DocumentsFrameworkV2` + feature flag to call API v2 instead, e.g. `UIPATH_FEATURE_DocumentsFrameworkV2=true`. + !!! warning "Preview Feature" This function is currently experimental. Behavior and parameters are subject to change in future versions. @@ -141,12 +206,79 @@ def __init__( super().__init__(config=config, execution_context=execution_context) self.polling_interval = polling_interval self.polling_timeout = polling_timeout + self._framework_v2 = FeatureFlags.is_flag_enabled( + FRAMEWORK_V2_FEATURE_FLAG, default=False + ) + self._api_version = "2.0" if self._framework_v2 else "1.1" def _get_common_headers(self) -> Dict[str, str]: return { "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 _tag_route(self, tag: str) -> Tuple[str, ...]: + # v2 moved tag-scoped routes under a literal `tags` segment. + return ("tags", tag) if self._framework_v2 else (tag,) + + def _extractor_url( + self, + 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, + *self._tag_route(tag), + "document-types", + document_type_id, + *segments, + ) + + def _classifier_url( + self, + 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, *self._tag_route(tag), *segments) + + def _classification_validation_url( + self, + 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, *self._tag_route(tag), "classifiers", "validation", *segments + ) + + def _allow_document_type_change(self) -> Dict[str, Any]: + # v2 moved this setting into the validation configuration. + if self._framework_v2: + return {"configuration": {"allowChangeOfDocumentType": True}} + return {"allowChangeOfDocumentType": True} + def _get_classifier_id( self, project_type: ProjectType, project_id: str, version: Optional[int] ) -> Optional[str]: @@ -156,17 +288,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": self._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 +313,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": self._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 +342,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": self._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 +374,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": self._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 +405,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": self._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 +433,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": self._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": self._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": self._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 +526,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": self._api_version}, headers=self._get_common_headers(), - ).json()["projectVersion"] + )["projectVersion"] async def _get_version_async( self, @@ -419,15 +553,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": self._api_version}, headers=self._get_common_headers(), ) - ).json()["projectVersion"] + )["projectVersion"] def _get_tag( self, @@ -488,15 +624,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": self._api_version}, headers=self._get_common_headers(), files={"File": handle}, - ).json()["documentId"] + )["documentId"] async def _start_digitization_async( self, @@ -506,27 +640,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": self._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": self._api_version}, headers=self._get_common_headers(), - ).json() + ) return ( result["status"], result.get("error", None), @@ -543,16 +673,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": self._api_version}, + headers=self._get_common_headers(), + ) return ( result["status"], result.get("error", None), @@ -578,17 +704,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": self._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 +735,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": self._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: @@ -635,22 +761,17 @@ def _start_extraction( document_type_id: str, document_id: str, ) -> StartExtractionResponse: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{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 = self._extractor_url( + project_id, extractor_id, tag, 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": self._api_version}, headers=self._get_common_headers(), json={"documentId": document_id}, - ).json()["operationId"] + )["operationId"] return StartExtractionResponse( operation_id=operation_id, @@ -667,24 +788,19 @@ async def _start_extraction_async( document_type_id: str, document_id: str, ) -> StartExtractionResponse: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{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 = self._extractor_url( + project_id, extractor_id, tag, 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": self._api_version}, headers=self._get_common_headers(), json={"documentId": document_id}, ) - ).json()["operationId"] + )["operationId"] return StartExtractionResponse( operation_id=operation_id, @@ -757,21 +873,22 @@ def _wait_for_extraction( project_type: ProjectType, ) -> 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}" - ) - else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/result/{operation_id}" - ) + url = self._extractor_url( + project_id, + extractor_id, + tag, + 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": self._api_version}, headers=self._get_common_headers(), - ).json() + ) return ( result["status"], result.get("error", None), @@ -805,23 +922,22 @@ 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 = Endpoint( - f"/du_/api/framework/projects/{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 = self._extractor_url( + project_id, + extractor_id, + tag, + 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": self._api_version}, + headers=self._get_common_headers(), + ) return ( result["status"], result.get("error", None), @@ -852,22 +968,17 @@ def _start_classification( classifier_id: Optional[str], document_id: str, ) -> str: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/start" - ) - else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classification/start" - ) + url = self._classifier_url( + project_id, classifier_id, tag, "classification", "start" + ) - return self.request( + return self._request_json( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": self._api_version}, headers=self._get_common_headers(), json={"documentId": document_id}, - ).json()["operationId"] + )["operationId"] async def _start_classification_async( self, @@ -876,24 +987,19 @@ async def _start_classification_async( classifier_id: Optional[str], document_id: str, ) -> str: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/start" - ) - else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classification/start" - ) + url = self._classifier_url( + project_id, classifier_id, tag, "classification", "start" + ) return ( - await self.request_async( + await self._request_json_async( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": self._api_version}, headers=self._get_common_headers(), json={"documentId": document_id}, ) - ).json()["operationId"] + )["operationId"] def _wait_for_classification( self, @@ -904,21 +1010,16 @@ def _wait_for_classification( operation_id: str, ) -> 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}" - ) - else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classification/result/{operation_id}" - ) + url = self._classifier_url( + project_id, classifier_id, tag, "classification", "result", operation_id + ) - result = self.request( + result = self._request_json( method="GET", url=url, - params={"api-version": 1.1}, + params={"api-version": self._api_version}, headers=self._get_common_headers(), - ).json() + ) return ( result["status"], result.get("error", None), @@ -949,23 +1050,16 @@ 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 = Endpoint( - f"/du_/api/framework/projects/{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 = self._classifier_url( + project_id, classifier_id, 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": self._api_version}, + headers=self._get_common_headers(), + ) return ( result["status"], result.get("error", None), @@ -1235,29 +1329,13 @@ 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": self._api_version}, headers=self._get_common_headers(), - ).json() - - 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, @@ -1265,31 +1343,13 @@ 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() - - 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") + response = await self._request_json_async( + method="GET", + url=url, + params={"api-version": self._api_version}, + headers=self._get_common_headers(), + ) + return _operation_result(response, operation_id, operation_name) @traced(name="documents_retrieve_ixp_extraction_result", run_type="uipath") def retrieve_ixp_extraction_result( @@ -1328,8 +1388,14 @@ 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 = self._extractor_url( + project_id, + None, + tag, + document_type_id, + "extraction", + "result", + operation_id, ) extraction_response = self._retrieve_operation_result( @@ -1356,8 +1422,14 @@ 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 = self._extractor_url( + project_id, + None, + tag, + document_type_id, + "extraction", + "result", + operation_id, ) extraction_response = await self._retrieve_operation_result_async( @@ -1609,19 +1681,14 @@ def _start_classification_validation( storage_bucket_name: Optional[str] = None, 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" - ) - else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/start" - ) + url = self._classification_validation_url( + project_id, classifier_id, tag, "start" + ) - return self.request( + return self._request_json( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": self._api_version}, headers=self._get_common_headers(), json={ "classificationResults": [ @@ -1635,7 +1702,7 @@ def _start_classification_validation( "storageBucketName": storage_bucket_name, "storageBucketDirectoryPath": storage_bucket_directory_path, }, - ).json()["operationId"] + )["operationId"] async def _start_classification_validation_async( self, @@ -1650,20 +1717,15 @@ 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 = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/start" - ) - else: - url = Endpoint( - f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/start" - ) + url = self._classification_validation_url( + project_id, classifier_id, tag, "start" + ) return ( - await self.request_async( + await self._request_json_async( "POST", url=url, - params={"api-version": 1.1}, + params={"api-version": self._api_version}, headers=self._get_common_headers(), json={ "classificationResults": [ @@ -1678,7 +1740,7 @@ async def _start_classification_validation_async( "storageBucketDirectoryPath": storage_bucket_directory_path, }, ) - ).json()["operationId"] + )["operationId"] def _start_extraction_validation( self, @@ -1694,19 +1756,14 @@ def _start_extraction_validation( storage_bucket_directory_path: Optional[str], extraction_response: ExtractionResponse, ) -> StartExtractionValidationResponse: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{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 = self._extractor_url( + project_id, extractor_id, tag, 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": self._api_version}, headers=self._get_common_headers(), json={ "extractionResult": extraction_response.extraction_result.model_dump(), @@ -1716,10 +1773,10 @@ def _start_extraction_validation( "actionCatalog": action_catalog, "actionFolder": action_folder, "storageBucketName": storage_bucket_name, - "allowChangeOfDocumentType": True, + **self._allow_document_type_change(), "storageBucketDirectoryPath": storage_bucket_directory_path, }, - ).json()["operationId"] + )["operationId"] return StartExtractionValidationResponse( operation_id=operation_id, @@ -1742,20 +1799,15 @@ async def _start_extraction_validation_async( storage_bucket_directory_path: Optional[str], extraction_response: ExtractionResponse, ) -> StartExtractionValidationResponse: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{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 = self._extractor_url( + project_id, extractor_id, tag, 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": self._api_version}, headers=self._get_common_headers(), json={ "extractionResult": extraction_response.extraction_result.model_dump(), @@ -1765,11 +1817,11 @@ async def _start_extraction_validation_async( "actionCatalog": action_catalog, "actionFolder": action_folder, "storageBucketName": storage_bucket_name, - "allowChangeOfDocumentType": True, + **self._allow_document_type_change(), "storageBucketDirectoryPath": storage_bucket_directory_path, }, ) - ).json()["operationId"] + )["operationId"] return StartExtractionValidationResponse( operation_id=operation_id, @@ -1910,8 +1962,14 @@ 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 = self._extractor_url( + project_id, + None, + tag, + document_type_id, + "validation", + "result", + operation_id, ) result = self._retrieve_operation_result( @@ -1942,8 +2000,14 @@ 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 = self._extractor_url( + project_id, + None, + tag, + document_type_id, + "validation", + "result", + operation_id, ) result = await self._retrieve_operation_result_async( @@ -1968,21 +2032,16 @@ def _get_classification_validation_result( tag: Optional[str], operation_id: str, ) -> Dict: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{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 = self._classification_validation_url( + project_id, classifier_id, tag, "result", operation_id + ) - return self.request( + return self._request_json( method="GET", url=url, - params={"api-version": 1.1}, + params={"api-version": self._api_version}, headers=self._get_common_headers(), - ).json() + ) async def _get_classification_validation_result_async( self, @@ -1991,23 +2050,16 @@ async def _get_classification_validation_result_async( tag: Optional[str], operation_id: str, ) -> Dict: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{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 = self._classification_validation_url( + project_id, classifier_id, tag, "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": self._api_version}, + headers=self._get_common_headers(), + ) def _get_extraction_validation_result( self, @@ -2017,21 +2069,22 @@ def _get_extraction_validation_result( document_type_id: str, operation_id: str, ) -> Dict: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{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}" - ) + url = self._extractor_url( + project_id, + extractor_id, + tag, + document_type_id, + "validation", + "result", + operation_id, + ) - return self.request( + return self._request_json( method="GET", url=url, - params={"api-version": 1.1}, + params={"api-version": self._api_version}, headers=self._get_common_headers(), - ).json() + ) async def _get_extraction_validation_result_async( self, @@ -2041,23 +2094,22 @@ async def _get_extraction_validation_result_async( document_type_id: str, operation_id: str, ) -> Dict: - if tag is None: - url = Endpoint( - f"/du_/api/framework/projects/{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}" - ) + url = self._extractor_url( + project_id, + extractor_id, + tag, + 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() + return await self._request_json_async( + method="GET", + url=url, + params={"api-version": self._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..42b48a99e 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, prefixed by its document type and field group, if any (e.g. `Invoices.name`, `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..815961ede 100644 --- a/packages/uipath-platform/tests/services/test_documents_service.py +++ b/packages/uipath-platform/tests/services/test_documents_service.py @@ -6,6 +6,7 @@ import pytest from pytest_httpx import HTTPXMock +from uipath.core.feature_flags import FeatureFlags from uipath.platform import UiPathApiConfig, UiPathExecutionContext from uipath.platform.documents import ( @@ -13,6 +14,7 @@ ClassificationResult, DocumentsService, ExtractionResponse, + FieldType, ProjectType, ValidateClassificationAction, ValidateExtractionAction, @@ -22,12 +24,12 @@ OperationNotCompleteException, ) +FRAMEWORK_V2_FLAG_ENV = "UIPATH_FEATURE_DocumentsFrameworkV2" -@pytest.fixture -def service( - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, -): + +def _create_service( + config: UiPathApiConfig, execution_context: UiPathExecutionContext +) -> DocumentsService: return DocumentsService( config=config, execution_context=execution_context, @@ -36,6 +38,49 @@ def service( ) +@pytest.fixture +def service( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setenv(FRAMEWORK_V2_FLAG_ENV, "true") + return _create_service(config, execution_context) + + +@pytest.fixture +def service_v1( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.delenv(FRAMEWORK_V2_FLAG_ENV, raising=False) + return _create_service(config, execution_context) + + +# 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 +98,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 +265,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 +274,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 +337,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 +349,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 +376,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 +388,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 +445,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 +454,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 +495,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 +504,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 +514,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 +523,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 +574,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 +589,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 +599,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 +608,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 +662,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 +677,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 +686,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 +695,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 +710,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 +720,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 +775,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 +790,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 +803,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 +812,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 +822,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 +831,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 +1031,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 +1046,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 +1056,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 +1065,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 +1092,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 +1102,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 +1153,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 +1167,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 +1181,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 +1190,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 +1198,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 +1208,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 +1216,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 +1224,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 +1276,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 +1285,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 +1294,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 +1309,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 +1321,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 +1370,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 +1385,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 +1395,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 +1404,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 +1419,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 +1488,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 +1503,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 +1512,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 +1521,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 +1536,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 +1563,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 +1575,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 +1629,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 +1644,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 +1659,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 +1668,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 +1677,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 +1692,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 +1704,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 +1777,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 +1851,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 +1894,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 +1906,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 +1963,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 +1982,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 +2052,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 +2071,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 +2141,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 +2160,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 +2258,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 +2277,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 +2329,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 +2342,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 +2425,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 +2436,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 +2517,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 +2528,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 +2618,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 +2631,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 +2700,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 +2764,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 +2827,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 +2898,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 +2977,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 +3056,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 +3166,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 +3176,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 +3185,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 +3252,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 +3292,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 +3335,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 +3383,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 +3394,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 +3459,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 +3470,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 +3524,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 +3573,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 +3626,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 +3670,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 +3693,585 @@ 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" + + +class TestDocumentsServiceFrameworkV1: + """Framework API v1.1, used while the DocumentsFrameworkV2 flag is off.""" + + @pytest.mark.parametrize( + "env_value,configured,expected", + [ + (None, None, "1.1"), + ("true", None, "2.0"), + ("false", None, "1.1"), + (None, True, "2.0"), + ("true", False, "1.1"), + ], + ) + def test_api_version_follows_feature_flag( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + monkeypatch: pytest.MonkeyPatch, + env_value: str | None, + configured: bool | None, + expected: str, + ): + # ARRANGE + monkeypatch.setattr(FeatureFlags, "_flags", {}) + if env_value is None: + monkeypatch.delenv(FRAMEWORK_V2_FLAG_ENV, raising=False) + else: + monkeypatch.setenv(FRAMEWORK_V2_FLAG_ENV, env_value) + if configured is not None: + FeatureFlags.configure_flags({"DocumentsFrameworkV2": configured}) + + # ACT + service = _create_service(config, execution_context) + + # ASSERT + assert service._api_version == expected + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_classify_with_tag( + self, + httpx_mock: HTTPXMock, + service_v1: 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=1.1&type=Modern", + json={"projects": [{"id": project_id, "name": "TestProject"}]}, + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/tags?api-version=1.1", + json={"tags": [{"name": "Production"}]}, + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/digitization/start?api-version=1.1", + json={"documentId": document_id}, + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/digitization/result/{document_id}?api-version=1.1", + json={"status": "Succeeded", "result": {}}, + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/Production/classification/start?api-version=1.1", + match_json={"documentId": document_id}, + json={"operationId": operation_id}, + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/Production/classification/result/{operation_id}?api-version=1.1", + json={"status": "Succeeded", "result": classification_response}, + ) + + # ACT + if mode == "async": + response = await service_v1.classify_async( + tag="Production", + project_name="TestProject", + project_type=ProjectType.MODERN, + file=b"test content", + ) + else: + response = service_v1.classify( + tag="Production", + project_name="TestProject", + project_type=ProjectType.MODERN, + file=b"test content", + ) + + # ASSERT + assert response[0].tag == "Production" + assert response[0].project_id == project_id + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_extract_ixp_with_tag( + self, + httpx_mock: HTTPXMock, + service_v1: DocumentsService, + base_url: str, + org: str, + tenant: str, + mode: str, + ixp_extraction_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" + route = f"{base}/{project_id}/live/document-types/{UUID(int=0)}/extraction" + + httpx_mock.add_response( + url=f"{base}?api-version=1.1&type=IXP", + json={"projects": [{"id": project_id, "name": "TestProjectIXP"}]}, + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/tags?api-version=1.1", + json={"tags": [{"name": "live"}]}, + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/digitization/start?api-version=1.1", + json={"documentId": document_id}, + ) + httpx_mock.add_response( + url=f"{base}/{project_id}/digitization/result/{document_id}?api-version=1.1", + json={"status": "Succeeded", "result": {}}, + ) + httpx_mock.add_response( + url=f"{route}/start?api-version=1.1", + match_json={"documentId": document_id}, + json={"operationId": operation_id}, + ) + httpx_mock.add_response( + url=f"{route}/result/{operation_id}?api-version=1.1", + json={"status": "Succeeded", "result": ixp_extraction_response}, + ) + + # ACT + if mode == "async": + response = await service_v1.extract_async( + project_type=ProjectType.IXP, + project_name="TestProjectIXP", + tag="live", + file=b"test content", + ) + else: + response = service_v1.extract( + project_type=ProjectType.IXP, + project_name="TestProjectIXP", + tag="live", + file=b"test content", + ) + + # ASSERT + assert ( + response.extraction_result.model_dump() + == (ixp_extraction_response["extractionResult"]) + ) + assert response.tag == "live" + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_create_validate_classification_action_with_tag( + self, + httpx_mock: HTTPXMock, + service_v1: DocumentsService, + base_url: str, + org: str, + tenant: str, + mode: str, + classification_response: dict, # type: ignore + create_validation_action_response: dict, # type: ignore + ): + # ARRANGE + project_id = str(uuid4()) + operation_id = str(uuid4()) + route = f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/Production/classifiers/validation" + classification_result = ClassificationResult.model_validate( + { + **classification_response["classificationResults"][0], + "ProjectId": project_id, + "ProjectType": ProjectType.MODERN, + "ClassifierId": None, + "Tag": "Production", + } + ) + + httpx_mock.add_response( + url=f"{route}/start?api-version=1.1", + json={"operationId": operation_id}, + ) + httpx_mock.add_response( + url=f"{route}/result/{operation_id}?api-version=1.1", + json={"status": "Succeeded", "result": create_validation_action_response}, + ) + + # ACT + if mode == "async": + response = await service_v1.create_validate_classification_action_async( + classification_results=[classification_result], + action_title="TestAction", + ) + else: + response = service_v1.create_validate_classification_action( + classification_results=[classification_result], + action_title="TestAction", + ) + + # ASSERT + assert response.operation_id == operation_id + assert response.tag == "Production" + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_create_validate_extraction_action_with_tag( + self, + httpx_mock: HTTPXMock, + service_v1: DocumentsService, + base_url: str, + org: str, + tenant: str, + mode: str, + ixp_extraction_response: dict, # type: ignore + create_validation_action_response: dict, # type: ignore + ): + # ARRANGE + project_id = str(uuid4()) + operation_id = str(uuid4()) + route = f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/live/document-types/{UUID(int=0)}/validation" + extraction_response = ExtractionResponse.model_validate( + { + **ixp_extraction_response, + "projectId": project_id, + "projectType": ProjectType.IXP, + "extractorId": None, + "tag": "live", + "documentTypeId": str(UUID(int=0)), + } + ) + + httpx_mock.add_response( + url=f"{route}/start?api-version=1.1", + match_json={ + "extractionResult": ixp_extraction_response["extractionResult"], + "documentId": ixp_extraction_response["extractionResult"]["DocumentId"], + "actionTitle": "TestAction", + "actionPriority": None, + "actionCatalog": None, + "actionFolder": None, + "storageBucketName": None, + "allowChangeOfDocumentType": True, + "storageBucketDirectoryPath": None, + }, + json={"operationId": operation_id}, + ) + httpx_mock.add_response( + url=f"{route}/result/{operation_id}?api-version=1.1", + json={"status": "Succeeded", "result": create_validation_action_response}, + ) + + # ACT + if mode == "async": + response = await service_v1.create_validate_extraction_action_async( + extraction_response=extraction_response, + action_title="TestAction", + ) + else: + response = service_v1.create_validate_extraction_action( + extraction_response=extraction_response, + action_title="TestAction", + ) + + # ASSERT + assert response.operation_id == operation_id + assert response.tag == "live" + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_retrieve_ixp_results_with_tag( + self, + httpx_mock: HTTPXMock, + service_v1: DocumentsService, + base_url: str, + org: str, + tenant: str, + mode: str, + ixp_extraction_response: dict, # type: ignore + extraction_validation_action_response_completed: dict, # type: ignore + ): + # ARRANGE + project_id = str(uuid4()) + operation_id = str(uuid4()) + route = f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/live/document-types/{UUID(int=0)}" + + httpx_mock.add_response( + url=f"{route}/extraction/result/{operation_id}?api-version=1.1", + json={"status": "Succeeded", "result": ixp_extraction_response}, + ) + httpx_mock.add_response( + url=f"{route}/validation/result/{operation_id}?api-version=1.1", + json={ + "status": "Succeeded", + "result": extraction_validation_action_response_completed, + }, + ) + + # ACT + if mode == "async": + extraction = await service_v1.retrieve_ixp_extraction_result_async( + project_id=project_id, tag="live", operation_id=operation_id + ) + validation = ( + await service_v1.retrieve_ixp_extraction_validation_result_async( + project_id=project_id, tag="live", operation_id=operation_id + ) + ) + else: + extraction = service_v1.retrieve_ixp_extraction_result( + project_id=project_id, tag="live", operation_id=operation_id + ) + validation = service_v1.retrieve_ixp_extraction_validation_result( + project_id=project_id, tag="live", operation_id=operation_id + ) + + # ASSERT + assert extraction.tag == "live" + assert validation.operation_id == operation_id 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 b02fd2c40..99070bbf4 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.35" +version = "0.2.36" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 9887fc03a..09541f1cc 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2762,7 +2762,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.35" +version = "0.2.36" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" },