|  | @@ -39,6 +39,12 @@ import msgpack.fallback
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import socket
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +# never use datetime.isoformat(), it is evil. always use one of these:
 | 
	
		
			
				|  |  | +# datetime.strftime(ISO_FORMAT)  # output always includes .microseconds
 | 
	
		
			
				|  |  | +# datetime.strftime(ISO_FORMAT_NO_USECS)  # output never includes microseconds
 | 
	
		
			
				|  |  | +ISO_FORMAT_NO_USECS = '%Y-%m-%dT%H:%M:%S'
 | 
	
		
			
				|  |  | +ISO_FORMAT = ISO_FORMAT_NO_USECS + '.%f'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  # 20 MiB minus 41 bytes for a Repository header (because the "size" field in the Repository includes
 | 
	
		
			
				|  |  |  # the header, and the total size was set to 20 MiB).
 | 
	
		
			
				|  |  |  MAX_DATA_SIZE = 20971479
 | 
	
	
		
			
				|  | @@ -306,11 +312,11 @@ class Manifest:
 | 
	
		
			
				|  |  |              self.config[b'tam_required'] = True
 | 
	
		
			
				|  |  |          # self.timestamp needs to be strictly monotonically increasing. Clocks often are not set correctly
 | 
	
		
			
				|  |  |          if self.timestamp is None:
 | 
	
		
			
				|  |  | -            self.timestamp = datetime.utcnow().isoformat()
 | 
	
		
			
				|  |  | +            self.timestamp = datetime.utcnow().strftime(ISO_FORMAT)
 | 
	
		
			
				|  |  |          else:
 | 
	
		
			
				|  |  | -            prev_ts = datetime.strptime(self.timestamp, "%Y-%m-%dT%H:%M:%S.%f")
 | 
	
		
			
				|  |  | -            incremented = (prev_ts + timedelta(microseconds=1)).isoformat()
 | 
	
		
			
				|  |  | -            self.timestamp = max(incremented, datetime.utcnow().isoformat())
 | 
	
		
			
				|  |  | +            prev_ts = parse_timestamp(self.timestamp, tzinfo=None)
 | 
	
		
			
				|  |  | +            incremented = (prev_ts + timedelta(microseconds=1)).strftime(ISO_FORMAT)
 | 
	
		
			
				|  |  | +            self.timestamp = max(incremented, datetime.utcnow().strftime(ISO_FORMAT))
 | 
	
		
			
				|  |  |          # include checks for limits as enforced by limited unpacker (used by load())
 | 
	
		
			
				|  |  |          assert len(self.archives) <= MAX_ARCHIVES
 | 
	
		
			
				|  |  |          assert all(len(name) <= 255 for name in self.archives)
 | 
	
	
		
			
				|  | @@ -485,12 +491,13 @@ def to_localtime(ts):
 | 
	
		
			
				|  |  |      return datetime(*time.localtime((ts - datetime(1970, 1, 1, tzinfo=timezone.utc)).total_seconds())[:6])
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -def parse_timestamp(timestamp):
 | 
	
		
			
				|  |  | +def parse_timestamp(timestamp, tzinfo=timezone.utc):
 | 
	
		
			
				|  |  |      """Parse a ISO 8601 timestamp string"""
 | 
	
		
			
				|  |  | -    if '.' in timestamp:  # microseconds might not be present
 | 
	
		
			
				|  |  | -        return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f').replace(tzinfo=timezone.utc)
 | 
	
		
			
				|  |  | -    else:
 | 
	
		
			
				|  |  | -        return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
 | 
	
		
			
				|  |  | +    fmt = ISO_FORMAT if '.' in timestamp else ISO_FORMAT_NO_USECS
 | 
	
		
			
				|  |  | +    dt = datetime.strptime(timestamp, fmt)
 | 
	
		
			
				|  |  | +    if tzinfo is not None:
 | 
	
		
			
				|  |  | +        dt = dt.replace(tzinfo=tzinfo)
 | 
	
		
			
				|  |  | +    return dt
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  def load_excludes(fh):
 |