diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5f4771759..1ff2ebff8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,21 @@ History All release highlights of this project will be documented in this file. +4.4.34 - April 11, 2025 +______________________ + +**Added** + + - ``SAClient.get_integrations`` Added id, createdAt, updatedAt, and creator_id in integration metadata. + - ``SAClient.list_workflows`` Retrieves all workflows for your team along with their metadata. + +**Updated** + - ``SAClient.get_project_metadata`` + +**Removed** + - ``SAClient.get_project_workflow`` + - ``SAClient.set_project_workflow`` + 4.4.33 - April 1, 2025 ______________________ diff --git a/docs/source/api_reference/api_project.rst b/docs/source/api_reference/api_project.rst index a099f903d..52be1c3b6 100644 --- a/docs/source/api_reference/api_project.rst +++ b/docs/source/api_reference/api_project.rst @@ -26,6 +26,4 @@ Projects .. automethod:: superannotate.SAClient.set_project_default_image_quality_in_editor .. automethod:: superannotate.SAClient.set_project_steps .. automethod:: superannotate.SAClient.get_project_steps -.. automethod:: superannotate.SAClient.set_project_workflow -.. automethod:: superannotate.SAClient.get_project_workflow .. automethod:: superannotate.SAClient.get_component_config diff --git a/docs/source/api_reference/api_team.rst b/docs/source/api_reference/api_team.rst index 05d2a72b5..15f8130c2 100644 --- a/docs/source/api_reference/api_team.rst +++ b/docs/source/api_reference/api_team.rst @@ -4,6 +4,7 @@ Team .. automethod:: superannotate.SAClient.get_team_metadata +.. automethod:: superannotate.SAClient.list_workflows .. automethod:: superannotate.SAClient.get_integrations .. automethod:: superannotate.SAClient.invite_contributors_to_team .. automethod:: superannotate.SAClient.search_team_contributors diff --git a/pytest.ini b/pytest.ini index c0f66b58e..d9f7f6cc3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,4 +3,4 @@ minversion = 3.7 log_cli=true python_files = test_*.py ;pytest_plugins = ['pytest_profiling'] -addopts = -n 6 --dist loadscope +;addopts = -n 6 --dist loadscope diff --git a/src/superannotate/__init__.py b/src/superannotate/__init__.py index b771ad9ad..57c8bd727 100644 --- a/src/superannotate/__init__.py +++ b/src/superannotate/__init__.py @@ -3,7 +3,7 @@ import sys -__version__ = "4.4.33" +__version__ = "4.4.34" os.environ.update({"sa_version": __version__}) diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index c52bdeb94..3b4e6e812 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -72,6 +72,8 @@ from lib.infrastructure.validators import wrap_error from lib.app.serializers import WMProjectSerializer from lib.core.entities.work_managament import WMUserTypeEnum +from lib.core.jsx_conditions import EmptyQuery + logger = logging.getLogger("sa") @@ -211,7 +213,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): return True def save(self): - if len(json.dumps(self.annotation).encode("utf-8")) > 16 * 1024 * 1024: + if len(json.dumps(self.annotation).encode("utf-8")) > 15 * 1024 * 1024: self._set_large_annotation_adapter(self.annotation) else: self._set_small_annotation_adapter(self.annotation) @@ -1354,7 +1356,7 @@ def get_project_metadata( the key "settings" :type include_settings: bool - :param include_workflow: Deprecated + :param include_workflow: Returns workflow metadata :type include_workflow: bool :param include_contributors: enables project contributors output under @@ -1377,6 +1379,7 @@ def get_project_metadata( client.get_project_metadata( project="Medical Annotations", + include_workflow=True, include_custom_fields=True ) @@ -1385,50 +1388,53 @@ def get_project_metadata( :: { - "classes": [], - "completed_items_count": None, - "contributors": [], "createdAt": "2025-02-04T12:04:01+00:00", + "updatedAt": "2024-02-04T12:04:01+00:00", + "id": 902174, + "team_id": 233435, + "name": "Medical Annotations", + "type": "Vector", + "description": "DESCRIPTION", + "instructions_link": None, "creator_id": "ecample@email.com", + "entropy_status": 1, + "sharing_status": None, + "status": "NotStarted", + "folder_id": 1191383, + "workflow_id": 1, + "workflow": { + "createdAt": "2024-09-03T12:48:09+00:00", + "updatedAt": "2024-09-03T12:48:09+00:00", + "id": 1, + "name": "System workflow", + "type": "system", + "description": "This workflow is generated by the system, and prevents annotators from completing items.", + "raw_config": {"roles": ["Annotator", "QA"], ...} + }, + "upload_state": "INITIAL", + "users": [], + "contributors": [], + "settings": [], + "classes": [], + "item_count": None, + "completed_items_count": None, + "root_folder_completed_items_count": None, "custom_fields": { "Notes": "Something", "Ann Quality threshold": 80, "Tag": ["Tag1", "Tag2", "Tag3"], "Due date": 1738281600.0, "Other_Custom_Field": None, - }, - "description": "DESCRIPTION", - "entropy_status": 1, - "folder_id": 1191383, - "id": 902174, - "instructions_link": None, - "item_count": None, - "name": "Medical Annotations", - "root_folder_completed_items_count": None, - "settings": [], - "sharing_status": None, - "status": "NotStarted", - "team_id": 233435, - "type": "Vector", - "updatedAt": "2024-02-04T12:04:01+00:00", - "upload_state": "INITIAL", - "users": [], - "workflow_id": 1, + } } """ project_name, _ = extract_project_folder(project) project = self.controller.get_project(project_name) - if include_workflow: - warnings.warn( - DeprecationWarning( - "The “include_workflow” parameter is deprecated." - " Please use the “get_project_steps” function instead." - ) - ) response = self.controller.projects.get_metadata( project, include_annotation_classes, include_settings, + include_workflow, include_contributors, include_complete_item_count, include_custom_fields, @@ -1464,18 +1470,6 @@ def get_project_settings(self, project: Union[NotEmptyStr, dict]): ] return settings - def get_project_workflow(self, project: Union[str, dict]): - """ - Deprecated - """ - warnings.warn( - DeprecationWarning( - "The “get_project_workflow” function is deprecated." - " Please use the “get_project_steps” function instead." - ) - ) - return self.get_project_steps(project) - def get_project_steps(self, project: Union[str, dict]): """Gets project's steps. @@ -1509,10 +1503,10 @@ def get_project_steps(self, project: Union[str, dict]): """ project_name, _ = extract_project_folder(project) project = self.controller.get_project(project_name) - workflow = self.controller.projects.list_workflow(project) - if workflow.errors: - raise AppException(workflow.errors) - return workflow.data + steps = self.controller.projects.list_steps(project) + if steps.errors: + raise AppException(steps.errors) + return steps.data def search_annotation_classes( self, project: Union[NotEmptyStr, dict], name_contains: Optional[str] = None @@ -2502,19 +2496,6 @@ def download_export( if response.errors: raise AppException(response.errors) - def set_project_workflow( - self, project: Union[NotEmptyStr, dict], new_workflow: List[dict] - ): - """ - Deprecated - """ - warnings.warn( - DeprecationWarning( - "The “set_project_workflow” function is deprecated. Please use the “set_project_steps” function instead." - ) - ) - return self.set_project_steps(project, new_workflow) - def set_project_steps(self, project: Union[NotEmptyStr, dict], steps: List[dict]): """Sets project's steps. @@ -2548,7 +2529,7 @@ def set_project_steps(self, project: Union[NotEmptyStr, dict], steps: List[dict] """ project_name, _ = extract_project_folder(project) project = self.controller.get_project(project_name) - response = self.controller.projects.set_workflows(project, steps=steps) + response = self.controller.projects.set_steps(project, steps=steps) if response.errors: raise AppException(response.errors) @@ -3273,14 +3254,33 @@ def get_integrations(self): :return: metadata objects of all integrations of the team. :rtype: list of dicts + + Request Example: + :: + + client.get_integrations() + + + Response Example: + :: + + [ + { + "createdAt": "2023-11-27T11:16:02.000Z", + "id": 5072, + "name": "My S3 Bucket", + "root": "test-openseadragon-1212", + "type": "aws", + "updatedAt": "2023-12-27T11:16:02.000Z", + "creator_id": "example@superannotate.com" + } + ] """ response = self.controller.integrations.list() if response.errors: raise AppException(response.errors) integrations = response.data - return BaseSerializer.serialize_iterable( - integrations, ("name", "type", "root") # noqa - ) + return BaseSerializer.serialize_iterable(integrations) def attach_items_from_integrated_storage( self, @@ -4783,3 +4783,46 @@ def item_context( item=_item, overwrite=overwrite, ) + + def list_workflows(self): + """ + Lists team’s all workflows and their metadata + + :return: metadata of workflows + :rtype: list of dicts + + + Request Example: + :: + + client.list_workflows() + + + Response Example: + :: + + [ + { + "createdAt": "2024-09-03T12:48:09+00:00", + "updatedAt": "2024-09-04T12:48:09+00:00", + "id": 1, + "name": "System workflow", + "type": "system", + "description": "This workflow is generated by the system, and prevents annotators from completing items.", + "raw_config": {"roles": ["Annotator", "QA"], ...} + }, + { + "createdAt": "2025-01-03T12:48:09+00:00", + "updatedAt": "2025-01-05T12:48:09+00:00", + "id": 58758, + "name": "Custom workflow", + "type": "user", + "description": "This workflow custom build.", + "raw_config": {"roles": ["Custom Annotator", "Custom QA"], ...} + } + ] + """ + workflows = self.controller.service_provider.work_management.list_workflows( + EmptyQuery() + ) + return BaseSerializer.serialize_iterable(workflows.data) diff --git a/src/superannotate/lib/core/entities/integrations.py b/src/superannotate/lib/core/entities/integrations.py index 731bc76af..e59e0935e 100644 --- a/src/superannotate/lib/core/entities/integrations.py +++ b/src/superannotate/lib/core/entities/integrations.py @@ -11,7 +11,7 @@ class IntegrationEntity(TimedBaseModel): id: int = None - user_id: str = None + creator_id: str = None name: str type: IntegrationTypeEnum = Field(None, alias="source") root: str = Field(None, alias="bucket_name") diff --git a/src/superannotate/lib/core/entities/project.py b/src/superannotate/lib/core/entities/project.py index 840abf56b..8d2a33c0d 100644 --- a/src/superannotate/lib/core/entities/project.py +++ b/src/superannotate/lib/core/entities/project.py @@ -88,6 +88,20 @@ class Config: extra = Extra.ignore +class WorkflowEntity(TimedBaseModel): + id: Optional[int] + name: Optional[str] + type: Optional[str] + description: Optional[str] + raw_config: Optional[dict] + + def is_system(self): + return self.type == "system" + + class Config: + extra = Extra.ignore + + class ProjectEntity(TimedBaseModel): id: Optional[int] team_id: Optional[int] @@ -101,6 +115,7 @@ class ProjectEntity(TimedBaseModel): status: Optional[ProjectStatus] folder_id: Optional[int] workflow_id: Optional[int] + workflow: Optional[WorkflowEntity] sync_status: Optional[int] upload_state: Optional[int] users: Optional[List[ContributorEntity]] = [] @@ -175,15 +190,3 @@ class CustomFieldEntity(BaseModel): class Config: extra = Extra.allow - - -class WorkflowEntity(BaseModel): - id: Optional[int] - name: Optional[str] - type: Optional[str] - - def is_system(self): - return self.type == "system" - - class Config: - extra = Extra.ignore diff --git a/src/superannotate/lib/core/serviceproviders.py b/src/superannotate/lib/core/serviceproviders.py index 1e0a30a13..1468c4dbe 100644 --- a/src/superannotate/lib/core/serviceproviders.py +++ b/src/superannotate/lib/core/serviceproviders.py @@ -261,17 +261,15 @@ def set_settings( raise NotImplementedError @abstractmethod - def list_workflows(self, project: entities.ProjectEntity): + def list_steps(self, project: entities.ProjectEntity): raise NotImplementedError @abstractmethod - def set_workflow( - self, project: entities.ProjectEntity, workflow: entities.WorkflowEntity - ): + def set_step(self, project: entities.ProjectEntity, step: entities.StepEntity): raise NotImplementedError @abstractmethod - def set_workflows(self, project: entities.ProjectEntity, steps: list): + def set_steps(self, project: entities.ProjectEntity, steps: list): raise NotImplementedError @abstractmethod @@ -283,7 +281,7 @@ def un_share(self, project: entities.ProjectEntity, user_id) -> ServiceResponse: raise NotImplementedError @abstractmethod - def set_project_workflow_attributes( + def set_project_step_attributes( self, project: entities.ProjectEntity, attributes: list ): raise NotImplementedError diff --git a/src/superannotate/lib/core/usecases/projects.py b/src/superannotate/lib/core/usecases/projects.py index aca47eb26..23a913ced 100644 --- a/src/superannotate/lib/core/usecases/projects.py +++ b/src/superannotate/lib/core/usecases/projects.py @@ -14,6 +14,8 @@ from lib.core.enums import CustomFieldType from lib.core.exceptions import AppException from lib.core.exceptions import AppValidationException +from lib.core.jsx_conditions import Filter +from lib.core.jsx_conditions import OperatorEnum from lib.core.response import Response from lib.core.serviceproviders import BaseServiceProvider from lib.core.usecases.base import BaseUseCase @@ -107,6 +109,7 @@ def __init__( service_provider: BaseServiceProvider, include_annotation_classes: bool, include_settings: bool, + include_workflow: bool, include_contributors: bool, include_complete_image_count: bool, include_custom_fields: bool, @@ -117,6 +120,7 @@ def __init__( self._include_annotation_classes = include_annotation_classes self._include_settings = include_settings + self._include_workflow = include_workflow self._include_contributors = include_contributors self._include_complete_image_count = include_complete_image_count self._include_custom_fields = include_custom_fields @@ -148,7 +152,16 @@ def execute(self): project.settings = self._service_provider.projects.list_settings( self._project ).data - + if self._include_workflow: + _workflows = self._service_provider.work_management.list_workflows( + Filter("id", project.workflow_id, OperatorEnum.EQ) + ) + project_workflow = next( + (i for i in _workflows.data if i.id == project.workflow_id), None + ) + if not project_workflow: + raise AppException("Workflow not fund.") + project.workflow = project_workflow if self._include_contributors: project.contributors = project.users else: @@ -464,7 +477,7 @@ def execute(self): return self._response -class GetWorkflowsUseCase(BaseUseCase): +class GetStepsUseCase(BaseUseCase): def __init__(self, project: ProjectEntity, service_provider: BaseServiceProvider): super().__init__() self._project = project @@ -479,19 +492,17 @@ def validate_project_type(self): def execute(self): if self.is_valid(): data = [] - workflows = self._service_provider.projects.list_workflows( - self._project - ).data - for workflow in workflows: - workflow_data = workflow.dict() + steps = self._service_provider.projects.list_steps(self._project).data + for step in steps: + step_data = step.dict() annotation_classes = self._service_provider.annotation_classes.list( Condition("project_id", self._project.id, EQ) ).data for annotation_class in annotation_classes: - if annotation_class.id == workflow.class_id: - workflow_data["className"] = annotation_class.name + if annotation_class.id == step.class_id: + step_data["className"] = annotation_class.name break - data.append(workflow_data) + data.append(step_data) self._response.data = data return self._response @@ -562,7 +573,7 @@ def execute(self): return self._response -class SetWorkflowUseCase(BaseUseCase): +class SetStepsUseCase(BaseUseCase): def __init__( self, service_provider: BaseServiceProvider, @@ -601,16 +612,16 @@ def execute(self): step["class_id"] = annotation_classes_map.get(step["className"], None) if not step["class_id"]: raise AppException("Annotation class not found.") - self._service_provider.projects.set_workflows( + self._service_provider.projects.set_steps( project=self._project, steps=self._steps, ) - existing_workflows = self._service_provider.projects.list_workflows( + existing_steps = self._service_provider.projects.list_steps( self._project ).data - existing_workflows_map = {} - for workflow in existing_workflows: - existing_workflows_map[workflow.step] = workflow.id + existing_steps_map = {} + for steps in existing_steps: + existing_steps_map[steps.step] = steps.id req_data = [] for step in self._steps: @@ -628,17 +639,17 @@ def execute(self): f"Attribute group name or attribute name not found {attribute_group_name}." ) - if not existing_workflows_map.get(step["step"], None): - raise AppException("Couldn't find step in workflow") + if not existing_steps_map.get(step["step"], None): + raise AppException("Couldn't find step in steps") req_data.append( { - "workflow_id": existing_workflows_map[step["step"]], + "workflow_id": existing_steps_map[step["step"]], "attribute_id": annotations_classes_attributes_map[ f"{annotation_class_name}__{attribute_group_name}__{attribute_name}" ], } ) - self._service_provider.projects.set_project_workflow_attributes( + self._service_provider.projects.set_project_step_attributes( project=self._project, attributes=req_data, ) diff --git a/src/superannotate/lib/infrastructure/annotation_adapter.py b/src/superannotate/lib/infrastructure/annotation_adapter.py index 13436510a..949a2a2bd 100644 --- a/src/superannotate/lib/infrastructure/annotation_adapter.py +++ b/src/superannotate/lib/infrastructure/annotation_adapter.py @@ -39,14 +39,46 @@ def save(self): raise NotImplementedError def get_component_value(self, component_id: str): - if component_id in self.annotation["data"]: - return self.annotation["data"][component_id]["value"] + component_data = self.annotation.get("data", {}).get(component_id) + if isinstance(component_data, dict): + return component_data.get("value") + elif isinstance(component_data, list): + # Find the dict with the smallest element_path + annotation = min( + ( + elem + for elem in component_data + if isinstance(elem, dict) and "element_path" in elem + ), + key=lambda x: x["element_path"], + default=None, + ) + if annotation is not None: + return annotation.get("value") return None def set_component_value(self, component_id: str, value: Any): - self.annotation.setdefault("data", {}).setdefault(component_id, {})[ - "value" - ] = value + data = self.annotation.setdefault("data", {}) + component_data = data.get(component_id) + + if component_data is None: + data[component_id] = [{"value": value}] + elif isinstance(component_data, dict): + component_data["value"] = value + elif isinstance(component_data, list): + # Find the dict with the smallest element_path + annotation = min( + ( + elem + for elem in component_data + if isinstance(elem, dict) and "element_path" in elem + ), + key=lambda x: x["element_path"], + default=None, + ) + if annotation is not None: + annotation["value"] = value + return self @@ -71,7 +103,7 @@ def annotation(self) -> dict: project=self._project, folder=self._folder, item_id=self._item.id, - transform_version="llmJsonV2", + transform_version="llmJsonV3", ) if not response or response.status_code == 404: self._annotation = { @@ -88,7 +120,7 @@ def save(self): project=self._project, folder=self._folder, item_id=self._item.id, - transform_version="llmJsonV2", + transform_version="llmJsonV3", data=self.annotation, overwrite=self._overwrite, etag=self._etag, @@ -104,7 +136,7 @@ def annotation(self) -> dict: project=self._project, item=self._item, reporter=self._controller.reporter, - transform_version="llmJsonV2", + transform_version="llmJsonV3", ) ) return self._annotation @@ -117,6 +149,6 @@ def save(self): item_id=self._item.id, data=StringIO(json.dumps(self._annotation)), chunk_size=5 * 1024 * 1024, - transform_version="llmJsonV2", + transform_version="llmJsonV3", ) ) diff --git a/src/superannotate/lib/infrastructure/controller.py b/src/superannotate/lib/infrastructure/controller.py index c1654b5a4..7e35a6e96 100644 --- a/src/superannotate/lib/infrastructure/controller.py +++ b/src/superannotate/lib/infrastructure/controller.py @@ -424,6 +424,7 @@ def get_metadata( project: ProjectEntity, include_annotation_classes: bool = False, include_settings: bool = False, + include_workflow: bool = False, include_contributors: bool = False, include_complete_image_count: bool = False, include_custom_fields: bool = False, @@ -433,6 +434,7 @@ def get_metadata( service_provider=self.service_provider, include_annotation_classes=include_annotation_classes, include_settings=include_settings, + include_workflow=include_workflow, include_contributors=include_contributors, include_complete_image_count=include_complete_image_count, include_custom_fields=include_custom_fields, @@ -478,14 +480,14 @@ def list_settings(self, project: ProjectEntity): ) return use_case.execute() - def list_workflow(self, project: ProjectEntity): - use_case = usecases.GetWorkflowsUseCase( + def list_steps(self, project: ProjectEntity): + use_case = usecases.GetStepsUseCase( project=project, service_provider=self.service_provider ) return use_case.execute() - def set_workflows(self, project: ProjectEntity, steps: List): - use_case = usecases.SetWorkflowUseCase( + def set_steps(self, project: ProjectEntity, steps: List): + use_case = usecases.SetStepsUseCase( service_provider=self.service_provider, steps=steps, project=project, diff --git a/src/superannotate/lib/infrastructure/services/annotation.py b/src/superannotate/lib/infrastructure/services/annotation.py index a1f5d9e46..b97a7063b 100644 --- a/src/superannotate/lib/infrastructure/services/annotation.py +++ b/src/superannotate/lib/infrastructure/services/annotation.py @@ -357,7 +357,7 @@ async def upload_big_annotation( "folder_id": folder.id, } if transform_version: - params["desired_transform_version"] = transform_version + params["current_transform_version"] = transform_version url = urljoin( self.get_assets_provider_url("v3.01"), self.URL_START_FILE_UPLOAD_PROCESS.format(item_id=item_id), diff --git a/src/superannotate/lib/infrastructure/services/project.py b/src/superannotate/lib/infrastructure/services/project.py index 0b1b43455..b84d2f67a 100644 --- a/src/superannotate/lib/infrastructure/services/project.py +++ b/src/superannotate/lib/infrastructure/services/project.py @@ -13,10 +13,10 @@ class ProjectService(BaseProjectService): URL_LIST = "api/v1/projects" URL_GET = "project/{}" URL_SETTINGS = "project/{}/settings" - URL_WORKFLOW = "project/{}/workflow" + URL_STEPS = "project/{}/workflow" URL_SHARE = "api/v1/project/{}/share/bulk" URL_SHARE_PROJECT = "project/{}/share" - URL_WORKFLOW_ATTRIBUTE = "project/{}/workflow_attribute" + URL_STEP_ATTRIBUTE = "project/{}/workflow_attribute" URL_UPLOAD_PRIORITY_SCORES = "images/updateEntropy" URL_ASSIGN_ITEMS = "images/editAssignment/" URL_GET_BY_ID = "api/v1/project/{project_id}" @@ -99,33 +99,31 @@ def share(self, project: entities.ProjectEntity, users: list): data={"users": users}, ) - def list_workflows(self, project: entities.ProjectEntity): + def list_steps(self, project: entities.ProjectEntity): return self.client.paginate( - self.URL_WORKFLOW.format(project.id), item_type=entities.StepEntity + self.URL_STEPS.format(project.id), item_type=entities.StepEntity ) - def set_workflow( - self, project: entities.ProjectEntity, workflow: entities.StepEntity - ): + def set_step(self, project: entities.ProjectEntity, step: entities.StepEntity): return self.client.request( - self.URL_WORKFLOW.format(project.id), + self.URL_STEPS.format(project.id), "post", - data={"steps": [workflow]}, + data={"steps": [step]}, ) # TODO check - def set_workflows(self, project: entities.ProjectEntity, steps: list): + def set_steps(self, project: entities.ProjectEntity, steps: list): return self.client.request( - self.URL_WORKFLOW.format(project.id), + self.URL_STEPS.format(project.id), "post", data={"steps": steps}, ) - def set_project_workflow_attributes( + def set_project_step_attributes( self, project: entities.ProjectEntity, attributes: list ): return self.client.request( - self.URL_WORKFLOW_ATTRIBUTE.format(project.id), + self.URL_STEP_ATTRIBUTE.format(project.id), "post", data={"data": attributes}, ) diff --git a/tests/integration/projects/test_basic_project.py b/tests/integration/projects/test_basic_project.py index 0639a735a..6f17ab21d 100644 --- a/tests/integration/projects/test_basic_project.py +++ b/tests/integration/projects/test_basic_project.py @@ -86,7 +86,7 @@ def test_workflow_get(self): }, ], ) - sa.set_project_workflow( + sa.set_project_steps( self.PROJECT_NAME, [ { diff --git a/tests/integration/projects/test_clone_project.py b/tests/integration/projects/test_clone_project.py index ad25ebe71..7358429a5 100644 --- a/tests/integration/projects/test_clone_project.py +++ b/tests/integration/projects/test_clone_project.py @@ -75,7 +75,7 @@ def test_clone_project(self): sa.set_project_default_image_quality_in_editor( self.PROJECT_NAME_1, self.IMAGE_QUALITY ) - sa.set_project_workflow( + sa.set_project_steps( self.PROJECT_NAME_1, self.WORKFLOWS, ) diff --git a/tests/integration/projects/test_create_project.py b/tests/integration/projects/test_create_project.py index 6749b5d2d..e873b7534 100644 --- a/tests/integration/projects/test_create_project.py +++ b/tests/integration/projects/test_create_project.py @@ -114,7 +114,7 @@ def test_create_project_with_classes_and_workflows(self): self.PROJECT_TYPE, classes=self.CLASSES, ) - sa.set_project_workflow(self.PROJECT, self.WORKFLOWS) + sa.set_project_steps(self.PROJECT, self.WORKFLOWS) assert len(project["classes"]) == 1 assert len(project["classes"][0]["attribute_groups"]) == 1 assert len(project["classes"][0]["attribute_groups"][0]["attributes"]) == 3 diff --git a/tests/integration/projects/test_get_project_metadata.py b/tests/integration/projects/test_get_project_metadata.py index 1ac00d9b0..aaf005e66 100644 --- a/tests/integration/projects/test_get_project_metadata.py +++ b/tests/integration/projects/test_get_project_metadata.py @@ -9,20 +9,80 @@ class TestGetProjectMetadata(BaseTestCase): PROJECT_TYPE = "Vector" PROJECT_DESCRIPTION = "DESCRIPTION" IGNORE_KEYS = {"id", "creator_id", "team_id", "createdAt", "updatedAt"} - EXPECTED_PROJECT_METADATA = { - "name": "TestGetProjectMetadata", - "type": "Vector", - "description": "DESCRIPTION", - "instructions_link": None, - "entropy_status": 1, - "sharing_status": None, - "status": "NotStarted", - "folder_id": None, - "upload_state": "EXTERNAL", - "users": [], - "completed_items_count": None, - "root_folder_completed_items_count": None, - } + EXPECTED_PROJECT_METADATA_KEYS = [ + "createdAt", + "updatedAt", + "id", + "team_id", + "name", + "type", + "description", + "instructions_link", + "creator_id", + "entropy_status", + "sharing_status", + "status", + "folder_id", + "workflow_id", + "workflow", + "upload_state", + "users", + "contributors", + "settings", + "classes", + "item_count", + "completed_items_count", + "root_folder_completed_items_count", + "custom_fields", + ] + + def test_get_project_metadata(self): + # without includes + project_metadata = sa.get_project_metadata(self.PROJECT_NAME) + for key in self.EXPECTED_PROJECT_METADATA_KEYS: + assert key in project_metadata + assert project_metadata["name"] == self.PROJECT_NAME + assert project_metadata["type"] == self.PROJECT_TYPE + assert project_metadata["description"] == self.PROJECT_DESCRIPTION + assert project_metadata["workflow_id"] == 1 + assert not project_metadata["workflow"] + assert not project_metadata["contributors"] + assert not project_metadata["completed_items_count"] + assert not project_metadata["custom_fields"] + assert not project_metadata["settings"] + assert not project_metadata["classes"] + + # with includes + project_metadata = sa.get_project_metadata( + self.PROJECT_NAME, + include_contributors=True, + include_complete_item_count=True, + include_custom_fields=True, + include_workflow=True, + include_settings=True, + include_annotation_classes=True, + ) + for key in self.EXPECTED_PROJECT_METADATA_KEYS: + assert key in project_metadata + assert project_metadata["name"] == self.PROJECT_NAME + assert project_metadata["type"] == self.PROJECT_TYPE + assert project_metadata["description"] == self.PROJECT_DESCRIPTION + assert project_metadata["workflow_id"] == 1 + assert not project_metadata["contributors"] + assert project_metadata["completed_items_count"] == 0 + assert project_metadata["settings"] + assert not project_metadata["classes"] + assert project_metadata["workflow"] + assert project_metadata["workflow"]["id"] == 1 + assert project_metadata["workflow"]["name"] == "System workflow" + assert project_metadata["workflow"]["createdAt"] + assert project_metadata["workflow"]["createdAt"] + assert project_metadata["workflow"]["type"] == "system" + assert ( + project_metadata["workflow"]["description"] + == "This workflow is generated by the system, and prevents annotators from completing items." + ) + assert project_metadata["workflow"]["raw_config"] def test_get_project_by_id(self): project_metadata = sa.get_project_metadata(self.PROJECT_NAME) diff --git a/tests/integration/test_depricated_functions_document.py b/tests/integration/test_depricated_functions_document.py index 481bb2950..64beac740 100644 --- a/tests/integration/test_depricated_functions_document.py +++ b/tests/integration/test_depricated_functions_document.py @@ -100,14 +100,6 @@ def test_deprecated_functions(self): sa.upload_video_to_project(self.PROJECT_NAME, "some path") except AppException as e: self.assertIn(self.EXCEPTION_MESSAGE, str(e)) - try: - sa.set_project_workflow(self.PROJECT_NAME, [{}]) - except AppException as e: - self.assertIn(self.EXCEPTION_MESSAGE, str(e)) - try: - sa.get_project_workflow(self.PROJECT_NAME) - except AppException as e: - self.assertIn(self.EXCEPTION_MESSAGE, str(e)) try: sa.prepare_export(self.PROJECT_NAME, include_fuse=True, only_pinned=True) except AppException as e: diff --git a/tests/integration/test_depricated_functions_video.py b/tests/integration/test_depricated_functions_video.py index 52b771cfb..a60926f2b 100644 --- a/tests/integration/test_depricated_functions_video.py +++ b/tests/integration/test_depricated_functions_video.py @@ -94,14 +94,6 @@ def test_deprecated_functions(self): sa.upload_video_to_project(self.PROJECT_NAME, "some path") except AppException as e: self.assertIn(self.EXCEPTION_MESSAGE, str(e)) - try: - sa.set_project_workflow(self.PROJECT_NAME, [{}]) - except AppException as e: - self.assertIn(self.EXCEPTION_MESSAGE, str(e)) - try: - sa.get_project_workflow(self.PROJECT_NAME) - except AppException as e: - self.assertIn(self.EXCEPTION_MESSAGE, str(e)) try: sa.set_project_default_image_quality_in_editor( self.PROJECT_NAME, "original" diff --git a/tests/integration/work_management/test_list_workflows.py b/tests/integration/work_management/test_list_workflows.py new file mode 100644 index 000000000..7e57a5d5a --- /dev/null +++ b/tests/integration/work_management/test_list_workflows.py @@ -0,0 +1,22 @@ +from unittest import TestCase + +from src.superannotate import SAClient + +sa = SAClient() + + +class TestListWorkflows(TestCase): + def test_list_workflows(self): + workflows = sa.list_workflows() + assert workflows + first_workflow = workflows[0] + assert first_workflow["id"] == 1 + assert first_workflow["type"] == "system" + assert first_workflow["createdAt"] + assert first_workflow["updatedAt"] + assert first_workflow["name"] == "System workflow" + assert first_workflow["description"] == ( + "This workflow is generated by the " + "system, and prevents annotators from completing items." + ) + assert first_workflow["raw_config"] diff --git a/tests/integration/work_management/test_pause_resume_user_activity.py b/tests/integration/work_management/test_pause_resume_user_activity.py index a706d8443..5cbdfeb70 100644 --- a/tests/integration/work_management/test_pause_resume_user_activity.py +++ b/tests/integration/work_management/test_pause_resume_user_activity.py @@ -37,6 +37,7 @@ def test_pause_and_resume_user_activity(self): u for u in users if u["role"] == "Contributor" and u["state"] == "Confirmed" ][0] import pdb + pdb.set_trace() sa.add_contributors_to_project(self.PROJECT_NAME, [scapegoat["email"]], "QA") with self.assertLogs("sa", level="INFO") as cm: