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
37 changes: 36 additions & 1 deletion 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
from ..schemas.annotation_templates import (
CreateTemplateParams,
QuestionType,
Option,
DatasetDataType,
)
from .. import constants
from ..client import LabellerrClient
import uuid
from typing import List


__all__ = [
"LabellerrAnnotationTemplate",
Expand Down Expand Up @@ -62,3 +69,31 @@ 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}"
Comment thread
ximihoque marked this conversation as resolved.
f"&uuid={unique_id}"
)
Comment thread
ximihoque marked this conversation as resolved.
Comment thread
ximihoque marked this conversation as resolved.

response = client.make_request(
"GET",
url,
extra_headers={"content-type": "application/json"},
request_id=unique_id,
)
return [
Comment thread
ximihoque marked this conversation as resolved.
LabellerrAnnotationTemplate.from_annotation_template_data(client, **item)
for item in response.get("response", [])
Comment thread
ximihoque marked this conversation as resolved.
Comment thread
ximihoque marked this conversation as resolved.
]
Comment thread
ximihoque marked this conversation as resolved.
100 changes: 89 additions & 11 deletions labellerr/core/annotation_templates/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,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 +47,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
Comment thread
ximihoque marked this conversation as resolved.
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 = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Silent failure with empty dictionary

The fallback to an empty dictionary means all property accessors (template_name, data_type, questions, etc.) will return None. This creates a silent failure mode that will be hard to debug.

Recommendation: Raise an exception here instead:

else:
    raise RuntimeError(
        "LabellerrAnnotationTemplate instantiated without data. "
        "Use from_annotation_template_data() or let __new__ fetch it."
    )


@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())
Comment thread
ximihoque marked this conversation as resolved.
if missing_fields:
raise ValueError(
f"Missing required fields in annotation_template_data: {missing_fields}"
)
Comment thread
ximihoque marked this conversation as resolved.

# 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 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")
Comment thread
ximihoque marked this conversation as resolved.
2 changes: 2 additions & 0 deletions labellerr/core/projects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .document_project import DocucmentProject as LabellerrDocumentProject
from .image_project import ImageProject as LabellerrImageProject
from .video_project import VideoProject as LabellerrVideoProject
from .text_project import TextProject as LabellerrTextProject
from .base import LabellerrProject
from ..annotation_templates import LabellerrAnnotationTemplate
from typing import List
Expand All @@ -20,6 +21,7 @@
"LabellerrDocumentProject",
"LabellerrImageProject",
"LabellerrVideoProject",
"LabellerrTextProject",
]


Expand Down
9 changes: 9 additions & 0 deletions labellerr/core/projects/text_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .base import LabellerrProject, LabellerrProjectMeta


class TextProject(LabellerrProject):

pass


LabellerrProjectMeta._register("text", TextProject)
Loading