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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ 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.19.1](https://github.com/scaleapi/nucleus-python-client/releases/tag/v0.19.1) - 2026-07-27

### Added
- **Model weights.** Attach a raw weights artifact (any binary, no format constraints) to a model and fetch it back: `NucleusClient.upload_model_weights(model, path)`, `download_model_weights(model, path)`, `get_model_weights(model)`, and `delete_model_weights(model)`, plus `Model.upload_weights()` / `download_weights()` / `weights()` / `delete_weights()` and the new `ModelWeights` metadata type (`present`, `status`, `size_bytes`, `original_filename`, `content_type`, `download_url`). Artifacts up to 10 GB are supported; uploading requires edit access on the model, downloading is available to anyone who can see it.
- Large artifacts are handled without any extra work on the caller's part, and both `upload_model_weights` and `download_model_weights` accept an `on_progress` callback receiving `(bytes_transferred, total_bytes)` to report progress on long transfers.

## [0.19.0](https://github.com/scaleapi/nucleus-python-client/releases/tag/v0.19.0) - 2026-07-10

### Added
Expand Down
163 changes: 163 additions & 0 deletions nucleus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"LinePrediction",
"Model",
"ModelCreationError",
"ModelWeights",
# "MultiCategoryAnnotation", # coming soon!
"NotFoundError",
"NucleusAPIError",
Expand Down Expand Up @@ -115,6 +116,7 @@
DATASET_IS_SCENE_KEY,
DATASET_PRIVACY_MODE_KEY,
DEFAULT_NETWORK_TIMEOUT_SEC,
DELETED_KEY,
EMBEDDING_DIMENSION_KEY,
EMBEDDINGS_URL_KEY,
ERROR_ITEMS,
Expand Down Expand Up @@ -146,6 +148,8 @@
SLICE_TAGS_KEY,
STATUS_CODE_KEY,
UPDATE_KEY,
UPLOAD_ID_KEY,
URL_KEY,
)
from .data_transfer_object.dataset_details import DatasetDetails
from .data_transfer_object.dataset_info import DatasetInfo
Expand Down Expand Up @@ -196,6 +200,15 @@
)
from .model import Model
from .model_run import ModelRun
from .model_weights import (
MODEL_WEIGHTS_MAX_BYTES,
ModelWeights,
ProgressCallback,
_finalize_payload,
_presign_payload,
_stream_weights_to_file,
_transfer_weights_to_storage,
)
from .payload_constructor import (
construct_annotation_payload,
construct_append_payload,
Expand Down Expand Up @@ -1638,6 +1651,156 @@ def delete_model(self, model_id: str) -> dict:
)
return response

def upload_model_weights(
self,
model: Union[Model, str],
path: str,
*,
content_type: Optional[str] = None,
original_filename: Optional[str] = None,
checksum_sha256: Optional[str] = None,
on_progress: Optional[ProgressCallback] = None,
) -> ModelWeights:
"""Attach a weights artifact to a model.

Any binary is accepted — there are no format constraints — up to 10 GB.
Requires edit access on the model.

::

import nucleus

client = nucleus.NucleusClient(YOUR_SCALE_API_KEY)
model = client.get_model(reference_id="My-CNN")
client.upload_model_weights(model, "/path/to/weights.bin")

Parameters:
model: A :class:`Model` or a model id (``prj_*``).
path: Local path of the artifact to upload.
content_type: Content type to record for the artifact. Defaults to
``application/octet-stream``.
original_filename: Filename to show for the artifact. Defaults to
the name of the file at ``path``.
checksum_sha256: Optional SHA-256 of the artifact.
on_progress: Called with ``(bytes_uploaded, total_bytes)`` as the
upload proceeds.

Returns:
:class:`ModelWeights`: Metadata for the uploaded artifact.
"""
model_id = model.id if isinstance(model, Model) else model
total_bytes = os.path.getsize(path)
if total_bytes > MODEL_WEIGHTS_MAX_BYTES:
raise ValueError(
f"{path} is {total_bytes} bytes, which exceeds the "
f"{MODEL_WEIGHTS_MAX_BYTES // 1024 ** 3} GB model weights limit"
)

presign = self.make_request(
_presign_payload(
total_bytes,
content_type,
original_filename
if original_filename is not None
else os.path.basename(path),
checksum_sha256,
),
f"model/{model_id}/weights/presign",
)
parts = _transfer_weights_to_storage(
path, presign, total_bytes, on_progress
)
finalized = self.make_request(
_finalize_payload(presign[UPLOAD_ID_KEY], parts),
f"model/{model_id}/weights/finalize",
)
return ModelWeights.from_json(finalized, self)

def download_model_weights(
self,
model: Union[Model, str],
path: str,
*,
on_progress: Optional[ProgressCallback] = None,
) -> str:
"""Download a model's weights artifact to a local path.

Available to anyone who can see the model.

::

import nucleus

client = nucleus.NucleusClient(YOUR_SCALE_API_KEY)
model = client.get_model(reference_id="My-CNN")
client.download_model_weights(model, "/path/to/save/weights.bin")

Parameters:
model: A :class:`Model` or a model id (``prj_*``).
path: Local path to write the artifact to. Parent directories are
created if needed.
on_progress: Called with ``(bytes_downloaded, total_bytes)`` as the
download proceeds. ``total_bytes`` is ``0`` when the size isn't
known ahead of time.

Returns:
str: The path written.

Raises:
ValueError: If the model has no weights artifact to download.
"""
model_id = model.id if isinstance(model, Model) else model
# Ask for the URL as JSON rather than following the redirect, so the
# API credentials aren't replayed to the download host.
signed = self.make_request(
{},
f"model/{model_id}/weights/download?json=1",
requests_command=requests.get,
)
url = signed.get(URL_KEY)
if not url:
raise ValueError(
f"Model {model_id} has no downloadable weights artifact"
)
return _stream_weights_to_file(url, path, on_progress)

def get_model_weights(self, model: Union[Model, str]) -> ModelWeights:
"""Fetch metadata for a model's weights artifact.

Parameters:
model: A :class:`Model` or a model id (``prj_*``).

Returns:
:class:`ModelWeights`: Metadata. ``present`` is ``False`` when the
model has no weights artifact available.
"""
model_id = model.id if isinstance(model, Model) else model
return ModelWeights.from_json(
self.make_request(
{}, f"model/{model_id}/weights", requests_command=requests.get
),
self,
)

def delete_model_weights(self, model: Union[Model, str]) -> bool:
"""Delete a model's weights artifact.

Requires edit access on the model.

Parameters:
model: A :class:`Model` or a model id (``prj_*``).

Returns:
bool: Whether an artifact was deleted.
"""
model_id = model.id if isinstance(model, Model) else model
response = self.make_request(
{},
f"model/{model_id}/weights",
requests_command=requests.delete,
)
return bool(response.get(DELETED_KEY, False))

def download_pointcloud_task(
self, task_id: str, frame_num: int
) -> List[Union[Point3D, LidarPoint]]:
Expand Down
20 changes: 20 additions & 0 deletions nucleus/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,23 @@
Y_KEY = "y"
Z_KEY = "z"
GLOB_SIZE_THRESHOLD_CHECK = 500

# Model weights payload keys. Unlike most of the keys above these are camelCase:
# the weights routes serialize their DTOs straight through rather than
# snake_casing them, so the wire format is camelCase in both directions.
CHECKSUM_SHA256_KEY = "checksumSha256"
CONTENT_TYPE_KEY = "contentType"
DECLARED_SIZE_BYTES_KEY = "declaredSizeBytes"
DELETED_KEY = "deleted"
DOWNLOAD_URL_KEY = "downloadUrl"
ETAG_KEY = "eTag"
MODEL_PROJECT_ID_KEY = "modelProjectId"
ORIGINAL_FILENAME_KEY = "originalFilename"
PART_NUMBER_KEY = "partNumber"
PART_SIZE_BYTES_KEY = "partSizeBytes"
PARTS_KEY = "parts"
PRESENT_KEY = "present"
REQUIRED_HEADERS_KEY = "requiredHeaders"
SIZE_BYTES_KEY = "sizeBytes"
UPLOAD_ID_KEY = "uploadId"
UPLOAD_URL_KEY = "uploadUrl"
36 changes: 36 additions & 0 deletions nucleus/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from .dataset import Dataset
from .model_run import ModelRun
from .model_weights import ModelWeights
from .prediction import (
BoxPrediction,
CuboidPrediction,
Expand Down Expand Up @@ -343,3 +344,38 @@ def remove_trained_slice_ids(self, slide_ids: List[str]):
)

return response.json()

def upload_weights(self, path: str, **kwargs) -> "ModelWeights":
"""Attach a weights artifact to this model. ::

import nucleus
client = nucleus.NucleusClient("YOUR_SCALE_API_KEY")
model = client.get_model(reference_id="My-CNN")

model.upload_weights("/path/to/weights.bin")

See :meth:`NucleusClient.upload_model_weights` for the accepted keyword
arguments.
"""
return self._client.upload_model_weights(self, path, **kwargs)

def download_weights(self, path: str, **kwargs) -> str:
"""Download this model's weights artifact to ``path``.

See :meth:`NucleusClient.download_model_weights`.
"""
return self._client.download_model_weights(self, path, **kwargs)

def weights(self) -> "ModelWeights":
"""Fetch metadata for this model's weights artifact.

See :meth:`NucleusClient.get_model_weights`.
"""
return self._client.get_model_weights(self)

def delete_weights(self) -> bool:
"""Delete this model's weights artifact.

See :meth:`NucleusClient.delete_model_weights`.
"""
return self._client.delete_model_weights(self)
Loading