# coding: utf-8
"""list/create teams and monitor their activity"""
# docs
from __future__ import annotations
from typing import Callable, Dict, List, Literal, NamedTuple, Optional, Union
from tqdm import tqdm
from supervisely.api.module_api import ApiField, ModuleNoParent, UpdateableModule
from supervisely.sly_logger import logger
# @TODO - umar will add meta with review status and duration
[docs]class ActivityAction:
"""
List of Team Actions to sort Team Activity.
"""
LOGIN = "login"
""""""
LOGOUT = "logout"
""""""
CREATE_PROJECT = "create_project"
""""""
UPDATE_PROJECT = "update_project"
""""""
DISABLE_PROJECT = "disable_project"
""""""
RESTORE_PROJECT = "restore_project"
""""""
CREATE_DATASET = "create_dataset"
""""""
UPDATE_DATASET = "update_dataset"
""""""
DISABLE_DATASET = "disable_dataset"
""""""
RESTORE_DATASET = "restore_dataset"
""""""
CREATE_IMAGE = "create_image"
""""""
UPDATE_IMAGE = "update_image"
""""""
DISABLE_IMAGE = "disable_image"
""""""
RESTORE_IMAGE = "restore_image"
""""""
CREATE_FIGURE = "create_figure"
""""""
UPDATE_FIGURE = "update_figure"
""""""
DISABLE_FIGURE = "disable_figure"
""""""
RESTORE_FIGURE = "restore_figure"
""""""
CREATE_CLASS = "create_class"
""""""
UPDATE_CLASS = "update_class"
""""""
DISABLE_CLASS = "disable_class"
""""""
RESTORE_CLASS = "restore_class"
""""""
CREATE_BACKUP = "create_backup"
""""""
EXPORT_PROJECT = "export_project"
""""""
MODEL_TRAIN = "model_train"
""""""
MODEL_INFERENCE = "model_inference"
""""""
CREATE_PLUGIN = "create_plugin"
""""""
DISABLE_PLUGIN = "disable_plugin"
""""""
RESTORE_PLUGIN = "restore_plugin"
""""""
CREATE_NODE = "create_node"
""""""
DISABLE_NODE = "disable_node"
""""""
RESTORE_NODE = "restore_node"
""""""
CREATE_WORKSPACE = "create_workspace"
""""""
DISABLE_WORKSPACE = "disable_workspace"
""""""
RESTORE_WORKSPACE = "restore_workspace"
""""""
CREATE_MODEL = "create_model"
""""""
DISABLE_MODEL = "disable_model"
""""""
RESTORE_MODEL = "restore_model"
""""""
ADD_MEMBER = "add_member"
""""""
REMOVE_MEMBER = "remove_member"
""""""
LOGIN_TO_TEAM = "login_to_team"
""""""
ATTACH_TAG = "attach_tag"
""""""
UPDATE_TAG_VALUE = "update_tag_value"
""""""
DETACH_TAG = "detach_tag"
""""""
ANNOTATION_DURATION = "annotation_duration"
""""""
IMAGE_REVIEW_STATUS_UPDATED = "image_review_status_updated"
""""""
LABELER_ID = "labelerId"
""""""
REVIEW_STATUS = "reviewStatus"
# case #1 - labeler pressed "finish image" button in labeling job
# action: IMAGE_REVIEW_STATUS_UPDATED -> meta["reviewStatus"] == 'done'
# case #2 - reviewer pressed "accept" or "reject" button
# action: IMAGE_REVIEW_STATUS_UPDATED -> meta["reviewStatus"] == 'accepted' or 'rejected'
# possible review statuses:
# 'done' - i.e. labeler finished the image,
# 'accepted' - reviewer
# 'rejected' - reviewer
# case #3 duration
# action: ANNOTATION_DURATION -> meta["duration"] e.g. meta-> {"duration": 30} in seconds
class UsageInfo(NamedTuple):
""" """
plan: str
class TeamInfo(NamedTuple):
""" """
id: int
name: str
description: str
role: str
created_at: str
updated_at: str
usage: UsageInfo
[docs]class TeamApi(ModuleNoParent, UpdateableModule):
"""
API for working with Team. :class:`TeamApi<TeamApi>` object is immutable.
:param api: API connection to the server
:type api: Api
:Usage example:
.. code-block:: python
import os
from dotenv import load_dotenv
import supervisely as sly
# Load secrets and create API object from .env file (recommended)
# Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
if sly.is_development():
load_dotenv(os.path.expanduser("~/supervisely.env"))
api = sly.Api.from_env()
# Pass values into the API constructor (optional, not recommended)
# api = sly.Api(server_address="https://app.supervise.ly", token="4r47N...xaTatb")
team_info = api.team.get_info_by_id(team_id) # api usage example
"""
[docs] @staticmethod
def info_sequence():
"""
NamedTuple TeamInfo containing information about Team.
:Example:
.. code-block:: python
TeamInfo(id=1,
name='Vehicle',
description='',
role='admin',
created_at='2020-03-31T14:49:08.931Z',
updated_at='2020-03-31T14:49:08.931Z',
UsageInfo(plan='free') )
"""
return [
ApiField.ID,
ApiField.NAME,
ApiField.DESCRIPTION,
ApiField.ROLE,
ApiField.CREATED_AT,
ApiField.UPDATED_AT,
ApiField.USAGE,
]
[docs] @staticmethod
def info_tuple_name():
"""
NamedTuple name - **TeamInfo**.
"""
return "TeamInfo"
def __init__(self, api):
ModuleNoParent.__init__(self, api)
UpdateableModule.__init__(self, api)
[docs] def get_list(self, filters: List[Dict[str, str]] = None) -> List[TeamInfo]:
"""
List of all Teams.
:param filters: List of params to sort output Teams.
:type filters: list, optional
:return: List of all Teams with information. See :class:`info_sequence<info_sequence>`
:rtype: :class:`List[TeamInfo]`
:Usage example:
.. code-block:: python
import supervisely as sly
team_id = 8
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
api = sly.Api.from_env()
team_list = api.team.get_list(team_id)
print(team_list)
# Output: [TeamInfo(id=1,
# name='Vehicle',
# description='',
# role='admin',
# created_at='2020-03-31T14:49:08.931Z',
# updated_at='2020-03-31T14:49:08.931Z',
# UsageInfo(plan='free') ),
# TeamInfo(id=2,
# name='Road',
# description='',
# role='admin',
# created_at='2020-03-31T08:52:11.000Z',
# updated_at='2020-03-31T08:52:11.000Z',
# UsageInfo(plan='free') ),
# TeamInfo(id=3,
# name='Animal',
# description='',
# role='admin',
# created_at='2020-04-02T08:59:03.717Z',
# updated_at='2020-04-02T08:59:03.717Z',
# UsageInfo(plan='free') )
# ]
# Filtered Team list
team_list = api.team.get_list(team_id, filters=[{ 'field': 'name', 'operator': '=', 'value': 'Animal' }])
print(team_list)
# Output: [TeamInfo(id=3,
# name='Animal',
# description='',
# role='admin',
# created_at='2020-04-02T08:59:03.717Z',
# updated_at='2020-04-02T08:59:03.717Z',
# UsageInfo(plan='free') )
# ]
"""
return self.get_list_all_pages("teams.list", {ApiField.FILTER: filters or []})
[docs] def get_info_by_id(self, id: int, raise_error: Optional[bool] = False) -> TeamInfo:
"""
Get Team information by ID.
:param id: Team ID in Supervisely.
:type id: int
:return: Information about Team. See :class:`info_sequence<info_sequence>`
:rtype: :class:`TeamInfo`
:Usage example:
.. code-block:: python
import supervisely as sly
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
api = sly.Api.from_env()
team_info = api.team.get_info_by_id(8)
print(team_info)
# Output: TeamInfo(id=8,
# name='Fruits',
# description='',
# role='admin',
# created_at='2020-04-15T10:50:41.926Z',
# updated_at='2020-04-15T10:50:41.926Z',
# UsageInfo(plan='free') )
# You can also get Team info by name
team_info = api.team.get_info_by_name("Fruits")
print(team_info)
# Output: TeamInfo(id=8,
# name='Fruits',
# description='',
# role='admin',
# created_at='2020-04-15T10:50:41.926Z',
# updated_at='2020-04-15T10:50:41.926Z',
# UsageInfo(plan='free') )
"""
info = self._get_info_by_id(id, "teams.info")
if info is None and raise_error is True:
raise KeyError(f"Team with id={id} not found in your account")
return info
[docs] def create(
self,
name: str,
description: Optional[str] = "",
change_name_if_conflict: Optional[bool] = False,
) -> TeamInfo:
"""
Creates Team with given name.
:param name: Team name.
:type name: str
:param description: Team description.
:type description: str
:param change_name_if_conflict: Checks if given name already exists and adds suffix to the end of the name.
:type change_name_if_conflict: bool, optional
:return: Information about Team. See :class:`info_sequence<info_sequence>`
:rtype: :class:`TeamInfo`
:Usage example:
.. code-block:: python
import supervisely as sly
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
api = sly.Api.from_env()
new_team = api.team.create("Flowers")
print(new_team)
# Output: TeamInfo(id=228,
# name='Flowers',
# description='',
# role='admin',
# created_at='2021-03-11T11:18:46.576Z',
# updated_at='2021-03-11T11:18:46.576Z',
# UsageInfo(plan='free') )
"""
effective_name = self._get_effective_new_name(
name=name, change_name_if_conflict=change_name_if_conflict
)
response = self._api.post(
"teams.add",
{ApiField.NAME: effective_name, ApiField.DESCRIPTION: description},
)
return self._convert_json_info(response.json())
def _get_update_method(self):
""" """
return "teams.editInfo"
[docs] def get_activity(
self,
team_id: int,
filter_user_id: Optional[int] = None,
filter_project_id: Optional[int] = None,
filter_job_id: Optional[int] = None,
filter_actions: Optional[List] = None,
progress_cb: Optional[Union[tqdm, Callable]] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
labeler_id: Optional[int] = None,
review_status: Optional[Literal["done", "accepted", "rejected"]] = None,
) -> List[Dict]:
"""
Get Team activity by ID.
:param team_id: Team ID in Supervisely.
:type team_id: int
:param filter_user_id: User ID by which the activity will be filtered.
:type filter_user_id: int, optional
:param filter_project_id: Project ID by which the activity will be filtered.
:type filter_project_id: int, optional
:param filter_job_id: Job ID by which the activity will be filtered.
:type filter_job_id: int, optional
:param filter_actions: List of ActivityAction by which the activity will be filtered.
:type filter_actions: list, optional
:param progress_cb: Function to check progress.
:type progress_cb: tqdm or callable, optional
:param start_date: Start date to get Team activity.
:type start_date: str, optional
:param end_date: End date to get Team activity.
:type end_date: str, optional
:param labeler_id: Labeler ID by which the activity will be filtered.
:type labeler_id: int, optional
:param review_status: Review status by which the activity will be filtered.
:type review_status: str, optional, one of ['done', 'accepted', 'rejected']
:return: Team activity
:rtype: :class:`List[dict]`
:Usage example:
.. code-block:: python
import supervisely as sly
from supervisely.api.team_api import ActivityAction as aa
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
api = sly.Api.from_env()
labeling_actions = [
aa.ATTACH_TAG,
aa.UPDATE_TAG_VALUE,
aa.DETACH_TAG,
]
team_activity = api.team.get_activity(8, filter_actions=labeling_actions)
print(team_activity)
# Output: [
# {
# "userId":7,
# "action":"detach_tag",
# "date":"2021-01-15T15:11:55.985Z",
# "user":"cxnt",
# "projectId":1817,
# "project":"App_Test_Poly",
# "datasetId":2370,
# "dataset":"train",
# "imageId":726985,
# "image":"IMG_8144.jpeg",
# "classId":"None",
# "class":"None",
# "figureId":"None",
# "job":"None",
# "jobId":"None",
# "tag":"hhhlk",
# "tagId":4720,
# "meta":{}
# },
# {
# "userId":7,
# "action":"attach_tag",
# "date":"2021-01-15T14:24:58.480Z",
# "user":"cxnt",
# "projectId":1817,
# "project":"App_Test_Poly",
# "datasetId":2370,
# "dataset":"train",
# "imageId":726985,
# "image":"IMG_8144.jpeg",
# "classId":"None",
# "class":"None",
# "figureId":"None",
# "job":"None",
# "jobId":"None",
# "tag":"hhhlk",
# "tagId":4720,
# "meta":{}
# }
# ]
"""
from datetime import datetime
filters = []
if labeler_id is not None:
filters.append(
{"field": ActivityAction.LABELER_ID, "operator": "=", "value": labeler_id}
)
if review_status is not None:
filters.append(
{
"field": ActivityAction.REVIEW_STATUS,
"operator": "=",
"value": review_status,
}
)
if filter_user_id is not None:
filters.append({"field": ApiField.USER_ID, "operator": "=", "value": filter_user_id})
if filter_project_id is not None:
filters.append(
{
"field": ApiField.PROJECT_ID,
"operator": "=",
"value": filter_project_id,
}
)
if filter_job_id is not None:
filters.append({"field": ApiField.JOB_ID, "operator": "=", "value": filter_job_id})
if filter_actions is not None:
if type(filter_actions) is not list:
raise TypeError(
"type(filter_actions) is {!r}. But has to be of type {!r}".format(
type(filter_actions), list
)
)
filters.append({"field": ApiField.TYPE, "operator": "in", "value": filter_actions})
def _add_dt_filter(filters, dt, op):
dt_iso = None
if dt is None:
return
if type(dt) is str:
dt_iso = dt
elif type(dt) is datetime:
dt_iso = dt.isoformat()
else:
raise TypeError(
"DT type must be string in ISO8601 format or datetime, not {}".format(type(dt))
)
filters.append({"field": ApiField.DATE, "operator": op, "value": dt_iso})
_add_dt_filter(filters, start_date, ">=")
_add_dt_filter(filters, end_date, "<=")
method = "teams.activity"
data = {ApiField.TEAM_ID: team_id, ApiField.FILTER: filters}
first_response = self._api.post(method, data)
first_response = first_response.json()
total = first_response["total"]
per_page = first_response["perPage"]
pages_count = first_response["pagesCount"]
results = first_response["entities"]
def set_tqdm(progress_cb, results, total):
progress_cb.total = total
progress_cb.update(len(results) - progress_cb.n)
progress_cb.refresh()
if progress_cb is not None:
if isinstance(progress_cb, tqdm):
set_tqdm(progress_cb, results, total)
else:
progress_cb(len(results), total)
if pages_count == 1 and len(first_response["entities"]) == total:
pass
else:
for page_idx in range(2, pages_count + 1):
temp_resp = self._api.post(method, {**data, "page": page_idx, "per_page": per_page})
temp_items = temp_resp.json()["entities"]
results.extend(temp_items)
if progress_cb is not None:
if isinstance(progress_cb, tqdm):
set_tqdm(progress_cb, results, total)
else:
progress_cb(len(results), total)
if len(results) != total:
logger.warn(
f"Method '{method}': new events were created during pagination, "
f"downloaded={len(results)}, total={total}"
)
return results
def _convert_json_info(self, info: dict, skip_missing=True) -> TeamInfo:
""" """
res = super()._convert_json_info(info, skip_missing=skip_missing)
res_dict = res._asdict()
if isinstance(res_dict.get("usage"), dict):
res_dict["usage"] = UsageInfo(**res_dict["usage"])
return TeamInfo(**res_dict)