Skip to content

Commit 15430ce

Browse files
authored
Merge pull request #825 from superannotateai/develop
Develop
2 parents 99a0491 + c3a8b29 commit 15430ce

23 files changed

Lines changed: 50338 additions & 71 deletions

CHANGELOG.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ History
66

77
All release highlights of this project will be documented in this file.
88

9+
10+
4.5.1 - February 5, 2026
11+
________________________
12+
13+
**Added**
14+
15+
- ``SAClient.get_folder_metadata`` Now returns a list of metadata of contributors assigned to the folder.
16+
17+
**Updated**
18+
19+
- SDK will now support Python versions 3.10+.
20+
21+
922
4.5.0 - December 4, 2025
1023
________________________
1124

README.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,10 @@ SuperAnnotate python SDK is available on PyPI:
105105
pip install superannotate
106106
107107
108-
The package officially supports Python 3.7+ and was tested under Linux and
108+
The package officially supports Python 3.10+ and was tested under Linux and
109109
Windows (`Anaconda <https://www.anaconda.com/products/individual#windows>`__
110110
) platforms.
111111

112-
For more detailed installation steps and package usage please have a look at the `tutorial <https://superannotate.readthedocs.io/en/stable/tutorial.sdk.html>`__
113-
114112

115113
Supported Features
116114
------------------

docs/source/userguide/quickstart.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ SDK is available on PyPI:
1515
1616
pip install superannotate
1717
18-
The package officially supports Python 3.7+ and was tested under Linux and
18+
The package officially supports Python 3.10+ and was tested under Linux and
1919
Windows (`Anaconda <https://www.anaconda.com/products/individual#windows>`_) platforms.
2020

2121
For certain video related functions to work, ffmpeg package needs to be installed.

setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,15 @@ def get_version():
5353
classifiers=[
5454
"Programming Language :: Python",
5555
"Programming Language :: Python :: 3",
56-
"Programming Language :: Python :: 3.7",
57-
"Programming Language :: Python :: 3.8",
58-
"Programming Language :: Python :: 3.9",
5956
"Programming Language :: Python :: 3.10",
6057
"Programming Language :: Python :: 3.11",
58+
"Programming Language :: Python :: 3.12",
59+
"Programming Language :: Python :: 3.13",
60+
"Programming Language :: Python :: 3.14",
6161
],
6262
project_urls={
6363
"Documentation": "https://superannotate.readthedocs.io/en/stable/",
6464
},
65-
python_requires=">=3.7",
65+
python_requires=">=3.10",
6666
include_package_data=True,
6767
)

src/superannotate/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44

55

6-
__version__ = "4.5.0"
6+
__version__ = "4.5.1"
77

88

99
os.environ.update({"sa_version": __version__})

src/superannotate/lib/app/interface/base_interface.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ def get_default_payload(team_name, user_email):
141141
def __init__(self, function):
142142
self.function = function
143143
self._client = None
144+
self.skip_flag = os.environ.get("SA_SKIP_METRICS", "False").lower() in (
145+
"true",
146+
"1",
147+
"t",
148+
)
144149
functools.update_wrapper(self, function)
145150

146151
def get_client(self):
@@ -190,8 +195,9 @@ def default_parser(function_name: str, kwargs: dict) -> tuple:
190195
return function_name, properties
191196

192197
def _track(self, user_id: str, event_name: str, data: dict):
193-
if "pytest" not in sys.modules:
194-
self.get_mp_instance().track(user_id, event_name, data)
198+
if "pytest" in sys.modules or self.skip_flag:
199+
return
200+
self.get_mp_instance().track(user_id, event_name, data)
195201

196202
def _track_method(self, args, kwargs, success: bool):
197203
try:

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
from lib.app.serializers import WMProjectSerializer
7474
from lib.core.entities.work_managament import WMUserTypeEnum
7575
from lib.core.jsx_conditions import EmptyQuery
76+
from lib.core.jsx_conditions import Join
77+
from lib.core.jsx_conditions import Fields
7678
from lib.core.entities.items import ProjectCategoryEntity
7779

7880
logger = logging.getLogger("sa")
@@ -511,7 +513,7 @@ def set_user_custom_field(
511513
def list_users(
512514
self,
513515
*,
514-
project: Union[int, str] = None,
516+
project: Union[NotEmptyStr, int] = None,
515517
include: List[Literal["custom_fields", "categories"]] = None,
516518
**filters,
517519
):
@@ -1088,7 +1090,12 @@ def search_team_contributors(
10881090
:return: metadata of found users
10891091
:rtype: list of dicts
10901092
"""
1091-
1093+
warnings.warn(
1094+
"This function search_team_contributors() will be deprecated and removed in version 4.6.0\n"
1095+
"Recommended replacement: get_user_metadata() or list_users()",
1096+
DeprecationWarning,
1097+
stacklevel=2,
1098+
)
10921099
contributors = self.controller.search_team_contributors(
10931100
email=email, first_name=first_name, last_name=last_name
10941101
).data
@@ -1124,6 +1131,13 @@ def search_projects(
11241131
:return: project names or metadatas
11251132
:rtype: list of strs or dicts
11261133
"""
1134+
warnings.warn(
1135+
"This function search_projects() will be deprecated and removed in version 4.6.0\n"
1136+
"Recommended replacement: get_project_metadata() or list_projects()",
1137+
DeprecationWarning,
1138+
stacklevel=2,
1139+
)
1140+
11271141
statuses = []
11281142
if status:
11291143
if isinstance(status, (list, tuple, set)):
@@ -1574,22 +1588,83 @@ def rename_project(self, project: NotEmptyStr, new_name: NotEmptyStr):
15741588
)
15751589
return ProjectSerializer(response.data).serialize()
15761590

1577-
def get_folder_metadata(self, project: NotEmptyStr, folder_name: NotEmptyStr):
1578-
"""Returns folder metadata
1591+
def get_folder_metadata(
1592+
self,
1593+
project: NotEmptyStr,
1594+
folder_name: NotEmptyStr,
1595+
include_contributors: bool = False,
1596+
):
1597+
"""
1598+
SAClient.get_folder_metadata(project, folder_name, include_contributors=False)
1599+
Returns folder metadata. Optionally includes a list of contributors that are currently
1600+
assigned to the folder.
15791601
15801602
:param project: project name
15811603
:type project: str
15821604
15831605
:param folder_name: folder's name
15841606
:type folder_name: str
15851607
1586-
:return: metadata of folder
1608+
:param include_contributors: If True, includes a list of contributors assigned to the folder in the response.
1609+
Defaults to False.
1610+
:type include_contributors: bool
1611+
1612+
:return: Folder metadata
15871613
:rtype: dict
1614+
1615+
Request Example:
1616+
::
1617+
1618+
sa_client.get_folder_metadata(
1619+
project="test_project",
1620+
folder_name="test_folder",
1621+
include_contributors=True
1622+
)
1623+
1624+
1625+
Response Example:
1626+
::
1627+
1628+
{
1629+
"createdAt": "2025-10-27T06:54:09.000Z",
1630+
"updatedAt": "2025-10-27T06:54:09.000Z",
1631+
"id": 1487195,
1632+
"name": "test_folder",
1633+
"status": "NotStarted",
1634+
"project_id": 1203397,
1635+
"team_id": 85922,
1636+
"contributors": [
1637+
{
1638+
"email": "test@superannotate.com",
1639+
"id": 1314658,
1640+
"role": "Annotator",
1641+
"state": "Confirmed"
1642+
}
1643+
]
1644+
}
1645+
15881646
"""
1589-
project, folder = self.controller.get_project_folder((project, folder_name))
1590-
if not folder:
1647+
project = self.controller.get_project(project)
1648+
query = Filter("name", folder_name, OperatorEnum.EQ)
1649+
fields = ["id", "project_id", "name", "status", "team_id"]
1650+
1651+
if include_contributors:
1652+
query &= Join("folderUsers", fields=["id"])
1653+
query &= Join(
1654+
"folderUsers.projectUser", fields=["id", "email", "role", "state"]
1655+
)
1656+
fields.append("folderUsers")
1657+
query &= Fields(fields)
1658+
response = self.controller.work_management.list_folders(
1659+
project=project, query=query
1660+
)
1661+
response.raise_for_status()
1662+
if not response.data:
15911663
raise AppException("Folder not found.")
1592-
return BaseSerializer(folder).serialize(exclude={"completedCount", "is_root"})
1664+
folder = response.data[0]
1665+
return BaseSerializer(folder).serialize(
1666+
exclude={"completedCount", "is_root"}, by_alias=False
1667+
)
15931668

15941669
def delete_folders(self, project: NotEmptyStr, folder_names: List[NotEmptyStr]):
15951670
"""Delete folder in project.
@@ -2148,7 +2223,7 @@ def assign_folder(
21482223
raise AppException(response.errors)
21492224
project = response.data
21502225
project_contributors = self.controller.work_management.list_users(
2151-
project=project
2226+
email__in=users, project=project
21522227
)
21532228
verified_users = [i.email for i in project_contributors]
21542229
verified_users = set(users).intersection(set(verified_users))
@@ -3949,6 +4024,12 @@ def search_items(
39494024
}
39504025
]
39514026
"""
4027+
warnings.warn(
4028+
"This function search_items() will be deprecated and removed in version 4.6.0\n"
4029+
"Recommended replacement: get_item_metadata() or list_items()",
4030+
DeprecationWarning,
4031+
stacklevel=2,
4032+
)
39524033
project, folder = self.controller.get_project_folder(project)
39534034
query_kwargs = {"include": ["assignments"]}
39544035
if name_contains:
@@ -4355,7 +4436,7 @@ def attach_items(
43554436
if annotation_status is not None:
43564437
warnings.warn(
43574438
DeprecationWarning(
4358-
"The “keep_status” parameter is deprecated. "
4439+
"The “keep_status” parameter is deprecated."
43594440
"Please use the “set_annotation_statuses” function instead."
43604441
)
43614442
)
@@ -4577,7 +4658,7 @@ def move_items(
45774658
def set_items_category(
45784659
self,
45794660
project: Union[NotEmptyStr, int, Tuple[int, int], Tuple[str, str]],
4580-
items: List[Union[int, str]],
4661+
items: List[Union[NotEmptyStr, int]],
45814662
category: NotEmptyStr,
45824663
):
45834664
"""
@@ -4615,7 +4696,7 @@ def set_items_category(
46154696
def remove_items_category(
46164697
self,
46174698
project: Union[NotEmptyStr, int, Tuple[int, int], Tuple[str, str]],
4618-
items: List[Union[int, str]],
4699+
items: List[Union[NotEmptyStr, int]],
46194700
):
46204701
"""
46214702
Remove categories from one or more items.
@@ -5349,7 +5430,7 @@ def remove_users(self, users: Union[List[int], List[str]]):
53495430
Request Example:
53505431
::
53515432
5352-
SAClient.remove_users(member=["example@gmail.com","example1@gmail.com"])
5433+
sa_client.remove_users(users=["example@gmail.com","example1@gmail.com"])
53535434
53545435
"""
53555436
success = 0
@@ -5369,7 +5450,7 @@ def remove_users_from_project(
53695450
self, project: Union[NotEmptyStr, int], users: Union[List[int], List[str]]
53705451
):
53715452
"""
5372-
Allows removing users from the team.
5453+
Allows removing users from a project.
53735454
53745455
:param project: The name or ID of the project.
53755456
:type project: Union[NotEmptyStr, int]
@@ -5382,7 +5463,7 @@ def remove_users_from_project(
53825463
Request Example:
53835464
::
53845465
5385-
SAClient.remove_users_from_project(project="Test Project", users=["example@gmail.com","example1@gmail.com"])
5466+
sa_client.remove_users_from_project(project="Test Project", users=["example@gmail.com","example1@gmail.com"])
53865467
53875468
"""
53885469
project = self.controller.get_project(project)

src/superannotate/lib/app/serializers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from abc import ABC
21
from enum import Enum
32
from typing import Any
43
from typing import List
@@ -10,7 +9,7 @@
109
from lib.core.pydantic_v1 import BaseModel
1110

1211

13-
class BaseSerializer(ABC):
12+
class BaseSerializer:
1413
def __init__(self, entity: BaseEntity):
1514
self._entity = entity
1615

src/superannotate/lib/core/entities/folder.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1+
from enum import Enum
12
from typing import List
23
from typing import Optional
34

5+
from lib.core.entities.base import BaseModel
46
from lib.core.entities.base import TimedBaseModel
57
from lib.core.enums import FolderStatus
8+
from lib.core.enums import WMUserStateEnum
69
from lib.core.pydantic_v1 import Extra
10+
from lib.core.pydantic_v1 import Field
11+
from lib.core.pydantic_v1 import root_validator
12+
13+
14+
class FolderUserEntity(BaseModel):
15+
email: Optional[str] = None
16+
id: Optional[int] = None
17+
role: Optional[int] = None
18+
state: Optional[WMUserStateEnum] = None
19+
20+
class Config:
21+
use_enum_names = True
22+
allow_population_by_field_name = True
23+
extra = Extra.ignore
24+
json_encoders = {Enum: lambda v: v.value}
725

826

927
class FolderEntity(TimedBaseModel):
@@ -13,8 +31,34 @@ class FolderEntity(TimedBaseModel):
1331
project_id: Optional[int]
1432
team_id: Optional[int]
1533
is_root: Optional[bool] = False
16-
folder_users: Optional[List[dict]]
34+
contributors: Optional[List[FolderUserEntity]] = Field(
35+
default_factory=list, alias="folderUsers"
36+
)
37+
1738
completedCount: Optional[int]
1839

40+
@root_validator(pre=True)
41+
def normalize_folder_users(cls, values: dict) -> dict:
42+
folder_users = values.get("folderUsers")
43+
if not folder_users:
44+
return values
45+
46+
normalized: List[dict] = []
47+
for fu in folder_users:
48+
pu = fu.get("projectUser") or {}
49+
50+
normalized.append(
51+
{
52+
"email": pu.get("email"),
53+
"id": pu.get("id"),
54+
"role": pu.get("role"),
55+
"state": pu.get("state"),
56+
}
57+
)
58+
59+
values["folderUsers"] = normalized
60+
return values
61+
1962
class Config:
2063
extra = Extra.ignore
64+
allow_population_by_field_name = True

0 commit comments

Comments
 (0)