klaatu-python 2.3.0__tar.gz → 2.4.0__tar.gz

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.
@@ -1,20 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: klaatu-python
3
- Version: 2.3.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
10
  Project-URL: Issues, https://github.com/Eboreg/klaatu-python/issues
8
11
  Project-URL: Source, https://github.com/Eboreg/klaatu-python
9
- Requires-Python: >=3.7
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
 
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.9.7,<0.10.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ authors = [
7
+ {name = "Robert Huselius", email = "robert@huseli.us"},
8
+ ]
9
+ license = "GPL-3.0-only"
10
+ license-files = ["LICENSE"]
11
+ name = "klaatu-python"
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ version = "2.4.0"
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/Eboreg/klaatu-python"
18
+ Issues = "https://github.com/Eboreg/klaatu-python/issues"
19
+ Source = "https://github.com/Eboreg/klaatu-python"
20
+
21
+ [tool.ruff]
22
+ line-length = 120
23
+ target-version = "py311"
24
+ exclude = [".venv", ".git", ".mypy_cache", "__pycache__", "build", "node_modules"]
25
+
26
+ [tool.ruff.lint]
27
+ select = ["E", "F", "W", "B", "I"]
28
+ ignore = ["E722"]
29
+
30
+ [tool.ruff.lint.isort]
31
+ lines-after-imports = 2
32
+ combine-as-imports = true
33
+ default-section = "third-party"
34
+ section-order = [
35
+ "future",
36
+ "standard-library",
37
+ "third-party",
38
+ "first-party",
39
+ "local-folder",
40
+ ]
41
+
42
+ [tool.ruff.format]
43
+ skip-magic-trailing-comma = false
44
+
45
+ [tool.flake8]
46
+ max-doc-length = 79
47
+ max-line-length = 119
@@ -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("_")
@@ -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
@@ -1,16 +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
3
5
  from locale import format_string, localeconv
4
6
  from math import ceil, floor, log10
5
- from typing import Any, Callable, Iterable, Iterator, Literal, Sequence, TypeVar, overload
7
+ from pathlib import Path
8
+ from typing import Any, Literal, TypeVar, overload
6
9
  from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
7
10
 
8
11
 
9
12
  _T = TypeVar("_T")
10
13
  _NT = TypeVar("_NT", int, float)
14
+ _HashableT = TypeVar("_HashableT", bound=Hashable)
11
15
 
12
16
 
13
- 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:
14
18
  """
15
19
  Adds GET query from `params` to `url`, or appends it if there already is
16
20
  one.
@@ -66,6 +70,31 @@ def daterange(start_date: date, end_date: date) -> Iterator[date]:
66
70
  yield start_date + timedelta(days=n)
67
71
 
68
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
+
69
98
  def filter_values_not_null(d: dict) -> dict:
70
99
  return {k: v for k, v in d.items() if v is not None}
71
100
 
@@ -226,9 +255,8 @@ def int_to_string(value: int | None, locale: str, nbsp: bool = False) -> str:
226
255
  # 5. Re-reverse the digits in each group & join them with separator string
227
256
  # 6. Re-reverse the order of the groups
228
257
  # 7. Add minus sign if value was negative
229
- return (
230
- ("-" if value < 0 else "") +
231
- 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]
232
260
  )
233
261
 
234
262
 
@@ -271,6 +299,27 @@ def localize_float(
271
299
  return ret
272
300
 
273
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
+
274
323
  def nonulls(items: Iterable[_T | None]) -> list[_T]:
275
324
  """Just filters away None values from `items`."""
276
325
  return [item for item in items if item is not None]
@@ -280,7 +329,7 @@ def partition(items: Sequence[_T], length: int) -> Iterator[Sequence[_T]]:
280
329
  """Simply splits `items` into subsequences of max `length` items."""
281
330
  offset = 0
282
331
  while offset == 0 or offset < len(items):
283
- yield items[offset:offset + length]
332
+ yield items[offset : offset + length]
284
333
  offset += length
285
334
 
286
335
 
@@ -290,6 +339,37 @@ def percent_rounded(part: int | float, whole: int | float) -> int:
290
339
  return round(part / whole * 100)
291
340
 
292
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
+
293
373
  def round_to_n(x: int | float, n: int) -> int | float:
294
374
  """
295
375
  Rounds x to n significant digits. If the result is a whole number, it is
@@ -322,7 +402,7 @@ def rounded_percentage(part: int | float, whole: int | float, digits=3) -> int |
322
402
  def strip_url_query(url: str) -> str:
323
403
  """Just returns `url` stripped of any GET parameters."""
324
404
  parts = urlsplit(url)
325
- return urlunsplit((parts.scheme, parts.netloc, parts.path, '', parts.fragment))
405
+ return urlunsplit((parts.scheme, parts.netloc, parts.path, "", parts.fragment))
326
406
 
327
407
 
328
408
  def to_int(value: Any, default: int | None = None) -> int | None:
@@ -341,7 +421,7 @@ def zip_dicts(*dicts: dict) -> dict:
341
421
  return {k: v for d in dicts for k, v in d.items()}
342
422
 
343
423
 
344
- 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]:
345
425
  """
346
426
  Zips a list of dict lists and combines them into single dicts.
347
427
  I.e. given dict_lists = [
@@ -352,5 +432,5 @@ def zip_dict_lists(dict_lists: Sequence[Sequence[dict]]) -> Iterator[dict]:
352
432
  {a1: 1, a2: 2, a3: 5, a4: 6}
353
433
  {b1: 3, b2: 4, b3: 7, b4: 8}
354
434
  """
355
- for dicts in zip(*dict_lists):
435
+ for dicts in zip(*dict_lists, strict=strict):
356
436
  yield zip_dicts(*dicts)
@@ -1,40 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: klaatu-python
3
- Version: 2.3.0
4
- Author-email: Robert Huselius <robert@huseli.us>
5
- License-Expression: GPL-3.0-only
6
- Project-URL: Homepage, https://github.com/Eboreg/klaatu-python
7
- Project-URL: Issues, https://github.com/Eboreg/klaatu-python/issues
8
- Project-URL: Source, https://github.com/Eboreg/klaatu-python
9
- Requires-Python: >=3.7
10
- 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
-
19
- # klaatu-python
20
-
21
- A bunch of more or less useful Python utils without any 3rd party dependencies.
22
-
23
- ## Development install
24
-
25
- ```shell
26
- python3 -m venv .venv
27
- source .venv/bin/activate
28
- pip install -e .[dev]
29
- ```
30
-
31
- ## Inclusion in projects
32
-
33
- In `pyproject.toml`:
34
- ```
35
- [project]
36
- dependencies = [
37
- "klaatu-python",
38
- ...
39
- ]
40
- ```
@@ -1,12 +0,0 @@
1
- LICENSE
2
- README.md
3
- pyproject.toml
4
- klaatu_python/__init__.py
5
- klaatu_python/case.py
6
- klaatu_python/lock.py
7
- klaatu_python/utils.py
8
- klaatu_python.egg-info/PKG-INFO
9
- klaatu_python.egg-info/SOURCES.txt
10
- klaatu_python.egg-info/dependency_links.txt
11
- klaatu_python.egg-info/requires.txt
12
- klaatu_python.egg-info/top_level.txt
@@ -1,6 +0,0 @@
1
-
2
- [dev]
3
- ipython
4
- ipdb
5
- isort
6
- pylint
@@ -1 +0,0 @@
1
- klaatu_python
@@ -1,62 +0,0 @@
1
- [build-system]
2
- build-backend = "setuptools.build_meta"
3
- requires = ["setuptools"]
4
-
5
- [project]
6
- authors = [
7
- {name = "Robert Huselius", email = "robert@huseli.us"},
8
- ]
9
- license = "GPL-3.0-only"
10
- license-files = ["LICENSE"]
11
- name = "klaatu-python"
12
- readme = "README.md"
13
- requires-python = ">=3.7"
14
- version = "2.3.0"
15
-
16
- [project.optional-dependencies]
17
- dev = [
18
- "ipython",
19
- "ipdb",
20
- "isort",
21
- "pylint",
22
- ]
23
-
24
- [project.urls]
25
- Homepage = "https://github.com/Eboreg/klaatu-python"
26
- Issues = "https://github.com/Eboreg/klaatu-python/issues"
27
- Source = "https://github.com/Eboreg/klaatu-python"
28
-
29
- [tool.isort]
30
- combine_as_imports = true
31
- default_section = "THIRDPARTY"
32
- include_trailing_comma = true
33
- known_self = "klaatu_python"
34
- line_length = 119
35
- lines_after_imports = 2
36
- multi_line_output = 3
37
- sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "SELF", "LOCALFOLDER"]
38
- skip = [".venv"]
39
-
40
- [tool.pylint.main]
41
- disable = [
42
- "broad-exception-caught",
43
- "invalid-name",
44
- "missing-class-docstring",
45
- "missing-function-docstring",
46
- "missing-module-docstring",
47
- "too-many-arguments",
48
- "unnecessary-lambda-assignment",
49
- "unspecified-encoding",
50
- ]
51
- ignore = [".venv", ".git", "__pycache__"]
52
- max-line-length = 119
53
-
54
- [tool.mypy]
55
- check_untyped_defs = true
56
- files = "klaatu_python"
57
- ignore_missing_imports = true
58
- show_error_codes = true
59
-
60
- [tool.flake8]
61
- max-doc-length = 79
62
- max-line-length = 119
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
File without changes
File without changes