Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d28b85c
Add ability to query.count
VaghinakDev Apr 29, 2026
2fb9efe
feat(query): return lazy QueryResult with .count() from SAClient.query()
VaghinakDev Apr 29, 2026
c191ee9
Update docsstring
VaghinakDev Apr 29, 2026
39b5cb4
remove dead code
Apr 30, 2026
600c54c
Update query.count
VaghinakDev Apr 30, 2026
b472b6a
fix in ItemContext
May 5, 2026
086ac5d
Update query.count
VaghinakDev May 6, 2026
eaa59a5
fix in set_annoation_status
May 6, 2026
24f4b9f
Merge pull request #852 from superannotateai/FRIDAY-5167
VaghinakDev May 12, 2026
e56f736
Merge pull request #850 from superannotateai/FRIDAY-3798
VaghinakDev May 12, 2026
18722f7
Merge pull request #849 from superannotateai/dead_code
VaghinakDev May 13, 2026
617d432
Update function arguments to support project ID
VaghinakDev May 14, 2026
f29798e
fix in ItemContext
May 14, 2026
5531fce
Merge pull request #854 from superannotateai/FRIDAY-3798
VaghinakDev May 14, 2026
1ed4435
Fix upload_images_to_project log msg
VaghinakDev May 14, 2026
254f2b7
Merge pull request #853 from superannotateai/FRIDAY-4961
VaghinakDev May 15, 2026
1adb70c
Add setting MaxIdleDuration
VaghinakDev May 18, 2026
e248ee5
Merge pull request #855 from superannotateai/FRIDAY-5185
VaghinakDev May 18, 2026
aab5e93
add project_user permissions grant revoke
May 19, 2026
c0ceefa
feat(sdk): accept project/folder IDs across SDK methods
VaghinakDev May 19, 2026
f48347c
Merge pull request #857 from superannotateai/FRIDAY-5185
VaghinakDev May 19, 2026
79439cb
add project_user_permission in CachedWorkManagementRepository
May 19, 2026
eaaee46
Update delete project
VaghinakDev May 20, 2026
1b4eda1
Update delete project
VaghinakDev May 20, 2026
cbd1db9
Update delete project
VaghinakDev May 20, 2026
55578f0
Update delete project
VaghinakDev May 20, 2026
1505cd1
Fix tests base class
VaghinakDev May 20, 2026
869c78c
Merge pull request #856 from superannotateai/FRIDAY-5212
VaghinakDev May 21, 2026
5682ecf
Update docstrings
VaghinakDev May 21, 2026
0415d1e
change in list_user pemissions
May 21, 2026
c82db91
Merge branch 'develop' into FRIDAY-5212
May 21, 2026
7505618
Merge pull request #858 from superannotateai/FRIDAY-5212
VaghinakDev May 21, 2026
230571b
Fix upload_images_to_project log msg
VaghinakDev May 25, 2026
462cd90
Fix get project method
VaghinakDev May 25, 2026
c40bb4e
Fix get project method
VaghinakDev May 25, 2026
3885e82
Fix get project method
VaghinakDev May 25, 2026
9633d02
Update changelog
VaghinakDev May 26, 2026
ae0896b
Merge branch 'master' into develop
VaghinakDev May 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repos:
name: Style Guide Enforcement (flake8)
args:
- '--max-line-length=120'
- --ignore=D100,D203,D405,W503,E203,E501,F841,E126,E712,E123,E131,F821,E121,W605,E402
- --ignore=D100,D203,D405,W503,E203,E501,F841,E126,E712,E123,E131,F821,E121,W605,E402,E704
- repo: 'https://github.com/asottile/pyupgrade'
rev: v3.21.2
hooks:
Expand Down Expand Up @@ -62,7 +62,9 @@ repos:
# args:
# - '--disable=R0903,C0111,C0301,W0703,R0914,R0801,R0913,E0401,W0511,C0413,R0902,C0103,W0201,C0209,W1203,W0707,C0415,W0611'
# - repo: 'https://github.com/asottile/dead'
# rev: v1.3.0
# rev: v2.1.0
# hooks:
# - id: dead
# args: [--exclude, docs/source/conf.py|src/superannotate/lib/app/interface/sdk_interface.py|src/superannotate/lib/app/interface/cli_interface.py]

exclude: src/lib/app/analytics | src/lib/app/input_converters
15 changes: 15 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ History

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

4.5.5 - May 31, 2026
______________________

**Added**

- ``SAClient.query.count()`` Returns the number of items that match a query.
- ``SAClient.grant_project_user_permissions()`` Grants project-level user permissions.
- ``SAClient.revoke_project_user_permissions()`` Revokes project-level user permissions.

**Updated**

- ``SAClient.list_users()`` Now returns user permissions.
- ``SAClient.create_project()`` Now supports the `MaxIdleDuration` setting.


4.5.4 - April 23, 2026
______________________

Expand Down
2 changes: 2 additions & 0 deletions docs/source/api_reference/api_project.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ Projects
.. automethod:: superannotate.SAClient.list_categories
.. automethod:: superannotate.SAClient.remove_categories
.. automethod:: superannotate.SAClient.remove_users_from_project
.. automethod:: superannotate.SAClient.grant_project_user_permissions
.. automethod:: superannotate.SAClient.revoke_project_user_permissions
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# -- Project information -----------------------------------------------------

project = "SuperAnnotate Python SDK"
copyright = "2021, SuperAnnotate AI"
copyright = "2026, SuperAnnotate AI"
author = "SuperAnnotate AI"

# The full version, including alpha/beta/rc tags
Expand Down
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ minversion = 3.7
log_cli=true
python_files = test_*.py
;pytest_plugins = ['pytest_profiling']
;addopts = -n 6 --dist loadscope
addopts = -n 6 --dist loadscope
2 changes: 1 addition & 1 deletion src/superannotate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import sys

__version__ = "4.5.4"
__version__ = "4.5.5"


os.environ.update({"sa_version": __version__})
Expand Down
8 changes: 0 additions & 8 deletions src/superannotate/lib/app/analytics/aggregators.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,7 @@ def aggregate_image_annotations_as_df(self, annotations_paths: list[str]):
annotation_json = None
with open(annotation_path) as fp:
annotation_json = json.load(fp)
parts = Path(annotation_path).name.split(self._annotation_suffix)
row_data = self.__fill_image_metadata(row_data, annotation_json["metadata"])
annotation_instance_id = 0

# include comments
for annotation in annotation_json["comments"]:
Expand Down Expand Up @@ -433,10 +431,8 @@ def aggregate_image_annotations_as_df(self, annotations_paths: list[str]):
if Path(annotation_path).parent != Path(self.project_root):
folder_name = Path(annotation_path).parent.name
instance_row.folderName = folder_name
num_added = 0
if not attributes:
rows.append(instance_row)
num_added = 1
else:
for attribute in attributes:
attribute_row = copy.copy(instance_row)
Expand Down Expand Up @@ -469,10 +465,6 @@ def aggregate_image_annotations_as_df(self, annotations_paths: list[str]):
attribute_row.attributeName = attribute_name

rows.append(attribute_row)
num_added += 1

if num_added > 0:
annotation_instance_id += 1

df = pd.DataFrame([row.__dict__ for row in rows], dtype=object)
df = df.astype({"probability": float})
Expand Down
48 changes: 0 additions & 48 deletions src/superannotate/lib/app/analytics/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from pathlib import Path

import pandas as pd
import plotly.express as px
from lib.core.exceptions import AppException

logger = logging.getLogger("sa")
Expand Down Expand Up @@ -558,50 +557,3 @@ def consensus(df, item_name, annot_type):
instance_id += 1

return image_data


def consensus_plot(consensus_df, *_, **__):
plot_data = consensus_df.copy()

# annotator-wise boxplot
annot_box_fig = px.box(
plot_data,
x="creatorEmail",
y="score",
points="all",
color="creatorEmail",
color_discrete_sequence=px.colors.qualitative.Dark24,
)
annot_box_fig.show()

# project-wise boxplot
project_box_fig = px.box(
plot_data,
x="folderName",
y="score",
points="all",
color="folderName",
color_discrete_sequence=px.colors.qualitative.Dark24,
)
project_box_fig.show()

# scatter plot of score vs area
fig = px.scatter(
plot_data,
x="area",
y="score",
color="className",
symbol="creatorEmail",
facet_col="folderName",
color_discrete_sequence=px.colors.qualitative.Dark24,
hover_data={
"className": False,
"itemName": True,
"folderName": False,
"area": False,
"score": False,
},
)
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
fig.for_each_trace(lambda t: t.update(name=t.name.split("=")[-1]))
fig.show()
10 changes: 0 additions & 10 deletions src/superannotate/lib/app/input_converters/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,6 @@ def _passes_type_sanity(params_info):
)


def _passes_list_members_type_sanity(lists_info):
for _list in lists_info:
for _list_member in _list[0]:
if not isinstance(_list_member, _list[2]):
raise AppException(
"'%s' should be list of '%s', but contains '%s'"
% (_list[1], _list[2], type(_list_member))
)


def _passes_value_sanity(values_info):
for value in values_info:
if value[0] not in value[2]:
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ def to_sa_format(self):
== "supervisely_keypoint_detection_to_sa_vector"
):
meta_json = json.load(open(self.export_root / "meta.json"))
sa_jsons = self.conversion_algorithm(
self.conversion_algorithm(
json_files, classes_id_map, meta_json, self.output_dir
)
else:
sa_jsons = self.conversion_algorithm(
self.conversion_algorithm(
json_files, classes_id_map, self.task, self.output_dir
)
(self.output_dir / "classes").mkdir(exist_ok=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,6 @@ def _iou(bbox1, bbox2):
)


def _get_image_shape_from_xml(file_path):
with open(os.path.splitext(file_path)[0] + ".xml") as f:
tree = ET.parse(f)

size = tree.find("size")
width = int(size.find("width").text)
height = int(size.find("height").text)

return height, width


def _get_image_metadata(file_path):
with open(os.path.splitext(file_path)[0] + ".xml") as f:
tree = ET.parse(f)
Expand Down
8 changes: 0 additions & 8 deletions src/superannotate/lib/app/input_converters/sa_conversion.py

This file was deleted.

95 changes: 95 additions & 0 deletions src/superannotate/lib/app/interface/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from __future__ import annotations

from collections.abc import Callable
from collections.abc import Iterator
from typing import Generic
from typing import overload
from typing import TypeVar

T = TypeVar("T")


class BaseResult(list, Generic[T]):
"""A generic list-like wrapper for results with lazy loading support.

Inherits from ``list`` for full backward compatibility with code that
expects a real list (``isinstance(x, list)``, JSON serializers, etc.).
Data is fetched lazily on first access.
"""

def __init__(self, data_fetcher: Callable[[], list[T]]) -> None:
super().__init__()
self._data_fetcher = data_fetcher
self._loaded = False

def _ensure_data(self) -> None:
"""Lazily fetch data if not already loaded."""
if not self._loaded:
list.extend(self, self._data_fetcher())
self._loaded = True

def data(self) -> list[T]:
self._ensure_data()
return list(self)

def __iter__(self) -> Iterator[T]:
self._ensure_data()
return list.__iter__(self)

def __len__(self) -> int:
self._ensure_data()
return list.__len__(self)

@overload
def __getitem__(self, index: int) -> T: ...

@overload
def __getitem__(self, index: slice) -> list[T]: ...

def __getitem__(self, index: int | slice) -> T | list[T]:
self._ensure_data()
return list.__getitem__(self, index)

def __repr__(self) -> str:
self._ensure_data()
return list.__repr__(self)

def __bool__(self) -> bool:
self._ensure_data()
return list.__len__(self) > 0

def __contains__(self, item: object) -> bool:
self._ensure_data()
return list.__contains__(self, item)

def __eq__(self, other: object) -> bool:
self._ensure_data()
return list.__eq__(self, other)

__hash__ = None # type: ignore[assignment]


class QueryResult(BaseResult[dict]):
"""A list-like wrapper for query results that supports .count() method.

This class wraps a list of query results while maintaining full backward
compatibility with list-like operations (iteration, indexing, len()).
Data is fetched lazily - only when accessed. Calling .count() does not
trigger data fetching.
"""

def __init__(
self,
data_fetcher: Callable[[], list[dict]],
count_fetcher: Callable[[], int],
) -> None:
super().__init__(data_fetcher)
self._count_fetcher = count_fetcher

def count(self) -> int:
"""Return the count of items matching the query from the server.

This method does not trigger data fetching - it makes a separate
lightweight API call to get only the count.
"""
return self._count_fetcher()
Loading
Loading