Skip to content

Commit

Permalink
[PY-576][PY-581] Create + Update of properties (#756)
Browse files Browse the repository at this point in the history
* create update scaffold

* create + property changes

* linting

* linting

* typing

* create + update: including e2e tests

* removing unnecessary import

* removing bad comments

* test cleanup

* linting

* unit tests
  • Loading branch information
Nathanjp91 authored Dec 18, 2023
1 parent a2b61f6 commit dc3f887
Show file tree
Hide file tree
Showing 18 changed files with 731 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"editor.formatOnSave": true,
"editor.tabSize": 4,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.organizeImports": "explicit"
},
"editor.defaultFormatter": "ms-python.black-formatter"
},
Expand Down
2 changes: 2 additions & 0 deletions darwin/future/core/properties/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from darwin.future.core.properties.create import create_property
from darwin.future.core.properties.get import (
get_property_by_id,
get_team_full_properties,
get_team_properties,
)
from darwin.future.core.properties.update import update_property, update_property_value
36 changes: 36 additions & 0 deletions darwin/future/core/properties/create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Optional, Union

from pydantic import parse_obj_as

from darwin.future.core.client import ClientCore
from darwin.future.core.types.common import JSONDict
from darwin.future.data_objects.properties import FullProperty


def create_property(
client: ClientCore,
params: Union[FullProperty, JSONDict],
team_slug: Optional[str] = None,
) -> FullProperty:
"""
Creates a property for the specified team slug.
Parameters:
client (ClientCore): The client to use for the request.
team_slug (Optional[str]): The slug of the team to get. If not specified, the
default team from the client's config will be used.
params (Optional[JSONType]): The JSON data to use for the request.
Returns:
FullProperty: FullProperty object for the created property.
Raises:
HTTPError: If the response status code is not in the 200-299 range.
"""
if not team_slug:
team_slug = client.config.default_team
if isinstance(params, FullProperty):
params = params.to_create_endpoint()
response = client.post(f"/v2/teams/{team_slug}/properties", data=params)
assert isinstance(response, dict)
return parse_obj_as(FullProperty, response)
2 changes: 1 addition & 1 deletion darwin/future/core/properties/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@ def get_property_by_id(
"""
if not team_slug:
team_slug = client.config.default_team
response = client.get(f"/v2/teams/{team_slug}/properties/{property_id}")
response = client.get(f"/v2/teams/{team_slug}/properties/{str(property_id)}")
assert isinstance(response, dict)
return parse_obj_as(FullProperty, response)
74 changes: 74 additions & 0 deletions darwin/future/core/properties/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from typing import Optional, Union

from pydantic import parse_obj_as

from darwin.future.core.client import ClientCore
from darwin.future.core.types.common import JSONDict
from darwin.future.data_objects.properties import FullProperty, PropertyValue


def update_property(
client: ClientCore,
params: Union[FullProperty, JSONDict],
team_slug: Optional[str] = None,
) -> FullProperty:
"""
Updates a property for the specified team slug.
Parameters:
client (ClientCore): The client to use for the request.
team_slug (Optional[str]): The slug of the team to get. If not specified, the
default team from the client's config will be used.
params (Optional[JSONType]): The JSON data to use for the request.
Returns:
FullProperty: FullProperty object for the created property.
Raises:
HTTPError: If the response status code is not in the 200-299 range.
"""
if not team_slug:
team_slug = client.config.default_team
if isinstance(params, FullProperty):
id, params = params.to_update_endpoint()
else:
id = params.get("id")
del params["id"]
response = client.put(f"/v2/teams/{team_slug}/properties/{id}", data=params)
assert isinstance(response, dict)
return parse_obj_as(FullProperty, response)


def update_property_value(
client: ClientCore,
params: Union[PropertyValue, JSONDict],
item_id: str,
team_slug: Optional[str] = None,
) -> PropertyValue:
"""
Updates a property value for the specified property id.
Parameters:
client (ClientCore): The client to use for the request.
team_slug (Optional[str]): The slug of the team to get. If not specified, the
default team from the client's config will be used.
params (Optional[JSONType]): The JSON data to use for the request.
Returns:
FullProperty: FullProperty object for the created property.
Raises:
HTTPError: If the response status code is not in the 200-299 range.
"""
if not team_slug:
team_slug = client.config.default_team
if isinstance(params, PropertyValue):
id, params = params.to_update_endpoint()
else:
id = params.get("id")
del params["id"]
response = client.put(
f"/v2/teams/{team_slug}/properties/{item_id}/property_values/{id}", data=params
)
assert isinstance(response, dict)
return parse_obj_as(PropertyValue, response)
1 change: 1 addition & 0 deletions darwin/future/core/types/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from darwin.future.data_objects import validators as darwin_validators

JSONType = Union[Dict[str, Any], List[Dict[str, Any]]] # type: ignore
JSONDict = Dict[str, Any] # type: ignore


class Implements_str(Protocol):
Expand Down
63 changes: 54 additions & 9 deletions darwin/future/data_objects/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,23 @@
import json
import os
from pathlib import Path
from typing import Dict, List, Optional, Union
from typing import Dict, List, Literal, Optional, Tuple, Union

from pydantic import validator

from darwin.future.data_objects.pydantic_base import DefaultDarwin

PropertyType = Literal[
"multi_select",
"single_select",
"text",
"attributes",
"instance_id",
"directional_vector",
]

class PropertyOption(DefaultDarwin):

class PropertyValue(DefaultDarwin):
"""
Describes a single option for a property
Expand All @@ -25,16 +34,29 @@ class PropertyOption(DefaultDarwin):

id: Optional[str]
position: Optional[int]
type: str
type: Literal["string"] = "string"
value: Union[Dict[str, str], str]
color: str
color: str = "auto"

@validator("color")
def validate_rgba(cls, v: str) -> str:
if not v.startswith("rgba"):
raise ValueError("Color must be in rgba format")
if not v.startswith("rgba") and v != "auto":
raise ValueError("Color must be in rgba format or 'auto'")
return v

@validator("value")
def validate_value(cls, v: Union[Dict[str, str], str]) -> Dict[str, str]:
"""TODO: Replace once the value.value bug is fixed in the API"""
if isinstance(v, str):
return {"value": v}
return v

def to_update_endpoint(self) -> Tuple[str, dict]:
if self.id is None:
raise ValueError("id must be set")
updated_base = self.dict(exclude={"id", "type"})
return self.id, updated_base


class FullProperty(DefaultDarwin):
"""
Expand All @@ -49,14 +71,37 @@ class FullProperty(DefaultDarwin):

id: Optional[str]
name: str
type: str
type: PropertyType
description: Optional[str]
required: bool
slug: Optional[str]
team_id: Optional[int]
annotation_class_id: Optional[int]
property_values: Optional[List[PropertyOption]]
options: Optional[List[PropertyOption]]
property_values: Optional[List[PropertyValue]]
options: Optional[List[PropertyValue]]

def to_create_endpoint(
self,
) -> dict:
if self.annotation_class_id is None:
raise ValueError("annotation_class_id must be set")
return self.dict(
include={
"name": True,
"type": True,
"required": True,
"annotation_class_id": True,
"property_values": {"__all__": {"type", "value", "color"}},
"description": True,
}
)

def to_update_endpoint(self) -> Tuple[str, dict]:
if self.id is None:
raise ValueError("id must be set")
updated_base = self.to_create_endpoint()
del updated_base["annotation_class_id"] # can't update this field
return self.id, updated_base


class MetaDataClass(DefaultDarwin):
Expand Down
14 changes: 7 additions & 7 deletions darwin/future/tests/core/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@
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.properties import FullProperty, PropertyOption
from darwin.future.data_objects.properties import FullProperty, PropertyValue
from darwin.future.data_objects.team import TeamCore, TeamMemberCore
from darwin.future.data_objects.team_member_role import TeamMemberRole
from darwin.future.data_objects.workflow import WorkflowCore


@pytest.fixture
def base_property_option() -> PropertyOption:
return PropertyOption(
def base_property_value() -> PropertyValue:
return PropertyValue(
id="0",
position=0,
type="text",
type="string",
value="test-value",
color="rgba(0,0,0,0)",
)


@pytest.fixture
def base_property_object(base_property_option: PropertyOption) -> FullProperty:
def base_property_object(base_property_value: PropertyValue) -> FullProperty:
return FullProperty(
id="0",
name="test-property",
Expand All @@ -36,8 +36,8 @@ def base_property_object(base_property_option: PropertyOption) -> FullProperty:
slug="test-property",
team_id=0,
annotation_class_id=0,
property_values=[base_property_option],
options=[base_property_option],
property_values=[base_property_value],
options=[base_property_value],
)


Expand Down
51 changes: 51 additions & 0 deletions darwin/future/tests/core/properties/test_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import responses

from darwin.future.core.client import ClientCore
from darwin.future.core.properties import create_property
from darwin.future.data_objects.properties import FullProperty
from darwin.future.tests.core.fixtures import *


@responses.activate
def test_create_property(
base_client: ClientCore, base_property_object: FullProperty
) -> None:
# Mocking the response using responses library
responses.add(
responses.POST,
f"{base_client.config.base_url}api/v2/teams/{base_client.config.default_team}/properties",
json=base_property_object.dict(),
status=200,
)
# Call the function being tested
property = create_property(
base_client,
params=base_property_object,
team_slug=base_client.config.default_team,
)

# Assertions
assert isinstance(property, FullProperty)
assert property == base_property_object


@responses.activate
def test_create_property_from_json(
base_client: ClientCore, base_property_object: FullProperty
) -> None:
json = base_property_object.to_create_endpoint()
# Mocking the response using responses library
responses.add(
responses.POST,
f"{base_client.config.base_url}api/v2/teams/{base_client.config.default_team}/properties",
json=base_property_object.dict(),
status=200,
)
# Call the function being tested
property = create_property(
base_client, params=json, team_slug=base_client.config.default_team
)

# Assertions
assert isinstance(property, FullProperty)
assert property == base_property_object
57 changes: 57 additions & 0 deletions darwin/future/tests/core/properties/test_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import responses

from darwin.future.core.client import ClientCore
from darwin.future.core.properties import update_property, update_property_value
from darwin.future.data_objects.properties import FullProperty, PropertyValue
from darwin.future.tests.core.fixtures import *


@responses.activate
def test_update_property(
base_client: ClientCore, base_property_object: FullProperty
) -> None:
# Mocking the response using responses library
responses.add(
responses.PUT,
f"{base_client.config.base_url}api/v2/teams/{base_client.config.default_team}/properties/{base_property_object.id}",
json=base_property_object.dict(),
status=200,
)
# Call the function being tested
property = update_property(
base_client,
params=base_property_object,
team_slug=base_client.config.default_team,
)

# Assertions
assert isinstance(property, FullProperty)
assert property == base_property_object


@responses.activate
def test_update_property_value(
base_client: ClientCore, base_property_object: FullProperty
) -> None:
# Mocking the response using responses library
item_id = base_property_object.id
assert item_id
assert base_property_object.property_values
pv = base_property_object.property_values[0]
responses.add(
responses.PUT,
f"{base_client.config.base_url}api/v2/teams/{base_client.config.default_team}/properties/{item_id}/property_values/{pv.id}",
json=pv.dict(),
status=200,
)
# Call the function being tested
property_value = update_property_value(
base_client,
params=pv,
item_id=item_id,
team_slug=base_client.config.default_team,
)

# Assertions
assert isinstance(property_value, PropertyValue)
assert property_value == pv
Loading

0 comments on commit dc3f887

Please sign in to comment.