klaatu-python 2.2.0__py3-none-any.whl → 2.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.
- klaatu_python/__init__.py +0 -1
- klaatu_python/case.py +14 -6
- klaatu_python/lock.py +5 -2
- klaatu_python/utils.py +133 -9
- {klaatu_python-2.2.0.dist-info → klaatu_python-2.4.0.dist-info}/METADATA +6 -11
- klaatu_python-2.4.0.dist-info/RECORD +8 -0
- klaatu_python-2.4.0.dist-info/WHEEL +4 -0
- klaatu_python-2.2.0.dist-info/RECORD +0 -9
- klaatu_python-2.2.0.dist-info/WHEEL +0 -5
- klaatu_python-2.2.0.dist-info/top_level.txt +0 -1
- {klaatu_python-2.2.0.dist-info → klaatu_python-2.4.0.dist-info}/licenses/LICENSE +0 -0
klaatu_python/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.2.0"
|
klaatu_python/case.py
CHANGED
|
@@ -5,19 +5,19 @@ from abc import abstractmethod
|
|
|
5
5
|
class CaseType:
|
|
6
6
|
@staticmethod
|
|
7
7
|
@abstractmethod
|
|
8
|
-
def split(value: str) -> list[str]:
|
|
9
|
-
...
|
|
8
|
+
def split(value: str) -> list[str]: ...
|
|
10
9
|
|
|
11
10
|
@staticmethod
|
|
12
11
|
@abstractmethod
|
|
13
|
-
def join(parts: list[str]) -> str:
|
|
14
|
-
...
|
|
12
|
+
def join(parts: list[str]) -> str: ...
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
class CamelCase(CaseType):
|
|
16
|
+
"""camelCase"""
|
|
17
|
+
|
|
18
18
|
@staticmethod
|
|
19
19
|
def split(value: str) -> list[str]:
|
|
20
|
-
return [m.group() for m in re.finditer(r"(^[a-z]
|
|
20
|
+
return [m.group() for m in re.finditer(r"(^[a-z]+)|([A-Z][a-z]*)", value)]
|
|
21
21
|
|
|
22
22
|
@staticmethod
|
|
23
23
|
def join(parts: list[str]) -> str:
|
|
@@ -27,6 +27,8 @@ class CamelCase(CaseType):
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class ConstantCase(CaseType):
|
|
30
|
+
"""CONSTANT_CASE"""
|
|
31
|
+
|
|
30
32
|
@staticmethod
|
|
31
33
|
def split(value: str) -> list[str]:
|
|
32
34
|
return value.split("_")
|
|
@@ -37,6 +39,8 @@ class ConstantCase(CaseType):
|
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
class KebabCase(CaseType):
|
|
42
|
+
"""kebab-case"""
|
|
43
|
+
|
|
40
44
|
@staticmethod
|
|
41
45
|
def split(value: str) -> list[str]:
|
|
42
46
|
return value.split("-")
|
|
@@ -47,9 +51,11 @@ class KebabCase(CaseType):
|
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
class PascalCase(CaseType):
|
|
54
|
+
"""PascalCase"""
|
|
55
|
+
|
|
50
56
|
@staticmethod
|
|
51
57
|
def split(value: str) -> list[str]:
|
|
52
|
-
return [m.group() for m in re.finditer(r"(^[a-z]
|
|
58
|
+
return [m.group() for m in re.finditer(r"(^[a-z]+)|([A-Z][a-z]*)", value)]
|
|
53
59
|
|
|
54
60
|
@staticmethod
|
|
55
61
|
def join(parts: list[str]) -> str:
|
|
@@ -57,6 +63,8 @@ class PascalCase(CaseType):
|
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
class SnakeCase(CaseType):
|
|
66
|
+
"""snake_case"""
|
|
67
|
+
|
|
60
68
|
@staticmethod
|
|
61
69
|
def split(value: str) -> list[str]:
|
|
62
70
|
return value.split("_")
|
klaatu_python/lock.py
CHANGED
|
@@ -2,12 +2,12 @@ import functools
|
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class LockException(Exception):
|
|
6
|
-
...
|
|
5
|
+
class LockException(Exception): ...
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class Lock:
|
|
10
9
|
"""Does the same as `lock`, but as a context manager."""
|
|
10
|
+
|
|
11
11
|
def __init__(self, lockfile: str):
|
|
12
12
|
self.lockfile = lockfile
|
|
13
13
|
|
|
@@ -26,6 +26,7 @@ class Lock:
|
|
|
26
26
|
|
|
27
27
|
def lock(lockfile: str):
|
|
28
28
|
"""Primitive mechanism to avoid concurrent execution of a function."""
|
|
29
|
+
|
|
29
30
|
def decorator(func):
|
|
30
31
|
@functools.wraps(func)
|
|
31
32
|
def wrapper(*args, **kwargs):
|
|
@@ -44,5 +45,7 @@ def lock(lockfile: str):
|
|
|
44
45
|
remote_attempts += 1
|
|
45
46
|
|
|
46
47
|
return result
|
|
48
|
+
|
|
47
49
|
return wrapper
|
|
50
|
+
|
|
48
51
|
return decorator
|
klaatu_python/utils.py
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import itertools
|
|
1
2
|
import re
|
|
3
|
+
from collections.abc import Callable, Hashable, Iterable, Iterator, Sequence
|
|
2
4
|
from datetime import date, timedelta
|
|
5
|
+
from locale import format_string, localeconv
|
|
3
6
|
from math import ceil, floor, log10
|
|
4
|
-
from
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Literal, TypeVar, overload
|
|
5
9
|
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
_T = TypeVar("_T")
|
|
9
13
|
_NT = TypeVar("_NT", int, float)
|
|
14
|
+
_HashableT = TypeVar("_HashableT", bound=Hashable)
|
|
10
15
|
|
|
11
16
|
|
|
12
|
-
def append_query_to_url(url: str, params: dict, conditional_params: dict | None = None, safe: str =
|
|
17
|
+
def append_query_to_url(url: str, params: dict, conditional_params: dict | None = None, safe: str = "") -> str:
|
|
13
18
|
"""
|
|
14
19
|
Adds GET query from `params` to `url`, or appends it if there already is
|
|
15
20
|
one.
|
|
@@ -65,10 +70,49 @@ def daterange(start_date: date, end_date: date) -> Iterator[date]:
|
|
|
65
70
|
yield start_date + timedelta(days=n)
|
|
66
71
|
|
|
67
72
|
|
|
73
|
+
def deepest_common_path(path1: Path, *paths: Path) -> Path:
|
|
74
|
+
"""
|
|
75
|
+
Finds and returns the deepest (as in "deep in the directory structure")
|
|
76
|
+
path that all parameters have in common.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> deepest_common_path(Path("/usr/local/bin/"), Path("/usr/share/"))
|
|
80
|
+
PosixPath('/usr')
|
|
81
|
+
"""
|
|
82
|
+
path1 = path1.resolve()
|
|
83
|
+
paths = tuple(p.resolve() for p in paths)
|
|
84
|
+
|
|
85
|
+
# Returned iterator will contain None unless all paths are identical:
|
|
86
|
+
if all(itertools.accumulate([path1, *paths], lambda p1, p2: p1 if p1 == p2 else None)):
|
|
87
|
+
return path1
|
|
88
|
+
|
|
89
|
+
paths_parts = [[p, *p.parents] for p in paths]
|
|
90
|
+
|
|
91
|
+
for part in [path1, *path1.parents]:
|
|
92
|
+
if all(part in parts for parts in paths_parts):
|
|
93
|
+
return part
|
|
94
|
+
|
|
95
|
+
raise ValueError("No common paths")
|
|
96
|
+
|
|
97
|
+
|
|
68
98
|
def filter_values_not_null(d: dict) -> dict:
|
|
69
99
|
return {k: v for k, v in d.items() if v is not None}
|
|
70
100
|
|
|
71
101
|
|
|
102
|
+
def first_not_null(*values: _T | None) -> _T:
|
|
103
|
+
ret = first_not_null_or_null(*values)
|
|
104
|
+
if ret is None:
|
|
105
|
+
raise TypeError("All values are None")
|
|
106
|
+
return ret
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def first_not_null_or_null(*values: _T | None) -> _T | None:
|
|
110
|
+
for value in values:
|
|
111
|
+
if value is not None:
|
|
112
|
+
return value
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
72
116
|
def getitem_nullable(seq: Iterable[_T], idx: int, cond: Callable[[_T], bool] | None = None) -> _T | None:
|
|
73
117
|
"""
|
|
74
118
|
If `seq` has an item at position `idx`, return that item. Otherwise return
|
|
@@ -211,9 +255,8 @@ def int_to_string(value: int | None, locale: str, nbsp: bool = False) -> str:
|
|
|
211
255
|
# 5. Re-reverse the digits in each group & join them with separator string
|
|
212
256
|
# 6. Re-reverse the order of the groups
|
|
213
257
|
# 7. Add minus sign if value was negative
|
|
214
|
-
return (
|
|
215
|
-
|
|
216
|
-
separator.join([v[::-1] for v in re.split(r"(\d{3})", str(abs(value))[::-1]) if v][::-1])
|
|
258
|
+
return ("-" if value < 0 else "") + separator.join(
|
|
259
|
+
[v[::-1] for v in re.split(r"(\d{3})", str(abs(value))[::-1]) if v][::-1]
|
|
217
260
|
)
|
|
218
261
|
|
|
219
262
|
|
|
@@ -227,6 +270,56 @@ def is_truthy(value: Any) -> bool:
|
|
|
227
270
|
return bool(value)
|
|
228
271
|
|
|
229
272
|
|
|
273
|
+
def localize_float(
|
|
274
|
+
value: float,
|
|
275
|
+
grouping: bool = True,
|
|
276
|
+
min_decimals: int | None = None,
|
|
277
|
+
max_decimals: int | None = None,
|
|
278
|
+
) -> str:
|
|
279
|
+
if max_decimals is None:
|
|
280
|
+
max_decimals = 15
|
|
281
|
+
|
|
282
|
+
if min_decimals is not None and min_decimals > max_decimals:
|
|
283
|
+
raise ValueError("min_decimals must be <= max_decimals")
|
|
284
|
+
if 15 < (min_decimals or 0) < 0:
|
|
285
|
+
raise ValueError("min_decimals must be >= 0 and <= 15")
|
|
286
|
+
if 15 < (max_decimals or 0) < 0:
|
|
287
|
+
raise ValueError("max_decimals must be >= 0 and <= 15")
|
|
288
|
+
|
|
289
|
+
decimal_separator = localeconv()["decimal_point"]
|
|
290
|
+
ret = format_string("%.*f", (max_decimals, value), grouping=grouping)
|
|
291
|
+
|
|
292
|
+
if decimal_separator in ret:
|
|
293
|
+
base, decimals = ret.split(decimal_separator)
|
|
294
|
+
decimals = decimals.rstrip("0").ljust(min_decimals or 0, "0")
|
|
295
|
+
if not decimals:
|
|
296
|
+
return base
|
|
297
|
+
return base + decimal_separator + decimals
|
|
298
|
+
|
|
299
|
+
return ret
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def most_common(values: Iterable[_HashableT]) -> tuple[_HashableT | None, int]:
|
|
303
|
+
"""
|
|
304
|
+
Given a non-empty iterable, this will return a 2-tuple with the most
|
|
305
|
+
commonly occurring value in said iterable, and the number of such
|
|
306
|
+
occurances.
|
|
307
|
+
|
|
308
|
+
Given an _empty_ iterable, it will return `(None, 0)`.
|
|
309
|
+
"""
|
|
310
|
+
occurrences: dict[_HashableT, int] = {}
|
|
311
|
+
|
|
312
|
+
for value in values:
|
|
313
|
+
if value not in occurrences:
|
|
314
|
+
occurrences[value] = 0
|
|
315
|
+
occurrences[value] += 1
|
|
316
|
+
|
|
317
|
+
if not occurrences:
|
|
318
|
+
return None, 0
|
|
319
|
+
|
|
320
|
+
return max(occurrences.items(), key=lambda tup: tup[1])
|
|
321
|
+
|
|
322
|
+
|
|
230
323
|
def nonulls(items: Iterable[_T | None]) -> list[_T]:
|
|
231
324
|
"""Just filters away None values from `items`."""
|
|
232
325
|
return [item for item in items if item is not None]
|
|
@@ -236,7 +329,7 @@ def partition(items: Sequence[_T], length: int) -> Iterator[Sequence[_T]]:
|
|
|
236
329
|
"""Simply splits `items` into subsequences of max `length` items."""
|
|
237
330
|
offset = 0
|
|
238
331
|
while offset == 0 or offset < len(items):
|
|
239
|
-
yield items[offset:offset + length]
|
|
332
|
+
yield items[offset : offset + length]
|
|
240
333
|
offset += length
|
|
241
334
|
|
|
242
335
|
|
|
@@ -246,6 +339,37 @@ def percent_rounded(part: int | float, whole: int | float) -> int:
|
|
|
246
339
|
return round(part / whole * 100)
|
|
247
340
|
|
|
248
341
|
|
|
342
|
+
def roman(n: int):
|
|
343
|
+
if n % 1:
|
|
344
|
+
raise ValueError("roman(): fractions not allowed")
|
|
345
|
+
if n >= 4000 or n < 0:
|
|
346
|
+
raise ValueError("roman(): value must be >= 0 and < 4000")
|
|
347
|
+
|
|
348
|
+
m = {
|
|
349
|
+
1000: "M",
|
|
350
|
+
900: "CM",
|
|
351
|
+
500: "D",
|
|
352
|
+
400: "CD",
|
|
353
|
+
100: "C",
|
|
354
|
+
90: "XC",
|
|
355
|
+
50: "L",
|
|
356
|
+
40: "XL",
|
|
357
|
+
10: "X",
|
|
358
|
+
9: "IX",
|
|
359
|
+
5: "V",
|
|
360
|
+
4: "IV",
|
|
361
|
+
1: "I",
|
|
362
|
+
}
|
|
363
|
+
r = ""
|
|
364
|
+
|
|
365
|
+
for k, v in m.items():
|
|
366
|
+
while n >= k:
|
|
367
|
+
r += v
|
|
368
|
+
n -= k
|
|
369
|
+
|
|
370
|
+
return r
|
|
371
|
+
|
|
372
|
+
|
|
249
373
|
def round_to_n(x: int | float, n: int) -> int | float:
|
|
250
374
|
"""
|
|
251
375
|
Rounds x to n significant digits. If the result is a whole number, it is
|
|
@@ -278,7 +402,7 @@ def rounded_percentage(part: int | float, whole: int | float, digits=3) -> int |
|
|
|
278
402
|
def strip_url_query(url: str) -> str:
|
|
279
403
|
"""Just returns `url` stripped of any GET parameters."""
|
|
280
404
|
parts = urlsplit(url)
|
|
281
|
-
return urlunsplit((parts.scheme, parts.netloc, parts.path,
|
|
405
|
+
return urlunsplit((parts.scheme, parts.netloc, parts.path, "", parts.fragment))
|
|
282
406
|
|
|
283
407
|
|
|
284
408
|
def to_int(value: Any, default: int | None = None) -> int | None:
|
|
@@ -297,7 +421,7 @@ def zip_dicts(*dicts: dict) -> dict:
|
|
|
297
421
|
return {k: v for d in dicts for k, v in d.items()}
|
|
298
422
|
|
|
299
423
|
|
|
300
|
-
def zip_dict_lists(dict_lists: Sequence[Sequence[dict]]) -> Iterator[dict]:
|
|
424
|
+
def zip_dict_lists(dict_lists: Sequence[Sequence[dict]], strict: bool = False) -> Iterator[dict]:
|
|
301
425
|
"""
|
|
302
426
|
Zips a list of dict lists and combines them into single dicts.
|
|
303
427
|
I.e. given dict_lists = [
|
|
@@ -308,5 +432,5 @@ def zip_dict_lists(dict_lists: Sequence[Sequence[dict]]) -> Iterator[dict]:
|
|
|
308
432
|
{a1: 1, a2: 2, a3: 5, a4: 6}
|
|
309
433
|
{b1: 3, b2: 4, b3: 7, b4: 8}
|
|
310
434
|
"""
|
|
311
|
-
for dicts in zip(*dict_lists):
|
|
435
|
+
for dicts in zip(*dict_lists, strict=strict):
|
|
312
436
|
yield zip_dicts(*dicts)
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: klaatu-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
|
+
Author: Robert Huselius
|
|
4
5
|
Author-email: Robert Huselius <robert@huseli.us>
|
|
5
6
|
License-Expression: GPL-3.0-only
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
6
9
|
Project-URL: Homepage, https://github.com/Eboreg/klaatu-python
|
|
7
|
-
Project-URL: Source, https://github.com/Eboreg/klaatu-python
|
|
8
10
|
Project-URL: Issues, https://github.com/Eboreg/klaatu-python/issues
|
|
9
|
-
|
|
11
|
+
Project-URL: Source, https://github.com/Eboreg/klaatu-python
|
|
10
12
|
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Provides-Extra: dev
|
|
13
|
-
Requires-Dist: ipython; extra == "dev"
|
|
14
|
-
Requires-Dist: ipdb; extra == "dev"
|
|
15
|
-
Requires-Dist: isort; extra == "dev"
|
|
16
|
-
Requires-Dist: pylint; extra == "dev"
|
|
17
|
-
Dynamic: license-file
|
|
18
13
|
|
|
19
14
|
# klaatu-python
|
|
20
15
|
|
|
@@ -34,7 +29,7 @@ In `pyproject.toml`:
|
|
|
34
29
|
```
|
|
35
30
|
[project]
|
|
36
31
|
dependencies = [
|
|
37
|
-
"
|
|
32
|
+
"klaatu-python",
|
|
38
33
|
...
|
|
39
34
|
]
|
|
40
35
|
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
klaatu_python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
klaatu_python/case.py,sha256=VEpFpufLMoOAl0ElDwvyF6M11z7hMbKml6JzQfkztKk,1788
|
|
3
|
+
klaatu_python/lock.py,sha256=D--5HjqAgtflOtNjp0klF8C_gXUjOrrT9YpKx4vFWn8,1393
|
|
4
|
+
klaatu_python/utils.py,sha256=CNQyB3uUQe1c85JH3ictz8gfDUCQfPiC9j0Kbw9X7Hc,13189
|
|
5
|
+
klaatu_python-2.4.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
6
|
+
klaatu_python-2.4.0.dist-info/WHEEL,sha256=5w2T7AS2mz1-rW9CNagNYWRCaB0iQqBMYLwKdlgiR4Q,78
|
|
7
|
+
klaatu_python-2.4.0.dist-info/METADATA,sha256=s_5Vr1eVv4hyPQ0p66RO73Ka8RbiQNEmgTIGKo0USRY,761
|
|
8
|
+
klaatu_python-2.4.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
klaatu_python/__init__.py,sha256=DKk-1b-rZsJFxFi1JoJ7TmEvIEQ0rf-C9HAZWwvjuM0,22
|
|
2
|
-
klaatu_python/case.py,sha256=2Tya4dlBrqkwhQZu9PTLEA0SiWSIoKw12Ar2UJZssd4,1692
|
|
3
|
-
klaatu_python/lock.py,sha256=STJpYdpYxHJDges9DFv08kC4hGa1IPtHMNxanrQlioM,1393
|
|
4
|
-
klaatu_python/utils.py,sha256=SffgWZV0AbMe63VfK0REz7bjzWutPCLdin3vBb3xc6o,9779
|
|
5
|
-
klaatu_python-2.2.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
6
|
-
klaatu_python-2.2.0.dist-info/METADATA,sha256=eSqjgxZZ4301cxMuweJJFHWr2nWzvzsGxUwBJvzcdVg,982
|
|
7
|
-
klaatu_python-2.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
klaatu_python-2.2.0.dist-info/top_level.txt,sha256=JxX5CCI_SC5L4jbAG7acjKwIPVWmCl46ofDZNZtnV1o,14
|
|
9
|
-
klaatu_python-2.2.0.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
klaatu_python
|
|
File without changes
|