transcrypto 1.7.0__py3-none-any.whl → 2.0.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.
- transcrypto/__init__.py +1 -1
- transcrypto/cli/__init__.py +3 -0
- transcrypto/cli/aeshash.py +370 -0
- transcrypto/cli/bidsecret.py +336 -0
- transcrypto/cli/clibase.py +183 -0
- transcrypto/cli/intmath.py +429 -0
- transcrypto/cli/publicalgos.py +878 -0
- transcrypto/core/__init__.py +3 -0
- transcrypto/{aes.py → core/aes.py} +17 -29
- transcrypto/core/bid.py +161 -0
- transcrypto/{dsa.py → core/dsa.py} +28 -27
- transcrypto/{elgamal.py → core/elgamal.py} +33 -32
- transcrypto/core/hashes.py +96 -0
- transcrypto/core/key.py +735 -0
- transcrypto/{modmath.py → core/modmath.py} +91 -17
- transcrypto/{rsa.py → core/rsa.py} +51 -50
- transcrypto/{sss.py → core/sss.py} +27 -26
- transcrypto/profiler.py +29 -13
- transcrypto/transcrypto.py +60 -1996
- transcrypto/utils/__init__.py +3 -0
- transcrypto/utils/base.py +72 -0
- transcrypto/utils/human.py +278 -0
- transcrypto/utils/logging.py +139 -0
- transcrypto/utils/saferandom.py +102 -0
- transcrypto/utils/stats.py +360 -0
- transcrypto/utils/timer.py +175 -0
- {transcrypto-1.7.0.dist-info → transcrypto-2.0.0.dist-info}/METADATA +111 -109
- transcrypto-2.0.0.dist-info/RECORD +33 -0
- transcrypto/base.py +0 -1918
- transcrypto-1.7.0.dist-info/RECORD +0 -17
- /transcrypto/{constants.py → core/constants.py} +0 -0
- {transcrypto-1.7.0.dist-info → transcrypto-2.0.0.dist-info}/WHEEL +0 -0
- {transcrypto-1.7.0.dist-info → transcrypto-2.0.0.dist-info}/entry_points.txt +0 -0
- {transcrypto-1.7.0.dist-info → transcrypto-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Balparda's TransCrypto basic statistics library."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
from collections import abc
|
|
9
|
+
|
|
10
|
+
from transcrypto.utils import base
|
|
11
|
+
|
|
12
|
+
# Lanczos coefficients for g=7, n=9; provides ~15 digit accuracy for gamma
|
|
13
|
+
_LANCZOS_G = 7
|
|
14
|
+
_LANCZOS_COEFF: tuple[float, ...] = (
|
|
15
|
+
0.99999999999980993,
|
|
16
|
+
676.5203681218851,
|
|
17
|
+
-1259.1392167224028,
|
|
18
|
+
771.32342877765313,
|
|
19
|
+
-176.61502916214059,
|
|
20
|
+
12.507343278686905,
|
|
21
|
+
-0.13857109526572012,
|
|
22
|
+
9.9843695780195716e-6,
|
|
23
|
+
1.5056327351493116e-7,
|
|
24
|
+
)
|
|
25
|
+
_TINY: float = 1e-30
|
|
26
|
+
_NSG: abc.Callable[[float], float] = (
|
|
27
|
+
lambda z: _TINY if abs(z) < _TINY else z # numerical stability guard
|
|
28
|
+
)
|
|
29
|
+
_BETA_INCOMPLETE_MAX_ITER: int = 200
|
|
30
|
+
_BETA_INCOMPLETE_TOL: float = 1e-14
|
|
31
|
+
_STUDENT_SMALL: float = 1e-12
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def GammaLanczos(z: float, /) -> float:
|
|
35
|
+
"""Compute the gamma function Γ(z) using the Lanczos approximation.
|
|
36
|
+
|
|
37
|
+
The Lanczos approximation provides an efficient method to compute
|
|
38
|
+
the gamma function with high accuracy (~15 digits). It uses the
|
|
39
|
+
reflection formula for z < 0.5.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
z (float): Input value. For z ≤ 0 where z is a non-positive integer,
|
|
43
|
+
the function will return ±inf.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
float: Γ(z), the gamma function evaluated at z.
|
|
47
|
+
|
|
48
|
+
Notes:
|
|
49
|
+
- Uses coefficients optimized for g=7, n=9.
|
|
50
|
+
- For z < 0.5, uses the reflection formula:
|
|
51
|
+
Γ(z) = π / (sin(πz) · Γ(1-z))
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
if z < 0.5: # noqa: PLR2004
|
|
55
|
+
# Reflection formula: Γ(z) = π / (sin(πz) Γ(1-z))
|
|
56
|
+
return math.pi / (math.sin(math.pi * z) * GammaLanczos(1.0 - z))
|
|
57
|
+
z -= 1.0
|
|
58
|
+
x: float = _LANCZOS_COEFF[0]
|
|
59
|
+
for i in range(1, len(_LANCZOS_COEFF)):
|
|
60
|
+
x += _LANCZOS_COEFF[i] / (z + i)
|
|
61
|
+
t: float = z + _LANCZOS_G + 0.5
|
|
62
|
+
tz: float = t ** (z + 0.5)
|
|
63
|
+
return math.sqrt(2.0 * math.pi) * tz * math.exp(-t) * x
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def BetaIncompleteCF(a: float, b: float, x: float, /) -> float:
|
|
67
|
+
"""Compute continued fraction for the regularized incomplete beta function.
|
|
68
|
+
|
|
69
|
+
Uses the modified Lentz algorithm to evaluate the continued fraction
|
|
70
|
+
expansion of I_x(a, b) efficiently and stably.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
a (float): First shape parameter (> 0).
|
|
74
|
+
b (float): Second shape parameter (> 0).
|
|
75
|
+
x (float): Point at which to evaluate (0 ≤ x ≤ 1).
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
float: The continued fraction value.
|
|
79
|
+
|
|
80
|
+
Notes:
|
|
81
|
+
- Internal helper for `_BetaIncomplete`.
|
|
82
|
+
- Convergence is typically achieved in < 100 iterations for typical inputs.
|
|
83
|
+
- Uses a floor of 1e-30 to prevent division by zero.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
qab: float = a + b
|
|
87
|
+
qap: float = a + 1.0
|
|
88
|
+
qam: float = a - 1.0
|
|
89
|
+
c: float = 1.0
|
|
90
|
+
d: float = 1.0 / _NSG(1.0 - qab * x / qap)
|
|
91
|
+
h: float = d
|
|
92
|
+
aa: float
|
|
93
|
+
delta: float
|
|
94
|
+
m2: int
|
|
95
|
+
for m in range(1, _BETA_INCOMPLETE_MAX_ITER + 1):
|
|
96
|
+
m2 = 2 * m
|
|
97
|
+
# even step
|
|
98
|
+
aa = m * (b - m) * x / ((qam + m2) * (a + m2))
|
|
99
|
+
c, d = _NSG(1.0 + aa / c), 1.0 / _NSG(1.0 + aa * d)
|
|
100
|
+
h *= d * c
|
|
101
|
+
# odd step
|
|
102
|
+
aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
|
|
103
|
+
c, d = _NSG(1.0 + aa / c), 1.0 / _NSG(1.0 + aa * d)
|
|
104
|
+
delta = d * c
|
|
105
|
+
h *= delta
|
|
106
|
+
if abs(delta - 1.0) < _BETA_INCOMPLETE_TOL:
|
|
107
|
+
break
|
|
108
|
+
return h
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def BetaIncomplete(a: float, b: float, x: float, /) -> float:
|
|
112
|
+
"""Compute the regularized incomplete beta function I_x(a, b).
|
|
113
|
+
|
|
114
|
+
The regularized incomplete beta function is defined as:
|
|
115
|
+
I_x(a, b) = B(x; a, b) / B(a, b)
|
|
116
|
+
where B(x; a, b) is the incomplete beta function and B(a, b) is the
|
|
117
|
+
complete beta function.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
a (float): First shape parameter (> 0).
|
|
121
|
+
b (float): Second shape parameter (> 0).
|
|
122
|
+
x (float): Upper limit of integration (0 ≤ x ≤ 1).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
float: I_x(a, b), the regularized incomplete beta at x.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
base.InputError: If x is outside [0, 1].
|
|
129
|
+
|
|
130
|
+
Notes:
|
|
131
|
+
- Uses continued fraction expansion with Lentz algorithm.
|
|
132
|
+
- For numerical stability, uses the symmetry relation when
|
|
133
|
+
x > (a + 1) / (a + b + 2).
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
if x < 0.0 or x > 1.0:
|
|
137
|
+
raise base.InputError(f'x must be in [0, 1], got {x}')
|
|
138
|
+
if x == 0.0:
|
|
139
|
+
return 0.0
|
|
140
|
+
if x == 1.0:
|
|
141
|
+
return 1.0
|
|
142
|
+
log_beta: float = math.lgamma(a) + math.lgamma(b) - math.lgamma(a + b)
|
|
143
|
+
front: float = math.exp(math.log(x) * a + math.log(1.0 - x) * b - log_beta) / a
|
|
144
|
+
if x < (a + 1.0) / (a + b + 2.0):
|
|
145
|
+
return front * BetaIncompleteCF(a, b, x)
|
|
146
|
+
return 1.0 - front * BetaIncompleteCF(b, a, 1.0 - x) * a / b
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def StudentTCDF(t_val: float, df: float, /) -> float:
|
|
150
|
+
"""Compute the cumulative distribution function (CDF) of Student's t-distribution.
|
|
151
|
+
|
|
152
|
+
The CDF gives the probability P(T ≤ t) where T follows a t-distribution
|
|
153
|
+
with `df` degrees of freedom.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
t_val (float): The t-statistic value.
|
|
157
|
+
df (float): Degrees of freedom (> 0).
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
float: Probability P(T ≤ t_val), in range [0, 1].
|
|
161
|
+
|
|
162
|
+
Notes:
|
|
163
|
+
- Uses the relationship between the t-distribution CDF and the
|
|
164
|
+
regularized incomplete beta function.
|
|
165
|
+
- For t ≥ 0: CDF = 0.5 + 0.5 * (1 - I_x(df/2, 0.5))
|
|
166
|
+
- For t < 0: CDF = 0.5 * I_x(df/2, 0.5)
|
|
167
|
+
- where x = df / (df + t²)
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
x: float = df / (df + t_val * t_val)
|
|
171
|
+
prob: float = 0.5 * BetaIncomplete(df / 2.0, 0.5, x)
|
|
172
|
+
return 0.5 + (0.5 - prob) if t_val >= 0 else prob
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def StudentTPPF(q: float, df: float, /) -> float:
|
|
176
|
+
"""Compute the percent point function (inverse CDF) of Student's t-distribution.
|
|
177
|
+
|
|
178
|
+
Given a probability q, find the value t such that P(T ≤ t) = q,
|
|
179
|
+
where T follows a t-distribution with `df` degrees of freedom.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
q (float): Probability (0 < q < 1).
|
|
183
|
+
df (float): Degrees of freedom (> 0).
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
float: The t-value such that CDF(t) = q.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
base.InputError: If q is not in (0, 1).
|
|
190
|
+
|
|
191
|
+
Notes:
|
|
192
|
+
- Uses Newton-Raphson iteration with an initial guess from
|
|
193
|
+
the normal distribution approximation.
|
|
194
|
+
- Converges to ~12 decimal places in typical cases.
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
if not 0.0 < q < 1.0:
|
|
198
|
+
raise base.InputError(f'q must be in (0, 1), got {q}')
|
|
199
|
+
# Special case: q=0.5 is exactly 0 by symmetry
|
|
200
|
+
if q == 0.5: # noqa: PLR2004
|
|
201
|
+
return 0.0
|
|
202
|
+
# Initial guess using inverse normal approximation (Abramowitz & Stegun 26.2.23)
|
|
203
|
+
if q < 0.5: # noqa: PLR2004
|
|
204
|
+
sign: float = -1.0
|
|
205
|
+
p: float = q
|
|
206
|
+
else:
|
|
207
|
+
sign = 1.0
|
|
208
|
+
p = 1.0 - q
|
|
209
|
+
# Protect against log(0) when p is very close to 0
|
|
210
|
+
p = max(p, 1e-300)
|
|
211
|
+
t_approx: float = math.sqrt(-2.0 * math.log(p))
|
|
212
|
+
c0 = 2.515517
|
|
213
|
+
c1 = 0.802853
|
|
214
|
+
c2 = 0.010328
|
|
215
|
+
d1 = 1.432788
|
|
216
|
+
d2 = 0.189269
|
|
217
|
+
d3 = 0.001308
|
|
218
|
+
x0: float = sign * (
|
|
219
|
+
t_approx
|
|
220
|
+
- (c0 + c1 * t_approx + c2 * t_approx**2)
|
|
221
|
+
/ (1 + d1 * t_approx + d2 * t_approx**2 + d3 * t_approx**3)
|
|
222
|
+
)
|
|
223
|
+
# Newton-Raphson refinement
|
|
224
|
+
for _ in range(50):
|
|
225
|
+
cdf_val: float = StudentTCDF(x0, df)
|
|
226
|
+
# PDF of Student's t-distribution (computed in log-space to avoid overflow for large df)
|
|
227
|
+
log_pdf: float = (
|
|
228
|
+
math.lgamma((df + 1) / 2)
|
|
229
|
+
- 0.5 * math.log(df * math.pi)
|
|
230
|
+
- math.lgamma(df / 2)
|
|
231
|
+
- ((df + 1) / 2) * math.log(1 + x0**2 / df)
|
|
232
|
+
)
|
|
233
|
+
pdf_val: float = _NSG(math.exp(log_pdf))
|
|
234
|
+
x1: float = x0 - (cdf_val - q) / pdf_val
|
|
235
|
+
if abs(x1 - x0) < _STUDENT_SMALL:
|
|
236
|
+
return x1
|
|
237
|
+
x0 = x1
|
|
238
|
+
return x0 # pragma: no cover - Newton-Raphson always converges for t-distribution
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def SampleVariance(data: list[int | float], mean: float, /) -> float:
|
|
242
|
+
"""Compute sample variance with Bessel's correction (n-1 denominator).
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
data (list[int | float]): Sequence of numeric measurements, with len(data) >= 2.
|
|
246
|
+
mean (float): Pre-computed mean of the data.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
float: Sample variance s² = Σ(xᵢ - x̄)² / (n - 1).
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
base.InputError: If len(data) < 2.
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
if (data_sz := len(data)) < 2: # noqa: PLR2004
|
|
256
|
+
raise base.InputError(f'sample variance requires at least 2 data points, got {data_sz}')
|
|
257
|
+
return sum((x - mean) ** 2 for x in data) / float(data_sz - 1)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def StandardErrorOfMean(data: list[int | float], /) -> tuple[float, float]:
|
|
261
|
+
"""Compute the mean and standard error of the mean (SEM).
|
|
262
|
+
|
|
263
|
+
The SEM is the standard deviation of the sampling distribution of the
|
|
264
|
+
sample mean, computed as s / √n where s is the sample standard deviation.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
data (list[int | float]): Sequence of numeric measurements (n >= 2).
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
tuple[float, float]: (mean, SEM) where:
|
|
271
|
+
- mean: arithmetic mean of the data
|
|
272
|
+
- SEM: standard error of the mean (σ / √n)
|
|
273
|
+
|
|
274
|
+
Notes:
|
|
275
|
+
- Assumes len(data) >= 2; returns (mean, inf) for single element handled by caller.
|
|
276
|
+
- Uses sample standard deviation (Bessel's correction).
|
|
277
|
+
|
|
278
|
+
""" # noqa: RUF002
|
|
279
|
+
n: int = len(data)
|
|
280
|
+
mean: float = sum(data) / n
|
|
281
|
+
variance: float = SampleVariance(data, mean)
|
|
282
|
+
return (mean, math.sqrt(variance / n))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def StudentTInterval(
|
|
286
|
+
confidence: float, df: int, loc: float, scale: float, /
|
|
287
|
+
) -> tuple[float, float]:
|
|
288
|
+
"""Compute a symmetric confidence interval using Student's t-distribution.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
confidence (float): Confidence level (e.g., 0.95 for 95% CI).
|
|
292
|
+
df (int): Degrees of freedom (n - 1 for a sample of size n).
|
|
293
|
+
loc (float): Center of the interval (typically the sample mean).
|
|
294
|
+
scale (float): Scale parameter (typically the SEM).
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
tuple[float, float]: (lower_bound, upper_bound) of the confidence interval.
|
|
298
|
+
|
|
299
|
+
Notes:
|
|
300
|
+
- The interval is symmetric around `loc`:
|
|
301
|
+
[loc - t_crit * scale, loc + t_crit * scale]
|
|
302
|
+
- where t_crit is the critical t-value for the given confidence and df.
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
alpha: float = 1.0 - confidence
|
|
306
|
+
t_crit: float = StudentTPPF(1.0 - alpha / 2.0, df)
|
|
307
|
+
margin: float = t_crit * scale
|
|
308
|
+
return (loc - margin, loc + margin)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def MeasurementStats(
|
|
312
|
+
data: list[int | float], /, *, confidence: float = 0.95
|
|
313
|
+
) -> tuple[int, float, float, float, tuple[float, float], float]:
|
|
314
|
+
"""Compute descriptive statistics for repeated measurements.
|
|
315
|
+
|
|
316
|
+
Given N ≥ 1 measurements, this function computes the sample mean, the
|
|
317
|
+
standard error of the mean (SEM), and the symmetric error estimate for
|
|
318
|
+
the chosen confidence interval using Student's t distribution.
|
|
319
|
+
|
|
320
|
+
Notes:
|
|
321
|
+
• If only one measurement is given, SEM and error are reported as +∞ and
|
|
322
|
+
the confidence interval is (-∞, +∞).
|
|
323
|
+
• This function assumes the underlying distribution is approximately
|
|
324
|
+
normal, or n is large enough for the Central Limit Theorem to apply.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
data (list[int | float]): Sequence of numeric measurements.
|
|
328
|
+
confidence (float, optional): Confidence level for the interval, 0.5 <= confidence < 1;
|
|
329
|
+
defaults to 0.95 (95% confidence interval).
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
tuple:
|
|
333
|
+
- n (int): number of measurements.
|
|
334
|
+
- mean (float): arithmetic mean of the data
|
|
335
|
+
- sem (float): standard error of the mean, sigma / √n
|
|
336
|
+
- error (float): half-width of the confidence interval (mean ± error)
|
|
337
|
+
- ci (tuple[float, float]): lower and upper confidence interval bounds
|
|
338
|
+
- confidence (float): the confidence level used
|
|
339
|
+
|
|
340
|
+
Raises:
|
|
341
|
+
base.InputError: if the input list is empty.
|
|
342
|
+
|
|
343
|
+
"""
|
|
344
|
+
# test inputs
|
|
345
|
+
n: int = len(data)
|
|
346
|
+
if not n:
|
|
347
|
+
raise base.InputError('no data')
|
|
348
|
+
if not 0.5 <= confidence < 1.0: # noqa: PLR2004
|
|
349
|
+
raise base.InputError(f'invalid confidence: {confidence=}')
|
|
350
|
+
# solve trivial case
|
|
351
|
+
if n == 1:
|
|
352
|
+
return (n, float(data[0]), math.inf, math.inf, (-math.inf, math.inf), confidence)
|
|
353
|
+
# compute statistics using local implementation (no scipy/numpy dependency)
|
|
354
|
+
mean: float
|
|
355
|
+
sem: float
|
|
356
|
+
mean, sem = StandardErrorOfMean(data)
|
|
357
|
+
ci: tuple[float, float] = StudentTInterval(confidence, n - 1, mean, sem)
|
|
358
|
+
t_crit: float = StudentTPPF((1.0 + confidence) / 2.0, n - 1)
|
|
359
|
+
error: float = t_crit * sem # half-width of the CI
|
|
360
|
+
return (n, mean, sem, error, ci, confidence)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Balparda's TransCrypto timer library."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
import functools
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from collections import abc
|
|
12
|
+
from types import TracebackType
|
|
13
|
+
from typing import Self
|
|
14
|
+
|
|
15
|
+
from transcrypto.utils import base, human
|
|
16
|
+
|
|
17
|
+
# Time utils
|
|
18
|
+
|
|
19
|
+
MIN_TM = int(datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=datetime.UTC).timestamp())
|
|
20
|
+
TIME_FORMAT = '%Y/%b/%d-%H:%M:%S-UTC'
|
|
21
|
+
TimeStr: abc.Callable[[int | float | None], str] = lambda tm: (
|
|
22
|
+
time.strftime(TIME_FORMAT, time.gmtime(tm)) if tm else '-'
|
|
23
|
+
)
|
|
24
|
+
Now: abc.Callable[[], int] = lambda: int(time.time())
|
|
25
|
+
StrNow: abc.Callable[[], str] = lambda: TimeStr(Now())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Timer:
|
|
29
|
+
"""An execution timing class that can be used as both a context manager and a decorator.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
# As a context manager
|
|
33
|
+
with Timer('Block timing'):
|
|
34
|
+
time.sleep(1.2)
|
|
35
|
+
|
|
36
|
+
# As a decorator
|
|
37
|
+
@Timer('Function timing')
|
|
38
|
+
def slow_function():
|
|
39
|
+
time.sleep(0.8)
|
|
40
|
+
|
|
41
|
+
# As a regular object
|
|
42
|
+
tm = Timer('Inline timing')
|
|
43
|
+
tm.Start()
|
|
44
|
+
time.sleep(0.1)
|
|
45
|
+
tm.Stop()
|
|
46
|
+
print(tm)
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
label (str, optional): Timer label
|
|
50
|
+
emit_log (bool, optional): If True (default) will logging.info() the timer, else will not
|
|
51
|
+
emit_print (bool, optional): If True will print() the timer, else (default) will not
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
label: str = '',
|
|
58
|
+
/,
|
|
59
|
+
*,
|
|
60
|
+
emit_log: bool = True,
|
|
61
|
+
emit_print: abc.Callable[[str], None] | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Initialize the Timer.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
label (str, optional): A description or name for the timed block or function
|
|
67
|
+
emit_log (bool, optional): Emit a log message when finished; default is True
|
|
68
|
+
emit_print (Callable[[str], None] | None, optional): Emit a print() message when
|
|
69
|
+
finished using the provided callable; default is None
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
self.emit_log: bool = emit_log
|
|
73
|
+
self.emit_print: abc.Callable[[str], None] | None = emit_print
|
|
74
|
+
self.label: str = label.strip()
|
|
75
|
+
self.start: float | None = None
|
|
76
|
+
self.end: float | None = None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def elapsed(self) -> float:
|
|
80
|
+
"""Elapsed time. Will be zero until a measurement is available with start/end.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
base.Error: negative elapsed time
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
float: elapsed time, in seconds
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
if self.start is None or self.end is None:
|
|
90
|
+
return 0.0
|
|
91
|
+
delta: float = self.end - self.start
|
|
92
|
+
if delta <= 0.0:
|
|
93
|
+
raise base.Error(f'negative/zero delta: {delta}')
|
|
94
|
+
return delta
|
|
95
|
+
|
|
96
|
+
def __str__(self) -> str:
|
|
97
|
+
"""Get current timer value.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: human-readable representation of current time value
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
if self.start is None:
|
|
104
|
+
return f'{self.label}: <UNSTARTED>' if self.label else '<UNSTARTED>'
|
|
105
|
+
if self.end is None:
|
|
106
|
+
return (
|
|
107
|
+
f'{self.label}: ' if self.label else ''
|
|
108
|
+
) + f'<PARTIAL> {human.HumanizedSeconds(time.perf_counter() - self.start)}'
|
|
109
|
+
return (f'{self.label}: ' if self.label else '') + f'{human.HumanizedSeconds(self.elapsed)}'
|
|
110
|
+
|
|
111
|
+
def Start(self) -> None:
|
|
112
|
+
"""Start the timer.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
base.Error: if you try to re-start the timer
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
if self.start is not None:
|
|
119
|
+
raise base.Error('Re-starting timer is forbidden')
|
|
120
|
+
self.start = time.perf_counter()
|
|
121
|
+
|
|
122
|
+
def __enter__(self) -> Self:
|
|
123
|
+
"""Start the timer when entering the context.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Timer: context object (self)
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
self.Start()
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def Stop(self) -> None:
|
|
133
|
+
"""Stop the timer and emit logging.info with timer message.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
base.Error: trying to re-start timer or stop unstarted timer
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
if self.start is None:
|
|
140
|
+
raise base.Error('Stopping an unstarted timer')
|
|
141
|
+
if self.end is not None:
|
|
142
|
+
raise base.Error('Re-stopping timer is forbidden')
|
|
143
|
+
self.end = time.perf_counter()
|
|
144
|
+
message: str = str(self)
|
|
145
|
+
if self.emit_log:
|
|
146
|
+
logging.info(message)
|
|
147
|
+
if self.emit_print is not None:
|
|
148
|
+
self.emit_print(message)
|
|
149
|
+
|
|
150
|
+
def __exit__(
|
|
151
|
+
self,
|
|
152
|
+
unused_exc_type: type[BaseException] | None,
|
|
153
|
+
unused_exc_val: BaseException | None,
|
|
154
|
+
exc_tb: TracebackType | None,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Stop the timer when exiting the context."""
|
|
157
|
+
self.Stop()
|
|
158
|
+
|
|
159
|
+
def __call__[**F, R](self, func: abc.Callable[F, R]) -> abc.Callable[F, R]:
|
|
160
|
+
"""Allow the Timer to be used as a decorator.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
func: The function to time.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The wrapped function with timing behavior.
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
@functools.wraps(func)
|
|
171
|
+
def _Wrapper(*args: F.args, **kwargs: F.kwargs) -> R:
|
|
172
|
+
with self.__class__(self.label, emit_log=self.emit_log, emit_print=self.emit_print):
|
|
173
|
+
return func(*args, **kwargs)
|
|
174
|
+
|
|
175
|
+
return _Wrapper
|