Skip to content
Merged
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
34 changes: 34 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@ History

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

4.6.2 - Oct 12, 2026
____________________


**Added**

- ``SAClient.generate_team_api_key()`` Generates a new API key for the specified team.

- ``SAClient.rotate_team_api_key()`` Generates a new active Team API Key while keeping the previous key valid for a configurable overlap period, enabling key rotation without downtime.

- ``SAClient.list_team_api_keys()`` Returns Team API Keys for the specified team.

- ``SAClient.revoke_team_api_key()`` Revokes a Team API Key that is in the Active or Rotating state.

- ``SAClient.explore`` Added a new namespace for working with Explore-related SDK functions.

- ``SAClient.explore.query()`` Returns items that satisfy a query string as a `QueryResult`, which behaves like a list of dictionaries and provides a `count()` method.

- ``SAClient.explore.query().count()`` Returns the subsets available in a project.

- ``SAClient.explore.get_subsets()`` Returns the subsets available in a project.

- ``SAClient.explore.add_items_to_subset()`` Associates items with a subset and creates the subset if it does not already exist.

**Deprecated**

- ``SAClient.query()`` and ``SAClient.query().count()`` Use ``SAClient.explore.query()``. Will be removed in 4.7.0.

- ``SAClient.get_subsets()`` Use ``SAClient.explore.get_subsets()``. Will be removed in 4.7.0.

- ``SAClient.add_items_to_subset()`` Use ``SAClient.explore.add_items_to_subset()``. Will be removed in 4.7.0.



4.6.1 - Sep 13, 2026
____________________

Expand Down
1 change: 1 addition & 0 deletions docs/source/api_reference/api_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Contents
api_annotation_class
api_export
api_custom_metadata
api_explore
api_subset
api_image
api_team
11 changes: 11 additions & 0 deletions docs/source/api_reference/api_explore.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
=======
Explore
=======

``SAClient.explore`` groups the functions that operate on the Explore surface of the
platform. Every query in this namespace uses the SuperAnnotate query language
documented at https://doc.superannotate.com/docs/queries-beta.

.. automethod:: lib.app.interface.sdk.explore.Explore.query
.. automethod:: lib.app.interface.sdk.explore.Explore.get_subsets
.. automethod:: lib.app.interface.sdk.explore.Explore.add_items_to_subset
4 changes: 4 additions & 0 deletions docs/source/api_reference/api_team.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ Team
.. automethod:: superannotate.SAClient.remove_contributors_categories
.. automethod:: superannotate.SAClient.grant_team_user_permissions
.. automethod:: superannotate.SAClient.revoke_team_user_permissions
.. automethod:: superannotate.SAClient.generate_team_api_key
.. automethod:: superannotate.SAClient.rotate_team_api_key
.. automethod:: superannotate.SAClient.list_team_api_keys
.. automethod:: superannotate.SAClient.revoke_team_api_key
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 8 --dist loadscope
addopts = -n 8 --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.6.1"
__version__ = "4.6.2dev1"


os.environ.update({"sa_version": __version__})
Expand Down
39 changes: 39 additions & 0 deletions src/superannotate/lib/app/helpers.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,55 @@
from __future__ import annotations

import os
import re
import sys
import uuid
import warnings
from pathlib import Path

import boto3
import numpy as np
import pandas as pd
import pydantic
from lib.core import ATTACHED_VIDEO_ANNOTATION_POSTFIX
from lib.core import VECTOR_ANNOTATION_POSTFIX
from lib.core.exceptions import AppException

# Frames in these directories are the SDK's own wrappers (Tracker, argument
# validation) - a deprecation warning is attributed to the first frame outside them.
_WRAPPER_DIRS = (
str(Path(__file__).resolve().parents[2]), # the superannotate package
str(Path(pydantic.__file__).resolve().parent),
)


# Python shows a DeprecationWarning by default only when it is attributed to
# __main__, so a deprecated SDK method called from any other module (a package, a
# notebook helper, a test) would be silent. Show the SDK's own deprecations wherever
# they are called from. Skipped when the user configured warnings themselves (-W or
# PYTHONWARNINGS); filters they add in code later still take precedence.
DEPRECATION_MESSAGE_PATTERN = r"This function .+ will be deprecated"
if not sys.warnoptions:
warnings.filterwarnings(
"default", message=DEPRECATION_MESSAGE_PATTERN, category=DeprecationWarning
)


def warn_deprecated(message: str):
"""Emit a DeprecationWarning attributed to the user's call.

The message must match DEPRECATION_MESSAGE_PATTERN to be shown by default. The
stacklevel is computed rather than fixed: a fixed one would land inside the
wrappers every public method goes through (Tracker, argument validation).
"""
assert re.match(DEPRECATION_MESSAGE_PATTERN, message), message
stacklevel = 1
frame = sys._getframe(0)
while frame is not None and frame.f_code.co_filename.startswith(_WRAPPER_DIRS):
frame = frame.f_back
stacklevel += 1
warnings.warn(message, DeprecationWarning, stacklevel=stacklevel)


def get_annotation_paths(folder_path, s3_bucket=None, recursive=False):
annotation_paths = []
Expand Down
16 changes: 13 additions & 3 deletions src/superannotate/lib/app/interface/base_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,11 @@ def get_default_payload(team_name, user_email, auth_type) -> dict:
"Env": os.environ.get("SA_ENV", "N/A"),
}

def __init__(self, function):
def __init__(self, function, event_name: str | None = None):
self.function = function
# Reported instead of the function's own name, e.g. for a helper that stands
# in for a method the user calls on a returned object (QueryResult.count).
self.event_name = event_name
functools.update_wrapper(self, function)

@staticmethod
Expand All @@ -253,7 +256,7 @@ def extract_arguments(function, *args, **kwargs) -> dict:
def default_parser(function_name: str, kwargs: dict) -> tuple:
properties = {}
for key, value in kwargs.items():
if key == "self":
if key == "self" or key.startswith("_"):
continue
if key == "token":
properties["sa_token"] = str(bool(value))
Expand Down Expand Up @@ -318,7 +321,14 @@ def _track_method(
try:
function_name = self.function.__name__ if self.function else ""
arguments = self.extract_arguments(self.function, *args, **kwargs)
event_name, properties = self.default_parser(function_name, arguments)
event_name, properties = self.default_parser(
self.event_name or function_name, arguments
)
# A namespace (e.g. SAClient.explore) reports its methods as
# "<namespace>.<method>", so they don't collide with SAClient's own.
tracking_prefix = getattr(instance, "TRACKING_PREFIX", None)
if tracking_prefix:
event_name = f"{tracking_prefix}.{event_name}"

# instance is args[0] - the actual object the call was made on, captured
# locally per call (see __call__), not shared/mutable state. It has no
Expand Down
12 changes: 12 additions & 0 deletions src/superannotate/lib/app/interface/sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class BaseNamespace:
"""A group of SAClient methods exposed as an attribute, e.g. SAClient.explore.

__init__ lives here rather than on the namespace, so TrackableMeta (applied to the
namespace) does not report building one as a telemetry event.
"""

#: Mixpanel events of the namespace's methods are reported as "<prefix>.<method>".
TRACKING_PREFIX: str

def __init__(self, controller):
self.controller = controller
Loading
Loading