Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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.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