Skip to content

Commit d446c33

Browse files
waffles-cfRavi Shankar
andauthored
Add video and activity CRUD (#43)
Co-authored-by: Ravi Shankar <wafflespeanut@gmail.com>
1 parent 506cb20 commit d446c33

14 files changed

Lines changed: 821 additions & 24 deletions

hasty/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from hasty.activity import Activity, ActivityType
12
from hasty.attribute import Attribute
23
from hasty.client import Client
34
from hasty.dataset import Dataset
@@ -9,6 +10,7 @@
910
from hasty.project import Project
1011
from hasty.tag import Tag
1112
from hasty.tag_class import TagClass
13+
from hasty.video import Video
1214
import hasty.label_utils as label_utils
1315

1416

@@ -23,6 +25,8 @@ def int_or_str(value):
2325
VERSION = tuple(map(int_or_str, __version__.split('.')))
2426

2527
__all__ = [
28+
'Activity',
29+
'ActivityType'
2630
'Attribute',
2731
'Attributer',
2832
'Client',
@@ -37,5 +41,6 @@ def int_or_str(value):
3741
'SemanticSegmentor',
3842
'Tag',
3943
'TagClass',
40-
'label_utils'
44+
'Video',
45+
'label_utils',
4146
]

hasty/activity.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
from collections import OrderedDict
2+
3+
from .exception import ValidationException
4+
from .hasty_object import HastyObject
5+
6+
7+
class ActivityType(HastyObject):
8+
endpoint = '/v1/projects/{project_id}/activity_types'
9+
endpoint_class = '/v1/projects/{project_id}/activity_types/{activity_type_id}'
10+
11+
def __repr__(self):
12+
return self.get__repr__(OrderedDict({"id": self._id, "name": self._name, "color": self._color}))
13+
14+
@property
15+
def id(self):
16+
"""
17+
:type: string
18+
"""
19+
return self._id
20+
21+
@property
22+
def name(self):
23+
"""
24+
:type: string
25+
"""
26+
return self._name
27+
28+
@property
29+
def project_id(self):
30+
"""
31+
:type: string
32+
"""
33+
return self._project_id
34+
35+
@property
36+
def color(self):
37+
"""
38+
:type: string
39+
"""
40+
return self._color
41+
42+
def _init_properties(self):
43+
self._id = None
44+
self._name = None
45+
self._project_id = None
46+
self._color = None
47+
48+
def _set_prop_values(self, data):
49+
if "id" in data:
50+
self._id = data["id"]
51+
if "name" in data:
52+
self._name = data["name"]
53+
if "project_id" in data:
54+
self._project_id = data["project_id"]
55+
if "color" in data:
56+
self._color = data["color"]
57+
58+
@classmethod
59+
def _create(cls, requester, project_id, name, color=None):
60+
res = requester.post(cls.endpoint.format(project_id=project_id),
61+
json_data={"name": name,
62+
"color": color})
63+
return cls(requester, res, {"project_id": project_id})
64+
65+
def edit(self, name, color=None):
66+
"""
67+
Edit activity type properties
68+
69+
Arguments:
70+
name (str): Label class name
71+
color (str, optional): Color in HEX format #0f0f0faa
72+
"""
73+
self._requester.put(self.endpoint_class.format(project_id=self._project_id, activity_type_id=self._id),
74+
json_data={"name": name, "color": color})
75+
self._name = name
76+
self._color = color
77+
78+
def delete(self):
79+
"""
80+
Delete activity type
81+
"""
82+
self._requester.delete(self.endpoint_class.format(project_id=self._project_id, activity_type_id=self._id))
83+
84+
85+
class Activity(HastyObject):
86+
endpoint = '/v1/projects/{project_id}/videos/{video_id}/segments'
87+
endpoint_class = '/v1/projects/{project_id}/segments/{segment_id}'
88+
89+
def __repr__(self):
90+
return self.get__repr__(OrderedDict({"id": self._id, "activities": self._activities,
91+
"start": self._start_time_ms, "end": self._end_time_ms}))
92+
93+
@property
94+
def id(self):
95+
"""
96+
:type: string
97+
"""
98+
return self._id
99+
100+
@property
101+
def video_id(self):
102+
"""
103+
:type: string
104+
"""
105+
return self._video_id
106+
107+
@property
108+
def activities(self):
109+
"""
110+
:type: list
111+
"""
112+
return self._activities
113+
114+
@property
115+
def start_time_ms(self):
116+
"""
117+
:type: int
118+
"""
119+
return self._start_time_ms
120+
121+
@property
122+
def end_time_ms(self):
123+
"""
124+
:type: int
125+
"""
126+
return self._end_time_ms
127+
128+
def _init_properties(self):
129+
self._id = None
130+
self._video_id = None
131+
self._activities = None
132+
self._start_time_ms = None
133+
self._end_time_ms = None
134+
self._project_id = None
135+
136+
def _set_prop_values(self, data):
137+
if "id" in data:
138+
self._id = data["id"]
139+
if "video_id" in data:
140+
self._video_id = data["video_id"]
141+
if "activities" in data:
142+
self._activities = data["activities"]
143+
if "start_time_ms" in data:
144+
self._start_time_ms = data["start_time_ms"]
145+
if "end_time_ms" in data:
146+
self._end_time_ms = data["end_time_ms"]
147+
if "project_id" in data:
148+
self._project_id = data["project_id"]
149+
150+
@classmethod
151+
def _create(cls, requester, project_id, video_id, activities,
152+
start_time_ms, end_time_ms, replace_overlap=False):
153+
if len(activities) == 0 or not isinstance(activities, list):
154+
raise ValidationException.invalid_activities()
155+
type_ids = []
156+
for a in activities:
157+
if isinstance(a, ActivityType):
158+
type_ids.append(a.id)
159+
elif isinstance(a, str):
160+
type_ids.append(a)
161+
else:
162+
raise ValidationException.invalid_activities()
163+
query_params = None
164+
if replace_overlap:
165+
query_params = {"replace_overlap": replace_overlap}
166+
res = requester.post(cls.endpoint.format(project_id=project_id, video_id=video_id),
167+
json_data={"activities": type_ids,
168+
"start_time_ms": start_time_ms,
169+
"end_time_ms": end_time_ms},
170+
query_params=query_params)
171+
return cls(requester, res, {"project_id": project_id})
172+
173+
def delete(self):
174+
"""
175+
Delete activity
176+
"""
177+
self._requester.delete(self.endpoint_class.format(project_id=self._project_id, segment_id=self._id))
178+
179+
def edit(self, start_time_ms, end_time_ms, activities):
180+
"""
181+
Edit activity properties
182+
183+
Arguments:
184+
start_time_ms (int): Start time in milliseconds
185+
end_time_ms (int): End time in milliseconds
186+
activities (list): List of `~hasty.ActivityType` or `str` (IDs)
187+
"""
188+
if len(activities) == 0 or not isinstance(activities, list):
189+
raise ValidationException.invalid_activities()
190+
type_ids = []
191+
for a in activities:
192+
if isinstance(a, ActivityType):
193+
type_ids.append(a.id)
194+
elif isinstance(a, str):
195+
type_ids.append(a)
196+
else:
197+
raise ValidationException.invalid_activities()
198+
res = self._requester.put(self.endpoint_class.format(project_id=self._project_id, segment_id=self._id),
199+
json_data={"activities": type_ids,
200+
"start_time_ms": start_time_ms,
201+
"end_time_ms": end_time_ms})
202+
self._set_prop_values(res)
203+
return Activity(self._requester, res, {"project_id": self._project_id})

hasty/client.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from typing import Union
22
import uuid
33

4+
from .constants import ProjectType
45
from .helper import PaginatedList
5-
from .project import Project
6+
from .project import Project, VideoProject
67
from .workspace import Workspace
78
from .requester import Requester
89

@@ -49,16 +50,18 @@ def get_project(self, project_id):
4950
res = self._requester.get(Project.endpoint_project.format(project_id=project_id))
5051
return Project(self._requester, res)
5152

52-
def create_project(self, workspace: Union[str, Workspace], name: str, description: str = None) -> Project:
53+
def create_project(self, workspace: Union[str, Workspace], name: str,
54+
description: str = None, content_type: str = ProjectType.Image) -> Union[Project, VideoProject]:
5355
"""
54-
Creates new project :py:class:`~hasty.Project`
56+
Creates new project :py:class:`~hasty.Project` or :py:class:`~hasty.VideoProject`
5557
5658
Arguments:
5759
workspace (:py:class:`~hasty.Workspace`, str): Workspace object or id which the project should belongs to
5860
name (str): Name of the project
5961
description (str, optional): Project description
62+
content_type (str, optional): Type of the project. Default is image
6063
"""
6164
workspace_id = workspace
6265
if isinstance(workspace, Workspace):
6366
workspace_id = workspace.id
64-
return Project.create(self._requester, workspace_id, name, description)
67+
return Project.create(self._requester, workspace_id, name, description, content_type)

hasty/constants.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
class ProjectType:
2+
Image = "IMAGES"
3+
Video = "VIDEOS"
4+
5+
16
class ImageStatus:
27
New = "NEW"
38
Done = "DONE"
@@ -8,6 +13,15 @@ class ImageStatus:
813
AutoLabelled = "AUTO-LABELLED"
914

1015

16+
class VideoStatus:
17+
New = ImageStatus.New
18+
InProgress = ImageStatus.InProgress
19+
ToReview = ImageStatus.ToReview
20+
Done = ImageStatus.Done
21+
Skipped = ImageStatus.Skipped
22+
Completed = ImageStatus.Completed
23+
24+
1125
class ExportFormat:
1226
JSON_v11 = "json_v1.1"
1327
SEMANTIC_PNG = "semantic_png"
@@ -31,6 +45,8 @@ class SemanticOrder:
3145

3246
VALID_STATUSES = [ImageStatus.New, ImageStatus.Done, ImageStatus.Skipped, ImageStatus.InProgress, ImageStatus.ToReview,
3347
ImageStatus.AutoLabelled, ImageStatus.Completed]
48+
VALID_VIDEO_STATUSES = [VideoStatus.New, VideoStatus.Done, VideoStatus.Skipped, VideoStatus.InProgress, VideoStatus.ToReview,
49+
VideoStatus.Completed]
3450
VALID_EXPORT_FORMATS = [ExportFormat.JSON_v11, ExportFormat.SEMANTIC_PNG, ExportFormat.JSON_COCO, ExportFormat.IMAGES]
3551
VALID_SEMANTIC_FORMATS = [SemanticFormat.GS_DESC, SemanticFormat.GS_ASC, SemanticFormat.CLASS_COLOR]
3652
VALID_SEMANTIC_ORDER = [SemanticOrder.Z_INDEX, SemanticOrder.CLASS_TYPE, SemanticOrder.CLASS_ORDER]

hasty/exception.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@ def __init__(self, message):
44

55
@classmethod
66
def export_in_progress(cls):
7-
raise ValidationException("Export is still running")
7+
return ValidationException("Export is still running")
88

9+
@classmethod
10+
def video_not_ready(cls):
11+
return ValidationException("Video is not ready")
12+
13+
@classmethod
14+
def invalid_activities(cls):
15+
return ValidationException("activities must be a non-empty list of ActitivityType objects or IDs")
916

1017
class AuthenticationException(Exception):
1118
def __init__(self, message):
1219
self.message = message
1320

1421
@classmethod
1522
def failed_authentication(cls):
16-
raise AuthenticationException("Authentication failed, check your API key")
23+
return AuthenticationException("Authentication failed, check your API key")
1724

1825

1926
class AuthorisationException(Exception):
@@ -22,7 +29,7 @@ def __init__(self, message):
2229

2330
@classmethod
2431
def permission_denied(cls):
25-
raise AuthorisationException("Looks like service account doesn't have a permission to perform this operation")
32+
return AuthorisationException("Looks like service account doesn't have a permission to perform this operation")
2633

2734

2835
class InsufficientCredits(Exception):
@@ -31,7 +38,7 @@ def __init__(self, message):
3138

3239
@classmethod
3340
def insufficient_credits(cls):
34-
raise InsufficientCredits("Looks like you out of credits, please top up your account or contact Hasty")
41+
return InsufficientCredits("Looks like you out of credits, please top up your account or contact Hasty")
3542

3643

3744
class NotFound(Exception):
@@ -40,7 +47,7 @@ def __init__(self, message):
4047

4148
@classmethod
4249
def object_not_found(cls):
43-
raise NotFound("Referred object not found, please check your script")
50+
return NotFound("Referred object not found, please check your script")
4451

4552

4653
class LimitExceededException(Exception):

hasty/hasty_object.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44

55
class HastyObject:
6+
endpoint_uploads = '/v1/projects/{project_id}/uploads'
67

78
def __init__(self, requester, data, obj_params=None):
89
self._requester = requester
@@ -37,3 +38,8 @@ def _init_properties(self):
3738

3839
def _set_prop_values(self, data):
3940
raise NotImplementedError()
41+
42+
@classmethod
43+
def _generate_sign_url(cls, requester, project_id):
44+
data = requester.get(cls.endpoint_uploads.format(project_id=project_id), query_params={"count": 1})
45+
return data["items"][0]

hasty/image.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
class Image(HastyObject):
1818
endpoint = '/v1/projects/{project_id}/images'
19-
endpoint_uploads = '/v1/projects/{project_id}/uploads'
2019
endpoint_image = '/v1/projects/{project_id}/images/{image_id}'
2120

2221
def __repr__(self):
@@ -131,15 +130,10 @@ def _get_by_id(requester, project_id, image_id):
131130
data = requester.get(Image.endpoint_image.format(project_id=project_id, image_id=image_id))
132131
return Image(requester, data, {"project_id": project_id})
133132

134-
@staticmethod
135-
def _generate_sign_url(requester, project_id):
136-
data = requester.get(Image.endpoint_uploads.format(project_id=project_id), query_params={"count": 1})
137-
return data["items"][0]
138-
139133
@staticmethod
140134
def _upload_from_file(requester, project_id, dataset_id, filepath, external_id: Optional[str] = None):
141135
filename = os.path.basename(filepath)
142-
url_data = Image._generate_sign_url(requester, project_id)
136+
url_data = HastyObject._generate_sign_url(requester, project_id)
143137
with open(filepath, 'rb') as f:
144138
requester.put(url_data['url'], data=f.read(), content_type="")
145139
res = requester.post(Image.endpoint.format(project_id=project_id),

0 commit comments

Comments
 (0)