batutils 2.0.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 (73) hide show
  1. batutils/__init__.py +5 -0
  2. batutils/_common/__init__.py +6 -0
  3. batutils/_common/_decode.py +40 -0
  4. batutils/dtu/__init__.py +19 -0
  5. batutils/dtu/casting/__init__.py +5 -0
  6. batutils/dtu/casting/_any.py +136 -0
  7. batutils/dtu/casting/_filetime.py +36 -0
  8. batutils/dtu/casting/_instant.py +31 -0
  9. batutils/dtu/casting/_string.py +81 -0
  10. batutils/dtu/casting/_timestamp.py +50 -0
  11. batutils/dtu/formatting/__init__.py +2 -0
  12. batutils/dtu/formatting/_delta.py +62 -0
  13. batutils/dtu/formatting/_serialize.py +26 -0
  14. batutils/dtu/parsers/__init__.py +2 -0
  15. batutils/dtu/parsers/_string.py +44 -0
  16. batutils/dtu/parsers/_timedelta.py +161 -0
  17. batutils/dtu/shortcuts/__init__.py +3 -0
  18. batutils/dtu/shortcuts/_aliases.py +29 -0
  19. batutils/dtu/shortcuts/_checkers.py +21 -0
  20. batutils/dtu/shortcuts/_finders.py +80 -0
  21. batutils/dtu/structs/__init__.py +5 -0
  22. batutils/dtu/structs/_base.py +5 -0
  23. batutils/dtu/structs/_deltasplit.py +278 -0
  24. batutils/dtu/structs/_types.py +24 -0
  25. batutils/dtu/structs/aliases.py +21 -0
  26. batutils/dtu/structs/consts.py +35 -0
  27. batutils/structs/__init__.py +7 -0
  28. batutils/structs/_base.py +21 -0
  29. batutils/tpu/__init__.py +0 -0
  30. batutils/tpu/casting/__init__.py +9 -0
  31. batutils/tpu/casting/_currency.py +52 -0
  32. batutils/tpu/casting/_evals.py +247 -0
  33. batutils/tpu/casting/_ip.py +58 -0
  34. batutils/tpu/casting/_mask.py +34 -0
  35. batutils/tpu/casting/_params.py +108 -0
  36. batutils/tpu/casting/_serialize.py +20 -0
  37. batutils/tpu/comparison/__init__.py +1 -0
  38. batutils/tpu/comparison/_mixin.py +30 -0
  39. batutils/tpu/insp/__init__.py +2 -0
  40. batutils/tpu/insp/_insp_old.py +347 -0
  41. batutils/tpu/insp/_typecheck.py +89 -0
  42. batutils/tpu/iters/__init__.py +4 -0
  43. batutils/tpu/iters/_attrs.py +84 -0
  44. batutils/tpu/iters/_dicts.py +130 -0
  45. batutils/tpu/iters/_group.py +50 -0
  46. batutils/tpu/iters/_lists.py +178 -0
  47. batutils/tpu/size/__init__.py +1 -0
  48. batutils/tpu/size/_total.py +52 -0
  49. batutils/tpu/strimp/__init__.py +1 -0
  50. batutils/tpu/strimp/_getters.py +285 -0
  51. batutils/tpu/string/__init__.py +1 -0
  52. batutils/tpu/string/_string.py +160 -0
  53. batutils/tpu/structs/__init__.py +1 -0
  54. batutils/tpu/structs/_base.py +3 -0
  55. batutils/tpu/structs/empty/__init__.py +1 -0
  56. batutils/tpu/structs/empty/_empty.py +387 -0
  57. batutils/tpu/structs/enumex/__init__.py +1 -0
  58. batutils/tpu/structs/enumex/_enumextra.py +25 -0
  59. batutils/tpu/structs/fluid/__init__.py +1 -0
  60. batutils/tpu/structs/fluid/_attrfluid.py +77 -0
  61. batutils/tpu/structs/sentience/__init__.py +1 -0
  62. batutils/tpu/structs/sentience/_builder.py +87 -0
  63. batutils/tpu/structs/serializers/__init__.py +3 -0
  64. batutils/tpu/structs/serializers/_json.py +12 -0
  65. batutils/tpu/structs/serializers/_jsonsafe.py +69 -0
  66. batutils/tpu/structs/serializers/_universal.py +318 -0
  67. batutils/tpu/structs/singleton/__init__.py +1 -0
  68. batutils/tpu/structs/singleton/_singleton.py +35 -0
  69. batutils-2.0.0.dist-info/LICENSE +22 -0
  70. batutils-2.0.0.dist-info/METADATA +125 -0
  71. batutils-2.0.0.dist-info/RECORD +73 -0
  72. batutils-2.0.0.dist-info/WHEEL +5 -0
  73. batutils-2.0.0.dist-info/top_level.txt +1 -0
batutils/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ __version__ = '2.0.0'
2
+
3
+ __author__ = 'Thordur Matthiasson <doddi79@gmail.com>'
4
+ __license__ = 'MIT License'
5
+ __copyright__ = 'Copyright 2024 - Thordur Matthiasson, Copyright 2013-2024 - CCP Games ehf'
@@ -0,0 +1,6 @@
1
+ """Here we should place code that is used in multiple submodules so they don't
2
+ import from one another, thus enabling us to use any single submodule without
3
+ everything else tagging along.
4
+
5
+ """
6
+ from ._decode import *
@@ -0,0 +1,40 @@
1
+ __all__ = [
2
+ 'decode_bytes',
3
+ ]
4
+
5
+ _ENCODINGS_TO_TRY = (
6
+ 'utf-8',
7
+ 'latin-1',
8
+ 'utf-16',
9
+ 'utf-32',
10
+ 'windows-1252',
11
+ 'big5',
12
+ 'shift_jis',
13
+ 'gb18030',
14
+ 'iso-8859-15'
15
+ )
16
+
17
+
18
+ def decode_bytes(byte_str: bytes) -> str:
19
+ """Tries to decode a bytes string into a str by trial-and-error, first using
20
+ the default encoding of the current runtime environment, and then by going
21
+ through a list of the most commonly used encodings used at the time of this
22
+ writing, in September 2023, as estimated by ChatGTP 4. :D
23
+
24
+ :raises ValueError: If it fails to decode the given bytes using any of the
25
+ encodings attempted
26
+ """
27
+ try: # First we try to Decode with default encoding!
28
+ return byte_str.decode()
29
+ except UnicodeDecodeError:
30
+ pass
31
+
32
+ # If not, then we go through the list...!
33
+ for enc in _ENCODINGS_TO_TRY:
34
+ try:
35
+ return byte_str.decode(enc)
36
+ except UnicodeDecodeError:
37
+ pass
38
+
39
+ # If we reach here, then we've failed to decode :(
40
+ raise ValueError(f'failed to decode bytes to str by trial and error: {byte_str!r}')
@@ -0,0 +1,19 @@
1
+ """This is the main API module of datetimeutils of batutils.
2
+
3
+ The intended use case is to go:
4
+ ```python
5
+ from batutils import dtu
6
+ ```
7
+
8
+ And then use `dtu` in your code as it serves as the "official" API of the
9
+ package.
10
+ """
11
+ from batutils.dtu.structs import *
12
+
13
+ from batutils.dtu.shortcuts import *
14
+
15
+ from batutils.dtu.casting import *
16
+ from batutils.dtu.formatting import *
17
+ from batutils.dtu.parsers import *
18
+
19
+ from batutils import __version__ as VERSION # noqa
@@ -0,0 +1,5 @@
1
+ from ._filetime import *
2
+ from ._string import *
3
+ from ._timestamp import *
4
+ from ._instant import *
5
+ from ._any import *
@@ -0,0 +1,136 @@
1
+ __all__ = [
2
+ 'any_to_datetime',
3
+ ]
4
+ import warnings
5
+
6
+ from batutils.dtu.structs import *
7
+ from batutils._common import *
8
+ from batutils.dtu.casting._filetime import *
9
+ from batutils.dtu.casting._string import *
10
+ from batutils.dtu.casting._timestamp import *
11
+ from batutils.dtu.casting._instant import *
12
+
13
+ _NOT_SUPPLIED = object()
14
+
15
+ _now = Datetime.now()
16
+ _1000_years_plus = _now.replace(_now.year + 1000)
17
+ _1000_years_minus = _now.replace(_now.year - 1000)
18
+
19
+ _TIMESTAMP_MIN_RANGE = datetime_to_timestamp(_1000_years_minus)
20
+ _TIMESTAMP_MAX_RANGE = datetime_to_timestamp(_1000_years_plus)
21
+
22
+ _INSTANT_MIN_RANGE = datetime_to_instant(_1000_years_minus)
23
+ _INSTANT_MAX_RANGE = datetime_to_instant(_1000_years_plus)
24
+
25
+ _REVERSE_DATETIME_REXEX = re.compile(r'(?P<day>3[01]|[012]?\d)[- /.,\\](?P<month>1[012]|0?\d)[- /.,\\]'
26
+ r'(?P<year>[12][0189]\d{2})(?:[ @Tt]{0,1}(?:(?P<hour>[2][0-3]|[01]?\d)[ .:,]'
27
+ r'(?P<minute>[012345]?\d))?(?:[ .:,](?P<second>[012345]?\d)'
28
+ r'(?:[ .:,](?P<millisecond>\d{0,6}))?)?)?')
29
+ _REVERSE_US_DATETIME_REXEX = re.compile(r'(?P<month>1[012]|0?\d)[- /.,\\](?P<day>3[01]|[012]?\d)[- /.,\\]'
30
+ r'(?P<year>[12][0189]\d{2})(?:[ @Tt]{0,1}(?:(?P<hour>[2][0-3]|[01]?\d)[ .:,]'
31
+ r'(?P<minute>[012345]?\d))?(?:[ .:,](?P<second>[012345]?\d)'
32
+ r'(?:[ .:,](?P<millisecond>\d{0,6}))?)?)?')
33
+
34
+
35
+ def any_to_datetime(temporal_object: T_TEMPORAL_VALUE,
36
+ default: Any = _NOT_SUPPLIED) -> Union[Datetime, Any]:
37
+ """Turns datetime, date, Windows filetime and posix time into a python
38
+ datetime if possible. By default, returns the same input value on failed
39
+ casting but another default return value can be given.
40
+
41
+ This function is timezone naive.
42
+
43
+ If given a number the following trickery is performed:
44
+
45
+ - If the number, treated as a timestamp, represents a datetime value that is
46
+ within 1000 years of now (past or future) it will be treated as a timestamp
47
+ because timestamps are the most commonly used numerical representations of
48
+ datetimes
49
+ - If the number is outside that range, we'll check if the number would be
50
+ within 1000 years of now if treated as an instant.
51
+ - Otherwise, we assume that such a large number must be a filetime
52
+
53
+ This does mean that there are certain cases that will yield incorrect
54
+ results, including:
55
+
56
+ - Timestamps more than 1000 years in the past or future (they'll be
57
+ treated as instants or filetimes)
58
+ - Instants within a couple of years of 1970 (1969-01-20 to 1971-01-22 at
59
+ the time of this writing) will be treated as timestamps
60
+ - Filetimes for the years 1600-1601 might get treated as instants or
61
+ timestamps
62
+
63
+ Concerning strings, there are also potential pitfalls if casting US
64
+ formatted YYYY-DD-MM strings, as this method will FIRST assume a standard
65
+ ISO format and only try the US one if that fails, so US formatted strings
66
+ with days between 1 and 12 will be treated as ISO and have their day and
67
+ month numbers switched.
68
+
69
+ Note: This is a "best-guess" method and if these edge cases are
70
+ unacceptable, you should totally not be using it, and instead, know exactly
71
+ what format your data is in and use the appropriate specific casting method.
72
+ """
73
+ if default == _NOT_SUPPLIED:
74
+ default = temporal_object
75
+ try:
76
+ if isinstance(temporal_object, Datetime):
77
+ return temporal_object
78
+
79
+ if isinstance(temporal_object, Date):
80
+ return datetime.datetime.combine(temporal_object, Time())
81
+
82
+ if isinstance(temporal_object, (float, int)):
83
+ if _TIMESTAMP_MIN_RANGE < temporal_object < _TIMESTAMP_MAX_RANGE:
84
+ # This range means that the number, if treated as a timestamp,
85
+ # represents a datetime within 1000 years to/from now so it's the
86
+ # most likely bet!
87
+ return timestamp_to_datetime(temporal_object)
88
+
89
+ if _INSTANT_MIN_RANGE < temporal_object < _INSTANT_MAX_RANGE:
90
+ # This range means that the number, if treated as an instant,
91
+ # represents a datetime within 1000 years to/from now so it's the
92
+ # second most likely bet!
93
+ return timestamp_to_datetime(temporal_object)
94
+
95
+ # This number is so large that it's most likely a filetime!
96
+ return filetime_to_datetime(temporal_object)
97
+
98
+ if isinstance(temporal_object, bytes):
99
+ try:
100
+ temporal_object = decode_bytes(temporal_object)
101
+ except ValueError:
102
+ return temporal_object
103
+
104
+ if isinstance(temporal_object, str):
105
+ # Is this a number in string format?
106
+ try:
107
+ # Let's just try and pass this through the int caster, if that works, we evaluate it again as an int
108
+ return any_to_datetime(int(temporal_object))
109
+ except (TypeError, ValueError):
110
+ pass
111
+
112
+ try:
113
+ # Let's just try and pass this through the float caster, if that works, we evaluate it again as an int
114
+ return any_to_datetime(float(temporal_object))
115
+ except (TypeError, ValueError):
116
+ pass
117
+
118
+ # First we'll try the day-month-year pattern
119
+ value = regex_to_datetime(temporal_object, _REVERSE_DATETIME_REXEX)
120
+ if value:
121
+ return value
122
+
123
+ # Then the month-day-year pattern
124
+ value = regex_to_datetime(temporal_object, _REVERSE_US_DATETIME_REXEX)
125
+ if value:
126
+ return value
127
+
128
+ # How'bout good old ISO year-month-day then? :D
129
+ value = isostr_to_datetime(temporal_object)
130
+ if value:
131
+ return value
132
+
133
+ except (OverflowError, ValueError):
134
+ pass # Oh well
135
+
136
+ return default
@@ -0,0 +1,36 @@
1
+ __all__ = [
2
+ 'filetime_to_datetime',
3
+ 'datetime_to_filetime',
4
+ ]
5
+
6
+ from batutils.dtu.structs import *
7
+
8
+ _FILETIME_NULL_DATE = Datetime(1601, 1, 1, 0, 0, 0)
9
+
10
+
11
+ def filetime_to_datetime(filetime: T_NUMBER) -> Datetime:
12
+ """Converts a Windows file time value (number of 100-nanosecond ticks since
13
+ 1 January 1601 00:00:00 UTC) to a standard python datetime.
14
+ Valid values are approx. -5.04911232e17 to 2.65046774399999999e18
15
+
16
+ This function is timezone naive.
17
+
18
+ :raise OverflowError: if filetime value is out of the range of python
19
+ datetime (between the year 1 and 9999 AD)
20
+ """
21
+ return _FILETIME_NULL_DATE + TimeDelta(microseconds=(filetime / 10))
22
+
23
+
24
+ def datetime_to_filetime(dt: T_DATE_VALUE) -> int:
25
+ """Converts a python datetime (or date) object to a Windows filetime value
26
+ (number of 100-nanosecond ticks since 1 January 1601 00:00:00 UTC).
27
+
28
+ If the supplied parameter is a date as opposed to a datetime, it will first
29
+ be converted locally to a datetime with the time 00:00:00.000000
30
+
31
+ This function is timezone naive.
32
+ """
33
+ if not isinstance(dt, Datetime) and isinstance(dt, Date):
34
+ dt = Datetime.combine(dt, Time(0, 0, 0))
35
+ delta = dt - _FILETIME_NULL_DATE
36
+ return 10 * ((delta.days * 86400 + delta.seconds) * 1000000 + delta.microseconds)
@@ -0,0 +1,31 @@
1
+ __all__ = [
2
+ 'instant_to_datetime',
3
+ 'datetime_to_instant',
4
+ ]
5
+ from batutils.dtu.structs import *
6
+ from ._timestamp import *
7
+
8
+
9
+ def instant_to_datetime(milliseconds_since_epoch: T_NUMBER, minmax_on_fail: bool = False) -> Datetime:
10
+ """Converts an integer representing milliseconds since the Unix epoch
11
+ (January 1, 1970) to a Python datetime object.
12
+
13
+ :param milliseconds_since_epoch: Milliseconds since Unix epoch (January 1, 1970).
14
+ :param minmax_on_fail: If True, will return the minimum or maximum possible
15
+ value of Datetime in case of overflow (positive or
16
+ negative)
17
+ :return: A Python Datetime
18
+ """
19
+ return timestamp_to_datetime(milliseconds_since_epoch / 1000., minmax_on_fail)
20
+
21
+
22
+ def datetime_to_instant(dt: T_DATE_VALUE) -> int:
23
+ """Converts a Python datetime object to the number of milliseconds since
24
+ Unix epoch (January 1, 1970).
25
+
26
+ If given a date only, it will assume a time of 00:00:00.000000.
27
+
28
+ :param dt: Python datetime (or date).
29
+ :return: Number of milliseconds since Unix epoch (January 1, 1970)
30
+ """
31
+ return int(datetime_to_timestamp(dt) * 1000)
@@ -0,0 +1,81 @@
1
+ __all__ = [
2
+ 'regex_to_datetime',
3
+ 'isostr_to_datetime',
4
+ ]
5
+
6
+ from batutils.dtu.structs import *
7
+ from typing import *
8
+
9
+ _T_PATTERN = Union[re.Pattern, str]
10
+
11
+ # Rexex from HELL! ;)
12
+ _ISODATE_REGEX = re.compile(r'^(?:(?P<year>[0]{0,3}[1-9]\d{0,3})[- /.,\\]'
13
+ r'(?P<month>1[012]|0?\d)[- /.,\\](?P<day>3[01]|[012]?\d))?'
14
+ r'(?:[ @Tt]{0,1}(?:(?P<hour>[2][0-3]|[01]?\d)[ .:,]'
15
+ r'(?P<minute>[012345]?\d))?(?:[ .:,](?P<second>[012345]?\d)'
16
+ r'(?:[ .:,](?P<millisecond>\d{0,6}))?)?)?')
17
+
18
+
19
+ def regex_to_datetime(string: str, regex_pattern: _T_PATTERN) -> Optional[Datetime]:
20
+ """Given a string and a regex pattern with named groups with the same
21
+ keywords as the datetime object takes, this method uses that pattern to grab
22
+ those keywords and initialize and return a datetime object.
23
+
24
+ If only date keywords are found, the resulting datetime will have its time
25
+ set to midnight (0:00:00.0).
26
+
27
+ If only time keywords are found, the resulting datetime will have its date
28
+ set to today's date.
29
+
30
+ Failing to find a valid pattern this will simply return a None.
31
+ """
32
+ if isinstance(regex_pattern, str):
33
+ regex_pattern = re.compile(regex_pattern)
34
+ match = regex_pattern.match(string.strip())
35
+ if match:
36
+ parts = match.groupdict()
37
+ found_date = False
38
+ if parts['year'] and parts['month'] and parts['day']:
39
+ date_part = Date(int(parts['year']), int(parts['month']), int(parts['day']))
40
+ found_date = True
41
+ else:
42
+ date_part = Date.today()
43
+
44
+ found_time = False
45
+ if parts['hour'] and parts['minute']:
46
+ time_part = Time(int(parts['hour']), int(parts['minute']), int(parts['second'] or 0), int(parts['millisecond'] or 0))
47
+ found_time = True
48
+ else:
49
+ time_part = Time()
50
+
51
+ if found_date or found_time:
52
+ return Datetime.combine(date_part, time_part)
53
+ return None
54
+
55
+
56
+ def isostr_to_datetime(string: str) -> Optional[Datetime]:
57
+ """Converts an iso(-ish) formatted string to datetime.
58
+
59
+ The pattern is quite forgiving in a few ways so context based
60
+ sanity-checking might be in order when parsing strings from "iffy"
61
+ sources (user input):
62
+
63
+ - The time part is optional, as well as individual time parts
64
+ - Seperator character for date parts can be: dash (-), slash (/),
65
+ backslash (\\), dot (.), comma (,), or space ( )
66
+ - Seperator character for time parts can be: colum (:), dot (.),
67
+ comma (,), or space ( )
68
+ - Seperator character of date and time halves can be: either case of
69
+ the letter T (T or t), the at symbol (@) or space ( )
70
+ - Values less than ten do not have to be zero-filled
71
+ ("2013-1-2T3:4:5.6789" == "2013-01-02T03:04:05.6789")
72
+ - The year can be any value from 1-9999
73
+
74
+ Otherwise, normal datetime restrictions apply (month must be 1-12, minutes
75
+ hust me 0-59 etc.)
76
+
77
+ This function is timezone naive.
78
+
79
+ Failing to find a valid pattern this will simply return a None.
80
+ """
81
+ return regex_to_datetime(string, _ISODATE_REGEX)
@@ -0,0 +1,50 @@
1
+ __all__ = [
2
+ 'timestamp_to_datetime',
3
+ 'datetime_to_timestamp',
4
+ ]
5
+ from batutils.dtu.structs import *
6
+ import calendar
7
+
8
+
9
+ def timestamp_to_datetime(seconds_since_epoch: T_NUMBER, minmax_on_fail: bool = False) -> Datetime:
10
+ """Converts an int or float representing seconds since the Unix epoch
11
+ (January 1, 1970) to a Python datetime object.
12
+
13
+ :param seconds_since_epoch: Seconds since Unix epoch (January 1, 1970).
14
+ :param minmax_on_fail: If True, will return the minimum or maximum possible
15
+ value of Datetime in case of overflow (positive or
16
+ negative)
17
+ :return: A Python Datetime
18
+ """
19
+ try:
20
+ return Datetime.fromtimestamp(seconds_since_epoch)
21
+ except OSError:
22
+ try:
23
+ return Datetime(1970, 1, 1, 0, 0, 0, 0) + TimeDelta(seconds=seconds_since_epoch)
24
+ except OverflowError:
25
+ if minmax_on_fail:
26
+ if seconds_since_epoch > 0:
27
+ return Datetime.max
28
+ else:
29
+ return Datetime.min
30
+ else:
31
+ raise
32
+
33
+
34
+ def datetime_to_timestamp(dt: T_DATE_VALUE) -> float:
35
+ """Converts a Python datetime object to the number of seconds since
36
+ Unix epoch (January 1, 1970) as a float, including fractional seconds.
37
+
38
+ If given a date only, it will assume a time of 00:00:00.000000.
39
+
40
+ :param dt: Python datetime (or date).
41
+ :return: Number of seconds since Unix epoch (January 1, 1970)
42
+ """
43
+ if not isinstance(dt, Datetime) and isinstance(dt, Date):
44
+ dt = Datetime.combine(dt, Time(0, 0, 0, 0))
45
+ # TODO(thordurm@ccpgames.com>) 2024-05-22: HANDLE SECONDS!!!
46
+
47
+ int_part = float(calendar.timegm(dt.utctimetuple()))
48
+ if dt.microsecond:
49
+ int_part += dt.microsecond / 1000000.0
50
+ return int_part
@@ -0,0 +1,2 @@
1
+ from ._serialize import *
2
+ from ._delta import *
@@ -0,0 +1,62 @@
1
+ __all__ = [
2
+ 'deltastr',
3
+ 'agostr',
4
+ ]
5
+
6
+ from batutils.dtu.structs import *
7
+
8
+
9
+ def deltastr(delta: TimeDelta, default: str = '') -> str:
10
+ """Turns timedelta into a string like "3 weeks"
11
+ or "a few seconds" or "1 year and 7 months".
12
+
13
+ Example:
14
+
15
+ >>> deltastr(datetime.timedelta(days=2002, seconds=20000))
16
+ '5 years'
17
+ >>> deltastr(datetime.timedelta(days=366, seconds=20000))
18
+ '1 year and 5 hours'
19
+ >>> deltastr(datetime.timedelta(seconds=360))
20
+ '6 minutes'
21
+ >>> deltastr(datetime.timedelta(seconds=90))
22
+ '1 minute'
23
+ >>> deltastr(datetime.timedelta(seconds=59))
24
+ 'a few seconds'
25
+ """
26
+ if not isinstance(delta, TimeDelta):
27
+ return default
28
+
29
+ ds = DeltaSplit(delta)
30
+ return ds.to_str(max_number_of_fields=2,
31
+ include_seconds=False,
32
+ directionals=None,
33
+ only_sec_str='a few seconds',
34
+ granularity_halting_threshold=1)
35
+
36
+
37
+ def agostr(delta_or_date: Union[T_DATETIME_VALUE, TimeDelta], default: str = '', utc: bool = True) -> str:
38
+ """Same as deltastr except if given a date/time/datetime value it
39
+ automatically calculates the timedelta from (or to) now to (or from) the
40
+ given value and uses that.
41
+
42
+ This function uses UTC time for comparison by default but can be instructed
43
+ to use the local timezone of its running environment.
44
+ """
45
+ if utc:
46
+ now = Datetime.utcnow()
47
+ else:
48
+ now = Datetime.now()
49
+
50
+ if isinstance(delta_or_date, Datetime):
51
+ delta_or_date = now - delta_or_date
52
+
53
+ elif isinstance(delta_or_date, Date):
54
+ delta_or_date = now.date() - delta_or_date
55
+
56
+ elif isinstance(delta_or_date, Time):
57
+ delta_or_date = Datetime.combine(now.date(), delta_or_date)
58
+
59
+ return deltastr(delta_or_date, default)
60
+
61
+
62
+
@@ -0,0 +1,26 @@
1
+ __all__ = [
2
+ 'serialize',
3
+ 'to_str',
4
+ ]
5
+
6
+ from batutils.dtu.structs import *
7
+
8
+
9
+ def serialize(temporal_object: T_TEMPORAL_VALUE) -> Union[str, float]:
10
+ """Returns the ISO string version of datetimes, dates, times and the float
11
+ version of total_seconds of timedeltas.
12
+
13
+ If the given value is none of these, the same value is just returned
14
+ """
15
+ if isinstance(temporal_object, (Datetime, Date, Time)):
16
+ return temporal_object.isoformat()
17
+ elif isinstance(temporal_object, TimeDelta):
18
+ return temporal_object.total_seconds()
19
+ else:
20
+ return temporal_object
21
+
22
+
23
+ def to_str(temporal_object: T_TEMPORAL_VALUE) -> str:
24
+ """Returns a string version of whatever serialize returned.
25
+ """
26
+ return str(serialize(temporal_object))
@@ -0,0 +1,2 @@
1
+ from ._string import *
2
+ from ._timedelta import *
@@ -0,0 +1,44 @@
1
+ __all__ = [
2
+ 'str_to_rel',
3
+ 'str_to_2dates',
4
+ ]
5
+
6
+
7
+ from batutils.dtu.structs import *
8
+
9
+ DATE_REXEX = re.compile(r'(?P<year>[0]{0,3}[1-9]\d{0,3})[- /.,\\](?P<month>1[012]|0?\d)[- /.,\\](?P<day>3[01]|[012]?\d)')
10
+
11
+
12
+ def str_to_rel(str_value: str) -> Optional[Datetime]:
13
+ """
14
+ """
15
+ str_value = str_value.strip().lower()
16
+ if str_value in ('now', 'today'):
17
+ return Datetime.now()
18
+ elif str_value == 'yesterday':
19
+ return Datetime.now() - TimeDelta(days=1)
20
+ elif str_value == 'tomorrow':
21
+ return Datetime.now() + TimeDelta(days=1)
22
+ else:
23
+ return None
24
+
25
+
26
+ def str_to_2dates(str_value: str) -> Optional[Tuple[Date, Date]]:
27
+ """Converts a string in the format "FROM_DATE SEPERATOR TO_DATE" where
28
+ FROM_DATE and TO_DATE are ISO formatted dates and SEPERATOR is anything and
29
+ turns it into a 2-tuple of python date objects.
30
+
31
+ >>> str_to_2dates('1969-08-15 to 1967-08-19') # Woodstock
32
+ (datetime.date(1969, 8, 15), datetime.date(1967, 8, 19))
33
+ """
34
+ try:
35
+ match = DATE_REXEX.findall(str_value.strip())
36
+ if match:
37
+ if len(match) == 2: # We want two dates
38
+ if len(match[0]) == 3 and len(match[1]) == 3: # We want three parts
39
+ date_from = datetime.date(int(match[0][0]), int(match[0][1]), int(match[0][2]))
40
+ date_to = datetime.date(int(match[1][0]), int(match[1][1]), int(match[1][2]))
41
+ return date_from, date_to
42
+ return None
43
+ except Exception:
44
+ return None