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 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]*)|([A-Z][a-z]*)", value)]
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]*)|([A-Z][a-z]*)", value)]
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 typing import Any, Callable, Iterable, Iterator, Literal, Sequence, TypeVar, overload
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 = '') -> 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
- ("-" if value < 0 else "") +
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, '', parts.fragment))
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.2.0
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
- Requires-Python: >=3.7
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
- "klaatu_python @ git+https://git@github.com/Eboreg/klaatu-python.git",
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.7
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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,5 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
3
- Root-Is-Purelib: true
4
- Tag: py3-none-any
5
-
@@ -1 +0,0 @@
1
- klaatu_python