Passed
Push — 2.x ( f498e5...0bc289 )
by Ramon
06:14
created

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

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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