diff --git a/src/uipath/platform/documents/_documents_service.py b/src/uipath/platform/documents/_documents_service.py index cca831fcd..dce8e358e 100644 --- a/src/uipath/platform/documents/_documents_service.py +++ b/src/uipath/platform/documents/_documents_service.py @@ -51,6 +51,7 @@ def _exactly_one_must_be_provided(**kwargs: Any) -> None: def _validate_classify_params( project_type: ProjectType, tag: Optional[str], + version: Optional[int], project_name: Optional[str], file: Optional[FileContent], file_path: Optional[str], @@ -60,16 +61,18 @@ def _validate_classify_params( _must_not_be_provided( project_name=project_name, tag=tag, + version=version, ) else: _must_be_provided( project_name=project_name, - tag=tag, ) + _exactly_one_must_be_provided(tag=tag, version=version) def _validate_extract_params_and_get_project_type( tag: Optional[str], + version: Optional[int], project_name: Optional[str], file: Optional[FileContent], file_path: Optional[str], @@ -85,21 +88,23 @@ def _validate_extract_params_and_get_project_type( ) if project_type == ProjectType.PRETRAINED: - _must_not_be_provided(tag=tag, project_name=project_name) + _must_not_be_provided(tag=tag, project_name=project_name, version=version) _must_be_provided(document_type_name=document_type_name) elif project_type == ProjectType.MODERN: _must_be_provided( project_name=project_name, - tag=tag, document_type_name=document_type_name, ) + _exactly_one_must_be_provided(version=version, tag=tag) else: - _must_be_provided(project_name=project_name, tag=tag) + _must_be_provided(project_name=project_name) + _exactly_one_must_be_provided(version=version, tag=tag) _must_not_be_provided(document_type_name=document_type_name) else: _must_be_provided(classification_result=classification_result) _must_not_be_provided( tag=tag, + version=version, project_name=project_name, project_type=project_type, file=file, @@ -137,9 +142,132 @@ def _get_common_headers(self) -> Dict[str, str]: "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", } - def _get_project_id_by_name( - self, project_name: str, project_type: ProjectType + def _get_classifier_id( + self, project_type: ProjectType, project_id: str, version: Optional[int] + ) -> Optional[str]: + if project_type == ProjectType.PRETRAINED: + return "ml-classification" + + if version is None: + return None + + response = self.request( + "GET", + url=Endpoint(f"/du_/api/framework/projects/{project_id}/classifiers"), + params={"api-version": 1.1}, + headers=self._get_common_headers(), + ) + + try: + return next( + classifier["id"] + for classifier in response.json().get("classifiers", []) + if classifier["projectVersion"] == version + ) + except StopIteration: + raise ValueError(f"Classifier for version '{version}' not found.") from None + + async def _get_classifier_id_async( + self, project_type: ProjectType, project_id: str, version: Optional[int] + ) -> Optional[str]: + if project_type == ProjectType.PRETRAINED: + return "ml-classification" + + if version is None: + return None + + response = await self.request_async( + "GET", + url=Endpoint(f"/du_/api/framework/projects/{project_id}/classifiers"), + params={"api-version": 1.1}, + headers=self._get_common_headers(), + ) + + try: + return next( + classifier["id"] + for classifier in response.json().get("classifiers", []) + if classifier["projectVersion"] == version + ) + except StopIteration: + raise ValueError(f"Classifier for version '{version}' not found.") from None + + def _get_extractor_id( + self, + project_id: str, + version: Optional[int], + document_type_id: str, + project_type: ProjectType, ) -> str: + if project_type == ProjectType.PRETRAINED: + return document_type_id + + if version is None: + return None + + response = self.request( + "GET", + url=Endpoint(f"/du_/api/framework/projects/{project_id}/extractors"), + params={"api-version": 1.1}, + headers=self._get_common_headers(), + ) + + try: + return next( + extractor["id"] + for extractor in response.json().get("extractors", []) + if extractor["projectVersion"] == version + and extractor["documentTypeId"] == document_type_id + ) + except StopIteration: + raise ValueError( + f"Extractor for version '{version}' and document type id '{document_type_id}' not found." + ) from None + + async def _get_extractor_id_async( + self, + project_id: str, + version: Optional[int], + document_type_id: str, + project_type: ProjectType, + ) -> str: + if project_type == ProjectType.PRETRAINED: + return document_type_id + + if version is None: + return None + + response = await self.request_async( + "GET", + url=Endpoint(f"/du_/api/framework/projects/{project_id}/extractors"), + params={"api-version": 1.1}, + headers=self._get_common_headers(), + ) + + try: + return next( + extractor["id"] + for extractor in response.json().get("extractors", []) + if extractor["projectVersion"] == version + and extractor["documentTypeId"] == document_type_id + ) + except StopIteration: + raise ValueError( + f"Extractor for version '{version}' and document type id '{document_type_id}' not found." + ) from None + + def _get_project_id( + self, + project_type: ProjectType, + project_name: Optional[str], + classification_result: Optional[ClassificationResult], + ) -> str: + if project_type == ProjectType.PRETRAINED: + return str(UUID(int=0)) + + if classification_result is not None: + return classification_result.project_id + response = self.request( "GET", url=Endpoint("/du_/api/framework/projects"), @@ -156,9 +284,18 @@ def _get_project_id_by_name( except StopIteration: raise ValueError(f"Project '{project_name}' not found.") from None - async def _get_project_id_by_name_async( - self, project_name: str, project_type: ProjectType + async def _get_project_id_async( + self, + project_type: ProjectType, + project_name: Optional[str], + classification_result: Optional[ClassificationResult], ) -> str: + if project_type == ProjectType.PRETRAINED: + return str(UUID(int=0)) + + if classification_result is not None: + return classification_result.project_id + response = await self.request_async( "GET", url=Endpoint("/du_/api/framework/projects"), @@ -237,23 +374,73 @@ async def _get_document_id_async( return document_id - def _get_project_id_and_tag( + def _get_version( + self, + version: Optional[int], + project_type: ProjectType, + classification_result: Optional[ClassificationResult], + ) -> Optional[int]: + if project_type == ProjectType.PRETRAINED: + return None + + if version is not None: + return version + + if classification_result is None or classification_result.classifier_id is None: + return None + + return self.request( + "GET", + url=Endpoint( + f"/du_/api/framework/projects/{classification_result.project_id}/classifiers/{classification_result.classifier_id}" + ), + params={"api-version": 1.1}, + headers=self._get_common_headers(), + ).json()["projectVersion"] + + async def _get_version_async( + self, + version: Optional[int], + project_type: ProjectType, + classification_result: Optional[ClassificationResult], + ) -> Optional[int]: + if project_type == ProjectType.PRETRAINED: + return None + + if version is not None: + return version + + if classification_result is None or classification_result.classifier_id is None: + return None + + return ( + await self.request_async( + "GET", + url=Endpoint( + f"/du_/api/framework/projects/{classification_result.project_id}/classifiers/{classification_result.classifier_id}" + ), + params={"api-version": 1.1}, + headers=self._get_common_headers(), + ) + ).json()["projectVersion"] + + def _get_tag( self, + project_type: ProjectType, + project_id: str, tag: Optional[str], + version: Optional[int], project_name: Optional[str], - project_type: Optional[ProjectType], classification_result: Optional[ClassificationResult], - ) -> Tuple[str, Optional[str]]: - if project_name is not None: - project_id = self._get_project_id_by_name( - project_name, - project_type, - ) - elif project_type == ProjectType.PRETRAINED: - return str(UUID(int=0)), None - else: - project_id = classification_result.project_id - tag = classification_result.tag + ) -> Optional[str]: + if project_type == ProjectType.PRETRAINED: + return None + + if version is not None: + return None + + if classification_result is not None: + return classification_result.tag tags = self._get_project_tags(project_id) if tag not in tags: @@ -261,25 +448,25 @@ def _get_project_id_and_tag( f"Tag '{tag}' not found in project '{project_name}'. Available tags: {tags}" ) - return project_id, tag + return tag - async def _get_project_id_and_tag_async( + async def _get_tag_async( self, + project_type: ProjectType, + project_id: str, tag: Optional[str], + version: Optional[int], project_name: Optional[str], - project_type: Optional[ProjectType], classification_result: Optional[ClassificationResult], - ) -> Tuple[str, Optional[str]]: - if project_name is not None: - project_id = await self._get_project_id_by_name_async( - project_name, - project_type, - ) - elif project_type == ProjectType.PRETRAINED: - return str(UUID(int=0)), None - else: - project_id = classification_result.project_id - tag = classification_result.tag + ) -> Optional[str]: + if project_type == ProjectType.PRETRAINED: + return None + + if version is not None: + return None + + if classification_result is not None: + return classification_result.tag tags = await self._get_project_tags_async(project_id) if tag not in tags: @@ -287,7 +474,7 @@ async def _get_project_id_and_tag_async( f"Tag '{tag}' not found in project '{project_name}'. Available tags: {tags}" ) - return project_id, tag + return tag def _start_digitization( self, @@ -438,14 +625,14 @@ async def _get_document_type_id_async( def _start_extraction( self, project_id: str, - project_type: ProjectType, + extractor_id: str, tag: Optional[str], document_type_id: str, document_id: str, ) -> StartExtractionResponse: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/start" + f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start" ) else: url = Endpoint( @@ -470,14 +657,14 @@ def _start_extraction( async def _start_extraction_async( self, project_id: str, - project_type: ProjectType, + extractor_id: str, tag: Optional[str], document_type_id: str, document_id: str, ) -> StartExtractionResponse: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/start" + f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/start" ) else: url = Endpoint( @@ -558,15 +745,16 @@ async def _wait_for_operation_async( def _wait_for_extraction( self, project_id: str, + extractor_id: Optional[str], tag: Optional[str], document_type_id: str, operation_id: str, project_type: ProjectType, ) -> Union[ExtractionResponse, ExtractionResponseIXP]: def result_getter() -> Tuple[str, str, Any]: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/result/{operation_id}" + f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}" ) else: url = Endpoint( @@ -592,6 +780,7 @@ def result_getter() -> Tuple[str, str, Any]: ) extraction_response["projectId"] = project_id + extraction_response["extractorId"] = extractor_id extraction_response["tag"] = tag extraction_response["documentTypeId"] = document_type_id extraction_response["projectType"] = project_type @@ -604,15 +793,16 @@ def result_getter() -> Tuple[str, str, Any]: async def _wait_for_extraction_async( self, project_id: str, + extractor_id: Optional[str], tag: Optional[str], document_type_id: str, operation_id: str, project_type: ProjectType, ) -> Union[ExtractionResponse, ExtractionResponseIXP]: async def result_getter() -> Tuple[str, str, Any]: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/result/{operation_id}" + f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/extraction/result/{operation_id}" ) else: url = Endpoint( @@ -640,6 +830,7 @@ async def result_getter() -> Tuple[str, str, Any]: ) extraction_response["projectId"] = project_id + extraction_response["extractorId"] = extractor_id extraction_response["tag"] = tag extraction_response["documentTypeId"] = document_type_id extraction_response["projectType"] = project_type @@ -652,13 +843,13 @@ async def result_getter() -> Tuple[str, str, Any]: def _start_classification( self, project_id: str, - project_type: ProjectType, tag: Optional[str], + classifier_id: Optional[str], document_id: str, ) -> str: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/start" + f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/start" ) else: url = Endpoint( @@ -676,13 +867,13 @@ def _start_classification( async def _start_classification_async( self, project_id: str, - project_type: ProjectType, tag: Optional[str], + classifier_id: Optional[str], document_id: str, ) -> str: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/start" + f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/start" ) else: url = Endpoint( @@ -703,13 +894,14 @@ def _wait_for_classification( self, project_id: str, project_type: ProjectType, + classifier_id: Optional[str], tag: Optional[str], operation_id: str, ) -> List[ClassificationResult]: def result_getter() -> Tuple[str, Optional[str], Optional[str]]: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/result/{operation_id}" + f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/result/{operation_id}" ) else: url = Endpoint( @@ -736,6 +928,7 @@ def result_getter() -> Tuple[str, Optional[str], Optional[str]]: for classification_result in classification_response["classificationResults"]: classification_result["ProjectId"] = project_id classification_result["ProjectType"] = project_type + classification_result["ClassifierId"] = classifier_id classification_result["Tag"] = tag return ClassificationResponse.model_validate( @@ -746,13 +939,14 @@ async def _wait_for_classification_async( self, project_id: str, project_type: ProjectType, + classifier_id: Optional[str], tag: Optional[str], operation_id: str, ) -> List[ClassificationResult]: async def result_getter() -> Tuple[str, Optional[str], Optional[str]]: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/result/{operation_id}" + f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/classification/result/{operation_id}" ) else: url = Endpoint( @@ -781,6 +975,7 @@ async def result_getter() -> Tuple[str, Optional[str], Optional[str]]: for classification_result in classification_response["classificationResults"]: classification_result["ProjectId"] = project_id classification_result["ProjectType"] = project_type + classification_result["ClassifierId"] = classifier_id classification_result["Tag"] = tag return ClassificationResponse.model_validate( @@ -792,6 +987,7 @@ def classify( self, project_type: ProjectType, tag: Optional[str] = None, + version: Optional[int] = None, project_name: Optional[str] = None, file: Optional[FileContent] = None, file_path: Optional[str] = None, @@ -802,6 +998,7 @@ def classify( project_type (ProjectType): Type of the project. project_name (str, optional): Name of the [DU Modern](https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/about-document-understanding) project. Must be provided if `project_type` is not `ProjectType.PRETRAINED`. tag (str, optional): Tag of the published project version. Must be provided if `project_type` is not `ProjectType.PRETRAINED`. + version (int, optional): Version of the published project. It can be used instead of `tag`. file (FileContent, optional): The document file to be classified. file_path (str, optional): Path to the document file to be classified. @@ -832,13 +1029,13 @@ def classify( _validate_classify_params( project_type=project_type, tag=tag, + version=version, project_name=project_name, file=file, file_path=file_path, ) - project_id, tag = self._get_project_id_and_tag( - tag=tag, + project_id = self._get_project_id( project_name=project_name, project_type=project_type, classification_result=None, @@ -851,15 +1048,29 @@ def classify( classification_result=None, ) + classifier_id = self._get_classifier_id( + project_type=project_type, project_id=project_id, version=version + ) + + tag = self._get_tag( + project_type=project_type, + project_id=project_id, + tag=tag, + version=version, + project_name=project_name, + classification_result=None, + ) + operation_id = self._start_classification( project_id=project_id, - project_type=project_type, tag=tag, + classifier_id=classifier_id, document_id=document_id, ) return self._wait_for_classification( project_id=project_id, project_type=project_type, + classifier_id=classifier_id, tag=tag, operation_id=operation_id, ) @@ -869,6 +1080,7 @@ async def classify_async( self, project_type: ProjectType, tag: Optional[str] = None, + version: Optional[int] = None, project_name: Optional[str] = None, file: Optional[FileContent] = None, file_path: Optional[str] = None, @@ -877,13 +1089,13 @@ async def classify_async( _validate_classify_params( project_type=project_type, tag=tag, + version=version, project_name=project_name, file=file, file_path=file_path, ) - project_id, tag = await self._get_project_id_and_tag_async( - tag=tag, + project_id = await self._get_project_id_async( project_name=project_name, project_type=project_type, classification_result=None, @@ -896,16 +1108,29 @@ async def classify_async( classification_result=None, ) + classifier_id = await self._get_classifier_id_async( + project_type=project_type, project_id=project_id, version=version + ) + + tag = await self._get_tag_async( + project_type=project_type, + project_id=project_id, + tag=tag, + version=version, + project_name=project_name, + classification_result=None, + ) + operation_id = await self._start_classification_async( project_id=project_id, - project_type=project_type, tag=tag, + classifier_id=classifier_id, document_id=document_id, ) - return await self._wait_for_classification_async( project_id=project_id, project_type=project_type, + classifier_id=classifier_id, tag=tag, operation_id=operation_id, ) @@ -948,7 +1173,11 @@ def start_ixp_extraction( """ _exactly_one_must_be_provided(file=file, file_path=file_path) - project_id = self._get_project_id_by_name(project_name, ProjectType.IXP) + project_id = self._get_project_id( + project_type=ProjectType.IXP, + project_name=project_name, + classification_result=None, + ) document_id = self._start_digitization( project_id=project_id, @@ -958,7 +1187,7 @@ def start_ixp_extraction( return self._start_extraction( project_id=project_id, - project_type=ProjectType.IXP, + extractor_id=None, tag=tag, document_type_id=str(UUID(int=0)), document_id=document_id, @@ -975,8 +1204,10 @@ async def start_ixp_extraction_async( """Asynchronous version of the [`start_ixp_extraction`][uipath.platform.documents._documents_service.DocumentsService.start_ixp_extraction] method.""" _exactly_one_must_be_provided(file=file, file_path=file_path) - project_id = await self._get_project_id_by_name_async( - project_name, ProjectType.IXP + project_id = await self._get_project_id_async( + project_type=ProjectType.IXP, + project_name=project_name, + classification_result=None, ) document_id = await self._start_digitization_async( @@ -987,7 +1218,7 @@ async def start_ixp_extraction_async( return await self._start_extraction_async( project_id=project_id, - project_type=ProjectType.IXP, + extractor_id=None, tag=tag, document_type_id=str(UUID(int=0)), document_id=document_id, @@ -1105,6 +1336,7 @@ def retrieve_ixp_extraction_result( ) extraction_response["projectId"] = project_id + extraction_response["extractorId"] = None extraction_response["tag"] = tag extraction_response["documentTypeId"] = document_type_id extraction_response["projectType"] = ProjectType.IXP @@ -1132,6 +1364,7 @@ async def retrieve_ixp_extraction_result_async( ) extraction_response["projectId"] = project_id + extraction_response["extractorId"] = None extraction_response["tag"] = tag extraction_response["documentTypeId"] = document_type_id extraction_response["projectType"] = ProjectType.IXP @@ -1142,6 +1375,7 @@ async def retrieve_ixp_extraction_result_async( def extract( self, tag: Optional[str] = None, + version: Optional[int] = None, project_name: Optional[str] = None, file: Optional[FileContent] = None, file_path: Optional[str] = None, @@ -1154,6 +1388,7 @@ def extract( Args: project_name (str, optional): Name of the [IXP](https://docs.uipath.com/ixp/automation-cloud/latest/overview/managing-projects#creating-a-new-project)/[DU Modern](https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/about-document-understanding) project. Must be provided if `classification_result` is not provided. tag (str): Tag of the published project version. Must be provided if `classification_result` is not provided and `project_type` is not `ProjectType.PRETRAINED`. + version (int, optional): Version of the published project. It can be used instead of `tag`. file (FileContent, optional): The document file to be processed. Must be provided if `classification_result` is not provided. file_path (str, optional): Path to the document file to be processed. Must be provided if `classification_result` is not provided. project_type (ProjectType, optional): Type of the project. Must be provided if `project_name` is provided. @@ -1205,6 +1440,7 @@ def extract( """ project_type = _validate_extract_params_and_get_project_type( tag=tag, + version=version, project_name=project_name, file=file, file_path=file_path, @@ -1213,13 +1449,27 @@ def extract( document_type_name=document_type_name, ) - project_id, tag = self._get_project_id_and_tag( - tag=tag, + project_id = self._get_project_id( project_name=project_name, project_type=project_type, classification_result=classification_result, ) + version = self._get_version( + version=version, + project_type=project_type, + classification_result=classification_result, + ) + + tag = self._get_tag( + project_type=project_type, + project_id=project_id, + tag=tag, + version=version, + project_name=project_name, + classification_result=classification_result, + ) + document_id = self._get_document_id( project_id=project_id, file=file, @@ -1234,9 +1484,16 @@ def extract( classification_result=classification_result, ) - operation_id = self._start_extraction( + extractor_id = self._get_extractor_id( project_id=project_id, + version=version, + document_type_id=document_type_id, project_type=project_type, + ) + + operation_id = self._start_extraction( + project_id=project_id, + extractor_id=extractor_id, tag=tag, document_type_id=document_type_id, document_id=document_id, @@ -1244,6 +1501,7 @@ def extract( return self._wait_for_extraction( project_id=project_id, + extractor_id=extractor_id, tag=tag, document_type_id=document_type_id, operation_id=operation_id, @@ -1254,6 +1512,7 @@ def extract( async def extract_async( self, tag: Optional[str] = None, + version: Optional[int] = None, project_name: Optional[str] = None, file: Optional[FileContent] = None, file_path: Optional[str] = None, @@ -1264,6 +1523,7 @@ async def extract_async( """Asynchronously version of the [`extract`][uipath.platform.documents._documents_service.DocumentsService.extract] method.""" project_type = _validate_extract_params_and_get_project_type( tag=tag, + version=version, project_name=project_name, file=file, file_path=file_path, @@ -1272,13 +1532,27 @@ async def extract_async( document_type_name=document_type_name, ) - project_id, tag = await self._get_project_id_and_tag_async( - tag=tag, + project_id = await self._get_project_id_async( project_name=project_name, project_type=project_type, classification_result=classification_result, ) + version = await self._get_version_async( + version=version, + project_type=project_type, + classification_result=classification_result, + ) + + tag = await self._get_tag_async( + project_type=project_type, + project_id=project_id, + tag=tag, + version=version, + project_name=project_name, + classification_result=classification_result, + ) + document_id = await self._get_document_id_async( project_id=project_id, file=file, @@ -1293,10 +1567,17 @@ async def extract_async( classification_result=classification_result, ) + extractor_id = await self._get_extractor_id_async( + project_id=project_id, + version=version, + document_type_id=document_type_id, + project_type=project_type, + ) + operation_id = ( await self._start_extraction_async( project_id=project_id, - project_type=project_type, + extractor_id=extractor_id, tag=tag, document_type_id=document_type_id, document_id=document_id, @@ -1305,6 +1586,7 @@ async def extract_async( return await self._wait_for_extraction_async( project_id=project_id, + extractor_id=extractor_id, tag=tag, document_type_id=document_type_id, operation_id=operation_id, @@ -1314,7 +1596,7 @@ async def extract_async( def _start_classification_validation( self, project_id: str, - project_type: ProjectType, + classifier_id: Optional[str], tag: Optional[str], classification_results: List[ClassificationResult], action_title: str, @@ -1324,9 +1606,9 @@ def _start_classification_validation( storage_bucket_name: Optional[str] = None, storage_bucket_directory_path: Optional[str] = None, ) -> str: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/start" + f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/start" ) else: url = Endpoint( @@ -1355,7 +1637,7 @@ def _start_classification_validation( async def _start_classification_validation_async( self, project_id: str, - project_type: ProjectType, + classifier_id: Optional[str], tag: Optional[str], classification_results: List[ClassificationResult], action_title: str, @@ -1365,9 +1647,9 @@ async def _start_classification_validation_async( storage_bucket_name: Optional[str] = None, storage_bucket_directory_path: Optional[str] = None, ) -> str: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/start" + f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/start" ) else: url = Endpoint( @@ -1398,7 +1680,7 @@ async def _start_classification_validation_async( def _start_extraction_validation( self, project_id: str, - project_type: ProjectType, + extractor_id: Optional[str], tag: Optional[str], document_type_id: str, action_title: str, @@ -1409,9 +1691,9 @@ def _start_extraction_validation( storage_bucket_directory_path: Optional[str], extraction_response: ExtractionResponse, ) -> StartOperationResponse: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/start" + f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/start" ) else: url = Endpoint( @@ -1446,7 +1728,7 @@ def _start_extraction_validation( async def _start_extraction_validation_async( self, project_id: str, - project_type: ProjectType, + extractor_id: Optional[str], tag: Optional[str], document_type_id: str, action_title: str, @@ -1457,9 +1739,9 @@ async def _start_extraction_validation_async( storage_bucket_directory_path: Optional[str], extraction_response: ExtractionResponse, ) -> StartOperationResponse: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/start" + f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/start" ) else: url = Endpoint( @@ -1534,7 +1816,7 @@ def start_ixp_extraction_validation( """ return self._start_extraction_validation( project_id=extraction_response.project_id, - project_type=ProjectType.IXP, + extractor_id=None, tag=extraction_response.tag, document_type_id=str(UUID(int=0)), action_title=action_title, @@ -1568,7 +1850,7 @@ async def start_ixp_extraction_validation_async( """Asynchronous version of the [`start_ixp_extraction_validation`][uipath.platform.documents._documents_service.DocumentsService.start_ixp_extraction_validation] method.""" return await self._start_extraction_validation_async( project_id=extraction_response.project_id, - project_type=ProjectType.IXP, + extractor_id=None, tag=extraction_response.tag, document_type_id=str(UUID(int=0)), action_title=action_title, @@ -1637,6 +1919,7 @@ def retrieve_ixp_extraction_validation_result( result["projectId"] = project_id result["projectType"] = ProjectType.IXP + result["extractorId"] = None result["tag"] = tag result["documentTypeId"] = str(UUID(int=0)) result["operationId"] = operation_id @@ -1668,6 +1951,7 @@ async def retrieve_ixp_extraction_validation_result_async( result["projectId"] = project_id result["projectType"] = ProjectType.IXP + result["extractorId"] = None result["tag"] = tag result["documentTypeId"] = str(UUID(int=0)) result["operationId"] = operation_id @@ -1677,13 +1961,13 @@ async def retrieve_ixp_extraction_validation_result_async( def _get_classification_validation_result( self, project_id: str, - project_type: ProjectType, + classifier_id: Optional[str], tag: Optional[str], operation_id: str, ) -> Dict: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}" + f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/result/{operation_id}" ) else: url = Endpoint( @@ -1700,13 +1984,13 @@ def _get_classification_validation_result( async def _get_classification_validation_result_async( self, project_id: str, - project_type: ProjectType, + classifier_id: Optional[str], tag: Optional[str], operation_id: str, ) -> Dict: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}" + f"/du_/api/framework/projects/{project_id}/classifiers/{classifier_id}/validation/result/{operation_id}" ) else: url = Endpoint( @@ -1725,14 +2009,14 @@ async def _get_classification_validation_result_async( def _get_extraction_validation_result( self, project_id: str, - project_type: ProjectType, + extractor_id: Optional[str], tag: Optional[str], document_type_id: str, operation_id: str, ) -> Dict: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}" + f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/result/{operation_id}" ) else: url = Endpoint( @@ -1749,14 +2033,14 @@ def _get_extraction_validation_result( async def _get_extraction_validation_result_async( self, project_id: str, - project_type: ProjectType, + extractor_id: Optional[str], tag: Optional[str], document_type_id: str, operation_id: str, ) -> Dict: - if project_type == ProjectType.PRETRAINED: + if tag is None: url = Endpoint( - f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}" + f"/du_/api/framework/projects/{project_id}/extractors/{extractor_id}/validation/result/{operation_id}" ) else: url = Endpoint( @@ -1776,13 +2060,14 @@ def _wait_for_create_validate_classification_action( self, project_id: str, project_type: ProjectType, + classifier_id: Optional[str], tag: Optional[str], operation_id: str, ) -> ValidateClassificationAction: def result_getter() -> Tuple[Any, Optional[Any], Optional[Any]]: result = self._get_classification_validation_result( project_id=project_id, - project_type=project_type, + classifier_id=classifier_id, tag=tag, operation_id=operation_id, ) @@ -1800,6 +2085,7 @@ def result_getter() -> Tuple[Any, Optional[Any], Optional[Any]]: response["projectId"] = project_id response["projectType"] = project_type + response["classifierId"] = classifier_id response["tag"] = tag response["operationId"] = operation_id return ValidateClassificationAction.model_validate(response) @@ -1808,13 +2094,14 @@ async def _wait_for_create_validate_classification_action_async( self, project_id: str, project_type: ProjectType, + classifier_id: Optional[str], tag: Optional[str], operation_id: str, ) -> ValidateClassificationAction: async def result_getter() -> Tuple[Any, Optional[Any], Optional[Any]]: result = await self._get_classification_validation_result_async( project_id=project_id, - project_type=project_type, + classifier_id=classifier_id, tag=tag, operation_id=operation_id, ) @@ -1832,6 +2119,7 @@ async def result_getter() -> Tuple[Any, Optional[Any], Optional[Any]]: response["projectId"] = project_id response["projectType"] = project_type + response["classifierId"] = classifier_id response["tag"] = tag response["operationId"] = operation_id return ValidateClassificationAction.model_validate(response) @@ -1840,6 +2128,7 @@ def _wait_for_create_validate_extraction_action( self, project_id: str, project_type: ProjectType, + extractor_id: Optional[str], tag: Optional[str], document_type_id: str, operation_id: str, @@ -1847,7 +2136,7 @@ def _wait_for_create_validate_extraction_action( def result_getter() -> Tuple[Any, Optional[Any], Optional[Any]]: result = self._get_extraction_validation_result( project_id=project_id, - project_type=project_type, + extractor_id=extractor_id, tag=tag, document_type_id=document_type_id, operation_id=operation_id, @@ -1866,6 +2155,7 @@ def result_getter() -> Tuple[Any, Optional[Any], Optional[Any]]: response["projectId"] = project_id response["projectType"] = project_type + response["extractorId"] = extractor_id response["tag"] = tag response["documentTypeId"] = document_type_id response["operationId"] = operation_id @@ -1875,6 +2165,7 @@ async def _wait_for_create_validate_extraction_action_async( self, project_id: str, project_type: ProjectType, + extractor_id: Optional[str], tag: Optional[str], document_type_id: str, operation_id: str, @@ -1882,7 +2173,7 @@ async def _wait_for_create_validate_extraction_action_async( async def result_getter_async() -> Tuple[Any, Optional[Any], Optional[Any]]: result = await self._get_extraction_validation_result_async( project_id=project_id, - project_type=project_type, + extractor_id=extractor_id, tag=tag, document_type_id=document_type_id, operation_id=operation_id, @@ -1901,6 +2192,7 @@ async def result_getter_async() -> Tuple[Any, Optional[Any], Optional[Any]]: response["projectId"] = project_id response["projectType"] = project_type + response["extractorId"] = extractor_id response["tag"] = tag response["documentTypeId"] = document_type_id response["operationId"] = operation_id @@ -1949,7 +2241,7 @@ def create_validate_classification_action( operation_id = self._start_classification_validation( project_id=classification_results[0].project_id, - project_type=classification_results[0].project_type, + classifier_id=classification_results[0].classifier_id, tag=classification_results[0].tag, action_title=action_title, action_priority=action_priority, @@ -1963,6 +2255,7 @@ def create_validate_classification_action( return self._wait_for_create_validate_classification_action( project_id=classification_results[0].project_id, project_type=classification_results[0].project_type, + classifier_id=classification_results[0].classifier_id, tag=classification_results[0].tag, operation_id=operation_id, ) @@ -1984,7 +2277,7 @@ async def create_validate_classification_action_async( operation_id = await self._start_classification_validation_async( project_id=classification_results[0].project_id, - project_type=classification_results[0].project_type, + classifier_id=classification_results[0].classifier_id, tag=classification_results[0].tag, action_title=action_title, action_priority=action_priority, @@ -1998,6 +2291,7 @@ async def create_validate_classification_action_async( return await self._wait_for_create_validate_classification_action_async( project_id=classification_results[0].project_id, project_type=classification_results[0].project_type, + classifier_id=classification_results[0].classifier_id, tag=classification_results[0].tag, operation_id=operation_id, ) @@ -2042,7 +2336,7 @@ def create_validate_extraction_action( """ operation_id = self._start_extraction_validation( project_id=extraction_response.project_id, - project_type=extraction_response.project_type, + extractor_id=extraction_response.extractor_id, tag=extraction_response.tag, document_type_id=extraction_response.document_type_id, action_title=action_title, @@ -2057,6 +2351,7 @@ def create_validate_extraction_action( return self._wait_for_create_validate_extraction_action( project_id=extraction_response.project_id, project_type=extraction_response.project_type, + extractor_id=extraction_response.extractor_id, tag=extraction_response.tag, document_type_id=extraction_response.document_type_id, operation_id=operation_id, @@ -2077,7 +2372,7 @@ async def create_validate_extraction_action_async( operation_id = ( await self._start_extraction_validation_async( project_id=extraction_response.project_id, - project_type=extraction_response.project_type, + extractor_id=extraction_response.extractor_id, tag=extraction_response.tag, document_type_id=extraction_response.document_type_id, action_title=action_title, @@ -2093,6 +2388,7 @@ async def create_validate_extraction_action_async( return await self._wait_for_create_validate_extraction_action_async( project_id=extraction_response.project_id, project_type=extraction_response.project_type, + extractor_id=extraction_response.extractor_id, tag=extraction_response.tag, document_type_id=extraction_response.document_type_id, operation_id=operation_id, @@ -2122,7 +2418,7 @@ def get_validate_classification_result( def result_getter() -> Tuple[str, None, Any]: result = self._get_classification_validation_result( project_id=validation_action.project_id, - project_type=validation_action.project_type, + classifier_id=validation_action.classifier_id, tag=validation_action.tag, operation_id=validation_action.operation_id, ) @@ -2137,6 +2433,7 @@ def result_getter() -> Tuple[str, None, Any]: for cr in response["validatedClassificationResults"]: cr["ProjectId"] = validation_action.project_id cr["ProjectType"] = validation_action.project_type + cr["ClassifierId"] = validation_action.classifier_id cr["Tag"] = validation_action.tag classification_results.append(ClassificationResult.model_validate(cr)) @@ -2153,7 +2450,7 @@ async def get_validate_classification_result_async( async def result_getter() -> Tuple[str, None, Any]: result = await self._get_classification_validation_result_async( project_id=validation_action.project_id, - project_type=validation_action.project_type, + classifier_id=validation_action.classifier_id, tag=validation_action.tag, operation_id=validation_action.operation_id, ) @@ -2168,6 +2465,7 @@ async def result_getter() -> Tuple[str, None, Any]: for cr in response["validatedClassificationResults"]: cr["ProjectId"] = validation_action.project_id cr["ProjectType"] = validation_action.project_type + cr["ClassifierId"] = validation_action.classifier_id cr["Tag"] = validation_action.tag classification_results.append(ClassificationResult.model_validate(cr)) @@ -2197,7 +2495,7 @@ def get_validate_extraction_result( def result_getter() -> Tuple[str, None, Any]: result = self._get_extraction_validation_result( project_id=validation_action.project_id, - project_type=validation_action.project_type, + extractor_id=validation_action.extractor_id, tag=validation_action.tag, document_type_id=validation_action.document_type_id, operation_id=validation_action.operation_id, @@ -2211,6 +2509,7 @@ def result_getter() -> Tuple[str, None, Any]: ) response["extractionResult"] = response.pop("validatedExtractionResults") response["projectId"] = validation_action.project_id + response["extractorId"] = validation_action.extractor_id response["tag"] = validation_action.tag response["documentTypeId"] = validation_action.document_type_id response["projectType"] = validation_action.project_type @@ -2229,7 +2528,7 @@ async def get_validate_extraction_result_async( async def result_getter() -> Tuple[str, None, Any]: result = await self._get_extraction_validation_result_async( project_id=validation_action.project_id, - project_type=validation_action.project_type, + extractor_id=validation_action.extractor_id, tag=validation_action.tag, document_type_id=validation_action.document_type_id, operation_id=validation_action.operation_id, @@ -2243,6 +2542,7 @@ async def result_getter() -> Tuple[str, None, Any]: ) response["extractionResult"] = response.pop("validatedExtractionResults") response["projectId"] = validation_action.project_id + response["extractorId"] = validation_action.extractor_id response["tag"] = validation_action.tag response["documentTypeId"] = validation_action.document_type_id response["projectType"] = validation_action.project_type @@ -2251,3 +2551,4 @@ async def result_getter() -> Tuple[str, None, Any]: return ExtractionResponseIXP.model_validate(response) return ExtractionResponse.model_validate(response) + return ExtractionResponse.model_validate(response) diff --git a/src/uipath/platform/documents/documents.py b/src/uipath/platform/documents/documents.py index 1a2fd57f7..a40e2f98e 100644 --- a/src/uipath/platform/documents/documents.py +++ b/src/uipath/platform/documents/documents.py @@ -117,6 +117,7 @@ class ExtractionResponse(BaseModel): extraction_result: ExtractionResult = Field(alias="extractionResult") project_id: str = Field(alias="projectId") project_type: ProjectType = Field(alias="projectType") + extractor_id: Optional[str] = Field(alias="extractorId") tag: Optional[str] document_type_id: str = Field(alias="documentTypeId") @@ -128,7 +129,10 @@ class ExtractionResponseIXP(ExtractionResponse): data_projection (List[FieldGroupValueProjection]): A simplified projection of the extracted data. """ - data_projection: List[FieldGroupValueProjection] = Field(alias="dataProjection") + data_projection: Optional[List[FieldGroupValueProjection]] = Field( + alias="dataProjection", + default=None, + ) class ValidationAction(BaseModel): @@ -159,12 +163,13 @@ class ValidationAction(BaseModel): class ValidateClassificationAction(ValidationAction): """A model representing a validation action for document classification.""" - pass + classifier_id: Optional[str] = Field(alias="classifierId") class ValidateExtractionAction(ValidationAction): """A model representing a validation action for document extraction.""" + extractor_id: Optional[str] = Field(alias="extractorId") document_type_id: str = Field(alias="documentTypeId") validated_extraction_result: Optional[ExtractionResult] = Field( alias="validatedExtractionResults", default=None @@ -233,6 +238,7 @@ class ClassificationResult(BaseModel): classifier_name: str = Field(alias="ClassifierName") project_id: str = Field(alias="ProjectId") project_type: ProjectType = Field(alias="ProjectType") + classifier_id: Optional[str] = Field(alias="ClassifierId") tag: Optional[str] = Field(alias="Tag") diff --git a/tests/cli/test_hitl.py b/tests/cli/test_hitl.py index 00dfcaafb..e0665ef9f 100644 --- a/tests/cli/test_hitl.py +++ b/tests/cli/test_hitl.py @@ -56,10 +56,7 @@ StartExtractionValidationResponse, ValidateExtractionAction, ) -from uipath.platform.orchestrator import ( - Job, - JobErrorInfo, -) +from uipath.platform.orchestrator import Job, JobErrorInfo from uipath.platform.resume_triggers import ( PropertyName, TriggerMarker, @@ -690,6 +687,7 @@ async def test_read_ixp_vs_escalation_trigger_successful( action_status=TaskStatus.COMPLETED.name, project_id=project_id, project_type=ProjectType.IXP, + extractor_id=None, tag=tag, operation_id=operation_id, document_type_id="test-doc-type", @@ -735,6 +733,7 @@ async def test_read_ixp_vs_escalation_trigger_pending( action_status=TaskStatus.PENDING.name, project_id=project_id, project_type=ProjectType.IXP, + extractor_id=None, tag=tag, operation_id=operation_id, document_type_id="test-doc-type", @@ -776,6 +775,7 @@ async def test_read_ixp_vs_escalation_trigger_unassigned( action_status=TaskStatus.UNASSIGNED.name, project_id=project_id, project_type=ProjectType.IXP, + extractor_id=None, tag=tag, operation_id=operation_id, document_type_id="test-doc-type", @@ -1203,6 +1203,7 @@ async def test_create_resume_trigger_document_extraction_validation( extraction_result=extraction_result, project_id=project_id, project_type=ProjectType.IXP, + extractor_id=None, tag=tag, document_type_id="doc-type-123", data_projection=[ diff --git a/tests/sdk/services/test_documents_service.py b/tests/sdk/services/test_documents_service.py index 7f033b221..1a6543afb 100644 --- a/tests/sdk/services/test_documents_service.py +++ b/tests/sdk/services/test_documents_service.py @@ -84,10 +84,11 @@ def extraction_validation_action_response_completed( class TestDocumentsService: @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.parametrize( - "tag,project_name,project_type,file,file_path,error", + "tag,version,project_name,project_type,file,file_path,error", [ ( "Production", + None, "TestProject", ProjectType.MODERN, None, @@ -96,6 +97,16 @@ class TestDocumentsService: ), ( "Production", + 4, + "TestProject", + ProjectType.MODERN, + b"something", + None, + "Exactly one of `tag, version` must be provided", + ), + ( + "Production", + None, "TestProject", ProjectType.MODERN, b"something", @@ -105,12 +116,14 @@ class TestDocumentsService: ( "Production", None, + None, ProjectType.PRETRAINED, b"something", None, "`tag` must not be provided", ), ( + None, None, "TestProject", ProjectType.PRETRAINED, @@ -119,6 +132,7 @@ class TestDocumentsService: "`project_name` must not be provided", ), ( + None, None, None, ProjectType.PRETRAINED, @@ -134,6 +148,7 @@ async def test_classify_with_invalid_parameters( service: DocumentsService, mode: str, tag, + version, project_name, project_type, file, @@ -148,6 +163,7 @@ async def test_classify_with_invalid_parameters( if mode == "async": await service.classify_async( tag=tag, + version=version, project_name=project_name, project_type=project_type, file=file, @@ -156,6 +172,7 @@ async def test_classify_with_invalid_parameters( else: service.classify( tag=tag, + version=version, project_name=project_name, project_type=project_type, file=file, @@ -178,12 +195,15 @@ async def test_extract_with_classification_result_predefined( # ARRANGE project_id = str(UUID(int=0)) document_id = str(uuid4()) - document_type_id = str(uuid4()) + document_type_id = "receipts" operation_id = str(uuid4()) classification_response["classificationResults"][0]["ProjectId"] = project_id classification_response["classificationResults"][0]["ProjectType"] = ( ProjectType.PRETRAINED.value ) + classification_response["classificationResults"][0]["ClassifierId"] = ( + "ml-classification" + ) classification_response["classificationResults"][0]["Tag"] = None classification_response["classificationResults"][0]["DocumentId"] = document_id classification_response["classificationResults"][0]["DocumentTypeId"] = ( @@ -222,13 +242,14 @@ async def test_extract_with_classification_result_predefined( # ASSERT modern_extraction_response["projectId"] = project_id modern_extraction_response["projectType"] = ProjectType.PRETRAINED + modern_extraction_response["extractorId"] = document_type_id modern_extraction_response["tag"] = None modern_extraction_response["documentTypeId"] = document_type_id assert response.model_dump() == modern_extraction_response @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - async def test_extract_with_classification_result_modern( + async def test_extract_with_classification_result_modern_with_version( self, httpx_mock: HTTPXMock, service: DocumentsService, @@ -243,11 +264,17 @@ async def test_extract_with_classification_result_modern( project_id = str(uuid4()) document_id = str(uuid4()) document_type_id = str(uuid4()) + classifier_id = "classifier_2" + version = 2 + extractor_id = "extractor_2" classification_response["classificationResults"][0]["ProjectId"] = project_id classification_response["classificationResults"][0]["ProjectType"] = ( ProjectType.MODERN.value ) - classification_response["classificationResults"][0]["Tag"] = "Production" + classification_response["classificationResults"][0]["ClassifierId"] = ( + classifier_id + ) + classification_response["classificationResults"][0]["Tag"] = None classification_response["classificationResults"][0]["DocumentId"] = document_id classification_response["classificationResults"][0]["DocumentTypeId"] = ( document_type_id @@ -256,19 +283,115 @@ async def test_extract_with_classification_result_modern( classification_response["classificationResults"][0] ) + operation_id = str(uuid4()) + 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}/classifiers/{classifier_id}?api-version=1.1", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", }, json={ - "tags": [ - {"name": "Staging"}, - {"name": "Production"}, + "id": classifier_id, + "projectVersion": version, + }, + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "extractors": [ + { + "id": str(uuid4()), + "projectVersion": 1, + "documentTypeId": str(uuid4()), + }, + { + "id": extractor_id, + "projectVersion": version, + "documentTypeId": document_type_id, + }, + { + "id": str(uuid4()), + "projectVersion": 3, + "documentTypeId": str(uuid4()), + }, ] }, ) + + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_json={"documentId": document_id}, + json={"operationId": operation_id}, + ) + + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={"status": status, "result": modern_extraction_response}, + ) + + # ACT + if mode == "async": + response = await service.extract_async( + classification_result=classification_result + ) + else: + response = service.extract(classification_result=classification_result) + + # ASSERT + modern_extraction_response["projectId"] = project_id + modern_extraction_response["projectType"] = ProjectType.MODERN + modern_extraction_response["extractorId"] = extractor_id + modern_extraction_response["tag"] = None + modern_extraction_response["documentTypeId"] = document_type_id + assert response.model_dump() == modern_extraction_response + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_extract_with_classification_result_modern_with_tag( + self, + httpx_mock: HTTPXMock, + service: DocumentsService, + base_url: str, + org: str, + tenant: str, + mode: str, + classification_response: dict, # type: ignore + modern_extraction_response: dict, # type: ignore + ): + # ARRANGE + project_id = str(uuid4()) + document_id = str(uuid4()) + document_type_id = str(uuid4()) + classification_response["classificationResults"][0]["ProjectId"] = project_id + classification_response["classificationResults"][0]["ProjectType"] = ( + ProjectType.MODERN.value + ) + classification_response["classificationResults"][0]["ClassifierId"] = None + classification_response["classificationResults"][0]["Tag"] = "Production" + classification_response["classificationResults"][0]["DocumentId"] = document_id + classification_response["classificationResults"][0]["DocumentTypeId"] = ( + document_type_id + ) + classification_result = ClassificationResult.model_validate( + classification_response["classificationResults"][0] + ) + 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", @@ -299,6 +422,7 @@ async def test_extract_with_classification_result_modern( # ASSERT modern_extraction_response["projectId"] = project_id modern_extraction_response["projectType"] = ProjectType.MODERN + modern_extraction_response["extractorId"] = None modern_extraction_response["tag"] = "Production" modern_extraction_response["documentTypeId"] = document_type_id assert response.model_dump() == modern_extraction_response @@ -373,6 +497,9 @@ async def test_classify_predefined( classification_response["classificationResults"][0]["ProjectType"] = ( ProjectType.PRETRAINED.value ) + classification_response["classificationResults"][0]["ClassifierId"] = ( + "ml-classification" + ) classification_response["classificationResults"][0]["Tag"] = None assert ( response[0].model_dump() @@ -381,7 +508,7 @@ async def test_classify_predefined( @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - async def test_classify_modern( + async def test_classify_with_version_classifier_not_found( self, httpx_mock: HTTPXMock, service: DocumentsService, @@ -389,11 +516,11 @@ async def test_classify_modern( org: str, tenant: str, mode: str, - classification_response: dict, # type: ignore ): # ARRANGE project_id = str(uuid4()) document_id = str(uuid4()) + version = 5 httpx_mock.add_response( url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", @@ -411,14 +538,89 @@ async def test_classify_modern( ) 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}/digitization/start?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_files={"File": b"test content"}, + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={"status": "Succeeded", "result": {}}, + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/classifiers?api-version=1.1", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", }, json={ - "tags": [ - {"name": "Production"}, + "classifiers": [ + {"id": "classifier_1", "projectVersion": 1}, + {"id": "classifier_2", "projectVersion": 2}, + {"id": "classifier_3", "projectVersion": 3}, + ] + }, + ) + + # ACT & ASSERT + with pytest.raises( + ValueError, + match=f"Classifier for version '{version}' not found.", + ): + if mode == "async": + await service.classify_async( + version=version, + project_name="TestProject", + project_type=ProjectType.MODERN, + file=b"test content", + ) + else: + service.classify( + version=version, + project_name="TestProject", + project_type=ProjectType.MODERN, + file=b"test content", + ) + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_classify_modern_with_version( + 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()) + version = 2 + classifier_id = "classifier_2" + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "projects": [ + {"id": str(uuid4()), "name": "OtherProject"}, + {"id": project_id, "name": "TestProject"}, + {"id": str(uuid4()), "name": "AnotherProject"}, ] }, ) @@ -441,9 +643,23 @@ async def test_classify_modern( json={"status": "Succeeded", "result": {}}, ) - 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}/classifiers?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "classifiers": [ + {"id": "classifier_1", "projectVersion": 1}, + {"id": classifier_id, "projectVersion": version}, + {"id": "classifier_3", "projectVersion": 3}, + ] + }, + ) + + 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", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -451,8 +667,9 @@ async def test_classify_modern( match_json={"documentId": document_id}, 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}/classifiers/{classifier_id}/classification/result/{operation_id}?api-version=1.1", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -463,14 +680,14 @@ async def test_classify_modern( # ACT if mode == "async": response = await service.classify_async( - tag="Production", + version=version, project_name="TestProject", project_type=ProjectType.MODERN, file=b"test content", ) else: response = service.classify( - tag="Production", + version=version, project_name="TestProject", project_type=ProjectType.MODERN, file=b"test content", @@ -481,7 +698,10 @@ async def test_classify_modern( classification_response["classificationResults"][0]["ProjectType"] = ( ProjectType.MODERN.value ) - classification_response["classificationResults"][0]["Tag"] = "Production" + classification_response["classificationResults"][0]["ClassifierId"] = ( + classifier_id + ) + classification_response["classificationResults"][0]["Tag"] = None assert ( response[0].model_dump() == classification_response["classificationResults"][0] @@ -489,81 +709,209 @@ async def test_classify_modern( @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - @pytest.mark.parametrize( - "tag,project_name,file,file_path,classification_result,project_type,document_type_name, error", - [ - ( - None, - None, - None, - None, - None, - None, - None, - "`classification_result` must be provided", - ), - ( - "live", - "TestProject", - None, - None, - None, - None, - None, - "`classification_result` must be provided", - ), - ( - "live", - "TestProject", - None, - None, - None, - ProjectType.IXP, - None, - "`classification_result` must be provided", - ), - ( - "live", - "TestProject", - b"something", - None, - None, - ProjectType.MODERN, - None, - "`document_type_name` must be provided", - ), - ( - "live", - "TestProject", - b"something", - None, - "dummy classification result", - ProjectType.MODERN, - "dummy doctype", - "`classification_result` must not be provided", - ), - ( - None, - "TestProject", - b"something", - None, - None, - ProjectType.MODERN, - "dummy doctype", - "`tag` must be provided", - ), - ( - "live", - "TestProject", - b"something", - "path/to/file.pdf", - None, - ProjectType.MODERN, - "dummy doctype", + async def test_classify_modern_with_tag( + 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()) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "projects": [ + {"id": str(uuid4()), "name": "OtherProject"}, + {"id": project_id, "name": "TestProject"}, + {"id": str(uuid4()), "name": "AnotherProject"}, + ] + }, + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "tags": [ + {"name": "Production"}, + ] + }, + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_files={"File": b"test content"}, + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={"status": "Succeeded", "result": {}}, + ) + + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_json={"documentId": document_id}, + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={"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 + classification_response["classificationResults"][0]["ProjectId"] = project_id + classification_response["classificationResults"][0]["ProjectType"] = ( + ProjectType.MODERN.value + ) + classification_response["classificationResults"][0]["ClassifierId"] = None + classification_response["classificationResults"][0]["Tag"] = "Production" + assert ( + response[0].model_dump() + == classification_response["classificationResults"][0] + ) + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + @pytest.mark.parametrize( + "tag,version,project_name,file,file_path,classification_result,project_type,document_type_name, error", + [ + ( + None, + None, + None, + None, + None, + None, + None, + None, + "`classification_result` must be provided", + ), + ( + "live", + None, + "TestProject", + None, + None, + None, + None, + None, + "`classification_result` must be provided", + ), + ( + "live", + None, + "TestProject", + None, + None, + None, + ProjectType.IXP, + None, + "`classification_result` must be provided", + ), + ( + "live", + None, + "TestProject", + b"something", + None, + None, + ProjectType.MODERN, + None, + "`document_type_name` must be provided", + ), + ( + "live", + None, + "TestProject", + b"something", + None, + "dummy classification result", + ProjectType.MODERN, + "dummy doctype", + "`classification_result` must not be provided", + ), + ( + None, + None, + "TestProject", + b"something", + None, + None, + ProjectType.MODERN, + "dummy doctype", + "Exactly one of `version, tag` must be provided", + ), + ( + "live", + None, + "TestProject", + b"something", + "path/to/file.pdf", + None, + ProjectType.MODERN, + "dummy doctype", "Exactly one of `file, file_path` must be provided", ), ( "live", + 4, + "TestProject", + b"something", + None, + None, + ProjectType.MODERN, + "dummy doctype", + "Exactly one of `version, tag` must be provided", + ), + ( + "live", + None, None, b"something", None, @@ -579,6 +927,7 @@ async def test_extract_with_invalid_parameters( service: DocumentsService, mode: str, tag, + version, project_name, file, file_path, @@ -592,6 +941,7 @@ async def test_extract_with_invalid_parameters( if mode == "async": await service.extract_async( tag=tag, + version=version, project_name=project_name, project_type=project_type, file=file, @@ -602,6 +952,7 @@ async def test_extract_with_invalid_parameters( else: service.extract( tag=tag, + version=version, project_name=project_name, project_type=project_type, file=file, @@ -612,23 +963,363 @@ async def test_extract_with_invalid_parameters( @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - async def test_extract_ixp( + async def test_extract_ixp_with_version( + self, + httpx_mock: HTTPXMock, + service: DocumentsService, + base_url: str, + org: str, + tenant: str, + ixp_extraction_response: dict, # type: ignore + mode: str, + ): + project_id = str(uuid4()) + document_id = str(uuid4()) + operation_id = str(uuid4()) + extractor_id = "ixp_3" + version = 3 + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=IXP", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "projects": [ + {"id": str(uuid4()), "name": "OtherProject"}, + {"id": project_id, "name": "TestProjectIXP"}, + {"id": str(uuid4()), "name": "AnotherProject"}, + ] + }, + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_files={"File": b"test content"}, + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={"status": "Succeeded", "result": {}}, + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/extractors?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "extractors": [ + { + "id": extractor_id, + "projectVersion": version, + "documentTypeId": str(UUID(int=0)), + }, + { + "id": "ixp_2", + "projectVersion": 2, + "documentTypeId": str(UUID(int=0)), + }, + { + "id": "ixp_1", + "projectVersion": 1, + "documentTypeId": str(UUID(int=0)), + }, + ] + }, + ) + + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_json={"documentId": document_id}, + json={"operationId": operation_id}, + ) + + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={"status": "Succeeded", "result": ixp_extraction_response}, + ) + + # ACT + if mode == "async": + response = await service.extract_async( + project_name="TestProjectIXP", + project_type=ProjectType.IXP, + version=version, + file=b"test content", + ) + else: + response = service.extract( + project_name="TestProjectIXP", + project_type=ProjectType.IXP, + version=version, + file=b"test content", + ) + + # ASSERT + expected_response = ixp_extraction_response + expected_response["projectId"] = project_id + expected_response["projectType"] = ProjectType.IXP.value + expected_response["extractorId"] = extractor_id + expected_response["tag"] = None + expected_response["documentTypeId"] = str(UUID(int=0)) + assert response.model_dump() == expected_response + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_extract_ixp_with_tag( + self, + httpx_mock: HTTPXMock, + service: DocumentsService, + base_url: str, + org: str, + tenant: str, + ixp_extraction_response: dict, # type: ignore + mode: str, + ): + # ARRANGE + project_id = str(uuid4()) + document_id = str(uuid4()) + operation_id = str(uuid4()) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=IXP", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "projects": [ + {"id": str(uuid4()), "name": "OtherProject"}, + {"id": project_id, "name": "TestProjectIXP"}, + {"id": str(uuid4()), "name": "AnotherProject"}, + ] + }, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "tags": [ + {"name": "draft"}, + {"name": "live"}, + {"name": "production"}, + ] + }, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_files={"File": b"test content"}, + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_json={"documentId": document_id}, + json={"operationId": operation_id}, + ) + + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={"status": "Succeeded", "result": ixp_extraction_response}, + ) + + # ACT + if mode == "async": + response = await service.extract_async( + project_name="TestProjectIXP", + project_type=ProjectType.IXP, + tag="live", + file=b"test content", + ) + else: + response = service.extract( + project_name="TestProjectIXP", + project_type=ProjectType.IXP, + tag="live", + file=b"test content", + ) + + # ASSERT + expected_response = ixp_extraction_response + expected_response["projectId"] = project_id + expected_response["projectType"] = ProjectType.IXP.value + expected_response["extractorId"] = None + expected_response["tag"] = "live" + expected_response["documentTypeId"] = str(UUID(int=0)) + assert response.model_dump() == expected_response + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_extract_predefined( + self, + httpx_mock: HTTPXMock, + service: DocumentsService, + base_url: str, + org: str, + tenant: str, + modern_extraction_response: dict, # type: ignore + mode: str, + ): + # ARRANGE + project_id = str(UUID(int=0)) + document_id = str(uuid4()) + document_type_id = "receipts" + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_files={"File": b"test content"}, + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={"status": "Succeeded", "result": {}}, + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/document-types?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "documentTypes": [ + {"id": str(uuid4()), "name": "Receipt"}, + {"id": document_type_id, "name": "Invoice"}, + {"id": str(uuid4()), "name": "Contract"}, + ] + }, + ) + + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + match_json={"documentId": document_id}, + json={"operationId": operation_id}, + ) + + 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", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={"status": status, "result": modern_extraction_response}, + ) + + # ACT + if mode == "async": + response = await service.extract_async( + project_type=ProjectType.PRETRAINED, + file=b"test content", + document_type_name="Invoice", + ) + else: + response = service.extract( + project_type=ProjectType.PRETRAINED, + file=b"test content", + document_type_name="Invoice", + ) + + # ASSERT + expected_response = modern_extraction_response + expected_response["projectId"] = project_id + expected_response["projectType"] = ProjectType.PRETRAINED.value + expected_response["extractorId"] = document_type_id + expected_response["tag"] = None + expected_response["documentTypeId"] = document_type_id + assert response.model_dump() == expected_response + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_extract_modern_with_version_extractor_not_found( self, httpx_mock: HTTPXMock, service: DocumentsService, base_url: str, org: str, tenant: str, - ixp_extraction_response: dict, # type: ignore mode: str, ): # ARRANGE project_id = str(uuid4()) document_id = str(uuid4()) - operation_id = str(uuid4()) + version = 5 + document_type_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=1.1&type=Modern", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -636,25 +1327,12 @@ async def test_extract_ixp( json={ "projects": [ {"id": str(uuid4()), "name": "OtherProject"}, - {"id": project_id, "name": "TestProjectIXP"}, + {"id": project_id, "name": "TestProjectModern"}, {"id": str(uuid4()), "name": "AnotherProject"}, ] }, ) - httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/tags?api-version=1.1", - status_code=200, - match_headers={ - "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", - }, - json={ - "tags": [ - {"name": "draft"}, - {"name": "live"}, - {"name": "production"}, - ] - }, - ) + httpx_mock.add_response( url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", status_code=200, @@ -664,6 +1342,7 @@ async def test_extract_ixp( match_files={"File": b"test content"}, 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", status_code=200, @@ -672,68 +1351,74 @@ async def test_extract_ixp( }, 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", - status_code=200, - match_headers={ - "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", - }, - match_json={"documentId": document_id}, - json={"operationId": operation_id}, - ) 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}/document-types?api-version=1.1", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", }, - 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", - status_code=200, - match_headers={ - "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + json={ + "documentTypes": [ + {"id": str(uuid4()), "name": "Receipt"}, + {"id": document_type_id, "name": "Invoice"}, + {"id": str(uuid4()), "name": "Contract"}, + ] }, - 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}/extractors?api-version=1.1", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", }, - json={"status": "Succeeded", "result": ixp_extraction_response}, + json={ + "extractors": [ + { + "id": str(uuid4()), + "projectVersion": 1, + "documentTypeId": str(uuid4()), + }, + { + "id": str(uuid4()), + "projectVersion": 2, + "documentTypeId": str(uuid4()), + }, + { + "id": str(uuid4()), + "projectVersion": 3, + "documentTypeId": str(uuid4()), + }, + ] + }, ) - # ACT - if mode == "async": - response = await service.extract_async( - project_name="TestProjectIXP", - project_type=ProjectType.IXP, - tag="live", - file=b"test content", - ) - else: - response = service.extract( - project_name="TestProjectIXP", - project_type=ProjectType.IXP, - tag="live", - file=b"test content", - ) - - # ASSERT - expected_response = ixp_extraction_response - expected_response["projectId"] = project_id - expected_response["projectType"] = ProjectType.IXP.value - expected_response["tag"] = "live" - expected_response["documentTypeId"] = str(UUID(int=0)) - assert response.model_dump() == expected_response + # ACT & ASSERT + with pytest.raises( + ValueError, + match=f"Extractor for version '{version}' and document type id '{document_type_id}' not found.", + ): + if mode == "async": + await service.extract_async( + project_name="TestProjectModern", + version=version, + file=b"test content", + project_type=ProjectType.MODERN, + document_type_name="Invoice", + ) + else: + service.extract( + project_name="TestProjectModern", + version=version, + file=b"test content", + project_type=ProjectType.MODERN, + document_type_name="Invoice", + ) @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - async def test_extract_predefined( + async def test_extract_modern_with_version( self, httpx_mock: HTTPXMock, service: DocumentsService, @@ -744,10 +1429,27 @@ async def test_extract_predefined( mode: str, ): # ARRANGE - project_id = str(UUID(int=0)) - document_id = str(uuid4()) + project_id = str(uuid4()) document_type_id = str(uuid4()) + document_id = str(uuid4()) operation_id = str(uuid4()) + extractor_id = str(uuid4()) + version = 2 + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/du_/api/framework/projects?api-version=1.1&type=Modern", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "projects": [ + {"id": str(uuid4()), "name": "OtherProject"}, + {"id": project_id, "name": "TestProjectModern"}, + {"id": str(uuid4()), "name": "AnotherProject"}, + ] + }, + ) httpx_mock.add_response( url=f"{base_url}{org}{tenant}/du_/api/framework/projects/{project_id}/digitization/start?api-version=1.1", @@ -783,7 +1485,34 @@ 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?api-version=1.1", + status_code=200, + match_headers={ + "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", + }, + json={ + "extractors": [ + { + "id": str(uuid4()), + "projectVersion": 1, + "documentTypeId": str(uuid4()), + }, + { + "id": extractor_id, + "projectVersion": version, + "documentTypeId": document_type_id, + }, + { + "id": str(uuid4()), + "projectVersion": 3, + "documentTypeId": str(uuid4()), + }, + ] + }, + ) + + 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", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -795,7 +1524,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/{extractor_id}/extraction/result/{operation_id}?api-version=1.1", status_code=200, match_headers={ "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents", @@ -806,28 +1535,33 @@ async def test_extract_predefined( # ACT if mode == "async": response = await service.extract_async( - project_type=ProjectType.PRETRAINED, + project_name="TestProjectModern", + version=version, file=b"test content", + project_type=ProjectType.MODERN, document_type_name="Invoice", ) else: response = service.extract( - project_type=ProjectType.PRETRAINED, + project_name="TestProjectModern", + version=version, file=b"test content", + project_type=ProjectType.MODERN, document_type_name="Invoice", ) # ASSERT expected_response = modern_extraction_response expected_response["projectId"] = project_id - expected_response["projectType"] = ProjectType.PRETRAINED.value + expected_response["projectType"] = ProjectType.MODERN.value + expected_response["extractorId"] = extractor_id expected_response["tag"] = None expected_response["documentTypeId"] = document_type_id assert response.model_dump() == expected_response @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - async def test_extract_modern( + async def test_extract_modern_with_tag( self, httpx_mock: HTTPXMock, service: DocumentsService, @@ -949,6 +1683,7 @@ async def test_extract_modern( expected_response = modern_extraction_response expected_response["projectId"] = project_id expected_response["projectType"] = ProjectType.MODERN.value + expected_response["extractorId"] = None expected_response["tag"] = "Production" expected_response["documentTypeId"] = document_type_id assert response.model_dump() == expected_response @@ -1126,26 +1861,115 @@ async def test_extract_with_wrong_tag( json={"tags": [{"name": "staging"}]}, ) - # ACT & ASSERT - with pytest.raises(ValueError, match="Tag 'live' not found."): - if mode == "async": - await service.extract_async( - project_name="TestProject", - project_type=ProjectType.IXP, - tag="live", - file=b"test content", - ) - else: - service.extract( - project_name="TestProject", - project_type=ProjectType.IXP, - tag="live", - file=b"test content", - ) + # ACT & ASSERT + with pytest.raises(ValueError, match="Tag 'live' not found."): + if mode == "async": + await service.extract_async( + project_name="TestProject", + project_type=ProjectType.IXP, + tag="live", + file=b"test content", + ) + else: + service.extract( + project_name="TestProject", + project_type=ProjectType.IXP, + tag="live", + file=b"test content", + ) + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_create_validate_classification_action_pretrained( + self, + httpx_mock: HTTPXMock, + service: DocumentsService, + base_url: str, + org: str, + tenant: str, + classification_response: dict, # type: ignore + create_validation_action_response: dict, # type: ignore + mode: str, + ): + # ARRANGE + project_id = str(UUID(int=0)) + operation_id = str(uuid4()) + tag = None + action_title = "TestAction" + action_priority = ActionPriority.LOW + action_catalog = "TestCatalog" + action_folder = "TestFolder" + storage_bucket_name = "TestBucket" + storage_bucket_directory_path = "Test/Directory/Path" + + classification_result = classification_response["classificationResults"][0] + classification_result["ProjectId"] = project_id + classification_result["ProjectType"] = ProjectType.PRETRAINED.value + classification_result["ClassifierId"] = "ml-classification" + classification_result["Tag"] = tag + classification_result = ClassificationResult.model_validate( + classification_result + ) + + 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", + status_code=200, + match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, + match_json={ + "actionTitle": action_title, + "actionPriority": action_priority, + "actionCatalog": action_catalog, + "actionFolder": action_folder, + "storageBucketName": storage_bucket_name, + "storageBucketDirectoryPath": storage_bucket_directory_path, + "classificationResults": [ + classification_result.model_dump(), + ], + "documentId": classification_result.document_id, + }, + json={"operationId": operation_id}, + ) + + 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", + status_code=200, + match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, + json={"status": "Succeeded", "result": create_validation_action_response}, + ) + + # ACT + if mode == "async": + response = await service.create_validate_classification_action_async( + action_title=action_title, + action_priority=action_priority, + action_catalog=action_catalog, + action_folder=action_folder, + storage_bucket_name=storage_bucket_name, + storage_bucket_directory_path=storage_bucket_directory_path, + classification_results=[classification_result], + ) + else: + response = service.create_validate_classification_action( + action_title=action_title, + action_priority=action_priority, + action_catalog=action_catalog, + action_folder=action_folder, + storage_bucket_name=storage_bucket_name, + storage_bucket_directory_path=storage_bucket_directory_path, + classification_results=[classification_result], + ) + + # ASSERT + create_validation_action_response["projectId"] = project_id + create_validation_action_response["projectType"] = ProjectType.PRETRAINED.value + create_validation_action_response["classifierId"] = "ml-classification" + create_validation_action_response["tag"] = tag + create_validation_action_response["operationId"] = operation_id + assert response.model_dump() == create_validation_action_response @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - async def test_create_validate_classification_action_pretrained( + async def test_create_validate_classification_action_modern_with_version( self, httpx_mock: HTTPXMock, service: DocumentsService, @@ -1157,11 +1981,11 @@ async def test_create_validate_classification_action_pretrained( mode: str, ): # ARRANGE - project_id = str(UUID(int=0)) + project_id = str(uuid4()) operation_id = str(uuid4()) - tag = None + classifier_id = str(uuid4()) action_title = "TestAction" - action_priority = ActionPriority.LOW + action_priority = ActionPriority.HIGH action_catalog = "TestCatalog" action_folder = "TestFolder" storage_bucket_name = "TestBucket" @@ -1169,14 +1993,15 @@ async def test_create_validate_classification_action_pretrained( classification_result = classification_response["classificationResults"][0] classification_result["ProjectId"] = project_id - classification_result["ProjectType"] = ProjectType.PRETRAINED.value - classification_result["Tag"] = tag + classification_result["ProjectType"] = ProjectType.MODERN.value + classification_result["ClassifierId"] = classifier_id + classification_result["Tag"] = None classification_result = ClassificationResult.model_validate( classification_result ) 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/{classifier_id}/validation/start?api-version=1.1", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, match_json={ @@ -1195,7 +2020,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/{classifier_id}/validation/result/{operation_id}?api-version=1.1", status_code=200, match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, json={"status": "Succeeded", "result": create_validation_action_response}, @@ -1225,14 +2050,15 @@ async def test_create_validate_classification_action_pretrained( # ASSERT create_validation_action_response["projectId"] = project_id - create_validation_action_response["projectType"] = ProjectType.PRETRAINED.value - create_validation_action_response["tag"] = tag + create_validation_action_response["projectType"] = ProjectType.MODERN.value + create_validation_action_response["classifierId"] = classifier_id + create_validation_action_response["tag"] = None create_validation_action_response["operationId"] = operation_id assert response.model_dump() == create_validation_action_response @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - async def test_create_validate_classification_action_modern( + async def test_create_validate_classification_action_modern_with_tag( self, httpx_mock: HTTPXMock, service: DocumentsService, @@ -1257,6 +2083,7 @@ async def test_create_validate_classification_action_modern( classification_result = classification_response["classificationResults"][0] classification_result["ProjectId"] = project_id classification_result["ProjectType"] = ProjectType.MODERN.value + classification_result["ClassifierId"] = None classification_result["Tag"] = tag classification_result = ClassificationResult.model_validate( classification_result @@ -1312,6 +2139,7 @@ async def test_create_validate_classification_action_modern( # ASSERT create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = ProjectType.MODERN.value + create_validation_action_response["classifierId"] = None create_validation_action_response["tag"] = tag create_validation_action_response["operationId"] = operation_id assert response.model_dump() == create_validation_action_response @@ -1365,12 +2193,14 @@ async def test_create_validate_classification_action_with_optional_params_omitte # ARRANGE project_id = str(UUID(int=0)) operation_id = str(uuid4()) + classifier_id = "ml-classification" tag = None action_title = "TestAction" classification_result = classification_response["classificationResults"][0] classification_result["ProjectId"] = project_id classification_result["ProjectType"] = ProjectType.PRETRAINED.value + classification_result["ClassifierId"] = classifier_id classification_result["Tag"] = tag classification_result = ClassificationResult.model_validate( classification_result @@ -1417,6 +2247,7 @@ async def test_create_validate_classification_action_with_optional_params_omitte # ASSERT create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = ProjectType.PRETRAINED.value + create_validation_action_response["classifierId"] = classifier_id create_validation_action_response["tag"] = tag create_validation_action_response["operationId"] = operation_id assert response.model_dump() == create_validation_action_response @@ -1437,7 +2268,7 @@ async def test_create_validate_extraction_action_pretrained( # ARRANGE project_id = str(UUID(int=0)) operation_id = str(uuid4()) - document_type_id = str(UUID(int=0)) + document_type_id = "invoices" tag = None action_title = "TestAction" action_priority = ActionPriority.MEDIUM @@ -1475,6 +2306,7 @@ async def test_create_validate_extraction_action_pretrained( modern_extraction_response["projectId"] = project_id modern_extraction_response["projectType"] = ProjectType.PRETRAINED.value + modern_extraction_response["extractorId"] = document_type_id modern_extraction_response["tag"] = tag modern_extraction_response["documentTypeId"] = document_type_id @@ -1507,6 +2339,100 @@ async def test_create_validate_extraction_action_pretrained( # ASSERT create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = ProjectType.PRETRAINED.value + create_validation_action_response["extractorId"] = document_type_id + create_validation_action_response["tag"] = tag + create_validation_action_response["documentTypeId"] = document_type_id + create_validation_action_response["operationId"] = operation_id + create_validation_action_response["validatedExtractionResults"] = None + create_validation_action_response["dataProjection"] = None + assert response.model_dump() == create_validation_action_response + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_create_validate_extraction_action_ixp_with_version( + self, + httpx_mock: HTTPXMock, + service: DocumentsService, + base_url: str, + org: str, + tenant: str, + ixp_extraction_response: dict, # type: ignore + create_validation_action_response: dict, # type: ignore + mode: str, + ): + # ARRANGE + project_id = str(uuid4()) + operation_id = str(uuid4()) + document_type_id = str(UUID(int=0)) + extractor_id = "ixp-4" + tag = None + action_title = "TestAction" + action_priority = ActionPriority.LOW + action_catalog = "TestCatalog" + action_folder = "TestFolder" + storage_bucket_name = "TestBucket" + 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", + status_code=200, + match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, + match_json={ + "extractionResult": ixp_extraction_response["extractionResult"], + "documentId": ixp_extraction_response["extractionResult"]["DocumentId"], + "actionTitle": action_title, + "actionPriority": action_priority, + "actionCatalog": action_catalog, + "actionFolder": action_folder, + "storageBucketName": storage_bucket_name, + "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", + status_code=200, + match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, + json={"status": "Succeeded", "result": create_validation_action_response}, + ) + + ixp_extraction_response["projectId"] = project_id + ixp_extraction_response["projectType"] = ProjectType.IXP.value + ixp_extraction_response["extractorId"] = extractor_id + ixp_extraction_response["tag"] = tag + ixp_extraction_response["documentTypeId"] = document_type_id + + # ACT + if mode == "async": + response = await service.create_validate_extraction_action_async( + action_title=action_title, + action_priority=action_priority, + action_catalog=action_catalog, + action_folder=action_folder, + storage_bucket_name=storage_bucket_name, + storage_bucket_directory_path=storage_bucket_directory_path, + extraction_response=ExtractionResponse.model_validate( + ixp_extraction_response + ), + ) + else: + response = service.create_validate_extraction_action( + action_title=action_title, + action_priority=action_priority, + action_catalog=action_catalog, + action_folder=action_folder, + storage_bucket_name=storage_bucket_name, + storage_bucket_directory_path=storage_bucket_directory_path, + extraction_response=ExtractionResponse.model_validate( + ixp_extraction_response + ), + ) + + # ASSERT + create_validation_action_response["projectId"] = project_id + create_validation_action_response["projectType"] = ProjectType.IXP.value + create_validation_action_response["extractorId"] = extractor_id create_validation_action_response["tag"] = tag create_validation_action_response["documentTypeId"] = document_type_id create_validation_action_response["operationId"] = operation_id @@ -1516,7 +2442,7 @@ async def test_create_validate_extraction_action_pretrained( @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - async def test_create_validate_extraction_action_ixp( + async def test_create_validate_extraction_action_ixp_with_tag( self, httpx_mock: HTTPXMock, service: DocumentsService, @@ -1579,6 +2505,7 @@ async def test_create_validate_extraction_action_ixp( ixp_extraction_response["projectId"] = project_id ixp_extraction_response["projectType"] = ProjectType.IXP.value + ixp_extraction_response["extractorId"] = None ixp_extraction_response["tag"] = tag ixp_extraction_response["documentTypeId"] = document_type_id @@ -1611,6 +2538,7 @@ async def test_create_validate_extraction_action_ixp( # ASSERT create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = ProjectType.IXP.value + create_validation_action_response["extractorId"] = None create_validation_action_response["tag"] = tag create_validation_action_response["documentTypeId"] = document_type_id create_validation_action_response["operationId"] = operation_id @@ -1634,7 +2562,7 @@ async def test_create_validate_extraction_action_with_optional_params_omitted( # ARRANGE project_id = str(UUID(int=0)) operation_id = str(uuid4()) - document_type_id = str(UUID(int=0)) + document_type_id = "invoices" tag = None action_title = "TestAction" @@ -1667,6 +2595,7 @@ async def test_create_validate_extraction_action_with_optional_params_omitted( modern_extraction_response["projectId"] = project_id modern_extraction_response["projectType"] = ProjectType.PRETRAINED.value + modern_extraction_response["extractorId"] = document_type_id modern_extraction_response["tag"] = tag modern_extraction_response["documentTypeId"] = document_type_id @@ -1689,6 +2618,7 @@ async def test_create_validate_extraction_action_with_optional_params_omitted( # ASSERT create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = ProjectType.PRETRAINED.value + create_validation_action_response["extractorId"] = document_type_id create_validation_action_response["tag"] = tag create_validation_action_response["documentTypeId"] = document_type_id create_validation_action_response["operationId"] = operation_id @@ -1727,6 +2657,7 @@ async def test_get_validate_classification_result_pretrained( create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = ProjectType.PRETRAINED.value + create_validation_action_response["classifierId"] = "ml-classification" create_validation_action_response["tag"] = None create_validation_action_response["operationId"] = operation_id @@ -1749,6 +2680,73 @@ async def test_get_validate_classification_result_pretrained( classification_response["classificationResults"][0]["ProjectType"] = ( ProjectType.PRETRAINED.value ) + classification_response["classificationResults"][0]["ClassifierId"] = ( + "ml-classification" + ) + classification_response["classificationResults"][0]["Tag"] = None + assert ( + results[0].model_dump() + == classification_response["classificationResults"][0] + ) + + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.asyncio + async def test_get_validate_classification_result_modern_with_version( + self, + httpx_mock: HTTPXMock, + base_url: str, + org: str, + tenant: str, + service: DocumentsService, + create_validation_action_response: dict, # type: ignore + classification_response: dict, # type: ignore + mode: str, + ): + # ARRANGE + project_id = str(uuid4()) + operation_id = str(uuid4()) + classifier_id = str(uuid4()) + + create_validation_action_response["actionStatus"] = "Completed" + create_validation_action_response["validatedClassificationResults"] = ( + classification_response["classificationResults"] + ) + + 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", + status_code=200, + match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, + json={"status": "Succeeded", "result": create_validation_action_response}, + ) + + create_validation_action_response["projectId"] = project_id + create_validation_action_response["projectType"] = ProjectType.MODERN.value + create_validation_action_response["classifierId"] = classifier_id + create_validation_action_response["tag"] = None + create_validation_action_response["operationId"] = operation_id + + # ACT + if mode == "async": + results = await service.get_validate_classification_result_async( + validation_action=ValidateClassificationAction.model_validate( + create_validation_action_response + ) + ) + else: + results = service.get_validate_classification_result( + validation_action=ValidateClassificationAction.model_validate( + create_validation_action_response + ) + ) + + # ASSERT + classification_response["classificationResults"][0]["ProjectId"] = project_id + classification_response["classificationResults"][0]["ProjectType"] = ( + ProjectType.MODERN.value + ) + classification_response["classificationResults"][0]["ClassifierId"] = ( + classifier_id + ) classification_response["classificationResults"][0]["Tag"] = None assert ( results[0].model_dump() @@ -1757,7 +2755,7 @@ async def test_get_validate_classification_result_pretrained( @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.asyncio - async def test_get_validate_classification_result_modern( + async def test_get_validate_classification_result_modern_with_tag( self, httpx_mock: HTTPXMock, base_url: str, @@ -1786,6 +2784,7 @@ async def test_get_validate_classification_result_modern( create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = ProjectType.MODERN.value + create_validation_action_response["classifierId"] = None create_validation_action_response["tag"] = "Production" create_validation_action_response["operationId"] = operation_id @@ -1808,6 +2807,7 @@ async def test_get_validate_classification_result_modern( classification_response["classificationResults"][0]["ProjectType"] = ( ProjectType.MODERN.value ) + classification_response["classificationResults"][0]["ClassifierId"] = None classification_response["classificationResults"][0]["Tag"] = "Production" assert ( results[0].model_dump() @@ -1830,10 +2830,11 @@ async def test_get_validate_extraction_result_pretrained( # ARRANGE project_id = str(UUID(int=0)) operation_id = str(uuid4()) - document_type_id = str(UUID(int=0)) + document_type_id = "invoices" create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = ProjectType.PRETRAINED.value + create_validation_action_response["extractorId"] = document_type_id create_validation_action_response["tag"] = None create_validation_action_response["documentTypeId"] = document_type_id create_validation_action_response["operationId"] = operation_id @@ -1869,10 +2870,90 @@ async def test_get_validate_extraction_result_pretrained( # ASSERT modern_extraction_response["projectId"] = project_id modern_extraction_response["projectType"] = ProjectType.PRETRAINED.value + modern_extraction_response["extractorId"] = document_type_id modern_extraction_response["tag"] = None modern_extraction_response["documentTypeId"] = document_type_id assert response.model_dump() == modern_extraction_response + @pytest.mark.parametrize("mode", ["sync", "async"]) + @pytest.mark.parametrize( + "project_type,extraction_response_fixture", + [ + (ProjectType.MODERN, "modern_extraction_response"), + (ProjectType.IXP, "ixp_extraction_response"), + ], + ) + @pytest.mark.asyncio + async def test_get_validate_extraction_result_with_version( + self, + httpx_mock: HTTPXMock, + base_url: str, + org: str, + tenant: str, + service: DocumentsService, + create_validation_action_response: dict, # type: ignore + modern_extraction_response: dict, # type: ignore + ixp_extraction_response: dict, # type: ignore + mode: str, + project_type: ProjectType, + extraction_response_fixture: str, + ): + # ARRANGE + project_id = str(uuid4()) + operation_id = str(uuid4()) + document_type_id = str(UUID(int=0)) + extractor_id = str(uuid4()) + + # Select the appropriate extraction response based on the fixture name + extraction_response = ( + modern_extraction_response + if extraction_response_fixture == "modern_extraction_response" + else ixp_extraction_response + ) + + create_validation_action_response["projectId"] = project_id + create_validation_action_response["projectType"] = project_type.value + create_validation_action_response["extractorId"] = extractor_id + create_validation_action_response["tag"] = None + create_validation_action_response["documentTypeId"] = document_type_id + create_validation_action_response["operationId"] = operation_id + create_validation_action_response["actionStatus"] = "Completed" + create_validation_action_response["validatedExtractionResults"] = ( + extraction_response["extractionResult"] + ) + create_validation_action_response["dataProjection"] = extraction_response.get( + "dataProjection", None + ) + + 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", + status_code=200, + match_headers={"X-UiPath-Internal-ConsumptionSourceType": "CodedAgents"}, + json={"status": "Succeeded", "result": create_validation_action_response}, + ) + + # ACT + if mode == "async": + response = await service.get_validate_extraction_result_async( + validation_action=ValidateExtractionAction.model_validate( + create_validation_action_response + ) + ) + else: + response = service.get_validate_extraction_result( + validation_action=ValidateExtractionAction.model_validate( + create_validation_action_response + ) + ) + + # ASSERT + extraction_response["projectId"] = project_id + extraction_response["projectType"] = project_type + extraction_response["extractorId"] = extractor_id + extraction_response["tag"] = None + extraction_response["documentTypeId"] = document_type_id + assert response.model_dump() == extraction_response + @pytest.mark.parametrize("mode", ["sync", "async"]) @pytest.mark.parametrize( "project_type,tag,extraction_response_fixture", @@ -1882,7 +2963,7 @@ async def test_get_validate_extraction_result_pretrained( ], ) @pytest.mark.asyncio - async def test_get_validate_extraction_result( + async def test_get_validate_extraction_result_with_tag( self, httpx_mock: HTTPXMock, base_url: str, @@ -1911,6 +2992,7 @@ async def test_get_validate_extraction_result( create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = project_type.value + create_validation_action_response["extractorId"] = None create_validation_action_response["tag"] = tag create_validation_action_response["documentTypeId"] = document_type_id create_validation_action_response["operationId"] = operation_id @@ -1946,6 +3028,7 @@ async def test_get_validate_extraction_result( # ASSERT extraction_response["projectId"] = project_id extraction_response["projectType"] = project_type + extraction_response["extractorId"] = None extraction_response["tag"] = tag extraction_response["documentTypeId"] = document_type_id assert response.model_dump() == extraction_response @@ -2268,6 +3351,7 @@ async def test_start_ixp_extraction_validation( ixp_extraction_response["projectId"] = project_id ixp_extraction_response["projectType"] = ProjectType.IXP.value + ixp_extraction_response["extractorId"] = None ixp_extraction_response["tag"] = tag ixp_extraction_response["documentTypeId"] = str(UUID(int=0)) @@ -2343,6 +3427,7 @@ async def test_start_ixp_extraction_validation_with_optional_params_omitted( ixp_extraction_response["projectId"] = project_id ixp_extraction_response["projectType"] = ProjectType.IXP.value + ixp_extraction_response["extractorId"] = None ixp_extraction_response["tag"] = tag ixp_extraction_response["documentTypeId"] = str(UUID(int=0)) @@ -2411,6 +3496,7 @@ async def test_retrieve_ixp_extraction_validation_result_unassigned( # ASSERT create_validation_action_response["projectId"] = project_id create_validation_action_response["projectType"] = ProjectType.IXP.value + create_validation_action_response["extractorId"] = None create_validation_action_response["tag"] = tag create_validation_action_response["documentTypeId"] = str(UUID(int=0)) create_validation_action_response["operationId"] = operation_id @@ -2464,6 +3550,7 @@ async def test_retrieve_ixp_extraction_validation_result_completed( extraction_validation_action_response_completed["projectType"] = ( ProjectType.IXP.value ) + extraction_validation_action_response_completed["extractorId"] = None extraction_validation_action_response_completed["tag"] = tag extraction_validation_action_response_completed["documentTypeId"] = str( UUID(int=0)