diff --git a/darwin/future/core/client.py b/darwin/future/core/client.py index f2c7e9874..b80b81031 100644 --- a/darwin/future/core/client.py +++ b/darwin/future/core/client.py @@ -10,7 +10,12 @@ from requests.adapters import HTTPAdapter, Retry from darwin.future.core.types.common import JSONType, QueryString -from darwin.future.exceptions import NotFound, Unauthorized, UnprocessibleEntity +from darwin.future.exceptions import ( + BadRequest, + NotFound, + Unauthorized, + UnprocessibleEntity, +) class TeamsConfig(BaseModel): @@ -235,6 +240,8 @@ def raise_for_darwin_exception(response: requests.Response) -> None: """ if response.status_code == 200: return + if response.status_code == 400: + raise BadRequest(response) if response.status_code == 401: raise Unauthorized(response) if response.status_code == 404: diff --git a/darwin/future/core/items/archive_items.py b/darwin/future/core/items/archive_items.py index 898f8e921..b83913b65 100644 --- a/darwin/future/core/items/archive_items.py +++ b/darwin/future/core/items/archive_items.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Dict, List -from uuid import UUID from darwin.future.core.client import ClientCore from darwin.future.core.types.common import JSONType @@ -7,10 +8,9 @@ def archive_list_of_items( - api_client: ClientCore, + client: ClientCore, team_slug: str, - dataset_id: int, - item_ids: List[UUID], + dataset_ids: int | List[int], filters: Dict[str, UnknownType] = {}, ) -> JSONType: """ @@ -19,26 +19,29 @@ def archive_list_of_items( Parameters ---------- client: Client - The client to use for the request + The client to use for the request. team_slug: str - The slug of the team containing the items - dataset_id: int - The ID of the dataset containing the items - item_ids: List[UUID] - The IDs of the items to be archived + The slug of the team containing the items. + dataset_ids: int | List[int] + The ID(s) of the dataset(s) containing the items. filters: Dict[str, UnknownType] - Dataset filter parameters + Filter parameters. Returns ------- JSONType + The response data. """ + assert ( + filters + ), "No parameters provided, please provide at least one non-dataset id filter" payload = { "filters": { - "dataset_ids": [dataset_id], - "item_ids": [str(item_id) for item_id in item_ids], + "dataset_ids": dataset_ids + if isinstance(dataset_ids, list) + else [dataset_ids], **filters, } } - return api_client.post(f"/v2/teams/{team_slug}/items/archive", data=payload) + return client.post(f"/v2/teams/{team_slug}/items/archive", data=payload) diff --git a/darwin/future/core/items/delete_items.py b/darwin/future/core/items/delete_items.py index 1fd4d6ae4..3100b6cc7 100644 --- a/darwin/future/core/items/delete_items.py +++ b/darwin/future/core/items/delete_items.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Dict, List -from uuid import UUID from darwin.future.core.client import ClientCore from darwin.future.core.types.common import JSONType @@ -7,10 +8,9 @@ def delete_list_of_items( - api_client: ClientCore, + client: ClientCore, team_slug: str, - dataset_id: int, - item_ids: List[UUID], + dataset_ids: int | List[int], filters: Dict[str, UnknownType] = {}, ) -> JSONType: """ @@ -19,26 +19,28 @@ def delete_list_of_items( Parameters ---------- client: Client - The client to use for the request + The client to use for the request. team_slug: str - The slug of the team containing the items - dataset_id: int - The ID of the dataset containing the items - item_ids: List[UUID] - The IDs of the items to be deleted + The slug of the team containing the items. + dataset_ids: int | List[int] + The ID(s) of the dataset(s) containing the items. filters: Dict[str, UnknownType] - Dataset filter parameters + Filter parameters Returns ------- JSONType + The response data. """ + assert ( + filters + ), "No parameters provided, please provide at least one non-dataset id filter" payload = { "filters": { - "dataset_ids": [dataset_id], - "item_ids": [str(item_id) for item_id in item_ids], + "dataset_ids": dataset_ids + if isinstance(dataset_ids, list) + else [dataset_ids], **filters, } } - - return api_client.delete(f"/v2/teams/{team_slug}/items", data=payload) + return client.delete(f"/v2/teams/{team_slug}/items", data=payload) diff --git a/darwin/future/core/items/get.py b/darwin/future/core/items/get.py index 8701d5828..4103b2bde 100644 --- a/darwin/future/core/items/get.py +++ b/darwin/future/core/items/get.py @@ -1,11 +1,13 @@ -from typing import List, Tuple, Union +from __future__ import annotations + +from typing import List, Literal, Tuple, Union from uuid import UUID from pydantic import ValidationError, parse_obj_as from darwin.future.core.client import ClientCore from darwin.future.core.types.common import QueryString -from darwin.future.data_objects.item import Folder, Item +from darwin.future.data_objects.item import Folder, ItemCore def get_item_ids( @@ -86,7 +88,7 @@ def get_item( team_slug: str, item_id: Union[UUID, str], params: QueryString = QueryString({}), -) -> Item: +) -> ItemCore: """ Returns an item @@ -106,14 +108,15 @@ def get_item( """ response = api_client.get(f"/v2/teams/{team_slug}/items/{item_id}", params) assert isinstance(response, dict) - return parse_obj_as(Item, response) + return parse_obj_as(ItemCore, response) def list_items( api_client: ClientCore, team_slug: str, - params: QueryString, -) -> Tuple[List[Item], List[ValidationError]]: + dataset_ids: int | list[int] | Literal["all"], + params: QueryString = QueryString({}), +) -> Tuple[List[ItemCore], List[ValidationError]]: """ Returns a list of items for the dataset @@ -133,15 +136,20 @@ def list_items( List[ValidationError] A list of ValidationError on failed objects """ - assert "dataset_ids" in params.value, "dataset_ids must be provided" + dataset_ids = ( + dataset_ids + if isinstance(dataset_ids, list) or dataset_ids == "all" + else [dataset_ids] + ) + params = params + QueryString({"dataset_ids": dataset_ids}) response = api_client.get(f"/v2/teams/{team_slug}/items", params) assert isinstance(response, dict) - items: List[Item] = [] + items: List[ItemCore] = [] exceptions: List[ValidationError] = [] for item in response["items"]: assert isinstance(item, dict) try: - items.append(parse_obj_as(Item, item)) + items.append(parse_obj_as(ItemCore, item)) except ValidationError as e: exceptions.append(e) return items, exceptions diff --git a/darwin/future/core/items/move_items.py b/darwin/future/core/items/move_items.py index b824a7f62..08b334b13 100644 --- a/darwin/future/core/items/move_items.py +++ b/darwin/future/core/items/move_items.py @@ -1,17 +1,20 @@ -from typing import List +from __future__ import annotations + +from typing import Dict, List from uuid import UUID from darwin.future.core.client import ClientCore from darwin.future.core.types.common import JSONType +from darwin.future.data_objects.typing import UnknownType def move_items_to_stage( - api_client: ClientCore, + client: ClientCore, team_slug: str, workflow_id: UUID, - dataset_id: int, + dataset_ids: int | List[int], stage_id: UUID, - item_ids: List[UUID], + filters: Dict[str, UnknownType] = {}, ) -> JSONType: """ Moves a list of items to a stage @@ -19,29 +22,35 @@ def move_items_to_stage( Parameters ---------- client: Client - The client to use for the request + The client to use for the request. team_slug: str - The slug of the team to move items for - dataset_id: str - The id or slug of the dataset to move items for - stage_id: str - The id or slug of the stage to move items to - item_ids: List[UUID] - A list of item ids to move to the stage + The slug of the team to move items for. + workflow_id: UUID + The id of the workflow to move items for. + dataset_ids: int | List[int] + The ID(s) of the dataset(s) containing the items. + stage_id: UUID + The id of the workflow to move items for. + filters: Dict[str, UnknownType] + Filter parameters. Returns ------- JSONType + The response data. """ - - return api_client.post( - f"/v2/teams/{team_slug}/items/stage", - { - "filters": { - "dataset_ids": [dataset_id], - "item_ids": [str(id) for id in item_ids], - }, - "stage_id": str(stage_id), - "workflow_id": str(workflow_id), + assert ( + filters + ), "No parameters provided, please provide at least one non-dataset id filter" + payload = { + "filters": { + "dataset_ids": dataset_ids + if isinstance(dataset_ids, list) + else [dataset_ids], + **filters, }, - ) + "stage_id": str(stage_id), + "workflow_id": str(workflow_id), + } + + return client.post(f"/v2/teams/{team_slug}/items/stage", data=payload) diff --git a/darwin/future/core/items/move_items_to_folder.py b/darwin/future/core/items/move_items_to_folder.py new file mode 100644 index 000000000..db8c5c0cf --- /dev/null +++ b/darwin/future/core/items/move_items_to_folder.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import Dict, List + +from darwin.future.core.client import ClientCore +from darwin.future.core.types.common import JSONType +from darwin.future.data_objects.typing import UnknownType + + +def move_list_of_items_to_folder( + client: ClientCore, + team_slug: str, + dataset_ids: int | List[int], + path: str, + filters: Dict[str, UnknownType] = {}, +) -> JSONType: + """ + Move specified items to a folder + + Parameters + ---------- + client: Client + The client to use for the request. + team_slug: str + The slug of the team containing the items. + dataset_ids: int | List[int] + The ID(s) of the dataset(s) containing the items. + path: str + The path to the folder to move the items to. + filters: Dict[str, UnknownType] + Filter parameters. + + Returns + ------- + JSONType + The response data. + """ + assert ( + filters + ), "No parameters provided, please provide at least one non-dataset id filter" + payload = { + "filters": { + "dataset_ids": dataset_ids + if isinstance(dataset_ids, list) + else [dataset_ids], + **filters, + } + } + + return client.post(f"/v2/teams/{team_slug}/items/path", data=payload) diff --git a/darwin/future/core/items/restore_items.py b/darwin/future/core/items/restore_items.py index 349cc7166..4aef6282c 100644 --- a/darwin/future/core/items/restore_items.py +++ b/darwin/future/core/items/restore_items.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Dict, List -from uuid import UUID from darwin.future.core.client import ClientCore from darwin.future.core.types.common import JSONType @@ -7,10 +8,9 @@ def restore_list_of_items( - api_client: ClientCore, + client: ClientCore, team_slug: str, - dataset_id: int, - item_ids: List[UUID], + dataset_ids: int | List[int], filters: Dict[str, UnknownType] = {}, ) -> JSONType: """ @@ -19,26 +19,29 @@ def restore_list_of_items( Parameters ---------- client: Client - The client to use for the request + The client to use for the request. team_slug: str - The slug of the team containing the items - dataset_id: int - The ID of the dataset containing the items - item_ids: List[UUID] - The IDs of the items to be restored + The slug of the team containing the items. + dataset_ids: int | List[int] + The ID(s) of the dataset(s) containing the items. filters: Dict[str, UnknownType] - Dataset filter parameters + Filter parameters. Returns ------- JSONType + The response data. """ + assert ( + filters + ), "No parameters provided, please provide at least one non-dataset id filter" payload = { "filters": { - "dataset_ids": [dataset_id], - "item_ids": [str(item_id) for item_id in item_ids], + "dataset_ids": dataset_ids + if isinstance(dataset_ids, list) + else [dataset_ids], **filters, } } - return api_client.post(f"/v2/teams/{team_slug}/items/restore", data=payload) + return client.post(f"/v2/teams/{team_slug}/items/restore", data=payload) diff --git a/darwin/future/core/items/set_item_layout.py b/darwin/future/core/items/set_item_layout.py new file mode 100644 index 000000000..2a2a3a837 --- /dev/null +++ b/darwin/future/core/items/set_item_layout.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import Dict + +from darwin.future.core.client import ClientCore +from darwin.future.core.types.common import JSONType +from darwin.future.data_objects.item import ItemLayout +from darwin.future.data_objects.typing import UnknownType + + +def set_item_layout( + client: ClientCore, + team_slug: str, + dataset_ids: int | list[int], + layout: ItemLayout, + filters: Dict[str, UnknownType], +) -> JSONType: + """ + Set the layout of a dataset and filtered items via filters. + + Args: + client (ClientCore): The Darwin Core client. + team_slug (str): The team slug. + dataset_ids (int | list[int]): The dataset ids. + layout (ItemLayout): The layout. + filters Dict[str, UnknownType]: The parameters of the filter. + + Returns: + JSONType: The response data. + """ + assert ( + filters + ), "No parameters provided, please provide at least one non-dataset id filter" + payload = { + "filters": { + "dataset_ids": dataset_ids + if isinstance(dataset_ids, list) + else [dataset_ids], + **filters, + }, + "layout": dict(layout), + } + + return client.post(f"/v2/teams/{team_slug}/items/layout", data=payload) diff --git a/darwin/future/core/items/set_item_priority.py b/darwin/future/core/items/set_item_priority.py index 7ec7124b0..b81bf5ccc 100644 --- a/darwin/future/core/items/set_item_priority.py +++ b/darwin/future/core/items/set_item_priority.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Dict, List -from uuid import UUID from darwin.future.core.client import ClientCore from darwin.future.core.types.common import JSONType @@ -7,10 +8,9 @@ def set_item_priority( - api_client: ClientCore, + client: ClientCore, team_slug: str, - dataset_id: int, - item_ids: List[UUID], + dataset_ids: int | List[int], priority: int, filters: Dict[str, UnknownType] = {}, ) -> JSONType: @@ -20,30 +20,32 @@ def set_item_priority( Parameters ---------- client: Client - The client to use for the request + The client to use for the request. team_slug: str - The slug of the team to set the priority for - dataset_id: int - The dataset to set the priority for - item_ids: List[UUID] - The item ids to set the priority for + The slug of the team containing the items. + dataset_id: int | List[int] + The ID(s) of the dataset(s) containing the items. priority: int - The priority to set + The priority to set. Returns ------- JSONType + The response data. """ + assert ( + filters + ), "No parameters provided, please provide at least one non-dataset id filter" payload = { - "priority": priority, "filters": { - "item_ids": [str(item_id) for item_id in item_ids], - "dataset_ids": [dataset_id], + "dataset_ids": dataset_ids + if isinstance(dataset_ids, list) + else [dataset_ids], **filters, - }, + } } - return api_client.post( - endpoint=f"/v2/teams/{team_slug}/items/priority", + return client.post( + f"/v2/teams/{team_slug}/items/priority", data=payload, ) diff --git a/darwin/future/core/types/common.py b/darwin/future/core/types/common.py index 489916509..9725c2d64 100644 --- a/darwin/future/core/types/common.py +++ b/darwin/future/core/types/common.py @@ -1,13 +1,20 @@ from __future__ import annotations -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Mapping, Protocol, Union from darwin.future.data_objects import validators as darwin_validators -from darwin.future.data_objects.typing import UnknownType JSONType = Union[Dict[str, Any], List[Dict[str, Any]]] # type: ignore +class Implements_str(Protocol): + def __str__(self) -> str: + ... + + +Stringable = Union[str, Implements_str] + + class TeamSlug(str): """ Represents a team slug, which is a string identifier for a team. @@ -73,18 +80,31 @@ class QueryString: Returns a string representation of the QueryString object, in the format "?key1=value1&key2=value2". """ - value: Dict[str, str] + value: dict[str, list[str] | str] - def dict_check(self, value: UnknownType) -> Dict[str, str]: - assert isinstance(value, dict) - assert all(isinstance(k, str) and isinstance(v, str) for k, v in value.items()) - return value + def dict_check( + self, value: Mapping[str, list[Stringable] | Stringable] + ) -> dict[str, list[str] | str]: + mapped: dict[str, list[str] | str] = {} + for k, v in value.items(): + if isinstance(v, list): + mapped[k] = [str(x) for x in v] + else: + mapped[k] = str(v) + return mapped - def __init__(self, value: Dict[str, str]) -> None: + def __init__(self, value: Mapping[str, list[Stringable] | Stringable]) -> None: self.value = self.dict_check(value) def __str__(self) -> str: - return "?" + "&".join(f"{k}={v}" for k, v in self.value.items()) + output: str = "?" if self.value else "" + for k, v in self.value.items(): + if isinstance(v, list): + for x in v: + output += f"{k}={x}&" + else: + output += f"{k}={v}&" + return output[:-1] # remove trailing & def __add__(self, other: QueryString) -> QueryString: return QueryString({**self.value, **other.value}) diff --git a/darwin/future/data_objects/item.py b/darwin/future/data_objects/item.py index b649c63a2..714b24610 100644 --- a/darwin/future/data_objects/item.py +++ b/darwin/future/data_objects/item.py @@ -15,7 +15,6 @@ def validate_no_slashes(v: UnknownType) -> str: assert isinstance(v, str), "Must be a string" assert len(v) > 0, "cannot be empty" assert "/" not in v, "cannot contain slashes" - assert " " not in v, "cannot contain spaces" return v @@ -75,7 +74,7 @@ def validate_fps(cls, values: dict) -> dict: elif isinstance(value, (int, float)): type = values.get("type") if type == "image": - assert value == 0, "fps must be 0 for images" + assert value == 0 or value == 1.0, "fps must be '0' or '1.0' for images" else: assert value >= 0, "fps must be greater than or equal to 0 for videos" @@ -127,7 +126,7 @@ def validate_name(cls, v: UnknownType) -> str: return validate_no_slashes(v) -class Item(DefaultDarwin): +class ItemCore(DefaultDarwin): # GraphotateWeb.Schemas.DatasetsV2.ItemRegistration.NewItem # Required fields diff --git a/darwin/future/exceptions.py b/darwin/future/exceptions.py index df48d8424..4bb22f744 100644 --- a/darwin/future/exceptions.py +++ b/darwin/future/exceptions.py @@ -2,6 +2,7 @@ from typing import Optional, Sequence + from darwin.future.data_objects.typing import KeyValuePairDict, UnknownType @@ -107,6 +108,10 @@ class UnrecognizableFileEncoding(DarwinException): pass +class BadRequest(DarwinException): + pass + + class MissingSlug(DarwinException): pass diff --git a/darwin/future/meta/objects/dataset.py b/darwin/future/meta/objects/dataset.py index e44eff0ae..3f17e15e4 100644 --- a/darwin/future/meta/objects/dataset.py +++ b/darwin/future/meta/objects/dataset.py @@ -2,6 +2,7 @@ from typing import List, Optional, Sequence, Union + from darwin.cli_functions import upload_data from darwin.dataset.upload_manager import LocalFile from darwin.datatypes import PathLike @@ -10,6 +11,7 @@ from darwin.future.data_objects.dataset import DatasetCore from darwin.future.helpers.assertion import assert_is from darwin.future.meta.objects.base import MetaBase +from darwin.future.meta.queries.item import ItemQuery from darwin.future.meta.queries.item_id import ItemIDQuery @@ -74,6 +76,11 @@ def item_ids(self) -> ItemIDQuery: meta_params = {"dataset_ids": self.id, **self.meta_params} return ItemIDQuery(self.client, meta_params=meta_params) + @property + def items(self) -> ItemQuery: + meta_params = {"dataset_ids": self.id, **self.meta_params} + return ItemQuery(self.client, meta_params=meta_params) + @classmethod def create_dataset(cls, client: ClientCore, slug: str) -> DatasetCore: """ diff --git a/darwin/future/meta/objects/item.py b/darwin/future/meta/objects/item.py new file mode 100644 index 000000000..7b438f721 --- /dev/null +++ b/darwin/future/meta/objects/item.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from typing import Dict, List, Optional, Union, cast +from uuid import UUID + +from darwin.future.core.items.delete_items import delete_list_of_items +from darwin.future.data_objects.item import ItemCore, ItemLayout, ItemSlot +from darwin.future.meta.objects.base import MetaBase + + +class Item(MetaBase[ItemCore]): + """ + Represents an item in a Darwin dataset. + + Args: + MetaBase (Stage): Generic MetaBase object expanded by ItemCore object + return type + + Attributes: + name (str): The name of the item. + id (UUID): The unique identifier of the item. + slots (List[ItemSlot]): A list of slots associated with the item. + path (str): The path of the item. + dataset_id (int): The ID of the dataset the item belongs to. + processing_status (str): The processing status of the item. + archived (Optional[bool]): Whether the item is archived or not. + priority (Optional[int]): The priority of the item. + tags (Optional[Union[List[str], Dict[str, str]]]): The tags associated with the item. + layout (Optional[ItemLayout]): The layout of the item. + + Methods: + delete(self) -> None: + Deletes the item from the Darwin dataset. + + Example usage: + # Get the item object + items = workflow.items.where(name='test').collect() # gets first page of items + + # Delete the items + [item.delete() for item in items] # will collect all pages of items and delete individually + + """ + + def delete(self) -> None: + team_slug, dataset_id = ( + self.meta_params["team_slug"], + self.meta_params["dataset_id"] + if "dataset_id" in self.meta_params + else self.meta_params["dataset_ids"], + ) + assert isinstance(team_slug, str) + dataset_id = cast(Union[int, List[int]], dataset_id) + filters = {"item_ids": [str(self.id)]} + delete_list_of_items(self.client, team_slug, dataset_id, filters) + + @property + def name(self) -> str: + return self._element.name + + @property + def id(self) -> UUID: + return self._element.id + + @property + def slots(self) -> List[ItemSlot]: + return self._element.slots + + @property + def path(self) -> str: + return self._element.path + + @property + def dataset_id(self) -> int: + return self._element.dataset_id + + @property + def processing_status(self) -> str: + return self._element.processing_status + + @property + def archived(self) -> Optional[bool]: + return self._element.archived + + @property + def priority(self) -> Optional[int]: + return self._element.priority + + @property + def tags(self) -> Optional[Union[List[str], Dict[str, str]]]: + return self._element.tags + + @property + def layout(self) -> Optional[ItemLayout]: + return self._element.layout diff --git a/darwin/future/meta/objects/stage.py b/darwin/future/meta/objects/stage.py index 86cb915c7..10e481079 100644 --- a/darwin/future/meta/objects/stage.py +++ b/darwin/future/meta/objects/stage.py @@ -9,6 +9,7 @@ from darwin.future.data_objects.workflow import WFEdgeCore, WFStageCore from darwin.future.exceptions import MaxRetriesError from darwin.future.meta.objects.base import MetaBase +from darwin.future.meta.queries.item import ItemQuery from darwin.future.meta.queries.item_id import ItemIDQuery @@ -45,6 +46,22 @@ class Stage(MetaBase[WFStageCore]): stage.move_attached_files_to_stage(new_stage_id=new_stage.id) """ + @property + def items(self) -> ItemQuery: + """Item ids attached to the stage + + Returns: + List[Item]: List of item ids + """ + assert self._element.id is not None + return ItemQuery( + self.client, + meta_params=self.meta_params, + filters=[ + QueryFilter(name="workflow_stage_ids", param=str(self._element.id)) + ], + ) + @property def item_ids(self) -> ItemIDQuery: """Item ids attached to the stage @@ -64,7 +81,7 @@ def item_ids(self) -> ItemIDQuery: def check_all_items_complete( self, slug: str, - item_ids: list[UUID], + item_ids: list[str], wait_max_attempts: int = 5, wait_time: float = 0.5, ) -> bool: @@ -73,7 +90,7 @@ def check_all_items_complete( Args: slug (str): Team slug - item_ids (list[UUID]): List of item ids + item_ids (list[str]): List of item ids max_attempts (int, optional): Max number of attempts. Defaults to 5. wait_time (float, optional): Wait time between attempts. Defaults to 0.5. """ @@ -114,22 +131,29 @@ def move_attached_files_to_stage( assert self.meta_params["dataset_id"] is not None and isinstance( self.meta_params["dataset_id"], int ) - slug, w_id, d_id = ( + team_slug, workflow_id, dataset_id = ( self.meta_params["team_slug"], self.meta_params["workflow_id"], self.meta_params["dataset_id"], ) - ids = [x.id for x in self.item_ids.collect_all()] + ids = [str(x.id) for x in self.item_ids.collect_all()] if wait: self.check_all_items_complete( - slug=slug, + slug=team_slug, item_ids=ids, wait_max_attempts=wait_max_attempts, wait_time=wait_time, ) - move_items_to_stage(self.client, slug, w_id, d_id, new_stage_id, ids) + move_items_to_stage( + self.client, + team_slug, + workflow_id, + dataset_id, + new_stage_id, + {"item_ids": ids}, + ) return self @property diff --git a/darwin/future/meta/objects/team.py b/darwin/future/meta/objects/team.py index 078693230..6bc82ecfa 100644 --- a/darwin/future/meta/objects/team.py +++ b/darwin/future/meta/objects/team.py @@ -9,6 +9,7 @@ from darwin.future.meta.objects.base import MetaBase from darwin.future.meta.objects.dataset import Dataset from darwin.future.meta.queries.dataset import DatasetQuery +from darwin.future.meta.queries.item import ItemQuery from darwin.future.meta.queries.team_member import TeamMemberQuery from darwin.future.meta.queries.workflow import WorkflowQuery @@ -95,6 +96,12 @@ def datasets(self) -> DatasetQuery: def workflows(self) -> WorkflowQuery: return WorkflowQuery(self.client, meta_params={"team_slug": self.slug}) + @property + def items(self) -> ItemQuery: + return ItemQuery( + self.client, meta_params={"team_slug": self.slug, "dataset_ids": "all"} + ) + @classmethod def delete_dataset(cls, client: ClientCore, dataset_id: Union[int, str]) -> int: """ diff --git a/darwin/future/meta/objects/workflow.py b/darwin/future/meta/objects/workflow.py index b07549d34..52d2e9f03 100644 --- a/darwin/future/meta/objects/workflow.py +++ b/darwin/future/meta/objects/workflow.py @@ -6,9 +6,11 @@ from darwin.cli_functions import upload_data from darwin.dataset.upload_manager import LocalFile from darwin.datatypes import PathLike +from darwin.future.core.types.query import QueryFilter from darwin.future.data_objects.workflow import WFDatasetCore, WFTypeCore, WorkflowCore from darwin.future.exceptions import MissingDataset from darwin.future.meta.objects.base import MetaBase +from darwin.future.meta.queries.item import ItemQuery from darwin.future.meta.queries.stage import Stage, StageQuery @@ -47,6 +49,14 @@ class Workflow(MetaBase[WorkflowCore]): datasets = workflow.datasets """ + @property + def items(self) -> ItemQuery: + return ItemQuery( + self.client, + meta_params=self.meta_params, + filters=[QueryFilter(name="workflow_id", param=str(self.id))], + ) + @property def stages(self) -> StageQuery: meta_params = self.meta_params.copy() diff --git a/darwin/future/meta/queries/item.py b/darwin/future/meta/queries/item.py new file mode 100644 index 000000000..5fd13e0e1 --- /dev/null +++ b/darwin/future/meta/queries/item.py @@ -0,0 +1,61 @@ +from functools import reduce +from typing import Dict + +from darwin.future.core.items.delete_items import delete_list_of_items +from darwin.future.core.items.get import list_items +from darwin.future.core.types.common import QueryString +from darwin.future.core.types.query import PaginatedQuery +from darwin.future.meta.objects.item import Item + + +class ItemQuery(PaginatedQuery[Item]): + def _collect(self) -> Dict[int, Item]: + if "team_slug" not in self.meta_params: + raise ValueError("Must specify team_slug to query items") + if ( + "dataset_ids" not in self.meta_params + and "dataset_id" not in self.meta_params + ): + raise ValueError("Must specify dataset_ids to query items") + dataset_ids = ( + self.meta_params["dataset_ids"] + if "dataset_ids" in self.meta_params + else self.meta_params["dataset_id"] + ) + team_slug = self.meta_params["team_slug"] + params: QueryString = reduce( + lambda s1, s2: s1 + s2, + [ + self.page.to_query_string(), + *[QueryString(f.to_dict()) for f in self.filters], + ], + ) + items_core, errors = list_items(self.client, team_slug, dataset_ids, params) + offset = self.page.offset + items = { + i + + offset: Item( + client=self.client, element=item, meta_params=self.meta_params + ) + for i, item in enumerate(items_core) + } + return items + + def delete(self) -> None: + if "team_slug" not in self.meta_params: + raise ValueError("Must specify team_slug to query items") + if ( + "dataset_ids" not in self.meta_params + and "dataset_id" not in self.meta_params + ): + raise ValueError("Must specify dataset_ids to query items") + dataset_ids = ( + self.meta_params["dataset_ids"] + if "dataset_ids" in self.meta_params + else self.meta_params["dataset_id"] + ) + team_slug = self.meta_params["team_slug"] + self.collect_all() + ids = [item.id for item in self] + filters = {"item_ids": [str(item) for item in ids]} + delete_list_of_items(self.client, team_slug, dataset_ids, filters) diff --git a/darwin/future/tests/core/datasets/test_create_dataset.py b/darwin/future/tests/core/datasets/test_create_dataset.py index 26630ac44..af9268665 100644 --- a/darwin/future/tests/core/datasets/test_create_dataset.py +++ b/darwin/future/tests/core/datasets/test_create_dataset.py @@ -1,10 +1,10 @@ import responses from pytest import raises -from requests import HTTPError from darwin.future.core.client import ClientCore from darwin.future.core.datasets import create_dataset from darwin.future.data_objects.dataset import DatasetCore +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * # noqa: F401, F403 from .fixtures import * # noqa: F401, F403 @@ -29,7 +29,7 @@ def test_it_creates_a_dataset( def test_it_raises_an_error_on_http_error( basic_dataset: DatasetCore, base_client: ClientCore ) -> None: - with raises(HTTPError): + with raises(BadRequest): with responses.RequestsMock() as rsps: rsps.add( rsps.POST, diff --git a/darwin/future/tests/core/datasets/test_delete_dataset.py b/darwin/future/tests/core/datasets/test_delete_dataset.py index f93eeeb01..4008b0fe8 100644 --- a/darwin/future/tests/core/datasets/test_delete_dataset.py +++ b/darwin/future/tests/core/datasets/test_delete_dataset.py @@ -1,9 +1,9 @@ import responses from pytest import raises -from requests import HTTPError from darwin.future.core.client import ClientCore from darwin.future.core.datasets import remove_dataset +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * from .fixtures import * @@ -26,7 +26,7 @@ def test_it_deletes_a_dataset(base_client: ClientCore) -> None: def test_it_throws_http_errors_returned_by_the_client(base_client: ClientCore) -> None: - with raises(HTTPError): + with raises(BadRequest): with responses.RequestsMock() as rsps: rsps.add( rsps.PUT, diff --git a/darwin/future/tests/core/datasets/test_get_dataset.py b/darwin/future/tests/core/datasets/test_get_dataset.py index 7fbe51d73..926df7123 100644 --- a/darwin/future/tests/core/datasets/test_get_dataset.py +++ b/darwin/future/tests/core/datasets/test_get_dataset.py @@ -1,11 +1,11 @@ import responses from pydantic import ValidationError from pytest import raises -from requests import HTTPError from darwin.future.core.client import ClientCore from darwin.future.core.datasets import get_dataset from darwin.future.data_objects.dataset import DatasetCore +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * from .fixtures import * @@ -34,7 +34,7 @@ def test_it_raises_an_error_on_http_error(base_client: ClientCore) -> None: json={}, status=400, ) - with raises(HTTPError): + with raises(BadRequest): get_dataset(base_client, "test-dataset") get_dataset(base_client, "test-dataset") diff --git a/darwin/future/tests/core/datasets/test_list_datasets.py b/darwin/future/tests/core/datasets/test_list_datasets.py index dcc84530a..b31cb5713 100644 --- a/darwin/future/tests/core/datasets/test_list_datasets.py +++ b/darwin/future/tests/core/datasets/test_list_datasets.py @@ -2,11 +2,11 @@ import pytest import responses -from requests.exceptions import HTTPError from darwin.future.core.client import ClientCore from darwin.future.core.datasets import list_datasets from darwin.future.data_objects.dataset import DatasetCore +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * from .fixtures import * @@ -42,7 +42,7 @@ def test_it_returns_an_error_if_the_client_returns_an_http_error( json={}, status=400, ) - with pytest.raises(HTTPError) as execinfo: + with pytest.raises(BadRequest) as execinfo: list_datasets(base_client) - assert execinfo.value.response.status_code == 400 # type: ignore + assert execinfo.value.args[0].status_code == 400 diff --git a/darwin/future/tests/core/fixtures.py b/darwin/future/tests/core/fixtures.py index 0d4782134..210042929 100644 --- a/darwin/future/tests/core/fixtures.py +++ b/darwin/future/tests/core/fixtures.py @@ -1,11 +1,13 @@ from pathlib import Path from typing import List +from uuid import uuid4 import orjson as json import pytest from darwin.future.core.client import ClientCore, DarwinConfig from darwin.future.data_objects.dataset import DatasetCore +from darwin.future.data_objects.item import ItemCore, ItemLayout, ItemSlot from darwin.future.data_objects.team import TeamCore, TeamMemberCore from darwin.future.data_objects.team_member_role import TeamMemberRole @@ -22,6 +24,36 @@ def base_config() -> DarwinConfig: ) +@pytest.fixture +def items_json(item_core_list: List[ItemCore]) -> List[dict]: + items: List[dict] = [] + for item in item_core_list: + temp = dict(item) + temp["id"] = str(temp["id"]) + temp["slots"] = [dict(slot) for slot in temp["slots"]] + temp["layout"] = dict(temp["layout"]) + items.append(temp) + return items + + +@pytest.fixture +def item_core_list() -> List[ItemCore]: + items = [] + for i in range(5): + slot = ItemSlot(slot_name=f"slot_{i}", file_name=f"file_{i}.jpg") + layout = ItemLayout(slots=[f"slot_{i}"], type="grid", version=1) + item = ItemCore( + name=f"item_{i}", + id=uuid4(), + slots=[slot], + dataset_id=i, + processing_status="processed", + layout=layout, + ) + items.append(item) + return items + + @pytest.fixture def base_client(base_config: DarwinConfig) -> ClientCore: return ClientCore(base_config) @@ -37,6 +69,28 @@ def base_team(base_team_json: dict) -> TeamCore: return TeamCore.parse_obj(base_team_json) +@pytest.fixture +def base_item_json() -> dict: + return { + "name": "test-item", + "id": "123e4567-e89b-12d3-a456-426655440000", + "slots": [ + {"slot_name": "slot1", "file_name": "file1.jpg", "fps": 30}, + {"slot_name": "slot2", "file_name": "file2.jpg", "fps": 24}, + ], + "path": "/", + "archived": False, + "priority": None, + "tags": [], + "layout": None, + } + + +@pytest.fixture +def base_item(base_item_json: dict) -> ItemCore: + return ItemCore.parse_obj(base_item_json) + + @pytest.fixture def base_team_member_json() -> dict: return { diff --git a/darwin/future/tests/core/items/fixtures.py b/darwin/future/tests/core/items/fixtures.py index b0fcb9402..330ec5f28 100644 --- a/darwin/future/tests/core/items/fixtures.py +++ b/darwin/future/tests/core/items/fixtures.py @@ -3,13 +3,20 @@ import pytest -from darwin.future.data_objects.item import Folder, Item +from darwin.future.data_objects.item import Folder, ItemCore, ItemLayout @pytest.fixture -def base_items() -> List[Item]: +def base_layout() -> ItemLayout: + return ItemLayout( + slots=["slot1", "slot2"], type="grid", layout_shape=[2, 1], version=2 + ) + + +@pytest.fixture +def base_items() -> List[ItemCore]: return [ - Item( + ItemCore( name=f"test_{i}", path="test_path", dataset_id=1, @@ -36,7 +43,7 @@ def base_folders() -> List[Folder]: @pytest.fixture -def base_items_json(base_items: List[Item]) -> List[dict]: +def base_items_json(base_items: List[ItemCore]) -> List[dict]: items = [item.dict() for item in base_items] # json library doesn't support UUIDs so need to be str'd for item in items: diff --git a/darwin/future/tests/core/items/test_archive_items.py b/darwin/future/tests/core/items/test_archive_items.py index da5b3740c..c0c335fa9 100644 --- a/darwin/future/tests/core/items/test_archive_items.py +++ b/darwin/future/tests/core/items/test_archive_items.py @@ -1,54 +1,81 @@ -from unittest.mock import Mock -from uuid import UUID - import pytest import responses -from darwin.exceptions import DarwinException from darwin.future.core.client import ClientCore from darwin.future.core.items.archive_items import archive_list_of_items +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * @responses.activate def test_archive_items_including_filters(base_client: ClientCore) -> None: - # Define the expected response + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + responses.add( responses.POST, base_client.config.api_endpoint + "v2/teams/test-team/items/archive", json={"affected_item_count": 2}, + status=200, ) - # Call the function response = archive_list_of_items( - api_client=base_client, - team_slug="test-team", - dataset_id=000000, - item_ids=[ - UUID("00000000-0000-0000-0000-000000000000"), - UUID("00000000-0000-0000-0000-000000000000"), - ], - filters={ - "not_statuses": ["uploading", "annotate"], - "not_assignees": [123, 456, 789], - }, + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, ) - # Check that the response mathces what we expect assert response == {"affected_item_count": 2} -def test_archive_items_with_error_response() -> None: - api_client = Mock(spec=ClientCore) - api_client.post.side_effect = DarwinException("Something went wrong") +@responses.activate +def test_archive_items_raises_on_incorrect_parameters( + base_client: ClientCore, +) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + + with pytest.raises(AssertionError): + archive_list_of_items( + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + ) + + +@responses.activate +def test_archive_items_with_error_response(base_client: ClientCore) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + + responses.add( + responses.POST, + base_client.config.api_endpoint + "v2/teams/test-team/items/archive", + json={"error": "Bad Request"}, + status=400, + ) - with pytest.raises(DarwinException): + with pytest.raises(BadRequest): archive_list_of_items( - api_client=api_client, - team_slug="test-team", - dataset_id=000000, - item_ids=[ - UUID("00000000-0000-0000-0000-000000000000"), - UUID("00000000-0000-0000-0000-000000000000"), - ], + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, ) diff --git a/darwin/future/tests/core/items/test_delete_items.py b/darwin/future/tests/core/items/test_delete_items.py index cf292d4ec..a72c2a26b 100644 --- a/darwin/future/tests/core/items/test_delete_items.py +++ b/darwin/future/tests/core/items/test_delete_items.py @@ -1,54 +1,86 @@ -from unittest.mock import Mock -from uuid import UUID +from typing import Dict import pytest import responses -from darwin.exceptions import DarwinException from darwin.future.core.client import ClientCore from darwin.future.core.items.delete_items import delete_list_of_items +from darwin.future.data_objects.typing import UnknownType +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * @responses.activate def test_delete_items_including_filters(base_client: ClientCore) -> None: - # Define the expected response + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters: Dict[str, UnknownType] = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + responses.add( responses.DELETE, base_client.config.api_endpoint + "v2/teams/test-team/items", json={"affected_item_count": 2}, + status=200, ) - # Call the function response = delete_list_of_items( - api_client=base_client, - team_slug="test-team", - dataset_id=000000, - item_ids=[ - UUID("00000000-0000-0000-0000-000000000000"), - UUID("00000000-0000-0000-0000-000000000000"), - ], - filters={ - "not_statuses": ["uploading", "annotate"], - "not_assignees": [123, 456, 789], - }, + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, ) - # Check that the response mathces what we expect assert response == {"affected_item_count": 2} -def test_delete_items_with_error_response() -> None: - api_client = Mock(spec=ClientCore) - api_client.delete.side_effect = DarwinException("Something went wrong") +@responses.activate +def test_delete_items_raises_on_incorrect_parameters( + base_client: ClientCore, +) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters: Dict[str, UnknownType] = {} + + with pytest.raises(AssertionError): + delete_list_of_items( + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, + ) + + +@responses.activate +def test_delete_items_with_error_response(base_client: ClientCore) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters: Dict[str, UnknownType] = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + + responses.add( + responses.DELETE, + base_client.config.api_endpoint + "v2/teams/test-team/items", + json={"error": "Bad Request"}, + status=400, + ) - with pytest.raises(DarwinException): + with pytest.raises(BadRequest): delete_list_of_items( - api_client=api_client, - team_slug="test-team", - dataset_id=000000, - item_ids=[ - UUID("00000000-0000-0000-0000-000000000000"), - UUID("00000000-0000-0000-0000-000000000000"), - ], + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, ) diff --git a/darwin/future/tests/core/items/test_get_items.py b/darwin/future/tests/core/items/test_get_items.py index 3e7a40386..cd174ea0b 100644 --- a/darwin/future/tests/core/items/test_get_items.py +++ b/darwin/future/tests/core/items/test_get_items.py @@ -8,7 +8,7 @@ from darwin.future.core.items import get_item_ids, get_item_ids_stage from darwin.future.core.items.get import get_item, list_folders, list_items from darwin.future.core.types.common import QueryString -from darwin.future.data_objects.item import Folder, Item +from darwin.future.data_objects.item import Folder, ItemCore from darwin.future.tests.core.fixtures import * from darwin.future.tests.core.items.fixtures import * @@ -45,7 +45,7 @@ def test_get_item_ids_stage( def test_get_item( - base_items_json: List[dict], base_items: List[Item], base_client: ClientCore + base_items_json: List[dict], base_items: List[ItemCore], base_client: ClientCore ) -> None: uuid = str(base_items[0].id) with responses.RequestsMock() as rsps: @@ -60,7 +60,7 @@ def test_get_item( def test_list_items( - base_items_json: List[dict], base_items: List[Item], base_client: ClientCore + base_items_json: List[dict], base_items: List[ItemCore], base_client: ClientCore ) -> None: with responses.RequestsMock() as rsps: rsps.add( @@ -70,9 +70,7 @@ def test_list_items( json={"items": base_items_json}, status=200, ) - items, _ = list_items( - base_client, "default-team", QueryString({"dataset_ids": "1337"}) - ) + items, _ = list_items(base_client, "default-team", dataset_ids=[1337]) for item, comparator in zip(items, base_items): assert item == comparator @@ -91,9 +89,7 @@ def test_list_items_breaks( json={"items": base_items_json}, status=200, ) - items, exceptions = list_items( - base_client, "default-team", QueryString({"dataset_ids": "1337"}) - ) + items, exceptions = list_items(base_client, "default-team", dataset_ids=[1337]) assert len(exceptions) == 1 assert isinstance(exceptions[0], ValidationError) diff --git a/darwin/future/tests/core/items/test_move_items.py b/darwin/future/tests/core/items/test_move_items.py index f9c357c76..d48665956 100644 --- a/darwin/future/tests/core/items/test_move_items.py +++ b/darwin/future/tests/core/items/test_move_items.py @@ -1,45 +1,95 @@ -from typing import Dict, List from uuid import UUID import pytest import responses from darwin.future.core.client import ClientCore -from darwin.future.core.items import move_items_to_stage +from darwin.future.core.items.move_items import move_items_to_stage +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * -from darwin.future.tests.core.items.fixtures import * - - -@pytest.fixture -def move_payload(UUIDs_str: List[str], stage_id: UUID, workflow_id: UUID) -> Dict: - return { - "filters": { - "dataset_ids": [1337], - "item_ids": UUIDs_str, - }, - "stage_id": str(stage_id), - "workflow_id": str(workflow_id), + + +@responses.activate +def test_move_items_to_stage_including_filters(base_client: ClientCore) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], } + workflow_id = UUID("00000000-0000-0000-0000-000000000000") + stage_id = UUID("00000000-0000-0000-0000-000000000000") + + responses.add( + responses.POST, + base_client.config.api_endpoint + "v2/teams/test-team/items/stage", + json={"affected_item_count": 2}, + status=200, + ) + + response = move_items_to_stage( + client=base_client, + team_slug=team_slug, + workflow_id=workflow_id, + dataset_ids=dataset_ids, + stage_id=stage_id, + filters=filters, + ) + assert response == {"affected_item_count": 2} -def test_move_items( + +@responses.activate +def test_move_items_to_stage_raises_on_incorrect_parameters( base_client: ClientCore, - move_payload: Dict, - stage_id: UUID, - workflow_id: UUID, - UUIDs_str: List[str], - UUIDs: List[UUID], ) -> None: - with responses.RequestsMock() as rsps: - rsps.add( - rsps.POST, - base_client.config.api_endpoint + "v2/teams/default-team/items/stage", - json={"success": UUIDs_str}, - status=200, - ) + dataset_ids = [1, 2, 3] + team_slug = "test-team" + workflow_id = UUID("00000000-0000-0000-0000-000000000000") + stage_id = UUID("00000000-0000-0000-0000-000000000000") + + with pytest.raises(AssertionError): move_items_to_stage( - base_client, "default-team", workflow_id, 1337, stage_id, UUIDs + client=base_client, + team_slug=team_slug, + workflow_id=workflow_id, + dataset_ids=dataset_ids, + stage_id=stage_id, ) - assert rsps.assert_call_count( - base_client.config.api_endpoint + "v2/teams/default-team/items/stage", 1 + + +@responses.activate +def test_move_items_to_stage_with_error_response(base_client: ClientCore) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + workflow_id = UUID("00000000-0000-0000-0000-000000000000") + stage_id = UUID("00000000-0000-0000-0000-000000000000") + + responses.add( + responses.POST, + base_client.config.api_endpoint + "v2/teams/test-team/items/stage", + json={"error": "Bad Request"}, + status=400, + ) + + with pytest.raises(BadRequest): + move_items_to_stage( + client=base_client, + team_slug=team_slug, + workflow_id=workflow_id, + dataset_ids=dataset_ids, + stage_id=stage_id, + filters=filters, ) diff --git a/darwin/future/tests/core/items/test_move_items_to_folder.py b/darwin/future/tests/core/items/test_move_items_to_folder.py new file mode 100644 index 000000000..9c55d5456 --- /dev/null +++ b/darwin/future/tests/core/items/test_move_items_to_folder.py @@ -0,0 +1,91 @@ +import pytest +import responses + +from darwin.future.core.client import ClientCore +from darwin.future.core.items.move_items_to_folder import move_list_of_items_to_folder +from darwin.future.exceptions import BadRequest +from darwin.future.tests.core.fixtures import * + + +@responses.activate +def test_move_list_of_items_to_folder_including_filters( + base_client: ClientCore, +) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + path = "/test/path" + + responses.add( + responses.POST, + base_client.config.api_endpoint + "v2/teams/test-team/items/path", + json={"affected_item_count": 2}, + status=200, + ) + + response = move_list_of_items_to_folder( + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, + path=path, + ) + + assert response == {"affected_item_count": 2} + + +@responses.activate +def test_move_list_of_items_to_folder_raises_on_incorrect_parameters( + base_client: ClientCore, +) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + path = "/test/path" + + with pytest.raises(AssertionError): + move_list_of_items_to_folder( + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + path=path, + ) + + +@responses.activate +def test_move_list_of_items_to_folders_with_error_response( + base_client: ClientCore, +) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + path = "/test/path" + + responses.add( + responses.POST, + base_client.config.api_endpoint + "v2/teams/test-team/items/path", + json={"error": "Bad Request"}, + status=400, + ) + + with pytest.raises(BadRequest): + move_list_of_items_to_folder( + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + path=path, + filters=filters, + ) diff --git a/darwin/future/tests/core/items/test_restore_items.py b/darwin/future/tests/core/items/test_restore_items.py index 63b998421..56e4c6df4 100644 --- a/darwin/future/tests/core/items/test_restore_items.py +++ b/darwin/future/tests/core/items/test_restore_items.py @@ -1,54 +1,81 @@ -from unittest.mock import Mock -from uuid import UUID - import pytest import responses -from darwin.exceptions import DarwinException from darwin.future.core.client import ClientCore from darwin.future.core.items.restore_items import restore_list_of_items +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * @responses.activate def test_restore_items_including_filters(base_client: ClientCore) -> None: - # Define the expected response + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + responses.add( responses.POST, base_client.config.api_endpoint + "v2/teams/test-team/items/restore", json={"affected_item_count": 2}, + status=200, ) - # Call the function response = restore_list_of_items( - api_client=base_client, - team_slug="test-team", - dataset_id=000000, - item_ids=[ - UUID("00000000-0000-0000-0000-000000000000"), - UUID("00000000-0000-0000-0000-000000000000"), - ], - filters={ - "not_statuses": ["uploading", "annotate"], - "not_assignees": [123, 456, 789], - }, + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, ) - # Check that the response mathces what we expect assert response == {"affected_item_count": 2} -def test_restore_items_with_error_response() -> None: - api_client = Mock(spec=ClientCore) - api_client.post.side_effect = DarwinException("Something went wrong") +@responses.activate +def test_restore_items_raises_on_incorrect_parameters( + base_client: ClientCore, +) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + + with pytest.raises(AssertionError): + restore_list_of_items( + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + ) + + +@responses.activate +def test_restore_items_with_error_response(base_client: ClientCore) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + + responses.add( + responses.POST, + base_client.config.api_endpoint + "v2/teams/test-team/items/restore", + json={"error": "Bad Request"}, + status=400, + ) - with pytest.raises(DarwinException): + with pytest.raises(BadRequest): restore_list_of_items( - api_client=api_client, - team_slug="test-team", - dataset_id=000000, - item_ids=[ - UUID("00000000-0000-0000-0000-000000000000"), - UUID("00000000-0000-0000-0000-000000000000"), - ], + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, ) diff --git a/darwin/future/tests/core/items/test_set_item_layout.py b/darwin/future/tests/core/items/test_set_item_layout.py new file mode 100644 index 000000000..3dddae791 --- /dev/null +++ b/darwin/future/tests/core/items/test_set_item_layout.py @@ -0,0 +1,88 @@ +from typing import Dict + +import pytest +import responses + +from darwin.future.core.client import ClientCore +from darwin.future.core.items.set_item_layout import set_item_layout +from darwin.future.data_objects.item import ItemLayout +from darwin.future.data_objects.typing import UnknownType +from darwin.future.exceptions import BadRequest +from darwin.future.tests.core.fixtures import * +from darwin.future.tests.core.items.fixtures import * + + +@responses.activate +def test_set_item_layout_returns_blank_object( + base_client: ClientCore, base_layout: ItemLayout +) -> None: + dataset_ids = [1, 2, 3] + params = {"param1": "value1", "param2": "value2"} + team_slug = "my_team" + responses.add( + responses.POST, + f"{base_client.config.api_endpoint}v2/teams/{team_slug}/items/layout", + json={"affected_item_count": 2}, + status=200, + ) + + response = set_item_layout(base_client, team_slug, dataset_ids, base_layout, params) + + assert response == {"affected_item_count": 2} + + +@responses.activate +def test_set_item_layout_raises_on_incorrect_parameters( + base_client: ClientCore, base_layout: ItemLayout +) -> None: + team_slug = "my_team" + dataset_ids = [1, 2, 3] + params: Dict[str, UnknownType] = {} + + with pytest.raises(AssertionError): + set_item_layout(base_client, team_slug, dataset_ids, base_layout, params) + + +@responses.activate +def test_set_item_layout_raises_on_4xx_status_code( + base_client: ClientCore, base_layout: ItemLayout +) -> None: + team_slug = "my_team" + dataset_ids = [1, 2, 3] + params = {"param1": "value1", "param2": "value2"} + + responses.add( + responses.POST, + f"{base_client.config.api_endpoint}v2/teams/{team_slug}/items/layout", + json={"error": "Bad Request"}, + status=400, + ) + + with pytest.raises(BadRequest): + set_item_layout(base_client, team_slug, dataset_ids, base_layout, params) + + +@responses.activate +def test_set_item_layout_sends_correct_payload( + base_client: ClientCore, base_layout: ItemLayout +) -> None: + team_slug = "my_team" + dataset_ids = [1, 2, 3] + params = {"param1": "value1", "param2": "value2"} + + responses.add( + responses.POST, + f"{base_client.config.api_endpoint}v2/teams/{team_slug}/items/layout", + json={}, + status=200, + match=[ + responses.json_params_matcher( + { + "filters": {"dataset_ids": dataset_ids, **params}, + "layout": dict(base_layout), + } + ) + ], + ) + + set_item_layout(base_client, team_slug, dataset_ids, base_layout, params) diff --git a/darwin/future/tests/core/items/test_set_priority.py b/darwin/future/tests/core/items/test_set_priority.py index b6aec7d77..9ea1a9daa 100644 --- a/darwin/future/tests/core/items/test_set_priority.py +++ b/darwin/future/tests/core/items/test_set_priority.py @@ -1,88 +1,87 @@ -from unittest.mock import Mock -from uuid import UUID - import pytest import responses -from darwin.exceptions import DarwinException from darwin.future.core.client import ClientCore from darwin.future.core.items.set_item_priority import set_item_priority +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * @responses.activate -def test_set_item_priority(base_client: ClientCore) -> None: +def test_set_item_priority_including_filters(base_client: ClientCore) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + priority = 100 + responses.add( responses.POST, base_client.config.api_endpoint + "v2/teams/test-team/items/priority", - json={"affected_item_count": 1}, + json={"affected_item_count": 2}, + status=200, ) response = set_item_priority( - base_client, - "test-team", - 123, - [UUID("00000000-0000-0000-0000-000000000000")], - 999, + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, + priority=priority, ) - assert response == {"affected_item_count": 1} + assert response == {"affected_item_count": 2} -def test_set_item_priority_with_filters() -> None: - base_client = Mock(spec=ClientCore) - - expected_payload = { - "priority": 10, - "filters": { - "dataset_ids": [123], - "item_ids": ["00000000-0000-0000-0000-000000000000"], - "status": "open", - }, - } - - # Define the expected endpoint - expected_endpoint = "/v2/teams/test-team/items/priority" - - # Define the expected response - expected_response = {"status": "success"} +@responses.activate +def test_set_item_priority_raises_on_incorrect_parameters( + base_client: ClientCore, +) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + priority = 100 + + with pytest.raises(AssertionError): + set_item_priority( + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + priority=priority, + ) - # Configure the mock API client to return the expected response - base_client.post.return_value = expected_response - # Call the function being tested - response = set_item_priority( - base_client, - "test-team", - 123, - [UUID("00000000-0000-0000-0000-000000000000")], - priority=10, - filters={"status": "open"}, - ) +@responses.activate +def test_set_item_priority_with_error_response(base_client: ClientCore) -> None: + dataset_ids = [1, 2, 3] + team_slug = "test-team" + filters = { + "not_statuses": ["uploading", "annotate"], + "not_assignees": [123, 456, 789], + "item_ids": [ + ("00000000-0000-0000-0000-000000000000"), + ("00000000-0000-0000-0000-000000000000"), + ], + } + priority = 100 - # Verify that the API client was called with the expected arguments - base_client.post.assert_called_once_with( - endpoint=expected_endpoint, - data=expected_payload, + responses.add( + responses.POST, + base_client.config.api_endpoint + "v2/teams/test-team/items/priority", + json={"error": "Bad Request"}, + status=400, ) - # Verify that the response matches the expected response - assert response == expected_response - - -def test_set_item_priority_with_error_response() -> None: - # Create a mock API client - api_client = Mock(spec=ClientCore) - - # Configure the mock API client to return the error response - api_client.post.side_effect = DarwinException("Something went wrong") - - # Call the function being tested - with pytest.raises(DarwinException): + with pytest.raises(BadRequest): set_item_priority( - api_client=api_client, - team_slug="test-team", - dataset_id=123, - item_ids=[UUID("00000000-0000-0000-0000-000000000000")], - priority=10, + client=base_client, + team_slug=team_slug, + dataset_ids=dataset_ids, + filters=filters, + priority=priority, ) diff --git a/darwin/future/tests/core/types/test_querystring.py b/darwin/future/tests/core/types/test_querystring.py index e92b0af94..4e471c15e 100644 --- a/darwin/future/tests/core/types/test_querystring.py +++ b/darwin/future/tests/core/types/test_querystring.py @@ -1,5 +1,3 @@ -from pytest import raises - from darwin.future.core.types.common import QueryString @@ -13,12 +11,23 @@ def test_querystring_happy_path() -> None: assert str(query_string_2) == "?foo=bar&baz=qux" query_string_3 = QueryString({}) - assert str(query_string_3) == "?" + assert str(query_string_3) == "" assert query_string.value == {"foo": "bar"} assert query_string_2.value == {"foo": "bar", "baz": "qux"} -def test_querystring_sad_path() -> None: - with raises(AssertionError): - QueryString({"foo": 1}) # type: ignore +def test_querystring_coerces_list() -> None: + query_string = QueryString({"foo": ["bar", "baz"]}) + assert str(query_string) == "?foo=bar&foo=baz" + assert query_string.value == {"foo": ["bar", "baz"]} + + +def test_querystring_coerces_stringable() -> None: + class Stringable: + def __str__(self) -> str: + return "bar" + + query_string = QueryString({"foo": Stringable()}) + assert str(query_string) == "?foo=bar" + assert query_string.value == {"foo": "bar"} diff --git a/darwin/future/tests/core/workflows/test_get_workflow.py b/darwin/future/tests/core/workflows/test_get_workflow.py index b073c0002..36d6923a1 100644 --- a/darwin/future/tests/core/workflows/test_get_workflow.py +++ b/darwin/future/tests/core/workflows/test_get_workflow.py @@ -1,12 +1,12 @@ import responses from pydantic import ValidationError from pytest import raises -from requests import HTTPError from darwin.future.core.client import ClientCore from darwin.future.core.types.common import JSONType from darwin.future.core.workflows import get_workflow from darwin.future.data_objects.workflow import WorkflowCore +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * @@ -84,5 +84,5 @@ def test_get_workflows_with_error(base_client: ClientCore) -> None: status=400 ) # fmt: on - with raises(HTTPError): + with raises(BadRequest): get_workflow(base_client, NON_EXISTENT_ID) diff --git a/darwin/future/tests/core/workflows/test_get_workflows.py b/darwin/future/tests/core/workflows/test_get_workflows.py index 9400c3658..aef85afea 100644 --- a/darwin/future/tests/core/workflows/test_get_workflows.py +++ b/darwin/future/tests/core/workflows/test_get_workflows.py @@ -3,12 +3,12 @@ import pytest import responses from pydantic import ValidationError -from requests import HTTPError from darwin.future.core.client import ClientCore from darwin.future.core.types.common import JSONType from darwin.future.core.workflows import get_workflows from darwin.future.data_objects.workflow import WorkflowCore +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * @@ -81,5 +81,5 @@ def test_get_workflows_with_error(base_client: ClientCore) -> None: ) # Call the function being tested - with pytest.raises(HTTPError): + with pytest.raises(BadRequest): get_workflows(base_client) diff --git a/darwin/future/tests/core/workflows/test_list_workflows.py b/darwin/future/tests/core/workflows/test_list_workflows.py index 23daf0ace..167c0799c 100644 --- a/darwin/future/tests/core/workflows/test_list_workflows.py +++ b/darwin/future/tests/core/workflows/test_list_workflows.py @@ -2,12 +2,12 @@ import responses from pydantic import ValidationError -from requests import HTTPError from darwin.future.core.client import ClientCore from darwin.future.core.types.common import JSONType from darwin.future.core.workflows import list_workflows from darwin.future.data_objects.workflow import WorkflowCore +from darwin.future.exceptions import BadRequest from darwin.future.tests.core.fixtures import * @@ -93,6 +93,6 @@ def test_list_workflows_with_error(base_client: ClientCore) -> None: assert isinstance(exceptions, List) assert len(exceptions) == 1 - assert isinstance(exceptions[0], HTTPError) + assert isinstance(exceptions[0], BadRequest) assert not workflows diff --git a/darwin/future/tests/meta/objects/fixtures.py b/darwin/future/tests/meta/objects/fixtures.py index 6aebae03d..d2c5d6890 100644 --- a/darwin/future/tests/meta/objects/fixtures.py +++ b/darwin/future/tests/meta/objects/fixtures.py @@ -5,15 +5,34 @@ from darwin.future.core.client import ClientCore from darwin.future.data_objects.dataset import DatasetCore +from darwin.future.data_objects.item import ItemCore from darwin.future.data_objects.team import TeamCore from darwin.future.data_objects.workflow import WFStageCore, WorkflowCore from darwin.future.meta.objects.dataset import Dataset +from darwin.future.meta.objects.item import Item from darwin.future.meta.objects.stage import Stage from darwin.future.meta.objects.team import Team from darwin.future.meta.objects.workflow import Workflow from darwin.future.tests.core.fixtures import * +@fixture +def items(base_client: ClientCore, item_core_list: List[ItemCore]) -> List[Item]: + return [ + Item( + client=base_client, + element=item, + meta_params={"team_slug": "test", "dataset_id": 1}, + ) + for item in item_core_list + ] + + +@fixture +def item(items: List[Item]) -> Item: + return items[0] + + @fixture def base_UUID() -> UUID: return UUID("00000000-0000-0000-0000-000000000000") @@ -48,3 +67,8 @@ def base_meta_dataset(base_client: ClientCore, base_dataset: DatasetCore) -> Dat return Dataset( client=base_client, element=base_dataset, meta_params={"team_slug": "test_team"} ) + + +@fixture +def base_meta_item(base_client: ClientCore, base_item: ItemCore) -> Item: + return Item(client=base_client, element=base_item) diff --git a/darwin/future/tests/meta/objects/test_itemmeta.py b/darwin/future/tests/meta/objects/test_itemmeta.py new file mode 100644 index 000000000..ada047eb2 --- /dev/null +++ b/darwin/future/tests/meta/objects/test_itemmeta.py @@ -0,0 +1,46 @@ +from uuid import UUID + +import responses +from responses import json_params_matcher + +from darwin.future.data_objects.item import ItemLayout, ItemSlot +from darwin.future.meta.objects.item import Item +from darwin.future.tests.meta.objects.fixtures import * + + +def test_item_properties(item: Item) -> None: + assert isinstance(item.name, str) + assert isinstance(item.id, UUID) + assert isinstance(item.slots, list) + for slot in item.slots: + assert isinstance(slot, ItemSlot) + assert isinstance(item.path, str) + assert isinstance(item.dataset_id, int) + assert isinstance(item.processing_status, str) + assert isinstance(item.archived, (bool, type(None))) + assert isinstance(item.priority, (int, type(None))) + assert isinstance(item.tags, (list, dict, type(None))) + assert isinstance(item.layout, (ItemLayout, type(None))) + + +def test_delete(item: Item) -> None: + with responses.RequestsMock() as rsps: + team_slug = item.meta_params["team_slug"] + dataset_id = item.meta_params["dataset_id"] + rsps.add( + rsps.DELETE, + item.client.config.api_endpoint + f"v2/teams/{team_slug}/items", + status=200, + match=[ + json_params_matcher( + { + "filters": { + "item_ids": [str(item.id)], + "dataset_ids": [dataset_id], + } + } + ) + ], + json={}, + ) + item.delete() diff --git a/darwin/future/tests/meta/objects/test_stagemeta.py b/darwin/future/tests/meta/objects/test_stagemeta.py index f5911eca3..502a9b3cb 100644 --- a/darwin/future/tests/meta/objects/test_stagemeta.py +++ b/darwin/future/tests/meta/objects/test_stagemeta.py @@ -8,6 +8,8 @@ from darwin.future.data_objects.workflow import WFEdgeCore, WFStageCore, WFTypeCore from darwin.future.meta.client import Client from darwin.future.meta.objects.stage import Stage +from darwin.future.meta.queries.item import ItemQuery +from darwin.future.meta.queries.item_id import ItemIDQuery from darwin.future.tests.core.fixtures import * from darwin.future.tests.core.items.fixtures import * from darwin.future.tests.meta.fixtures import * @@ -215,3 +217,12 @@ def test_stage_str_method(stage_meta: Stage) -> None: def test_stage_repr_method(stage_meta: Stage) -> None: assert repr(stage_meta) == str(stage_meta) + + +def test_has_item_properties(stage_meta: Stage) -> None: + assert isinstance(stage_meta.items, ItemQuery) + assert isinstance(stage_meta.item_ids, ItemIDQuery) + assert isinstance(stage_meta.id, UUID) + assert isinstance(stage_meta.name, str) + assert isinstance(stage_meta.type, str) + assert isinstance(stage_meta.edges, list) diff --git a/darwin/future/tests/meta/queries/test_item.py b/darwin/future/tests/meta/queries/test_item.py new file mode 100644 index 000000000..3824a8788 --- /dev/null +++ b/darwin/future/tests/meta/queries/test_item.py @@ -0,0 +1,72 @@ +from typing import List + +import pytest +import responses +from responses.matchers import json_params_matcher, query_param_matcher + +from darwin.future.core.client import ClientCore +from darwin.future.meta.objects.item import Item +from darwin.future.meta.queries.item import ItemQuery +from darwin.future.tests.core.fixtures import * +from darwin.future.tests.meta.fixtures import * +from darwin.future.tests.meta.objects.fixtures import * + + +@pytest.fixture +def item_query(base_client: ClientCore) -> ItemQuery: + return ItemQuery( + client=base_client, meta_params={"team_slug": "test", "dataset_id": 1} + ) + + +def test_item_query_collect(item_query: ItemQuery, items_json: List[dict]) -> None: + with responses.RequestsMock() as rsps: + rsps.add( + rsps.GET, + item_query.client.config.api_endpoint + "v2/teams/test/items", + match=[ + query_param_matcher( + {"page[offset]": "0", "page[size]": "500", "dataset_ids": "1"} + ) + ], + json={"items": items_json, "errors": []}, + ) + items = item_query.collect_all() + assert len(items) == 5 + for i in range(5): + assert items[i].name == f"item_{i}" + + +def test_delete( + item_query: ItemQuery, items_json: List[dict], items: List[Item] +) -> None: + with responses.RequestsMock() as rsps: + rsps.add( + rsps.GET, + item_query.client.config.api_endpoint + "v2/teams/test/items", + match=[ + query_param_matcher( + {"page[offset]": "0", "page[size]": "500", "dataset_ids": "1"} + ) + ], + json={"items": items_json, "errors": []}, + ) + team_slug = items[0].meta_params["team_slug"] + dataset_id = items[0].meta_params["dataset_id"] + rsps.add( + rsps.DELETE, + items[0].client.config.api_endpoint + f"v2/teams/{team_slug}/items", + status=200, + match=[ + json_params_matcher( + { + "filters": { + "item_ids": [str(item.id) for item in items], + "dataset_ids": [dataset_id], + } + } + ) + ], + json={}, + ) + item_query.delete() diff --git a/darwin/future/tests/meta/queries/test_team_id.py b/darwin/future/tests/meta/queries/test_item_id.py similarity index 100% rename from darwin/future/tests/meta/queries/test_team_id.py rename to darwin/future/tests/meta/queries/test_item_id.py