Coverage for /pythoncovmergedfiles/medio/medio/src/airflow/build/lib/airflow/utils/timezone.py: 49%
111 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1#
2# Licensed to the Apache Software Foundation (ASF) under one
3# or more contributor license agreements. See the NOTICE file
4# distributed with this work for additional information
5# regarding copyright ownership. The ASF licenses this file
6# to you under the Apache License, Version 2.0 (the
7# "License"); you may not use this file except in compliance
8# with the License. You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing,
13# software distributed under the License is distributed on an
14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15# KIND, either express or implied. See the License for the
16# specific language governing permissions and limitations
17# under the License.
18from __future__ import annotations
20import datetime as dt
21from typing import overload
23import pendulum
24from dateutil.relativedelta import relativedelta
25from pendulum.datetime import DateTime
27# UTC time zone as a tzinfo instance.
28utc = pendulum.tz.timezone("UTC")
31def is_localized(value):
32 """Determine if a given datetime.datetime is aware.
34 The concept is defined in Python documentation. Assuming the tzinfo is
35 either None or a proper ``datetime.tzinfo`` instance, ``value.utcoffset()``
36 implements the appropriate logic.
38 .. seealso:: http://docs.python.org/library/datetime.html#datetime.tzinfo
39 """
40 return value.utcoffset() is not None
43def is_naive(value):
44 """Determine if a given datetime.datetime is naive.
46 The concept is defined in Python documentation. Assuming the tzinfo is
47 either None or a proper ``datetime.tzinfo`` instance, ``value.utcoffset()``
48 implements the appropriate logic.
50 .. seealso:: http://docs.python.org/library/datetime.html#datetime.tzinfo
51 """
52 return value.utcoffset() is None
55def utcnow() -> dt.datetime:
56 """Get the current date and time in UTC."""
57 # pendulum utcnow() is not used as that sets a TimezoneInfo object
58 # instead of a Timezone. This is not picklable and also creates issues
59 # when using replace()
60 result = dt.datetime.utcnow()
61 result = result.replace(tzinfo=utc)
63 return result
66def utc_epoch() -> dt.datetime:
67 """Gets the epoch in the users timezone."""
68 # pendulum utcnow() is not used as that sets a TimezoneInfo object
69 # instead of a Timezone. This is not picklable and also creates issues
70 # when using replace()
71 result = dt.datetime(1970, 1, 1)
72 result = result.replace(tzinfo=utc)
74 return result
77@overload
78def convert_to_utc(value: None) -> None:
79 ...
82@overload
83def convert_to_utc(value: dt.datetime) -> DateTime:
84 ...
87def convert_to_utc(value: dt.datetime | None) -> DateTime | None:
88 """Creates a datetime with the default timezone added if none is associated.
90 :param value: datetime
91 :return: datetime with tzinfo
92 """
93 if value is None:
94 return value
96 if not is_localized(value):
97 from airflow.settings import TIMEZONE
99 value = pendulum.instance(value, TIMEZONE)
101 return pendulum.instance(value.astimezone(utc))
104@overload
105def make_aware(value: None, timezone: dt.tzinfo | None = None) -> None:
106 ...
109@overload
110def make_aware(value: DateTime, timezone: dt.tzinfo | None = None) -> DateTime:
111 ...
114@overload
115def make_aware(value: dt.datetime, timezone: dt.tzinfo | None = None) -> dt.datetime:
116 ...
119def make_aware(value: dt.datetime | None, timezone: dt.tzinfo | None = None) -> dt.datetime | None:
120 """
121 Make a naive datetime.datetime in a given time zone aware.
123 :param value: datetime
124 :param timezone: timezone
125 :return: localized datetime in settings.TIMEZONE or timezone
126 """
127 if timezone is None:
128 from airflow.settings import TIMEZONE
130 timezone = TIMEZONE
132 if not value:
133 return None
135 # Check that we won't overwrite the timezone of an aware datetime.
136 if is_localized(value):
137 raise ValueError(f"make_aware expects a naive datetime, got {value}")
138 if hasattr(value, "fold"):
139 # In case of python 3.6 we want to do the same that pendulum does for python3.5
140 # i.e in case we move clock back we want to schedule the run at the time of the second
141 # instance of the same clock time rather than the first one.
142 # Fold parameter has no impact in other cases so we can safely set it to 1 here
143 value = value.replace(fold=1)
144 localized = getattr(timezone, "localize", None)
145 if localized is not None:
146 # This method is available for pytz time zones
147 return localized(value)
148 convert = getattr(timezone, "convert", None)
149 if convert is not None:
150 # For pendulum
151 return convert(value)
152 # This may be wrong around DST changes!
153 return value.replace(tzinfo=timezone)
156def make_naive(value, timezone=None):
157 """
158 Make an aware datetime.datetime naive in a given time zone.
160 :param value: datetime
161 :param timezone: timezone
162 :return: naive datetime
163 """
164 if timezone is None:
165 from airflow.settings import TIMEZONE
167 timezone = TIMEZONE
169 # Emulate the behavior of astimezone() on Python < 3.6.
170 if is_naive(value):
171 raise ValueError("make_naive() cannot be applied to a naive datetime")
173 date = value.astimezone(timezone)
175 # cross library compatibility
176 naive = dt.datetime(
177 date.year, date.month, date.day, date.hour, date.minute, date.second, date.microsecond
178 )
180 return naive
183def datetime(*args, **kwargs):
184 """
185 Wrapper around datetime.datetime that adds settings.TIMEZONE if tzinfo not specified.
187 :return: datetime.datetime
188 """
189 if "tzinfo" not in kwargs:
190 from airflow.settings import TIMEZONE
192 kwargs["tzinfo"] = TIMEZONE
194 return dt.datetime(*args, **kwargs)
197def parse(string: str, timezone=None) -> DateTime:
198 """
199 Parse a time string and return an aware datetime.
201 :param string: time string
202 :param timezone: the timezone
203 """
204 from airflow.settings import TIMEZONE
206 return pendulum.parse(string, tz=timezone or TIMEZONE, strict=False) # type: ignore
209@overload
210def coerce_datetime(v: None, tz: dt.tzinfo | None = None) -> None:
211 ...
214@overload
215def coerce_datetime(v: DateTime, tz: dt.tzinfo | None = None) -> DateTime:
216 ...
219@overload
220def coerce_datetime(v: dt.datetime, tz: dt.tzinfo | None = None) -> DateTime:
221 ...
224def coerce_datetime(v: dt.datetime | None, tz: dt.tzinfo | None = None) -> DateTime | None:
225 """Convert ``v`` into a timezone-aware ``pendulum.DateTime``.
227 * If ``v`` is *None*, *None* is returned.
228 * If ``v`` is a naive datetime, it is converted to an aware Pendulum DateTime.
229 * If ``v`` is an aware datetime, it is converted to a Pendulum DateTime.
230 Note that ``tz`` is **not** taken into account in this case; the datetime
231 will maintain its original tzinfo!
232 """
233 if v is None:
234 return None
235 if isinstance(v, DateTime):
236 return v if v.tzinfo else make_aware(v, tz)
237 # Only dt.datetime is left here.
238 return pendulum.instance(v if v.tzinfo else make_aware(v, tz))
241def td_format(td_object: None | dt.timedelta | float | int) -> str | None:
242 """
243 Format a timedelta object or float/int into a readable string for time duration.
244 For example timedelta(seconds=3752) would become `1h:2M:32s`.
245 If the time is less than a second, the return will be `<1s`.
246 """
247 if not td_object:
248 return None
249 if isinstance(td_object, dt.timedelta):
250 delta = relativedelta() + td_object
251 else:
252 delta = relativedelta(seconds=int(td_object))
253 # relativedelta for timedelta cannot convert days to months
254 # so calculate months by assuming 30 day months and normalize
255 months, delta.days = divmod(delta.days, 30)
256 delta = delta.normalized() + relativedelta(months=months)
258 def _format_part(key: str) -> str:
259 value = int(getattr(delta, key))
260 if value < 1:
261 return ""
262 # distinguish between month/minute following strftime format
263 # and take first char of each unit, i.e. years='y', days='d'
264 if key == "minutes":
265 key = key.upper()
266 key = key[0]
267 return f"{value}{key}"
269 parts = map(_format_part, ("years", "months", "days", "hours", "minutes", "seconds"))
270 joined = ":".join(part for part in parts if part)
271 if not joined:
272 return "<1s"
273 return joined