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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ All notable changes to this project will be documented in this file.
YOLOv10. Existing models remain available for inference, and YOLO dataset
formats remain supported ([#572](https://github.com/roboflow/roboflow-python/pull/572)).

### Fixed

- `TrainedModel.download()` saves the file as `weights` plus the extension of
the artifact that the API serves, for example `weights.onnx`. Before, it
always used `weights.pt`
([#582](https://github.com/roboflow/roboflow-python/pull/582)).

## 1.7.3

### Added
Expand Down
11 changes: 9 additions & 2 deletions roboflow/core/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

import json
import os
from pathlib import PurePosixPath
from typing import List
from urllib.parse import urlparse

from roboflow.adapters import rfapi
from roboflow.config import (
Expand Down Expand Up @@ -159,12 +161,17 @@ def poll_until_video_results(self, job_id=None) -> dict:
return self._video_model().poll_until_video_results(job_id)

def download(self, format="pt", location="."):
"""Download this model's PyTorch weights to ``location/weights.pt``."""
"""Download this model's weights to ``location`` and return the file path.

The file is ``weights`` plus the extension of the artifact that the API
serves: ``weights.pt`` for most models, ``weights.onnx`` for a NAS child.
"""
weights_url = rfapi.get_model_weights_url(
self.__api_key, self.workspace, self.project, self._weights_id, model_format=format
)
suffix = PurePosixPath(urlparse(weights_url).path).suffix or ".pt"
os.makedirs(location, exist_ok=True)
out_path = os.path.join(location, "weights.pt")
out_path = os.path.join(location, f"weights{suffix}")
response = http.get(weights_url, stream=True)
response.raise_for_status()
with open(out_path, "wb") as f:
Expand Down
32 changes: 31 additions & 1 deletion tests/test_training.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import tempfile
import unittest
from unittest.mock import patch
from pathlib import Path
from unittest.mock import MagicMock, patch

from roboflow.config import (
CLASSIFICATION_MODEL,
Expand Down Expand Up @@ -88,6 +90,34 @@ def test_poll_reuses_the_predict_video_legacy_model(self):
self.assertIs(poll.call_args.args[0], model._video_model())


class TestTrainedModelDownload(unittest.TestCase):
def test_download_keeps_the_extension_of_the_served_artifact(self):
signed_query = "?X-Goog-Signature=abc.def"
cases = [
("https://storage.googleapis.com/bucket/ws/models/doc/weights.pt", "weights.pt"),
("https://storage.googleapis.com/bucket/ws/proj/3/best.pt", "weights.pt"),
("https://storage.googleapis.com/bucket/ws/models/doc/weights.onnx", "weights.onnx"),
("https://storage.googleapis.com/bucket/ws/models/doc/model.safetensors", "weights.safetensors"),
("https://storage.googleapis.com/bucket/ws/models/doc/weights", "weights.pt"),
]

for artifact_url, expected_filename in cases:
with self.subTest(artifact_url=artifact_url), tempfile.TemporaryDirectory() as location:
model = TrainedModel("key", "ws", "proj", "ws/model-slug", model_type="rfdetr-base")
response = MagicMock()
response.iter_content.return_value = [b"weights-bytes"]
weights_url = artifact_url + signed_query

with (
patch("roboflow.core.training.rfapi.get_model_weights_url", return_value=weights_url),
patch("roboflow.core.training.http.get", return_value=response),
):
out_path = model.download(location=location)

self.assertEqual(Path(out_path), Path(location) / expected_filename)
self.assertEqual(Path(out_path).read_bytes(), b"weights-bytes")


class TestTrainingModels(unittest.TestCase):
def test_models_are_cached_until_refresh(self):
training = Training("key", "ws", "proj", "1", {"trainingId": "training-1"})
Expand Down