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/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
+ ]