Skip to content
Open
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
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 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.4dev1"
__version__ = "4.5.5dev2"


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