Skip to content

Commit 19af381

Browse files
authored
Merge pull request #53 from scaleapi/da/repr2
Improve reprs for all objects in API
2 parents f6f74ae + 99e6969 commit 19af381

21 files changed

+333
-160
lines changed

.DS_Store

0 Bytes
Binary file not shown.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,7 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
132+
# Mac OS desktop services store
133+
.DS_Store

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2021 Scale AI
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

conftest.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# https://github.com/gevent/gevent/issues/1016#issuecomment-328530533
77
# https://github.com/spyoungtech/grequests/issues/8
88
import grequests
9+
910
################
1011

1112
import logging
@@ -19,26 +20,28 @@
1920

2021
from tests.helpers import TEST_DATASET_NAME, TEST_DATASET_ITEMS
2122

22-
assert 'NUCLEUS_PYTEST_API_KEY' in os.environ, \
23-
"You must set the 'NUCLEUS_PYTEST_API_KEY' environment variable to a valid " \
23+
assert "NUCLEUS_PYTEST_API_KEY" in os.environ, (
24+
"You must set the 'NUCLEUS_PYTEST_API_KEY' environment variable to a valid "
2425
"Nucleus API key to run the test suite"
26+
)
2527

26-
API_KEY = os.environ['NUCLEUS_PYTEST_API_KEY']
28+
API_KEY = os.environ["NUCLEUS_PYTEST_API_KEY"]
2729

2830

29-
@pytest.fixture(scope='session')
31+
@pytest.fixture(scope="session")
3032
def monkeypatch_session(request):
31-
""" This workaround is needed to allow monkeypatching in session-scoped fixtures.
33+
"""This workaround is needed to allow monkeypatching in session-scoped fixtures.
3234
3335
See https://github.com/pytest-dev/pytest/issues/363
3436
"""
3537
from _pytest.monkeypatch import MonkeyPatch
38+
3639
mpatch = MonkeyPatch()
3740
yield mpatch
3841
mpatch.undo()
3942

4043

41-
@pytest.fixture(scope='session')
44+
@pytest.fixture(scope="session")
4245
def CLIENT(monkeypatch_session):
4346
client = nucleus.NucleusClient(API_KEY)
4447

@@ -49,14 +52,16 @@ def _make_request_patch(
4952
payload: dict, route: str, requests_command=requests.post
5053
) -> dict:
5154
response = client._make_request_raw(payload, route, requests_command)
52-
assert response.status_code in SUCCESS_STATUS_CODES, \
53-
f"HTTP response had status code: {response.status_code}. " \
55+
assert response.status_code in SUCCESS_STATUS_CODES, (
56+
f"HTTP response had status code: {response.status_code}. "
5457
f"Full JSON: {response.json()}"
58+
)
5559
return response.json()
5660

5761
monkeypatch_session.setattr(client, "_make_request", _make_request_patch)
5862
return client
5963

64+
6065
@pytest.fixture()
6166
def dataset(CLIENT):
6267
ds = CLIENT.create_dataset(TEST_DATASET_NAME)

nucleus/__init__.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,11 @@
6666
from requests.adapters import HTTPAdapter
6767

6868
# pylint: disable=E1101
69+
# TODO: refactor to reduce this file to under 1000 lines.
70+
# pylint: disable=C0302
6971
from requests.packages.urllib3.util.retry import Retry
7072

73+
from .constants import REFERENCE_IDS_KEY, DATASET_ITEM_IDS_KEY
7174
from .dataset import Dataset
7275
from .dataset_item import DatasetItem
7376
from .annotation import (
@@ -146,9 +149,19 @@ class NucleusClient:
146149
def __init__(self, api_key: str, use_notebook: bool = False):
147150
self.api_key = api_key
148151
self.tqdm_bar = tqdm.tqdm
152+
self._use_notebook = use_notebook
149153
if use_notebook:
150154
self.tqdm_bar = tqdm_notebook.tqdm
151155

156+
def __repr__(self):
157+
return f"NucleusClient(api_key='{self.api_key}', use_notebook={self._use_notebook})"
158+
159+
def __eq__(self, other):
160+
if self.api_key == other.api_key:
161+
if self._use_notebook == other._use_notebook:
162+
return True
163+
return False
164+
152165
def list_models(self) -> List[Model]:
153166
"""
154167
Lists available models in your repo.
@@ -962,6 +975,42 @@ def delete_slice(self, slice_id: str) -> dict:
962975
)
963976
return response
964977

978+
def append_to_slice(
979+
self,
980+
slice_id: str,
981+
dataset_item_ids: List[str] = None,
982+
reference_ids: List[str] = None,
983+
) -> dict:
984+
"""
985+
Appends to a slice from items already present in a dataset.
986+
The caller must exclusively use either datasetItemIds or reference_ids
987+
as a means of identifying items in the dataset.
988+
989+
:param
990+
dataset_item_ids: List[str],
991+
reference_ids: List[str],
992+
993+
:return:
994+
{
995+
"slice_id": str,
996+
}
997+
"""
998+
if dataset_item_ids and reference_ids:
999+
raise Exception(
1000+
"You cannot specify both dataset_item_ids and reference_ids"
1001+
)
1002+
1003+
ids_to_append: Dict[str, Any] = {}
1004+
if dataset_item_ids:
1005+
ids_to_append[DATASET_ITEM_IDS_KEY] = dataset_item_ids
1006+
if reference_ids:
1007+
ids_to_append[REFERENCE_IDS_KEY] = reference_ids
1008+
1009+
response = self._make_request(
1010+
ids_to_append, f"slice/{slice_id}/append"
1011+
)
1012+
return response
1013+
9651014
def list_autotags(self, dataset_id: str) -> List[str]:
9661015
"""
9671016
Fetches a list of autotags for a given dataset id

nucleus/annotation.py

Lines changed: 50 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,16 @@
2121
ANNOTATIONS_KEY,
2222
)
2323

24+
from dataclasses import dataclass
25+
2426

2527
class Annotation:
28+
def _check_ids(self):
29+
if bool(self.reference_id) == bool(self.item_id):
30+
raise Exception(
31+
"You must specify either a reference_id or an item_id for an annotation."
32+
)
33+
2634
@classmethod
2735
def from_json(cls, payload: dict):
2836
if payload.get(TYPE_KEY, None) == BOX_TYPE:
@@ -33,16 +41,11 @@ def from_json(cls, payload: dict):
3341
return SegmentationAnnotation.from_json(payload)
3442

3543

44+
@dataclass
3645
class Segment:
37-
def __init__(
38-
self, label: str, index: int, metadata: Optional[dict] = None
39-
):
40-
self.label = label
41-
self.index = index
42-
self.metadata = metadata
43-
44-
def __str__(self):
45-
return str(self.to_payload())
46+
label: str
47+
index: int
48+
metadata: Optional[dict] = None
4649

4750
@classmethod
4851
def from_json(cls, payload: dict):
@@ -62,35 +65,25 @@ def to_payload(self) -> dict:
6265
return payload
6366

6467

68+
@dataclass
6569
class SegmentationAnnotation(Annotation):
66-
def __init__(
67-
self,
68-
mask_url: str,
69-
annotations: List[Segment],
70-
reference_id: Optional[str] = None,
71-
item_id: Optional[str] = None,
72-
annotation_id: Optional[str] = None,
73-
):
74-
super().__init__()
75-
if not mask_url:
70+
mask_url: str
71+
annotations: List[Segment]
72+
reference_id: Optional[str] = None
73+
item_id: Optional[str] = None
74+
annotation_id: Optional[str] = None
75+
76+
def __post_init__(self):
77+
if not self.mask_url:
7678
raise Exception("You must specify a mask_url.")
77-
if bool(reference_id) == bool(item_id):
78-
raise Exception(
79-
"You must specify either a reference_id or an item_id for an annotation."
80-
)
81-
self.mask_url = mask_url
82-
self.annotations = annotations
83-
self.reference_id = reference_id
84-
self.item_id = item_id
85-
self.annotation_id = annotation_id
86-
87-
def __str__(self):
88-
return str(self.to_payload())
79+
self._check_ids()
8980

9081
@classmethod
9182
def from_json(cls, payload: dict):
83+
if MASK_URL_KEY not in payload:
84+
raise ValueError(f"Missing {MASK_URL_KEY} in json")
9285
return cls(
93-
mask_url=payload.get(MASK_URL_KEY),
86+
mask_url=payload[MASK_URL_KEY],
9487
annotations=[
9588
Segment.from_json(ann)
9689
for ann in payload.get(ANNOTATIONS_KEY, [])
@@ -118,35 +111,21 @@ class AnnotationTypes(Enum):
118111
POLYGON = POLYGON_TYPE
119112

120113

121-
# TODO: Add base annotation class to reduce repeated code here
114+
@dataclass
122115
class BoxAnnotation(Annotation):
123-
# pylint: disable=too-many-instance-attributes
124-
def __init__(
125-
self,
126-
label: str,
127-
x: Union[float, int],
128-
y: Union[float, int],
129-
width: Union[float, int],
130-
height: Union[float, int],
131-
reference_id: Optional[str] = None,
132-
item_id: Optional[str] = None,
133-
annotation_id: Optional[str] = None,
134-
metadata: Optional[Dict] = None,
135-
):
136-
super().__init__()
137-
if bool(reference_id) == bool(item_id):
138-
raise Exception(
139-
"You must specify either a reference_id or an item_id for an annotation."
140-
)
141-
self.label = label
142-
self.x = x
143-
self.y = y
144-
self.width = width
145-
self.height = height
146-
self.reference_id = reference_id
147-
self.item_id = item_id
148-
self.annotation_id = annotation_id
149-
self.metadata = metadata if metadata else {}
116+
label: str
117+
x: Union[float, int]
118+
y: Union[float, int]
119+
width: Union[float, int]
120+
height: Union[float, int]
121+
reference_id: Optional[str] = None
122+
item_id: Optional[str] = None
123+
annotation_id: Optional[str] = None
124+
metadata: Optional[Dict] = None
125+
126+
def __post_init__(self):
127+
self._check_ids()
128+
self.metadata = self.metadata if self.metadata else {}
150129

151130
@classmethod
152131
def from_json(cls, payload: dict):
@@ -178,32 +157,20 @@ def to_payload(self) -> dict:
178157
METADATA_KEY: self.metadata,
179158
}
180159

181-
def __str__(self):
182-
return str(self.to_payload())
183-
184160

185161
# TODO: Add Generic type for 2D point
162+
@dataclass
186163
class PolygonAnnotation(Annotation):
187-
def __init__(
188-
self,
189-
label: str,
190-
vertices: List[Any],
191-
reference_id: Optional[str] = None,
192-
item_id: Optional[str] = None,
193-
annotation_id: Optional[str] = None,
194-
metadata: Optional[Dict] = None,
195-
):
196-
super().__init__()
197-
if bool(reference_id) == bool(item_id):
198-
raise Exception(
199-
"You must specify either a reference_id or an item_id for an annotation."
200-
)
201-
self.label = label
202-
self.vertices = vertices
203-
self.reference_id = reference_id
204-
self.item_id = item_id
205-
self.annotation_id = annotation_id
206-
self.metadata = metadata if metadata else {}
164+
label: str
165+
vertices: List[Any]
166+
reference_id: Optional[str] = None
167+
item_id: Optional[str] = None
168+
annotation_id: Optional[str] = None
169+
metadata: Optional[Dict] = None
170+
171+
def __post_init__(self):
172+
self._check_ids()
173+
self.metadata = self.metadata if self.metadata else {}
207174

208175
@classmethod
209176
def from_json(cls, payload: dict):
@@ -226,6 +193,3 @@ def to_payload(self) -> dict:
226193
ANNOTATION_ID_KEY: self.annotation_id,
227194
METADATA_KEY: self.metadata,
228195
}
229-
230-
def __str__(self):
231-
return str(self.to_payload())

nucleus/dataset.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ def __init__(self, dataset_id: str, client):
3232
self.id = dataset_id
3333
self._client = client
3434

35+
def __repr__(self):
36+
return f"Dataset(dataset_id='{self.id}', client={self._client})"
37+
38+
def __eq__(self, other):
39+
if self.id == other.id:
40+
if self._client == other._client:
41+
return True
42+
return False
43+
3544
@property
3645
def name(self) -> str:
3746
return self.info().get(DATASET_NAME_KEY, "")

0 commit comments

Comments
 (0)