diff --git a/README.rst b/README.rst index 2b7314a..09af378 100644 --- a/README.rst +++ b/README.rst @@ -1,112 +1,84 @@ -=================== +===================== Scale AI | Python SDK -=================== - +===================== Installation -============ +____________ + .. code-block:: bash $ pip install --upgrade scaleapi - + Note: We strongly suggest using `scaleapi` with Python version 2.7.9 or greater due to SSL issues with prior versions. Usage -===== +_____ + .. code-block:: python import scaleapi client = scaleapi.ScaleClient('YOUR_API_KEY_HERE') Tasks -===== +_____ -Most of these methods will return a ``scaleapi.Task`` object, which will contain information +Most of these methods will return a `scaleapi.Task` object, which will contain information about the json response (task_id, status...). -Any parameter available in the documentation_ can be passed as an argument option with the corresponding type. +Any parameter available in the documentation\_ can be passed as an argument option with the corresponding type. -.. _documentation: https://scale.com/docs +.. \_documentation: https://docs.scale.com/reference#task-object The following endpoints for tasks are available: -Create Categorization Task -========================== +Create Task +^^^^^^^^^^^ -Check `this`__ for further information. +This method can be used for any Scale supported task type using the following format: +`client.create_{{Task Type}}_task(...)` and passing the applicable values into the function definition. The applicable fields and further information for each task type can be found in scales API docs `here`\_\_ for further information. -__ https://scale.com/docs/#create-categorization-task - -.. code-block:: python - - task = client.create_categorization_task( - callback_url = 'http://www.example.com/callback', - instruction = 'Is this company public or private?', - attachment_type = 'website', - attachment = 'http://www.google.com/', - categories = ['public', 'private'] - ) - -Create Image Annotation Task -====================== - -Check `this`__ for further information. - -__ https://docs.scale.com/reference#general-image-annotation +\_\_ hhttps://docs.scale.com/reference#general-image-annotation .. code-block:: python client.create_imageannotation_task( - callback_url = 'http://www.example.com/callback', - instruction= 'Draw a box around each baby cow and big cow.', - attachment_type = "image", - attachment = "http://i.imgur.com/v4cBreD.jpg", - geometries = { - "box": { - "objects_to_annotate: ["Baby Cow", "Big Cow"], - "min_height": 10, - "min_width": 10 + project = 'test_project', + callback_url = "http://www.example.com/callback", + instruction= "Draw a box around each baby cow and big cow.", + attachment_type = "image", + attachment = "http://i.imgur.com/v4cBreD.jpg", + geometries = { + "box": { + "objects_to_annotate": ["Baby Cow", "Big Cow"], + "min_height": 10, + "min_width": 10 + } } - } ) Retrieve task -============= +^^^^^^^^^^^^^ -Check `this`__ for further information. +Check `this`\_\_ for further information. -__ https://docs.scale.com/reference#retrieve-tasks +\_\_ https://docs.scale.com/reference#retrieve-tasks Retrieve a task given its id. .. code-block :: python task = client.fetch_task('asdfasdfasdfasdfasdfasdf') - task.id == 'asdfasdfasdfasdfasdfasdf' # true + print(task.status) // Task status ('pending', 'completed', 'error', 'canceled') + print(task.response) // If task is complete -Cancel task -=========== +List Tasks +^^^^^^^^^^ -Check `this`__ for further information. +Check `this`\_\_ for further information. -__ https://docs.scale.com/reference#cancel-task +\_\_ https://docs.scale.com/reference#list-multiple-tasks -Cancel a task given its id, only if it's not completed. - -.. code-block :: python - - task = client.cancel_task('asdfasdfasdfasdfasdfasdf') - -List tasks -========== - -Check `this`__ for further information. - -__ https://docs.scale.com/reference#list-multiple-tasks - -Retrieve a list of tasks, with optional filter by date/type. Paginated with limit/offset. -The return value is a ``scaleapi.Tasklist``, which acts as a list, but also has fields -for the total number of tasks, the limit and offset, and whether or not there's more. +Retrieve a list of tasks, with optional filter by stand and end date/type. Paginated with `next_token`. The return value is a `scaleapi.Tasklist`, which acts as a list, but also has fields for the total number of tasks, the limit and offset, and whether or not there's more. .. code-block :: python @@ -122,18 +94,174 @@ for the total number of tasks, the limit and offset, and whether or not there's ) for task in tasks: counter += 1 - print(f'Downloading Task {counter} | {task.task_id}') + print('Downloading Task %s | %s' % (counter, task.task_id)) all_tasks.append(task.__dict__['param_dict']) next_token = tasks.next_token if next_token is None: break print(all_tasks) +Cancel Task +^^^^^^^^^^^ + +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#cancel-task + +Cancel a task given its id if work has not stared on the task (task status is "que). + +.. code-block :: python + + task = client.cancel_task('asdfasdfasdfasdfasdfasdf') + +Batches +_______ + +Create Batch +^^^^^^^^^^^^ + +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#batch-creation + +.. code-block:: python + + client.create_batch( + project = 'test_project', + callback = "http://www.example.com/callback", + name = 'batch_name_01_07_2021' + ) + +Finalize Batceh +^^^^^^^^^^^^^^^ + +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#batch-finalization + +.. code-block:: python + + client.create_batch(batch_name = 'batch_name_01_07_2021') + +Check Batch Status +^^^^^^^^^^^^^^^^^^ + +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#batch-status + +.. code-block:: python + + client.batch_status(batch_name = 'batch_name_01_07_2021') + +Retrieve Batch +^^^^^^^^^^^^^^ + +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#batch-retrieval + +.. code-block:: python + + client.get_batch( batch_name = "batch_name_01_07_2021" ) + +List Batches +^^^^^^^^^^^^ + +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#batch-list + +Retrieve a list of batches + +.. code-block :: python + + next_token = None; + counter = 0 + all_batchs =[] + while True: + batches = client.list_batches( + status = "completed" + ) + for batch in batches: + counter += 1 + print('Downloading Batch %s | %s | %s' % (counter, batch.name, batch.param_dict['status'])) + all_batchs.append(batch.__dict__['param_dict']) + next_token = batches.next_token + if next_token is None: + break + print(all_batchs) + +Projects +________ + +Create Project +^^^^^^^^^^^^^^ + +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#project-creation + +.. code-block:: python + + client.create_project( + project_name = 'test_project', + type = 'imageannotation, + params = {'instruction':'Please label the kittens'} + ) + +Retrieve Project +^^^^^^^^^^^^^^^^ + +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#project-retrieval + +.. code-block:: python + + client.get_projet(project_name = 'test_project') + +List Projects +^^^^^^^^^^^^^ + +This function does not take any arguments. It will return information for every project. +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#batch-list + +Retrieve a list of batches + +.. code-block :: python + + counter = 0 + projects = client.projects() + for project in projects: + counter += 1 + print('Downloading project %s | %s | %s' % (counter, project['name'], project['type'])) + +Update Project +^^^^^^^^^^^^^^ + +Check `this`\_\_ for further information. + +\_\_ https://docs.scale.com/reference#project-update-parameters + +Retrieve a list of batches + +.. code-block :: python + + data = client.update_project( + project_name='test_project', + pathc = false, + instruction='update: Please label all the stuff', + +) + Error handling -============== +______________ If something went wrong while making API calls, then exceptions will be raised automatically -as a ``scaleapi.ScaleException`` or ``scaleapi.ScaleInvalidRequest`` runtime error. For example: +as a `scaleapi.ScaleException` or `scaleapi.ScaleInvalidRequest` runtime error. For example: .. code-block:: python @@ -144,6 +272,6 @@ as a ``scaleapi.ScaleException`` or ``scaleapi.ScaleInvalidRequest`` runtime er print(e.message) # missing param X Troubleshooting -=============== +_______________ If you notice any problems, please email us at support@scale.com. diff --git a/scaleapi/__init__.py b/scaleapi/__init__.py index ba18f02..c315518 100644 --- a/scaleapi/__init__.py +++ b/scaleapi/__init__.py @@ -2,9 +2,10 @@ from .tasks import Task from .batches import Batch +from .projects import Project TASK_TYPES = [ - 'annotation', + 'annotation', 'audiotranscription', 'categorization', 'comparison', @@ -17,8 +18,10 @@ 'polygonannotation', 'segmentannotation', 'transcription', + 'documenttranscription', 'videoannotation', 'videoboxannotation', + 'videoplaybackannotation', 'videocuboidannotation' ] SCALE_ENDPOINT = 'https://api.scale.com/v1/' @@ -158,25 +161,57 @@ def create_batch(self, project, batch_name, callback): payload = dict(project=project, name=batch_name, callback=callback) batchdata = self._postrequest('batches', payload) return Batch(batchdata, self) + + def finalize_batch(self, batch_name): + batchdata = self._postrequest('batches/%s/finalize' % batch_name) + return Batch(batchdata, self) + + def batch_status(self, batch_name): + batchdata = self._getrequest('batches/%s/status' % batch_name) + return Batch.get_status(self) - def get_batch(self, batch_name: str): + def get_batch(self, batch_name): batchdata = self._getrequest('batches/%s' % batch_name) return Batch(batchdata, self) def list_batches(self, **kwargs): allowed_kwargs = {'start_time', 'end_time', 'status', 'project', - 'batch', 'limit', 'offset', } + 'limit', 'offset', } for key in kwargs: if key not in allowed_kwargs: - raise ScaleInvalidRequest('Illegal parameter %s for ScaleClient.tasks()' + raise ScaleInvalidRequest('Illegal parameter %s for ScaleClient.list_batches()' % key, None) - response = self._getrequest('tasks', params=kwargs) + response = self._getrequest('batches', params=kwargs) docs = [Batch(doc, self) for doc in response['docs']] return Batchlist( - docs, response['total'], response['limit'], response['offset'], - response['has_more'], response.get('next_token'), + docs, response['totalDocs'], response['limit'],response['has_more'], response.get('next_token'), ) + def create_project(self, project_name, type, params): + payload = dict(type=type, name=project_name, params=params) + projectdata = self._postrequest('projects', payload) + return Project(projectdata, self) + + def get_projet(self, project_name): + projectdata = self._getrequest('projects/%s' % project_name) + return Project(projectdata, self) + + def projects(self): + response = self._getrequest('projects') + return response + + def update_project(self,project_name,**kwargs): + allowed_kwargs = {'patch', 'instruction'} + for key in kwargs: + if key not in allowed_kwargs: + raise ScaleInvalidRequest('Illegal parameter %s for ScaleClient.update_project()' + % key, None) + projectdata = self._postrequest('projects/%s/setParams' % project_name) + return projectdata + + + + def _AddTaskTypeCreator(task_type): def create_task_wrapper(self, **kwargs): diff --git a/scaleapi/projects.py b/scaleapi/projects.py new file mode 100644 index 0000000..e619a07 --- /dev/null +++ b/scaleapi/projects.py @@ -0,0 +1,14 @@ +class Project(object): + def __init__(self, param_dict, client): + self.param_dict = param_dict + self.name = param_dict['name'] + self.client = client + + def __hash__(self): + return hash(self.name) + + def __str__(self): + return 'Project(name=%s)' % self.name + + def __repr__(self): + return 'Project(%s)' % self.param_dict diff --git a/tests/test_client.py b/tests/test_client.py index 4d94dce..7891ec5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -12,18 +12,20 @@ except KeyError: raise Exception("Please set the environment variable SCALE_TEST_API_KEY to run tests.") - def make_a_task(): - return client.create_comparison_task( - callback_url='http://www.example.com/callback', - instruction='Do the objects in these images have the same pattern?', - attachment_type='image', - attachments=[ - 'http://i.ebayimg.com/00/$T2eC16dHJGwFFZKjy5ZjBRfNyMC4Ig~~_32.JPG', - 'http://images.wisegeek.com/checkered-tablecloth.jpg' - ], - choices=['yes', 'no']) - + return client.create_imageannotation_task( + callback_url = "http://www.example.com/callback", + instruction = "Draw a box around each baby cow and big cow.", + attachment_type = "image", + attachment = "http://i.imgur.com/v4cBreD.jpg", + geometries = { + "box": { + "objects_to_annotate": ["Baby Cow", "Big Cow"], + "min_height": 10, + "min_width": 10 + } + } + ) def test_categorize_ok(): task = client.create_categorization_task( @@ -33,14 +35,12 @@ def test_categorize_ok(): attachment='http://www.google.com/', categories=['public', 'private']) - def test_categorize_fail(): with pytest.raises(scaleapi.ScaleInvalidRequest): client.create_categorization_task( callback_url='http://www.example.com/callback', categories=['public', 'private']) - def test_transcription_ok(): task = client.create_transcription_task( callback_url='http://www.example.com/callback', @@ -56,53 +56,53 @@ def test_transcription_ok(): 'comment_count': 'Number of comments' }) - def test_transcription_fail(): with pytest.raises(scaleapi.ScaleInvalidRequest): client.create_transcription_task( callback_url='http://www.example.com/callback', attachment_type='website') +def test_imageannotation_ok(): + client.create_imageannotation_task( + callback_url = "http://www.example.com/callback", + instruction = "Draw a box around each baby cow and big cow.", + attachment_type = "image", + attachment = "http://i.imgur.com/v4cBreD.jpg", + geometries = { + "box": { + "objects_to_annotate": ["Baby Cow", "Big Cow"], + "min_height": 10, + "min_width": 10 + } + } + ) -@pytest.mark.skip(reason="Deprecated at the moment") -def test_phonecall_ok(): - task = client.create_phonecall_task( - callback_url='http://www.example.com/callback', - instruction='Call this person and follow the script provided, recording responses', - phone_number='5055006865', - entity_name='Alexandr Wang', - script='Hello ! Are you happy today? (pause) One more thing - what is your email address?', - fields={'email': 'Email Address'}, - choices=['He is happy', 'He is not happy']) - - -@pytest.mark.skip(reason="Deprecated at the moment") -def test_phonecall_fail(): +def test_imageannotation_fail(): with pytest.raises(scaleapi.ScaleInvalidRequest): - client.create_phonecall_task( + client.create_imageannotation_task( callback_url='http://www.example.com/callback', - instruction='Call this person and follow the script provided, recording responses') - - -def test_comparison_ok(): - task = client.create_comparison_task( - callback_url='http://www.example.com/callback', - instruction='Do the objects in these images have the same pattern?', - attachment_type='image', - attachments=[ - 'http://i.ebayimg.com/00/$T2eC16dHJGwFFZKjy5ZjBRfNyMC4Ig~~_32.JPG', - 'http://images.wisegeek.com/checkered-tablecloth.jpg' - ], - choices=['yes', 'no']) + instruction='Draw a box around each **baby cow** and **big cow**', + attachment_type='image') +def test_documenttranscription_ok(): + client.create_documenttranscription_task( + callback_url= 'http://www.example.com/callback', + instruction= 'Please transcribe this receipt.', + attachment= 'http://document.scale.com/receipt-20200519.jpg', + features= [ + { + 'type': "block", + 'label': "barcode", + } + ] + ) -def test_comparison_fail(): +def test_documenttranscription_fail(): with pytest.raises(scaleapi.ScaleInvalidRequest): - client.create_comparison_task( + client.create_imageannotation_task( callback_url='http://www.example.com/callback', - instruction='Do the objects in these images have the same pattern?', - attachment_type='image') - + instruction='Please transcribe this receipt.', + ) def test_annotation_ok(): task = client.create_annotation_task( @@ -114,16 +114,6 @@ def test_annotation_ok(): min_height='30', objects_to_annotate=['baby cow', 'big cow'], with_labels=True) - # min_width and min_height should be optional - task2 = client.create_annotation_task( - callback_url='http://www.example.com/callback', - instruction='Draw a box around each **baby cow** and **big cow**', - attachment_type='image', - attachment='http://i.imgur.com/v4cBreD.jpg', - objects_to_annotate=['baby cow', 'big cow'], - with_labels=True) - - def test_annotation_fail(): with pytest.raises(scaleapi.ScaleInvalidRequest): @@ -132,7 +122,6 @@ def test_annotation_fail(): instruction='Draw a box around each **baby cow** and **big cow**', attachment_type='image') - def test_polygonannotation_ok(): task = client.create_polygonannotation_task( callback_url='http://www.example.com/callback', @@ -142,7 +131,6 @@ def test_polygonannotation_ok(): objects_to_annotate=['big cow'], with_labels=True) - def test_polygonannotation_fail(): with pytest.raises(scaleapi.ScaleInvalidRequest): client.create_polygonannotation_task( @@ -150,7 +138,6 @@ def test_polygonannotation_fail(): instruction='Draw a tight shape around the big cow', attachment_type='image') - def test_lineannotation_ok(): task = client.create_lineannotation_task( callback_url='http://www.example.com/callback', @@ -160,7 +147,6 @@ def test_lineannotation_ok(): objects_to_annotate=['big cow'], with_labels=True) - def test_lineannotation_fail(): with pytest.raises(scaleapi.ScaleInvalidRequest): client.create_lineannotation_task( @@ -168,7 +154,6 @@ def test_lineannotation_fail(): instruction='Draw a tight shape around the big cow', attachment_type='image') - def test_datacollection_ok(): task = client.create_datacollection_task( callback_url='http://www.example.com/callback', @@ -177,14 +162,12 @@ def test_datacollection_ok(): attachment='http://www.google.com/', fields={ 'hiring_page': 'Hiring Page URL' }) - def test_datacollection_fail(): with pytest.raises(scaleapi.ScaleInvalidRequest): client.create_datacollection_task( callback_url='http://www.example.com/callback', attachment_type='website') - def test_audiotranscription_ok(): task = client.create_audiotranscription_task( callback_url='http://www.example.com/callback', @@ -194,21 +177,12 @@ def test_audiotranscription_ok(): phrases=['avocado', 'stone'] ) - def test_audiotranscription_fail(): with pytest.raises(scaleapi.ScaleInvalidRequest): client.create_audiotranscription_task( callback_url='http://www.example.com/callback', attachment_type='audio') - -def test_audiotranscription_fail2(): - with pytest.raises(scaleapi.ScaleInvalidRequest): - client.create_audiotranscription_task( - callback_url='http://www.example.com/callback', - attachment='some_non_url') - - def test_namedentityrecognition_ok(): return client.create_namedentityrecognition_task( callback_url='http://www.example.com/callback', @@ -218,24 +192,13 @@ def test_namedentityrecognition_ok(): 'name': 'Label_A', 'description': 'the first label', }]) - - -def test_unicode_ok(): - task = client.create_categorization_task( - callback_url='http://www.example.com/callback', - instruction='Hello, 世界', - attachment_type='website', - attachment='http://www.google.com/', - categories=['public', 'private']) - - + def test_cancel(): task = make_a_task() # raises a scaleexception, because test tasks complete instantly with pytest.raises(scaleapi.ScaleException): task.cancel() - def test_task_retrieval(): task = make_a_task() task2 = client.fetch_task(task.id) @@ -251,7 +214,6 @@ def test_task_retrieval(): assert task2.type == task.type assert task2.created_at == task.created_at - def test_task_retrieval_time(): task = make_a_task() time.sleep(0.5) @@ -261,12 +223,10 @@ def test_task_retrieval_time(): tasks = client.tasks(start_time=start_time, end_time=end_time) assert tasks.docs == [] - def test_task_retrieval_fail(): with pytest.raises(scaleapi.ScaleException): client.fetch_task('fake_id_qwertyuiop') - def test_tasks(): tasks = [] for i in range(3): @@ -275,7 +235,25 @@ def test_tasks(): for task in client.tasks(limit=3): assert task.id in task_ids - def test_tasks_invalid(): with pytest.raises(scaleapi.ScaleException): client.tasks(bogus=0) + +def create_a_batch(): + return client.create_batch( + callback = "http://www.example.com/callback", + batch_name = "kitten_labeling_2020-07", + project = "kitten_labeling" + ) + +def test_finalize_batch(): + batch = create_a_batch() + client.finalize_batch(batch.name) + +def get_batch_status(): + batch = create_a_batch() + client.batch_status(batch.name) + +def get_batch(): + batch = create_a_batch() + client.get_batch(batch.name)