Passed
Push — 2.x ( 3fd052...8cc6ed )
by Ramon
14:06 queued 03:44
created

senaite.core.api.dtime.get_tzinfo()   A

Complexity

Conditions 4

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 21
rs 9.9
c 0
b 0
f 0
cc 4
nop 2
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2023 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import os
22
import time
23
from datetime import date
24
from datetime import datetime
25
from dateutil.relativedelta import relativedelta
26
from string import Template
27
28
import six
29
30
import pytz
31
from bika.lims import logger
32
from bika.lims.api import APIError
33
from bika.lims.api import get_tool
34
from DateTime import DateTime
35
from DateTime.DateTime import DateError
36
from DateTime.DateTime import SyntaxError
37
from DateTime.DateTime import TimeError
38
from zope.i18n import translate
39
40
41
def is_str(obj):
42
    """Check if the given object is a string
43
44
    :param obj: arbitrary object
45
    :returns: True when the object is a string
46
    """
47
    return isinstance(obj, six.string_types)
48
49
50
def is_d(dt):
51
    """Check if the date is a Python `date` object
52
53
    :param dt: date to check
54
    :returns: True when the date is a Python `date`
55
    """
56
    return type(dt) is date
57
58
59
def is_dt(dt):
60
    """Check if the date is a Python `datetime` object
61
62
    :param dt: date to check
63
    :returns: True when the date is a Python `datetime`
64
    """
65
    return type(dt) is datetime
66
67
68
def is_DT(dt):
69
    """Check if the date is a Zope `DateTime` object
70
71
    :param dt: object to check
72
    :returns: True when the object is a Zope `DateTime`
73
    """
74
    return type(dt) is DateTime
75
76
77
def is_date(dt):
78
    """Check if the date is a datetime or DateTime object
79
80
    :param dt: date to check
81
    :returns: True when the object is either a datetime or DateTime
82
    """
83
    if is_str(dt):
84
        DT = to_DT(dt)
85
        return is_date(DT)
86
    if is_d(dt):
87
        return True
88
    if is_dt(dt):
89
        return True
90
    if is_DT(dt):
91
        return True
92
    return False
93
94
95
def is_timezone_naive(dt):
96
    """Check if the date is timezone naive
97
98
    :param dt: date to check
99
    :returns: True when the date has no timezone
100
    """
101
    if is_d(dt):
102
        return True
103
    elif is_DT(dt):
104
        return dt.timezoneNaive()
105
    elif is_dt(dt):
106
        return dt.tzinfo is None
107
    elif is_str(dt):
108
        DT = to_DT(dt)
109
        return is_timezone_naive(DT)
110
    raise APIError("Expected a date type, got '%r'" % type(dt))
111
112
113
def is_timezone_aware(dt):
114
    """Check if the date is timezone aware
115
116
    :param dt: date to check
117
    :returns: True when the date has a timezone
118
    """
119
    return not is_timezone_naive(dt)
120
121
122
def to_DT(dt):
123
    """Convert to DateTime
124
125
    :param dt: DateTime/datetime/date
126
    :returns: DateTime object
127
    """
128
    if is_DT(dt):
129
        return dt
130
    elif is_str(dt):
131
        try:
132
            return DateTime(dt)
133
        except (DateError, TimeError):
134
            try:
135
                dt = ansi_to_dt(dt)
136
                return to_DT(dt)
137
            except ValueError:
138
                return None
139
        except (SyntaxError, IndexError):
140
            return None
141
    elif is_dt(dt):
142
        return DateTime(dt.isoformat())
143
    elif is_d(dt):
144
        dt = datetime(dt.year, dt.month, dt.day)
145
        return DateTime(dt.isoformat())
146
    else:
147
        return None
148
149
150
def to_dt(dt):
151
    """Convert to datetime
152
153
    :param dt: DateTime/datetime/date
154
    :returns: datetime object
155
    """
156
    if is_DT(dt):
157
        # get a valid pytz timezone
158
        tz = get_timezone(dt)
159
        dt = dt.asdatetime()
160
        if is_valid_timezone(tz):
161
            dt = to_zone(dt, tz)
162
        return dt
163
    elif is_str(dt):
164
        DT = to_DT(dt)
165
        return to_dt(DT)
166
    elif is_dt(dt):
167
        return dt
168
    elif is_d(dt):
169
        return datetime(dt.year, dt.month, dt.day)
170
    else:
171
        return None
172
173
174
def ansi_to_dt(dt):
175
    """The YYYYMMDD format is defined by ANSI X3.30. Therefore, 2 December 1,
176
    1989 would be represented as 19891201. When times are transmitted, they
177
    shall be represented as HHMMSS, and shall be linked to dates as specified
178
    by ANSI X3.43.3 Date and time together shall be specified as up to a
179
    14-character string: YYYYMMDD[HHMMSS]
180
    :param str:
181
    :return: datetime object
182
    """
183
    if not is_str(dt):
184
        raise TypeError("Type is not supported")
185
    if len(dt) == 8:
186
        date_format = "%Y%m%d"
187
    elif len(dt) == 14:
188
        date_format = "%Y%m%d%H%M%S"
189
    else:
190
        raise ValueError("No ANSI format date")
191
    return datetime.strptime(dt, date_format)
192
193
194
def to_ansi(dt, show_time=True):
195
    """Returns the date in ANSI X3.30/X4.43.3) format
196
    :param dt: DateTime/datetime/date
197
    :param show_time: if true, returns YYYYMMDDHHMMSS. YYYYMMDD otherwise
198
    :returns: str that represents the datetime in ANSI format
199
    """
200
    dt = to_dt(dt)
201
    if dt is None:
202
        return None
203
204
    ansi = "{:04d}{:02d}{:02d}".format(dt.year, dt.month, dt.day)
205
    if not show_time:
206
        return ansi
207
    return "{}{:02d}{:02d}{:02d}".format(ansi, dt.hour, dt.minute, dt.second)
208
209
210
def get_timezone(dt, default="Etc/GMT"):
211
    """Get a valid pytz timezone name of the datetime object
212
213
    :param dt: date object
214
    :returns: timezone as string, e.g. Etc/GMT or CET
215
    """
216
    tz = None
217
    if is_dt(dt):
218
        tz = dt.tzname()
219
    elif is_DT(dt):
220
        tz = dt.timezone()
221
    elif is_d(dt):
222
        tz = default
223
224
    if tz:
225
        # convert DateTime `GMT` to `Etc/GMT` timezones
226
        # NOTE: `GMT+1` get `Etc/GMT-1`!
227
        if tz.startswith("GMT+0"):
228
            tz = tz.replace("GMT+0", "Etc/GMT")
229
        elif tz.startswith("GMT+"):
230
            tz = tz.replace("GMT+", "Etc/GMT-")
231
        elif tz.startswith("GMT-"):
232
            tz = tz.replace("GMT-", "Etc/GMT+")
233
        elif tz.startswith("GMT"):
234
            tz = tz.replace("GMT", "Etc/GMT")
235
    else:
236
        tz = default
237
238
    return tz
239
240
241
def get_tzinfo(dt_tz, default=pytz.UTC):
242
    """Returns the valid pytz tinfo from the date or timezone name
243
244
    Returns the default timezone info if date does not have a valid timezone
245
    set or is TZ-naive
246
247
    :param dt: timezone name or date object to extract the tzinfo
248
    :type dt: str/date/datetime/DateTime
249
    :param: default: timezone name or pytz tzinfo object
250
    :returns: pytz tzinfo object, e.g. `<UTC>, <StaticTzInfo 'Etc/GMT+2'>
251
    :rtype: UTC/BaseTzInfo/StaticTzInfo/DstTzInfo
252
    """
253
    if is_str(default):
254
        default = pytz.timezone(default)
255
    try:
256
        if is_str(dt_tz):
257
            return pytz.timezone(dt_tz)
258
        tz = get_timezone(dt_tz, default=default.zone)
259
        return pytz.timezone(tz)
260
    except pytz.UnknownTimeZoneError:
261
        return default
262
263
264
def is_valid_timezone(timezone):
265
    """Checks if the timezone is a valid pytz/Olson name
266
267
    :param timezone: pytz/Olson timezone name
268
    :returns: True when the timezone is a valid zone
269
    """
270
    try:
271
        pytz.timezone(timezone)
272
        return True
273
    except pytz.UnknownTimeZoneError:
274
        return False
275
276
277
def get_os_timezone(default="Etc/GMT"):
278
    """Return the default timezone of the system
279
280
    :returns: OS timezone or default timezone
281
    """
282
    timezone = None
283
    if "TZ" in os.environ.keys():
284
        # Timezone from OS env var
285
        timezone = os.environ["TZ"]
286
    if not timezone:
287
        # Timezone from python time
288
        zones = time.tzname
289
        if zones and len(zones) > 0:
290
            timezone = zones[0]
291
        else:
292
            logger.warn(
293
                "Operating system\'s timezone cannot be found. "
294
                "Falling back to %s." % default)
295
            timezone = default
296
    if not is_valid_timezone(timezone):
297
        return default
298
    return timezone
299
300
301
def to_zone(dt, timezone):
302
    """Convert date to timezone
303
304
    Adds the timezone for timezone naive datetimes
305
306
    :param dt: date object
307
    :param timezone: timezone
308
    :returns: date converted to timezone
309
    """
310
    if is_dt(dt) or is_d(dt):
311
        dt = to_dt(dt)
312
        zone = pytz.timezone(timezone)
313
        if is_timezone_aware(dt):
314
            return dt.astimezone(zone)
315
        return zone.localize(dt)
316
    elif is_DT(dt):
317
        # NOTE: This shifts the time according to the TZ offset
318
        return dt.toZone(timezone)
319
    raise TypeError("Expected a date, got '%r'" % type(dt))
320
321
322
def to_timestamp(dt):
323
    """Generate a Portable Operating System Interface (POSIX) timestamp
324
325
    :param dt: date object
326
    :returns: timestamp in seconds
327
    """
328
    timestamp = 0
329
    if is_DT(dt):
330
        timestamp = dt.timeTime()
331
    elif is_dt(dt):
332
        timestamp = time.mktime(dt.timetuple())
333
    elif is_str(dt):
334
        DT = to_DT(dt)
335
        return to_timestamp(DT)
336
    return timestamp
337
338
339
def from_timestamp(timestamp):
340
    """Generate a datetime object from a POSIX timestamp
341
342
    :param timestamp: POSIX timestamp
343
    :returns: datetime object
344
    """
345
    return datetime.utcfromtimestamp(timestamp)
346
347
348
def to_iso_format(dt):
349
    """Convert to ISO format
350
    """
351
    if is_dt(dt):
352
        return dt.isoformat()
353
    elif is_DT(dt):
354
        return dt.ISO()
355
    elif is_str(dt):
356
        DT = to_DT(dt)
357
        return to_iso_format(DT)
358
    return None
359
360
361
def date_to_string(dt, fmt="%Y-%m-%d", default=""):
362
    """Format the date to string
363
    """
364
    if not is_date(dt):
365
        return default
366
367
    # NOTE: The function `is_date` evaluates also string dates as `True`.
368
    #       We ensure in such a case to have a `DateTime` object and leave
369
    #       possible `datetime` objects unchanged.
370
    if isinstance(dt, six.string_types):
371
        dt = to_DT(dt)
372
373
    try:
374
        return dt.strftime(fmt)
375
    except ValueError:
376
        #  Fix ValueError: year=1111 is before 1900;
377
        #  the datetime strftime() methods require year >= 1900
378
379
        # convert format string to be something like "${Y}-${m}-${d}"
380
        new_fmt = ""
381
        var = False
382
        for x in fmt:
383
            if x == "%":
384
                var = True
385
                new_fmt += "${"
386
                continue
387
            if var:
388
                new_fmt += x
389
                new_fmt += "}"
390
                var = False
391
            else:
392
                new_fmt += x
393
394
        def pad(val):
395
            """Add a zero if val is a single digit
396
            """
397
            return "{:0>2}".format(val)
398
399
        # Manually extract relevant date and time parts
400
        dt = to_DT(dt)
401
        data = {
402
            "Y": dt.year(),
403
            "y": dt.yy(),
404
            "m": dt.mm(),
405
            "d": dt.dd(),
406
            "H": pad(dt.h_24()),
407
            "I": pad(dt.h_12()),
408
            "M": pad(dt.minute()),
409
            "p": dt.ampm().upper(),
410
            "S": dt.second(),
411
        }
412
413
        return Template(new_fmt).safe_substitute(data)
414
415
416
def to_localized_time(dt, long_format=None, time_only=None,
417
                      context=None, request=None, default=""):
418
    """Convert a date object to a localized string
419
420
    :param dt: The date/time to localize
421
    :type dt: str/datetime/DateTime
422
    :param long_format: Return long date/time if True
423
    :type portal_type: boolean/null
424
    :param time_only: If True, only returns time.
425
    :type title: boolean/null
426
    :param context: The current context
427
    :type context: ATContentType
428
    :param request: The current request
429
    :type request: HTTPRequest object
430
    :returns: The formatted date as string
431
    :rtype: string
432
    """
433
    if not dt:
434
        return default
435
436
    try:
437
        ts = get_tool("translation_service")
438
        time_str = ts.ulocalized_time(
439
            dt, long_format, time_only, context, "senaite.core", request)
440
    except ValueError:
441
        # Handle dates < 1900
442
443
        # code taken from Products.CMFPlone.i18nl110n.ulocalized_time
444
        if time_only:
445
            msgid = "time_format"
446
        elif long_format:
447
            msgid = "date_format_long"
448
        else:
449
            msgid = "date_format_short"
450
451
        formatstring = translate(msgid, "senaite.core", {}, request)
452
        if formatstring == msgid:
453
            if msgid == "date_format_long":
454
                formatstring = "%Y-%m-%d %H:%M"  # 2038-01-19 03:14
455
            elif msgid == "date_format_short":
456
                formatstring = "%Y-%m-%d"  # 2038-01-19
457
            elif msgid == "time_format":
458
                formatstring = "%H:%M"  # 03:14
459
            else:
460
                formatstring = "[INTERNAL ERROR]"
461
        time_str = date_to_string(dt, formatstring, default=default)
462
    return time_str
463
464
465
def get_relative_delta(dt1, dt2=None):
466
    """Calculates the relative delta between two dates or datetimes
467
468
    If `dt2` is None, the current datetime is used.
469
470
    :param dt1: the first date/time to compare
471
    :type dt1: string/date/datetime/DateTime
472
    :param dt2: the second date/time to compare
473
    :type dt2: string/date/datetime/DateTime
474
    :returns: interval of time (e.g. `relativedelta(hours=+3)`)
475
    :rtype: dateutil.relativedelta
476
    """
477
    if not dt2:
478
        dt2 = datetime.now()
479
480
    dt1 = to_dt(dt1)
481
    dt2 = to_dt(dt2)
482
    if not all([dt1, dt2]):
483
        raise ValueError("No valid date or dates")
484
485
    naives = [is_timezone_naive(dt) for dt in [dt1, dt2]]
486
    if all(naives):
487
        # Both naive, no need to do anything special
488
        return relativedelta(dt2, dt1)
489
490
    elif is_timezone_naive(dt1):
491
        # From date is naive, assume same TZ as the to date
492
        tzinfo = get_tzinfo(dt2)
493
        dt1 = dt1.replace(tzinfo=tzinfo)
494
495
    elif is_timezone_naive(dt2):
496
        # To date is naive, assume same TZ as the from date
497
        tzinfo = get_tzinfo(dt1)
498
        dt2 = dt2.replace(tzinfo=tzinfo)
499
500
    return relativedelta(dt2, dt1)
501