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
4 changes: 3 additions & 1 deletion omlmd/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,6 @@ def push(
if empty_metadata:
logger.warning(f"Pushing to {target} with empty metadata.")
md = deserialize_mdfile(metadata) if metadata else {}
click.echo(Helper.from_default_registry(plain_http).push(target, path, **md))
response = Helper.from_default_registry(plain_http).push(target, path, **md)
digest = response.headers.get("Docker-Content-Digest", "unknown")
click.echo(f"Pushed {target}@{digest}")
2 changes: 2 additions & 0 deletions omlmd/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ def to_annotations_dict(self) -> dict[str, str]:
result[k] = v
elif v is None:
continue
elif isinstance(v, dict) and not v:
continue # skip empty dicts (e.g. empty customProperties)
else:
result[f"{k}+json"] = json.dumps(
v
Expand Down
37 changes: 37 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import typing as t
from hashlib import sha256
from pathlib import Path
from unittest.mock import MagicMock

import pytest
from click.testing import CliRunner

from omlmd.cli import cli
from omlmd.constants import MIME_APPLICATION_MLMODEL
from omlmd.helpers import Helper
from omlmd.listener import Event, Listener
Expand Down Expand Up @@ -162,3 +165,37 @@ def test_e2e_push_pull_column(tmp_path, target):
assert pulled_sha == content_sha
finally:
temp.unlink()


def test_cli_push_logs_digest(mocker):
"""Issue #19: CLI push should log the digest, not the raw Response object."""
mock_response = MagicMock()
mock_response.headers = {"Docker-Content-Digest": "sha256:abc123def456"}
mocker.patch.object(Helper, "push", return_value=mock_response)

runner = CliRunner()
with runner.isolated_filesystem():
Path("model.bin").write_text("fake model")
Path("meta.json").write_text('{"name": "test"}')
result = runner.invoke(cli, ["push", "--plain-http", "localhost:5000/test:v1", "model.bin", "-m", "meta.json"])

assert result.exit_code == 0
assert "sha256:abc123def456" in result.output
assert "Pushed" in result.output
assert "<Response" not in result.output


def test_cli_push_logs_unknown_when_no_digest(mocker):
"""CLI push should handle missing digest header gracefully."""
mock_response = MagicMock()
mock_response.headers = {}
mocker.patch.object(Helper, "push", return_value=mock_response)

runner = CliRunner()
with runner.isolated_filesystem():
Path("model.bin").write_text("fake model")
Path("meta.json").write_text('{"name": "test"}')
result = runner.invoke(cli, ["push", "--plain-http", "localhost:5000/test:v1", "model.bin", "-m", "meta.json"])

assert result.exit_code == 0
assert "unknown" in result.output
23 changes: 23 additions & 0 deletions tests/test_omlmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,26 @@ def test_is_empty():
name="mnist",
)
assert not md.is_empty()


def test_to_annotations_dict_skips_empty_custom_properties():
"""Issue #18: empty customProperties should not produce an annotation."""
md = ModelMetadata(name="test-model", customProperties={})
annotations = md.to_annotations_dict()
assert "name" in annotations
assert "customProperties+json" not in annotations


def test_to_annotations_dict_includes_non_empty_custom_properties():
md = ModelMetadata(name="test-model", customProperties={"accuracy": 0.99})
annotations = md.to_annotations_dict()
assert "name" in annotations
assert "customProperties+json" in annotations


def test_to_annotations_dict_skips_none_values():
md = ModelMetadata(name="test-model")
annotations = md.to_annotations_dict()
assert "name" in annotations
assert "description" not in annotations
assert "author" not in annotations