Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e833f55
Add test script for preannotation API functionality
yashsuman15 Nov 17, 2025
7227696
Add test script for preannotation API functionality
yashsuman15 Dec 1, 2025
677e2e2
minor changes
yashsuman15 Dec 2, 2025
a2cadb6
Add test script for preannotation API functionality
yashsuman15 Nov 17, 2025
9a7bf4d
Add test script for preannotation API functionality
yashsuman15 Dec 1, 2025
cffbb31
minor changes
yashsuman15 Dec 2, 2025
529edfa
Merge branch 'LABIMP-7672' of https://github.com/Labellerr/SDKPython …
yashsuman15 Dec 2, 2025
52f6b3c
modified video sampling scripts
yashsuman15 Dec 3, 2025
012762e
- added video_json support to constants
yashsuman15 Dec 8, 2025
33060af
Merge remote-tracking branch 'origin' into LABIMP-7672
yashsuman15 Dec 8, 2025
8f6e556
- Fixed the fiile naming convention for algo
yashsuman15 Dec 9, 2025
4e92d06
fixing git ci/cd failure
yashsuman15 Dec 9, 2025
2b1b7cb
[LABIMP-8422] List templates API integration (#34)
nupursharma-labellerr Dec 9, 2025
2e11a4b
[LABIMP-8411] Fetch files function to return a generator. LabellerrFi…
ximihoque Dec 9, 2025
58567b8
[LABIMP-8483] Updated the property to annotation_template_id (#37)
nupursharma-labellerr Dec 11, 2025
93a4579
Update claude-code-review.yml
ximihoque Dec 16, 2025
85041ad
[LABIMP-8446] Integrate list exports API in SDK (#40)
kunal351411 Dec 17, 2025
9abc091
[LABIMP-8446] Patched get_status: removing json.loads
ximihoque Dec 17, 2025
4958c4b
Sync selected files with origin/main
yashsuman15 Dec 22, 2025
eac98e4
merging conflit after fixing
yashsuman15 Dec 22, 2025
0636123
Revert "fixing git ci/cd failure"
yashsuman15 Dec 22, 2025
5ea0b90
merging origin
yashsuman15 Dec 22, 2025
9ed67cd
merging origin -2
yashsuman15 Dec 22, 2025
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
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[flake8]
max-line-length = 200
extend-ignore = E203, W503, E402, F405
exclude = .git,__pycache__,.venv,build,dist,venv,driver.py
exclude = .git,__pycache__,.venv,build,dist,venv,driver.py,.history
8 changes: 6 additions & 2 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ name: Claude Auto Review
on:
pull_request:
types: [opened, synchronize]
paths-ignore:
- "**/*.md"
- "docs/**"

jobs:
review:
if: github.event.pull_request.update_count < 3
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -31,7 +35,7 @@ jobs:
- Security concerns
- Test coverage
- Code repeatability
- Over engineering
- Over engineering

Note: The PR branch is already checked out in the current working directory.

Expand All @@ -40,4 +44,4 @@ jobs:
Only post GitHub comments - don't submit review text as messages.

claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ download
labellerr/__pycache__/
env.*
claude.md
.history/
69 changes: 66 additions & 3 deletions labellerr/core/annotation_templates/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from .base import LabellerrAnnotationTemplate
from ..schemas.annotation_templates import CreateTemplateParams, QuestionType, Option
import uuid
from typing import List

from .. import constants
from ..client import LabellerrClient
import uuid
from ..schemas.annotation_templates import (
CreateTemplateParams,
DatasetDataType,
Option,
QuestionType,
)
from .base import LabellerrAnnotationTemplate

__all__ = [
"LabellerrAnnotationTemplate",
Expand Down Expand Up @@ -62,3 +69,59 @@ def create_template(
client=client,
annotation_template_id=response.get("response", None).get("template_id"),
)


def list_templates(
client: LabellerrClient, data_type: DatasetDataType
) -> List[LabellerrAnnotationTemplate]:
"""
List all annotation templates for a given data type

:param client: The client to use for the request.
:param data_type: The data type to list templates for.
:return: A list of LabellerrAnnotationTemplate instances.
"""
unique_id = str(uuid.uuid4())
url = (
f"{constants.BASE_URL}/annotations/list_questions_templates?client_id={client.client_id}&data_type={data_type.value}"
f"&uuid={unique_id}"
)

response = client.make_request(
"GET",
url,
extra_headers={"content-type": "application/json"},
request_id=unique_id,
)
return [
LabellerrAnnotationTemplate.from_annotation_template_data(client, **item)
for item in response.get("response", [])
]


def list_templates(
client: LabellerrClient, data_type: DatasetDataType
) -> List[LabellerrAnnotationTemplate]:
"""
List all annotation templates for a given data type

:param client: The client to use for the request.
:param data_type: The data type to list templates for.
:return: A list of LabellerrAnnotationTemplate instances.
"""
unique_id = str(uuid.uuid4())
url = (
f"{constants.BASE_URL}/annotations/list_questions_templates?client_id={client.client_id}&data_type={data_type.value}"
f"&uuid={unique_id}"
)

response = client.make_request(
"GET",
url,
extra_headers={"content-type": "application/json"},
request_id=unique_id,
)
return [
LabellerrAnnotationTemplate.from_annotation_template_data(client, **item)
for item in response.get("response", [])
]
Comment on lines +74 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Duplicate Function Definition

The list_templates function is defined twice (lines 74-99 and 102-127) with identical implementations. This is a copy-paste error that will cause issues.

Impact:

  • The second definition overwrites the first
  • Code duplication makes maintenance harder
  • Could lead to confusion about which implementation is being used

Recommendation:

Suggested change
def list_templates(
client: LabellerrClient, data_type: DatasetDataType
) -> List[LabellerrAnnotationTemplate]:
"""
List all annotation templates for a given data type
:param client: The client to use for the request.
:param data_type: The data type to list templates for.
:return: A list of LabellerrAnnotationTemplate instances.
"""
unique_id = str(uuid.uuid4())
url = (
f"{constants.BASE_URL}/annotations/list_questions_templates?client_id={client.client_id}&data_type={data_type.value}"
f"&uuid={unique_id}"
)
response = client.make_request(
"GET",
url,
extra_headers={"content-type": "application/json"},
request_id=unique_id,
)
return [
LabellerrAnnotationTemplate.from_annotation_template_data(client, **item)
for item in response.get("response", [])
]
def list_templates(
client: LabellerrClient, data_type: DatasetDataType
) -> List[LabellerrAnnotationTemplate]:
"""
List all annotation templates for a given data type
:param client: The client to use for the request.
:param data_type: The data type to list templates for.
:return: A list of LabellerrAnnotationTemplate instances.
"""
unique_id = str(uuid.uuid4())
url = (
f"{constants.BASE_URL}/annotations/list_questions_templates?client_id={client.client_id}&data_type={data_type.value}"
f"&uuid={unique_id}"
)
response = client.make_request(
"GET",
url,
extra_headers={"content-type": "application/json"},
request_id=unique_id,
)
return [
LabellerrAnnotationTemplate.from_annotation_template_data(client, **item)
for item in response.get("response", [])
]
# Remove duplicate function - delete lines 102-127

Delete one of these duplicate definitions.

103 changes: 91 additions & 12 deletions labellerr/core/annotation_templates/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import uuid

from .. import constants
from ..client import LabellerrClient
from ..exceptions import InvalidAnnotationTemplateError
import uuid


class LabellerrAnnotationTemplate:
Expand All @@ -24,8 +25,18 @@ def get_annotation_template(client: "LabellerrClient", annotation_template_id: s

"""Base class for all Labellerr projects with factory behavior"""

def __new__(cls, client: "LabellerrClient", annotation_template_id: str):
# Validate that the annotation template exists before creating the instance
def __new__(
cls,
client: "LabellerrClient",
annotation_template_id: str,
_skip_api_fetch: bool = False,
**kwargs,
):
# If skip flag is set, create instance without API call
if _skip_api_fetch:
return super().__new__(cls)

# Otherwise, fetch from API and validate
annotation_template_data = cls.get_annotation_template(
client, annotation_template_id
)
Expand All @@ -37,14 +48,82 @@ def __new__(cls, client: "LabellerrClient", annotation_template_id: str):
f"Annotation template with ID '{annotation_template_id}' does not exist or could not be retrieved."
)

# Create the instance only if validation passes
instance = super().__new__(cls)
# Store the data on the instance to avoid calling API again in __init__
instance.__annotation_template_data = annotation_template_data
return instance
# Pass fetched data to __init__ via kwargs
kwargs["_fetched_data"] = annotation_template_data
return super().__new__(cls)

def __init__(self, client: "LabellerrClient", annotation_template_id: str):
def __init__(
self,
client: "LabellerrClient",
annotation_template_id: str,
_skip_api_fetch: bool = False,
**kwargs,
):
self.client = client
self.annotation_template_id = annotation_template_id
# Use the data already fetched in __new__
self.annotation_template_data = self.__annotation_template_data
self.__annotation_template_id = annotation_template_id

# Set __annotation_template_data from either source
if "_cached_data" in kwargs:
# Data provided directly (from factory method)
self.__annotation_template_data = kwargs["_cached_data"]
elif "_fetched_data" in kwargs:
# Data fetched in __new__
self.__annotation_template_data = kwargs["_fetched_data"]
else:
# Fallback - shouldn't happen in normal usage
self.__annotation_template_data = {}

@classmethod
def from_annotation_template_data(cls, client: "LabellerrClient", **kwargs):
"""
Create a LabellerrAnnotationTemplate instance from annotation template data.

:param client: LabellerrClient instance
:param kwargs: Annotation template fields (template_id, template_name, questions, etc.)
:return: Instance of LabellerrAnnotationTemplate
"""
# Validate required fields
required_fields = {
"template_id",
"template_name",
"questions",
"created_at",
"created_by",
}
missing_fields = required_fields - set(kwargs.keys())
if missing_fields:
raise ValueError(
f"Missing required fields in annotation_template_data: {missing_fields}"
)

# Create instance without API call - explicit flag makes intent clear
return cls(
client,
annotation_template_id=kwargs.get("template_id"),
_skip_api_fetch=True,
_cached_data=kwargs,
)

@property
def template_name(self):
return self.__annotation_template_data.get("template_name")

@property
def data_type(self):
return self.__annotation_template_data.get("data_type")

@property
def annotation_template_id(self):
return self.__annotation_template_id

@property
def created_at(self):
return self.__annotation_template_data.get("created_at")

@property
def created_by(self):
return self.__annotation_template_data.get("created_by")

@property
def questions(self):
return self.__annotation_template_data.get("questions")
4 changes: 2 additions & 2 deletions labellerr/core/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
BASE_URL = "https://api.labellerr.com"
BASE_URL = "https://api-gateway-qcb3iv2gaa-uc.a.run.app"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL SECURITY ISSUE: The BASE_URL has been changed to a development/staging environment (api-gateway-qcb3iv2gaa-uc.a.run.app). This should NOT be committed to the main branch.

Suggested change
BASE_URL = "https://api-gateway-qcb3iv2gaa-uc.a.run.app"
BASE_URL = "https://api.labellerr.com"

This looks like a development configuration that was accidentally committed. Please revert this change before merging.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Hardcoded Development/QA API URL

The BASE_URL is pointing to a non-production API endpoint (api-gateway-qcb3iv2gaa-uc.a.run.app) instead of the production URL (api.labellerr.com).

Impact:

  • This PR should not be merged with a development/QA endpoint
  • Production users will hit the wrong API
  • Could cause data to go to the wrong environment

Security Risk: HIGH

Recommendation:
Revert to production URL before merging:

Suggested change
BASE_URL = "https://api-gateway-qcb3iv2gaa-uc.a.run.app"
BASE_URL = "https://api.labellerr.com"

ALLOWED_ORIGINS = "https://pro.labellerr.com"


Expand All @@ -7,7 +7,7 @@
TOTAL_FILES_SIZE_LIMIT_PER_DATASET = 2.5 * 1024 * 1024 * 1024 # 2.5GB
TOTAL_FILES_COUNT_LIMIT_PER_DATASET = 2500

ANNOTATION_FORMAT = ["json", "coco_json", "csv", "png"]
ANNOTATION_FORMAT = ["json", "coco_json", "csv", "png", "video_json"]
LOCAL_EXPORT_FORMAT = ["json", "coco_json", "csv", "png"]
LOCAL_EXPORT_STATUS = [
"review",
Expand Down
62 changes: 31 additions & 31 deletions labellerr/core/datasets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import logging
import uuid
from abc import ABCMeta
from typing import Dict, Any, List, TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, Generator

from .. import constants
from ..exceptions import InvalidDatasetError, LabellerrError
from ..client import LabellerrClient

from ..files import LabellerrFile
from ..connectors import LabellerrConnection
from ..exceptions import InvalidDatasetError, LabellerrError
from ..files import LabellerrFile

if TYPE_CHECKING:
from ..projects import LabellerrProject
Expand Down Expand Up @@ -175,15 +174,22 @@ def on_success(dataset_data):
on_success=on_success,
)

def fetch_files(self, page_size: int = 1000) -> List[LabellerrFile]:
def fetch_files(
self, page_size: int = 1000
) -> Generator[LabellerrFile, None, None]:
def fetch_files(
self, page_size: int = 1000
) -> Generator[LabellerrFile, None, None]:
Comment on lines +177 to +182

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Duplicate Method Signature

The method signature for fetch_files is declared twice on consecutive lines (177-179 and 180-182). This is a merge error.

Impact:

  • Syntax error or confusing code
  • Duplicate docstrings and parameter documentation

Recommendation:

Suggested change
def fetch_files(
self, page_size: int = 1000
) -> Generator[LabellerrFile, None, None]:
def fetch_files(
self, page_size: int = 1000
) -> Generator[LabellerrFile, None, None]:
def fetch_files(
self, page_size: int = 1000
) -> Generator[LabellerrFile, None, None]:
"""
Fetch all files in this dataset as LabellerrFile instances.
:param page_size: Number of files to fetch per API request (default: 1000)
:return: Generator yielding LabellerrFile instances
"""

Remove the duplicate lines 180-189.

"""
Fetch all files in this dataset as LabellerrFile instances.

:param page_size: Number of files to fetch per API request (default: 10)
:return: List of file IDs
:param page_size: Number of files to fetch per API request (default: 1000)
:return: Generator yielding LabellerrFile instances
:param page_size: Number of files to fetch per API request (default: 1000)
:return: Generator yielding LabellerrFile instances
"""
print(f"Fetching files for dataset: {self.dataset_id}")
file_ids = []
logging.info(f"Fetching files for dataset: {self.dataset_id}")
logging.info(f"Fetching files for dataset: {self.dataset_id}")
Comment on lines +191 to +192

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Duplicate Logging Statement

The same logging statement appears twice on consecutive lines. This is redundant.

Recommendation:

Suggested change
logging.info(f"Fetching files for dataset: {self.dataset_id}")
logging.info(f"Fetching files for dataset: {self.dataset_id}")
logging.info(f"Fetching files for dataset: {self.dataset_id}")

next_search_after = None # Start with None for first page

while True:
Expand All @@ -205,15 +211,26 @@ def fetch_files(self, page_size: int = 1000) -> List[LabellerrFile]:
response = self.client.make_request(
"GET", url, extra_headers=None, request_id=unique_id, params=params
)
print(response)
# Extract files from the response
files = response.get("response", {}).get("files", [])

# Collect file IDs
for file_info in files:
file_id = file_info.get("file_id")
if file_id:
file_ids.append(file_id)
for file_data in files:
try:
_file = LabellerrFile.from_file_data(self.client, file_data)
yield _file
except LabellerrError as e:
logging.warning(
f"Warning: Failed to create file instance for {file_data.get('file_id')}: {str(e)}"
)
for file_data in files:
try:
_file = LabellerrFile.from_file_data(self.client, file_data)
yield _file
except LabellerrError as e:
logging.warning(
f"Warning: Failed to create file instance for {file_data.get('file_id')}: {str(e)}"
)
Comment on lines +218 to +233

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Duplicate Loop Logic

The same for-loop appears twice (lines 218-225 and 226-233), processing files and yielding them. This will cause each file to be yielded twice.

Impact:

  • Files will be duplicated in the results
  • Generator will return each file twice
  • Potential memory and performance issues

Recommendation:

Suggested change
for file_data in files:
try:
_file = LabellerrFile.from_file_data(self.client, file_data)
yield _file
except LabellerrError as e:
logging.warning(
f"Warning: Failed to create file instance for {file_data.get('file_id')}: {str(e)}"
)
for file_data in files:
try:
_file = LabellerrFile.from_file_data(self.client, file_data)
yield _file
except LabellerrError as e:
logging.warning(
f"Warning: Failed to create file instance for {file_data.get('file_id')}: {str(e)}"
)
# Collect file IDs
for file_data in files:
try:
_file = LabellerrFile.from_file_data(self.client, file_data)
yield _file
except LabellerrError as e:
logging.warning(
f"Warning: Failed to create file instance for {file_data.get('file_id')}: {str(e)}"
)

Remove the duplicate loop (lines 226-233).


# Get next_search_after for pagination
next_search_after = response.get("response", {}).get("next_search_after")
Expand All @@ -222,23 +239,6 @@ def fetch_files(self, page_size: int = 1000) -> List[LabellerrFile]:
if not next_search_after or not files:
break

files = []

for file_id in file_ids:
try:
_file = LabellerrFile(
client=self.client,
file_id=file_id,
dataset_id=self.dataset_id,
)
files.append(_file)
except LabellerrError as e:
logging.warning(
f"Warning: Failed to create file instance for {file_id}: {str(e)}"
)

return files

def sync_with_connection(
self,
project: "LabellerrProject",
Expand Down
9 changes: 2 additions & 7 deletions labellerr/core/exports/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
Export class for handling export operations with status tracking and polling.
"""

from typing import TYPE_CHECKING, Dict, Any, Optional
import logging
import json
import logging
from typing import TYPE_CHECKING, Any, Dict, Optional

if TYPE_CHECKING:
from ..projects.base import LabellerrProject
Expand Down Expand Up @@ -39,9 +39,6 @@ def report_id(self) -> str:
def _status(self) -> Dict[str, Any]:
"""Get current export status (single check)."""
response = self._project.check_export_status([self._report_id])
if isinstance(response, str):
response = json.loads(response)

for status_item in response.get("status", []):
if status_item.get("report_id") == self._report_id:
return status_item
Expand All @@ -58,8 +55,6 @@ def status(

def get_status():
response = self._project.check_export_status([self._report_id])
if isinstance(response, str):
response = json.loads(response)
return response

def is_completed(response_data):
Expand Down
Loading
Loading