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
2 changes: 1 addition & 1 deletion libs/labelbox/src/labelbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2806,7 +2806,7 @@ def get_label_version(

Example:
>>> version = client.get_label_version("cm2v...")
>>> print(version.action, version.created_by_id, version.created_at)
>>> print(version.action, version.created_by_email, version.created_at)
>>> annotations = version.label_json
"""
return _label_version.get_label_version(
Expand Down
2 changes: 1 addition & 1 deletion libs/labelbox/src/labelbox/schema/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def versions(self, include_annotations: bool = True) -> PaginatedCollection:
Example:
>>> for version in label.versions():
>>> print(version.created_at, version.action,
>>> version.created_by_id, version.label_json)
>>> version.created_by_email, version.label_json)
"""
return _label_version.get_label_versions(
self.client,
Expand Down
13 changes: 12 additions & 1 deletion libs/labelbox/src/labelbox/schema/label_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ class LabelVersion(_CamelCaseMixin):
created_at (datetime): When the version was recorded.
created_by_id (str): The user who made the hand-off: the labeler for
a submit, the reviewer for a review save or decision.
created_by_email (Optional[str]): That user's email, masked the way
other user emails are between a workforce provider and the
organizations it works for. ``None`` when the user can't be read.
stage (Optional[LabelVersionStage]): The workflow step the data row
was in at the hand-off. ``None`` when it was in none (Done). For
``SENT_TO_REWORK`` this is the step the row was moved out of, so
Expand All @@ -101,6 +104,7 @@ class LabelVersion(_CamelCaseMixin):
action: Union[LabelVersionAction, str]
created_at: datetime
created_by_id: str
created_by_email: Optional[str] = None
stage: Optional[LabelVersionStage] = None
annotation_count: int
label: Optional[str] = None
Expand All @@ -109,6 +113,7 @@ class LabelVersion(_CamelCaseMixin):

_FIELDS = """
id labelId dataRowId projectId action createdAt createdById
createdBy { email }
stage { taskQueueId name type }
annotationCount
"""
Expand Down Expand Up @@ -162,8 +167,14 @@ def _to_version(result: Dict[str, Any]) -> LabelVersion:
action = result.get("action")
if action in LabelVersionAction._value2member_map_:
action = LabelVersionAction(action)
created_by = result.get("createdBy") or {}
return LabelVersion.model_validate(
{**result, "labelJson": label_json, "action": action}
{
**result,
"labelJson": label_json,
"action": action,
"createdByEmail": created_by.get("email"),
}
)


Expand Down
9 changes: 6 additions & 3 deletions libs/labelbox/src/labelbox/schema/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,9 @@ def label_versions(
saved from the editor, approved or rejected in a review step,
submitted from a rework step, or moved into a rework step. Review and
rework edit the same label, so one label's versions show what each
reviewer and rework changed. Autosaves and drafts record none, there
reviewer and rework changed. A review decision on a whole data row
records a version for each label on it, all with the same time and
reviewer. Autosaves and drafts record none, there
are none from before recording was enabled for the project, and
versions are never changed or deleted.

Expand Down Expand Up @@ -389,8 +391,9 @@ def label_versions(
ValueError: A bad page size or time range.

Example:
>>> for version in project.label_versions(label=label_id):
>>> print(version.action, version.created_by_id,
>>> for version in project.label_versions(data_row=data_row_id):
>>> print(version.label_id, version.action,
>>> version.created_by_email,
>>> version.stage and version.stage.name)
>>>
>>> # Everything reviewers did in October, without annotations.
Expand Down
11 changes: 11 additions & 0 deletions libs/labelbox/tests/unit/schema/test_label_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def node(index, **overrides):
"action": "SUBMITTED",
"createdAt": f"2026-10-19T10:00:0{index}.000Z",
"createdById": "clabeler000000000000000001",
"createdBy": {"email": "labeler@example.com"},
"stage": {
"taskQueueId": "b1a2c3d4-0000-0000-0000-000000000001",
"name": "Initial labeling task",
Expand Down Expand Up @@ -78,6 +79,7 @@ def test_pages_by_cursor_and_parses_versions(self):
2026, 10, 19, 10, 0, 1, tzinfo=timezone.utc
)
assert first.created_by_id == "clabeler000000000000000001"
assert first.created_by_email == "labeler@example.com"
assert first.stage == LabelVersionStage(
task_queue_id="b1a2c3d4-0000-0000-0000-000000000001",
name="Initial labeling task",
Expand All @@ -95,6 +97,7 @@ def test_pages_by_cursor_and_parses_versions(self):
query1
)
assert "label labelJson" in query1
assert "createdBy { email }" in query1
assert params1 == {
"where": {"projectId": PROJECT_ID},
"first": 2,
Expand Down Expand Up @@ -161,6 +164,14 @@ def test_keeps_an_unknown_action_as_a_string(self):

assert version.action == "NEW_ACTION"

def test_leaves_the_email_empty_when_the_user_is_unreadable(self):
client = client_returning(page([node(1, createdBy=None)]))

[version] = list(label_version.get_label_versions(client, PROJECT_ID))

assert version.created_by_id == "clabeler000000000000000001"
assert version.created_by_email is None

def test_parses_label_json_sent_as_text(self):
client = client_returning(page([node(1, labelJson='{"objects": []}')]))

Expand Down
Loading