datatypes 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. datatypes/__init__.py +153 -0
  2. datatypes/collections/__init__.py +22 -0
  3. datatypes/collections/container.py +150 -0
  4. datatypes/collections/mapping.py +614 -0
  5. datatypes/collections/sequence.py +469 -0
  6. datatypes/command.py +572 -0
  7. datatypes/compat.py +205 -0
  8. datatypes/config/__init__.py +9 -0
  9. datatypes/config/environ.py +302 -0
  10. datatypes/config/parser.py +255 -0
  11. datatypes/copy.py +151 -0
  12. datatypes/csv.py +322 -0
  13. datatypes/datetime.py +888 -0
  14. datatypes/decorators/__init__.py +23 -0
  15. datatypes/decorators/base.py +343 -0
  16. datatypes/decorators/descriptor.py +389 -0
  17. datatypes/decorators/misc.py +118 -0
  18. datatypes/email.py +351 -0
  19. datatypes/enum.py +191 -0
  20. datatypes/event.py +207 -0
  21. datatypes/filetypes/__init__.py +12 -0
  22. datatypes/filetypes/html.py +556 -0
  23. datatypes/filetypes/toml.py +260 -0
  24. datatypes/http.py +723 -0
  25. datatypes/logging.py +384 -0
  26. datatypes/number.py +187 -0
  27. datatypes/parse.py +246 -0
  28. datatypes/path.py +3637 -0
  29. datatypes/profile.py +232 -0
  30. datatypes/reflection.py +1498 -0
  31. datatypes/server.py +408 -0
  32. datatypes/service.py +212 -0
  33. datatypes/string.py +1328 -0
  34. datatypes/token/__init__.py +16 -0
  35. datatypes/token/abnf.py +1704 -0
  36. datatypes/token/base.py +475 -0
  37. datatypes/token/word.py +259 -0
  38. datatypes/url.py +841 -0
  39. datatypes/utils.py +142 -0
  40. datatypes-0.12.0.dist-info/LICENSE.txt +21 -0
  41. datatypes-0.12.0.dist-info/METADATA +52 -0
  42. datatypes-0.12.0.dist-info/RECORD +44 -0
  43. datatypes-0.12.0.dist-info/WHEEL +5 -0
  44. datatypes-0.12.0.dist-info/top_level.txt +1 -0
datatypes/datetime.py ADDED
@@ -0,0 +1,888 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import unicode_literals, division, print_function, absolute_import
3
+ import datetime
4
+ import inspect
5
+ import logging
6
+ import re
7
+ import time
8
+ import math
9
+ import calendar
10
+
11
+ from .compat import *
12
+ from .string import String
13
+
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Datetime(datetime.datetime):
19
+ """Wrapper around standard datetime.datetime class that assures UTC time and
20
+ full ISO8601 date strings with Z timezone.
21
+
22
+ You can create an object multiple ways:
23
+
24
+ d = Datetime() # current utc time
25
+ d = Datetime("2019-10-01T23:26:22.079296Z") # parses ISO8601 date from string
26
+ d = Datetime(2019, 10, 1) # standard creation syntax
27
+
28
+ https://docs.python.org/3/library/datetime.html#datetime-objects
29
+ """
30
+ FORMAT_ISO8601 = "%Y-%m-%dT%H:%M:%S.%f"
31
+ FORMAT_ISO8601_TZ = "%Y-%m-%dT%H:%M:%S.%f%Z"
32
+ FORMAT_ISO8601_OFFSET = "%Y-%m-%dT%H:%M:%S.%f%z"
33
+ FORMAT_ISO8601_SECONDS = "%Y-%m-%dT%H:%M:%S"
34
+ FORMAT_ISO8601_SECONDS_TZ = "%Y-%m-%dT%H:%M:%S%Z"
35
+ FORMAT_ISO8601_SECONDS_OFFSET = "%Y-%m-%dT%H:%M:%S%z"
36
+ FORMAT_ISO8601_DAY = "%Y-%m-%d"
37
+
38
+ @property
39
+ def yearname(self):
40
+ """return the full 4 digit year"""
41
+ return self.strftime("%Y")
42
+
43
+ @property
44
+ def monthname(self):
45
+ """return the full month name"""
46
+ return self.strftime("%B")
47
+
48
+ @property
49
+ def dayname(self):
50
+ """return the full day name"""
51
+ return self.strftime("%A")
52
+
53
+ @classmethod
54
+ def formats(cls):
55
+ is_valid = lambda k, v: k.startswith("FORMAT")
56
+ formats = set(v for k, v in inspect.getmembers(cls) if is_valid(k, v))
57
+ return formats
58
+
59
+ @classmethod
60
+ def parse(cls, d):
61
+ dt = cls.parse_formats(d)
62
+
63
+ if dt is None:
64
+ dt = cls.parse_regex(d)
65
+
66
+ if dt is None:
67
+ raise ValueError("Cannot infer datetime value from {}".format(d))
68
+
69
+ return dt
70
+
71
+ @classmethod
72
+ def parse_formats(cls, d):
73
+ fs = cls.formats()
74
+ for f in fs:
75
+ try:
76
+ logger.debug("Attempting to parse date: {} with format: {}".format(
77
+ d,
78
+ f,
79
+ ))
80
+
81
+ ret = cls.strptime(d, f)
82
+ if ("%z" in f) or ("%Z" in f):
83
+ ret = ret.astimezone(datetime.timezone.utc)
84
+
85
+ else:
86
+ ret = ret.replace(tzinfo=datetime.timezone.utc)
87
+
88
+ logger.debug(f"Date: {d} parsed with format: {f}")
89
+ return ret
90
+
91
+ except ValueError as e:
92
+ logger.debug(e)
93
+ pass
94
+
95
+ @classmethod
96
+ def parse_regex(cls, d):
97
+ dt = None
98
+ if re.match(r"^\-?\d+\.\d+$", d):
99
+ # account for unix timestamps with microseconds
100
+ logger.debug("Date: {} parsed with float unix timestamp regex".format(d))
101
+ dt = cls.fromtimestamp(float(d), tz=datetime.timezone.utc)
102
+
103
+ elif re.match(r"^\-?\d+$", d):
104
+ # account for unix timestamps without microseconds
105
+ logger.debug("Date: {} parsed with integer unix timestamp regex".format(d))
106
+ val = int(d)
107
+ dt = cls.fromtimestamp(val, tz=datetime.timezone.utc)
108
+
109
+ else:
110
+ # in 3.11+ we can maybe replace this with:
111
+ # https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat
112
+ #
113
+ # ISO 8601 is not very strict with the date format and this tries to
114
+ # capture most of that leniency, with the one exception that the
115
+ # date must be in UTC
116
+ # https://en.wikipedia.org/wiki/ISO_8601
117
+ m = re.match(
118
+ r"""
119
+ ^ # match from the beginning of the string
120
+ (\d{4}) # 0 - YYYY (year)
121
+ .? # deliminator
122
+ (\d{2}) # 1 - MM (month)
123
+ .? # deliminator
124
+ (\d{2}) # 2 - DD (day)
125
+ . # Date and time separator (usually T)
126
+ (\d{2}) # 3 - HH (hour)
127
+ (?::?(\d{2}))? # 4 - MM (minute)
128
+ (?::?(\d{2}))? # 5 - SS (second)
129
+ (?:\.(\d+))? # 6 - MS (milliseconds)
130
+ """,
131
+ d,
132
+ flags=re.X
133
+ )
134
+
135
+ if m:
136
+ logger.debug("Date: {} parsed with ISO regex".format(d))
137
+
138
+ parsed_dateparts = m.groups()
139
+ dateparts = list(map(lambda x: int(x) if x else 0, parsed_dateparts[:6]))
140
+ dt = cls(*dateparts, tzinfo=datetime.timezone.utc)
141
+
142
+ # account for ms with leading zeros
143
+ if parsed_dateparts[6]:
144
+ ms_len = len(parsed_dateparts[6])
145
+ if ms_len >= 3:
146
+ millis = parsed_dateparts[6][:3]
147
+ micros = parsed_dateparts[6][3:] or 0
148
+
149
+ else:
150
+ millis = parsed_dateparts[6] or 0
151
+ micros = 0
152
+
153
+ # make sure each part is 3 digits by zero padding on the right
154
+ if millis:
155
+ millis = "{:0<3.3}".format(millis)
156
+ if micros:
157
+ micros = "{:0<3.3}".format(micros)
158
+
159
+ dt += datetime.timedelta(
160
+ milliseconds=int(millis),
161
+ microseconds=int(micros)
162
+ )
163
+
164
+ tzoffset = d[m.regs[m.lastindex][1]:]
165
+ if tzoffset:
166
+ # https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators
167
+ if tzoffset != "Z":
168
+ moffset = re.match(
169
+ r"""
170
+ ([+-]) # 1 - positive or negative offset
171
+ (\d{2}) # 2 - HH (hour)
172
+ (?::?(\d{2}))? # 3 - MM (minute)
173
+ $
174
+ """,
175
+ tzoffset,
176
+ flags=re.X
177
+ )
178
+
179
+ if moffset:
180
+ sign = moffset.group(1)
181
+ seconds = 0
182
+ if hours := moffset.group(2):
183
+ seconds += int(hours) * 3600
184
+
185
+ if minutes := moffset.group(3):
186
+ seconds += int(minutes) * 60
187
+
188
+ if seconds:
189
+ if sign == "+":
190
+ dt -= datetime.timedelta(seconds=seconds)
191
+ else:
192
+ dt += datetime.timedelta(seconds=seconds)
193
+
194
+ return dt
195
+
196
+ @classmethod
197
+ def parse_args(self, args, kwargs):
198
+ """Parse out custom arguments so you can pass *args, **kwargs to the
199
+ standard datetime class initializer
200
+
201
+ :param args: tuple, the *args passed to a function
202
+ :param kwargs: dict, the **kwargs passed to a function
203
+ :returns: tuple, (args, kwargs, custom)
204
+ """
205
+ timedelta_kwargs = {}
206
+ replace_kwargs = {}
207
+
208
+ timedelta_names = {
209
+ "years": ["years", "yrs"],
210
+ "months": ["months"],
211
+ "weeks": ["weeks", "week"],
212
+ "days": ["days"],
213
+ "hours": ["hours"],
214
+ "minutes": ["minutes", "mins"],
215
+ "seconds": ["seconds", "secs"],
216
+ "microseconds": ["microseconds", "usecs"],
217
+ "milliseconds": ["milliseconds", "msecs"],
218
+ }
219
+ for nk, ks in timedelta_names.items():
220
+ for k in ks:
221
+ if k in kwargs:
222
+ timedelta_kwargs.setdefault(nk, 0)
223
+ timedelta_kwargs[nk] += kwargs.pop(k)
224
+
225
+ if timedelta := kwargs.pop("timedelta", None):
226
+ timedelta_kwargs["timedelta"] = timedelta
227
+
228
+ if args and isinstance(args[0], datetime.timedelta):
229
+ if "timedelta" in timedelta_kwargs:
230
+ timedelta_kwargs["timedelta"] += args[0]
231
+
232
+ else:
233
+ timedelta_kwargs["timedelta"] = args[0]
234
+
235
+ args = args[1:]
236
+
237
+ replace_names = {
238
+ "year": ["year", "yr", "y", "Y"],
239
+ "month": ["month", "m"],
240
+ "day": ["day", "d"],
241
+ "hour": ["hour", "hr", "H"],
242
+ "minute": ["minute", "min", "M"],
243
+ "second": ["second", "sec", "S"],
244
+ "microsecond": ["microsecond", "us", "usec"],
245
+ "millisecond": ["millisecond", "ms", "msec"],
246
+ }
247
+ for nk, ks in replace_names.items():
248
+ for k in ks:
249
+ if k in kwargs:
250
+ replace_kwargs.setdefault(nk, 0)
251
+ replace_kwargs[nk] += kwargs.pop(k)
252
+
253
+ if len(args) < 8:
254
+ replace_kwargs.setdefault(
255
+ "tzinfo",
256
+ kwargs.pop("tzinfo", datetime.timezone.utc)
257
+ )
258
+
259
+ else:
260
+ if tzinfo := kwargs.pop("tzinfo", None):
261
+ replace_kwargs["tzinfo"] = tzinfo
262
+
263
+ if not args and "now" in kwargs:
264
+ args = [kwargs.pop("now")]
265
+
266
+ return args, replace_kwargs, timedelta_kwargs
267
+
268
+ def __new__(cls, *args, **kwargs):
269
+ # remove any custom keywords
270
+ args, replace_kwargs, timedelta_kwargs = cls.parse_args(args, kwargs)
271
+
272
+ if not args:
273
+ instance = cls.now(datetime.timezone.utc)
274
+
275
+ elif len(args) == 1:
276
+ if isinstance(args[0], datetime.datetime):
277
+ dt = args[0].astimezone(datetime.timezone.utc)
278
+ instance = super().__new__(
279
+ cls,
280
+ dt.year,
281
+ dt.month,
282
+ dt.day,
283
+ dt.hour,
284
+ dt.minute,
285
+ dt.second,
286
+ dt.microsecond,
287
+ tzinfo=dt.tzinfo,
288
+ )
289
+
290
+ elif isinstance(args[0], datetime.date):
291
+ instance = super().__new__(
292
+ cls,
293
+ args[0].year,
294
+ args[0].month,
295
+ args[0].day,
296
+ 0,
297
+ 0,
298
+ 0,
299
+ 0,
300
+ tzinfo=datetime.timezone.utc,
301
+ )
302
+
303
+ elif isinstance(args[0], (int, float)):
304
+ try:
305
+ instance = cls.fromtimestamp(
306
+ args[0],
307
+ tz=datetime.timezone.utc
308
+ )
309
+
310
+ except ValueError as e:
311
+ raise ValueError(
312
+ f"timestamp {args[0]} is out of bounds"
313
+ ) from e
314
+
315
+ except OSError:
316
+ if isinstance(args[0], int):
317
+ timestamp = str(args[0])
318
+ s, ms = str(time.time()).split(".")
319
+
320
+ seconds = timestamp[:len(s)]
321
+ milliseconds = timestamp[len(s):]
322
+ instance = cls.fromtimestamp(
323
+ float(f"{seconds}.{milliseconds}"),
324
+ tz=datetime.timezone.utc
325
+ )
326
+
327
+ else:
328
+ raise
329
+
330
+
331
+ else:
332
+ if args[0]:
333
+ try:
334
+ # if the object is pickled we would get the pickled
335
+ # string as our one passed in value
336
+ instance = super().__new__(cls, *args)
337
+
338
+ except TypeError:
339
+ fs = cls.parse(args[0])
340
+ if fs is None:
341
+ raise
342
+
343
+ else:
344
+ instance = fs
345
+
346
+ else:
347
+ instance = cls.now(datetime.timezone.utc)
348
+
349
+ else:
350
+ instance = super().__new__(cls, *args, **replace_kwargs)
351
+ replace_kwargs = {} # we've consumed them
352
+
353
+ #instance = instance.replace_timedelta(**kw)
354
+ if replace_kwargs or timedelta_kwargs:
355
+ instance = instance.replace(**replace_kwargs, **timedelta_kwargs)
356
+
357
+ return instance
358
+
359
+ def __str__(self):
360
+ return self.isoformat()
361
+
362
+ def __add__(self, other):
363
+ return type(self)(super().__add__(other))
364
+
365
+ def __sub__(self, other):
366
+ if isinstance(other, datetime.timedelta):
367
+ return type(self)(super().__sub__(other))
368
+ else:
369
+ return super(Datetime, self).__sub__(other)
370
+
371
+ def __eq__(self, other):
372
+ if isinstance(other, datetime.date) and \
373
+ not isinstance(other, datetime.datetime):
374
+ return self.date() == other
375
+
376
+ else:
377
+ return super().__eq__(other.astimezone(self.tzinfo))
378
+
379
+ def __lt__(self, other):
380
+ if isinstance(other, datetime.date) and \
381
+ not isinstance(other, datetime.datetime):
382
+ return self.date() < other
383
+
384
+ else:
385
+ return super(Datetime, self).__lt__(other.astimezone(self.tzinfo))
386
+
387
+ def __gt__(self, other):
388
+ return not self.__lt__(other) and not self.__eq__(other)
389
+
390
+ def __ge__(self, other):
391
+ return not self.__lt__(other) or self.__eq__(other)
392
+
393
+ def __le__(self, other):
394
+ return self.__lt__(other) or self.__eq__(other)
395
+
396
+ def __ne__(self, other):
397
+ return not self.__eq__(other)
398
+
399
+ def __hash__(self):
400
+ # https://stackoverflow.com/questions/10254594/what-makes-a-user-defined-class-unhashable
401
+ return super().__hash__()
402
+
403
+ def has_time(self):
404
+ return not (
405
+ self.hour == 0 and
406
+ self.minute == 0 and
407
+ self.second == 0 and
408
+ self.microsecond == 0
409
+ )
410
+
411
+ def timestamp(self):
412
+ """
413
+ return the current utc timestamp of self
414
+
415
+ http://crazytechthoughts.blogspot.com/2012/02/get-current-utc-timestamp-in-python.html
416
+
417
+ :returns: float, the current utc timestamp with microsecond precision
418
+ """
419
+ # this only returns second precision, which is why we don't use it
420
+ #now = calendar.timegm(datetime.datetime.utcnow().utctimetuple())
421
+
422
+ # this returns microsecond precision
423
+ # http://bugs.python.org/msg180110
424
+ epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
425
+ return (self - epoch).total_seconds()
426
+
427
+ def timestamp_ns(self):
428
+ """Similar to .timestamp() but returns time as an integer number of
429
+ nanoseconds since the epoch
430
+
431
+ see time.time_ns()
432
+
433
+ :returns: int
434
+ """
435
+ size = len(str(time.time_ns()))
436
+ timestamp = str(self.timestamp()).replace(".", "")
437
+ return int("{{:0<{}}}".format(size).format(timestamp))
438
+
439
+ def since(self, now=None, chunks=2):
440
+ """Returns a description of the amount of time that has passed from self
441
+ to now. This is more exact than .estsince() but does a lot more
442
+ computation to make it accurate
443
+
444
+ :param now: datetime, if None will default to the current datetime
445
+ :param chunks: int, if a postive value only that many chunks will be
446
+ returned, if None or -1 then all found chunks will be returned
447
+ :returns: str, the elapsed time in english (eg, x years, xx months, or
448
+ x days, xx hours)
449
+ """
450
+ output = []
451
+
452
+ # time period chunks that aren't variable
453
+ period_chunks = [
454
+ (60 * 60 * 24 * 7, 'week', 'weeks'),
455
+ (60 * 60 * 24 , 'day', 'days'),
456
+ (60 * 60 , 'hour', 'hours'),
457
+ (60 , 'minute', 'minutes'),
458
+ (1 , 'second', 'seconds'),
459
+ ]
460
+
461
+ # now will equal None if we want to know the time elapsed between self
462
+ # and the current time, otherwise it will be between self and now
463
+ if not chunks or chunks < 0:
464
+ chunks = len(period_chunks) + 2
465
+
466
+ now = Datetime(now)
467
+ months = 0
468
+ month_days = 0
469
+ for month in self.months(now, inclusive=False):
470
+ months += 1
471
+ month_days += month.days_in_month()
472
+
473
+ if months:
474
+ years = months // 12
475
+ if years:
476
+ name = "year" if years == 1 else "years"
477
+ output.append(f"{years} {name}")
478
+ chunks -= 1
479
+
480
+ if chunks > 0:
481
+ name = "month" if months == 1 else "months"
482
+ output.append(f"{months} {name}")
483
+ chunks -= 1
484
+
485
+ # difference in seconds
486
+ since = (now - self).total_seconds()
487
+ since -= (period_chunks[1][0] * month_days)
488
+
489
+ for seconds, name, names in period_chunks:
490
+ if count := math.floor(since / seconds):
491
+ output.append(f"1 {name}" if count == 1 else f"{count} {names}")
492
+ since -= (seconds * count)
493
+ chunks -= 1
494
+ if since <= 0 or chunks <= 0:
495
+ break
496
+
497
+ return ", ".join(output) if output else ""
498
+
499
+ def estsince(self, now=None, chunks=2):
500
+ """Returns a ballpark/estimate description of the amount of time that
501
+ has passed from self to now
502
+
503
+ This is based on Plancast's Formatting.php timeSince method
504
+
505
+ NOTE -- this can drift since it uses 30 days for the months, you can see
506
+ this by doing:
507
+
508
+ d = Datetime(months=-5, days=-3)
509
+ d.since(now=Datetime(month=8, day=1, year=2023)) # 5 months, 6 days
510
+
511
+ :param now: datetime, if None will default to the current datetime
512
+ :param chunks: int, if a postive value only that many chunks will be
513
+ returned, if None or -1 then all found chunks will be returned
514
+ :returns: str, the elapsed time in english (eg, x years, xx months, or
515
+ x days, xx hours)
516
+ """
517
+ output = []
518
+
519
+ # time period chunks
520
+ period_chunks = [
521
+ (60 * 60 * 24 * 365 , 'year', 'years'),
522
+ (60 * 60 * 24 * 30 , 'month', 'months'),
523
+ (60 * 60 * 24 * 7, 'week', 'weeks'),
524
+ (60 * 60 * 24 , 'day', 'days'),
525
+ (60 * 60 , 'hour', 'hours'),
526
+ (60 , 'minute', 'minutes'),
527
+ (1 , 'second', 'seconds'),
528
+ ]
529
+
530
+ # now will equal None if we want to know the time elapsed between self
531
+ # and the current time, otherwise it will be between self and now
532
+ now = now or self.now(self.tzinfo)
533
+ if not chunks or chunks < 0:
534
+ chunks = len(period_chunks)
535
+
536
+ # difference in seconds
537
+ since = (now - self).total_seconds()
538
+
539
+ for seconds, name, names in period_chunks:
540
+ if count := math.floor(since / seconds):
541
+ output.append(f"1 {name}" if count == 1 else f"{count} {names}")
542
+ since -= (seconds * count)
543
+ chunks -= 1
544
+ if since <= 0 or chunks <= 0:
545
+ break
546
+
547
+ return ", ".join(output) if output else ""
548
+ def esince(self, *args, **kwargs):
549
+ return self.estsince(*args, **kwargs)
550
+
551
+ def datehash(self):
552
+ """Return a datehash, kind of similar to a geohash where each character
553
+ represents a more exact time
554
+
555
+ This is something I've noodled on since Plancast days, and I wanted to
556
+ finally have a canonical place where I could mess with it and track
557
+ changes
558
+
559
+ :returns: str, A 7 character string, with indexes that represent:
560
+ 0,1 - the year
561
+ 2 - month
562
+ 3 - day
563
+ 4 - hour
564
+ 5 - minute
565
+ 6 - second
566
+ """
567
+ h = ""
568
+ chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
569
+
570
+ # year
571
+ h += chars[self.year // 100]
572
+ h += chars[self.year % 100]
573
+
574
+ # month
575
+ h += chars[self.month]
576
+
577
+ # day
578
+ h += chars[self.day]
579
+
580
+ if self.has_time():
581
+ h += chars[self.hour]
582
+ h += chars[self.minute]
583
+ h += chars[self.second]
584
+
585
+ return h
586
+
587
+ @classmethod
588
+ def fromdatehash(cls, h):
589
+ chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
590
+ indexes = {v:k for k, v in enumerate(chars)}
591
+
592
+ year = int(str(indexes[h[0]]) + str(indexes[h[1]]))
593
+ month = indexes[h[2]]
594
+ day = indexes[h[3]]
595
+
596
+ hour = minute = second = 0
597
+ if len(h) >= 4:
598
+ hour = indexes[h[4]]
599
+
600
+ if len(h) >= 5:
601
+ minute = indexes[h[5]]
602
+
603
+ if len(h) >= 6:
604
+ second = indexes[h[6]]
605
+
606
+ return cls(year, month, day, hour, minute, second)
607
+
608
+ @classmethod
609
+ def now(cls, tz=datetime.timezone.utc):
610
+ """Passthrough but sets timezone to utc by default instead of None, so
611
+ by default this now returns a timezone aware instance
612
+
613
+ https://docs.python.org/3/library/datetime.html#datetime.datetime.now
614
+ """
615
+ return super().now(tz)
616
+
617
+ @classmethod
618
+ def utcnow(cls):
619
+ """Overrides parent to return a timezone aware datetime instance with
620
+ the timezone UTC set
621
+
622
+ https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow
623
+ """
624
+ return cls.now(datetime.timezone.utc)
625
+
626
+ def isodate(self):
627
+ """returns datetime as ISO-8601 string with just YYYY-MM-DD"""
628
+ return self.strftime(self.FORMAT_ISO8601_DAY)
629
+ iso_day = isodate
630
+ isoday = isodate
631
+ iso_date = isodate
632
+
633
+ def isoseconds(self):
634
+ """returns datetime as ISO-8601 string with no milliseconds"""
635
+ return self.isoformat(timespec="seconds")
636
+ iso_seconds = isoseconds
637
+
638
+ def iso8601(self):
639
+ """returns datetime as a full ISO-8601 string with milliseconds"""
640
+ return self.isoformat(timespec="microseconds")
641
+ iso_8601 = iso8601
642
+
643
+ def isofull(self):
644
+ return self.isoformat(timespec="microseconds")
645
+
646
+ def isoformat(self, sep="T", timespec="auto"):
647
+ """provides python 3 compatible isoformat even for py2
648
+
649
+ https://docs.python.org/3/library/datetime.html#datetime.datetime.isoformat
650
+
651
+ Differences between this and builtin datetime.isoformat:
652
+
653
+ * this will always include a timezone, defaulting to Z if no other
654
+ timezone is present
655
+ * This will return YYYY-MM-DD if no time information is set and
656
+ timespec is "auto"
657
+
658
+ :returns: string, the iso8601 date
659
+ """
660
+ if self.has_time():
661
+ if self.utcoffset():
662
+ ret = super().isoformat(sep, timespec)
663
+
664
+ else:
665
+ ret = self.datetime().replace(tzinfo=None).isoformat(
666
+ sep,
667
+ timespec
668
+ )
669
+ ret += "Z"
670
+
671
+ else:
672
+ if timespec == "auto":
673
+ ret = self.date().isoformat()
674
+
675
+ else:
676
+ ret = self.datetime().replace(tzinfo=None).isoformat(
677
+ sep,
678
+ timespec
679
+ )
680
+ ret += "Z"
681
+
682
+ return ret
683
+
684
+ def within(self, start, stop, now=None):
685
+ """return True if this datetime is within start and stop dates
686
+
687
+ :param start: int|timedelta|datetime, if int, then seconds from now, so a
688
+ negative integer would be in the past from now
689
+ :param stop: int|timedelta|datetime, same as start
690
+ :param now: datetime, what you want now to be
691
+ :returns: boolean, True if self is between start and stop
692
+ """
693
+ if not now:
694
+ now = type(self)()
695
+
696
+ if isinstance(start, int):
697
+ start = now + datetime.timedelta(seconds=start)
698
+ start = type(self)(start)
699
+
700
+ if isinstance(stop, int):
701
+ stop = now + datetime.timedelta(seconds=stop)
702
+ stop = type(self)(stop)
703
+
704
+ return start <= self <= stop
705
+
706
+ def strftime(self, *args, **kwargs):
707
+ """Make sure strftime always returns a unicode object"""
708
+
709
+ # this was moved here on 2021-5-5 from prom.config.Field.jsonable
710
+ try:
711
+ val = super(Datetime, self).strftime(*args, **kwargs)
712
+
713
+ except ValueError as e:
714
+ # strftime can fail on dates <1900
715
+ # Note that Python 2.7, 3.0 and 3.1 have errors before the year 1900,
716
+ # Python 3.2 has errors before the year 1000. Additionally, pre-3.2
717
+ # versions interpret years between 0 and 99 as between 1969 and 2068.
718
+ # Python versions from 3.3 onward support all positive years in
719
+ # datetime (and negative years in time.strftime), and time.strftime
720
+ # doesn't do any mapping of years between 0 and 99.
721
+ # https://stackoverflow.com/a/32206673/5006
722
+ logger.warning(e, exc_info=True)
723
+
724
+ # we correct this issue by just giving it a dumb year,
725
+ # creating the timestamp and then replacing the year, we can
726
+ # do this semi-confidently because our format_str doesn't have
727
+ # day of the week (eg, Monday), we account for leap years
728
+ # just in case
729
+ orig_year = self.year
730
+ if (orig_year % 4) == 0:
731
+ if (orig_year % 100) == 0:
732
+ if (orig_year % 400) == 0:
733
+ placeholder_year = 2000
734
+
735
+ else:
736
+ placeholder_year = 1900
737
+
738
+ else:
739
+ placeholder_year = 2012
740
+
741
+ else:
742
+ placeholder_year = 1997
743
+
744
+ dt = self.replace(year=placeholder_year)
745
+ val = dt.strftime(*args, **kwargs)
746
+ val = re.sub(r"^{}".format(placeholder_year), String(orig_year), val)
747
+
748
+ return String(val)
749
+
750
+ def replace(self, *args, **kwargs):
751
+ """Overrides default behavior to account for custom behavior
752
+
753
+ https://docs.python.org/3/library/datetime.html#datetime.datetime.replace
754
+
755
+ Returns a new datetime instance with any time deltas applied
756
+
757
+ :param timedelta: datetime.timedelta
758
+ :param **kwargs: besides the standard keywords, this can also have keyword
759
+ values for: months, weeks, days, hours, seconds, and timedelta
760
+ :returns: Datetime
761
+ """
762
+ args, replace_kwargs, timedelta_kwargs = self.parse_args(args, kwargs)
763
+
764
+ if replace_kwargs:
765
+ dt = super().replace(*args, **replace_kwargs)
766
+
767
+ else:
768
+ dt = super().replace(*args)
769
+
770
+ if timedelta := timedelta_kwargs.pop("timedelta", None):
771
+ dt = dt + timedelta
772
+
773
+ if timedelta_kwargs:
774
+ if "years" in timedelta_kwargs:
775
+ year = int(timedelta_kwargs.pop("years"))
776
+ dt = dt.replace(self.year + year)
777
+
778
+ if "months" in timedelta_kwargs:
779
+ months = int(timedelta_kwargs.pop("months"))
780
+ # https://stackoverflow.com/a/546354/5006
781
+ year = self.year + (
782
+ (
783
+ (
784
+ self.month + (months - 1 if months else months)
785
+ ) or -1
786
+ ) // 12
787
+ )
788
+ month = ((self.month + months) % 12) or 12
789
+ day = self.day - 1
790
+ dt = dt.replace(year, month, 1) + datetime.timedelta(days=day)
791
+
792
+ dt += datetime.timedelta(**timedelta_kwargs)
793
+
794
+ return dt
795
+
796
+ def datetime(self):
797
+ """Similar to .date() but returns vanilla datetime instance"""
798
+ return datetime.datetime(
799
+ self.year,
800
+ self.month,
801
+ self.day,
802
+ self.hour,
803
+ self.minute,
804
+ self.second,
805
+ self.microsecond,
806
+ tzinfo=self.tzinfo,
807
+ )
808
+
809
+ def days_in_month(self):
810
+ """Return how many days there are in self.month
811
+
812
+ Months with 28 or 20 days:
813
+ * February
814
+
815
+ Months with 30 days:
816
+ * April
817
+ * June
818
+ * September
819
+ * November
820
+
821
+ Months with 31 days:
822
+ * January
823
+ * March
824
+ * May
825
+ * July
826
+ * August
827
+ * October
828
+ * December
829
+ """
830
+ _, monthlen = calendar.monthrange(self.year, self.month)
831
+ return monthlen
832
+
833
+ def next_month(self):
834
+ """Returns the first day of the next month from self"""
835
+ month = self.month
836
+ year = self.year
837
+ if month == 12:
838
+ month = 1
839
+ year += 1
840
+
841
+ else:
842
+ month += 1
843
+
844
+ return Datetime(year, month, 1)
845
+
846
+ def prev_month(self):
847
+ """Returns the first day of the previous month from self"""
848
+ month = self.month
849
+ year = self.year
850
+ if month == 1:
851
+ month = 12
852
+ year -= 1
853
+
854
+ else:
855
+ month -= 1
856
+
857
+ return Datetime(year, month, 1)
858
+
859
+ def current_month(self):
860
+ """Returns the first day of the current month from self"""
861
+ return Datetime(self.year, self.month, 1)
862
+
863
+ def months(self, now=None, inclusive=True):
864
+ """Returns all the months between self and now
865
+
866
+ :param now: datetime, if None then defaults to now
867
+ :param inclusive: bool, by default, if self was Jan and now was Aug and
868
+ you iterated through the months it would be: Jan, Feb, Mar, Apr, May
869
+ June, July, Aug, because it includes the edges. If inclusive is
870
+ False then it would be: Feb, Mar, Apr, May, june, July
871
+ :returns: generator[datetime], each datetime instance will be midnight
872
+ on the first day of that month
873
+ """
874
+ now = Datetime(now)
875
+ month = self.current_month() if inclusive else self.next_month()
876
+ now_month = now.current_month() if inclusive else now.prev_month()
877
+
878
+ if month >= now_month:
879
+ if inclusive:
880
+ yield self.current_month()
881
+
882
+ else:
883
+ while month < now_month:
884
+ yield month
885
+ month = month.next_month()
886
+
887
+ yield month
888
+