diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f56bbe4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Upload package to PyPI + +on: + release: + types: + - published + +jobs: + build-n-publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + + - name: Build + run: | + python setup.py sdist + python setup.py bdist_wheel + + - name: Check + run: | + ls ./dist + + - name: Publish a Python distribution to PyPI + if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/README.md b/README.md index d2b00e1..2836d77 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ _If you are using FastLabel prototype, please install version 0.2.2._ - [Multi Image](#multi-image) - [Video](#video) - [Video Classification](#video-classification) + - [Text](#text) + - [Text Classification](#text-classification) + - [Audio](#audio) + - [Audio Classification](#audio-classification) - [Common](#common) - [Annotation](#annotation) - [Project](#project) @@ -336,6 +340,12 @@ Find a single task. task = client.find_image_classification_task(task_id="YOUR_TASK_ID") ``` +Find a single task by name. + +```python +tasks = client.find_image_classification_task_by_name(project="YOUR_PROJECT_SLUG", task_name="YOUR_TASK_NAME") +``` + #### Get Tasks Get tasks. (Up to 1000 tasks) @@ -354,7 +364,7 @@ task_id = client.update_image_classification_task( task_id="YOUR_TASK_ID", status="approved", assignee="USER_SLUG", - tags=["tag1", "tag2"] + tags=["tag1", "tag2"], attributes=[ { "key": "attribute-key", @@ -453,6 +463,12 @@ Find a single task. task = client.find_multi_image_task(task_id="YOUR_TASK_ID") ``` +Find a single task by name. + +```python +tasks = client.find_multi_image_task_by_name(project="YOUR_PROJECT_SLUG", task_name="YOUR_TASK_NAME") +``` + #### Get Tasks Get tasks. @@ -609,6 +625,12 @@ Find a single task. task = client.find_video_task(task_id="YOUR_TASK_ID") ``` +Find a single task by name. + +```python +tasks = client.find_video_task_by_name(project="YOUR_PROJECT_SLUG", task_name="YOUR_TASK_NAME") +``` + #### Get Tasks Get tasks. (Up to 10 tasks) @@ -762,6 +784,12 @@ Find a single task. task = client.find_video_classification_task(task_id="YOUR_TASK_ID") ``` +Find a single task by name. + +```python +tasks = client.find_video_classification_task_by_name(project="YOUR_PROJECT_SLUG", task_name="YOUR_TASK_NAME") +``` + #### Get Tasks Get tasks. (Up to 1000 tasks) @@ -779,7 +807,366 @@ task_id = client.update_video_classification_task( task_id="YOUR_TASK_ID", status="approved", assignee="USER_SLUG", - tags=["tag1", "tag2"] + tags=["tag1", "tag2"], + attributes=[ + { + "key": "attribute-key", + "value": "attribute-value" + } + ], +) +``` + +### Text + +Supported following project types: + +- Text - NER + +#### Create Task + +Create a new task. + +```python +task_id = client.create_text_task( + project="YOUR_PROJECT_SLUG", + name="sample.txt", + file_path="./sample.txt" +) +``` + +Create a new task with pre-defined annotations. (Class should be configured on your project in advance) + +```python +task_id = client.create_text_task( + project="YOUR_PROJECT_SLUG", + name="sample.txt", + file_path="./sample.txt", + annotations=[{ + "type": "ner", + "value": "person", + "start": 0, + "end": 10, + "text": "1234567890" + }] +) +``` + +##### Limitation +* You can upload up to a size of 2 MB. + +#### Find Task + +Find a single task. + +```python +task = client.find_text_task(task_id="YOUR_TASK_ID") +``` + +Find a single task by name. + +```python +tasks = client.find_text_task_by_name(project="YOUR_PROJECT_SLUG", task_name="YOUR_TASK_NAME") +``` + +#### Get Tasks + +Get tasks. (Up to 10 tasks) + +```python +tasks = client.get_text_tasks(project="YOUR_PROJECT_SLUG") +``` + +#### Update Task + +Update a single task. + +```python +task_id = client.update_text_task( + task_id="YOUR_TASK_ID", + status="approved", + assignee="USER_SLUG", + tags=["tag1", "tag2"], + annotations=[{ + "type": "bbox", + "value": "bird", + "start": 0, + "end": 10, + "text": "0123456789" + }] +) +``` + +#### Response + +Example of a single text task object + +```python +{ + "id": "YOUR_TASK_ID", + "name": "cat.txt", + "url": "YOUR_TASK_URL", + "status": "registered", + "externalStatus": "registered", + "tags": [], + "assignee": "ASSIGNEE_NAME", + "reviewer": "REVIEWER_NAME", + "externalAssignee": "EXTERNAL_ASSIGNEE_NAME", + "externalReviewer": "EXTERNAL_REVIEWER_NAME", + "annotations": [ + { + "attributes": [], + "color": "#b36d18", + "text": "0123456789", + "start": 0, + "end": 10, + "title": "Cat", + "type": "ner", + "value": "cat" + } + ], + "createdAt": "2021-02-22T11:25:27.158Z", + "updatedAt": "2021-02-22T11:25:27.158Z" +} +``` + +### Text Classification + +Supported following project types: + +- Text - Classification (Single) + +#### Create Task + +Create a new task. + +```python +task_id = client.create_text_classification_task( + project="YOUR_PROJECT_SLUG", + name="sample.txt", + file_path="./sample.txt", + attributes=[ + { + "key": "attribute-key", + "value": "attribute-value" + } + ], +) +``` + +##### Limitation +* You can upload up to a size of 2 MB. + +#### Find Task + +Find a single task. + +```python +task = client.find_text_classification_task(task_id="YOUR_TASK_ID") +``` + +Find a single task by name. + +```python +tasks = client.find_text_classification_task_by_name(project="YOUR_PROJECT_SLUG", task_name="YOUR_TASK_NAME") +``` + +#### Get Tasks + +Get tasks. (Up to 1000 tasks) + +```python +tasks = client.get_text_classification_tasks(project="YOUR_PROJECT_SLUG") +``` + +#### Update Tasks + +Update a single task. + +```python +task_id = client.update_text_classification_task( + task_id="YOUR_TASK_ID", + status="approved", + assignee="USER_SLUG", + tags=["tag1", "tag2"], + attributes=[ + { + "key": "attribute-key", + "value": "attribute-value" + } + ], +) +``` + +### Audio + +Supported following project types: + +- Audio - Segmentation + +#### Create Task + +Create a new task. + +```python +task_id = client.create_audio_task( + project="YOUR_PROJECT_SLUG", + name="sample.mp3", + file_path="./sample.mp3" +) +``` + +Create a new task with pre-defined annotations. (Class should be configured on your project in advance) + +```python +task_id = client.create_audio_task( + project="YOUR_PROJECT_SLUG", + name="sample.mp3", + file_path="./sample.mp3", + annotations=[{ + "type": "segmentation", + "value": "person", + "start": 0.4, + "end": 0.5 + }] +) +``` + +##### Limitation +* You can upload up to a size of 120 MB. + +#### Find Task + +Find a single task. + +```python +task = client.find_audio_task(task_id="YOUR_TASK_ID") +``` + +Find a single task by name. + +```python +tasks = client.find_audio_task_by_name(project="YOUR_PROJECT_SLUG", task_name="YOUR_TASK_NAME") +``` + +#### Get Tasks + +Get tasks. (Up to 10 tasks) + +```python +tasks = client.get_audio_tasks(project="YOUR_PROJECT_SLUG") +``` + +#### Update Task + +Update a single task. + +```python +task_id = client.update_audio_task( + task_id="YOUR_TASK_ID", + status="approved", + assignee="USER_SLUG", + tags=["tag1", "tag2"], + annotations=[{ + "type": "segmentation", + "value": "bird", + "start": 0.4, + "end": 0.5 + }] +) +``` + +#### Response + +Example of a single audio task object + +```python +{ + "id": "YOUR_TASK_ID", + "name": "cat.mp3", + "url": "YOUR_TASK_URL", + "status": "registered", + "externalStatus": "registered", + "tags": [], + "assignee": "ASSIGNEE_NAME", + "reviewer": "REVIEWER_NAME", + "externalAssignee": "EXTERNAL_ASSIGNEE_NAME", + "externalReviewer": "EXTERNAL_REVIEWER_NAME", + "annotations": [ + { + "attributes": [], + "color": "#b36d18", + "start": 0.4, + "end": 0.5, + "title": "Bird", + "type": "segmentation", + "value": "bird" + } + ], + "createdAt": "2021-02-22T11:25:27.158Z", + "updatedAt": "2021-02-22T11:25:27.158Z" +} +``` + +### Audio Classification + +Supported following project types: + +- Audio - Classification (Single) + +#### Create Task + +Create a new task. + +```python +task_id = client.create_audio_classification_task( + project="YOUR_PROJECT_SLUG", + name="sample.mp3", + file_path="./sample.mp3", + attributes=[ + { + "key": "attribute-key", + "value": "attribute-value" + } + ], +) +``` + +##### Limitation +* You can upload up to a size of 120 MB. + +#### Find Task + +Find a single task. + +```python +task = client.find_audio_classification_task(task_id="YOUR_TASK_ID") +``` + +Find a single task by name. + +```python +tasks = client.find_audio_classification_task_by_name(project="YOUR_PROJECT_SLUG", task_name="YOUR_TASK_NAME") +``` + +#### Get Tasks + +Get tasks. (Up to 1000 tasks) + +```python +tasks = client.get_audio_classification_tasks(project="YOUR_PROJECT_SLUG") +``` + +#### Update Tasks + +Update a single task. + +```python +task_id = client.update_audio_classification_task( + task_id="YOUR_TASK_ID", + status="approved", + assignee="USER_SLUG", + tags=["tag1", "tag2"], attributes=[ { "key": "attribute-key", diff --git a/fastlabel/__init__.py b/fastlabel/__init__.py index e799a46..c94be6b 100644 --- a/fastlabel/__init__.py +++ b/fastlabel/__init__.py @@ -115,6 +115,104 @@ def find_video_task_by_name(self, project: str, task_name: str) -> dict: return None return tasks[0] + def find_video_classification_task_by_name( + self, project: str, task_name: str + ) -> dict: + """ + Find a single video classification task by name. + + project is slug of your project (Required). + task_name is a task name (Required). + """ + tasks = self.get_video_classification_tasks( + project=project, task_name=task_name + ) + if not tasks: + return None + return tasks[0] + + def find_text_task(self, task_id: str) -> dict: + """ + Find a single text task. + """ + endpoint = "tasks/text/" + task_id + return self.api.get_request(endpoint) + + def find_text_classification_task(self, task_id: str) -> dict: + """ + Find a single text classification task. + """ + endpoint = "tasks/text/classification/" + task_id + return self.api.get_request(endpoint) + + def find_text_task_by_name(self, project: str, task_name: str) -> dict: + """ + Find a single text task by name. + + project is slug of your project (Required). + task_name is a task name (Required). + """ + tasks = self.get_text_tasks(project=project, task_name=task_name) + if not tasks: + return None + return tasks[0] + + def find_text_classification_task_by_name( + self, project: str, task_name: str + ) -> dict: + """ + Find a single text classification task by name. + + project is slug of your project (Required). + task_name is a task name (Required). + """ + tasks = self.get_text_classification_tasks(project=project, task_name=task_name) + if not tasks: + return None + return tasks[0] + + def find_audio_task(self, task_id: str) -> dict: + """ + Find a single audio task. + """ + endpoint = "tasks/audio/" + task_id + return self.api.get_request(endpoint) + + def find_audio_classification_task(self, task_id: str) -> dict: + """ + Find a single audio classification task. + """ + endpoint = "tasks/audio/classification/" + task_id + return self.api.get_request(endpoint) + + def find_audio_task_by_name(self, project: str, task_name: str) -> dict: + """ + Find a single audio task by name. + + project is slug of your project (Required). + task_name is a task name (Required). + """ + tasks = self.get_audio_tasks(project=project, task_name=task_name) + if not tasks: + return None + return tasks[0] + + def find_audio_classification_task_by_name( + self, project: str, task_name: str + ) -> dict: + """ + Find a single audio classification task by name. + + project is slug of your project (Required). + task_name is a task name (Required). + """ + tasks = self.get_audio_classification_tasks( + project=project, task_name=task_name + ) + if not tasks: + return None + return tasks[0] + # Task Get def get_image_tasks( @@ -332,6 +430,176 @@ def get_video_classification_tasks( params["limit"] = limit return self.api.get_request(endpoint, params=params) + def get_text_tasks( + self, + project: str, + status: str = None, + external_status: str = None, + tags: list = [], + task_name: str = None, + offset: int = None, + limit: int = 100, + ) -> list: + """ + Returns a list of text tasks. + Returns up to 10 at a time, to get more, set offset as the starting position + to fetch. + + project is slug of your project (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + tags is a list of tag (Optional). + task_name is a task name (Optional). + offset is the starting position number to fetch (Optional). + limit is the max number to fetch (Optional). + """ + if limit > 1000: + raise FastLabelInvalidException( + "Limit must be less than or equal to 1000.", 422 + ) + endpoint = "tasks/text" + params = {"project": project} + if status: + params["status"] = status + if external_status: + params["externalStatus"] = external_status + if tags: + params["tags"] = tags + if task_name: + params["taskName"] = task_name + if offset: + params["offset"] = offset + if limit: + params["limit"] = limit + return self.api.get_request(endpoint, params=params) + + def get_text_classification_tasks( + self, + project: str, + status: str = None, + external_status: str = None, + tags: list = [], + task_name: str = None, + offset: int = None, + limit: int = 100, + ) -> list: + """ + Returns a list of text classification tasks. + Returns up to 1000 at a time, to get more, set offset as the starting position + to fetch. + + project is slug of your project (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined'. (Optional) + tags is a list of tag (Optional). + offset is the starting position number to fetch (Optional). + limit is the max number to fetch (Optional). + """ + endpoint = "tasks/text/classification" + params = {"project": project} + if status: + params["status"] = status + if external_status: + params["externalStatus"] = external_status + if tags: + params["tags"] = tags + if task_name: + params["taskName"] = task_name + if offset: + params["offset"] = offset + if limit: + params["limit"] = limit + return self.api.get_request(endpoint, params=params) + + def get_audio_tasks( + self, + project: str, + status: str = None, + external_status: str = None, + tags: list = [], + task_name: str = None, + offset: int = None, + limit: int = 100, + ) -> list: + """ + Returns a list of audio tasks. + Returns up to 10 at a time, to get more, set offset as the starting position + to fetch. + + project is slug of your project (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + tags is a list of tag (Optional). + task_name is a task name (Optional). + offset is the starting position number to fetch (Optional). + limit is the max number to fetch (Optional). + """ + if limit > 1000: + raise FastLabelInvalidException( + "Limit must be less than or equal to 1000.", 422 + ) + endpoint = "tasks/audio" + params = {"project": project} + if status: + params["status"] = status + if external_status: + params["externalStatus"] = external_status + if tags: + params["tags"] = tags + if task_name: + params["taskName"] = task_name + if offset: + params["offset"] = offset + if limit: + params["limit"] = limit + return self.api.get_request(endpoint, params=params) + + def get_audio_classification_tasks( + self, + project: str, + status: str = None, + external_status: str = None, + tags: list = [], + task_name: str = None, + offset: int = None, + limit: int = 100, + ) -> list: + """ + Returns a list of audio classification tasks. + Returns up to 1000 at a time, to get more, set offset as the starting position + to fetch. + + project is slug of your project (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined'. (Optional) + tags is a list of tag (Optional). + offset is the starting position number to fetch (Optional). + limit is the max number to fetch (Optional). + """ + endpoint = "tasks/audio/classification" + params = {"project": project} + if status: + params["status"] = status + if external_status: + params["externalStatus"] = external_status + if tags: + params["tags"] = tags + if task_name: + params["taskName"] = task_name + if offset: + params["offset"] = offset + if limit: + params["limit"] = limit + return self.api.get_request(endpoint, params=params) + def get_task_id_name_map( self, project: str, @@ -662,25 +930,29 @@ def create_video_classification_task( return self.api.post_request(endpoint, payload=payload) - # Task Update - - def update_task( + def create_text_task( self, - task_id: str, + project: str, + name: str, + file_path: str, status: str = None, external_status: str = None, + annotations: list = [], tags: list = [], **kwargs, ) -> str: """ - Update a single task. + Create a single text task. - task_id is an id of the task (Required). + project is slug of your project (Required). + name is an unique identifier of task in your project (Required). + file_path is a path to data. Supported extensions are txt (Required). status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', 'approved', 'declined' (Optional). external_status can be 'registered', 'completed', 'skipped', 'reviewed', - 'sent_back', 'approved', 'declined', 'customer_declined'. (Optional) - tags is a list of tag to be set (Optional). + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + annotations is a list of annotation to be set in advance (Optional). + tags is a list of tag to be set in advance (Optional). assignee is slug of assigned user (Optional). reviewer is slug of review user (Optional). approver is slug of approve user (Optional). @@ -688,18 +960,230 @@ def update_task( external_reviewer is slug of external review user (Optional). external_approver is slug of external approve user (Optional). """ - endpoint = "tasks/" + task_id - payload = {} + endpoint = "tasks/text" + if not utils.is_text_supported_ext(file_path): + raise FastLabelInvalidException("Supported extensions are txt.", 422) + if not utils.is_text_supported_size(file_path): + raise FastLabelInvalidException("Supported text size is under 2 MB.", 422) + + file = utils.base64_encode(file_path) + payload = {"project": project, "name": name, "file": file} if status: payload["status"] = status if external_status: payload["externalStatus"] = external_status + if annotations: + for annotation in annotations: + annotation["content"] = name + payload["annotations"] = annotations if tags: payload["tags"] = tags self.__fill_assign_users(payload, **kwargs) - return self.api.put_request(endpoint, payload=payload) + return self.api.post_request(endpoint, payload=payload) + + def create_text_classification_task( + self, + project: str, + name: str, + file_path: str, + status: str = None, + external_status: str = None, + attributes: list = [], + tags: list = [], + **kwargs, + ) -> str: + """ + Create a single text classification task. + + project is slug of your project (Required). + name is an unique identifier of task in your project (Required). + file_path is a path to data. Supported extensions are mp4 (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + attributes is a list of attribute to be set in advance (Optional). + tags is a list of tag to be set in advance (Optional). + assignee is slug of assigned user (Optional). + reviewer is slug of review user (Optional). + approver is slug of approve user (Optional). + external_assignee is slug of external assigned user (Optional). + external_reviewer is slug of external review user (Optional). + external_approver is slug of external approve user (Optional). + """ + endpoint = "tasks/text/classification" + if not utils.is_text_supported_ext(file_path): + raise FastLabelInvalidException("Supported extensions are txt.", 422) + if not utils.is_text_supported_size(file_path): + raise FastLabelInvalidException("Supported text size is under 2 MB.", 422) + + file = utils.base64_encode(file_path) + payload = {"project": project, "name": name, "file": file} + if status: + payload["status"] = status + if external_status: + payload["externalStatus"] = external_status + if attributes: + payload["attributes"] = attributes + if tags: + payload["tags"] = tags + + self.__fill_assign_users(payload, **kwargs) + + return self.api.post_request(endpoint, payload=payload) + + def create_audio_task( + self, + project: str, + name: str, + file_path: str, + status: str = None, + external_status: str = None, + annotations: list = [], + tags: list = [], + **kwargs, + ) -> str: + """ + Create a single audio task. + + project is slug of your project (Required). + name is an unique identifier of task in your project (Required). + file_path is a path to data. Supported extensions are mp4 (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + annotations is a list of annotation to be set in advance (Optional). + tags is a list of tag to be set in advance (Optional). + assignee is slug of assigned user (Optional). + reviewer is slug of review user (Optional). + approver is slug of approve user (Optional). + external_assignee is slug of external assigned user (Optional). + external_reviewer is slug of external review user (Optional). + external_approver is slug of external approve user (Optional). + """ + endpoint = "tasks/audio" + if not utils.is_audio_supported_ext(file_path): + raise FastLabelInvalidException( + "Supported extensions are mp3, wav and w4a.", 422 + ) + if not utils.is_audio_supported_size(file_path): + raise FastLabelInvalidException( + "Supported audio size is under 120 MB.", 422 + ) + + file = utils.base64_encode(file_path) + payload = {"project": project, "name": name, "file": file} + if status: + payload["status"] = status + if external_status: + payload["externalStatus"] = external_status + if annotations: + for annotation in annotations: + annotation["content"] = name + payload["annotations"] = annotations + if tags: + payload["tags"] = tags + + self.__fill_assign_users(payload, **kwargs) + + return self.api.post_request(endpoint, payload=payload) + + def create_audio_classification_task( + self, + project: str, + name: str, + file_path: str, + status: str = None, + external_status: str = None, + attributes: list = [], + tags: list = [], + **kwargs, + ) -> str: + """ + Create a single audio classification task. + + project is slug of your project (Required). + name is an unique identifier of task in your project (Required). + file_path is a path to data. Supported extensions are mp4 (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + attributes is a list of attribute to be set in advance (Optional). + tags is a list of tag to be set in advance (Optional). + assignee is slug of assigned user (Optional). + reviewer is slug of review user (Optional). + approver is slug of approve user (Optional). + external_assignee is slug of external assigned user (Optional). + external_reviewer is slug of external review user (Optional). + external_approver is slug of external approve user (Optional). + """ + endpoint = "tasks/audio/classification" + if not utils.is_audio_supported_ext(file_path): + raise FastLabelInvalidException( + "Supported extensions are mp3, wav and w4a.", 422 + ) + if not utils.is_audio_supported_size(file_path): + raise FastLabelInvalidException( + "Supported audio size is under 120 MB.", 422 + ) + + file = utils.base64_encode(file_path) + payload = {"project": project, "name": name, "file": file} + if status: + payload["status"] = status + if external_status: + payload["externalStatus"] = external_status + if attributes: + payload["attributes"] = attributes + if tags: + payload["tags"] = tags + + self.__fill_assign_users(payload, **kwargs) + + return self.api.post_request(endpoint, payload=payload) + + # Task Update + + def update_task( + self, + task_id: str, + status: str = None, + external_status: str = None, + tags: list = [], + **kwargs, + ) -> str: + """ + Update a single task. + + task_id is an id of the task (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined'. (Optional) + tags is a list of tag to be set (Optional). + assignee is slug of assigned user (Optional). + reviewer is slug of review user (Optional). + approver is slug of approve user (Optional). + external_assignee is slug of external assigned user (Optional). + external_reviewer is slug of external review user (Optional). + external_approver is slug of external approve user (Optional). + """ + endpoint = "tasks/" + task_id + payload = {} + if status: + payload["status"] = status + if external_status: + payload["externalStatus"] = external_status + if tags: + payload["tags"] = tags + + self.__fill_assign_users(payload, **kwargs) + + return self.api.put_request(endpoint, payload=payload) def update_image_task( self, @@ -912,6 +1396,174 @@ def update_video_classification_task( return self.api.put_request(endpoint, payload=payload) + def update_text_task( + self, + task_id: str, + status: str = None, + external_status: str = None, + tags: list = [], + annotations: List[dict] = [], + **kwargs, + ) -> str: + """ + Update a single text task. + + task_id is an id of the task (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + tags is a list of tag to be set (Optional). + annotations is a list of annotation to be set (Optional). + assignee is slug of assigned user (Optional). + reviewer is slug of review user (Optional). + approver is slug of approve user (Optional). + external_assignee is slug of external assigned user (Optional). + external_reviewer is slug of external review user (Optional). + external_approver is slug of external approve user (Optional). + """ + endpoint = "tasks/text/" + task_id + payload = {} + if status: + payload["status"] = status + if external_status: + payload["externalStatus"] = external_status + if tags: + payload["tags"] = tags + if annotations: + for annotation in annotations: + annotation["content"] = "" + payload["annotations"] = annotations + + self.__fill_assign_users(payload, **kwargs) + + return self.api.put_request(endpoint, payload=payload) + + def update_text_classification_task( + self, + task_id: str, + status: str = None, + external_status: str = None, + attributes: list = [], + tags: list = [], + **kwargs, + ) -> str: + """ + Update a single text classification task. + + task_id is an id of the task (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + attributes is a list of attribute to be set in advance (Optional). + tags is a list of tag to be set in advance (Optional). + assignee is slug of assigned user (Optional). + reviewer is slug of review user (Optional). + approver is slug of approve user (Optional). + external_assignee is slug of external assigned user (Optional). + external_reviewer is slug of external review user (Optional). + external_approver is slug of external approve user (Optional). + """ + endpoint = "tasks/text/classification/" + task_id + payload = {} + if status: + payload["status"] = status + if external_status: + payload["externalStatus"] = external_status + if attributes: + payload["attributes"] = attributes + if tags: + payload["tags"] = tags + + self.__fill_assign_users(payload, **kwargs) + + return self.api.put_request(endpoint, payload=payload) + + def update_audio_task( + self, + task_id: str, + status: str = None, + external_status: str = None, + tags: list = [], + annotations: List[dict] = [], + **kwargs, + ) -> str: + """ + Update a single audio task. + + task_id is an id of the task (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + tags is a list of tag to be set (Optional). + annotations is a list of annotation to be set (Optional). + assignee is slug of assigned user (Optional). + reviewer is slug of review user (Optional). + approver is slug of approve user (Optional). + external_assignee is slug of external assigned user (Optional). + external_reviewer is slug of external review user (Optional). + external_approver is slug of external approve user (Optional). + """ + endpoint = "tasks/audio/" + task_id + payload = {} + if status: + payload["status"] = status + if external_status: + payload["externalStatus"] = external_status + if tags: + payload["tags"] = tags + if annotations: + for annotation in annotations: + annotation["content"] = "" + payload["annotations"] = annotations + + self.__fill_assign_users(payload, **kwargs) + + return self.api.put_request(endpoint, payload=payload) + + def update_audio_classification_task( + self, + task_id: str, + status: str = None, + external_status: str = None, + attributes: list = [], + tags: list = [], + **kwargs, + ) -> str: + """ + Update a single audio classification task. + + task_id is an id of the task (Required). + status can be 'registered', 'completed', 'skipped', 'reviewed', 'sent_back', + 'approved', 'declined' (Optional). + external_status can be 'registered', 'completed', 'skipped', 'reviewed', + 'sent_back', 'approved', 'declined', 'customer_declined' (Optional). + attributes is a list of attribute to be set in advance (Optional). + tags is a list of tag to be set in advance (Optional). + assignee is slug of assigned user (Optional). + reviewer is slug of review user (Optional). + approver is slug of approve user (Optional). + external_assignee is slug of external assigned user (Optional). + external_reviewer is slug of external review user (Optional). + external_approver is slug of external approve user (Optional). + """ + endpoint = "tasks/audio/classification/" + task_id + payload = {} + if status: + payload["status"] = status + if external_status: + payload["externalStatus"] = external_status + if attributes: + payload["attributes"] = attributes + if tags: + payload["tags"] = tags + + self.__fill_assign_users(payload, **kwargs) + + return self.api.put_request(endpoint, payload=payload) + # Task Delete def delete_task(self, task_id: str) -> None: diff --git a/fastlabel/const.py b/fastlabel/const.py index 9a20a07..9701141 100644 --- a/fastlabel/const.py +++ b/fastlabel/const.py @@ -2,10 +2,187 @@ from enum import Enum # only 57 types -COLOR_PALETTE = [0, 0, 0, 228, 26, 28, 55, 126, 184, 77, 175, 74, 152, 78, 163, 255, 127, 0, 255, 255, 51, 166, 86, 40, 247, 129, 191, 153, 153, 153, 102, 194, 165, 252, 141, 98, 141, 160, 203, 231, 138, 195, 166, 216, 84, 255, 217, 47, 229, 196, 148, 179, 179, 179, 141, 211, 199, 255, 255, 179, 190, 186, 218, 251, 128, 114, 128, 177, 211, 253, 180, 98, 179, 222, 105, 252, 205, 229, 217, 217, 217, 188, 128, 189, 204, 235, 197, 255, 237, 111, 166, 206, 227, 31, 120, 180, 178, 223, 138, 51, 160, 44, 251, 154, 153, 227, 26, 28, 253, 191, 111, 255, 127, 0, 202, 178, 214, 106, 61, 154, 255, 255, 153, 177, 89, 40, 127, 201, 127, 190, 174, 212, 253, 192, 134, 255, 255, 153, 56, 108, 176, 240, 2, 127, 191, 91, 22, 102, 102, 102, 27, 158, 119, 217, 95, 2, 117, 112, 179, 231, 41, 138, 102, 166, 30, 230, 171, 2, 166, 118, 29, 102, 102, 102] +COLOR_PALETTE = [ + 0, + 0, + 0, + 228, + 26, + 28, + 55, + 126, + 184, + 77, + 175, + 74, + 152, + 78, + 163, + 255, + 127, + 0, + 255, + 255, + 51, + 166, + 86, + 40, + 247, + 129, + 191, + 153, + 153, + 153, + 102, + 194, + 165, + 252, + 141, + 98, + 141, + 160, + 203, + 231, + 138, + 195, + 166, + 216, + 84, + 255, + 217, + 47, + 229, + 196, + 148, + 179, + 179, + 179, + 141, + 211, + 199, + 255, + 255, + 179, + 190, + 186, + 218, + 251, + 128, + 114, + 128, + 177, + 211, + 253, + 180, + 98, + 179, + 222, + 105, + 252, + 205, + 229, + 217, + 217, + 217, + 188, + 128, + 189, + 204, + 235, + 197, + 255, + 237, + 111, + 166, + 206, + 227, + 31, + 120, + 180, + 178, + 223, + 138, + 51, + 160, + 44, + 251, + 154, + 153, + 227, + 26, + 28, + 253, + 191, + 111, + 255, + 127, + 0, + 202, + 178, + 214, + 106, + 61, + 154, + 255, + 255, + 153, + 177, + 89, + 40, + 127, + 201, + 127, + 190, + 174, + 212, + 253, + 192, + 134, + 255, + 255, + 153, + 56, + 108, + 176, + 240, + 2, + 127, + 191, + 91, + 22, + 102, + 102, + 102, + 27, + 158, + 119, + 217, + 95, + 2, + 117, + 112, + 179, + 231, + 41, + 138, + 102, + 166, + 30, + 230, + 171, + 2, + 166, + 118, + 29, + 102, + 102, + 102, +] -# under 512 MB. Actual size is 536870888 bytes, but to consider other attributes, minus 888 bytes. -# Because of V8's limitation, API only can accept the JSON string that length is under this. +# under 512 MB. Actual size is 536870888 bytes, but to consider other attributes, +# minus 888 bytes. +# Because of V8's limitation, API only can accept the JSON string that length is +# under this. SUPPORTED_CONTENTS_SIZE = 536870000 # API can accept under 250 MB @@ -14,6 +191,12 @@ # API can accept under 20 MB SUPPORTED_IMAGE_SIZE = 20 * math.pow(1024, 2) +# API can accept under 2 MB +SUPPORTED_TEXT_SIZE = 2 * math.pow(1024, 2) + +# API can accept under 120 MB +SUPPORTED_AUDIO_SIZE = 120 * math.pow(1024, 2) + class AnnotationType(Enum): bbox = "bbox" diff --git a/fastlabel/utils.py b/fastlabel/utils.py index b717f6e..2680822 100644 --- a/fastlabel/utils.py +++ b/fastlabel/utils.py @@ -1,9 +1,11 @@ -import os import base64 -import numpy as np -import geojson import json +import os from typing import List + +import geojson +import numpy as np + from fastlabel import const @@ -13,11 +15,19 @@ def base64_encode(file_path: str) -> str: def is_image_supported_ext(file_path: str) -> bool: - return file_path.lower().endswith(('.png', '.jpg', '.jpeg')) + return file_path.lower().endswith((".png", ".jpg", ".jpeg")) def is_video_supported_ext(file_path: str) -> bool: - return file_path.lower().endswith('.mp4') + return file_path.lower().endswith(".mp4") + + +def is_text_supported_ext(file_path: str) -> bool: + return file_path.lower().endswith(".txt") + + +def is_audio_supported_ext(file_path: str) -> bool: + return file_path.lower().endswith((".mp3", ".wav", ".m4a")) def is_image_supported_size(file_path: str) -> bool: @@ -28,8 +38,16 @@ def is_video_supported_size(file_path: str) -> bool: return os.path.getsize(file_path) <= const.SUPPORTED_VIDEO_SIZE +def is_text_supported_size(file_path: str) -> bool: + return os.path.getsize(file_path) <= const.SUPPORTED_TEXT_SIZE + + +def is_audio_supported_size(file_path: str) -> bool: + return os.path.getsize(file_path) <= const.SUPPORTED_AUDIO_SIZE + + def is_json_ext(file_name: str) -> bool: - return file_name.lower().endswith('.json') + return file_name.lower().endswith(".json") def get_basename(file_path: str) -> str: @@ -52,10 +70,8 @@ def reverse_points(points: List[int]) -> List[int]: reversed_points = [] for index, _ in enumerate(points): if index % 2 == 0: - reversed_points.insert( - 0, points[index + 1]) - reversed_points.insert( - 0, points[index]) + reversed_points.insert(0, points[index + 1]) + reversed_points.insert(0, points[index]) return reversed_points @@ -63,20 +79,26 @@ def is_clockwise(points: list) -> bool: """ points: [x1, y1, x2, y2, x3, y3, ... xn, yn] Sum over the edges, (x2 − x1)(y2 + y1). - If the result is positive the curve is clockwise, if it's negative the curve is counter-clockwise. + If the result is positive the curve is clockwise, + if it's negative the curve is counter-clockwise. The above is assumes a normal Cartesian coordinate system. HTML5 canvas, use an inverted Y-axis. Therefore If the area is negative, the curve is clockwise. """ - points_splitted = [points[idx:idx + 2] - for idx in range(0, len(points), 2)] + points_splitted = [points[idx : idx + 2] for idx in range(0, len(points), 2)] polygon_geo = geojson.Polygon(points_splitted) coords = np.array(list(geojson.utils.coords(polygon_geo))) xs, ys = map(list, zip(*coords)) xs.append(xs[0]) ys.append(ys[0]) - sum_edges = sum((xs[i] - xs[i - 1]) * (ys[i] + ys[i - 1]) for i in range(1, len(points_splitted))) / 2.0 + sum_edges = ( + sum( + (xs[i] - xs[i - 1]) * (ys[i] + ys[i - 1]) + for i in range(1, len(points_splitted)) + ) + / 2.0 + ) if sum_edges < 0: return True @@ -86,4 +108,3 @@ def is_clockwise(points: list) -> bool: def get_json_length(value) -> int: json_str = json.dumps(value) return len(json_str) - diff --git a/pypi_update_guide.md b/pypi_update_guide.md index 5b52730..1a65a00 100644 --- a/pypi_update_guide.md +++ b/pypi_update_guide.md @@ -4,50 +4,66 @@ _Creating and deploying a new package version is easy_ ## Prerequisites -1. Ensure you're on the latest master +1. Ensure you have a PyPI account created and are added as a Collaborator -2. Ensure you have a PyPI account created and are added as a Collaborator +2. Store PyPI API Token to GitHub Secrets + If you have already created GitHub Secret `PYPI_API_TOKEN`, skip this step. -## Deployment Steps: + 1. Get PyPI API Token + 1. Go to [PyPI Account Settings Page](https://pypi.org/manage/account/) + 2. Click `Add API Token` button in API Token section + 3. Enter the following + - Token name: `GitHub Actions Token` + - Scope: `Project: fastlabel` + 4. Click `Add Token` button and get API Token -**Step 0: Critical - Bump Project Version** + 2. Store Token to GitHub Secrets + 1. Go to GitHub `fastlabel-python-sdk` repository + 2. Go to Settings > Secrets > Actions + 3. Click `New repository secret` and enter the following + - Name: `PYPI_API_TOKEN` + - Value: PyPI API Token -In `setup.py`, you need to specify a new project version. +## Deployment Steps -We use [semantic versioning](https://packaging.python.org/guides/distributing-packages-using-setuptools/#semantic-versioning-preferred). If you are adding a meaningful feature, bump the minor version. If you are fixing a bug, bump the incremental version. +**Step 1: Create a new release** -**Step 1: Remove Previous Versions** +1. Click `Releases` label in `Code` tab and go to Releases page -Clear out any previously packaged files in the `dist` folder +2. Click `Draft a new release` button -**Step 2: Create a Source Distribution** +3. Enter the following + - Tag + - Click `Choose a tag` select box + - input [version](#version) (ex: `1.12.0`) + - Click `Create new tag: x.x.x` -``` -python3 setup.py sdist -``` + - Target: main -**Step 3: Create `wheel`** + - Release title: `Release x.x.x` (ex: `Release 1.12.0`) -You should also create a wheel for your project. A wheel is a built package that can be installed without needing to go through the “build” process. Installing wheels is substantially faster for the end user than installing from a source distribution + - (Optional) fill in the description -``` -python3 setup.py bdist_wheel -``` +4. Click `Publish release` button -**Step 4: Install Twine** +**Step 2: (Automatically) Execute GitHub Actions Workflow** -Twine is what is used to manage PyPI pacakges +After creating a release, GitHub Actions Workflow will be triggered automatically. +This workflow builds the SDK distribution and uploads it to PyPI. -``` -pip install twine -``` +If the workflow fails, follow these steps: +1. Fix the cause of the error +2. Remove release created in Step 1 +3. Remove tag created in Step 1 +4. Repeat from Step 1 -**Step 5: Upload distribution to PyPI** +**Step 3: Check out the PyPI page to ensure all looks good** -``` -python3 -m twine upload dist/* -``` +[https://pypi.org/project/fastlabel/](https://pypi.org/project/fastlabel/) -**Step 6: Check out the PyPI page to ensure all looks good** -[https://pypi.org/project/fastlabel/](https://pypi.org/project/fastlabel/) +--- +### Version +We use [semantic versioning](https://packaging.python.org/guides/distributing-packages-using-setuptools/#semantic-versioning-preferred). +If you are adding a meaningful feature, bump the minor version. +If you are fixing a bug, bump the incremental version. diff --git a/setup.py b/setup.py index dc0e256..2677dd3 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,6 @@ setuptools.setup( name="fastlabel", - version="0.11.16", author="eisuke-ueta", author_email="eisuke.ueta@fastlabel.ai", description="The official Python SDK for FastLabel API, the Data Platform for AI", @@ -18,4 +17,8 @@ install_requires=install_requires, python_requires=">=3.7", include_package_data=True, + use_scm_version=True, + setup_requires=[ + "setuptools_scm" + ], )