Skip to content

Commit 24ac1f1

Browse files
authored
Merge pull request #818 from superannotateai/develop
Develop
2 parents b93fb96 + dd470d1 commit 24ac1f1

File tree

16 files changed

+238
-50
lines changed

16 files changed

+238
-50
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repos:
1414
- id: black
1515
name: Code Formatter (black)
1616
- repo: 'https://github.com/PyCQA/flake8'
17-
rev: 3.8.2
17+
rev: 6.1.0
1818
hooks:
1919
- id: flake8
2020
exclude: src/lib/app/analytics | src/lib/app/converters | src/lib/app/input_converters

CHANGELOG.rst

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

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

9+
4.4.39 - November 13, 2025
10+
________________________
11+
12+
**Updated**
13+
14+
- ``SAClient.get_item_by_id`` now supports an optional include parameter to fetch additional fields like custom_metadata and categories.
15+
16+
**Updated**
17+
18+
- ``SAClient.assign_items`` now supports assigning items and folders to pending users.
19+
- ``SAClient.assign_folder`` now supports assigning items and folders to pending users.
20+
921
4.4.38 - August 20, 2025
1022
________________________
1123

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ pydantic>=1.10,!=2.0.*
22
aiohttp~=3.8
33
boto3~=1.26
44
opencv-python-headless~=4.7
5-
packaging~=24.0
65
plotly~=5.14
76
pandas~=2.0
87
pillow>=9.5,~=10.0

requirements_extra.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
setuptools~=67.7
22
wheel~=0.40
3-
Sphinx==6.2.1
4-
tox==4.5.1
3+
Sphinx~=6.2.1
4+
tox~=4.0
55
sphinx_rtd_theme==1.2.0
66
furo==2023.3.27
77
jaraco.tidelift==1.5.1
88
sphinx-notfound-page==0.8.3
99
sphinx_inline_tabs==2023.4.21
10-
pytest==7.3.1
10+
pytest~=8.0
1111
pytest-xdist==3.2.1
1212
pytest-parallel==0.1.1
1313
pytest-cov==4.0.0

src/superannotate/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
import sys
44

55

6-
__version__ = "4.4.38"
6+
__version__ = "4.4.39"
77

88

99
os.environ.update({"sa_version": __version__})
1010
sys.path.append(os.path.split(os.path.realpath(__file__))[0])
1111

1212
import requests
1313
from lib.core import enums
14-
from packaging.version import parse
14+
from lib.core.utils import parse_version
1515
from lib.core import PACKAGE_VERSION_UPGRADE
1616
from lib.core import PACKAGE_VERSION_INFO_MESSAGE
1717
from lib.core import PACKAGE_VERSION_MAJOR_UPGRADE
@@ -47,15 +47,15 @@
4747

4848
def log_version_info():
4949
logging.StreamHandler(sys.stdout)
50-
local_version = parse(__version__)
50+
local_version = parse_version(__version__)
5151
if local_version.is_prerelease:
5252
logging.info(PACKAGE_VERSION_INFO_MESSAGE.format(__version__))
5353
req = requests.get("https://pypi.org/pypi/superannotate/json")
5454
if req.ok:
5555
releases = req.json().get("releases", [])
56-
pip_version = parse("0")
56+
pip_version = parse_version("0")
5757
for release in releases:
58-
ver = parse(release)
58+
ver = parse_version(release)
5959
if not ver.is_prerelease or local_version.is_prerelease:
6060
pip_version = max(pip_version, ver)
6161
if pip_version.major > local_version.major:

src/superannotate/lib/app/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,5 +123,5 @@ def wrap_error(errors_list: List[Tuple[str, str]]) -> str:
123123
_tabulation = tabulation - len(key)
124124
if not key:
125125
key, value, _tabulation = value, "", 0
126-
msgs.append(f'{key}{ " " * _tabulation}{value}')
126+
msgs.append(f'{key}{" " * _tabulation}{value}')
127127
return "\n".join(msgs)

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

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,12 @@ def get_folder_by_id(self, project_id: int, folder_id: int):
335335
exclude={"completedCount", "is_root"}
336336
)
337337

338-
def get_item_by_id(self, project_id: int, item_id: int):
338+
def get_item_by_id(
339+
self,
340+
project_id: int,
341+
item_id: int,
342+
include: List[Literal["custom_metadata", "categories"]] = None,
343+
):
339344
"""Returns the item metadata
340345
341346
:param project_id: the id of the project
@@ -344,16 +349,50 @@ def get_item_by_id(self, project_id: int, item_id: int):
344349
:param item_id: the id of the item
345350
:type item_id: int
346351
352+
:param include: Specifies additional fields to include in the response.
353+
354+
Possible values are
355+
356+
- "custom_metadata": Includes custom metadata attached to the item.
357+
- "categories": Includes categories attached to the item.
358+
:type include: list of str, optional
359+
347360
:return: item metadata
348361
:rtype: dict
349362
"""
350363
project_response = self.controller.get_project_by_id(project_id=project_id)
351364
project_response.raise_for_status()
352-
item = self.controller.get_item_by_id(
353-
item_id=item_id, project=project_response.data
365+
366+
if (
367+
include
368+
and "categories" in include
369+
and project_response.data.type != ProjectType.MULTIMODAL.value
370+
):
371+
raise AppException(
372+
"The 'categories' option in the 'include' field is only supported for Multimodal projects."
373+
)
374+
375+
# always join assignments for all project types
376+
_include = {"assignments"}
377+
if include:
378+
_include.update(set(include))
379+
include = list(_include)
380+
381+
include_custom_metadata = "custom_metadata" in include
382+
if include_custom_metadata:
383+
include.remove("custom_metadata")
384+
385+
item = self.controller.items.get_item_by_id(
386+
item_id=item_id, project=project_response.data, include=include
354387
)
355388

356-
return BaseSerializer(item).serialize(exclude={"url", "meta"})
389+
if include_custom_metadata:
390+
item_custom_fields = self.controller.custom_fields.list_fields(
391+
project=project_response.data, item_ids=[item.id]
392+
)
393+
item.custom_metadata = item_custom_fields[item.id]
394+
395+
return BaseSerializer(item).serialize(exclude={"url", "meta"}, by_alias=False)
357396

358397
def get_team_metadata(self, include: List[Literal["scores"]] = None):
359398
"""
@@ -2102,15 +2141,10 @@ def assign_folder(
21022141
if response.errors:
21032142
raise AppException(response.errors)
21042143
project = response.data
2105-
response = self.controller.projects.get_metadata(
2106-
project=project, include_contributors=True
2144+
project_contributors = self.controller.work_management.list_users(
2145+
project=project
21072146
)
2108-
2109-
if response.errors:
2110-
raise AppException(response.errors)
2111-
2112-
contributors = response.data.users
2113-
verified_users = [i.user_id for i in contributors]
2147+
verified_users = [i.email for i in project_contributors]
21142148
verified_users = set(users).intersection(set(verified_users))
21152149
unverified_contributor = set(users) - verified_users
21162150

@@ -4161,7 +4195,7 @@ def list_projects(
41614195
41624196
:param filters: Specifies filtering criteria, with all conditions combined using logical AND.
41634197
4164-
- Only users matching all filter conditions are returned.
4198+
- Only projects matching all filter conditions are returned.
41654199
41664200
- If no filter operation is provided, an exact match is applied.
41674201
@@ -4195,7 +4229,7 @@ def list_projects(
41954229
Custom Fields Filtering:
41964230
41974231
- Custom fields must be prefixed with `custom_field__`.
4198-
- Example: custom_field__Due_date__gte="1738281600" (filtering users whose Due_date is after the given Unix timestamp).
4232+
- Example: custom_field__Due_date__gte="1738281600" (filtering projects whose Due_date is after the given Unix timestamp).
41994233
- If include does not include “custom_fields” but filter contains ‘custom_field’, an error will be returned
42004234
42014235
- **Text** custom field only works with the following filter params: __in, __notin, __contains
@@ -5237,7 +5271,7 @@ def item_context(
52375271
"This function is only supported for Multimodal projects."
52385272
)
52395273
if isinstance(item, int):
5240-
_item = self.controller.get_item_by_id(item_id=item, project=project)
5274+
_item = self.controller.items.get_item_by_id(item_id=item, project=project)
52415275
else:
52425276
items = self.controller.items.list_items(project, folder, name=item)
52435277
if not items:

src/superannotate/lib/core/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import os
3+
import sys
34
from logging import Formatter
45
from logging.handlers import RotatingFileHandler
56
from os.path import expanduser
@@ -36,10 +37,21 @@ def setup_logging(level=DEFAULT_LOGGING_LEVEL, file_path=LOG_FILE_LOCATION):
3637
logger.removeHandler(handler)
3738
logger.propagate = False
3839
logger.setLevel(level)
39-
stream_handler = logging.StreamHandler()
40+
41+
# Separate handlers for different log levels
42+
stdout_handler = logging.StreamHandler(sys.stdout)
43+
stdout_handler.setLevel(logging.DEBUG)
44+
stdout_handler.addFilter(lambda record: record.levelno < logging.WARNING)
45+
46+
stderr_handler = logging.StreamHandler(sys.stderr)
47+
stderr_handler.setLevel(logging.WARNING)
48+
4049
formatter = Formatter("SA-PYTHON-SDK - %(levelname)s - %(message)s")
41-
stream_handler.setFormatter(formatter)
42-
logger.addHandler(stream_handler)
50+
stdout_handler.setFormatter(formatter)
51+
stderr_handler.setFormatter(formatter)
52+
53+
logger.addHandler(stdout_handler)
54+
logger.addHandler(stderr_handler)
4355
try:
4456
os.makedirs(file_path, exist_ok=True)
4557
log_file_path = os.path.join(file_path, "sa.log")

src/superannotate/lib/core/pydantic_v1.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from packaging.version import parse as parse_version
1+
from lib.core.utils import parse_version
22
from pydantic import VERSION
33

4+
45
if parse_version(VERSION).major < 2:
56
import pydantic
67
else:

src/superannotate/lib/core/utils.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import re
23
import typing
34
from threading import Thread
45

@@ -46,3 +47,58 @@ def wrapper(func: typing.Callable):
4647
thread.start()
4748
thread.join()
4849
return response[0]
50+
51+
52+
def parse_version(version_string):
53+
"""Smart version parsing with support for various formats"""
54+
# Remove 'v' prefix if present
55+
version_string = version_string.lstrip("v")
56+
57+
# Extract version parts using regex
58+
match = re.match(
59+
r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[-.]?(a|b|rc|dev|alpha|beta)(\d*))?",
60+
version_string,
61+
)
62+
63+
if not match:
64+
raise ValueError(f"Invalid version format: {version_string}")
65+
66+
major = int(match.group(1))
67+
minor = int(match.group(2) or 0)
68+
patch = int(match.group(3) or 0)
69+
pre_type = match.group(4)
70+
pre_num = int(match.group(5) or 0) if match.group(5) else 0
71+
72+
class Version:
73+
def __init__(self, major, minor, patch, pre_type=None, pre_num=0):
74+
self.major = major
75+
self.minor = minor
76+
self.patch = patch
77+
self.pre_type = pre_type
78+
self.pre_num = pre_num
79+
80+
@property
81+
def is_prerelease(self):
82+
return self.pre_type is not None
83+
84+
def __str__(self):
85+
version = f"{self.major}.{self.minor}.{self.patch}"
86+
if self.pre_type:
87+
version += f"-{self.pre_type}{self.pre_num}"
88+
return version
89+
90+
def __gt__(self, other):
91+
if self.major != other.major:
92+
return self.major > other.major
93+
if self.minor != other.minor:
94+
return self.minor > other.minor
95+
if self.patch != other.patch:
96+
return self.patch > other.patch
97+
# Handle prerelease comparison
98+
if self.is_prerelease and not other.is_prerelease:
99+
return False
100+
if not self.is_prerelease and other.is_prerelease:
101+
return True
102+
return self.pre_num > other.pre_num
103+
104+
return Version(major, minor, patch, pre_type, pre_num)

0 commit comments

Comments
 (0)