none-shall-parse 0.3.0__py3-none-any.whl → 0.4.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.
- none_shall_parse/__init__.py +1 -1
- none_shall_parse/dates.py +682 -0
- none_shall_parse/imeis.py +30 -30
- none_shall_parse/lists.py +2 -1
- none_shall_parse/parse.py +8 -8
- none_shall_parse/strings.py +14 -13
- none_shall_parse/types.py +2 -1
- {none_shall_parse-0.3.0.dist-info → none_shall_parse-0.4.0.dist-info}/METADATA +3 -2
- none_shall_parse-0.4.0.dist-info/RECORD +10 -0
- none_shall_parse-0.3.0.dist-info/RECORD +0 -9
- {none_shall_parse-0.3.0.dist-info → none_shall_parse-0.4.0.dist-info}/WHEEL +0 -0
none_shall_parse/__init__.py
CHANGED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
from typing import Callable, Any, Tuple, Sequence, List
|
|
6
|
+
|
|
7
|
+
import pendulum
|
|
8
|
+
from pendulum import local_timezone
|
|
9
|
+
from pendulum.tz.exceptions import InvalidTimezone
|
|
10
|
+
|
|
11
|
+
ZA_TZ = 'Africa/Johannesburg'
|
|
12
|
+
UTC_TZ = 'UTC'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DateUtilsError(Exception):
|
|
16
|
+
"""
|
|
17
|
+
Raised when some date calc gets crap input.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
message = None
|
|
21
|
+
|
|
22
|
+
def __init__(self, message):
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.message = message
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def assert_week_start_date_is_valid(wso):
|
|
28
|
+
if wso < 1 or wso > 7:
|
|
29
|
+
raise DateUtilsError("Weeks can only start on days between 1 and 7")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def assert_month_start_date_is_valid(mso):
|
|
33
|
+
if mso > 28 or mso < 1:
|
|
34
|
+
raise DateUtilsError("Months can only start on days between 1 and 28")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_datetime_now(naive: bool = False, tz: str | None = None) -> datetime:
|
|
38
|
+
"""
|
|
39
|
+
Get the current date and time.
|
|
40
|
+
|
|
41
|
+
This function retrieves the current date and time using the pendulum library. It can
|
|
42
|
+
return the datetime in either a naive or timezone-aware format, depending on the
|
|
43
|
+
parameters provided.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
naive (bool): If True, returns a naive datetime object without timezone information.
|
|
47
|
+
Defaults to True.
|
|
48
|
+
tz (Optional[str]): The timezone to use if a timezone-aware datetime is requested.
|
|
49
|
+
If not provided and naive is False, the default system timezone
|
|
50
|
+
is used.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
datetime: The current date and time based on the specified parameters.
|
|
54
|
+
"""
|
|
55
|
+
return pendulum.now().naive() if naive else pendulum.now(tz=tz)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def za_now() -> datetime:
|
|
59
|
+
"""
|
|
60
|
+
Returns the current date and time in the South African timezone.
|
|
61
|
+
|
|
62
|
+
This function retrieves the current date and time, ensuring it is aware of
|
|
63
|
+
time zone settings. It uses the South African timezone (ZA_TZ) for proper
|
|
64
|
+
time localization.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
datetime: The current date and time in the South African timezone.
|
|
68
|
+
"""
|
|
69
|
+
return get_datetime_now(naive=False, tz=ZA_TZ)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def za_ordinal_year_day_now() -> int:
|
|
73
|
+
"""
|
|
74
|
+
Returns the current ordinal day of the year for the ZA_TZ timezone.
|
|
75
|
+
|
|
76
|
+
This function calculates and returns the day of the year based on the current
|
|
77
|
+
date and time in the ZA (South Africa) timezone.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
int: The current ordinal day of the year in ZA_TZ timezone.
|
|
81
|
+
"""
|
|
82
|
+
return pendulum.now(ZA_TZ).day_of_year
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def za_ordinal_year_day_tomorrow() -> int:
|
|
86
|
+
"""
|
|
87
|
+
Returns the ordinal day of the year for tomorrow in the ZA_TZ timezone.
|
|
88
|
+
|
|
89
|
+
This function calculates the ordinal day of the year (1 to 366) for the date
|
|
90
|
+
that is one day ahead of today, as per the ZA_TZ timezone.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
int: The ordinal day of the year for tomorrow in the ZA_TZ timezone.
|
|
94
|
+
"""
|
|
95
|
+
return pendulum.now(ZA_TZ).add(days=1).day_of_year
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def utc_epoch_start() -> datetime:
|
|
99
|
+
"""
|
|
100
|
+
Gets the UTC epoch start time as a datetime object.
|
|
101
|
+
|
|
102
|
+
This function calculates the start of the UNIX epoch (January 1, 1970)
|
|
103
|
+
in UTC as a datetime object. It leverages the `pendulum` library for
|
|
104
|
+
handling the time calculation with the specified UTC timezone.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
datetime: A datetime object representing the start of the UTC epoch.
|
|
108
|
+
"""
|
|
109
|
+
return pendulum.from_timestamp(0)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _now_offset_n_units(n: int, units: str, naive: bool = False,
|
|
113
|
+
tz: str | None = None) -> datetime:
|
|
114
|
+
"""
|
|
115
|
+
Calculate a datetime object offset by a specified number of time units.
|
|
116
|
+
|
|
117
|
+
This function allows you to calculate a datetime offset by a given number of units
|
|
118
|
+
(minutes, hours, days, etc.) from the current time. You can also specify the timezone
|
|
119
|
+
and whether the returned datetime should be naive or timezone-aware.
|
|
120
|
+
|
|
121
|
+
Parameters:
|
|
122
|
+
n (int): The number of units to offset the current time by.
|
|
123
|
+
units (str): The type of time unit to offset by.
|
|
124
|
+
naive (bool): Whether to return a naive datetime (without timezone information).
|
|
125
|
+
Defaults to False.
|
|
126
|
+
tz (Optional[str]): The timezone of the resulting datetime. If not provided,
|
|
127
|
+
the system's local timezone is used.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
datetime: The calculated datetime object, optionally timezone-aware or naive.
|
|
131
|
+
"""
|
|
132
|
+
kwargs = {units: n}
|
|
133
|
+
return pendulum.now().add(**kwargs).naive() if naive else pendulum.now(tz).add(
|
|
134
|
+
**kwargs)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def now_offset_n_minutes(n: int, naive: bool = False,
|
|
138
|
+
tz: str | None = None) -> datetime:
|
|
139
|
+
return _now_offset_n_units(n, units="minutes", naive=naive, tz=tz)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def now_offset_n_hours(n: int, naive: bool = False,
|
|
143
|
+
tz: str | None = None) -> datetime:
|
|
144
|
+
return _now_offset_n_units(n, units="hours", naive=naive, tz=tz)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def now_offset_n_days(n: int, naive: bool = False,
|
|
148
|
+
tz: str | None = None) -> datetime:
|
|
149
|
+
return _now_offset_n_units(n, units="days", naive=naive, tz=tz)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_datetime_tomorrow(naive: bool = False,
|
|
153
|
+
tz: str | None = None) -> datetime:
|
|
154
|
+
"""Get tomorrow's datetime"""
|
|
155
|
+
return now_offset_n_days(1, naive=naive, tz=tz)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_datetime_yesterday(naive: bool = False,
|
|
159
|
+
tz: str | None = None) -> datetime:
|
|
160
|
+
"""Get yesterday's datetime"""
|
|
161
|
+
return now_offset_n_days(-1, naive=naive, tz=tz)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_utc_datetime_offset_n_days(n: int = 0) -> datetime:
|
|
165
|
+
"""Get UTC datetime n with an offset of n days"""
|
|
166
|
+
return pendulum.now(UTC_TZ).add(days=n)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def epoch_to_datetime(epoch_int: int | float, naive: bool = False,
|
|
170
|
+
tz: str | None = None) -> datetime:
|
|
171
|
+
"""
|
|
172
|
+
Converts an epoch timestamp to a datetime object.
|
|
173
|
+
|
|
174
|
+
This function takes an input epoch timestamp and converts it into a
|
|
175
|
+
datetime object using the Pendulum library. It supports conversion
|
|
176
|
+
to either naive or timezone-aware datetime objects based on the
|
|
177
|
+
parameters provided.
|
|
178
|
+
|
|
179
|
+
Parameters:
|
|
180
|
+
epoch_int (int | float): The epoch timestamp to convert to a datetime
|
|
181
|
+
object. It can be provided as an integer or float value.
|
|
182
|
+
naive (bool): If True, the resulting datetime object will be naive
|
|
183
|
+
(without timezone information). Defaults to False.
|
|
184
|
+
tz (Optional[str]): The timezone in which the resulting datetime object
|
|
185
|
+
should be created. If not provided, the system's default timezone
|
|
186
|
+
will be used.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
datetime: A datetime object representing the converted epoch timestamp.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
# We force the timezone to the user's local timezone if none was supplied
|
|
193
|
+
# to make this function behave the same as the other functions, where the absense
|
|
194
|
+
# of a timezone is interpreted as the user's local timezone.'
|
|
195
|
+
if tz is None:
|
|
196
|
+
tz = local_timezone()
|
|
197
|
+
|
|
198
|
+
if naive:
|
|
199
|
+
return pendulum.from_timestamp(int(epoch_int), tz=tz).naive()
|
|
200
|
+
return pendulum.from_timestamp(int(epoch_int), tz=tz)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def epoch_to_utc_datetime(epoch_int: int | float) -> datetime:
|
|
204
|
+
"""
|
|
205
|
+
Converts an epoch timestamp to a UTC datetime object.
|
|
206
|
+
|
|
207
|
+
This function takes an integer or float representing an epoch timestamp,
|
|
208
|
+
and converts it to a datetime object in UTC timezone using Pendulum.
|
|
209
|
+
|
|
210
|
+
Parameters:
|
|
211
|
+
epoch_int: An epoch timestamp represented as an integer or float.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
A datetime object in UTC corresponding to the provided epoch timestamp.
|
|
215
|
+
"""
|
|
216
|
+
return pendulum.from_timestamp(int(epoch_int), tz=UTC_TZ)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def is_office_hours_in_timezone(epoch_int: int | float, tz: str | None = None) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
Determines if a given epoch timestamp falls within office hours for a
|
|
222
|
+
specific timezone.
|
|
223
|
+
|
|
224
|
+
Office hours are considered to be between 08:00 and 17:00 (8 AM to 5 PM) in
|
|
225
|
+
the specified timezone. The function converts the provided epoch timestamp into
|
|
226
|
+
a datetime object according to the given timezone and checks whether the time falls
|
|
227
|
+
within the defined office hours.
|
|
228
|
+
|
|
229
|
+
Parameters:
|
|
230
|
+
epoch_int: int | float
|
|
231
|
+
Epoch timestamp to be checked. It must represent the number of seconds
|
|
232
|
+
(or fraction of seconds, if float) since the UNIX epoch.
|
|
233
|
+
tz: str, optional
|
|
234
|
+
Timezone expressed as a string. Defaults to local system timezone.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
bool
|
|
238
|
+
True if the epoch timestamp occurs within office hours for the specified
|
|
239
|
+
timezone otherwise, False.
|
|
240
|
+
"""
|
|
241
|
+
dt = pendulum.from_timestamp(int(epoch_int), tz=tz)
|
|
242
|
+
|
|
243
|
+
# Create office hours boundaries for the same date
|
|
244
|
+
oh_begin = dt.replace(hour=8, minute=0, second=0, microsecond=0)
|
|
245
|
+
oh_end = dt.replace(hour=17, minute=0, second=0, microsecond=0)
|
|
246
|
+
|
|
247
|
+
return oh_begin < dt < oh_end
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def get_datetime_from_ordinal_and_sentinel(
|
|
251
|
+
sentinel: datetime | None = None) -> Callable[[int], datetime]:
|
|
252
|
+
"""
|
|
253
|
+
Given an ordinal year day, and a sentinel datetime, get the closest past
|
|
254
|
+
datetime to the sentinel that had the given ordinal year day.
|
|
255
|
+
|
|
256
|
+
If the given sentinel is timezone-aware, the results will be as well, and in the
|
|
257
|
+
same timezone. If the given sentinel is naive, the results will be naive.
|
|
258
|
+
"""
|
|
259
|
+
# Check timezone awareness
|
|
260
|
+
naive = sentinel.tzinfo is None
|
|
261
|
+
|
|
262
|
+
# Convert to Pendulum for easier manipulation
|
|
263
|
+
sentinel_pdt = pendulum.instance(sentinel)
|
|
264
|
+
sentinel_doy = sentinel_pdt.day_of_year
|
|
265
|
+
sentinel_year = sentinel_pdt.year
|
|
266
|
+
|
|
267
|
+
# Create start of years with CORRECT timezone handling
|
|
268
|
+
if naive:
|
|
269
|
+
# Use pendulum.naive() to create naive datetimes
|
|
270
|
+
this_year = pendulum.naive(sentinel_year, 1, 1)
|
|
271
|
+
last_year = pendulum.naive(sentinel_year - 1, 1, 1)
|
|
272
|
+
else:
|
|
273
|
+
# Create in the same timezone as sentinel (not UTC!)
|
|
274
|
+
this_year = pendulum.datetime(sentinel_year, 1, 1, tz=sentinel_pdt.timezone)
|
|
275
|
+
last_year = pendulum.datetime(sentinel_year - 1, 1, 1, tz=sentinel_pdt.timezone)
|
|
276
|
+
|
|
277
|
+
def f(ordinal: int) -> datetime:
|
|
278
|
+
# Choose year based on whether ordinal is before or after sentinel's day of year
|
|
279
|
+
dt = this_year if ordinal <= sentinel_doy else last_year
|
|
280
|
+
|
|
281
|
+
# Handle leap year edge case
|
|
282
|
+
if ordinal == 366 and not dt.is_leap_year():
|
|
283
|
+
if naive:
|
|
284
|
+
return pendulum.naive(1970, 1, 1)
|
|
285
|
+
else:
|
|
286
|
+
return pendulum.datetime(1970, 1, 1, tz=sentinel_pdt.timezone)
|
|
287
|
+
|
|
288
|
+
# Add (ordinal - 1) days to start of year
|
|
289
|
+
result = dt.add(days=ordinal - 1)
|
|
290
|
+
|
|
291
|
+
# Return as-is (Pendulum datetime preserves timezone awareness)
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
return f
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ------------------------------------------------------------------[ Span Functions ]--
|
|
298
|
+
def day_span(pts: datetime) -> Tuple[datetime, datetime]:
|
|
299
|
+
"""
|
|
300
|
+
Returns the beginning and end of the day passed in.
|
|
301
|
+
begin is inclusive and end is exclusive.
|
|
302
|
+
|
|
303
|
+
If the given datetime is timezone aware, the results will be as well, and in the
|
|
304
|
+
same timezone. If the given sentinel is naive, the results will be naive.
|
|
305
|
+
|
|
306
|
+
Examples:
|
|
307
|
+
from datetime import datetime
|
|
308
|
+
dt = datetime(2023, 12, 25, 14, 30, 45)
|
|
309
|
+
|
|
310
|
+
start, end = day_span(dt)
|
|
311
|
+
print(type(start)) # <class 'datetime.datetime'>
|
|
312
|
+
|
|
313
|
+
"""
|
|
314
|
+
# Check if input is naive or aware
|
|
315
|
+
naive = pts.tzinfo is None
|
|
316
|
+
|
|
317
|
+
# Convert to Pendulum for easier manipulation
|
|
318
|
+
pdt = pendulum.instance(pts)
|
|
319
|
+
|
|
320
|
+
# Use Pendulum's clean API
|
|
321
|
+
begin = pdt.start_of('day')
|
|
322
|
+
end = pdt.add(days=1).start_of('day')
|
|
323
|
+
|
|
324
|
+
if naive:
|
|
325
|
+
return begin.naive(), end.naive()
|
|
326
|
+
else:
|
|
327
|
+
return begin, end
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def week_span(wso: int) -> Callable[[datetime], Tuple[datetime, datetime]]:
|
|
331
|
+
"""
|
|
332
|
+
Given an integer between 1 and 7, return a function that will give the
|
|
333
|
+
start and end dates of the week.
|
|
334
|
+
|
|
335
|
+
If the given datetime is timezone aware, the results will be as well, and in the
|
|
336
|
+
same timezone. If the given sentinel is naive, the results will be naive.
|
|
337
|
+
|
|
338
|
+
Examples:
|
|
339
|
+
from datetime import datetime
|
|
340
|
+
dt = datetime(2023, 12, 25, 14, 30, 45)
|
|
341
|
+
|
|
342
|
+
# Week starting on Wednesday (ISO day 3)
|
|
343
|
+
week_func = week_span(3)
|
|
344
|
+
week_start, week_end = week_func(dt)
|
|
345
|
+
|
|
346
|
+
:param wso: ISO weekday integer (1=Monday, 7=Sunday)
|
|
347
|
+
"""
|
|
348
|
+
assert_week_start_date_is_valid(wso)
|
|
349
|
+
|
|
350
|
+
def find_dates(pts: datetime) -> Tuple[datetime, datetime]:
|
|
351
|
+
# Check if input is naive or aware
|
|
352
|
+
naive = pts.tzinfo is None
|
|
353
|
+
pdt = pendulum.instance(pts)
|
|
354
|
+
|
|
355
|
+
# Get to the desired week start day
|
|
356
|
+
current_weekday = pdt.weekday() + 1 # Pendulum uses 0-6, we want 1-7
|
|
357
|
+
days_back = (current_weekday - wso) % 7
|
|
358
|
+
|
|
359
|
+
begin = pdt.start_of('day').subtract(days=days_back)
|
|
360
|
+
end = begin.add(days=7)
|
|
361
|
+
|
|
362
|
+
if naive:
|
|
363
|
+
return begin.naive(), end.naive()
|
|
364
|
+
else:
|
|
365
|
+
return begin, end
|
|
366
|
+
|
|
367
|
+
return find_dates
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def month_span(mso: int) -> Callable[[datetime], Tuple[datetime, datetime]]:
|
|
371
|
+
"""
|
|
372
|
+
Given an integer between 1 and 28, return a function that will give the
|
|
373
|
+
start and end dates of the custom month period.
|
|
374
|
+
|
|
375
|
+
If the given datetime is timezone aware, the results will be as well, and in the
|
|
376
|
+
same timezone. If the given sentinel is naive, the results will be naive.
|
|
377
|
+
|
|
378
|
+
Examples:
|
|
379
|
+
from datetime import datetime
|
|
380
|
+
dt = datetime(2023, 12, 25, 14, 30, 45)
|
|
381
|
+
|
|
382
|
+
# Month starting on 15th
|
|
383
|
+
month_func = month_span(15)
|
|
384
|
+
month_start, month_end = month_func(dt)
|
|
385
|
+
|
|
386
|
+
:param mso: Integer (1-28, the day of month to start periods on)
|
|
387
|
+
"""
|
|
388
|
+
assert_month_start_date_is_valid(mso)
|
|
389
|
+
|
|
390
|
+
def find_dates(pts: datetime) -> Tuple[datetime, datetime]:
|
|
391
|
+
# Convert to Pendulum
|
|
392
|
+
naive = pts.tzinfo is None
|
|
393
|
+
pdt = pendulum.instance(pts)
|
|
394
|
+
current_day = pdt.day
|
|
395
|
+
|
|
396
|
+
if current_day >= mso:
|
|
397
|
+
# We're in the current month period
|
|
398
|
+
begin = pdt.start_of('day').replace(day=mso)
|
|
399
|
+
else:
|
|
400
|
+
# We're in the previous month period
|
|
401
|
+
begin = pdt.start_of('day').subtract(months=1).replace(day=mso)
|
|
402
|
+
|
|
403
|
+
# End is mso of next month from `begin`
|
|
404
|
+
end = begin.add(months=1)
|
|
405
|
+
|
|
406
|
+
if naive:
|
|
407
|
+
return begin.naive(), end.naive()
|
|
408
|
+
else:
|
|
409
|
+
return begin, end
|
|
410
|
+
|
|
411
|
+
return find_dates
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def arb_span(dates: Sequence[str | datetime], naive: bool = False) -> Callable[
|
|
415
|
+
[], Tuple[datetime, datetime]]:
|
|
416
|
+
"""
|
|
417
|
+
Parses two given dates and returns a callable function that provides the date range
|
|
418
|
+
as a tuple of datetime objects. The function ensures the date range is valid and
|
|
419
|
+
always returns the earlier date as the start and the later date as the end.
|
|
420
|
+
|
|
421
|
+
Parameters:
|
|
422
|
+
dates (Sequence[str | datetime]): A sequence containing exactly two dates where each
|
|
423
|
+
date is either a string or a datetime object.
|
|
424
|
+
naive (bool): Optional flag. If True, the returned datetime objects will not
|
|
425
|
+
have timezone information (naive datetime). Defaults to False.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Callable[[], Tuple[datetime, datetime]]: A function that, when invoked, returns a
|
|
429
|
+
tuple of datetime objects (start, end)
|
|
430
|
+
representing the date range.
|
|
431
|
+
|
|
432
|
+
Raises:
|
|
433
|
+
DateUtilsError: If the provided dates are invalid, identical, or there's an error
|
|
434
|
+
during parsing.
|
|
435
|
+
"""
|
|
436
|
+
try:
|
|
437
|
+
parsed_dates = []
|
|
438
|
+
|
|
439
|
+
for date in dates[:2]:
|
|
440
|
+
if isinstance(date, str):
|
|
441
|
+
# Parse string - pendulum.parse returns UTC for date-only strings
|
|
442
|
+
parsed = pendulum.parse(date)
|
|
443
|
+
|
|
444
|
+
# If it's a date-only string (no time/timezone info), treat as naive
|
|
445
|
+
if 'T' not in date and ' ' not in date:
|
|
446
|
+
parsed = parsed.naive()
|
|
447
|
+
|
|
448
|
+
parsed_dates.append(parsed.start_of('day'))
|
|
449
|
+
else:
|
|
450
|
+
# It's already a datetime
|
|
451
|
+
if date.tzinfo is None:
|
|
452
|
+
# Input is naive, keep it naive using pendulum.naive()
|
|
453
|
+
parsed = pendulum.naive(date.year, date.month, date.day,
|
|
454
|
+
date.hour, date.minute, date.second,
|
|
455
|
+
date.microsecond)
|
|
456
|
+
else:
|
|
457
|
+
# Input is timezone-aware, preserve it
|
|
458
|
+
parsed = pendulum.instance(date)
|
|
459
|
+
|
|
460
|
+
parsed_dates.append(parsed.start_of('day'))
|
|
461
|
+
|
|
462
|
+
a, b = parsed_dates
|
|
463
|
+
|
|
464
|
+
# Check if they're comparable (both naive or both aware)
|
|
465
|
+
if (a.tzinfo is None) != (b.tzinfo is None):
|
|
466
|
+
raise DateUtilsError("Cannot compare naive and timezone-aware datetimes")
|
|
467
|
+
|
|
468
|
+
if a == b:
|
|
469
|
+
raise DateUtilsError("Dates may not be the same")
|
|
470
|
+
|
|
471
|
+
# Ensure `begin` is the earlier date and `end` is the later date
|
|
472
|
+
begin = a if a < b else b
|
|
473
|
+
end = b if a < b else a
|
|
474
|
+
|
|
475
|
+
except Exception as ex:
|
|
476
|
+
raise DateUtilsError(f"Error parsing dates: {ex}")
|
|
477
|
+
|
|
478
|
+
def find_dates() -> Tuple[datetime, datetime]:
|
|
479
|
+
"""
|
|
480
|
+
:return: tuple of datetime objects (start, end)
|
|
481
|
+
"""
|
|
482
|
+
if naive:
|
|
483
|
+
return begin.naive(), end.naive()
|
|
484
|
+
return begin, end
|
|
485
|
+
|
|
486
|
+
return find_dates
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def calendar_month_start_end(date_in_month: datetime | None = None) -> Tuple[
|
|
490
|
+
datetime, datetime]:
|
|
491
|
+
naive = date_in_month.tzinfo is None
|
|
492
|
+
|
|
493
|
+
if date_in_month is None:
|
|
494
|
+
date_in_month = za_now()
|
|
495
|
+
|
|
496
|
+
pdt = pendulum.instance(date_in_month)
|
|
497
|
+
|
|
498
|
+
# One-liner for both values
|
|
499
|
+
start = pdt.start_of('month')
|
|
500
|
+
end = start.add(months=1)
|
|
501
|
+
|
|
502
|
+
if naive:
|
|
503
|
+
return start.naive(), end.naive()
|
|
504
|
+
|
|
505
|
+
return start, end
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# --------------------------------------------------------------------[ Unaware time ]--
|
|
509
|
+
def unix_timestamp() -> int:
|
|
510
|
+
"""
|
|
511
|
+
Unix timestamps are, by definition, the number of seconds since the epoch - a
|
|
512
|
+
fixed moment in time, defined as 01-01-1970 UTC.
|
|
513
|
+
:return: Current Unix timestamp
|
|
514
|
+
"""
|
|
515
|
+
return round(time.time())
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def sentinel_date_and_ordinal_to_date(sentinel_date: datetime,
|
|
519
|
+
ordinal: int | float) -> date:
|
|
520
|
+
"""Convert sentinel date and ordinal day to actual date"""
|
|
521
|
+
year = sentinel_date.year
|
|
522
|
+
int_ordinal = int(ordinal)
|
|
523
|
+
|
|
524
|
+
# If sentinel is Jan 1st and ordinal > 1, use previous year
|
|
525
|
+
if sentinel_date.month == 1 and sentinel_date.day == 1 and int_ordinal > 1:
|
|
526
|
+
year = year - 1
|
|
527
|
+
|
|
528
|
+
# Use Pendulum for date arithmetic
|
|
529
|
+
dt = pendulum.datetime(year, 1, 1).add(days=int_ordinal - 1)
|
|
530
|
+
return dt.date()
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def seconds_to_end_of_month() -> int:
|
|
534
|
+
"""Calculate seconds remaining until the end of the current month"""
|
|
535
|
+
now = pendulum.now(UTC_TZ)
|
|
536
|
+
end_of_month = now.end_of('month')
|
|
537
|
+
return int((end_of_month - now).total_seconds())
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def standard_tz_timestring(ts: int | float, tz: str = ZA_TZ) -> str:
|
|
541
|
+
"""
|
|
542
|
+
Format timestamp as: 2022-02-22 15:28:10 (SAST)
|
|
543
|
+
:param ts: Seconds since epoch
|
|
544
|
+
:param tz: Timezone string
|
|
545
|
+
:return: Formatted datetime string
|
|
546
|
+
"""
|
|
547
|
+
dt = pendulum.from_timestamp(int(ts), tz=tz)
|
|
548
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S (%Z)")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def get_notice_end_date(given_date: datetime | date | None = None) -> date:
|
|
552
|
+
"""
|
|
553
|
+
A notice end date is the end of the month of the given date if the given date
|
|
554
|
+
is before or on the 15th. If the given date is after the 15th, the notice period
|
|
555
|
+
ends at the end of the next month.
|
|
556
|
+
:param given_date: Date to calculate the notice end from
|
|
557
|
+
:return: Notice end date
|
|
558
|
+
"""
|
|
559
|
+
if given_date is None:
|
|
560
|
+
given_date = pendulum.now().today()
|
|
561
|
+
elif isinstance(given_date, datetime):
|
|
562
|
+
given_date = given_date.date()
|
|
563
|
+
elif not isinstance(given_date, date):
|
|
564
|
+
raise ValueError(
|
|
565
|
+
"Given date must be a datetime.date or datetime.datetime object")
|
|
566
|
+
|
|
567
|
+
pdt = pendulum.instance(given_date)
|
|
568
|
+
if given_date.day <= 15:
|
|
569
|
+
# End of current month
|
|
570
|
+
end_date = pdt.add(months=1).start_of('month')
|
|
571
|
+
else:
|
|
572
|
+
# End of next month
|
|
573
|
+
end_date = pdt.add(months=2).start_of('month')
|
|
574
|
+
|
|
575
|
+
return end_date
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def dt_to_za_time_string(v: datetime) -> str:
|
|
579
|
+
"""Convert datetime to South Africa time string"""
|
|
580
|
+
# Convert to Pendulum
|
|
581
|
+
naive = v.tzinfo is None
|
|
582
|
+
if naive:
|
|
583
|
+
pdt = pendulum.instance(v, tz=ZA_TZ)
|
|
584
|
+
else:
|
|
585
|
+
pdt = pendulum.instance(v).in_timezone(ZA_TZ)
|
|
586
|
+
return pdt.strftime("%Y-%m-%d %H:%M:%S")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def months_ago_selection() -> List[Tuple[int, str]]:
|
|
590
|
+
"""Generate list of (index, "Month-Year") tuples for last 12 months"""
|
|
591
|
+
today = pendulum.today()
|
|
592
|
+
|
|
593
|
+
return [
|
|
594
|
+
(i, today.subtract(months=i).strftime("%B-%Y"))
|
|
595
|
+
for i in range(12)
|
|
596
|
+
]
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def is_aware(dt: datetime) -> bool:
|
|
600
|
+
"""Check if a datetime object is timezone-aware."""
|
|
601
|
+
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def make_aware(dt: datetime | None, tz: str = None) -> datetime | None:
|
|
605
|
+
"""
|
|
606
|
+
Convert a naive datetime to a timezone-aware datetime using Pendulum.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
dt: The datetime object to convert. If None, returns None.
|
|
610
|
+
tz: The timezone to apply (default: The user's default timezone).).
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
A timezone-aware datetime object.
|
|
614
|
+
|
|
615
|
+
Raises:
|
|
616
|
+
TypeError: If dt is not a datetime object or None.
|
|
617
|
+
ValueError: If dt is already timezone-aware.
|
|
618
|
+
DateUtilsError: If the timezone string is invalid.
|
|
619
|
+
"""
|
|
620
|
+
# We force the timezone to the user's local timezone if none was supplied
|
|
621
|
+
# to make this function behave the same as the other functions, where the absense
|
|
622
|
+
# of a timezone is interpreted as the user's local timezone.
|
|
623
|
+
if tz is None:
|
|
624
|
+
tz = local_timezone()
|
|
625
|
+
|
|
626
|
+
if dt is None:
|
|
627
|
+
return None
|
|
628
|
+
if not isinstance(dt, datetime):
|
|
629
|
+
raise TypeError(f"Expected datetime or None, got {type(dt).__name__}")
|
|
630
|
+
if is_aware(dt):
|
|
631
|
+
raise ValueError(f"Datetime is already timezone-aware with {dt.tzinfo}")
|
|
632
|
+
|
|
633
|
+
try:
|
|
634
|
+
return pendulum.instance(dt, tz=tz)
|
|
635
|
+
except InvalidTimezone as e:
|
|
636
|
+
raise DateUtilsError(f"Invalid timezone: {tz}") from e
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def unaware_to_utc_aware(dt: datetime | None) -> datetime | None:
|
|
640
|
+
"""Convert naive datetime to UTC-aware datetime using Pendulum."""
|
|
641
|
+
if not isinstance(dt, (datetime, type(None))):
|
|
642
|
+
raise TypeError(f"Expected datetime or None, got {type(dt)}")
|
|
643
|
+
|
|
644
|
+
if dt is None or is_aware(dt):
|
|
645
|
+
return dt
|
|
646
|
+
|
|
647
|
+
# Use Pendulum for clean UTC conversion
|
|
648
|
+
pdt = pendulum.instance(dt, tz=UTC_TZ)
|
|
649
|
+
return pdt
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def timer_decorator(logger: logging.Logger | None = None):
|
|
653
|
+
"""
|
|
654
|
+
Timer decorator that optionally accepts a logger.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
logger: Logger instance to use for timing output. If None, uses print().
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
Decorator function
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
def decorator(func: Callable) -> Callable:
|
|
664
|
+
@functools.wraps(func)
|
|
665
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
666
|
+
start_time = time.perf_counter()
|
|
667
|
+
result = func(*args, **kwargs)
|
|
668
|
+
end_time = time.perf_counter()
|
|
669
|
+
execution_time = end_time - start_time
|
|
670
|
+
|
|
671
|
+
message = f"{func.__name__}:TIME:{execution_time:.6f}s"
|
|
672
|
+
|
|
673
|
+
if logger:
|
|
674
|
+
logger.info(message)
|
|
675
|
+
else:
|
|
676
|
+
print(message) # Fallback to print if no logger provided
|
|
677
|
+
|
|
678
|
+
return result
|
|
679
|
+
|
|
680
|
+
return wrapper
|
|
681
|
+
|
|
682
|
+
return decorator
|
none_shall_parse/imeis.py
CHANGED
|
@@ -47,28 +47,28 @@ def is_valid_luhn(n: Union[str, int]) -> bool:
|
|
|
47
47
|
e
|
|
48
48
|
for e in n
|
|
49
49
|
if e
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
50
|
+
in [
|
|
51
|
+
0,
|
|
52
|
+
1,
|
|
53
|
+
2,
|
|
54
|
+
3,
|
|
55
|
+
4,
|
|
56
|
+
5,
|
|
57
|
+
6,
|
|
58
|
+
7,
|
|
59
|
+
8,
|
|
60
|
+
9,
|
|
61
|
+
"0",
|
|
62
|
+
"1",
|
|
63
|
+
"2",
|
|
64
|
+
"3",
|
|
65
|
+
"4",
|
|
66
|
+
"5",
|
|
67
|
+
"6",
|
|
68
|
+
"7",
|
|
69
|
+
"8",
|
|
70
|
+
"9",
|
|
71
|
+
]
|
|
72
72
|
]
|
|
73
73
|
)
|
|
74
74
|
chars = [int(ch) for ch in str(n)][::-1] # Reversed Digits
|
|
@@ -135,16 +135,16 @@ def get_tac_from_imei(n: Union[str, int]) -> tuple[bool, str]:
|
|
|
135
135
|
"""
|
|
136
136
|
Determines the validity of an IMEI number and extracts its TAC if valid.
|
|
137
137
|
|
|
138
|
-
This function checks whether a provided IMEI (International Mobile Equipment
|
|
139
|
-
Identity) number is valid based on IMEI validation rules. If the given IMEI
|
|
140
|
-
is valid, the function also extracts and returns the TAC (Type Allocation
|
|
138
|
+
This function checks whether a provided IMEI (International Mobile Equipment
|
|
139
|
+
Identity) number is valid based on IMEI validation rules. If the given IMEI
|
|
140
|
+
is valid, the function also extracts and returns the TAC (Type Allocation
|
|
141
141
|
Code), which corresponds to the first 8 digits of the IMEI.
|
|
142
142
|
|
|
143
143
|
Parameters:
|
|
144
144
|
n (str): The IMEI number to be validated and processed.
|
|
145
145
|
|
|
146
146
|
Returns:
|
|
147
|
-
tuple: A tuple containing a boolean indicating whether the IMEI is valid
|
|
147
|
+
tuple: A tuple containing a boolean indicating whether the IMEI is valid
|
|
148
148
|
and a string representing the TAC if valid or a placeholder if invalid.
|
|
149
149
|
"""
|
|
150
150
|
tac = "Not a Valid IMEI"
|
|
@@ -188,9 +188,9 @@ def increment_imei(n: Union[str, int]) -> tuple[bool, str]:
|
|
|
188
188
|
"""
|
|
189
189
|
Determines if a given IMEI number is valid and increments it by 1 if valid.
|
|
190
190
|
|
|
191
|
-
This function first checks if the provided IMEI number is valid using the
|
|
191
|
+
This function first checks if the provided IMEI number is valid using the
|
|
192
192
|
is_valid_imei function. If the input is a valid IMEI, it increments the IMEI
|
|
193
|
-
value by 1 while retaining only the first 14 digits. If the input is not valid,
|
|
193
|
+
value by 1 while retaining only the first 14 digits. If the input is not valid,
|
|
194
194
|
it returns a predefined invalid result.
|
|
195
195
|
|
|
196
196
|
Parameters:
|
|
@@ -199,8 +199,8 @@ def increment_imei(n: Union[str, int]) -> tuple[bool, str]:
|
|
|
199
199
|
|
|
200
200
|
Returns:
|
|
201
201
|
tuple[bool, str]
|
|
202
|
-
A tuple where the first element is a boolean indicating whether the operation
|
|
203
|
-
was successful, and the second element is a string containing the incremented
|
|
202
|
+
A tuple where the first element is a boolean indicating whether the operation
|
|
203
|
+
was successful, and the second element is a string containing the incremented
|
|
204
204
|
IMEI number if valid or an error message if not valid.
|
|
205
205
|
"""
|
|
206
206
|
result = "Not a Valid IMEI"
|
none_shall_parse/lists.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import collections
|
|
2
2
|
from typing import Iterable, Generator, Any, List
|
|
3
|
+
|
|
3
4
|
from .types import T
|
|
4
5
|
|
|
6
|
+
|
|
5
7
|
def flatten(some_list: Iterable) -> Generator[Any, None, None]:
|
|
6
8
|
"""
|
|
7
9
|
Flattens a nested iterable into a one-dimensional generator.
|
|
@@ -44,4 +46,3 @@ def safe_list_get(lst: List[T], idx: int, default: T) -> T:
|
|
|
44
46
|
return lst[idx]
|
|
45
47
|
except IndexError:
|
|
46
48
|
return default
|
|
47
|
-
|
none_shall_parse/parse.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from typing import Callable, Any
|
|
2
2
|
from typing import Union
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
from
|
|
4
|
+
from .strings import slugify
|
|
5
|
+
from .types import ChoicesType, StringLike
|
|
6
6
|
|
|
7
|
-
_true_set = {
|
|
8
|
-
_false_set = {
|
|
7
|
+
_true_set = {"yes", "true", "t", "y", "1"}
|
|
8
|
+
_false_set = {"no", "false", "f", "n", "0"}
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def str_to_bool(v: Any, raise_exc: bool = False) -> bool | None:
|
|
@@ -123,8 +123,8 @@ def int_or_none(s: int | float | str | None) -> int | None:
|
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def choices_code_to_string(
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
choices: ChoicesType, default: str | None = None, to_slug: bool = False
|
|
127
|
+
) -> Callable[[Union[int, StringLike]], StringLike | None]:
|
|
128
128
|
"""
|
|
129
129
|
Converts a code to a corresponding string representation based on provided choices.
|
|
130
130
|
The function allows optional fallback to a default value and can slugify the resulting string
|
|
@@ -157,8 +157,8 @@ def choices_code_to_string(
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def choices_string_to_code(
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
choices: ChoicesType, default: Any = None, to_lower: bool = False
|
|
161
|
+
) -> Callable[[str], Union[int, str, None]]:
|
|
162
162
|
"""
|
|
163
163
|
Converts a dictionary of choices into a callable function that maps input strings
|
|
164
164
|
to their corresponding codes. This helper function is particularly useful for handling
|
none_shall_parse/strings.py
CHANGED
|
@@ -9,7 +9,8 @@ import unicodedata
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
_control_chars = "".join(
|
|
12
|
-
map(chr, itertools.chain(range(0x00, 0x20), range(0x7F, 0xA0)))
|
|
12
|
+
map(chr, itertools.chain(range(0x00, 0x20), range(0x7F, 0xA0)))
|
|
13
|
+
)
|
|
13
14
|
_re_control_char = re.compile("[%s]" % re.escape(_control_chars))
|
|
14
15
|
_re_combine_whitespace = re.compile(r"\s+")
|
|
15
16
|
|
|
@@ -74,8 +75,8 @@ def is_quoted_string(s: str, strip: bool = False) -> tuple[bool, str]:
|
|
|
74
75
|
"""
|
|
75
76
|
Checks if a given string is enclosed in quotes and optionally strips the quotes.
|
|
76
77
|
|
|
77
|
-
The function determines whether a given string starts and ends with matching quotes,
|
|
78
|
-
either single quotes (') or double quotes ("). If the string is quoted and
|
|
78
|
+
The function determines whether a given string starts and ends with matching quotes,
|
|
79
|
+
either single quotes (') or double quotes ("). If the string is quoted and
|
|
79
80
|
the `strip` parameter is set to True, it removes the enclosing quotes and returns
|
|
80
81
|
the unquoted string.
|
|
81
82
|
|
|
@@ -88,8 +89,8 @@ def is_quoted_string(s: str, strip: bool = False) -> tuple[bool, str]:
|
|
|
88
89
|
|
|
89
90
|
Returns:
|
|
90
91
|
tuple[bool, str]
|
|
91
|
-
A tuple where the first element is a boolean indicating whether the string
|
|
92
|
-
is quoted, and the second element is the original string or the stripped
|
|
92
|
+
A tuple where the first element is a boolean indicating whether the string
|
|
93
|
+
is quoted, and the second element is the original string or the stripped
|
|
93
94
|
version if `strip` is True.
|
|
94
95
|
"""
|
|
95
96
|
is_quoted = False
|
|
@@ -131,7 +132,7 @@ def is_numeric_string(s: str, convert: bool = False) -> tuple[bool, str | int |
|
|
|
131
132
|
tuple
|
|
132
133
|
A tuple containing a boolean and the result:
|
|
133
134
|
- A boolean indicating whether the input string is numeric or not.
|
|
134
|
-
- The numeric value if `convert` is True and the string is numeric;
|
|
135
|
+
- The numeric value if `convert` is True and the string is numeric;
|
|
135
136
|
otherwise, the original string.
|
|
136
137
|
"""
|
|
137
138
|
is_numeric = False
|
|
@@ -218,15 +219,15 @@ def calc_hash(*args: Any) -> str:
|
|
|
218
219
|
Returns:
|
|
219
220
|
str: The computed SHA-1 hash as a hexadecimal string.
|
|
220
221
|
"""
|
|
221
|
-
s =
|
|
222
|
+
s = "_".join(map(str, args))
|
|
222
223
|
return hashlib.sha1(s.encode("utf-16")).hexdigest()
|
|
223
224
|
|
|
224
225
|
|
|
225
226
|
def generate_random_password(n: int = 10) -> str:
|
|
226
227
|
"""
|
|
227
|
-
Generates a random password meeting specific criteria for complexity. The
|
|
228
|
-
function ensures the password contains at least one lowercase letter, one
|
|
229
|
-
uppercase letter, and at least three numeric digits. The length of the
|
|
228
|
+
Generates a random password meeting specific criteria for complexity. The
|
|
229
|
+
function ensures the password contains at least one lowercase letter, one
|
|
230
|
+
uppercase letter, and at least three numeric digits. The length of the
|
|
230
231
|
password can be customized using the 'n' parameter.
|
|
231
232
|
|
|
232
233
|
Parameters:
|
|
@@ -239,9 +240,9 @@ def generate_random_password(n: int = 10) -> str:
|
|
|
239
240
|
while True:
|
|
240
241
|
password = "".join(secrets.choice(alphabet) for i in range(n))
|
|
241
242
|
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
any(c.islower() for c in password)
|
|
244
|
+
and any(c.isupper() for c in password)
|
|
245
|
+
and sum(c.isdigit() for c in password) >= 3
|
|
245
246
|
):
|
|
246
247
|
break
|
|
247
248
|
return password
|
none_shall_parse/types.py
CHANGED
|
@@ -12,6 +12,7 @@ class StringLike(Protocol):
|
|
|
12
12
|
commonly used string manipulation methods. Objects adhering to
|
|
13
13
|
this protocol can mimic the behavior of standard Python strings.
|
|
14
14
|
"""
|
|
15
|
+
|
|
15
16
|
def __str__(self) -> str: ...
|
|
16
17
|
def __len__(self) -> int: ...
|
|
17
18
|
def __add__(self, other: str) -> str: ...
|
|
@@ -28,4 +29,4 @@ class StringLike(Protocol):
|
|
|
28
29
|
|
|
29
30
|
ChoicesType = Sequence[Tuple[Union[int, str], StringLike]]
|
|
30
31
|
|
|
31
|
-
T = TypeVar(
|
|
32
|
+
T = TypeVar("T")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: none-shall-parse
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Trinity Shared Python utilities.
|
|
5
5
|
Author: Andries Niemandt, Jan Badenhorst
|
|
6
6
|
Author-email: Andries Niemandt <andries.niemandt@trintel.co.za>, Jan Badenhorst <jan@trintel.co.za>
|
|
@@ -8,6 +8,7 @@ License: MIT
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Dist: pendulum
|
|
11
12
|
Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
|
|
12
13
|
Requires-Dist: ruff>=0.12.3 ; extra == 'dev'
|
|
13
14
|
Requires-Dist: isort ; extra == 'dev'
|
|
@@ -53,7 +54,7 @@ pip install none-shall-parse
|
|
|
53
54
|
|
|
54
55
|
## Development Quick Start
|
|
55
56
|
|
|
56
|
-
#### To build
|
|
57
|
+
#### To build and publish to pypi:
|
|
57
58
|
|
|
58
59
|
Update the version in the `pyproject.toml` file, then:
|
|
59
60
|
```bash
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
none_shall_parse/__init__.py,sha256=3uhWV40LVbfVnCtRGCDLdTKyBBcoH56zgIApAQWWN0Q,549
|
|
2
|
+
none_shall_parse/dates.py,sha256=njpvrozrbAqWsfdXTl5crR9SiQWONSv8W27QE-CCV94,23430
|
|
3
|
+
none_shall_parse/imeis.py,sha256=o-TgbWDU4tmv1MHnAxfLCeZUpCZ8lt7VKOHrIeBmUFE,6837
|
|
4
|
+
none_shall_parse/lists.py,sha256=buAahex2iOYXZIcGOFfE9y9BYgBgvo3RilIiv1BALJ8,1706
|
|
5
|
+
none_shall_parse/parse.py,sha256=77bXZAtwFksRwuZ9Ax0lPxEjFpyjkQBqRa5mBc1WkF4,6843
|
|
6
|
+
none_shall_parse/strings.py,sha256=Eqrl8Sb-wOzjTu1_bbO-ALljlDKVa-1LcpcACqOmZuE,7931
|
|
7
|
+
none_shall_parse/types.py,sha256=PsljcR1UyyZZizm50wgyZPRNaeYvInSQoPS3i0RLgKo,1123
|
|
8
|
+
none_shall_parse-0.4.0.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
9
|
+
none_shall_parse-0.4.0.dist-info/METADATA,sha256=x4drnmVl1J-P9Mp8NQ3R-8EKC5VTJgZ0jZ8n9IcvzK8,1701
|
|
10
|
+
none_shall_parse-0.4.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
none_shall_parse/__init__.py,sha256=ElNUb98vffLm3_IvHJWLklIPWvr0p84SdpVLHof2n1o,548
|
|
2
|
-
none_shall_parse/imeis.py,sha256=sNLGeeGU0K4SvTb4xp-yuV0P3c-JjYDanble9f09uBc,6911
|
|
3
|
-
none_shall_parse/lists.py,sha256=3s9Oi0-aUVcfzhIdW6N9-z7ZI56VxTa3WRDj1zAiJQI,1705
|
|
4
|
-
none_shall_parse/parse.py,sha256=U4FUuQqtqgEKEC-3blUd78EFYyQHbBgbWW194VXdcYw,6905
|
|
5
|
-
none_shall_parse/strings.py,sha256=HAcaDOHkrUZ_pDfNjfh89GQ5EJeBo6WjH5of1B11vos,7950
|
|
6
|
-
none_shall_parse/types.py,sha256=SGHhzzIxC9_89STRa9eAQt18_cZVuklpj2cUnyrW8l0,1121
|
|
7
|
-
none_shall_parse-0.3.0.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
8
|
-
none_shall_parse-0.3.0.dist-info/METADATA,sha256=d-W1OJJiftIMIVXi3EzRYfgAWLu_74FYrchoBHmROOs,1676
|
|
9
|
-
none_shall_parse-0.3.0.dist-info/RECORD,,
|
|
File without changes
|