|
1
|
|
|
""" |
|
2
|
|
|
PRIVATE MODULE: do not import (from) it directly. |
|
3
|
|
|
|
|
4
|
|
|
This module contains functionality for ``datetime`` related stuff. |
|
5
|
|
|
""" |
|
6
|
|
|
from datetime import datetime, timezone, timedelta, time, date |
|
7
|
|
|
from typing import Union |
|
8
|
|
|
|
|
9
|
|
|
RFC3339_DATE_PATTERN = '%Y-%m-%d' |
|
10
|
|
|
RFC3339_TIME_PATTERN = '%H:%M:%S' |
|
11
|
|
|
RFC3339_DATETIME_PATTERN = '{}T{}'.format( |
|
12
|
|
|
RFC3339_DATE_PATTERN, RFC3339_TIME_PATTERN) |
|
13
|
|
|
|
|
14
|
|
|
|
|
15
|
|
|
def to_str( |
|
16
|
|
|
dt: Union[datetime, date], |
|
17
|
|
|
strip_microseconds: bool, |
|
18
|
|
|
fork_inst: type, |
|
19
|
|
|
pattern: str = RFC3339_DATETIME_PATTERN) -> str: |
|
20
|
|
|
offset = get_offset_str(dt, fork_inst) |
|
21
|
|
|
if not strip_microseconds and getattr(dt, 'microsecond', None): |
|
22
|
|
|
pattern += '.%f' |
|
23
|
|
|
return dt.strftime("{}{}".format(pattern, offset)) |
|
24
|
|
|
|
|
25
|
|
|
|
|
26
|
|
|
def get_offset_str( |
|
27
|
|
|
obj: Union[datetime, date, timedelta], |
|
28
|
|
|
fork_inst: type) -> str: |
|
29
|
|
|
""" |
|
30
|
|
|
Return the textual offset of the given ``obj``. |
|
31
|
|
|
:param obj: a datetime or timedelta instance. |
|
32
|
|
|
:param fork_inst: the state holder that is used. |
|
33
|
|
|
:return: the offset following RFC3339. |
|
34
|
|
|
""" |
|
35
|
|
|
result = '' |
|
36
|
|
|
if isinstance(obj, datetime): |
|
37
|
|
|
result = _datetime_offset_str(obj, fork_inst) |
|
38
|
|
|
elif isinstance(obj, timedelta): |
|
39
|
|
|
result = _timedelta_offset_str(obj) |
|
40
|
|
|
return result |
|
41
|
|
|
|
|
42
|
|
|
|
|
43
|
|
|
def get_datetime_inst(obj: str, pattern: str) -> datetime: |
|
44
|
|
|
""" |
|
45
|
|
|
Return a datetime instance with timezone info from the given ``obj``. |
|
46
|
|
|
:param obj: the ``obj`` in RFC3339 format. |
|
47
|
|
|
:param pattern: the datetime pattern. |
|
48
|
|
|
:return: a datetime instance with timezone info. |
|
49
|
|
|
""" |
|
50
|
|
|
if obj[-1] == 'Z': |
|
51
|
|
|
result = _datetime_utc_inst(obj, pattern) |
|
52
|
|
|
elif 'T' in pattern: |
|
53
|
|
|
result = _datetime_offset_inst(obj, pattern) |
|
54
|
|
|
else: |
|
55
|
|
|
result = datetime.strptime(obj, pattern) |
|
56
|
|
|
return result |
|
57
|
|
|
|
|
58
|
|
|
|
|
59
|
|
|
def _datetime_offset_str(obj: datetime, fork_inst: type) -> str: |
|
60
|
|
|
""" |
|
61
|
|
|
Return a textual offset (e.g. +01:00 or Z) for the given datetime. |
|
62
|
|
|
:param obj: the datetime instance. |
|
63
|
|
|
:return: the offset for ``obj``. |
|
64
|
|
|
""" |
|
65
|
|
|
tzone = obj.tzinfo |
|
66
|
|
|
if not tzone: |
|
67
|
|
|
# datetimes without tzinfo are treated as local times. |
|
68
|
|
|
fork_inst._warn('The use of datetimes without timezone is dangerous ' |
|
69
|
|
|
'and can lead to undesired results.', |
|
70
|
|
|
'datetime-without-tz') |
|
71
|
|
|
tzone = datetime.now(timezone.utc).astimezone().tzinfo |
|
72
|
|
|
if tzone is timezone.utc or tzone.utc is timezone.utc: |
|
73
|
|
|
return '+00:00' |
|
74
|
|
|
offset = 'Z' |
|
75
|
|
|
if tzone.tzname(None) not in ('UTC', 'UTC+00:00'): |
|
76
|
|
|
tdelta = tzone.utcoffset(None) or \ |
|
77
|
|
|
getattr(tzone, 'adjusted_offset', tzone.utcoffset(obj)) |
|
78
|
|
|
offset = _timedelta_offset_str(tdelta) |
|
79
|
|
|
return offset |
|
80
|
|
|
|
|
81
|
|
|
|
|
82
|
|
|
def _timedelta_offset_str(tdelta: timedelta) -> str: |
|
83
|
|
|
""" |
|
84
|
|
|
Return a textual offset (e.g. +01:00 or Z) for the given timedelta. |
|
85
|
|
|
:param tdelta: the timedelta instance. |
|
86
|
|
|
:return: the offset for ``tdelta``. |
|
87
|
|
|
""" |
|
88
|
|
|
offset_s = tdelta.total_seconds() |
|
89
|
|
|
offset_h = int(offset_s / 3600) |
|
90
|
|
|
offset_m = int((offset_s / 60) % 60) |
|
91
|
|
|
offset_t = time(abs(offset_h), abs(offset_m)) |
|
92
|
|
|
operator = '+' if offset_s > 0 else '-' |
|
93
|
|
|
offset = offset_t.strftime('{}%H:%M'.format(operator)) |
|
94
|
|
|
return offset |
|
95
|
|
|
|
|
96
|
|
|
|
|
97
|
|
|
def _datetime_utc_inst(obj: str, pattern: str) -> datetime: |
|
98
|
|
|
""" |
|
99
|
|
|
Return a datetime instance with UTC timezone info. |
|
100
|
|
|
:param obj: a datetime in RFC3339 format. |
|
101
|
|
|
:param pattern: the datetime pattern that is used. |
|
102
|
|
|
:return: a datetime instance with timezone info. |
|
103
|
|
|
""" |
|
104
|
|
|
dattim_str = obj[0:-1] |
|
105
|
|
|
dattim_obj = datetime.strptime(dattim_str, pattern) |
|
106
|
|
|
return _new_datetime(dattim_obj.date(), dattim_obj.time(), timezone.utc) |
|
107
|
|
|
|
|
108
|
|
|
|
|
109
|
|
|
def _datetime_offset_inst(obj: str, pattern: str) -> datetime: |
|
110
|
|
|
""" |
|
111
|
|
|
Return a datetime instance with timezone info. |
|
112
|
|
|
:param obj: a datetime in RFC3339 format. |
|
113
|
|
|
:param pattern: the datetime pattern that is used. |
|
114
|
|
|
:return: a datetime instance with timezone info. |
|
115
|
|
|
""" |
|
116
|
|
|
dat_str, tim_str = obj.split('T') |
|
117
|
|
|
splitter, factor = ('+', 1) if '+' in tim_str else ('-', -1) |
|
118
|
|
|
naive_tim_str, offset = tim_str.split(splitter) |
|
119
|
|
|
naive_dattim_str = '{}T{}'.format(dat_str, naive_tim_str) |
|
120
|
|
|
dattim_obj = datetime.strptime(naive_dattim_str, pattern) |
|
121
|
|
|
hrs_str, mins_str = offset.split(':') |
|
122
|
|
|
hrs = int(hrs_str) * factor |
|
123
|
|
|
mins = int(mins_str) * factor |
|
124
|
|
|
tz = timezone(offset=timedelta(hours=hrs, minutes=mins)) |
|
125
|
|
|
return _new_datetime(dattim_obj.date(), dattim_obj.time(), tz) |
|
126
|
|
|
|
|
127
|
|
|
|
|
128
|
|
|
def _new_datetime(date_inst: date, time_inst: time, tzinfo: timezone) \ |
|
129
|
|
|
-> datetime: |
|
130
|
|
|
""" |
|
131
|
|
|
Return a datetime instance from a date, time and timezone. |
|
132
|
|
|
|
|
133
|
|
|
This function was required due to the missing argument for tzinfo under the |
|
134
|
|
|
Linux Python distribution. |
|
135
|
|
|
:param date_inst: the date. |
|
136
|
|
|
:param time_inst: the time. |
|
137
|
|
|
:param tzinfo: the Timezone. |
|
138
|
|
|
:return: a combined datetime instance. |
|
139
|
|
|
""" |
|
140
|
|
|
return datetime.combine(date_inst, time_inst).replace(tzinfo=tzinfo) |
|
141
|
|
|
|