Skip to content

Commit d177ffe

Browse files
committed
feat: address review feedback on Python SDK
- Remove copied config-schema.json, load from repo root as single source of truth - Fix validator dialect: use Draft4Validator to match schema's draft-04 declaration - Fix timestamp serialization to use 'Z' suffix for UTC, matching Go's RFC 3339 - Fix FileMetadata.mtime to always serialize (matches Go's non-pointer time.Time) - Migrate from setup.py to pyproject.toml - Fix import sorting (ruff) Closes #138 Signed-off-by: pradhyum6144 <pradhyum314@gmail.com>
1 parent 44e3464 commit d177ffe

File tree

9 files changed

+98
-248
lines changed

9 files changed

+98
-248
lines changed

specs-python/modelpack/v1/__init__.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,43 @@
1414

1515
"""ModelPack Python SDK - CNCF standard for packaging and distributing AI models."""
1616

17+
from modelpack.v1.annotations import (
18+
ANNOTATION_FILE_METADATA,
19+
ANNOTATION_FILEPATH,
20+
ANNOTATION_MEDIA_TYPE_UNTESTED,
21+
FileMetadata,
22+
)
1723
from modelpack.v1.config import (
24+
Modality,
1825
Model,
1926
ModelCapabilities,
2027
ModelConfig,
2128
ModelDescriptor,
2229
ModelFS,
23-
Modality,
24-
)
25-
from modelpack.v1.annotations import (
26-
ANNOTATION_FILEPATH,
27-
ANNOTATION_FILE_METADATA,
28-
ANNOTATION_MEDIA_TYPE_UNTESTED,
29-
FileMetadata,
3030
)
3131
from modelpack.v1.mediatype import (
3232
ARTIFACT_TYPE_MODEL_MANIFEST,
33-
MEDIA_TYPE_MODEL_CONFIG,
34-
MEDIA_TYPE_MODEL_WEIGHT_RAW,
35-
MEDIA_TYPE_MODEL_WEIGHT,
36-
MEDIA_TYPE_MODEL_WEIGHT_GZIP,
37-
MEDIA_TYPE_MODEL_WEIGHT_ZSTD,
38-
MEDIA_TYPE_MODEL_WEIGHT_CONFIG_RAW,
39-
MEDIA_TYPE_MODEL_WEIGHT_CONFIG,
40-
MEDIA_TYPE_MODEL_WEIGHT_CONFIG_GZIP,
41-
MEDIA_TYPE_MODEL_WEIGHT_CONFIG_ZSTD,
42-
MEDIA_TYPE_MODEL_DOC_RAW,
43-
MEDIA_TYPE_MODEL_DOC,
44-
MEDIA_TYPE_MODEL_DOC_GZIP,
45-
MEDIA_TYPE_MODEL_DOC_ZSTD,
46-
MEDIA_TYPE_MODEL_CODE_RAW,
4733
MEDIA_TYPE_MODEL_CODE,
4834
MEDIA_TYPE_MODEL_CODE_GZIP,
35+
MEDIA_TYPE_MODEL_CODE_RAW,
4936
MEDIA_TYPE_MODEL_CODE_ZSTD,
50-
MEDIA_TYPE_MODEL_DATASET_RAW,
37+
MEDIA_TYPE_MODEL_CONFIG,
5138
MEDIA_TYPE_MODEL_DATASET,
5239
MEDIA_TYPE_MODEL_DATASET_GZIP,
40+
MEDIA_TYPE_MODEL_DATASET_RAW,
5341
MEDIA_TYPE_MODEL_DATASET_ZSTD,
42+
MEDIA_TYPE_MODEL_DOC,
43+
MEDIA_TYPE_MODEL_DOC_GZIP,
44+
MEDIA_TYPE_MODEL_DOC_RAW,
45+
MEDIA_TYPE_MODEL_DOC_ZSTD,
46+
MEDIA_TYPE_MODEL_WEIGHT,
47+
MEDIA_TYPE_MODEL_WEIGHT_CONFIG,
48+
MEDIA_TYPE_MODEL_WEIGHT_CONFIG_GZIP,
49+
MEDIA_TYPE_MODEL_WEIGHT_CONFIG_RAW,
50+
MEDIA_TYPE_MODEL_WEIGHT_CONFIG_ZSTD,
51+
MEDIA_TYPE_MODEL_WEIGHT_GZIP,
52+
MEDIA_TYPE_MODEL_WEIGHT_RAW,
53+
MEDIA_TYPE_MODEL_WEIGHT_ZSTD,
5454
)
5555
from modelpack.v1.validator import validate_config
5656

specs-python/modelpack/v1/annotations.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
from __future__ import annotations
1818

19-
from dataclasses import dataclass
20-
from datetime import datetime
19+
from dataclasses import dataclass, field
20+
from datetime import datetime, timezone
2121

2222
# Annotation key for the file path of the layer.
2323
ANNOTATION_FILEPATH = "org.cncf.model.filepath"
@@ -29,6 +29,14 @@
2929
ANNOTATION_MEDIA_TYPE_UNTESTED = "org.cncf.model.file.mediatype.untested"
3030

3131

32+
def _format_datetime(dt: datetime) -> str:
33+
"""Format a datetime as RFC 3339 with 'Z' suffix for UTC, matching Go."""
34+
s = dt.isoformat()
35+
if s.endswith("+00:00"):
36+
s = s[:-6] + "Z"
37+
return s
38+
39+
3240
@dataclass
3341
class FileMetadata:
3442
"""Represents the metadata of a file.
@@ -41,31 +49,33 @@ class FileMetadata:
4149
uid: int = 0
4250
gid: int = 0
4351
size: int = 0
44-
mod_time: datetime | None = None
52+
mod_time: datetime = field(
53+
default_factory=lambda: datetime(1, 1, 1, tzinfo=timezone.utc)
54+
)
4555
typeflag: int = 0
4656

4757
def to_dict(self) -> dict:
48-
"""Serialize to a dict matching the JSON field names."""
49-
d: dict = {
58+
"""Serialize to a dict matching the JSON field names.
59+
60+
All fields are always present, matching Go's FileMetadata
61+
which has no omitempty tags.
62+
"""
63+
return {
5064
"name": self.name,
5165
"mode": self.mode,
5266
"uid": self.uid,
5367
"gid": self.gid,
5468
"size": self.size,
69+
"mtime": _format_datetime(self.mod_time),
5570
"typeflag": self.typeflag,
5671
}
57-
if self.mod_time is not None:
58-
d["mtime"] = self.mod_time.isoformat()
59-
return d
6072

6173
@classmethod
6274
def from_dict(cls, data: dict) -> FileMetadata:
6375
"""Deserialize from a dict with JSON field names."""
6476
mod_time = None
6577
if "mtime" in data:
66-
mod_time = datetime.fromisoformat(
67-
data["mtime"].replace("Z", "+00:00")
68-
)
78+
mod_time = datetime.fromisoformat(data["mtime"].replace("Z", "+00:00"))
6979
return cls(
7080
name=data.get("name", ""),
7181
mode=data.get("mode", 0),

specs-python/modelpack/v1/config-schema.json

Lines changed: 0 additions & 168 deletions
This file was deleted.

specs-python/modelpack/v1/config.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
from typing import Optional
2424

2525

26+
def _format_datetime(dt: datetime) -> str:
27+
"""Format a datetime as RFC 3339 with 'Z' suffix for UTC, matching Go."""
28+
s = dt.isoformat()
29+
if s.endswith("+00:00"):
30+
s = s[:-6] + "Z"
31+
return s
32+
33+
2634
class Modality(str, Enum):
2735
"""Defines the input and output types of the model.
2836
@@ -60,7 +68,7 @@ def to_dict(self) -> dict:
6068
if self.output_types is not None:
6169
d["outputTypes"] = [m.value for m in self.output_types]
6270
if self.knowledge_cutoff is not None:
63-
d["knowledgeCutoff"] = self.knowledge_cutoff.isoformat()
71+
d["knowledgeCutoff"] = _format_datetime(self.knowledge_cutoff)
6472
if self.reasoning is not None:
6573
d["reasoning"] = self.reasoning
6674
if self.tool_usage is not None:
@@ -192,7 +200,7 @@ def to_dict(self) -> dict:
192200
"""Serialize to a dict matching the JSON schema field names."""
193201
d: dict = {}
194202
if self.created_at is not None:
195-
d["createdAt"] = self.created_at.isoformat()
203+
d["createdAt"] = _format_datetime(self.created_at)
196204
if self.authors is not None:
197205
d["authors"] = self.authors
198206
if self.family:

specs-python/modelpack/v1/validator.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,26 @@
1414

1515
"""JSON schema validation for ModelPack configs.
1616
17-
Uses the same config-schema.json as the Go validator to ensure
18-
consistent validation behavior across languages.
17+
Loads config-schema.json from the repo root (schema/config-schema.json)
18+
as the single source of truth, matching the Go validator.
1919
"""
2020

2121
from __future__ import annotations
2222

23-
import importlib.resources
2423
import json
24+
from pathlib import Path
2525

26-
from jsonschema import Draft202012Validator, FormatChecker
26+
from jsonschema import Draft4Validator, FormatChecker
2727

2828

2929
def _load_schema() -> dict:
30-
"""Load and return the config JSON schema."""
31-
schema_file = importlib.resources.files("modelpack.v1").joinpath(
32-
"config-schema.json"
30+
"""Load and return the config JSON schema from the repo root."""
31+
schema_path = (
32+
Path(__file__).resolve().parent.parent.parent.parent
33+
/ "schema"
34+
/ "config-schema.json"
3335
)
34-
with schema_file.open(encoding="utf-8") as f:
36+
with schema_path.open(encoding="utf-8") as f:
3537
return json.load(f)
3638

3739

@@ -51,4 +53,4 @@ def validate_config(data: dict | str) -> None:
5153

5254
schema = _load_schema()
5355
format_checker = FormatChecker()
54-
Draft202012Validator(schema, format_checker=format_checker).validate(data)
56+
Draft4Validator(schema, format_checker=format_checker).validate(data)

0 commit comments

Comments
 (0)