Passed
Push — 2.x ( 1da8e4...ac8200 )
by Jordi
05:35
created

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

Complexity

Conditions 5

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 16
rs 9.3333
c 0
b 0
f 0
cc 5
nop 1
1
# -*- coding: utf-8 -*-
2
3
import os
4
import time
5
from datetime import date
6
from datetime import datetime
7
from string import Template
8
9
import six
10
11
import pytz
12
from bika.lims import logger
13
from bika.lims.api import APIError
14
from DateTime import DateTime
15
from DateTime.DateTime import DateError
16
from DateTime.DateTime import SyntaxError
17
from DateTime.DateTime import TimeError
18
from Products.CMFPlone.i18nl10n import ulocalized_time
19
from zope.i18n import translate
20
21
22
def is_str(obj):
23
    """Check if the given object is a string
24
25
    :param obj: arbitrary object
26
    :returns: True when the object is a string
27
    """
28
    return isinstance(obj, six.string_types)
29
30
31
def is_d(dt):
32
    """Check if the date is a Python `date` object
33
34
    :param dt: date to check
35
    :returns: True when the date is a Python `date`
36
    """
37
    return type(dt) is date
38
39
40
def is_dt(dt):
41
    """Check if the date is a Python `datetime` object
42
43
    :param dt: date to check
44
    :returns: True when the date is a Python `datetime`
45
    """
46
    return type(dt) is datetime
47
48
49
def is_DT(dt):
50
    """Check if the date is a Zope `DateTime` object
51
52
    :param dt: object to check
53
    :returns: True when the object is a Zope `DateTime`
54
    """
55
    return type(dt) is DateTime
56
57
58
def is_date(dt):
59
    """Check if the date is a datetime or DateTime object
60
61
    :param dt: date to check
62
    :returns: True when the object is either a datetime or DateTime
63
    """
64
    if is_str(dt):
65
        DT = to_DT(dt)
66
        return is_date(DT)
67
    if is_d(dt):
68
        return True
69
    if is_dt(dt):
70
        return True
71
    if is_DT(dt):
72
        return True
73
    return False
74
75
76
def is_timezone_naive(dt):
77
    """Check if the date is timezone naive
78
79
    :param dt: date to check
80
    :returns: True when the date has no timezone
81
    """
82
    if is_d(dt):
83
        return True
84
    elif is_DT(dt):
85
        return dt.timezoneNaive()
86
    elif is_dt(dt):
87
        return dt.tzinfo is None
88
    elif is_str(dt):
89
        DT = to_DT(dt)
90
        return is_timezone_naive(DT)
91
    raise APIError("Expected a date type, got '%r'" % type(dt))
92
93
94
def is_timezone_aware(dt):
95
    """Check if the date is timezone aware
96
97
    :param dt: date to check
98
    :returns: True when the date has a timezone
99
    """
100
    return not is_timezone_naive(dt)
101
102
103
def to_DT(dt):
104
    """Convert to DateTime
105
106
    :param dt: DateTime/datetime/date
107
    :returns: DateTime object
108
    """
109
    if is_DT(dt):
110
        return dt
111
    elif is_str(dt):
112
        try:
113
            return DateTime(dt)
114
        except (DateError, TimeError, SyntaxError, IndexError):
115
            return None
116
    elif is_dt(dt):
117
        return DateTime(dt.isoformat())
118
    elif is_d(dt):
119
        dt = datetime(dt.year, dt.month, dt.day)
120
        return DateTime(dt.isoformat())
121
    else:
122
        return None
123
124
125
def to_dt(dt):
126
    """Convert to datetime
127
128
    :param dt: DateTime/datetime/date
129
    :returns: datetime object
130
    """
131
    if is_DT(dt):
132
        # get a valid pytz timezone
133
        tz = get_timezone(dt)
134
        dt = dt.asdatetime()
135
        if is_valid_timezone(tz):
136
            dt = to_zone(dt, tz)
137
        return dt
138
    elif is_str(dt):
139
        DT = to_DT(dt)
140
        return to_dt(DT)
141
    elif is_dt(dt):
142
        return dt
143
    elif is_d(dt):
144
        return datetime(dt.year, dt.month, dt.day)
145
    else:
146
        return None
147
148
149
def get_timezone(dt, default="Etc/GMT"):
150
    """Get a valid pytz timezone of the datetime object
151
152
    :param dt: date object
153
    :returns: timezone as string, e.g. Etc/GMT or CET
154
    """
155
    tz = None
156
    if is_dt(dt):
157
        tz = dt.tzname()
158
    elif is_DT(dt):
159
        tz = dt.timezone()
160
    elif is_d(dt):
161
        tz = default
162
163
    if tz:
164
        # convert DateTime `GMT` to `Etc/GMT` timezones
165
        # NOTE: `GMT+1` get `Etc/GMT-1`!
166
        if tz.startswith("GMT+0"):
167
            tz = tz.replace("GMT+0", "Etc/GMT")
168
        elif tz.startswith("GMT+"):
169
            tz = tz.replace("GMT+", "Etc/GMT-")
170
        elif tz.startswith("GMT-"):
171
            tz = tz.replace("GMT-", "Etc/GMT+")
172
        elif tz.startswith("GMT"):
173
            tz = tz.replace("GMT", "Etc/GMT")
174
    else:
175
        tz = default
176
177
    return tz
178
179
180
def is_valid_timezone(timezone):
181
    """Checks if the timezone is a valid pytz/Olson name
182
183
    :param timezone: pytz/Olson timezone name
184
    :returns: True when the timezone is a valid zone
185
    """
186
    try:
187
        pytz.timezone(timezone)
188
        return True
189
    except pytz.UnknownTimeZoneError:
190
        return False
191
192
193
def get_os_timezone(default="Etc/GMT"):
194
    """Return the default timezone of the system
195
196
    :returns: OS timezone or default timezone
197
    """
198
    timezone = None
199
    if "TZ" in os.environ.keys():
200
        # Timezone from OS env var
201
        timezone = os.environ["TZ"]
202
    if not timezone:
203
        # Timezone from python time
204
        zones = time.tzname
205
        if zones and len(zones) > 0:
206
            timezone = zones[0]
207
        else:
208
            logger.warn(
209
                "Operating system\'s timezone cannot be found. "
210
                "Falling back to %s." % default)
211
            timezone = default
212
    if not is_valid_timezone(timezone):
213
        return default
214
    return timezone
215
216
217
def to_zone(dt, timezone):
218
    """Convert date to timezone
219
220
    Adds the timezone for timezone naive datetimes
221
222
    :param dt: date object
223
    :param timezone: timezone
224
    :returns: date converted to timezone
225
    """
226
    if is_dt(dt) or is_d(dt):
227
        dt = to_dt(dt)
228
        zone = pytz.timezone(timezone)
229
        if is_timezone_aware(dt):
230
            return dt.astimezone(zone)
231
        return zone.localize(dt)
232
    elif is_DT(dt):
233
        # NOTE: This shifts the time according to the TZ offset
234
        return dt.toZone(timezone)
235
    raise TypeError("Expected a date, got '%r'" % type(dt))
236
237
238
def to_timestamp(dt):
239
    """Generate a Portable Operating System Interface (POSIX) timestamp
240
241
    :param dt: date object
242
    :returns: timestamp in seconds
243
    """
244
    timestamp = 0
245
    if is_DT(dt):
246
        timestamp = dt.timeTime()
247
    elif is_dt(dt):
248
        timestamp = time.mktime(dt.timetuple())
249
    elif is_str(dt):
250
        DT = to_DT(dt)
251
        return to_timestamp(DT)
252
    return timestamp
253
254
255
def from_timestamp(timestamp):
256
    """Generate a datetime object from a POSIX timestamp
257
258
    :param timestamp: POSIX timestamp
259
    :returns: datetime object
260
    """
261
    return datetime.utcfromtimestamp(timestamp)
262
263
264
def to_iso_format(dt):
265
    """Convert to ISO format
266
    """
267
    if is_dt(dt):
268
        return dt.isoformat()
269
    elif is_DT(dt):
270
        return dt.ISO()
271
    elif is_str(dt):
272
        DT = to_DT(dt)
273
        return to_iso_format(DT)
274
    return None
275
276
277
def date_to_string(dt, fmt="%Y-%m-%d", default=""):
278
    """Format the date to string
279
    """
280
    if not is_date(dt):
281
        return default
282
283
    # NOTE: The function `is_date` evaluates also string dates as `True`.
284
    #       We ensure in such a case to have a `DateTime` object and leave
285
    #       possible `datetime` objects unchanged.
286
    if isinstance(dt, six.string_types):
287
        dt = to_DT(dt)
288
289
    try:
290
        return dt.strftime(fmt)
291
    except ValueError:
292
        #  Fix ValueError: year=1111 is before 1900;
293
        #  the datetime strftime() methods require year >= 1900
294
295
        # convert format string to be something like "${Y}-${m}-${d}"
296
        new_fmt = ""
297
        var = False
298
        for x in fmt:
299
            if x == "%":
300
                var = True
301
                new_fmt += "${"
302
                continue
303
            if var:
304
                new_fmt += x
305
                new_fmt += "}"
306
                var = False
307
            else:
308
                new_fmt += x
309
310
        def pad(val):
311
            """Add a zero if val is a single digit
312
            """
313
            return "{:0>2}".format(val)
314
315
        # Manually extract relevant date and time parts
316
        dt = to_DT(dt)
317
        data = {
318
            "Y": dt.year(),
319
            "y": dt.yy(),
320
            "m": dt.mm(),
321
            "d": dt.dd(),
322
            "H": pad(dt.h_24()),
323
            "I": pad(dt.h_12()),
324
            "M": pad(dt.minute()),
325
            "p": dt.ampm().upper(),
326
            "S": dt.second(),
327
        }
328
329
        return Template(new_fmt).safe_substitute(data)
330
331
332
def to_localized_time(dt, long_format=None, time_only=None,
333
                      context=None, request=None, default=""):
334
    """Convert a date object to a localized string
335
336
    :param dt: The date/time to localize
337
    :type dt: str/datetime/DateTime
338
    :param long_format: Return long date/time if True
339
    :type portal_type: boolean/null
340
    :param time_only: If True, only returns time.
341
    :type title: boolean/null
342
    :param context: The current context
343
    :type context: ATContentType
344
    :param request: The current request
345
    :type request: HTTPRequest object
346
    :returns: The formatted date as string
347
    :rtype: string
348
    """
349
    dt = to_DT(dt)
350
    if not dt:
351
        return default
352
353
    try:
354
        time_str = ulocalized_time(
355
            dt, long_format, time_only, context, "senaite.core", request)
356
    except ValueError:
357
        # Handle dates < 1900
358
359
        # code taken from Products.CMFPlone.i18nl110n.ulocalized_time
360
        if time_only:
361
            msgid = "time_format"
362
        elif long_format:
363
            msgid = "date_format_long"
364
        else:
365
            msgid = "date_format_short"
366
367
        formatstring = translate(msgid, "senaite.core", {}, request)
368
        if formatstring == msgid:
369
            if msgid == "date_format_long":
370
                formatstring = "%Y-%m-%d %H:%M"  # 2038-01-19 03:14
371
            elif msgid == "date_format_short":
372
                formatstring = "%Y-%m-%d"  # 2038-01-19
373
            elif msgid == "time_format":
374
                formatstring = "%H:%M"  # 03:14
375
            else:
376
                formatstring = "[INTERNAL ERROR]"
377
        time_str = date_to_string(dt, formatstring)
378
    return time_str
379