adiumentum 0.1.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.
adiumentum/__init__.py ADDED
@@ -0,0 +1,116 @@
1
+ from .color import Colorizer
2
+ from .comparison import equal_within, nearly_equal
3
+ from .exceptions import CustomValidationError
4
+ from .file_modification_time import (
5
+ first_newer,
6
+ get_time_created,
7
+ get_time_modified,
8
+ time_created_readable,
9
+ time_modified_readable,
10
+ )
11
+ from .frozendict import FrozenDefaultDict
12
+ from .functional import (
13
+ dmap,
14
+ fold_dictionaries,
15
+ identity,
16
+ kmap,
17
+ lmap,
18
+ smap,
19
+ tmap,
20
+ vmap,
21
+ )
22
+ from .io import (
23
+ list_full,
24
+ read_json,
25
+ read_raw,
26
+ write_json,
27
+ write_raw,
28
+ write_raw_bytes,
29
+ )
30
+ from .markers import (
31
+ helper,
32
+ impure,
33
+ mutates,
34
+ mutates_and_returns_instance,
35
+ mutates_instance,
36
+ pure,
37
+ refactor,
38
+ step_data,
39
+ step_transition,
40
+ validator,
41
+ )
42
+ from .numerical import evenly_spaced, ihash, round5
43
+ from .performance_logging import log_perf
44
+ from .string import (
45
+ MixedValidated,
46
+ PromptTypeName,
47
+ as_json,
48
+ cast_as,
49
+ flexsplit,
50
+ indent_lines,
51
+ parse_sequence,
52
+ )
53
+ from .timestamping import insert_timestamp
54
+ from .typing_utils import (
55
+ areinstances,
56
+ call_fallback_if_none,
57
+ fallback_if_none,
58
+ )
59
+
60
+ DELIMITER = "᜶"
61
+
62
+ __all__ = [
63
+ "DELIMITER",
64
+ "Colorizer",
65
+ "CustomValidationError",
66
+ "FrozenDefaultDict",
67
+ "MixedValidated",
68
+ "NoneDate",
69
+ "NoneTime",
70
+ "PromptTypeName",
71
+ "areinstances",
72
+ "args_to_dict",
73
+ "as_json",
74
+ "call_fallback_if_none",
75
+ "cast_as",
76
+ "dmap",
77
+ "equal_within",
78
+ "evenly_spaced",
79
+ "fallback_if_none",
80
+ "first_newer",
81
+ "flexsplit",
82
+ "fold_dictionaries",
83
+ "get_time_created",
84
+ "get_time_modified",
85
+ "helper",
86
+ "identity",
87
+ "ihash",
88
+ "impure",
89
+ "indent_lines",
90
+ "insert_timestamp",
91
+ "kmap",
92
+ "list_full",
93
+ "lmap",
94
+ "log_perf",
95
+ "mutates",
96
+ "mutates_and_returns_instance",
97
+ "mutates_instance",
98
+ "nearly_equal",
99
+ "parse_sequence",
100
+ "pure",
101
+ "read_json",
102
+ "read_raw",
103
+ "refactor",
104
+ "round5",
105
+ "smap",
106
+ "step_data",
107
+ "step_transition",
108
+ "time_created_readable",
109
+ "time_modified_readable",
110
+ "tmap",
111
+ "validator",
112
+ "vmap",
113
+ "write_json",
114
+ "write_raw",
115
+ "write_raw_bytes",
116
+ ]
adiumentum/color.py ADDED
@@ -0,0 +1,56 @@
1
+ class Colorizer:
2
+ def __init__(self, use_colors: bool = True) -> None:
3
+ if use_colors:
4
+ self.BLACK = "\u001b[30m"
5
+ self.RED = "\u001b[31m"
6
+ self.GREEN = "\u001b[32m"
7
+ self.YELLOW = "\u001b[33m"
8
+ self.BLUE = "\u001b[34m"
9
+ self.MAGENTA = "\u001b[35m"
10
+ self.CYAN = "\u001b[36m"
11
+ self.WHITE = "\u001b[37m"
12
+ self.RESET = "\u001b[0m"
13
+ else:
14
+ self.BLACK = ""
15
+ self.RED = ""
16
+ self.GREEN = ""
17
+ self.YELLOW = ""
18
+ self.BLUE = ""
19
+ self.MAGENTA = ""
20
+ self.CYAN = ""
21
+ self.WHITE = ""
22
+ self.RESET = ""
23
+
24
+ @staticmethod
25
+ def strip(s: str) -> str:
26
+ return s[5:-4] if s.startswith("\u001b") else s
27
+
28
+ def length(self, s: str) -> int:
29
+ return len(self.strip(s))
30
+
31
+ def black(self, text: str) -> str:
32
+ return self._format(text, self.BLACK)
33
+
34
+ def red(self, text: str) -> str:
35
+ return self._format(text, self.RED)
36
+
37
+ def green(self, text: str) -> str:
38
+ return self._format(text, self.GREEN)
39
+
40
+ def yellow(self, text: str) -> str:
41
+ return self._format(text, self.YELLOW)
42
+
43
+ def blue(self, text: str) -> str:
44
+ return self._format(text, self.BLUE)
45
+
46
+ def magenta(self, text: str) -> str:
47
+ return self._format(text, self.MAGENTA)
48
+
49
+ def cyan(self, text: str) -> str:
50
+ return self._format(text, self.CYAN)
51
+
52
+ def white(self, text: str) -> str:
53
+ return self._format(text, self.WHITE)
54
+
55
+ def _format(self, text: str, color_code: str) -> str:
56
+ return f"{color_code}{text}{self.RESET}"
@@ -0,0 +1,6 @@
1
+ def nearly_equal(a: int | float, b: int | float) -> bool:
2
+ return abs(a - b) < 0.00001
3
+
4
+
5
+ def equal_within(a: int | float, b: int | float, epsilon: float | int) -> bool:
6
+ return abs(a - b) < epsilon
@@ -0,0 +1,4 @@
1
+ def ics_to_json(): ...
2
+
3
+
4
+ def json_to_ics(): ...
File without changes
@@ -0,0 +1,2 @@
1
+ class CustomValidationError(TypeError):
2
+ pass
@@ -0,0 +1,31 @@
1
+ import os
2
+ import time
3
+ from pathlib import Path
4
+
5
+
6
+ def get_time_created(path: Path | str) -> float:
7
+ return os.path.getctime(path)
8
+
9
+
10
+ def time_created_readable(path: Path | str) -> str:
11
+ time_created: time.struct_time = time.strptime(time.ctime(get_time_created(path)))
12
+ return time.strftime("%Y-%m-%d %H:%M:%S", time_created)
13
+
14
+
15
+ def get_time_modified(path: Path | str) -> float:
16
+ return os.path.getmtime(path)
17
+
18
+
19
+ def time_modified_readable(path: Path | str) -> str:
20
+ time_modified: time.struct_time = time.strptime(time.ctime(get_time_modified(path)))
21
+ return time.strftime("%Y-%m-%d %H:%M:%S", time_modified)
22
+
23
+
24
+ def first_newer(file1: str | Path, file2: str | Path | tuple[Path, ...] | tuple[str, ...]) -> bool:
25
+ m1 = os.path.getmtime(file1)
26
+
27
+ if isinstance(file2, tuple):
28
+ m2 = max(os.path.getmtime(f) for f in file2)
29
+ else:
30
+ m2 = os.path.getmtime(file2)
31
+ return m1 > m2
@@ -0,0 +1,27 @@
1
+ from collections import defaultdict
2
+ from collections.abc import Callable
3
+ from typing import Generic, TypeVar, cast
4
+
5
+ T = TypeVar("T")
6
+ K = TypeVar("K")
7
+
8
+
9
+ class FrozenDefaultDict(defaultdict[K, T], Generic[K, T]):
10
+ def __init__(self, default_factory: Callable[[], T], dictionary: dict[K, T]):
11
+ super().__init__(default_factory, dictionary)
12
+
13
+ def __repr__(self) -> str:
14
+ default = self.default_factory
15
+ result = default.__name__ if callable(default) else repr(default)
16
+ return f"{self.__class__.__name__}({dict(self)}, default={result})"
17
+
18
+ def __getitem__(self, key: K) -> T:
19
+ if key in self:
20
+ return super().__getitem__(key)
21
+ return cast(Callable, self.default_factory)()
22
+
23
+ def __setitem__(self, key: K, value: T) -> None:
24
+ raise TypeError(f"{self.__class__.__name__} is immutable")
25
+
26
+ def __delitem__(self, key) -> None:
27
+ raise TypeError(f"{self.__class__.__name__} is immutable")
@@ -0,0 +1,44 @@
1
+ from collections.abc import Callable, Iterable
2
+ from functools import reduce
3
+ from typing import TypeVar
4
+
5
+ T = TypeVar("T")
6
+ TPost = TypeVar("TPost")
7
+ TPre = TypeVar("TPre")
8
+ K = TypeVar("K")
9
+ V = TypeVar("V")
10
+
11
+
12
+ def lmap(callable_: Callable[[TPre], TPost], iterable: Iterable[TPre]) -> list[TPost]:
13
+ return list(map(callable_, iterable))
14
+
15
+
16
+ def smap(callable_: Callable[[TPre], TPost], iterable: Iterable[TPre]) -> set[TPost]:
17
+ return set(map(callable_, iterable))
18
+
19
+
20
+ def tmap(callable_: Callable[[TPre], TPost], iterable: Iterable[TPre]) -> tuple[TPost, ...]:
21
+ return tuple(map(callable_, iterable))
22
+
23
+
24
+ def vmap(callable_: Callable[[TPre], TPost], dictionary: dict[K, TPre]) -> dict[K, TPost]:
25
+ return {k: callable_(v) for k, v in dictionary.items()}
26
+
27
+
28
+ def kmap(callable_: Callable[[TPre], TPost], dictionary: dict[TPre, V]) -> dict[TPost, V]:
29
+ return {callable_(k): v for k, v in dictionary.items()}
30
+
31
+
32
+ def dmap(callable_: Callable[[TPre], TPost], dictionary: dict[TPre, TPre]) -> dict[TPost, TPost]:
33
+ return {callable_(k): callable_(v) for k, v in dictionary.items()}
34
+
35
+
36
+ def identity(x: T) -> T:
37
+ return x
38
+
39
+
40
+ def fold_dictionaries(dicts: Iterable[dict[K, T]]) -> dict[K, T]:
41
+ def _or(dict1: dict[K, T], dict2: dict[K, T]) -> dict[K, T]:
42
+ return dict1 | dict2
43
+
44
+ return reduce(_or, dicts)
adiumentum/io.py ADDED
@@ -0,0 +1,33 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+
5
+
6
+ def list_full(directory: str | Path, ending: str = "") -> list[Path]:
7
+ directory = Path(directory)
8
+ return sorted([directory / file for file in os.listdir(directory) if file.endswith(ending)])
9
+
10
+
11
+ def read_raw(json_path: Path) -> str:
12
+ with open(json_path, encoding="utf-8") as f:
13
+ return f.read()
14
+
15
+
16
+ def read_json(json_path: Path) -> dict | list:
17
+ with open(json_path, encoding="utf-8") as f:
18
+ return json.load(f)
19
+
20
+
21
+ def write_json(python_obj: dict | list | bytes, json_path: Path) -> None:
22
+ with open(json_path, "w", encoding="utf-8") as f:
23
+ json.dump(python_obj, f, indent=4, ensure_ascii=False)
24
+
25
+
26
+ def write_raw(text: str, file_path: Path) -> None:
27
+ with open(file_path, "w", encoding="utf-8") as f:
28
+ f.write(text)
29
+
30
+
31
+ def write_raw_bytes(text: bytes, json_path: Path) -> None:
32
+ with open(json_path, "wb") as f:
33
+ f.write(text)
adiumentum/markers.py ADDED
@@ -0,0 +1,117 @@
1
+ from collections.abc import Callable
2
+ from functools import wraps
3
+
4
+ from .functional import identity
5
+
6
+
7
+ def impure(callable_or_none: Callable | None = None, **kwargs) -> Callable:
8
+ if callable_or_none is None:
9
+ return identity
10
+ else:
11
+ return callable_or_none
12
+
13
+
14
+ def pure(callable_or_none: Callable | None = None, **kwargs) -> Callable:
15
+ if callable_or_none is None:
16
+ return identity
17
+ else:
18
+ return callable_or_none
19
+
20
+
21
+ def helper(callable_or_none: Callable | None = None, **kwargs) -> Callable:
22
+ if callable_or_none is None:
23
+ return identity
24
+ else:
25
+ return callable_or_none
26
+
27
+
28
+ def step_data(func: Callable) -> Callable:
29
+ @wraps(func)
30
+ def wrapper(*fargs, **fkwargs):
31
+ return func(*fargs, **fkwargs)
32
+
33
+ return wrapper
34
+
35
+
36
+ def step_transition(func: Callable) -> Callable:
37
+ @wraps(func)
38
+ def wrapper(*fargs, **fkwargs):
39
+ return func(*fargs, **fkwargs)
40
+
41
+ return wrapper
42
+
43
+
44
+ def validator(func: Callable) -> Callable:
45
+ @wraps(func)
46
+ def wrapper(*fargs, **fkwargs):
47
+ return func(*fargs, **fkwargs)
48
+
49
+ return wrapper
50
+
51
+
52
+ def mutates_instance(func: Callable) -> Callable:
53
+ @wraps(func)
54
+ def wrapper(*fargs, **fkwargs):
55
+ # print(
56
+ # (
57
+ # f"\u001b[31mMutating in place\u001b[0m: "
58
+ # f"\u001b[32m{type(fargs[0]).__name__:<25}\u001b[0m"
59
+ # f" via \u001b[33m{func.__name__:<25}\u001b[0m"
60
+ # f" in \u001b[34m{func.__module__}\u001b[0m"
61
+ # )
62
+ # )
63
+ return func(*fargs, **fkwargs)
64
+
65
+ return wrapper
66
+
67
+
68
+ def mutates_and_returns_instance(func: Callable) -> Callable:
69
+ @wraps(func)
70
+ def wrapper(*fargs, **fkwargs):
71
+ # print(
72
+ # (
73
+ # f"\u001b[31mMutating and returning\u001b[0m: "
74
+ # f"\u001b[32m{type(fargs[0]).__name__:<25}\u001b[0m"
75
+ # f" via \u001b[33m{func.__name__:<25}\u001b[0m"
76
+ # f" in \u001b[34m{func.__module__}\u001b[0m"
77
+ # )
78
+ # )
79
+ return func(*fargs, **fkwargs)
80
+
81
+ return wrapper
82
+
83
+
84
+ def mutates(*args, **kwargs) -> Callable:
85
+ def decorator(func: Callable) -> Callable:
86
+ @wraps(func)
87
+ def wrapper(*fargs, **fkwargs):
88
+ print(
89
+ f"Mutating attributes \u001b[36m{', '.join(args):<50}\u001b[0m"
90
+ f" of \u001b[32m{type(fargs[0]).__name__:<25}\u001b[0m"
91
+ f" via \u001b[33m{func.__name__:<25}\u001b[0m"
92
+ f" in \u001b[34m{func.__module__.replace('consilium.', '.'):<40}\u001b[0m"
93
+ )
94
+
95
+ return func(*fargs, **fkwargs)
96
+
97
+ return wrapper
98
+
99
+ return decorator
100
+
101
+
102
+ def refactor(*args) -> Callable:
103
+ def decorator(func: Callable) -> Callable:
104
+ @wraps(func)
105
+ def wrapper(*fargs, **fkwargs):
106
+ print(
107
+ f"\u001b[36mREFACTOR\u001b[0m"
108
+ f" \u001b[33m{func.__name__}\u001b[0m"
109
+ f" in \u001b[34m{func.__module__.replace('consilium.', '.')}\u001b[0m."
110
+ f" Notes: \u001b[32m{', '.join(args)}\u001b[0m"
111
+ )
112
+
113
+ return func(*fargs, **fkwargs)
114
+
115
+ return wrapper
116
+
117
+ return decorator
@@ -0,0 +1,28 @@
1
+ def round5(num: int | float, min_val: int | float = 15) -> int:
2
+ """For numbers greater than min_val, round to the nearest multiple of 5."""
3
+ if num < min_val:
4
+ return round(num)
5
+ q, r = divmod(num, 5)
6
+ return int(5 * (q + int(r > 2)))
7
+
8
+
9
+ def evenly_spaced(
10
+ start: float,
11
+ end: float,
12
+ steps: int,
13
+ reversed: bool = False,
14
+ ) -> list[float]:
15
+ if start > end:
16
+ start, end = end, start
17
+ reversed = True
18
+ start += 1e-7
19
+ end -= 1e-7
20
+ step_size = (end - start) / steps
21
+ numbers = [start + step_size * i for i in range(steps)]
22
+ if reversed:
23
+ numbers.reverse()
24
+ return numbers
25
+
26
+
27
+ def ihash(i: int) -> str:
28
+ return str(hash(str(i)))[-4:]
@@ -0,0 +1,50 @@
1
+ import json
2
+ import os
3
+ import time
4
+ from collections.abc import Callable
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ DEFAULT_PATH = Path("codeqa/performance/perf_log/perf_log.jsonl")
10
+
11
+
12
+ def get_callback_name(cb: Callable) -> str:
13
+ return getattr(cb, "__name__", None) or cb.__class__.__name__
14
+
15
+
16
+ def log_perf(
17
+ callable_or_none: Callable | None = None,
18
+ *,
19
+ log_path: Path | Callable = DEFAULT_PATH,
20
+ extra_info: dict[str, Any] | None = None,
21
+ ) -> Callable:
22
+ def wrapper(_callback: Callable) -> Callable:
23
+ if not os.environ.get("LOG_PERFORMANCE"):
24
+ return _callback
25
+
26
+ def inner(*args, **kwargs):
27
+ start_time = time.time()
28
+ result = _callback(*args, **kwargs)
29
+ end_time = time.time()
30
+
31
+ log_info = (
32
+ {
33
+ "name": get_callback_name(_callback),
34
+ "timeElapsed": round(end_time - start_time, 4),
35
+ "timestamp": datetime.now().isoformat(sep="_")[:-3],
36
+ "file": _callback.__module__,
37
+ }
38
+ | (extra_info or {})
39
+ | {"args": str(args), "kwargs": str(kwargs)}
40
+ )
41
+ with log_path.open("a", encoding="utf-8") as f:
42
+ f.write(json.dumps(log_info) + "\n")
43
+
44
+ return result
45
+
46
+ return inner
47
+
48
+ if callable(callable_or_none):
49
+ return wrapper(callable_or_none)
50
+ return wrapper
adiumentum/string.py ADDED
@@ -0,0 +1,179 @@
1
+ import json
2
+ import re
3
+ from collections.abc import Callable, Iterable
4
+ from typing import Literal, TypeAlias, cast
5
+
6
+ from datethyme import Date, Time
7
+
8
+ from .functional import lmap
9
+
10
+ MixedValidated: TypeAlias = (
11
+ str
12
+ | bool
13
+ | int
14
+ | float
15
+ | Time
16
+ | Date
17
+ | tuple[str, ...]
18
+ | tuple[str, str]
19
+ | tuple[float, float]
20
+ | None
21
+ )
22
+ PromptTypeName = Literal[
23
+ "string",
24
+ "boolean",
25
+ "integer",
26
+ "float",
27
+ "minutes",
28
+ "time",
29
+ "positiveScore",
30
+ "negativeScore",
31
+ "date",
32
+ "stringtuple",
33
+ ]
34
+
35
+
36
+ def indent_lines(lines: Iterable[object], indent_size: int = 4) -> str:
37
+ joiner = "\n" + indent_size * " "
38
+ return joiner + joiner.join(map(str, lines))
39
+
40
+
41
+ def flexsplit(s: str) -> list[str]:
42
+ return re.split(r", ?| ", s)
43
+
44
+
45
+ def split_sequence_string(seq: str) -> tuple[str, str, int]:
46
+ _start: str | None
47
+ _end: str | None
48
+ _step: str | None
49
+ match seq.count(":"):
50
+ case 2:
51
+ _start, _end, _step = seq.split(":")
52
+ case 1:
53
+ _start, _end = seq.split(":")
54
+ _step = None
55
+ case 0:
56
+ _start, _end, _step = None, seq, None
57
+ case _:
58
+ raise ValueError
59
+ return (_start or "").upper(), (_end or "").upper(), int(_step or 1)
60
+
61
+
62
+ def parse_sequence(s: str) -> list[str]:
63
+ def interpolate(_s: str) -> list[str]:
64
+ start, end, step = split_sequence_string(_s)
65
+ if end.isnumeric():
66
+ return lmap(str, range(int(start or 1), int(end) + 1, step))
67
+ elif end.isalpha():
68
+ start = start or "A"
69
+ assert len(start) == len(end) == 1
70
+ return lmap(chr, range(ord(start), ord(end) + 1, step))
71
+ else:
72
+ pattern = re.compile(r"[A-Z]\d+$")
73
+ letter = start[0]
74
+ assert re.match(pattern, start) and re.match(pattern, end) and (letter == end[0])
75
+ start, end = start[1:], end[1:]
76
+ return [f"{letter}{i}" for i in range(int(start), int(end) + 1, step)]
77
+
78
+ segments = []
79
+ for subseq in s.strip().split(","):
80
+ segments.extend(interpolate(subseq))
81
+ return segments
82
+
83
+
84
+ def cast_to_bool(s: str | bool) -> bool:
85
+ if isinstance(s, bool):
86
+ return s
87
+ s = s.lower()
88
+ if s.startswith(("y", "t")):
89
+ return True
90
+ if s.startswith(("f", "n")):
91
+ return False
92
+ raise ValueError(f"Ambiguous input for 'cast_to_bool': '{s}'")
93
+
94
+
95
+ def cast_to_int(s: str | bool) -> int:
96
+ if isinstance(s, bool):
97
+ return int(s)
98
+ return int(s.strip())
99
+
100
+
101
+ def cast_to_float(s: str | bool) -> float:
102
+ if isinstance(s, bool):
103
+ return float(s)
104
+ return float(s.strip())
105
+
106
+
107
+ def cast_to_minutes(s: str | bool) -> int:
108
+ if isinstance(s, bool):
109
+ raise TypeError
110
+ s = s.strip()
111
+ if ":" in s:
112
+ if s.count(":") > 1:
113
+ raise ValueError
114
+ hours, minutes = s.split(":")
115
+ return 60 * int(hours) + int(minutes)
116
+ return int(s)
117
+
118
+
119
+ def cast_to_positive_score(s: str | bool) -> float:
120
+ if isinstance(s, bool):
121
+ raise TypeError
122
+ score = float(s.strip())
123
+ if not 0.0 <= score <= 5.0:
124
+ raise ValueError
125
+ return score
126
+
127
+
128
+ def cast_to_negative_score(s: str | bool) -> float:
129
+ if isinstance(s, bool):
130
+ raise TypeError
131
+ score = float(s.strip())
132
+ if not -5.0 <= score <= 0.0:
133
+ raise ValueError
134
+ return score
135
+
136
+
137
+ def cast_to_Date(s: str | bool) -> Date:
138
+ if isinstance(s, bool):
139
+ raise TypeError
140
+ return Date.model_validate(s)
141
+
142
+
143
+ def cast_to_stringtuple(s: str | bool) -> tuple[str, ...]:
144
+ if isinstance(s, bool):
145
+ raise TypeError
146
+ if not s:
147
+ return tuple()
148
+ return cast(tuple[str, ...], tuple(re.split(r"[ ,;]+", s)))
149
+
150
+
151
+ def cast_as(
152
+ input_type: PromptTypeName,
153
+ ) -> Callable[[str | bool], MixedValidated]:
154
+ dispatch: dict[
155
+ PromptTypeName,
156
+ Callable[[str | bool], MixedValidated],
157
+ ] = {
158
+ "boolean": cast_to_bool,
159
+ "integer": cast_to_int,
160
+ "float": cast_to_float,
161
+ "minutes": cast_to_minutes,
162
+ "time": Time.model_validate,
163
+ "positiveScore": cast_to_positive_score,
164
+ "negativeScore": cast_to_negative_score,
165
+ "date": cast_to_Date,
166
+ "stringtuple": cast_to_stringtuple,
167
+ }
168
+ caster = dispatch[input_type]
169
+
170
+ def type_specific_caster(s: str | bool) -> MixedValidated:
171
+ if str(s).lower() in {"none", "null", "skip", "pass"}:
172
+ return None
173
+ return caster(s)
174
+
175
+ return type_specific_caster
176
+
177
+
178
+ def as_json(d: dict) -> str:
179
+ return json.dumps(d, ensure_ascii=False, indent=4)
@@ -0,0 +1,12 @@
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+
4
+
5
+ def insert_timestamp(p: Path | str) -> Path:
6
+ new_path = str(p)
7
+
8
+ if "." in new_path:
9
+ base, suffix = new_path.rsplit(".", 1)
10
+ return Path(f"{base}__{datetime.now():%Y-%m-%d_%H:%M:%S}.{suffix}")
11
+
12
+ return Path(f"{new_path}__{datetime.now():%Y-%m-%d_%H:%M:%S}")
@@ -0,0 +1,19 @@
1
+ from collections.abc import Callable, Iterable
2
+ from types import UnionType
3
+ from typing import TypeVar
4
+
5
+ T = TypeVar("T")
6
+
7
+ ClassInfo = type | UnionType | tuple["ClassInfo"]
8
+
9
+
10
+ def areinstances(iterable_instance: Iterable, class_or_tuple: ClassInfo) -> bool:
11
+ return all(map(lambda inst: isinstance(inst, class_or_tuple), iterable_instance))
12
+
13
+
14
+ def fallback_if_none(orig: T | None, alt: T) -> T:
15
+ return alt if (orig is None) else orig
16
+
17
+
18
+ def call_fallback_if_none(orig: T | None, alt: Callable[[], T]) -> T:
19
+ return alt() if (orig is None) else orig
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.3
2
+ Name: adiumentum
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Isaac Riley
6
+ Author-email: Isaac Riley <yelircaasi@proton.me>
7
+ Requires-Dist: multipledispatch>=1
8
+ Requires-Dist: loguru>=0.7.3
9
+ Requires-Dist: datethyme>=0.4.0
10
+ Requires-Python: >=3.12, <3.14
11
+ Description-Content-Type: text/markdown
12
+
13
+ # adiumentum
14
+
15
+ Running this project requires `copier`, `git`, `uv`, and `flake`. Install `nix` and
16
+ install the others by running `nix-shell -p uv python3 git`. You can now copy the project and run the toy CLI provided out-of-the-box:
17
+
18
+ ```sh
19
+ copier copy --trust \
20
+ /home/isaac/repos/dev-envs/python-uv-nix /tmp/hello-world \
21
+ --data-file /home/isaac/repos/dev-envs/python-uv-nix/example-data-uv.yml
22
+
23
+ nix run /tmp/hello-world
24
+ ```
25
+
26
+ You can also enter a development environment with all dependencies installed:
27
+
28
+ ```sh
29
+ nix develop
30
+ ```
31
+
32
+ Once in this dev shell, you have a number of development utils you can try out (via just):
33
+
34
+ ```sh
35
+ ✔just
36
+ ✔just format
37
+ ✔just check
38
+ ✔just fix
39
+ ✔just typecheck
40
+ ✔just lint
41
+ ✔just deal
42
+ ✔just vulture
43
+ ✔just pydeps-full
44
+ ✔just pydeps
45
+ ✔just pydeps-simple
46
+ ✔just view-deps
47
+ ✔just snakefood
48
+ ✔just deply
49
+ ✔just bandit
50
+ ✔just bandit-html
51
+ ✔just bandit-view
52
+ ✔just pyflame
53
+ ✔just flamegraph
54
+ ✔just perf-flamegraph
55
+ ✔just check-structure
56
+ ✔just check-imports
57
+ ✔just smoke
58
+ ✔just unit
59
+ ✔just test
60
+ ✔just test-cov
61
+ ✔just docs
62
+ ✔just scalene
63
+ ✔just view-cov
64
+ ✔just view-docs
65
+ ✔just view-flamegraphs
66
+ ✔just sbom
67
+
68
+ lefthook validate
69
+ lefthook run all
70
+ ```
71
+
72
+ ## Roadmap
73
+
74
+ 00. ✔ Sync package lists in uv.nix.jinja and README.md
75
+
76
+ 01. ✔ Parametrize each package for copier and update copier.yml
77
+
78
+ 02. ✔ Clean up copier.yml and example-data.yml
79
+
80
+ 03. ✔ Get impure environment working -> draw from buildFHSUserEnv approach in consilium and other projects
81
+
82
+ 04. ✔ Write working version of scripts and test them on datethyme
83
+
84
+ 05. ✔ create reference project (revised from datethyme)
85
+
86
+ 06. ✔ use copier to re-create it (make a datethyme answers file)
87
+
88
+ 08. ✔ find good CLI tools for diffing an entire folder
89
+
90
+ 09. ✔ iteratively modify template until copier perfectly re-creates the reference project
91
+
92
+ 10. ✔ package mdformat with mdformat-mkdocs (via nix)
93
+
94
+ 11. ✔ remove super-linter, but look over it and steal any good ideas
95
+
96
+ 12. ✔ add [commitizen](https://github.com/commitizen-tools/commitizen) and [commitmsgfmt](https://gitlab.com/mkjeldsen/commitmsgfmt) ✔
97
+
98
+ 13. ✔ Modify nix to support any python version via [nixpkgs-python](https://github.com/cachix/nixpkgs-python) and [tox](https://tox.wiki/en/4.27.0/index.html)
99
+
100
+ 14. ✔ Read through [jinja2-ansible-filters](https://gitlab.com/dreamer-labs/libraries/)
101
+
102
+ 15. Go through https://www.youtube.com/results?search_query=nix+and+python and any relevant NixCon talks
103
+
104
+ 16. ✔ restructure nix code
105
+
106
+ 17. ✔ add poetry support (selectable via copier)
107
+
108
+ 18. re-make datethyme package using template, iteratively polishing the template
109
+
110
+ 19. add copier switch to include a CLI or not
111
+
112
+ ### Later
113
+
114
+ Note: first get working the way it is for datathyme.
115
+
116
+ 0. [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog)
117
+
118
+ 1. Package all docs packages via nix, since they run independently of the other Python packages.
119
+
120
+ 2. Do the same for testing dependencies, if possible.
121
+
122
+ 3. Add different types of git hooks
123
+
124
+ - Client-Side Hooks
125
+
126
+ - **Pre-commit hooks** run before a commit is created and are ideal for code quality checks. They're perfect for running linters, formatters, static analysis tools, or tests to catch issues before they enter the repository. If the hook exits with a non-zero status, the commit is aborted
127
+
128
+ - **Prepare-commit-msg hooks** execute after the default commit message is created but before the editor opens. These work well for automatically adding ticket numbers, branch names, or standardized formatting to commit messages based on branch patterns or other context
129
+
130
+ - **Commit-msg hooks** run after you've written your commit message and are excellent for enforcing commit message conventions. They can validate that messages follow specific formats, contain required information like issue references, or meet length requirements
131
+
132
+ - **Post-commit hooks** trigger after a commit completes successfully. Since they can't affect the commit outcome, they're useful for notifications, triggering builds, updating external systems, or logging commit information
133
+
134
+ - **Pre-rebase hooks** run before rebasing and help prevent rebasing published commits or branches that shouldn't be rebased. They're particularly valuable for protecting main branches or enforcing workflow policies
135
+
136
+ - **Post-checkout and post-merge hooks** execute after checking out branches or completing merges. These are ideal for environment setup tasks like updating dependencies, clearing caches, generating files, or syncing external resources that depend on the current branch state
137
+
138
+ - Server-Side Hooks
139
+
140
+ - **Pre-receive hooks** run before any references are updated when receiving a push. They're powerful for enforcing repository-wide policies like preventing force pushes to protected branches, validating that all commits meet standards, or checking permissions before allowing updates
141
+
142
+ - **Update hooks** execute once per branch being updated and are perfect for branch-specific policies. They can enforce different rules for different branches, validate individual commits, or check that updates follow branching strategies
143
+
144
+ - **Post-receive hooks** run after all references are successfully updated and are ideal for deployment triggers, sending notifications, updating issue trackers, or kicking off CI/CD pipelines. Since they run after the push succeeds, they're commonly used for automation that depends on the repository being in its new state
145
+
146
+ ## Dependency Classes
147
+
148
+ ## Dependency Classes
149
+
150
+ TODO: look at jj-fzf, lazyjj, gg-jj look at luxuries
151
+
152
+ - dependency resolution (should already be installed)
153
+
154
+ - [uv](https://github.com/astral-sh/uv) ✔
155
+
156
+ - miscellaneous (semver via Python)
157
+
158
+ - [semver](https://github.com/python-semver/python-semver) ✔
159
+
160
+ - task running / hooks (installable via Python or Nix; Nix preferred)
161
+
162
+ - [just](https://just.systems/man/en/) ✔
163
+ - [lefthook](https://lefthook.dev/) ✔
164
+
165
+ - interactive programming (installable via Python or Nix; Nix preferred)
166
+
167
+ - [ipython](https://ipython.org/) ✔
168
+
169
+ - static type checking (installable via Python or Nix; Nix preferred)
170
+
171
+ - [mypy](https://mypy.readthedocs.io/en/stable/) ✔
172
+ - [ty](https://github.com/astral-sh/ty) ✔
173
+
174
+ - source code visualization (graphviz and pydeps via Nix, the rest via Python)
175
+
176
+ - [pydeps](https://github.com/thebjorn/pydeps) ✔
177
+ - [graphviz](https://graphviz.org/) ✔
178
+ - [deply](https://vashkatsi.github.io/deply/) ✔
179
+ - [snakefood3](https://furius.ca/snakefood/) ✔
180
+ - [grimp](https://grimp.readthedocs.io/en/stable/usage.html) ✔
181
+
182
+ - performance profiling (flamegraph and scalene via Nix, pyflame via Python)
183
+
184
+ - [pyflame](https://pyflame.readthedocs.io/en/latest/) ✔
185
+ - [scalene](https://github.com/plasma-umass/scalene) ✔
186
+ - [flamegraph-rs](https://github.com/flamegraph-rs/flamegraph)
187
+ (cargo-flamegraph in nix) ✔
188
+
189
+ - software supply chain, security (installable via Python, but Nix preferred)
190
+
191
+ - [cyclonedx-python](https://github.com/CycloneDX/cyclonedx-python) ✔
192
+ - [bandit](https://bandit.readthedocs.io/en/latest/) ✔
193
+
194
+ - testing (installed via Python for now)
195
+
196
+ - [pytest](https://docs.pytest.org/en/stable/) ✔
197
+ - pytest plugins: --> look at [these](https://github.com/man-group/pytest-plugins)
198
+ - [mock](https://pytest-mock.readthedocs.io/en/latest/) ✔
199
+ - [testmon](https://testmon.org/) ✔
200
+ - [cov](https://pytest-cov.readthedocs.io/en/latest/) ✔
201
+ - [loguru](https://github.com/mcarans/pytest-loguru) ✔
202
+ - [profiling](https://github.com/man-group/pytest-plugins/tree/master/pytest-profiling) [video](https://www.youtube.com/watch?v=OexWnUTsQGU) ✔
203
+ - [coverage](https://coverage.readthedocs.io/en/7.8.2/) ✔
204
+ - [hypothesis](https://hypothesis.readthedocs.io/en/latest/) ✔
205
+ - [tox](https://tox.wiki/en/4.26.0/) ✔
206
+
207
+ - docs (installed via Python for now)
208
+
209
+ - [mkdocs](https://www.mkdocs.org/) ✔
210
+ - [mkdocstrings](https://mkdocstrings.github.io/) ✔
211
+ - [mkdocstrings-python](https://mkdocstrings.github.io/python/) ✔
212
+ - [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) ✔
213
+ - [pygments](https://pygments.org/) ✔
214
+
215
+ - formatters / linters
216
+
217
+ - installable via Python or Nix:
218
+ - [ruff](https://astral.sh/ruff) ✔
219
+ - [mdformat](https://github.com/hukkin/mdformat) + [mdformat-mkdocs](https://github.com/KyleKing/mdformat-mkdocs) TODO
220
+ - [yamlfmt](https://github.com/google/yamlfmt)
221
+ OR [yamllint](https://github.com/adrienverge/yamllint) ✔
222
+ - [pyprojectsort](https://github.com/kieran-ryan/pyprojectsort)
223
+ - [toml-sort](https://github.com/pappasam/toml-sort) ✔
224
+ - installable only via Nix or other package manager:
225
+ - [treefmt](https://treefmt.com/latest/) + [treefmt-nix](https://github.com/numtide/treefmt-nix) ✔
226
+ - [alejandra](https://github.com/kamadorueda/alejandra) ✔
227
+ - [super-linter](https://github.com/super-linter/super-linter) TODO
228
+ - [markdown-code-runner](https://github.com/drupol/markdown-code-runner)
229
+ OR [mdsf](https://github.com/hougesen/mdsf) ✔
230
+ - [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2)
231
+ OR [markdownlint-cli](https://github.com/DavidAnson/markdownlint-cli) TODO
232
+ - [just-formatter](https://github.com/eli-yip/just-formatter) ✔
233
+
234
+ ## To Look at for Later
235
+
236
+ - [pystackflame](https://pypi.org/project/pystackflame/)
@@ -0,0 +1,19 @@
1
+ adiumentum/__init__.py,sha256=5bd4edf50fe42ca697d5de8a1188f6b78c346663b91ad2b23cb350f81e1e5f20,2180
2
+ adiumentum/color.py,sha256=dfdebfecf0cdcf32794103fff75bb2e20dd0ae3514d1d051b1af95e6560f6790,1673
3
+ adiumentum/comparison.py,sha256=5d3045dbe9364c52c88ae52be29a6739d280f2f1789fa7813ccda0c05f1e1bad,204
4
+ adiumentum/converters.py,sha256=ae64583d622aa3f9e242fcba479ffb2fa5ec0bcbdf237e6bfc7f980f69914221,48
5
+ adiumentum/elementary_types.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
6
+ adiumentum/exceptions.py,sha256=b6851f704e236f8970998060317b68ead953f20e496b990ccd11eb740e5a01e3,49
7
+ adiumentum/file_modification_time.py,sha256=0c36086590f6c2e9790d7ed144fc7f5eac93794f3a7dd9a61647b81ccf37fae9,912
8
+ adiumentum/frozendict.py,sha256=1a7c8a1d8b8c1791040a30f2dfcbda820c6076f6b5a9e23719ec6ec13d7b9bee,960
9
+ adiumentum/functional.py,sha256=58d3bbbd2085bcd20b97c0aba59af7ac8d4331836b328a843b2f97256d7e9182,1311
10
+ adiumentum/io.py,sha256=8e00e9cb11d0d3bdd3d56b0ffbaecf0064d00866670a899426a59601a765eee8,929
11
+ adiumentum/markers.py,sha256=be50f8f45ff885d92429370efc3409962cb03b6c9cbf5d462bf58ab226e26a1f,3198
12
+ adiumentum/numerical.py,sha256=adad3335e7220c8bf9666ce8c79f01ca4f1a0933316bb8e6fbfc0440293fbbed,704
13
+ adiumentum/performance_logging.py,sha256=bd0c42337fb5c77921700e5487d366ea103e8cd25825138962bfb44c1b54773b,1471
14
+ adiumentum/string.py,sha256=bf43e7b023bdcf02b61004d093c93e69a69cf5e274f940c6086ff3c36df2d8f6,4557
15
+ adiumentum/timestamping.py,sha256=6e7c6bd0e88c9e2cb68489687eb853476ba5a351db4ee2d6698f34113e3b5017,340
16
+ adiumentum/typing_utils.py,sha256=36c90fb08da12c88aacce9c6b14d79e9da3db8aec5d52655179f6aaf676250b7,554
17
+ adiumentum-0.1.0.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
18
+ adiumentum-0.1.0.dist-info/METADATA,sha256=2a7b36433ead718b35bf50a2493f071d3643518e5bec67a964d6f65e16e6efd8,9669
19
+ adiumentum-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.7.22
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any