stdl 0.6.7__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.
- stdl/__init__.py +2 -0
- stdl/color.py +1505 -0
- stdl/decorators.py +96 -0
- stdl/dt.py +355 -0
- stdl/fs.py +1711 -0
- stdl/import_lazy.py +123 -0
- stdl/log.py +142 -0
- stdl/lst.py +166 -0
- stdl/net.py +80 -0
- stdl/st.py +422 -0
- stdl-0.6.7.dist-info/METADATA +141 -0
- stdl-0.6.7.dist-info/RECORD +14 -0
- stdl-0.6.7.dist-info/WHEEL +4 -0
- stdl-0.6.7.dist-info/licenses/LICENSE +21 -0
stdl/decorators.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Any, ParamSpec, TypeVar
|
|
8
|
+
|
|
9
|
+
P = ParamSpec("P")
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def timer(
|
|
14
|
+
r: int | None = 2,
|
|
15
|
+
sink: Callable[[str], None] = sys.stdout.write,
|
|
16
|
+
show_args: bool = False,
|
|
17
|
+
serialize: bool = False,
|
|
18
|
+
sep: str = " | ",
|
|
19
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
20
|
+
"""
|
|
21
|
+
A decorator that displays the time taken for a function to execute.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
r (int, optional): Number of decimal places to round the time to. Defaults to 2.
|
|
25
|
+
sink (Callable, optional): A function that takes a string and does something with it. Defaults to print.
|
|
26
|
+
show_args (bool, optional): Whether to show the arguments passed to the function. Defaults to False.
|
|
27
|
+
serialize (bool, optional): Whether to serialize the output as JSON. Defaults to False.
|
|
28
|
+
sep (str, optional): Separator between the function/method name and the arguments. Defaults to " | ".
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
32
|
+
@wraps(func)
|
|
33
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
34
|
+
time_start = time.perf_counter()
|
|
35
|
+
result = func(*args, **kwargs)
|
|
36
|
+
|
|
37
|
+
time_end = time.perf_counter()
|
|
38
|
+
time_elapsed = time_end - time_start
|
|
39
|
+
if r is not None:
|
|
40
|
+
time_elapsed = round(time_elapsed, r)
|
|
41
|
+
|
|
42
|
+
is_method = "self" in list(inspect.signature(func).parameters.keys())
|
|
43
|
+
func_args = args[1:] if is_method else args
|
|
44
|
+
name = func.__qualname__ if is_method else func.__name__
|
|
45
|
+
message = f"'{name}' took {time_elapsed}s"
|
|
46
|
+
|
|
47
|
+
if show_args:
|
|
48
|
+
kwargs_info = ", ".join(f"{k}={v}" for k, v in kwargs.items())
|
|
49
|
+
all_args = ", ".join(map(str, func_args)) + (
|
|
50
|
+
", " + kwargs_info if kwargs_info else ""
|
|
51
|
+
)
|
|
52
|
+
message += f"{sep}Args: {all_args}"
|
|
53
|
+
|
|
54
|
+
if serialize:
|
|
55
|
+
message_dict: dict[str, Any] = {
|
|
56
|
+
"name": name,
|
|
57
|
+
"time": time_elapsed,
|
|
58
|
+
"args": func_args,
|
|
59
|
+
"kwargs": kwargs,
|
|
60
|
+
}
|
|
61
|
+
if show_args:
|
|
62
|
+
message_dict["args"] = func_args
|
|
63
|
+
message_dict["kwargs"] = kwargs
|
|
64
|
+
message = json.dumps(message_dict)
|
|
65
|
+
|
|
66
|
+
sink(message)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
return wrapper
|
|
70
|
+
|
|
71
|
+
return decorator
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def retry(attempts: int, delay: float = 0) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
75
|
+
if attempts < 1:
|
|
76
|
+
raise ValueError("Attempts must be greater than 0")
|
|
77
|
+
|
|
78
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
79
|
+
@wraps(func)
|
|
80
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
81
|
+
for attempt in range(attempts):
|
|
82
|
+
try:
|
|
83
|
+
return func(*args, **kwargs)
|
|
84
|
+
except Exception:
|
|
85
|
+
if attempt < attempts - 1:
|
|
86
|
+
time.sleep(delay)
|
|
87
|
+
else:
|
|
88
|
+
raise
|
|
89
|
+
return func(*args, **kwargs)
|
|
90
|
+
|
|
91
|
+
return wrapper
|
|
92
|
+
|
|
93
|
+
return decorator
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = ["timer", "retry"]
|
stdl/dt.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import time
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import date, datetime, timedelta, timezone, tzinfo
|
|
6
|
+
|
|
7
|
+
from dateutil.parser import parse as parse_datetime_str
|
|
8
|
+
|
|
9
|
+
local_timezone: tzinfo | None = datetime.now().astimezone().tzinfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TimerStop:
|
|
14
|
+
total: timedelta
|
|
15
|
+
since_last: timedelta
|
|
16
|
+
at: float
|
|
17
|
+
label: str | None = None
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
if self.label is None:
|
|
21
|
+
return f"total={self.total}, since_last=({self.since_last}), at={datetime_fmt(self.at)}"
|
|
22
|
+
return f"{self.label} | total={self.total}, since_last={self.since_last}, at={datetime_fmt(self.at)}"
|
|
23
|
+
|
|
24
|
+
def total_seconds(self, *, r: int | None = None) -> float:
|
|
25
|
+
seconds = self.total.total_seconds()
|
|
26
|
+
if r is not None:
|
|
27
|
+
return round(seconds, r)
|
|
28
|
+
return seconds
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Timer:
|
|
32
|
+
"""A simple timer class that keeps track of all the stops."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, *, ms: bool = True) -> None:
|
|
35
|
+
self.ms = ms
|
|
36
|
+
self.reset()
|
|
37
|
+
|
|
38
|
+
def stop(self, label: str | None = None) -> TimerStop:
|
|
39
|
+
"""
|
|
40
|
+
Stop the timer.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
label (str | None, optional): Label for the stop.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
TimerStop: The stop data
|
|
47
|
+
"""
|
|
48
|
+
t = time.perf_counter()
|
|
49
|
+
elapsed_total = t - self.start
|
|
50
|
+
since_last = t - self.stops[-1].at if self.stops else 0
|
|
51
|
+
if not self.ms:
|
|
52
|
+
elapsed_total = round(elapsed_total)
|
|
53
|
+
since_last = round(since_last)
|
|
54
|
+
|
|
55
|
+
elapsed_total = timedelta(seconds=elapsed_total)
|
|
56
|
+
since_last = timedelta(seconds=since_last)
|
|
57
|
+
|
|
58
|
+
timer_stop = TimerStop(
|
|
59
|
+
total=elapsed_total,
|
|
60
|
+
since_last=since_last,
|
|
61
|
+
at=t,
|
|
62
|
+
label=label,
|
|
63
|
+
)
|
|
64
|
+
self.stops.append(timer_stop)
|
|
65
|
+
return timer_stop
|
|
66
|
+
|
|
67
|
+
def taken_seconds(self, r: int | None = None) -> float:
|
|
68
|
+
"""Returns the total time taken by the timer in seconds."""
|
|
69
|
+
return self.stop().total_seconds(r=r)
|
|
70
|
+
|
|
71
|
+
def taken(self) -> timedelta:
|
|
72
|
+
"""Returns the total time taken by the timer as timedelta."""
|
|
73
|
+
return self.stop().total
|
|
74
|
+
|
|
75
|
+
def reset(self) -> None:
|
|
76
|
+
"""Reset the timer."""
|
|
77
|
+
self.start = time.perf_counter()
|
|
78
|
+
self.stops: list[TimerStop] = [
|
|
79
|
+
TimerStop(
|
|
80
|
+
total=timedelta(seconds=0),
|
|
81
|
+
since_last=timedelta(seconds=0),
|
|
82
|
+
at=self.start,
|
|
83
|
+
label="start",
|
|
84
|
+
)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def datetime_fmt(
|
|
89
|
+
d: str | float | int | None | datetime = None,
|
|
90
|
+
fmt: str = "Ymd",
|
|
91
|
+
dsep: str = "-",
|
|
92
|
+
tsep: str = ":",
|
|
93
|
+
*,
|
|
94
|
+
ms: bool = False,
|
|
95
|
+
utc: bool = True,
|
|
96
|
+
) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Format date and time
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
d (str | float | None | datetime, optional): Input datetime. Defaults to current date and time.
|
|
102
|
+
fmt (str, optional): Date format.
|
|
103
|
+
dsep (str, optional): Date values separator.
|
|
104
|
+
tsep (str, optional): Time values separator.
|
|
105
|
+
ms (bool, optional): include milliseconds.
|
|
106
|
+
utc (bool, optional): Use the UTC timezone when building a datetime object from a timestamp.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
TypeError: Raises TypeError if the type of ``d`` is not one of the following: float, str, None, datetime, int
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
str: Formatted date and time.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
```python
|
|
116
|
+
>>> fmt_datetime()
|
|
117
|
+
'2022-11-22 22:40:04' # current date and time
|
|
118
|
+
>>> fmt_datetime(0)
|
|
119
|
+
"1970-01-01 00:00:00"
|
|
120
|
+
>>> fmt_datetime(0, fmt="dmY", dsep="/")
|
|
121
|
+
"01/01/1970 00:00:00"
|
|
122
|
+
```
|
|
123
|
+
"""
|
|
124
|
+
if d is None:
|
|
125
|
+
d = datetime.fromtimestamp(time.time(), **{"tz": timezone.utc} if utc else {})
|
|
126
|
+
|
|
127
|
+
elif isinstance(d, str):
|
|
128
|
+
d = parse_datetime_str(d)
|
|
129
|
+
elif isinstance(d, (float, int)):
|
|
130
|
+
d = datetime.fromtimestamp(d, **{"tz": timezone.utc} if utc else {})
|
|
131
|
+
elif isinstance(d, datetime):
|
|
132
|
+
pass
|
|
133
|
+
else:
|
|
134
|
+
raise TypeError(type(d))
|
|
135
|
+
date_fmt = f"%{fmt[0]}{dsep}%{fmt[1]}{dsep}%{fmt[2]}"
|
|
136
|
+
time_fmt = f"%H{tsep}%M{tsep}%S"
|
|
137
|
+
if ms:
|
|
138
|
+
time_fmt += ".%f"
|
|
139
|
+
dt_str = d.strftime(f"{date_fmt} {time_fmt}")
|
|
140
|
+
return dt_str[:-3] if ms else dt_str
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def time_fmt(
|
|
144
|
+
t: float | datetime | int | None = None, sep: str = ":", *, ms: bool = False, utc: bool = True
|
|
145
|
+
) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Format time
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
t (float | datetime | int | None, optional): Input time. Defaults to current time.
|
|
151
|
+
sep (str, optional): Character(s) that separates hours, minutes, seconds...
|
|
152
|
+
ms (bool, optional): include milliseconds.
|
|
153
|
+
utc (bool, optional): Use the UTC timezone when building a time object from a timestamp.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
TypeError: Raises TypeError if the type of ``t`` is not one of the following: float, None, datetime, int
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
str: Formatted time.
|
|
160
|
+
"""
|
|
161
|
+
if t is None:
|
|
162
|
+
tm = datetime.fromtimestamp(time.time(), **{"tz": timezone.utc} if utc else {})
|
|
163
|
+
elif isinstance(t, datetime):
|
|
164
|
+
tm = t.time()
|
|
165
|
+
elif isinstance(t, (int, float)):
|
|
166
|
+
tm = datetime.fromtimestamp(t, **{"tz": timezone.utc} if utc else {})
|
|
167
|
+
else:
|
|
168
|
+
raise TypeError(type(t))
|
|
169
|
+
ts = tm.strftime(f"%H{sep}%M{sep}%S")
|
|
170
|
+
if ms:
|
|
171
|
+
ms_str = f"{int(tm.microsecond / 1000)}".zfill(3)
|
|
172
|
+
ts = f"{ts}.{ms_str}"
|
|
173
|
+
return ts
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def date_fmt(
|
|
177
|
+
d: float | date | datetime | int | None = None,
|
|
178
|
+
fmt: str = "Ymd",
|
|
179
|
+
sep: str = "-",
|
|
180
|
+
) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Format date
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
d (float | date | datetime | int | None ): Input date. Defaults to current date.
|
|
186
|
+
fmt (str, optional): Date format.
|
|
187
|
+
sep (str, optional): Character(s) that separates days, months and years.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
TypeError: Raises TypeError if the type of ``d`` is not one of the following: float, None, date, datetime, int
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
str: Formatted date.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
```python
|
|
197
|
+
>>> fmt_date(date(2022,11,22))
|
|
198
|
+
'2022-11-22'
|
|
199
|
+
>>> fmt_date(date(2022,11,22), sep="/")
|
|
200
|
+
'2022/11/22'
|
|
201
|
+
>>> fmt_date(date(2022,11,22), sep="/", fmt="dmY")
|
|
202
|
+
'22/11/2022'
|
|
203
|
+
>>> fmt_date()
|
|
204
|
+
'2022-11-22'
|
|
205
|
+
```
|
|
206
|
+
"""
|
|
207
|
+
if d is None:
|
|
208
|
+
d = date.fromtimestamp(time.time())
|
|
209
|
+
elif isinstance(d, (float, int)):
|
|
210
|
+
d = date.fromtimestamp(d)
|
|
211
|
+
elif isinstance(d, datetime):
|
|
212
|
+
d = d.date()
|
|
213
|
+
elif isinstance(d, date):
|
|
214
|
+
pass
|
|
215
|
+
else:
|
|
216
|
+
raise TypeError(type(d))
|
|
217
|
+
return d.strftime(f"%{fmt[0]}{sep}%{fmt[1]}{sep}%{fmt[2]}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def date_range(start: date, end: date) -> Generator[date, None, None]:
|
|
221
|
+
"""
|
|
222
|
+
Returns a generator for dates between ``start`` and ``end``
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
```python
|
|
226
|
+
>>> for i in date_range(date(2022,11,19), date(2022,11,22)): print(i)
|
|
227
|
+
2022-11-19
|
|
228
|
+
2022-11-20
|
|
229
|
+
2022-11-21
|
|
230
|
+
```
|
|
231
|
+
"""
|
|
232
|
+
for n in range(int((end - start).days)):
|
|
233
|
+
yield start + timedelta(n)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def sleep(lo: float, hi: float | None = None) -> float:
|
|
237
|
+
"""
|
|
238
|
+
Sleeps for a random duration within a specified range.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
lo (float): The lower bound of the sleep duration range (inclusive).
|
|
242
|
+
hi (float, optional): The upper bound of the sleep duration range (inclusive). If not provided, the function will sleep for the exact duration specified by `lo`.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
float: The actual sleep duration.
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
ValueError: If `hi` is provided and `lo` is greater than `hi`.
|
|
249
|
+
"""
|
|
250
|
+
if hi is None:
|
|
251
|
+
time.sleep(lo)
|
|
252
|
+
return lo
|
|
253
|
+
if lo < hi:
|
|
254
|
+
raise ValueError(f"Minimum sleep time is higher that maximum. {(lo, hi)}")
|
|
255
|
+
t = random.uniform(lo, hi)
|
|
256
|
+
time.sleep(t)
|
|
257
|
+
return t
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def seconds_to_hms(time: int | float, ms: bool = False) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Converts time in seconds to a string in the format HH:MM:SS[.mmm]
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
>>> seconds_to_hms(123.456)
|
|
266
|
+
'00:02:03'
|
|
267
|
+
>>> seconds_to_hms(123.456, ms=True)
|
|
268
|
+
'00:02:03.456'
|
|
269
|
+
>>> seconds_to_hms(-123.456, ms=True)
|
|
270
|
+
'-00:02:03.456'
|
|
271
|
+
"""
|
|
272
|
+
sign = "-" if time < 0 else ""
|
|
273
|
+
time = abs(time)
|
|
274
|
+
|
|
275
|
+
s = int(time % 60)
|
|
276
|
+
m = int((time // 60) % 60)
|
|
277
|
+
h = int(time // 3600)
|
|
278
|
+
time_str = f"{sign}{h:02d}:{m:02d}:{s:02d}"
|
|
279
|
+
|
|
280
|
+
if ms:
|
|
281
|
+
milis = int(round((time - int(time)) * 1000))
|
|
282
|
+
time_str += f".{milis:03d}"
|
|
283
|
+
return time_str
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def hms_to_seconds(time: str, ms: bool = False) -> float | None:
|
|
287
|
+
"""
|
|
288
|
+
Converts a string in the format HH:MM:SS[.mmm] or h:m:s or m:s to time in seconds.
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
>>> hms_to_seconds('00:02:03')
|
|
292
|
+
123.0
|
|
293
|
+
>>> hms_to_seconds('00:02:03.456', ms=True)
|
|
294
|
+
123.456
|
|
295
|
+
"""
|
|
296
|
+
if time is None:
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
negative = False
|
|
300
|
+
time = time.strip()
|
|
301
|
+
if time.startswith("-"):
|
|
302
|
+
negative = True
|
|
303
|
+
time = time[1:]
|
|
304
|
+
|
|
305
|
+
parts = time.split(":")
|
|
306
|
+
if len(parts) == 1:
|
|
307
|
+
return float(parts[0])
|
|
308
|
+
time_milis = 0.0
|
|
309
|
+
|
|
310
|
+
if ms:
|
|
311
|
+
if "." in parts[-1]:
|
|
312
|
+
dot_split = parts[-1].split(".", 1)
|
|
313
|
+
parts[-1], millis_str = dot_split
|
|
314
|
+
if not millis_str.isdigit():
|
|
315
|
+
raise ValueError(f"Invalid milliseconds in time: '{time}'")
|
|
316
|
+
millis = int(millis_str.ljust(3, "0"))
|
|
317
|
+
time_milis = millis / 1000
|
|
318
|
+
|
|
319
|
+
if len(parts) not in (2, 3):
|
|
320
|
+
raise ValueError("Invalid number of time segments.")
|
|
321
|
+
|
|
322
|
+
if len(parts) == 2:
|
|
323
|
+
minute_part, second_part = parts
|
|
324
|
+
m = int(minute_part)
|
|
325
|
+
s = int(second_part)
|
|
326
|
+
if s >= 60:
|
|
327
|
+
raise ValueError(f"Seconds ({s}) must be < 60.")
|
|
328
|
+
total = m * 60 + s
|
|
329
|
+
else:
|
|
330
|
+
hour_part, minute_part, second_part = parts
|
|
331
|
+
h = int(hour_part)
|
|
332
|
+
m = int(minute_part)
|
|
333
|
+
s = int(second_part)
|
|
334
|
+
if m >= 60 or s >= 60:
|
|
335
|
+
raise ValueError(f"Minutes ({m}) or seconds ({s}) must be < 60.")
|
|
336
|
+
total = h * 3600 + m * 60 + s
|
|
337
|
+
|
|
338
|
+
result = total + time_milis
|
|
339
|
+
return -result if negative else result
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
__all__ = [
|
|
343
|
+
"Timer",
|
|
344
|
+
"parse_datetime_str",
|
|
345
|
+
"datetime_fmt",
|
|
346
|
+
"time_fmt",
|
|
347
|
+
"date_fmt",
|
|
348
|
+
"date_range",
|
|
349
|
+
"sleep",
|
|
350
|
+
"local_timezone",
|
|
351
|
+
"timezone",
|
|
352
|
+
"date",
|
|
353
|
+
"datetime",
|
|
354
|
+
"time",
|
|
355
|
+
]
|