Skip to content
Draft
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to the [Nucleus Python Client](https://github.com/scaleapi/n
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.20.1](https://github.com/scaleapi/nucleus-python-client/releases/tag/v0.20.1) - 2026-08-14

### Added
- **`dataset_item_id` on exported items and objects.** Batch exports now carry the Nucleus-internal dataset item id (`di_*`) everywhere `reference_id` already appeared: on `DatasetItem`, and on every exported annotation and prediction (`box`, `line`, `polygon`, `keypoints`, `cuboid`, `category`, `multicategory`, `segmentation`). Video/scene exports carry it on each track frame. Previously only `reference_id` was returned, so keying predictions back to items required a second lookup.

The field is server-assigned and read-only: it is populated by `from_json`, left `None` on objects you construct locally, excluded from `__eq__`, and never sent in `to_payload`. Exports from an older backend that does not return it simply leave it `None`.

## [0.20.0](https://github.com/scaleapi/nucleus-python-client/releases/tag/v0.20.0) - 2026-08-11

### Added
Expand Down
81 changes: 81 additions & 0 deletions nucleus/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
BOX_TYPE,
CATEGORY_TYPE,
CUBOID_TYPE,
DATASET_ITEM_ID_KEY,
DIMENSIONS_KEY,
EMBEDDING_VECTOR_KEY,
GEOMETRY_KEY,
Expand Down Expand Up @@ -58,6 +59,9 @@ class Annotation:
"""

reference_id: str
# Set on every subclass so callers can read it off the base type. See the note on
# BoxAnnotation for why it is server-assigned and excluded from equality.
dataset_item_id: Optional[str]

@classmethod
def from_json(cls, payload: dict):
Expand Down Expand Up @@ -160,6 +164,14 @@ class BoxAnnotation(Annotation): # pylint: disable=R0902
embedding_vector: Optional[list] = None
track_reference_id: Optional[str] = None
_task_id: Optional[str] = field(default=None, repr=False)
# Nucleus-internal dataset item id (``di_*``) of the item this object sits on.
# Server-assigned and read-only: populated on objects returned by the API, ``None``
# on ones you construct locally to upload, and never sent in ``to_payload``.
# Excluded from ``__eq__`` so a locally-built object still compares equal to its
# round-tripped self (same reason as ``DatasetItem.phash``).
dataset_item_id: Optional[str] = field(
default=None, repr=False, compare=False
)

def __post_init__(self):
self.metadata = self.metadata if self.metadata else {}
Expand All @@ -181,6 +193,7 @@ def from_json(cls, payload: dict):
embedding_vector=payload.get(EMBEDDING_VECTOR_KEY, None),
track_reference_id=payload.get(TRACK_REFERENCE_ID_KEY, None),
_task_id=payload.get(TASK_ID_KEY, None),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY, None),
)

def to_payload(self) -> dict:
Expand Down Expand Up @@ -281,6 +294,14 @@ class LineAnnotation(Annotation):
metadata: Optional[Dict] = None
track_reference_id: Optional[str] = None
_task_id: Optional[str] = field(default=None, repr=False)
# Nucleus-internal dataset item id (``di_*``) of the item this object sits on.
# Server-assigned and read-only: populated on objects returned by the API, ``None``
# on ones you construct locally to upload, and never sent in ``to_payload``.
# Excluded from ``__eq__`` so a locally-built object still compares equal to its
# round-tripped self (same reason as ``DatasetItem.phash``).
dataset_item_id: Optional[str] = field(
default=None, repr=False, compare=False
)

def __post_init__(self):
self.metadata = self.metadata if self.metadata else {}
Expand Down Expand Up @@ -311,6 +332,7 @@ def from_json(cls, payload: dict):
metadata=payload.get(METADATA_KEY, {}),
track_reference_id=payload.get(TRACK_REFERENCE_ID_KEY, None),
_task_id=payload.get(TASK_ID_KEY, None),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY, None),
)

def to_payload(self) -> dict:
Expand Down Expand Up @@ -376,6 +398,14 @@ class PolygonAnnotation(Annotation):
embedding_vector: Optional[list] = None
track_reference_id: Optional[str] = None
_task_id: Optional[str] = field(default=None, repr=False)
# Nucleus-internal dataset item id (``di_*``) of the item this object sits on.
# Server-assigned and read-only: populated on objects returned by the API, ``None``
# on ones you construct locally to upload, and never sent in ``to_payload``.
# Excluded from ``__eq__`` so a locally-built object still compares equal to its
# round-tripped self (same reason as ``DatasetItem.phash``).
dataset_item_id: Optional[str] = field(
default=None, repr=False, compare=False
)

def __post_init__(self):
self.metadata = self.metadata if self.metadata else {}
Expand Down Expand Up @@ -407,6 +437,7 @@ def from_json(cls, payload: dict):
embedding_vector=payload.get(EMBEDDING_VECTOR_KEY, None),
track_reference_id=payload.get(TRACK_REFERENCE_ID_KEY, None),
_task_id=payload.get(TASK_ID_KEY, None),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY, None),
)

def to_payload(self) -> dict:
Expand Down Expand Up @@ -519,6 +550,14 @@ class KeypointsAnnotation(Annotation):
metadata: Optional[Dict] = None
track_reference_id: Optional[str] = None
_task_id: Optional[str] = field(default=None, repr=False)
# Nucleus-internal dataset item id (``di_*``) of the item this object sits on.
# Server-assigned and read-only: populated on objects returned by the API, ``None``
# on ones you construct locally to upload, and never sent in ``to_payload``.
# Excluded from ``__eq__`` so a locally-built object still compares equal to its
# round-tripped self (same reason as ``DatasetItem.phash``).
dataset_item_id: Optional[str] = field(
default=None, repr=False, compare=False
)

def __post_init__(self):
self.metadata = self.metadata or {}
Expand Down Expand Up @@ -572,6 +611,7 @@ def from_json(cls, payload: dict):
metadata=payload.get(METADATA_KEY, {}),
track_reference_id=payload.get(TRACK_REFERENCE_ID_KEY, None),
_task_id=payload.get(TASK_ID_KEY, None),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY, None),
)

def to_payload(self) -> dict:
Expand Down Expand Up @@ -693,6 +733,14 @@ class CuboidAnnotation(Annotation): # pylint: disable=R0902
metadata: Optional[Dict] = None
track_reference_id: Optional[str] = None
_task_id: Optional[str] = field(default=None, repr=False)
# Nucleus-internal dataset item id (``di_*``) of the item this object sits on.
# Server-assigned and read-only: populated on objects returned by the API, ``None``
# on ones you construct locally to upload, and never sent in ``to_payload``.
# Excluded from ``__eq__`` so a locally-built object still compares equal to its
# round-tripped self (same reason as ``DatasetItem.phash``).
dataset_item_id: Optional[str] = field(
default=None, repr=False, compare=False
)

def __post_init__(self):
self.metadata = self.metadata if self.metadata else {}
Expand All @@ -710,6 +758,7 @@ def from_json(cls, payload: dict):
metadata=payload.get(METADATA_KEY, {}),
track_reference_id=payload.get(TRACK_REFERENCE_ID_KEY, None),
_task_id=payload.get(TASK_ID_KEY, None),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY, None),
)

def to_payload(self) -> dict:
Expand Down Expand Up @@ -846,6 +895,10 @@ class SegmentationAnnotation(Annotation):
reference_id: str
annotation_id: Optional[str] = None
# metadata: Optional[dict] = None # TODO(sc: 422637)
# See the note on BoxAnnotation.dataset_item_id — server-assigned, read-only.
dataset_item_id: Optional[str] = field(
default=None, repr=False, compare=False
)

def __post_init__(self):
if not self.mask_url:
Expand All @@ -863,6 +916,7 @@ def from_json(cls, payload: dict):
],
reference_id=payload[REFERENCE_ID_KEY],
annotation_id=payload.get(ANNOTATION_ID_KEY, None),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY, None),
# metadata=payload.get(METADATA_KEY, None), # TODO(sc: 422637)
)

Expand Down Expand Up @@ -947,6 +1001,14 @@ class CategoryAnnotation(Annotation):
metadata: Optional[Dict] = None
track_reference_id: Optional[str] = None
_task_id: Optional[str] = field(default=None, repr=False)
# Nucleus-internal dataset item id (``di_*``) of the item this object sits on.
# Server-assigned and read-only: populated on objects returned by the API, ``None``
# on ones you construct locally to upload, and never sent in ``to_payload``.
# Excluded from ``__eq__`` so a locally-built object still compares equal to its
# round-tripped self (same reason as ``DatasetItem.phash``).
dataset_item_id: Optional[str] = field(
default=None, repr=False, compare=False
)

def __post_init__(self):
self.metadata = self.metadata if self.metadata else {}
Expand All @@ -960,6 +1022,7 @@ def from_json(cls, payload: dict):
metadata=payload.get(METADATA_KEY, {}),
track_reference_id=payload.get(TRACK_REFERENCE_ID_KEY, None),
_task_id=payload.get(TASK_ID_KEY, None),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY, None),
)

def to_payload(self) -> dict:
Expand Down Expand Up @@ -987,6 +1050,14 @@ class MultiCategoryAnnotation(Annotation):
metadata: Optional[Dict] = None
track_reference_id: Optional[str] = None
_task_id: Optional[str] = field(default=None, repr=False)
# Nucleus-internal dataset item id (``di_*``) of the item this object sits on.
# Server-assigned and read-only: populated on objects returned by the API, ``None``
# on ones you construct locally to upload, and never sent in ``to_payload``.
# Excluded from ``__eq__`` so a locally-built object still compares equal to its
# round-tripped self (same reason as ``DatasetItem.phash``).
dataset_item_id: Optional[str] = field(
default=None, repr=False, compare=False
)

def __post_init__(self):
self.metadata = self.metadata if self.metadata else {}
Expand All @@ -1000,6 +1071,7 @@ def from_json(cls, payload: dict):
metadata=payload.get(METADATA_KEY, {}),
track_reference_id=payload.get(TRACK_REFERENCE_ID_KEY, None),
_task_id=payload.get(TASK_ID_KEY, None),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY, None),
)

def to_payload(self) -> dict:
Expand Down Expand Up @@ -1050,6 +1122,14 @@ class SceneCategoryAnnotation(Annotation):
taxonomy_name: Optional[str] = None
metadata: Optional[Dict] = field(default_factory=dict)
_task_id: Optional[str] = field(default=None, repr=False)
# Nucleus-internal dataset item id (``di_*``) of the item this object sits on.
# Server-assigned and read-only: populated on objects returned by the API, ``None``
# on ones you construct locally to upload, and never sent in ``to_payload``.
# Excluded from ``__eq__`` so a locally-built object still compares equal to its
# round-tripped self (same reason as ``DatasetItem.phash``).
dataset_item_id: Optional[str] = field(
default=None, repr=False, compare=False
)

@classmethod
def from_json(cls, payload: dict):
Expand All @@ -1059,6 +1139,7 @@ def from_json(cls, payload: dict):
taxonomy_name=payload.get(TAXONOMY_NAME_KEY, None),
metadata=payload.get(METADATA_KEY, {}),
_task_id=payload.get(TASK_ID_KEY, None),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY, None),
)

def to_payload(self) -> dict:
Expand Down
1 change: 1 addition & 0 deletions nucleus/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,7 @@ def scene_and_annotation_generator(
"width": int,
"height": int,
"key": str, # frame key
"dataset_item_id": str, # id of the frame's dataset item
"metadata": Dict[str, Any]
}]
}
Expand Down
7 changes: 7 additions & 0 deletions nucleus/dataset_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .constants import (
BACKEND_REFERENCE_ID_KEY,
CAMERA_PARAMS_KEY,
DATASET_ITEM_ID_KEY,
EMBEDDING_INFO_KEY,
EMBEDDING_VECTOR_KEY,
HEIGHT_KEY,
Expand Down Expand Up @@ -131,6 +132,11 @@ class DatasetItem: # pylint: disable=R0902
# returned object — so locally-constructed items would otherwise spuriously
# differ from round-tripped ones.
phash: Optional[str] = field(default=None, compare=False)
# Nucleus-internal dataset item id (``di_*``), assigned server-side. Populated on
# items returned by the API; ``None`` on items you construct locally to upload.
# Excluded from auto-generated __eq__ for the same reason as ``phash`` — a
# locally-built item would otherwise never compare equal to its round-tripped self.
dataset_item_id: Optional[str] = field(default=None, compare=False)

def __post_init__(self):
assert self.reference_id is not None, "reference_id is required."
Expand Down Expand Up @@ -187,6 +193,7 @@ def from_json(cls, payload: dict):
reference_id=payload.get(REFERENCE_ID_KEY),
metadata=payload.get(METADATA_KEY, {}),
phash=payload.get(PHASH_KEY),
dataset_item_id=payload.get(DATASET_ITEM_ID_KEY),
)

def local_file_exists(self):
Expand Down
Loading