-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathtime.py
More file actions
439 lines (350 loc) · 16.4 KB
/
Copy pathtime.py
File metadata and controls
439 lines (350 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
#! /usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author: Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
# https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.
# https://github.com/Linuxfabrik/lib/blob/main/CONTRIBUTING.md
"""Provides datetime functions."""
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2026082901'
import datetime
import re
import time
try:
import zoneinfo
except ImportError:
# zoneinfo is part of the standard library since Python 3.9. On older
# interpreters (such as the system Python 3.6 on RHEL 8 / Rocky 8) it is
# missing; degrade to UTC in get_timezone() instead of failing at import,
# so consumers that do not need named time zones keep working.
zoneinfo = None
def epoch2iso(timestamp):
"""
Converts a UNIX epoch timestamp to an ISO-formatted date and time string.
This function takes a UNIX timestamp (int or float) and returns a string representing the local
time in ISO 8601 format (YYYY-MM-DD HH:MM:SS).
### Parameters
- **timestamp** (`int` or `float`): UNIX epoch timestamp (seconds since 1970-01-01).
### Returns
- **str**: Local date and time in ISO 8601 format.
### Example
>>> epoch2iso(1620459129)
'2021-05-08 09:32:09'
"""
try:
ts = float(timestamp)
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts))
except (TypeError, ValueError):
return ''
def get_timezone(tz_name):
"""
Load an IANA time-zone by name and return a ZoneInfo object, defaulting to UTC if invalid.
This function takes an IANA time-zone name (str) and returns the corresponding
zoneinfo.ZoneInfo object. If loading fails, UTC ("Etc/UTC") is returned.
### Parameters
- **tz_name** (`str`): IANA time-zone identifier (e.g. "Europe/London").
### Returns
- **ZoneInfo**: A `zoneinfo.ZoneInfo` object for the requested zone, or UTC if not found.
### Example
>>> get_timezone('Europe/London').key
'Europe/London'
>>> get_timezone('Invalid/Zone').key
'Etc/UTC'
"""
if zoneinfo is None:
# Python < 3.9 without the zoneinfo backport
return datetime.timezone.utc
try:
return zoneinfo.ZoneInfo(tz_name)
except Exception:
# Fallback to UTC if the name isn't found
try:
return zoneinfo.ZoneInfo('Etc/UTC')
except Exception:
return datetime.timezone.utc
def get_weekday(epoch):
"""
Convert a UNIX epoch timestamp to a lowercase three-letter weekday abbreviation.
This function takes a UNIX timestamp (int or float) and returns the local weekday
as a three-letter lowercase string: 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', or 'sun'.
### Parameters
- **epoch** (`int` or `float`): UNIX epoch timestamp (seconds since 1970-01-01).
### Returns
- **str**: Lowercase three-letter abbreviation of the weekday corresponding to the local date.
### Example
>>> get_weekday(1620459129)
'sat'
"""
_WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
return _WEEKDAYS[time.localtime(epoch).tm_wday]
_MACRO_COMPONENT = re.compile(r'\{%([YymdHMS])\}')
def macro2timestr(s, format=''):
"""
Expand time macros in a string.
Supported macros (case-sensitive, strftime-faithful):
- `{today}` → current date, formatted with `format` (default `%Y-%m-%d`).
- `{yesterday}` → current date minus one day, formatted with `format`.
- `{%Y}` / `{%y}` → 4-digit / 2-digit year of `now`.
- `{%m}` → month of `now` (01-12).
- `{%d}` → day of `now` (01-31).
- `{%H}` → hour of `now` (00-23).
- `{%M}` → minute of `now` (00-59).
- `{%S}` → second of `now` (00-59).
Unknown tokens are passed through unchanged.
### Parameters
- **s** (`str`): Template string.
- **format** (`str`, optional): strftime pattern used for `{today}`
and `{yesterday}`. Defaults to ISO 8601 date `%Y-%m-%d`.
### Returns
- **str**: `s` with all recognised macros replaced.
### Example
>>> # Assuming today is 2026-04-22:
>>> macro2timestr('/var/log/laravel/laravel-{today}.log')
'/var/log/laravel/laravel-2026-04-22.log'
>>> macro2timestr('C:\\\\logs\\\\{%Y}{%m}{%d}.log')
'C:\\\\logs\\\\20260422.log'
>>> macro2timestr('{today}', format='%Y%m%d')
'20260422'
>>> macro2timestr('{yesterday}', format='%d.%m.%Y')
'21.04.2026'
"""
default_format = format or '%Y-%m-%d'
base = now(as_type='datetime')
result = s.replace('{today}', base.strftime(default_format))
result = result.replace(
'{yesterday}',
(base - datetime.timedelta(days=1)).strftime(default_format),
)
return _MACRO_COMPONENT.sub(
lambda match: base.strftime('%' + match.group(1)),
result,
)
def now(as_type=''):
"""
Returns the current date and time in various formats.
Depending on `as_type`, this returns:
- Integer UNIX epoch (default)
- Floating-point UNIX epoch ('float')
- datetime.datetime object in local time ('datetime')
- datetime.datetime object in UTC, naive ('utc')
- ISO string 'YYYY-MM-DD HH:MM:SS' in local time ('iso')
Use 'utc' for fields that are defined as UTC by spec (x509
`notBefore` / `notAfter`, HTTP `Date`, RFC 3339 timestamps, ...).
Returned as naive (no tzinfo) so it drops in wherever the
callee expects a naive datetime.
### Parameters
- **as_type** (`str`, optional):
'', 'epoch', 'float', 'datetime', 'utc' or 'iso'. Defaults to ''.
### Returns
- **int**, **float**, **datetime.datetime**, or **str**: Current time in the requested format.
### Example
>>> now()
1586422786
>>> now(as_type='float')
1586422786.1521912
>>> now(as_type='datetime')
datetime.datetime(2020, 4, 9, 11, 1, 41, 228752)
>>> now(as_type='utc')
datetime.datetime(2020, 4, 9, 9, 1, 41, 228752)
>>> now(as_type='iso')
'2020-04-09 11:31:24'
"""
if as_type == 'datetime':
return datetime.datetime.now()
if as_type == 'utc':
return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
if as_type == 'iso':
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if as_type == 'float':
return time.time()
return int(time.time())
# Matches the fractional seconds of an ISO 8601 / RFC 3339 timestamp. A date or
# time never carries another dot, so the first match is the fraction.
_ISO8601_FRACTION_REGEX = re.compile(r'\.(\d+)')
def _normalize_iso8601_fraction(timestr):
"""
Rewrites the fractional seconds of an ISO 8601 string to exactly six digits.
`datetime.fromisoformat()` accepts only three or six fractional digits before Python 3.11,
while tools written in Go emit RFC 3339 with nanosecond precision and drop trailing zeros
(`time.RFC3339Nano`). Both ends of that range therefore fail on the interpreters shipped with
RHEL 9 and RHEL 8: `.076976146` has too many digits, `.07` too few. Padding and trimming to
six digits keeps the value within a microsecond and makes the string parse everywhere.
### Parameters
- **timestr** (`str`): An ISO 8601 / RFC 3339 timestamp, with or without fractional seconds.
### Returns
- **str**: The timestamp with its fractional seconds normalized, unchanged if it carries none.
### Notes
- Verified against Python 3.9, 3.10, 3.11 and 3.14: only 3.11 and newer accept the raw
nanosecond form.
"""
return _ISO8601_FRACTION_REGEX.sub(
lambda match: '.' + match.group(1)[:6].ljust(6, '0'),
timestr,
count=1,
)
# Matches a UTC offset written without the colon `fromisoformat()` wants before
# Python 3.11, at the very end of the string. ISO 8601 allows both `+0200` and
# `+02:00`, and `journalctl --output=short-iso` writes the former.
_ISO8601_OFFSET_REGEX = re.compile(r'([+-])(\d{2})(\d{2})$')
def _normalize_iso8601_offset(timestr):
"""Insert the colon into a `+hhmm` offset, which `fromisoformat()` needs before 3.11.
Leaves a value that already carries the colon, one that ends in `Z`, and one without an
offset at all untouched.
"""
return _ISO8601_OFFSET_REGEX.sub(r'\1\2:\3', timestr, count=1)
# The trailing offset in the form `fromisoformat()` wants, for taking a value
# apart by hand where that method does not exist yet.
_ISO8601_COLON_OFFSET_REGEX = re.compile(r'([+-])(\d{2}):(\d{2})$')
def _fromisoformat(iso):
"""`datetime.fromisoformat()`, hand-rolled where the interpreter has none.
Python 3.6 - which RHEL 8 ships as its system interpreter and which our own plugins run
on there - gained no `fromisoformat()`, and `%z` did not read an offset written with a
colon before 3.7 either. The value arrives normalized, so the layouts it can still have
are few enough to try in turn.
"""
if hasattr(datetime.datetime, 'fromisoformat'):
return datetime.datetime.fromisoformat(iso)
tzinfo = None
match = _ISO8601_COLON_OFFSET_REGEX.search(iso)
if match:
offset = datetime.timedelta(
hours=int(match.group(2)),
minutes=int(match.group(3)),
)
tzinfo = datetime.timezone(-offset if match.group(1) == '-' else offset)
iso = iso[: match.start()]
separator = 'T' if 'T' in iso else ' '
_, _, time_part = iso.partition(separator)
layout = '%Y-%m-%d'
if time_part:
layout += separator + '%H:%M'
if time_part.count(':') > 1:
layout += ':%S'
if '.' in time_part:
layout += '.%f'
dt = datetime.datetime.strptime(iso, layout)
return dt.replace(tzinfo=tzinfo) if tzinfo is not None else dt
def _parse(timestr, pattern, tzinfo=None):
"""Turn a time string into a datetime, either by `strptime` layout or as ISO 8601.
Shared by `timestr2datetime()` and `timestr2epoch()` so both accept the same values; see
`timestr2epoch()` for what `pattern='iso8601'` covers.
"""
if pattern == 'iso8601':
# fromisoformat() accepts a trailing 'Z' only from Python 3.11, so
# normalize it first. Same for fractional seconds that are not exactly
# three or six digits long, and for an offset written without a colon.
iso = timestr.strip()
if iso.endswith('Z'):
iso = iso[:-1] + '+00:00'
iso = _normalize_iso8601_offset(_normalize_iso8601_fraction(iso))
dt = _fromisoformat(iso)
# A value that already carries an offset keeps it; a naive value is
# tagged with `tzinfo` when one is given.
if dt.tzinfo is None and tzinfo is not None:
dt = dt.replace(tzinfo=tzinfo)
return dt
dt = datetime.datetime.strptime(timestr, pattern)
# If a timezone is provided, make the datetime timezone-aware.
if tzinfo is not None:
dt = dt.replace(tzinfo=tzinfo)
return dt
def timestr2datetime(timestr, pattern='%Y-%m-%d %H:%M:%S', tzinfo=None):
"""
Converts a time string into a datetime object using the specified format.
This function parses a string representing a date and time into a `datetime.datetime`
object based on the provided format pattern. The default format is ISO (YYYY-MM-DD HH:MM:SS).
### Parameters
- **timestr** (`str`): A string representing the date and time.
- **pattern** (`str`, optional): The format string corresponding to the structure of `timestr`.
Defaults to '%Y-%m-%d %H:%M:%S'. For more details on format codes, see:
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
Pass the special value `'iso8601'` to parse without knowing the exact layout in advance;
`timestr2epoch()` describes what that covers.
- **tzinfo** (`datetime.tzinfo`, optional): Timezone to tag a value that carries none.
A value that brings its own offset keeps it. Defaults to None, which leaves the result
naive.
### Returns
- **datetime.datetime**: A datetime object corresponding to the parsed date and time.
### Example
>>> timestr2datetime('2021-05-08 09:32:09')
datetime.datetime(2021, 5, 8, 9, 32, 9)
>>> timestr2datetime('2026-08-28T17:16:18+0200', pattern='iso8601')
datetime.datetime(2026, 8, 28, 17, 16, 18, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)))
"""
return _parse(timestr, pattern, tzinfo)
def timestr2epoch(timestr, pattern='%Y-%m-%d %H:%M:%S', tzinfo=None):
"""
Converts a time string to a UNIX epoch timestamp.
### Parameters
- **timestr** (`str`): The time string to convert.
- **pattern** (`str`): The format of the time string (default is '%Y-%m-%d %H:%M:%S').
Pass the special value `'iso8601'` to parse without knowing the exact `strptime` layout in
advance. Despite the name, this mode is backed by `datetime.fromisoformat()` (with a trailing
`Z` normalized to `+00:00` first), not a full ISO 8601 parser: it reliably handles RFC 3339
timestamps (date, `T`, time, and a `Z` or `+hh:mm` offset) and date-only values, but rejects
other valid ISO 8601 forms such as ordinal dates (`2024-015`). Which further layouts are
accepted depends on the Python version, because `fromisoformat()` was narrow before 3.11 and
broad from 3.11 on; RFC 3339 works consistently on 3.7+, including the nanosecond precision
of Go-based tools, whose fractional seconds are normalized to microseconds first. A value
that carries an offset (or `Z`) keeps it; a value without one is treated per `tzinfo`
(local time if `tzinfo` is None). An offset written without the colon (`+0200`, which
`journalctl --output=short-iso` produces) is accepted on every supported Python, even
though `fromisoformat()` itself only takes it from 3.11 on.
- **tzinfo** (`datetime.tzinfo`, optional): Timezone information.
If provided, the parsed datetime is set to this timezone.
If None, the time is assumed to be local time.
A value that already carries its own offset (e.g. a `Z` or `+hh:mm` in an iso8601 string)
keeps it.
### Returns
- **float**: The UNIX epoch timestamp (seconds since January 1, 1970, 00:00:00 UTC).
### Raises
- **ValueError**: If the time string does not match the provided format (or is not accepted by
`datetime.fromisoformat()` after `Z` normalization when `pattern='iso8601'`).
### Example
# Convert a time string in local time:
epoch_local = timestr2epoch("2025-03-01 12:00:00")
# Convert a time string assuming it's in UTC:
epoch_utc = timestr2epoch("2025-03-01 12:00:00", tzinfo=datetime.timezone.utc)
# Convert an ISO 8601 string without specifying its exact layout:
epoch_iso = timestr2epoch("2025-03-01T12:00:00Z", pattern='iso8601')
"""
return _parse(timestr, pattern, tzinfo).timestamp()
def timestrdiff(
timestr1, timestr2, pattern1='%Y-%m-%d %H:%M:%S', pattern2='%Y-%m-%d %H:%M:%S'
):
"""
Computes the absolute difference in seconds between two datetime strings.
This function converts two datetime strings into `datetime.datetime` objects using
their respective format patterns, then calculates the absolute time difference between them.
By default, both strings are expected to be in ISO format (YYYY-MM-DD HH:MM:SS).
### Parameters
- **timestr1** (`str`): The first datetime string.
- **timestr2** (`str`): The second datetime string.
- **pattern1** (`str`, optional): The format pattern for `timestr1`. Defaults to '%Y-%m-%d %H:%M:%S'.
- **pattern2** (`str`, optional): The format pattern for `timestr2`. Defaults to '%Y-%m-%d %H:%M:%S'.
For more information on format codes, refer to:
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
### Returns
- **float**: The absolute difference between the two timestamps in seconds.
### Example
>>> timestrdiff('2021-05-08 09:32:09', '2021-05-08 09:30:00')
129.0
"""
dt1 = timestr2datetime(timestr1, pattern1)
dt2 = timestr2datetime(timestr2, pattern2)
return abs((dt1 - dt2).total_seconds())
def utc_offset():
"""
Retrieves the current local UTC offset as a string.
This function returns the local timezone's UTC offset formatted as a string
in the format ±HHMM (e.g., '+0200' or '-0500'), where HH represents hours and MM represents
minutes.
### Returns
- **str**: The current local UTC offset.
### Example
>>> utc_offset()
'+0200'
"""
return time.strftime('%z')