77from dataclasses import dataclass
88from datetime import datetime , timezone
99from pathlib import Path
10- from typing import Dict , Optional , cast
10+ from typing import Dict , Optional , Union , cast
1111
1212import tomlkit
1313from tomlkit .items import Item
@@ -65,46 +65,45 @@ def update_infra_sync_time(self) -> None:
6565 self .latest_infra_sync_time = datetime .now (timezone .utc )
6666
6767
68- def _parse_datetime_from_toml ( datetime_str : str ) -> datetime :
68+ def _parse_time_from_toml ( time_value : Union [ str , int , float , datetime ] ) -> Optional [ datetime ] :
6969 """
70- Parse datetime string from TOML file and ensure it's timezone-aware UTC .
70+ Parse time from TOML file - supports both epoch and ISO format .
7171
72- Handles three formats:
73- 1. With explicit timezone: "2024-05-08T15:16:43+00:00"
74- 2. Without timezone: "2024-05-08T15:16:43"
75- 3. With Z suffix (Zulu time): "2024-05-08T15:16:43Z"
76-
77- Python 3.9/3.10 don't support 'Z' suffix in fromisoformat(), so we convert it.
72+ Handles legacy ISO format strings as sam migrat to using epoch timestamps for backward compatibility.
73+ New writes always use epoch format.
7874
7975 Parameters
8076 ----------
81- datetime_str: str
82- ISO format datetime string from TOML file
77+ time_value: Union[ str, int, float, datetime]
78+ Either epoch timestamp (new format), ISO format string (legacy), or datetime object
8379
8480 Returns
8581 -------
86- datetime
87- Timezone-aware datetime in UTC
88-
89- Raises
90- ------
91- ValueError
92- If datetime_str is not a valid ISO format datetime string
82+ Optional[datetime]
83+ Timezone-aware datetime in UTC, or None if parsing fails
9384 """
9485 try :
95- # Handle 'Z' suffix for UTC timezone (Python 3.9/3.10 compatibility)
96- if datetime_str .endswith ("Z" ):
97- datetime_str = datetime_str [:- 1 ] + "+00:00"
86+ if isinstance (time_value , datetime ):
87+ return time_value if time_value .tzinfo else time_value .replace (tzinfo = timezone .utc )
9888
99- parsed_datetime = datetime .fromisoformat (datetime_str )
89+ if isinstance (time_value , (int , float )):
90+ return datetime .fromtimestamp (time_value , tz = timezone .utc )
10091
101- # Ensure timezone-aware (handles old sync.toml files without timezone)
102- if parsed_datetime .tzinfo is None :
103- parsed_datetime = parsed_datetime .replace (tzinfo = timezone .utc )
92+ if isinstance (time_value , str ):
93+ if time_value .endswith ("Z" ):
94+ time_value = time_value [:- 1 ] + "+00:00"
95+ parsed = datetime .fromisoformat (time_value )
96+ return parsed if parsed .tzinfo else parsed .replace (tzinfo = timezone .utc )
10497
105- return parsed_datetime
106- except (ValueError , AttributeError ) as e :
107- raise ValueError (f"Invalid datetime format in sync.toml: '{ datetime_str } '" ) from e
98+ LOG .warning ("Invalid time format in sync.toml: %s. Triggering full CloudFormation deployment." , time_value )
99+ return None
100+ except (ValueError , OSError ) as e :
101+ LOG .warning (
102+ "Failed to parse timestamp from sync.toml: %s. Triggering full CloudFormation deployment. Error: %s" ,
103+ time_value ,
104+ e ,
105+ )
106+ return None
108107
109108
110109def _sync_state_to_toml_document (sync_state : SyncState ) -> TOMLDocument :
@@ -124,7 +123,7 @@ def _sync_state_to_toml_document(sync_state: SyncState) -> TOMLDocument:
124123 sync_state_toml_table = tomlkit .table ()
125124 sync_state_toml_table [DEPENDENCY_LAYER ] = sync_state .dependency_layer
126125 if sync_state .latest_infra_sync_time :
127- sync_state_toml_table [LATEST_INFRA_SYNC_TIME ] = sync_state .latest_infra_sync_time .isoformat ()
126+ sync_state_toml_table [LATEST_INFRA_SYNC_TIME ] = sync_state .latest_infra_sync_time .timestamp ()
128127
129128 resource_sync_states_toml_table = tomlkit .table ()
130129 for resource_id in sync_state .resource_sync_states :
@@ -133,7 +132,7 @@ def _sync_state_to_toml_document(sync_state: SyncState) -> TOMLDocument:
133132 resource_sync_state_toml_table = tomlkit .table ()
134133
135134 resource_sync_state_toml_table [HASH ] = resource_sync_state .hash_value
136- resource_sync_state_toml_table [SYNC_TIME ] = resource_sync_state .sync_time .isoformat ()
135+ resource_sync_state_toml_table [SYNC_TIME ] = resource_sync_state .sync_time .timestamp ()
137136
138137 # For Nested stack resources, replace "/" with "-"
139138 resource_id_toml = resource_id .replace ("/" , "-" )
@@ -173,7 +172,10 @@ def _toml_document_to_sync_state(toml_document: Dict) -> Optional[SyncState]:
173172 resource_sync_state_toml_table = resource_sync_states_toml_table .get (resource_id )
174173 sync_time_str = resource_sync_state_toml_table .get (SYNC_TIME )
175174 # Parse datetime and ensure it's timezone-aware UTC (consistent with how we write)
176- sync_time = _parse_datetime_from_toml (sync_time_str )
175+ sync_time = _parse_time_from_toml (sync_time_str )
176+ if sync_time is None :
177+ # Skip this resource if timestamp is invalid - resource will be re-synced on next sam sync
178+ continue
177179 resource_sync_state = ResourceSyncState (
178180 resource_sync_state_toml_table .get (HASH ),
179181 sync_time ,
@@ -189,8 +191,7 @@ def _toml_document_to_sync_state(toml_document: Dict) -> Optional[SyncState]:
189191 dependency_layer = sync_state_toml_table .get (DEPENDENCY_LAYER )
190192 latest_infra_sync_time_str = sync_state_toml_table .get (LATEST_INFRA_SYNC_TIME )
191193 if latest_infra_sync_time_str :
192- # Parse datetime and ensure it's timezone-aware UTC (consistent with how we write)
193- latest_infra_sync_time = _parse_datetime_from_toml (str (latest_infra_sync_time_str ))
194+ latest_infra_sync_time = _parse_time_from_toml (latest_infra_sync_time_str )
194195 sync_state = SyncState (dependency_layer , resource_sync_states , latest_infra_sync_time )
195196
196197 return sync_state
0 commit comments