dately 2.1.3__tar.gz → 2.1.4__tar.gz
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.
- {dately-2.1.3 → dately-2.1.4}/PKG-INFO +11 -3
- {dately-2.1.3 → dately-2.1.4}/dately/core.py +27 -4
- {dately-2.1.3 → dately-2.1.4}/dately/timeutils.py +2 -50
- {dately-2.1.3 → dately-2.1.4}/dately/timezone.py +43 -64
- {dately-2.1.3 → dately-2.1.4}/dately.egg-info/PKG-INFO +11 -3
- dately-2.1.4/setup.py +53 -0
- dately-2.1.3/setup.py +0 -41
- {dately-2.1.3 → dately-2.1.4}/README.md +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/__init__.py +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/__init__.py +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/Compiled.cp38-win_amd64.pyd +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/__init__.py +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/cdatetime/UniversalDateFormatter.cp38-win_amd64.pyd +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/cdatetime/__init__.py +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/cdatetime/iso8601T.cp38-win_amd64.pyd +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/cdatetime/iso8601Z.cp38-win_amd64.pyd +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/cdatetime/whichformat.cp38-win_amd64.pyd +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/clean_str.cp38-win_amd64.pyd +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/time_zones.cp38-win_amd64.pyd +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/mskutils.py +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/sysutils.py +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately/utils.py +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately.egg-info/SOURCES.txt +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately.egg-info/dependency_links.txt +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately.egg-info/requires.txt +0 -0
- {dately-2.1.3 → dately-2.1.4}/dately.egg-info/top_level.txt +0 -0
- {dately-2.1.3 → dately-2.1.4}/setup.cfg +0 -0
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dately
|
|
3
|
-
Version: 2.1.
|
|
4
|
-
Summary: A library for
|
|
3
|
+
Version: 2.1.4
|
|
4
|
+
Summary: A comprehensive Python library for advanced date and time manipulation.
|
|
5
5
|
Home-page: https://github.com/cedricmoorejr/dately
|
|
6
6
|
Author: Cedric Moore Jr.
|
|
7
|
-
Author-email:
|
|
7
|
+
Author-email: cedricmoorejunior5@gmail.com
|
|
8
8
|
License: MIT
|
|
9
9
|
Project-URL: Source Code, https://github.com/cedricmoorejr/dately/tree/main/dately
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
21
|
Requires-Python: >=3.8
|
|
14
22
|
Description-Content-Type: text/markdown
|
|
15
23
|
|
|
@@ -66,7 +66,6 @@ class DatelyDate:
|
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
68
|
def __dir__(self):
|
|
69
|
-
# Override the __dir__ method to exclude private methods
|
|
70
69
|
original_dir = super().__dir__()
|
|
71
70
|
return [item for item in original_dir if not item.startswith('_DatelyDate__')]
|
|
72
71
|
|
|
@@ -373,10 +372,34 @@ class DatelyDate:
|
|
|
373
372
|
format, or if the tzinfo is invalid, raises an error to ensure that the function usage is clear and safe
|
|
374
373
|
within expected data types.
|
|
375
374
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
def replace_timestring(self, datetime_strings, *args, **kwargs):
|
|
376
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k != 'time_indicator'}
|
|
377
|
+
|
|
378
|
+
if isinstance(datetime_strings, str):
|
|
379
|
+
if is_iso_date(datetime_strings):
|
|
380
|
+
return self.__repl_iso_timestring(datetime_strings, *args, **filtered_kwargs)
|
|
381
|
+
else:
|
|
382
|
+
return self._repl_timestring(datetime_strings, *args, **kwargs)
|
|
383
|
+
elif isinstance(datetime_strings, list):
|
|
384
|
+
return [
|
|
385
|
+
self.__repl_iso_timestring(dt_string, *args, **filtered_kwargs) if is_iso_date(dt_string)
|
|
386
|
+
else self._repl_timestring(dt_string, *args, **kwargs)
|
|
387
|
+
for dt_string in datetime_strings
|
|
388
|
+
]
|
|
389
|
+
elif isinstance(datetime_strings, np.ndarray):
|
|
390
|
+
vectorized_iso_func = np.vectorize(
|
|
391
|
+
lambda dt_string: self.__repl_iso_timestring(dt_string, *args, **filtered_kwargs) if is_iso_date(dt_string)
|
|
392
|
+
else self._repl_timestring(dt_string, *args, **kwargs),
|
|
393
|
+
otypes=[np.object]
|
|
394
|
+
)
|
|
395
|
+
return vectorized_iso_func(datetime_strings)
|
|
396
|
+
elif isinstance(datetime_strings, pd.Series):
|
|
397
|
+
return datetime_strings.apply(
|
|
398
|
+
lambda dt_string: self.__repl_iso_timestring(dt_string, *args, **filtered_kwargs) if is_iso_date(dt_string)
|
|
399
|
+
else self._repl_timestring(dt_string, *args, **kwargs)
|
|
400
|
+
)
|
|
378
401
|
else:
|
|
379
|
-
|
|
402
|
+
raise ValueError("Unsupported data type. The input must be a scalar (str), list, numpy.ndarray, or pandas.Series.")
|
|
380
403
|
|
|
381
404
|
def replace_datestring(self, date_strings, year=None, month=None, day=None):
|
|
382
405
|
"""
|
|
@@ -36,11 +36,8 @@ __all__ = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
def get_time():
|
|
39
|
-
# Get the current time as a timestamp
|
|
40
39
|
current_time = time.time()
|
|
41
|
-
# Convert timestamp to a time tuple in local time
|
|
42
40
|
local_time = time.localtime(current_time)
|
|
43
|
-
# Format the time to include hours, minutes, seconds, and microseconds
|
|
44
41
|
formatted_time = time.strftime("%H:%M:%S", local_time) + f".{int(current_time % 1 * 1_000_000)}"
|
|
45
42
|
return formatted_time
|
|
46
43
|
|
|
@@ -59,31 +56,23 @@ def offset_convert(number):
|
|
|
59
56
|
str: The formatted time offset as a string with a leading sign (either '+' or '-') followed
|
|
60
57
|
by two digits for hours and two digits for minutes, separated by a colon.
|
|
61
58
|
"""
|
|
62
|
-
# Convert string input to float if necessary
|
|
63
59
|
if isinstance(number, str):
|
|
64
60
|
number = float(number)
|
|
65
|
-
|
|
66
61
|
sign = '+' if number >= 0 else '-'
|
|
67
|
-
# Get the absolute value
|
|
68
62
|
abs_number = abs(number)
|
|
69
|
-
# Extract hours and minutes
|
|
70
63
|
hours = int(abs_number)
|
|
71
64
|
minutes = int((abs_number - hours) * 60)
|
|
72
|
-
# Format the result as +-HH:MM
|
|
73
65
|
formatted_time = f"{sign}{hours:02}:{minutes:02}"
|
|
74
66
|
return formatted_time
|
|
75
67
|
|
|
76
68
|
def datetime_offset(offset):
|
|
77
|
-
# Validate the offset
|
|
78
69
|
if not isinstance(offset, (int, float)):
|
|
79
70
|
raise ValueError("Offset must be an integer or float representing hours.")
|
|
80
|
-
# Create a timezone object for datetime's tzinfo
|
|
81
71
|
timezone = datetime.timezone(datetime.timedelta(hours=offset))
|
|
82
72
|
return timezone
|
|
83
73
|
|
|
84
74
|
def strTime(datetime_string):
|
|
85
75
|
""" Extracts and returns detailed time information from a datetime string. """
|
|
86
|
-
# Regex for finding time
|
|
87
76
|
time_exists = timeboundary_regex.search(datetime_string)
|
|
88
77
|
|
|
89
78
|
if time_exists:
|
|
@@ -91,11 +80,9 @@ def strTime(datetime_string):
|
|
|
91
80
|
full_time_end_position = time_exists.end()
|
|
92
81
|
full_time_string = time_exists.group()
|
|
93
82
|
|
|
94
|
-
# Search for more specific time details within the extracted time string
|
|
95
83
|
time_match = time_only_regex.search(full_time_string)
|
|
96
84
|
|
|
97
85
|
if time_match:
|
|
98
|
-
# If a specific time format is found, return start, end, and the actual time value as a dictionary
|
|
99
86
|
time_details = {
|
|
100
87
|
'time_found': time_match.group(),
|
|
101
88
|
'start': time_match.start() + full_time_start_position,
|
|
@@ -132,16 +119,10 @@ def stripTimeIndicator(datetime_string):
|
|
|
132
119
|
time_end = match['time_details']["end"]
|
|
133
120
|
fulltime_start = match['full_time_details']['start']
|
|
134
121
|
fulltime_end = match['full_time_details']["end"]
|
|
135
|
-
|
|
136
122
|
timezone_data = datetime_string[time_end:]
|
|
137
|
-
|
|
138
123
|
if timezone_data == '':
|
|
139
124
|
return datetime_string
|
|
140
|
-
|
|
141
|
-
# Remove the time indicator
|
|
142
125
|
cleaned_string = re.sub(timemeridiem_regex, ' ', timezone_data)
|
|
143
|
-
|
|
144
|
-
# Combine the parts
|
|
145
126
|
return datetime_string[:fulltime_start] + time_match + cleaned_string + datetime_string[fulltime_end:]
|
|
146
127
|
|
|
147
128
|
return datetime_string
|
|
@@ -182,10 +163,8 @@ def remove_marker(text):
|
|
|
182
163
|
Returns:
|
|
183
164
|
str: The cleaned text without the specified marker.
|
|
184
165
|
"""
|
|
185
|
-
# Define the regex pattern to match the specific string to remove
|
|
186
166
|
pattern = re.compile(r" NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET")
|
|
187
167
|
|
|
188
|
-
# Replace the matched string with nothing (effectively removing it)
|
|
189
168
|
cleaned_text = pattern.sub("", text)
|
|
190
169
|
return cleaned_text
|
|
191
170
|
|
|
@@ -205,15 +184,11 @@ def make_datetime_string(date_string):
|
|
|
205
184
|
str: A standardized datetime string. If the format is date-only, '00:00:00.000000 NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET' is appended to the date string. If the format
|
|
206
185
|
includes time information or is not detected, the original date string is returned.
|
|
207
186
|
"""
|
|
208
|
-
# Define the regex pattern for time extraction
|
|
209
187
|
time_pattern = anytime_regex
|
|
210
188
|
no_other_time_placeholder = "NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET"
|
|
211
|
-
|
|
212
|
-
# Search for the time component in the date string
|
|
213
189
|
match = time_pattern.search(date_string)
|
|
214
190
|
|
|
215
191
|
if match:
|
|
216
|
-
# Check if the matched time component has a valid tzinfo
|
|
217
192
|
time_component = match.group()
|
|
218
193
|
tzinfo_pattern = gtf('tzinfo')
|
|
219
194
|
if tzinfo_pattern.search(time_component):
|
|
@@ -221,7 +196,6 @@ def make_datetime_string(date_string):
|
|
|
221
196
|
else:
|
|
222
197
|
return f"{date_string.strip()} {get_time()} {no_other_time_placeholder}"
|
|
223
198
|
else:
|
|
224
|
-
# If no time component is found, append '00:00:00.000000 NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET' to the date string
|
|
225
199
|
return f"{date_string.strip()} {get_time()} {no_other_time_placeholder}"
|
|
226
200
|
|
|
227
201
|
def validate_timezone(datetime_string):
|
|
@@ -233,7 +207,6 @@ def validate_timezone(datetime_string):
|
|
|
233
207
|
timezone_data = timezone_data.lstrip()
|
|
234
208
|
if timezone_data == '':
|
|
235
209
|
return True, "The time string is valid."
|
|
236
|
-
# Check the occurrences of each component
|
|
237
210
|
if len(timemeridiem_regex.findall(timezone_data)) > 1:
|
|
238
211
|
return False, "More than one time indicator found."
|
|
239
212
|
if len(timezone_offset_regex.findall(timezone_data)) > 1:
|
|
@@ -254,27 +227,19 @@ def validate_date(date_string, date_format):
|
|
|
254
227
|
then identifies the components of the date (month, day, year) and
|
|
255
228
|
checks their validity based on standard calendar rules.
|
|
256
229
|
"""
|
|
257
|
-
# Extract any localized time fragment and clean the string.
|
|
258
230
|
date_str = stripTime(date_string)
|
|
259
|
-
|
|
260
|
-
# This dictionary will store the spans of each component.
|
|
261
231
|
components_spans = {"month": None, "day": None, "year": None}
|
|
262
|
-
|
|
263
|
-
# Single pass to find month, day, and year
|
|
264
232
|
pattern = datetime_pattern_search(date_format)
|
|
265
233
|
match = pattern.match(date_str)
|
|
266
234
|
if match:
|
|
267
|
-
# Iterate through possible date components
|
|
268
235
|
for key in components_spans.keys():
|
|
269
|
-
if key in match.groupdict():
|
|
270
|
-
components_spans[key] = match.span(key)
|
|
236
|
+
if key in match.groupdict():
|
|
237
|
+
components_spans[key] = match.span(key)
|
|
271
238
|
|
|
272
|
-
# Extract day, month, and year based on their spans in the string
|
|
273
239
|
day = int(date_str[slice(*components_spans['day'])])
|
|
274
240
|
month = int(date_str[slice(*components_spans['month'])])
|
|
275
241
|
year = int(date_str[slice(*components_spans['year'])])
|
|
276
242
|
|
|
277
|
-
# Define the maximum days in each month, adjusting February based on leap year
|
|
278
243
|
month_days = {1: 31, 2: 29 if is_leap_year(year) else 28, 3: 31, 4: 30, 5: 31, 6: 30,
|
|
279
244
|
7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31}
|
|
280
245
|
if month < 1 or month > 12:
|
|
@@ -369,42 +334,29 @@ def replace_time_by_position(datetime_string, component, new_value):
|
|
|
369
334
|
|
|
370
335
|
time_component_pattern = gtf(component)
|
|
371
336
|
gt_time_pattern_match = timeboundary_regex.search(datetime_string)
|
|
372
|
-
|
|
373
337
|
if gt_time_pattern_match:
|
|
374
338
|
time_start_position = gt_time_pattern_match.start()
|
|
375
339
|
time_end_position = gt_time_pattern_match.end()
|
|
376
340
|
time_matched_value = gt_time_pattern_match.group()
|
|
377
|
-
|
|
378
341
|
if component == 'tzinfo':
|
|
379
342
|
new_value = offset_convert(new_value)
|
|
380
343
|
time_component_matches = list(time_component_pattern.finditer(time_matched_value))
|
|
381
344
|
if time_component_matches:
|
|
382
|
-
# Find the match with the largest end position
|
|
383
345
|
largest_match = max(time_component_matches, key=lambda m: m.end())
|
|
384
|
-
# Replace all tzinfo matches with the new value
|
|
385
346
|
part_before = datetime_string[:time_start_position + time_component_matches[0].start()]
|
|
386
347
|
part_after = datetime_string[time_start_position + largest_match.end():]
|
|
387
348
|
datetime_string = part_before + new_value + part_after
|
|
388
349
|
return datetime_string.replace('NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET', new_value).strip()
|
|
389
350
|
else:
|
|
390
|
-
# Replace the placeholders with the new tzinfo value
|
|
391
351
|
return datetime_string.replace('NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET', new_value).strip()
|
|
392
|
-
|
|
393
|
-
# Match the specific component within the matched time
|
|
394
352
|
time_component_match = time_component_pattern.search(time_matched_value)
|
|
395
353
|
if time_component_match:
|
|
396
|
-
# Replace the component with the new value
|
|
397
354
|
part_before = datetime_string[:time_start_position + time_component_match.start(1)]
|
|
398
355
|
part_after = datetime_string[time_start_position + time_component_match.end(1):]
|
|
399
356
|
|
|
400
357
|
if component == 'microsecond':
|
|
401
358
|
new_value = hundred_thousandths_place(new_value, decimal=False)
|
|
402
|
-
|
|
403
359
|
datetime_string = part_before + new_value + part_after
|
|
404
|
-
|
|
405
360
|
return datetime_string.replace('NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET', '').strip()
|
|
406
|
-
|
|
407
|
-
# Always remove placeholders if they exist
|
|
408
361
|
datetime_string = datetime_string.replace('NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET', '').strip()
|
|
409
|
-
|
|
410
362
|
return datetime_string
|
|
@@ -3,7 +3,6 @@ import json
|
|
|
3
3
|
import tempfile
|
|
4
4
|
import shutil
|
|
5
5
|
import requests
|
|
6
|
-
from requests.exceptions import ConnectionError, HTTPError, Timeout
|
|
7
6
|
import time
|
|
8
7
|
import random
|
|
9
8
|
import re
|
|
@@ -12,8 +11,13 @@ import pandas as pd
|
|
|
12
11
|
from .mskutils import *
|
|
13
12
|
from .sysutils import *
|
|
14
13
|
|
|
14
|
+
TimeZoner = None
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
class NoAPIKeysError(Exception):
|
|
18
|
+
"""Exception raised when no API keys are available."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
17
21
|
class Request:
|
|
18
22
|
def __init__(self, base_url='aHR0cDovL2FwaS50aW1lem9uZWRiLmNvbS92Mi4xL2xpc3QtdGltZS16b25lP2tleT0=', use_api_key=True):
|
|
19
23
|
""" Initialize the API utility with the given base URL. """
|
|
@@ -42,14 +46,27 @@ class Request:
|
|
|
42
46
|
response = requests.get(url, timeout=10)
|
|
43
47
|
response.raise_for_status()
|
|
44
48
|
self.api_keys = response.json()
|
|
49
|
+
except requests.exceptions.HTTPError as e:
|
|
50
|
+
print(f"HTTP error occurred: {e}")
|
|
51
|
+
self.api_keys = {}
|
|
52
|
+
except requests.exceptions.ConnectionError as e:
|
|
53
|
+
print(f"Connection error occurred: {e}")
|
|
54
|
+
self.api_keys = {}
|
|
55
|
+
except requests.exceptions.Timeout as e:
|
|
56
|
+
print(f"Timeout occurred: {e}")
|
|
57
|
+
self.api_keys = {}
|
|
45
58
|
except Exception as e:
|
|
59
|
+
print(f"An unexpected error occurred: {e}")
|
|
46
60
|
self.api_keys = {}
|
|
47
|
-
|
|
61
|
+
except requests.exceptions.SSLError as e:
|
|
62
|
+
print(f"SSL error occurred: {e}")
|
|
63
|
+
self.api_keys = {}
|
|
64
|
+
|
|
48
65
|
def get_random_key(self):
|
|
49
66
|
"""Get a random API key from the available keys."""
|
|
50
67
|
keys = list(self.api_keys.keys())
|
|
51
68
|
if not keys:
|
|
52
|
-
raise
|
|
69
|
+
raise NoAPIKeysError("No API keys available")
|
|
53
70
|
|
|
54
71
|
random_key = random.choice(keys)
|
|
55
72
|
while random_key == self.last_key and len(keys) > 1:
|
|
@@ -61,12 +78,7 @@ class Request:
|
|
|
61
78
|
return api_key
|
|
62
79
|
|
|
63
80
|
def update_base_url(self, new_url):
|
|
64
|
-
"""
|
|
65
|
-
Update the base URL for the API utility.
|
|
66
|
-
|
|
67
|
-
Parameters:
|
|
68
|
-
new_url (str): The new base URL for the API endpoint.
|
|
69
|
-
"""
|
|
81
|
+
""" Update the base URL for the API utility. """
|
|
70
82
|
self.base_url = new_url
|
|
71
83
|
|
|
72
84
|
def set_use_api_key(self, use_api_key):
|
|
@@ -97,21 +109,15 @@ class Request:
|
|
|
97
109
|
reset_time = UnixTime.Date(self.rate_limit_reset)
|
|
98
110
|
else:
|
|
99
111
|
reset_time = 'unknown'
|
|
100
|
-
# print(f"Rate limit status:\n"
|
|
101
|
-
# f" Limit: {self.rate_limit_limit}\n"
|
|
102
|
-
# f" Remaining: {self.rate_limit_remaining}\n"
|
|
103
|
-
# f" Reset: {reset_time}")
|
|
104
112
|
|
|
105
113
|
def make_request(self, params, headers=None):
|
|
106
114
|
""" Make a request to the specified API. """
|
|
107
115
|
current_time = time.time()
|
|
108
116
|
time_since_last_request = current_time - self.last_request_time
|
|
109
117
|
|
|
110
|
-
# Ensure at least 2 seconds have passed since the last request
|
|
111
118
|
if time_since_last_request < 2:
|
|
112
119
|
time.sleep(2 - time_since_last_request)
|
|
113
120
|
|
|
114
|
-
# Ensure we are not exceeding the rate limit
|
|
115
121
|
if self.rate_limit_remaining is not None and self.rate_limit_remaining <= 0:
|
|
116
122
|
if self.rate_limit_reset is not None:
|
|
117
123
|
reset_time = UnixTime.Date(self.rate_limit_reset)
|
|
@@ -132,29 +138,21 @@ class Request:
|
|
|
132
138
|
if self.use_api_key:
|
|
133
139
|
api_key = self.get_random_key()
|
|
134
140
|
params['key'] = api_key
|
|
135
|
-
|
|
136
|
-
# Ensure the 'format' parameter is always set to 'json' if not already specified
|
|
141
|
+
|
|
137
142
|
if 'format' not in params:
|
|
138
143
|
params['format'] = 'json'
|
|
139
144
|
|
|
140
145
|
try:
|
|
141
146
|
response = requests.get(self.base_url, params=params, headers=headers, timeout=10)
|
|
142
147
|
response.raise_for_status()
|
|
143
|
-
|
|
144
|
-
# Extract and update rate limit information
|
|
145
148
|
rate_limit_info = self.extract_rate_limit_info(response.headers)
|
|
146
|
-
|
|
147
|
-
self.last_request_time = time.time() # Update the last request time
|
|
148
|
-
|
|
149
|
-
# Log the rate limit status automatically
|
|
149
|
+
self.last_request_time = time.time()
|
|
150
150
|
self.log_rate_limit_status()
|
|
151
|
-
|
|
152
151
|
return {
|
|
153
152
|
'response': response.json(),
|
|
154
153
|
'rate_limit_info': rate_limit_info
|
|
155
154
|
}
|
|
156
155
|
except Exception as e:
|
|
157
|
-
# Log the rate limit status automatically even in case of error
|
|
158
156
|
self.log_rate_limit_status()
|
|
159
157
|
|
|
160
158
|
return {
|
|
@@ -165,7 +163,6 @@ class Request:
|
|
|
165
163
|
|
|
166
164
|
|
|
167
165
|
|
|
168
|
-
|
|
169
166
|
class parse_zoneinfo_html_table:
|
|
170
167
|
"""
|
|
171
168
|
This class is designed to parse an HTML IANA Time Zones table from a specified URL, clean the HTML content,
|
|
@@ -205,18 +202,13 @@ class parse_zoneinfo_html_table:
|
|
|
205
202
|
|
|
206
203
|
def extract_table_from_html(self):
|
|
207
204
|
try:
|
|
208
|
-
# Find and extract the whole table
|
|
209
205
|
table_html = self.extract_tag_contents(self.html_content, 'table', first_only=True)
|
|
210
206
|
if not table_html:
|
|
211
207
|
return None, None
|
|
212
|
-
|
|
213
|
-
# Extract table headers
|
|
214
208
|
headers = []
|
|
215
209
|
headers_html = self.extract_tag_contents(table_html, 'tr', first_only=True)
|
|
216
210
|
for header_content in self.extract_tag_contents(headers_html, 'th'):
|
|
217
211
|
headers.append(self.clean_html(header_content).strip())
|
|
218
|
-
|
|
219
|
-
# Extract table rows
|
|
220
212
|
rows = []
|
|
221
213
|
row_tags = self.extract_tag_contents(table_html, 'tr')[1:]
|
|
222
214
|
for row_html in row_tags:
|
|
@@ -251,23 +243,18 @@ class TimezoneOffset:
|
|
|
251
243
|
"""Converts a numeric or string time offset into a formatted string representing the offset in hours and minutes."""
|
|
252
244
|
@staticmethod
|
|
253
245
|
def format(offset):
|
|
254
|
-
# Check if the value is a string
|
|
255
246
|
if isinstance(offset, str):
|
|
256
247
|
offset = offset.strip()
|
|
257
248
|
|
|
258
|
-
# Handle the case where the offset is "UTC"
|
|
259
249
|
if offset == "UTC":
|
|
260
250
|
return "+00:00"
|
|
261
251
|
|
|
262
|
-
# Handle the case where the offset is an all-caps abbreviation
|
|
263
252
|
if offset.isupper():
|
|
264
253
|
return offset
|
|
265
254
|
|
|
266
|
-
# Check if the value is already in the correct HH:MM format
|
|
267
255
|
if re.match(r'^[+-]\d{2}:\d{2}$', offset):
|
|
268
256
|
return offset
|
|
269
257
|
|
|
270
|
-
# Handle the case where offset is given as a string with hours and optional minutes
|
|
271
258
|
pattern_hm = r'^([+-]?)(\d{1,2})(?::?(\d{1,2})?)?$'
|
|
272
259
|
match_hm = re.match(pattern_hm, str(offset).strip())
|
|
273
260
|
if match_hm:
|
|
@@ -292,7 +279,6 @@ class TimezoneOffset:
|
|
|
292
279
|
except ValueError:
|
|
293
280
|
return None
|
|
294
281
|
|
|
295
|
-
# Handle the case where offset is given as total seconds
|
|
296
282
|
pattern_seconds = r'^([+-]?)(\d+)$'
|
|
297
283
|
match_seconds = re.match(pattern_seconds, str(offset).strip())
|
|
298
284
|
if match_seconds:
|
|
@@ -423,24 +409,21 @@ class tzoneDataManager:
|
|
|
423
409
|
shutil.rmtree(self.storage_dir)
|
|
424
410
|
|
|
425
411
|
|
|
426
|
-
|
|
427
|
-
|
|
428
412
|
class DatelyTz:
|
|
429
413
|
"""DatelyTz class is responsible for managing timezone data retrieval."""
|
|
430
414
|
def __init__(self, rq_instance=None):
|
|
431
415
|
"""Initialize the DatelyTz class. Loads data from file if it exists, otherwise fetches from the API and saves it."""
|
|
432
416
|
self.tzone_manager = tzoneDataManager()
|
|
433
|
-
self.Request = rq_instance
|
|
417
|
+
self.Request = rq_instance
|
|
434
418
|
|
|
435
419
|
if tzoneDataManager.data_file_exists():
|
|
436
420
|
self.tzone_manager.load_data_from_file()
|
|
437
421
|
self.timezonedata = self.tzone_manager.timezonedata
|
|
438
422
|
else:
|
|
439
|
-
self.timezonedata = self._from_api()
|
|
423
|
+
self.timezonedata = self._from_api()
|
|
440
424
|
self.tzone_manager.timezonedata = self.timezonedata
|
|
441
425
|
self.tzone_manager.save_data_to_file()
|
|
442
426
|
|
|
443
|
-
# Update self.timezonedata to include the full dataset with IANA table data
|
|
444
427
|
self.tzone_manager.load_data_from_file()
|
|
445
428
|
self.timezonedata = self.tzone_manager.timezonedata
|
|
446
429
|
|
|
@@ -452,7 +435,7 @@ class DatelyTz:
|
|
|
452
435
|
result = self.Request.make_request(params)
|
|
453
436
|
if 'response' in result:
|
|
454
437
|
data = result['response']
|
|
455
|
-
return self._format_api_data(data)
|
|
438
|
+
return self._format_api_data(data)
|
|
456
439
|
else:
|
|
457
440
|
return result
|
|
458
441
|
|
|
@@ -472,18 +455,6 @@ class DatelyTz:
|
|
|
472
455
|
return data
|
|
473
456
|
|
|
474
457
|
|
|
475
|
-
|
|
476
|
-
rq_instance = Request(use_api_key=True)
|
|
477
|
-
|
|
478
|
-
# Create an instance of DatelyTz
|
|
479
|
-
tz = DatelyTz(rq_instance)
|
|
480
|
-
|
|
481
|
-
# Access the time zone data from tzoneDataManager instance
|
|
482
|
-
timezonedata = tz.tzone_manager.timezonedata
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
458
|
class ZoneInfoManager:
|
|
488
459
|
"""
|
|
489
460
|
A class to manage and query time zone information.
|
|
@@ -523,7 +494,6 @@ class ZoneInfoManager:
|
|
|
523
494
|
self.ObservesDST = self.__get_zones_by_dst_observance()
|
|
524
495
|
|
|
525
496
|
def __dir__(self):
|
|
526
|
-
# Override the __dir__ method to exclude private methods and attributes
|
|
527
497
|
original_dir = super().__dir__()
|
|
528
498
|
return [item for item in original_dir if not item.startswith('_')]
|
|
529
499
|
|
|
@@ -626,16 +596,13 @@ class ZoneInfoManager:
|
|
|
626
596
|
if not self.__Request:
|
|
627
597
|
return None
|
|
628
598
|
|
|
629
|
-
# Temporarily update the base URL
|
|
630
599
|
original_url = self.__Request.base_url
|
|
631
600
|
temp = Shift.type.map('aHR0cDovL3dvcmxkdGltZWFwaS5vcmcvYXBpL3RpbWV6b25lLw==', zone_name, ret=True)
|
|
632
601
|
self.__Request.update_base_url(temp)
|
|
633
602
|
|
|
634
603
|
try:
|
|
635
|
-
# Make the request without parameters
|
|
636
604
|
result = self.__Request.make_request(params={})
|
|
637
605
|
finally:
|
|
638
|
-
# Restore the original base URL
|
|
639
606
|
self.__Request.update_base_url(original_url)
|
|
640
607
|
|
|
641
608
|
if 'response' in result:
|
|
@@ -643,11 +610,23 @@ class ZoneInfoManager:
|
|
|
643
610
|
else:
|
|
644
611
|
return result
|
|
645
612
|
|
|
646
|
-
# Create an instance of ZoneInfoManager
|
|
647
|
-
TimeZoner = ZoneInfoManager(timezonedata, rq_instance)
|
|
648
613
|
|
|
614
|
+
try:
|
|
615
|
+
rq_instance = Request(use_api_key=True)
|
|
616
|
+
tz = DatelyTz(rq_instance)
|
|
617
|
+
timezonedata = tz.tzone_manager.timezonedata
|
|
618
|
+
TimeZoner = ZoneInfoManager(timezonedata, rq_instance)
|
|
619
|
+
except Exception as e:
|
|
620
|
+
print(f"Failed to initialize TimeZoner due to: {e}")
|
|
649
621
|
|
|
650
|
-
|
|
651
|
-
|
|
622
|
+
# TimeZoner Fail
|
|
623
|
+
if TimeZoner is None:
|
|
624
|
+
class ImportError(Exception):
|
|
625
|
+
def __init__(self, message="TimeZoner could not be imported correctly and cannot be used."):
|
|
626
|
+
self.message = message
|
|
627
|
+
super().__init__(self.message)
|
|
628
|
+
|
|
629
|
+
TimeZoner = lambda *args, **kwargs: (_ for _ in ()).throw(ImportError())
|
|
652
630
|
|
|
653
631
|
|
|
632
|
+
__all__ = ['TimeZoner']
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dately
|
|
3
|
-
Version: 2.1.
|
|
4
|
-
Summary: A library for
|
|
3
|
+
Version: 2.1.4
|
|
4
|
+
Summary: A comprehensive Python library for advanced date and time manipulation.
|
|
5
5
|
Home-page: https://github.com/cedricmoorejr/dately
|
|
6
6
|
Author: Cedric Moore Jr.
|
|
7
|
-
Author-email:
|
|
7
|
+
Author-email: cedricmoorejunior5@gmail.com
|
|
8
8
|
License: MIT
|
|
9
9
|
Project-URL: Source Code, https://github.com/cedricmoorejr/dately/tree/main/dately
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
21
|
Requires-Python: >=3.8
|
|
14
22
|
Description-Content-Type: text/markdown
|
|
15
23
|
|
dately-2.1.4/setup.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# Check if the system is Windows
|
|
5
|
+
if sys.platform != 'win32':
|
|
6
|
+
raise RuntimeError('This package is only compatible with Windows.')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
setup(
|
|
10
|
+
name='dately',
|
|
11
|
+
version="2.1.4",
|
|
12
|
+
author='Cedric Moore Jr.',
|
|
13
|
+
author_email='cedricmoorejunior5@gmail.com',
|
|
14
|
+
description='A comprehensive Python library for advanced date and time manipulation.',
|
|
15
|
+
long_description=open('README.md').read(),
|
|
16
|
+
long_description_content_type='text/markdown',
|
|
17
|
+
url='https://github.com/cedricmoorejr/dately',
|
|
18
|
+
project_urls={
|
|
19
|
+
'Source Code': 'https://github.com/cedricmoorejr/dately/tree/main/dately',
|
|
20
|
+
},
|
|
21
|
+
packages=find_packages(),
|
|
22
|
+
package_data={
|
|
23
|
+
'dately': [
|
|
24
|
+
'mold/pyd/*.pyd',
|
|
25
|
+
'mold/pyd/cdatetime/*.pyd'
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
classifiers=[
|
|
29
|
+
'Programming Language :: Python :: 3',
|
|
30
|
+
'License :: OSI Approved :: MIT License',
|
|
31
|
+
'Operating System :: Microsoft :: Windows',
|
|
32
|
+
'Development Status :: 5 - Production/Stable',
|
|
33
|
+
'Intended Audience :: Developers',
|
|
34
|
+
'Natural Language :: English',
|
|
35
|
+
'License :: OSI Approved :: MIT License',
|
|
36
|
+
'Programming Language :: Python :: 3',
|
|
37
|
+
'Programming Language :: Python :: 3.8',
|
|
38
|
+
'Programming Language :: Python :: 3.9',
|
|
39
|
+
'Programming Language :: Python :: 3.10',
|
|
40
|
+
],
|
|
41
|
+
python_requires='>=3.8',
|
|
42
|
+
install_requires=[
|
|
43
|
+
'numpy',
|
|
44
|
+
'pandas',
|
|
45
|
+
'pytz',
|
|
46
|
+
'requests',
|
|
47
|
+
],
|
|
48
|
+
license='MIT',
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
dately-2.1.3/setup.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
# Check if the system is Windows
|
|
5
|
-
if sys.platform != 'win32':
|
|
6
|
-
raise RuntimeError("This package is only compatible with Windows.")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
setup(
|
|
10
|
-
name="dately",
|
|
11
|
-
version="2.1.3",
|
|
12
|
-
author="Cedric Moore Jr.",
|
|
13
|
-
author_email="cedric@doydl.tech",
|
|
14
|
-
description="A library for manipulating datetime strings.",
|
|
15
|
-
long_description=open("README.md").read(),
|
|
16
|
-
long_description_content_type="text/markdown",
|
|
17
|
-
url="https://github.com/cedricmoorejr/dately",
|
|
18
|
-
project_urls={
|
|
19
|
-
"Source Code": "https://github.com/cedricmoorejr/dately/tree/main/dately",
|
|
20
|
-
},
|
|
21
|
-
packages=find_packages(),
|
|
22
|
-
package_data={
|
|
23
|
-
'dately': [
|
|
24
|
-
'mold/pyd/*.pyd',
|
|
25
|
-
'mold/pyd/cdatetime/*.pyd'
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
classifiers=[
|
|
29
|
-
"Programming Language :: Python :: 3",
|
|
30
|
-
"License :: OSI Approved :: MIT License",
|
|
31
|
-
"Operating System :: Microsoft :: Windows",
|
|
32
|
-
],
|
|
33
|
-
python_requires='>=3.8',
|
|
34
|
-
install_requires=[
|
|
35
|
-
"numpy",
|
|
36
|
-
"pandas",
|
|
37
|
-
"pytz",
|
|
38
|
-
"requests",
|
|
39
|
-
],
|
|
40
|
-
license="MIT",
|
|
41
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dately-2.1.3 → dately-2.1.4}/dately/mold/pyd/cdatetime/UniversalDateFormatter.cp38-win_amd64.pyd
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|