44import re
55import string
66import time
7- from datetime import timedelta , datetime
7+ from datetime import timedelta , datetime , timezone
88from elasticsearch8 .exceptions import NotFoundError
99from curator .exceptions import ConfigurationError
1010from curator .defaults .settings import date_regex
@@ -43,6 +43,7 @@ def get_epoch(self, searchme):
4343 timestamp = match .group ("date" )
4444 return datetime_to_epoch (get_datetime (timestamp , self .timestring ))
4545
46+
4647def absolute_date_range (
4748 unit , date_from , date_to ,
4849 date_from_format = None , date_to_format = None
@@ -77,7 +78,7 @@ def absolute_date_range(
7778 raise ConfigurationError ('Must provide "date_from_format" and "date_to_format"' )
7879 try :
7980 start_epoch = datetime_to_epoch (get_datetime (date_from , date_from_format ))
80- logger .debug ('Start ISO8601 = %s' , datetime . utcfromtimestamp (start_epoch ). isoformat ( ))
81+ logger .debug ('Start ISO8601 = %s' , epoch2iso (start_epoch ))
8182 except Exception as err :
8283 raise ConfigurationError (
8384 f'Unable to parse "date_from" { date_from } and "date_from_format" { date_from_format } . '
@@ -116,7 +117,7 @@ def absolute_date_range(
116117 end_epoch = get_point_of_reference (
117118 unit , - 1 , epoch = datetime_to_epoch (end_date )) - 1
118119
119- logger .debug ('End ISO8601 = %s' , datetime . utcfromtimestamp (end_epoch ). isoformat ( ))
120+ logger .debug ('End ISO8601 = %s' , epoch2iso (end_epoch ))
120121 return (start_epoch , end_epoch )
121122
122123def date_range (unit , range_from , range_to , epoch = None , week_starts_on = 'sunday' ):
@@ -152,7 +153,7 @@ def date_range(unit, range_from, range_to, epoch=None, week_starts_on='sunday'):
152153 if not epoch :
153154 epoch = time .time ()
154155 epoch = fix_epoch (epoch )
155- raw_point_of_ref = datetime .utcfromtimestamp (epoch )
156+ raw_point_of_ref = datetime .fromtimestamp (epoch , timezone . utc )
156157 logger .debug ('Raw point of Reference = %s' , raw_point_of_ref )
157158 # Reverse the polarity, because -1 as last week makes sense when read by
158159 # humans, but datetime timedelta math makes -1 in the future.
@@ -208,7 +209,7 @@ def date_range(unit, range_from, range_to, epoch=None, week_starts_on='sunday'):
208209 start_date = point_of_ref - start_delta
209210 # By this point, we know our start date and can convert it to epoch time
210211 start_epoch = datetime_to_epoch (start_date )
211- logger .debug ('Start ISO8601 = %s' , datetime . utcfromtimestamp (start_epoch ). isoformat ( ))
212+ logger .debug ('Start ISO8601 = %s' , epoch2iso (start_epoch ))
212213 # This is the number of units we need to consider.
213214 count = (range_to - range_from ) + 1
214215 # We have to iterate to one more month, and then subtract a second to get
@@ -234,7 +235,7 @@ def date_range(unit, range_from, range_to, epoch=None, week_starts_on='sunday'):
234235 # to get hours, days, or weeks, as they don't change
235236 end_epoch = get_point_of_reference (
236237 unit , count * - 1 , epoch = start_epoch ) - 1
237- logger .debug ('End ISO8601 = %s' , datetime . utcfromtimestamp (end_epoch ). isoformat ( ))
238+ logger .debug ('End ISO8601 = %s' , epoch2iso (end_epoch ))
238239 return (start_epoch , end_epoch )
239240
240241def datetime_to_epoch (mydate ):
@@ -247,9 +248,53 @@ def datetime_to_epoch(mydate):
247248 :returns: An epoch timestamp based on ``mydate``
248249 :rtype: int
249250 """
250- tdelta = ( mydate - datetime (1970 , 1 , 1 ) )
251+ tdelta = mydate - datetime (1970 , 1 , 1 )
251252 return tdelta .seconds + tdelta .days * 24 * 3600
252253
254+ def epoch2iso (epoch : int ) -> str :
255+ """
256+ Return an ISO8601 value for epoch
257+
258+ :param epoch: An epoch timestamp
259+ :type epoch: int
260+
261+ :returns: An ISO8601 timestamp
262+ :rtype: str
263+ """
264+ # Because Python 3.12 now requires non-naive timezone declarations, we must change.
265+ #
266+ ### Example:
267+ ### epoch == 1491256800
268+ ###
269+ ### The old way:
270+ ###datetime.utcfromtimestamp(epoch)
271+ ### datetime.datetime(2017, 4, 3, 22, 0).isoformat()
272+ ### Result: 2017-04-03T22:00:00
273+ ###
274+ ### The new way:
275+ ### datetime.fromtimestamp(epoch, timezone.utc)
276+ ### datetime.datetime(2017, 4, 3, 22, 0, tzinfo=datetime.timezone.utc).isoformat()
277+ ### Result: 2017-04-03T22:00:00+00:00
278+ ###
279+ ### End Example
280+ #
281+ # Note that the +00:00 is appended now where we affirmatively declare the UTC timezone
282+ #
283+ # As a result, we will use this function to prune away the timezone if it is +00:00 and replace
284+ # it with Z, which is shorter Zulu notation for UTC (which Elasticsearch uses)
285+ #
286+ # We are MANUALLY, FORCEFULLY declaring timezone.utc, so it should ALWAYS be +00:00, but could
287+ # in theory sometime show up as a Z, so we test for that.
288+
289+ parts = datetime .fromtimestamp (epoch , timezone .utc ).isoformat ().split ('+' )
290+ if len (parts ) == 1 :
291+ if parts [0 ][- 1 ] == 'Z' :
292+ return parts [0 ] # Our ISO8601 already ends with a Z for Zulu/UTC time
293+ return f'{ parts [0 ]} Z' # It doesn't end with a Z so we put one there
294+ if parts [1 ] == '00:00' :
295+ return f'{ parts [0 ]} Z' # It doesn't end with a Z so we put one there
296+ return f'{ parts [0 ]} +{ parts [1 ]} ' # Fallback publishes the +TZ, whatever that was
297+
253298def fix_epoch (epoch ):
254299 """
255300 Fix value of ``epoch`` to be the count since the epoch in seconds only, which should be 10 or
@@ -537,7 +582,7 @@ def parse_date_pattern(name):
537582 if char == '%' :
538583 pass
539584 elif char in date_regex () and prev == '%' :
540- rendered += str (datetime .utcnow ( ).strftime (f'%{ char } ' ))
585+ rendered += str (datetime .now ( timezone . utc ).strftime (f'%{ char } ' ))
541586 else :
542587 rendered += char
543588 logger .debug ('Partially rendered name: %s' , rendered )
0 commit comments