Skip to content

Commit 0468f7b

Browse files
committed
Add setters to some provenance fields
Most of the fields of the e3.slsa.provenance classes are read-only. Some of those may be set at provenance creation time, but (for instance) the RunDetails started_on and finished_on should be writable. Here is the list of fields which should be writable - `Builder.id` if the provenance is created before the build ID is actually created - `BuildMetadata.finished_on`, or the builds needs to be finished when the provenance is created - BuildMetadata.started_on, to allow setting it at real build start time. Closes #43+
1 parent 43770eb commit 0468f7b

File tree

2 files changed

+45
-5
lines changed

2 files changed

+45
-5
lines changed

src/e3/slsa/provenance.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ def id(self) -> TypeURI:
149149
""" # noqa RST304
150150
return self.__id
151151

152+
@id.setter
153+
def id(self, value: str | TypeURI) -> None:
154+
self.__id = value if isinstance(value, TypeURI) else TypeURI(value)
155+
152156
@property
153157
def version(self) -> dict[str, str]:
154158
"""Builder version mapping.
@@ -279,6 +283,10 @@ def finished_on(self) -> datetime:
279283
"""The timestamp of when the build completed."""
280284
return self.__finished_on
281285

286+
@finished_on.setter
287+
def finished_on(self, value: datetime | str) -> None:
288+
self.__finished_on = self.__validate_timestamp(value)
289+
282290
@property
283291
def invocation_id(self) -> str:
284292
"""Build invocation identifier.
@@ -299,6 +307,10 @@ def started_on(self) -> datetime:
299307
"""The timestamp of when the build started."""
300308
return self.__started_on
301309

310+
@started_on.setter
311+
def started_on(self, value: datetime | str) -> None:
312+
self.__started_on = self.__validate_timestamp(value)
313+
302314
# --------------------------- Public methods ---------------------------- #
303315

304316
def as_dict(self) -> dict:
@@ -380,13 +392,20 @@ def load_json(cls, initializer: str) -> BuildMetadata:
380392
# --------------------------- Private methods --------------------------- #
381393

382394
@staticmethod
383-
def __validate_timestamp(timestamp: datetime) -> datetime:
395+
def __validate_timestamp(timestamp: datetime | str) -> datetime:
384396
"""Validate a timestamp."""
385-
valid_timestamp: datetime
386-
if isinstance(timestamp, datetime):
397+
valid_timestamp: datetime | None = None
398+
if isinstance(timestamp, str):
399+
valid_timestamp = date_parser.parse(timestamp)
400+
elif isinstance(timestamp, datetime):
401+
valid_timestamp = timestamp
402+
403+
if isinstance(valid_timestamp, datetime):
387404
# When converting to JSON representation, the microseconds
388405
# are lost. Just remove them.
389-
valid_timestamp = timestamp.astimezone(timezone.utc).replace(microsecond=0)
406+
valid_timestamp = valid_timestamp.astimezone(timezone.utc).replace(
407+
microsecond=0
408+
)
390409
else:
391410
raise TypeError(f"Invalid timestamp type {type(timestamp)}")
392411

tests/tests_e3/slsa/provenance_test.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def test_builder_as_dict() -> None:
237237
dict_dep: dict = desc.as_dict()
238238
assert json.dumps(dict_repr, indent=" ") != ""
239239
assert dict_repr.get(Builder.ATTR_BUILD_ID) == build_id
240-
assert dict_repr.get(Builder.ATTR_BUILDER_DEPENDENCIES)[0] == dict_dep
240+
assert dict_repr.get(Builder.ATTR_BUILDER_DEPENDENCIES, [])[0] == dict_dep
241241
assert dict_repr.get(Builder.ATTR_VERSION) == version
242242

243243

@@ -264,6 +264,13 @@ def test_builder_init() -> None:
264264
assert builder.version == version
265265
# Test the __eq__ method with a wrong type.
266266
assert builder != {}
267+
# Test setting the builder ID
268+
builder_id: str = "protocol://path/file"
269+
builder.id = builder_id
270+
builder.id = TypeURI(builder_id)
271+
with pytest.raises(ValueError) as invalid_builder_id:
272+
builder.id = None
273+
assert "Invalid URI None" in invalid_builder_id.value.args[0]
267274

268275

269276
def test_builder_load_dict() -> None:
@@ -333,6 +340,20 @@ def test_buildmetadata_init() -> None:
333340
invocation_id=invocation_id, started_on=None, finished_on=finish_time
334341
)
335342
assert "Invalid timestamp type" in invalid_timestamp_type.value.args[0]
343+
# Set start and finish times with datetime, string or invalid values.
344+
test_time: str = "2025-04-16T07:32:19Z"
345+
bm.started_on = test_time
346+
bm.started_on = date_parser.parse(test_time)
347+
with pytest.raises(TypeError) as invalid_timestamp_type:
348+
# noinspection PyTypeChecker
349+
bm.started_on = None
350+
assert "Invalid timestamp type" in invalid_timestamp_type.value.args[0]
351+
bm.finished_on = test_time
352+
bm.finished_on = date_parser.parse(test_time)
353+
with pytest.raises(TypeError) as invalid_timestamp_type:
354+
# noinspection PyTypeChecker
355+
bm.finished_on = None
356+
assert "Invalid timestamp type" in invalid_timestamp_type.value.args[0]
336357

337358

338359
def test_buildmetadata_load_dict() -> None:

0 commit comments

Comments
 (0)