diff --git a/cvat-sdk/cvat_sdk/core/proxies/projects.py b/cvat-sdk/cvat_sdk/core/proxies/projects.py index db77f9f1..63a9fa37 100644 --- a/cvat-sdk/cvat_sdk/core/proxies/projects.py +++ b/cvat-sdk/cvat_sdk/core/proxies/projects.py @@ -6,7 +6,7 @@ from __future__ import annotations import json from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional from cvat_sdk.api_client import apis, models from cvat_sdk.core.downloading import Downloader @@ -19,6 +19,7 @@ from cvat_sdk.core.proxies.model_proxy import ( ModelUpdateMixin, build_model_bases, ) +from cvat_sdk.core.proxies.tasks import Task from cvat_sdk.core.uploading import DatasetUploader, Uploader if TYPE_CHECKING: @@ -30,7 +31,10 @@ _ProjectEntityBase, _ProjectRepoBase = build_model_bases( class Project( - _ProjectEntityBase, models.IProjectRead, ModelUpdateMixin[models.IPatchedProjectWriteRequest] + _ProjectEntityBase, + models.IProjectRead, + ModelUpdateMixin[models.IPatchedProjectWriteRequest], + ModelDeleteMixin, ): _model_partial_update_arg = "patched_project_write_request" @@ -117,13 +121,15 @@ class Project( (annotations, _) = self.api.retrieve_annotations(self.id) return annotations + def get_tasks(self) -> List[Task]: + return [Task(self._client, m) for m in self.api.list_tasks(id=self.id)[0].results] + class ProjectsRepo( _ProjectRepoBase, ModelCreateMixin[Project, models.IProjectWriteRequest], ModelListMixin[Project], ModelRetrieveMixin[Project], - ModelDeleteMixin, ): _entity_type = Project @@ -170,12 +176,12 @@ class ProjectsRepo( filename = Path(filename) if status_check_period is None: - status_check_period = self.config.status_check_period + status_check_period = self._client.config.status_check_period params = {"filename": filename.name} - url = self.api_map.make_endpoint_url(self.api.create_backup_endpoint.path) + url = self._client.api_map.make_endpoint_url(self.api.create_backup_endpoint.path) - uploader = Uploader(self) + uploader = Uploader(self._client) response = uploader.upload_file( url, filename, @@ -195,6 +201,8 @@ class ProjectsRepo( ) project_id = json.loads(response.data)["id"] - self._client.logger.info(f"Project has been imported sucessfully. Project ID: {project_id}") + self._client.logger.info( + f"Project has been imported successfully. Project ID: {project_id}" + ) return self.retrieve(project_id) diff --git a/tests/python/sdk/fixtures.py b/tests/python/sdk/fixtures.py index f9fb0232..7dbe2cab 100644 --- a/tests/python/sdk/fixtures.py +++ b/tests/python/sdk/fixtures.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: MIT from pathlib import Path +from zipfile import ZipFile import pytest from cvat_sdk import Client @@ -56,3 +57,13 @@ def fxt_login(admin_user: str, restore_db_per_class): with client: client.login((user, USER_PASS)) yield (client, user) + + +@pytest.fixture +def fxt_coco_dataset(tmp_path: Path, fxt_image_file: Path, fxt_coco_file: Path): + dataset_path = tmp_path / "coco_dataset.zip" + with ZipFile(dataset_path, "x") as f: + f.write(fxt_image_file, arcname="images/" + fxt_image_file.name) + f.write(fxt_coco_file, arcname="annotations/instances_default.json") + + yield dataset_path diff --git a/tests/python/sdk/test_projects.py b/tests/python/sdk/test_projects.py new file mode 100644 index 00000000..06af0ead --- /dev/null +++ b/tests/python/sdk/test_projects.py @@ -0,0 +1,189 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import io +from logging import Logger +from pathlib import Path +from typing import Tuple + +import pytest +from cvat_sdk import Client, models +from cvat_sdk.api_client import exceptions +from cvat_sdk.core.proxies.projects import Project +from cvat_sdk.core.proxies.tasks import ResourceType, Task +from cvat_sdk.core.utils import filter_dict + +from .util import make_pbar + + +class TestProjectUsecases: + @pytest.fixture(autouse=True) + def setup( + self, + tmp_path: Path, + fxt_login: Tuple[Client, str], + fxt_logger: Tuple[Logger, io.StringIO], + fxt_stdout: io.StringIO, + ): + self.tmp_path = tmp_path + logger, self.logger_stream = fxt_logger + self.stdout = fxt_stdout + self.client, self.user = fxt_login + self.client.logger = logger + + api_client = self.client.api_client + for k in api_client.configuration.logger: + api_client.configuration.logger[k] = logger + + @pytest.fixture + def fxt_new_task(self, fxt_image_file: Path): + task = self.client.tasks.create_from_data( + spec={ + "name": "test_task", + "labels": [{"name": "car"}, {"name": "person"}], + }, + resource_type=ResourceType.LOCAL, + resources=[str(fxt_image_file)], + data_params={"image_quality": 80}, + ) + + return task + + @pytest.fixture + def fxt_task_with_shapes(self, fxt_new_task: Task): + fxt_new_task.set_annotations( + models.LabeledDataRequest( + shapes=[ + models.LabeledShapeRequest( + frame=0, + label_id=fxt_new_task.labels[0].id, + type="rectangle", + points=[1, 1, 2, 2], + ), + ], + ) + ) + + return fxt_new_task + + @pytest.fixture + def fxt_new_project(self): + project = self.client.projects.create( + spec={ + "name": "test_project", + "labels": [{"name": "car"}, {"name": "person"}], + }, + ) + + return project + + @pytest.fixture + def fxt_empty_project(self): + return self.client.projects.create(spec={"name": "test_project"}) + + @pytest.fixture + def fxt_project_with_shapes(self, fxt_task_with_shapes: Task): + project = self.client.projects.create( + spec=models.ProjectWriteRequest( + name="test_project", + labels=[ + models.PatchedLabelRequest( + **filter_dict(label.to_dict(), drop=["id", "has_parent"]) + ) + for label in fxt_task_with_shapes.labels + ], + ) + ) + fxt_task_with_shapes.update(models.PatchedTaskWriteRequest(project_id=project.id)) + project.fetch() + return project + + @pytest.fixture + def fxt_backup_file(self, fxt_project_with_shapes: Project): + backup_path = self.tmp_path / "backup.zip" + + fxt_project_with_shapes.download_backup(str(backup_path)) + + yield backup_path + + def test_can_create_empty_project(self): + project = self.client.projects.create(spec=models.ProjectWriteRequest(name="test project")) + + assert project.id != 0 + assert project.name == "test project" + + def test_can_create_project_from_dataset(self, fxt_coco_dataset: Path): + pbar_out = io.StringIO() + pbar = make_pbar(file=pbar_out) + + project = self.client.projects.create_from_dataset( + spec=models.ProjectWriteRequest(name="project with data"), + dataset_path=fxt_coco_dataset, + dataset_format="COCO 1.0", + pbar=pbar, + ) + + assert project.get_tasks()[0].size == 1 + assert "100%" in pbar_out.getvalue().strip("\r").split("\r")[-1] + assert self.stdout.getvalue() == "" + + def test_can_retrieve_project(self, fxt_new_project: Project): + project_id = fxt_new_project.id + + project = self.client.projects.retrieve(project_id) + + assert project.id == project_id + assert self.stdout.getvalue() == "" + + def test_can_list_projects(self, fxt_new_project: Project): + project_id = fxt_new_project.id + + projects = self.client.projects.list() + + assert any(p.id == project_id for p in projects) + assert self.stdout.getvalue() == "" + + def test_can_update_project(self, fxt_new_project: Project): + fxt_new_project.update(models.PatchedProjectWriteRequest(name="foo")) + + retrieved_project = self.client.projects.retrieve(fxt_new_project.id) + assert retrieved_project.name == "foo" + assert fxt_new_project.name == retrieved_project.name + assert self.stdout.getvalue() == "" + + def test_can_delete_project(self, fxt_new_project: Project): + fxt_new_project.remove() + + with pytest.raises(exceptions.NotFoundException): + fxt_new_project.fetch() + assert self.stdout.getvalue() == "" + + def test_can_get_tasks(self, fxt_project_with_shapes: Project): + task_ids = set(fxt_project_with_shapes.tasks) + + tasks = fxt_project_with_shapes.get_tasks() + + assert len(tasks) == 1 + assert {tasks[0].id} == task_ids + + def test_can_download_backup(self, fxt_project_with_shapes: Project): + pbar_out = io.StringIO() + pbar = make_pbar(file=pbar_out) + backup_path = self.tmp_path / "backup.zip" + + fxt_project_with_shapes.download_backup(str(backup_path), pbar=pbar) + + assert backup_path.stat().st_size > 0 + assert "100%" in pbar_out.getvalue().strip("\r").split("\r")[-1] + assert self.stdout.getvalue() == "" + + def test_can_create_from_backup(self, fxt_backup_file: Path): + pbar_out = io.StringIO() + pbar = make_pbar(file=pbar_out) + + restored_project = self.client.projects.create_from_backup(fxt_backup_file, pbar=pbar) + + assert restored_project.get_tasks()[0].size == 1 + assert "100%" in pbar_out.getvalue().strip("\r").split("\r")[-1] + assert self.stdout.getvalue() == ""