Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion hasty/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from hasty.activity import Activity, ActivityType
from hasty.attribute import Attribute
from hasty.client import Client
from hasty.dataset import Dataset
Expand All @@ -9,6 +10,7 @@
from hasty.project import Project
from hasty.tag import Tag
from hasty.tag_class import TagClass
from hasty.video import Video
import hasty.label_utils as label_utils


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

__all__ = [
'Activity',
'ActivityType'
'Attribute',
'Attributer',
'Client',
Expand All @@ -37,5 +41,6 @@ def int_or_str(value):
'SemanticSegmentor',
'Tag',
'TagClass',
'label_utils'
'Video',
'label_utils',
]
203 changes: 203 additions & 0 deletions hasty/activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
from collections import OrderedDict

from .exception import ValidationException
from .hasty_object import HastyObject


class ActivityType(HastyObject):
endpoint = '/v1/projects/{project_id}/activity_types'
endpoint_class = '/v1/projects/{project_id}/activity_types/{activity_type_id}'

def __repr__(self):
return self.get__repr__(OrderedDict({"id": self._id, "name": self._name, "color": self._color}))

@property
def id(self):
"""
:type: string
"""
return self._id

@property
def name(self):
"""
:type: string
"""
return self._name

@property
def project_id(self):
"""
:type: string
"""
return self._project_id

@property
def color(self):
"""
:type: string
"""
return self._color

def _init_properties(self):
self._id = None
self._name = None
self._project_id = None
self._color = None

def _set_prop_values(self, data):
if "id" in data:
self._id = data["id"]
if "name" in data:
self._name = data["name"]
if "project_id" in data:
self._project_id = data["project_id"]
if "color" in data:
self._color = data["color"]

@classmethod
def _create(cls, requester, project_id, name, color=None):
res = requester.post(cls.endpoint.format(project_id=project_id),
json_data={"name": name,
"color": color})
return cls(requester, res, {"project_id": project_id})

def edit(self, name, color=None):
"""
Edit activity type properties

Arguments:
name (str): Label class name
color (str, optional): Color in HEX format #0f0f0faa
"""
self._requester.put(self.endpoint_class.format(project_id=self._project_id, activity_type_id=self._id),
json_data={"name": name, "color": color})
self._name = name
self._color = color

def delete(self):
"""
Delete activity type
"""
self._requester.delete(self.endpoint_class.format(project_id=self._project_id, activity_type_id=self._id))


class Activity(HastyObject):
endpoint = '/v1/projects/{project_id}/videos/{video_id}/segments'
endpoint_class = '/v1/projects/{project_id}/segments/{segment_id}'

def __repr__(self):
return self.get__repr__(OrderedDict({"id": self._id, "activities": self._activities,
"start": self._start_time_ms, "end": self._end_time_ms}))

@property
def id(self):
"""
:type: string
"""
return self._id

@property
def video_id(self):
"""
:type: string
"""
return self._video_id

@property
def activities(self):
"""
:type: list
"""
return self._activities

@property
def start_time_ms(self):
"""
:type: int
"""
return self._start_time_ms

@property
def end_time_ms(self):
"""
:type: int
"""
return self._end_time_ms

def _init_properties(self):
self._id = None
self._video_id = None
self._activities = None
self._start_time_ms = None
self._end_time_ms = None
self._project_id = None

def _set_prop_values(self, data):
if "id" in data:
self._id = data["id"]
if "video_id" in data:
self._video_id = data["video_id"]
if "activities" in data:
self._activities = data["activities"]
if "start_time_ms" in data:
self._start_time_ms = data["start_time_ms"]
if "end_time_ms" in data:
self._end_time_ms = data["end_time_ms"]
if "project_id" in data:
self._project_id = data["project_id"]

@classmethod
def _create(cls, requester, project_id, video_id, activities,
start_time_ms, end_time_ms, replace_overlap=False):
if len(activities) == 0 or not isinstance(activities, list):
raise ValidationException.invalid_activities()
type_ids = []
for a in activities:
if isinstance(a, ActivityType):
type_ids.append(a.id)
elif isinstance(a, str):
type_ids.append(a)
else:
raise ValidationException.invalid_activities()
query_params = None
if replace_overlap:
query_params = {"replace_overlap": replace_overlap}
res = requester.post(cls.endpoint.format(project_id=project_id, video_id=video_id),
json_data={"activities": type_ids,
"start_time_ms": start_time_ms,
"end_time_ms": end_time_ms},
query_params=query_params)
return cls(requester, res, {"project_id": project_id})

def delete(self):
"""
Delete activity
"""
self._requester.delete(self.endpoint_class.format(project_id=self._project_id, segment_id=self._id))

def edit(self, start_time_ms, end_time_ms, activities):
"""
Edit activity properties

Arguments:
start_time_ms (int): Start time in milliseconds
end_time_ms (int): End time in milliseconds
activities (list): List of `~hasty.ActivityType` or `str` (IDs)
"""
if len(activities) == 0 or not isinstance(activities, list):
raise ValidationException.invalid_activities()
type_ids = []
for a in activities:
if isinstance(a, ActivityType):
type_ids.append(a.id)
elif isinstance(a, str):
type_ids.append(a)
else:
raise ValidationException.invalid_activities()
res = self._requester.put(self.endpoint_class.format(project_id=self._project_id, segment_id=self._id),
json_data={"activities": type_ids,
"start_time_ms": start_time_ms,
"end_time_ms": end_time_ms})
self._set_prop_values(res)
return Activity(self._requester, res, {"project_id": self._project_id})
11 changes: 7 additions & 4 deletions hasty/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Union
import uuid

from .constants import ProjectType
from .helper import PaginatedList
from .project import Project
from .project import Project, VideoProject
from .workspace import Workspace
from .requester import Requester

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

def create_project(self, workspace: Union[str, Workspace], name: str, description: str = None) -> Project:
def create_project(self, workspace: Union[str, Workspace], name: str,
description: str = None, content_type: str = ProjectType.Image) -> Union[Project, VideoProject]:
"""
Creates new project :py:class:`~hasty.Project`
Creates new project :py:class:`~hasty.Project` or :py:class:`~hasty.VideoProject`

Arguments:
workspace (:py:class:`~hasty.Workspace`, str): Workspace object or id which the project should belongs to
name (str): Name of the project
description (str, optional): Project description
content_type (str, optional): Type of the project. Default is image
"""
workspace_id = workspace
if isinstance(workspace, Workspace):
workspace_id = workspace.id
return Project.create(self._requester, workspace_id, name, description)
return Project.create(self._requester, workspace_id, name, description, content_type)
16 changes: 16 additions & 0 deletions hasty/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
class ProjectType:
Image = "IMAGES"
Video = "VIDEOS"


class ImageStatus:
New = "NEW"
Done = "DONE"
Expand All @@ -8,6 +13,15 @@ class ImageStatus:
AutoLabelled = "AUTO-LABELLED"


class VideoStatus:
New = ImageStatus.New
InProgress = ImageStatus.InProgress
ToReview = ImageStatus.ToReview
Done = ImageStatus.Done
Skipped = ImageStatus.Skipped
Completed = ImageStatus.Completed


class ExportFormat:
JSON_v11 = "json_v1.1"
SEMANTIC_PNG = "semantic_png"
Expand All @@ -31,6 +45,8 @@ class SemanticOrder:

VALID_STATUSES = [ImageStatus.New, ImageStatus.Done, ImageStatus.Skipped, ImageStatus.InProgress, ImageStatus.ToReview,
ImageStatus.AutoLabelled, ImageStatus.Completed]
VALID_VIDEO_STATUSES = [VideoStatus.New, VideoStatus.Done, VideoStatus.Skipped, VideoStatus.InProgress, VideoStatus.ToReview,
VideoStatus.Completed]
VALID_EXPORT_FORMATS = [ExportFormat.JSON_v11, ExportFormat.SEMANTIC_PNG, ExportFormat.JSON_COCO, ExportFormat.IMAGES]
VALID_SEMANTIC_FORMATS = [SemanticFormat.GS_DESC, SemanticFormat.GS_ASC, SemanticFormat.CLASS_COLOR]
VALID_SEMANTIC_ORDER = [SemanticOrder.Z_INDEX, SemanticOrder.CLASS_TYPE, SemanticOrder.CLASS_ORDER]
17 changes: 12 additions & 5 deletions hasty/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@ def __init__(self, message):

@classmethod
def export_in_progress(cls):
raise ValidationException("Export is still running")
return ValidationException("Export is still running")

@classmethod
def video_not_ready(cls):
return ValidationException("Video is not ready")

@classmethod
def invalid_activities(cls):
return ValidationException("activities must be a non-empty list of ActitivityType objects or IDs")

class AuthenticationException(Exception):
def __init__(self, message):
self.message = message

@classmethod
def failed_authentication(cls):
raise AuthenticationException("Authentication failed, check your API key")
return AuthenticationException("Authentication failed, check your API key")


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

@classmethod
def permission_denied(cls):
raise AuthorisationException("Looks like service account doesn't have a permission to perform this operation")
return AuthorisationException("Looks like service account doesn't have a permission to perform this operation")


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

@classmethod
def insufficient_credits(cls):
raise InsufficientCredits("Looks like you out of credits, please top up your account or contact Hasty")
return InsufficientCredits("Looks like you out of credits, please top up your account or contact Hasty")


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

@classmethod
def object_not_found(cls):
raise NotFound("Referred object not found, please check your script")
return NotFound("Referred object not found, please check your script")


class LimitExceededException(Exception):
Expand Down
6 changes: 6 additions & 0 deletions hasty/hasty_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


class HastyObject:
endpoint_uploads = '/v1/projects/{project_id}/uploads'

def __init__(self, requester, data, obj_params=None):
self._requester = requester
Expand Down Expand Up @@ -37,3 +38,8 @@ def _init_properties(self):

def _set_prop_values(self, data):
raise NotImplementedError()

@classmethod
def _generate_sign_url(cls, requester, project_id):
data = requester.get(cls.endpoint_uploads.format(project_id=project_id), query_params={"count": 1})
return data["items"][0]
8 changes: 1 addition & 7 deletions hasty/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

class Image(HastyObject):
endpoint = '/v1/projects/{project_id}/images'
endpoint_uploads = '/v1/projects/{project_id}/uploads'
endpoint_image = '/v1/projects/{project_id}/images/{image_id}'

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

@staticmethod
def _generate_sign_url(requester, project_id):
data = requester.get(Image.endpoint_uploads.format(project_id=project_id), query_params={"count": 1})
return data["items"][0]

@staticmethod
def _upload_from_file(requester, project_id, dataset_id, filepath, external_id: Optional[str] = None):
filename = os.path.basename(filepath)
url_data = Image._generate_sign_url(requester, project_id)
url_data = HastyObject._generate_sign_url(requester, project_id)
with open(filepath, 'rb') as f:
requester.put(url_data['url'], data=f.read(), content_type="")
res = requester.post(Image.endpoint.format(project_id=project_id),
Expand Down
Loading