@@ -69,9 +69,7 @@ class Metadata():
6969 ]
7070
7171 """
72- def __init__ (
73- self , signed : 'Signed' = None , signatures : list = None ) -> None :
74- # TODO: How much init magic do we want?
72+ def __init__ (self , signed : 'Signed' , signatures : list ) -> None :
7573 self .signed = signed
7674 self .signatures = signatures
7775
@@ -167,18 +165,37 @@ def from_json_file(
167165
168166 Raises:
169167 securesystemslib.exceptions.StorageError: The file cannot be read.
170- securesystemslib.exceptions.Error, ValueError: The metadata cannot
171- be parsed.
168+ securesystemslib.exceptions.Error, ValueError, KeyError : The
169+ metadata cannot be parsed.
172170
173171 Returns:
174172 A TUF Metadata object.
175173
176174 """
177- signable = load_json_file (filename , storage_backend )
175+ return cls .from_dict (load_json_file (filename , storage_backend ))
176+
177+ @classmethod
178+ def from_dict (cls , metadata : JsonDict ) -> 'Metadata' :
179+ """Creates Metadata object from its JSON/dict representation.
180+
181+ Calls 'from_dict' for any complex metadata attribute represented by a
182+ class also that has a 'from_dict' factory method. (Currently this is
183+ only the signed attribute.)
184+
185+ Arguments:
186+ metadata: TUF metadata in JSON/dict representation, as e.g.
187+ returned by 'json.loads'.
188+
189+ Raises:
190+ KeyError: The metadata dict format is invalid.
191+ ValueError: The metadata has an unrecognized signed._type field.
178192
179- # TODO: Should we use constants?
180- # And/or maybe a dispatch table? (<-- maybe too much magic)
181- _type = signable ['signed' ]['_type' ]
193+ Returns:
194+ A TUF Metadata object.
195+
196+ """
197+ # Dispatch to contained metadata class on metadata _type field.
198+ _type = metadata ['signed' ]['_type' ]
182199
183200 if _type == 'targets' :
184201 inner_cls = Targets
@@ -192,9 +209,13 @@ def from_json_file(
192209 else :
193210 raise ValueError (f'unrecognized metadata type "{ _type } "' )
194211
195- return Metadata (
196- signed = inner_cls (** signable ['signed' ]),
197- signatures = signable ['signatures' ])
212+ # NOTE: If Signature becomes a class, we should iterate over
213+ # metadata['signatures'], call Signature.from_dict for each item, and
214+ # pass a list of Signature objects to the Metadata constructor intead.
215+ return cls (
216+ signed = inner_cls .from_dict (metadata ['signed' ]),
217+ signatures = metadata ['signatures' ])
218+
198219
199220 def to_json_file (
200221 self , filename : str , compact : bool = False ,
@@ -236,41 +257,48 @@ class Signed:
236257 # we keep it to match spec terminology (I often refer to this as "payload",
237258 # or "inner metadata")
238259
239- # TODO: Re-think default values. It might be better to pass some things
240- # as args and not es kwargs. Then we'd need to pop those from
241- # signable["signed"] in read_from_json and pass them explicitly, which
242- # some say is better than implicit. :)
243260 def __init__ (
244- self , _type : str = None , version : int = 0 ,
245- spec_version : str = None , expires : datetime = None
246- ) -> None :
247- # TODO: How much init magic do we want?
261+ self , _type : str , version : int , spec_version : str ,
262+ expires : datetime ) -> None :
248263
249264 self ._type = _type
265+ self .version = version
250266 self .spec_version = spec_version
267+ self .expires = expires
251268
252- # We always intend times to be UTC
253- # NOTE: we could do this with datetime.fromisoformat() but that is not
254- # available in Python 2.7's datetime
255- # NOTE: Store as datetime object for convenient handling, use 'expires'
256- # property to get the TUF metadata format representation
257- self .__expiration = iso8601 .parse_date (expires ).replace (tzinfo = None )
258-
269+ # TODO: Should we separate data validation from constructor?
259270 if version < 0 :
260271 raise ValueError (f'version must be < 0, got { version } ' )
261272 self .version = version
262273
274+ @classmethod
275+ def from_dict (cls , signed_dict ) -> 'Signed' :
276+ """Creates Signed object from its JSON/dict representation. """
277+
278+ # Convert 'expires' TUF metadata string to a datetime object, which is
279+ # what the constructor expects and what we store. The inverse operation
280+ # is implemented in 'to_dict'.
281+ signed_dict ['expires' ] = iso8601 .parse_date (
282+ signed_dict ['expires' ]).replace (tzinfo = None )
283+ # NOTE: We write the converted 'expires' back into 'signed_dict' above
284+ # so that we can pass it to the constructor as '**signed_dict' below,
285+ # along with other fields that belong to Signed subclasses.
286+ # Any 'from_dict'(-like) conversions of fields that correspond to a
287+ # subclass should be performed in the 'from_dict' method of that
288+ # subclass and also be written back into 'signed_dict' before calling
289+ # super().from_dict.
290+
291+ # NOTE: cls might be a subclass of Signed, if 'from_dict' was called on
292+ # that subclass (see e.g. Metadata.from_dict).
293+ return cls (** signed_dict )
263294
264- @property
265- def expires (self ) -> str :
266- return self .__expiration .isoformat () + 'Z'
267295 def to_canonical_bytes (self ) -> bytes :
268296 """Returns the UTF-8 encoded canonical JSON representation of self. """
269297 return encode_canonical (self .to_dict ()).encode ('UTF-8' )
270298
271299 def bump_expiration (self , delta : timedelta = timedelta (days = 1 )) -> None :
272300 """Increments the expires attribute by the passed timedelta. """
273- self .__expiration = self . __expiration + delta
301+ self .expires += delta
274302
275303 def bump_version (self ) -> None :
276304 """Increments the metadata version number by 1."""
@@ -282,7 +310,7 @@ def to_dict(self) -> JsonDict:
282310 '_type' : self ._type ,
283311 'version' : self .version ,
284312 'spec_version' : self .spec_version ,
285- 'expires' : self .expires
313+ 'expires' : self .expires . isoformat () + 'Z'
286314 }
287315
288316class Timestamp (Signed ):
@@ -304,10 +332,11 @@ class Timestamp(Signed):
304332 }
305333
306334 """
307- def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
308- super ().__init__ (** kwargs )
309- # TODO: How much init magic do we want?
310- # TODO: Is there merit in creating classes for dict fields?
335+ def __init__ (
336+ self , _type : str , version : int , spec_version : str ,
337+ expires : datetime , meta : JsonDict ) -> None :
338+ super ().__init__ (_type , version , spec_version , expires )
339+ # TODO: Add class for meta
311340 self .meta = meta
312341
313342 def to_dict (self ) -> JsonDict :
@@ -353,10 +382,11 @@ class Snapshot(Signed):
353382 }
354383
355384 """
356- def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
357- # TODO: How much init magic do we want?
358- # TODO: Is there merit in creating classes for dict fields?
359- super ().__init__ (** kwargs )
385+ def __init__ (
386+ self , _type : str , version : int , spec_version : str ,
387+ expires : datetime , meta : JsonDict ) -> None :
388+ super ().__init__ (_type , version , spec_version , expires )
389+ # TODO: Add class for meta
360390 self .meta = meta
361391
362392 def to_dict (self ) -> JsonDict :
@@ -436,14 +466,15 @@ class Targets(Signed):
436466
437467 """
438468 def __init__ (
439- self , targets : JsonDict = None , delegations : JsonDict = None ,
440- ** kwargs ) -> None :
441- # TODO: How much init magic do we want?
442- # TODO: Is there merit in creating classes for dict fields?
443- super (). __init__ ( ** kwargs )
469+ self , _type : str , version : int , spec_version : str ,
470+ expires : datetime , targets : JsonDict , delegations : JsonDict
471+ ) -> None :
472+ super (). __init__ ( _type , version , spec_version , expires )
473+ # TODO: Add class for meta
444474 self .targets = targets
445475 self .delegations = delegations
446476
477+
447478 def to_dict (self ) -> JsonDict :
448479 """Returns the JSON-serializable dictionary representation of self. """
449480 json_dict = super ().to_dict ()
0 commit comments