dcicutils 8.8.3.1b12__tar.gz → 8.8.3.1b14__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/PKG-INFO +1 -1
  2. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/data_readers.py +11 -0
  3. dcicutils-8.8.3.1b14/dcicutils/datetime_utils.py +213 -0
  4. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/progress_bar.py +3 -1
  5. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/pyproject.toml +1 -1
  6. dcicutils-8.8.3.1b12/dcicutils/datetime_utils.py +0 -98
  7. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/LICENSE.txt +0 -0
  8. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/README.rst +0 -0
  9. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/__init__.py +0 -0
  10. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/base.py +0 -0
  11. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/beanstalk_utils.py +0 -0
  12. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/bundle_utils.py +0 -0
  13. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/captured_output.py +0 -0
  14. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/cloudformation_utils.py +0 -0
  15. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/codebuild_utils.py +0 -0
  16. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/command_utils.py +0 -0
  17. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/common.py +0 -0
  18. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/contribution_scripts.py +0 -0
  19. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/contribution_utils.py +0 -0
  20. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/creds_utils.py +0 -0
  21. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/data_utils.py +0 -0
  22. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/deployment_utils.py +0 -0
  23. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/diff_utils.py +0 -0
  24. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/docker_utils.py +0 -0
  25. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/ecr_scripts.py +0 -0
  26. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/ecr_utils.py +0 -0
  27. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/ecs_utils.py +0 -0
  28. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/env_base.py +0 -0
  29. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/env_manager.py +0 -0
  30. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/env_scripts.py +0 -0
  31. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/env_utils.py +0 -0
  32. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/env_utils_legacy.py +0 -0
  33. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/es_utils.py +0 -0
  34. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/exceptions.py +0 -0
  35. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/ff_mocks.py +0 -0
  36. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/ff_utils.py +0 -0
  37. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/file_utils.py +0 -0
  38. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/function_cache_decorator.py +0 -0
  39. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/glacier_utils.py +0 -0
  40. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/jh_utils.py +0 -0
  41. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/kibana/dashboards.json +0 -0
  42. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/kibana/readme.md +0 -0
  43. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/lang_utils.py +0 -0
  44. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/license_policies/c4-infrastructure.jsonc +0 -0
  45. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/license_policies/c4-python-infrastructure.jsonc +0 -0
  46. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/license_policies/park-lab-common-server.jsonc +0 -0
  47. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/license_policies/park-lab-common.jsonc +0 -0
  48. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/license_policies/park-lab-gpl-pipeline.jsonc +0 -0
  49. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/license_policies/park-lab-pipeline.jsonc +0 -0
  50. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/license_utils.py +0 -0
  51. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/log_utils.py +0 -0
  52. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/misc_utils.py +0 -0
  53. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/obfuscation_utils.py +0 -0
  54. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/opensearch_utils.py +0 -0
  55. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/portal_object_utils.py +0 -0
  56. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/portal_utils.py +0 -0
  57. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/project_utils.py +0 -0
  58. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/qa_checkers.py +0 -0
  59. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/qa_utils.py +0 -0
  60. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/redis_tools.py +0 -0
  61. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/redis_utils.py +0 -0
  62. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/s3_utils.py +0 -0
  63. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/schema_utils.py +0 -0
  64. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/scripts/publish_to_pypi.py +0 -0
  65. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/scripts/run_license_checker.py +0 -0
  66. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/scripts/view_portal_object.py +0 -0
  67. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/secrets_utils.py +0 -0
  68. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/sheet_utils.py +0 -0
  69. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/snapshot_utils.py +0 -0
  70. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/ssl_certificate_utils.py +0 -0
  71. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/structured_data.py +0 -0
  72. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/submitr/progress_constants.py +0 -0
  73. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/submitr/ref_lookup_strategy.py +0 -0
  74. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/task_utils.py +0 -0
  75. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/tmpfile_utils.py +0 -0
  76. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/trace_utils.py +0 -0
  77. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/validation_utils.py +0 -0
  78. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/variant_utils.py +0 -0
  79. {dcicutils-8.8.3.1b12 → dcicutils-8.8.3.1b14}/dcicutils/zip_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.8.3.1b12
3
+ Version: 8.8.3.1b14
4
4
  Summary: Utility package for interacting with the 4DN Data Portal and other 4DN resources
5
5
  Home-page: https://github.com/4dn-dcic/utils
6
6
  License: MIT
@@ -66,6 +66,13 @@ class RowReader(abc.ABC):
66
66
  else:
67
67
  return value
68
68
 
69
+ @property
70
+ def nrows(self) -> int:
71
+ nrows = 0
72
+ for row in self:
73
+ nrows += 1
74
+ return nrows
75
+
69
76
  def open(self) -> None:
70
77
  pass
71
78
 
@@ -192,6 +199,10 @@ class Excel:
192
199
  return True
193
200
  return False
194
201
 
202
+ @property
203
+ def nsheets(self) -> int:
204
+ return len(self.sheet_names)
205
+
195
206
  def __del__(self) -> None:
196
207
  if (workbook := self._workbook) is not None:
197
208
  self._workbook = None
@@ -0,0 +1,213 @@
1
+ from dcicutils.misc_utils import normalize_spaces
2
+ from datetime import datetime, timedelta, timezone
3
+ from dateutil import parser as datetime_parser
4
+ from typing import Optional, Tuple, Union
5
+
6
+
7
+ def parse_datetime_string(value: str) -> Optional[datetime]:
8
+ """
9
+ Parses the given string into a datetime object and returns it, or if ill-formated then returns None.
10
+ The given string is assumed to be in the format "YYYY-MM-DD hh:mm:ss" and with an optional timezone
11
+ suffix in format "+hh:mm" or "+hh". Also allowed is just a date of the format "YYYY-MM-DD" in which
12
+ case a time of "00:00:00" is assumed. If no timezone is specified then the local timezone is assumed.
13
+ """
14
+ if not isinstance(value, str) or not (value := normalize_spaces(value)):
15
+ return None
16
+ tz_hours = -1
17
+ tz_minutes = -1
18
+ if value.rfind("T") > 0:
19
+ value = value.replace("T", " ")
20
+ if (space := value.find(" ")) > 0 and (value_suffix := value[space + 1:]):
21
+ if (plus := value_suffix.rfind("+")) > 0 or (minus := value_suffix.rfind("-")) > 0:
22
+ value = normalize_spaces(value[:space] + " " + value_suffix[:(plus if plus > 0 else minus)])
23
+ if value_tz := normalize_spaces(value_suffix[(plus if plus > 0 else minus) + 1:]):
24
+ if len(value_tz := value_tz.split(":")) == 2:
25
+ value_tz_hours = value_tz[0].strip()
26
+ value_tz_minutes = value_tz[1].strip()
27
+ else:
28
+ value_tz_hours = value_tz[0].strip()
29
+ value_tz_minutes = "0"
30
+ if value_tz_hours.isdigit() and value_tz_minutes.isdigit():
31
+ tz_hours = int(value_tz_hours)
32
+ tz_minutes = int(value_tz_minutes)
33
+ if not (plus > 0):
34
+ tz_hours = -tz_hours
35
+ else:
36
+ value = value + " 00:00:00"
37
+ if tz_hours < 0 or tz_minutes < 0:
38
+ tz_hours, tz_minutes = get_local_timezone_hours_minutes()
39
+ try:
40
+ dt = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
41
+ tz = timezone(timedelta(hours=tz_hours, minutes=tz_minutes))
42
+ return dt.replace(tzinfo=tz)
43
+ except Exception:
44
+ return None
45
+
46
+
47
+ def parse_date_string(value: str) -> Optional[datetime]:
48
+ """
49
+ Parses the given string into a datetime object representing only a date and
50
+ returns it, or if ill-formated then returns None. The given string is assumed
51
+ to be in the format "YYYY-MM-DD"; if a given string of this format is suffixed
52
+ with a space or a "T" and ANYTHING else, then that trailing portion is ignored.
53
+ """
54
+ if isinstance(value, str) and (value := normalize_spaces(value)):
55
+ if (separator := value.find(" ")) > 0 or (separator := value.find("T")) > 0:
56
+ value = value[:separator]
57
+ try:
58
+ return datetime.strptime(value, "%Y-%m-%d")
59
+ except Exception:
60
+ pass
61
+
62
+
63
+ def normalize_datetime_string(value: str) -> Optional[str]:
64
+ """
65
+ Parses the given string into a datetime object and returns a string for that datetime in ISO-8601 format,
66
+ or if ill-formated then returns None. The given string is assumed to be in the format "YYYY-MM-DD hh:mm:ss"
67
+ and with an optional timezone suffix in format "+hh:mm" or "+hh". Also allowed is just a date of the
68
+ format "YYYY-MM-DD" in which case a time of "00:00:00" is assumed. If no timezone is specified then
69
+ the local timezone is assumed. The returned format looks like this: "2024-02-08T10:37:51-05:00"
70
+ """
71
+ dt = parse_datetime_string(value)
72
+ return dt.isoformat() if dt else None
73
+
74
+
75
+ def normalize_date_string(value: str) -> Optional[str]:
76
+ """
77
+ Parses the given string into a datetime object representing only a date and returns a string for that
78
+ date in ISO-8601 format, or if ill-formated then returns None. The given string is assumed to be in
79
+ the format "YYYY-MM-DD"; but if a given string of this format is suffixed with a space followed by
80
+ ANYTHING else, then that trailing portion is ignored. The returned format looks like this: "2024-02-08"
81
+ """
82
+ d = parse_date_string(value)
83
+ return d.strftime("%Y-%m-%d") if d else None
84
+
85
+
86
+ def get_timezone(hours: int, minutes: Optional[int] = None) -> timezone:
87
+ try:
88
+ return timezone(timedelta(hours=hours, minutes=minutes or 0))
89
+ except Exception:
90
+ return timezone.utc
91
+
92
+
93
+ def get_timezone_hours_minutes(tz: timezone) -> Tuple[int, int]:
94
+ """
95
+ Returns a tuple with the integer hours and minutes offset for the given timezone.
96
+ """
97
+ tz_minutes = datetime.now(tz).utcoffset().total_seconds() / 60
98
+ return int(tz_minutes // 60), int(abs(tz_minutes % 60))
99
+
100
+
101
+ def get_utc_timezone() -> timezone:
102
+ return timezone.utc
103
+
104
+
105
+ def get_local_timezone() -> timezone:
106
+ """
107
+ Returns current/local timezone as a datetime.timezone object.
108
+ """
109
+ return datetime.now().astimezone().tzinfo
110
+
111
+
112
+ def get_local_timezone_string() -> str:
113
+ """
114
+ Returns current/local timezone in format like: "-05:00".
115
+ """
116
+ tz_hours, tz_minutes = get_local_timezone_hours_minutes()
117
+ return f"{tz_hours:+03d}:{tz_minutes:02d}"
118
+
119
+
120
+ def get_local_timezone_hours_minutes() -> Tuple[int, int]:
121
+ """
122
+ Returns a tuple with the integer hours and minutes offset for the current/local timezone.
123
+ """
124
+ tz_minutes = datetime.now(timezone.utc).astimezone().utcoffset().total_seconds() / 60
125
+ return int(tz_minutes // 60), int(abs(tz_minutes % 60))
126
+
127
+
128
+ def parse_datetime(value: str, utc: bool = False, tz: Optional[timezone] = None) -> Optional[datetime]:
129
+ """
130
+ Parses the given string into a datetime, if possible, and returns that value,
131
+ or None if not able to parse. The timezone of the returned datetime will be the
132
+ local timezone; or if the given utc argument is True then it will be UTC; or if the
133
+ given tz argument is a datetime.timezone then return datetime will be in that timezone.
134
+ """
135
+ if isinstance(value, datetime):
136
+ return value
137
+ elif not isinstance(value, str):
138
+ return None
139
+ try:
140
+ # This dateutil.parser handles quite a wide variety of formats and suits our needs.
141
+ value = datetime_parser.parse(value)
142
+ if utc is True:
143
+ # If the given utc argument is True then it trumps any tz argument if given.
144
+ tz = timezone.utc
145
+ if value.tzinfo is not None:
146
+ # The given value had an explicit timezone specified.
147
+ if isinstance(tz, timezone):
148
+ return value.astimezone(tz)
149
+ return value
150
+ return value.replace(tzinfo=tz if isinstance(tz, timezone) else get_local_timezone())
151
+ except Exception:
152
+ return None
153
+
154
+
155
+ def format_datetime(value: datetime,
156
+ utc: bool = False,
157
+ iso: bool = False,
158
+ ms: bool = False,
159
+ tz: Optional[Union[timezone, bool]] = None,
160
+ notz: bool = False,
161
+ noseconds: bool = False,
162
+ verbose: bool = False,
163
+ noseparator: bool = False,
164
+ noday: bool = False) -> Optional[str]:
165
+ """
166
+ Returns the given datetime as a string in "YYYY:MM:DD hh:mm:ss tz" format, for
167
+ example "2024-04-17 15:42:26 EDT". If the given notz argument is True then omits
168
+ the timezone; if the noseconds argument is given the omits the seconds. If the given
169
+ verbose argument is True then returns a really verbose version of the datetime, for
170
+ example "Wednesday, April 17, 2024 | 15:42:26 EDT"; if the noseparator argument is
171
+ True then omits the "|" separator; if the noday argument is True then omits the day
172
+ of week part. The timezone of the returned datetime string will default to the local
173
+ one; if the given utc argument is True then it will be UTC; or if the given tz
174
+ argument is a datetime.timezone it will be in that timezone.
175
+ """
176
+ if not isinstance(value, datetime):
177
+ if not isinstance(value, str) or not (value := parse_datetime(value)):
178
+ return None
179
+ try:
180
+ if utc is True:
181
+ tz = timezone.utc
182
+ elif not isinstance(tz, timezone):
183
+ tz = get_local_timezone()
184
+ if tz is True:
185
+ notz = False
186
+ elif tz is False:
187
+ notz = True
188
+ if noseconds is True:
189
+ ms = False
190
+ value = value.astimezone(tz)
191
+ if iso:
192
+ if notz is True:
193
+ value = value.replace(tzinfo=None)
194
+ if not (ms is True):
195
+ value = value.replace(microsecond=0)
196
+ if noseconds is True:
197
+ if notz is True:
198
+ return value.strftime(f"%Y-%m-%dT%H:%M")
199
+ tz = value.strftime("%z")
200
+ tz = tz[:3] + ":" + tz[3:]
201
+ return value.strftime(f"%Y-%m-%dT%H:%M") + tz
202
+ return value.isoformat()
203
+ if verbose:
204
+ return value.strftime(
205
+ f"{'' if noday is True else '%A, '}%B %-d, %Y{'' if noseparator is True else ' |'}"
206
+ f" %-I:%M{'' if noseconds is True else ':%S'}"
207
+ f"{f'.%f' if ms is True else ''} %p{'' if notz is True else ' %Z'}")
208
+ else:
209
+ return value.strftime(
210
+ f"%Y-%m-%d %H:%M{'' if noseconds is True else ':%S'}"
211
+ f"{f'.%f' if ms is True else ''}{'' if notz is True else ' %Z'}")
212
+ except Exception:
213
+ return None
@@ -50,6 +50,7 @@ class ProgressBar:
50
50
  def __init__(self, total: Optional[int] = None,
51
51
  description: Optional[str] = None,
52
52
  use_byte_size_for_rate: bool = False,
53
+ use_ascii: bool = False,
53
54
  catch_interrupt: bool = True,
54
55
  interrupt: Optional[Callable] = None,
55
56
  interrupt_continue: Optional[Callable] = None,
@@ -66,6 +67,7 @@ class ProgressBar:
66
67
  self._tidy_output_hack = (tidy_output_hack is True)
67
68
  self._stop_requested = False
68
69
  self._use_byte_size_for_rate = (use_byte_size_for_rate is True and self._tidy_output_hack)
70
+ self._use_ascii = (use_ascii is True)
69
71
  # Interrupt handling. We do not do the actual (signal) interrupt setup
70
72
  # in self._initialize as that could be called from a (sub) thread; and in
71
73
  # Python we can only set a signal (SIGINT in our case) on the main thread.
@@ -103,7 +105,7 @@ class ProgressBar:
103
105
  else:
104
106
  bar_format = "{l_bar}{bar}| {n_fmt}/{total_fmt} | {rate_fmt} | {elapsed}{postfix} | ETA: {remaining} "
105
107
  self._bar = TQDM(total=self._total, desc=self._description,
106
- dynamic_ncols=True, bar_format=bar_format, unit="", file=sys.stdout)
108
+ dynamic_ncols=True, bar_format=bar_format, unit="", file=sys.stdout, ascii=self._use_ascii)
107
109
  self._started = time.time()
108
110
  if self._disabled:
109
111
  self._bar.disable = True
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dcicutils"
3
- version = "8.8.3.1b12" # TODO: To become 8.8.4
3
+ version = "8.8.3.1b14" # TODO: To become 8.8.4
4
4
  description = "Utility package for interacting with the 4DN Data Portal and other 4DN resources"
5
5
  authors = ["4DN-DCIC Team <support@4dnucleome.org>"]
6
6
  license = "MIT"
@@ -1,98 +0,0 @@
1
- from dcicutils.misc_utils import normalize_spaces
2
- from datetime import datetime, timedelta, timezone
3
- from typing import Optional, Tuple
4
-
5
-
6
- def parse_datetime_string(value: str) -> Optional[datetime]:
7
- """
8
- Parses the given string into a datetime object and returns it, or if ill-formated then returns None.
9
- The given string is assumed to be in the format "YYYY-MM-DD hh:mm:ss" and with an optional timezone
10
- suffix in format "+hh:mm" or "+hh". Also allowed is just a date of the format "YYYY-MM-DD" in which
11
- case a time of "00:00:00" is assumed. If no timezone is specified then the local timezone is assumed.
12
- """
13
- if not isinstance(value, str) or not (value := normalize_spaces(value)):
14
- return None
15
- tz_hours = -1
16
- tz_minutes = -1
17
- if value.rfind("T") > 0:
18
- value = value.replace("T", " ")
19
- if (space := value.find(" ")) > 0 and (value_suffix := value[space + 1:]):
20
- if (plus := value_suffix.rfind("+")) > 0 or (minus := value_suffix.rfind("-")) > 0:
21
- value = normalize_spaces(value[:space] + " " + value_suffix[:(plus if plus > 0 else minus)])
22
- if value_tz := normalize_spaces(value_suffix[(plus if plus > 0 else minus) + 1:]):
23
- if len(value_tz := value_tz.split(":")) == 2:
24
- value_tz_hours = value_tz[0].strip()
25
- value_tz_minutes = value_tz[1].strip()
26
- else:
27
- value_tz_hours = value_tz[0].strip()
28
- value_tz_minutes = "0"
29
- if value_tz_hours.isdigit() and value_tz_minutes.isdigit():
30
- tz_hours = int(value_tz_hours)
31
- tz_minutes = int(value_tz_minutes)
32
- if not (plus > 0):
33
- tz_hours = -tz_hours
34
- else:
35
- value = value + " 00:00:00"
36
- if tz_hours < 0 or tz_minutes < 0:
37
- tz_hours, tz_minutes = get_local_timezone_hours_minutes()
38
- try:
39
- dt = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
40
- tz = timezone(timedelta(hours=tz_hours, minutes=tz_minutes))
41
- return dt.replace(tzinfo=tz)
42
- except Exception:
43
- return None
44
-
45
-
46
- def parse_date_string(value: str) -> Optional[datetime]:
47
- """
48
- Parses the given string into a datetime object representing only a date and
49
- returns it, or if ill-formated then returns None. The given string is assumed
50
- to be in the format "YYYY-MM-DD"; if a given string of this format is suffixed
51
- with a space or a "T" and ANYTHING else, then that trailing portion is ignored.
52
- """
53
- if isinstance(value, str) and (value := normalize_spaces(value)):
54
- if (separator := value.find(" ")) > 0 or (separator := value.find("T")) > 0:
55
- value = value[:separator]
56
- try:
57
- return datetime.strptime(value, "%Y-%m-%d")
58
- except Exception:
59
- pass
60
-
61
-
62
- def normalize_datetime_string(value: str) -> Optional[str]:
63
- """
64
- Parses the given string into a datetime object and returns a string for that datetime in ISO-8601 format,
65
- or if ill-formated then returns None. The given string is assumed to be in the format "YYYY-MM-DD hh:mm:ss"
66
- and with an optional timezone suffix in format "+hh:mm" or "+hh". Also allowed is just a date of the
67
- format "YYYY-MM-DD" in which case a time of "00:00:00" is assumed. If no timezone is specified then
68
- the local timezone is assumed. The returned format looks like this: "2024-02-08T10:37:51-05:00"
69
- """
70
- dt = parse_datetime_string(value)
71
- return dt.isoformat() if dt else None
72
-
73
-
74
- def normalize_date_string(value: str) -> Optional[str]:
75
- """
76
- Parses the given string into a datetime object representing only a date and returns a string for that
77
- date in ISO-8601 format, or if ill-formated then returns None. The given string is assumed to be in
78
- the format "YYYY-MM-DD"; but if a given string of this format is suffixed with a space followed by
79
- ANYTHING else, then that trailing portion is ignored. The returned format looks like this: "2024-02-08"
80
- """
81
- d = parse_date_string(value)
82
- return d.strftime("%Y-%m-%d") if d else None
83
-
84
-
85
- def get_local_timezone_string() -> str:
86
- """
87
- Returns current/local timezone in format like: "-05:00".
88
- """
89
- tz_hours, tz_minutes = get_local_timezone_hours_minutes()
90
- return f"{tz_hours:+03d}:{tz_minutes:02d}"
91
-
92
-
93
- def get_local_timezone_hours_minutes() -> Tuple[int, int]:
94
- """
95
- Returns a tuple with the integer hours and minutes offset for the current/local timezone.
96
- """
97
- tz_minutes = datetime.now(timezone.utc).astimezone().utcoffset().total_seconds() / 60
98
- return int(tz_minutes // 60), int(abs(tz_minutes % 60))