ml3macro-utils 0.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.
@@ -0,0 +1 @@
1
+ """ml3macro Utils - Common utilities and shared code for ml3macro projects."""
@@ -0,0 +1,125 @@
1
+ """This is like a frozen (immutable) dict, except that you can add new keys to it.
2
+ Not allowed: updating/deleting existing keys or their values."""
3
+
4
+ from typing import (
5
+ Any,
6
+ Generic,
7
+ Hashable,
8
+ ItemsView,
9
+ Iterator,
10
+ KeysView,
11
+ Mapping,
12
+ Self,
13
+ TypeVar,
14
+ ValuesView,
15
+ overload,
16
+ )
17
+
18
+ KeyType = TypeVar("KeyType", bound=Hashable) # Allow any hashable type as key
19
+ ValueType = TypeVar("ValueType")
20
+ _T = TypeVar("_T")
21
+
22
+
23
+ class AdditiveMapping(Generic[KeyType, ValueType], Mapping[KeyType, ValueType]):
24
+ """An immutable mapping that allows adding new keys but not modifying existing ones.
25
+
26
+ Once created, existing keys cannot be updated or deleted, but new keys can be
27
+ added using the `add()` method.
28
+ """
29
+
30
+ __map: dict[KeyType, ValueType]
31
+
32
+ def __init__(self, **kwargs: ValueType) -> None:
33
+ self.__map = dict(kwargs) # type: ignore[assignment]
34
+
35
+ def __getitem__(self, key: KeyType, /) -> ValueType:
36
+ return self.__map[key]
37
+
38
+ def __len__(self) -> int:
39
+ return len(self.__map)
40
+
41
+ def __iter__(self) -> Iterator[KeyType]:
42
+ return iter(self.__map)
43
+
44
+ @overload
45
+ def get(self, __key: KeyType) -> ValueType | None: ...
46
+
47
+ @overload
48
+ def get(self, __key: KeyType, __default: ValueType) -> ValueType: ...
49
+
50
+ @overload
51
+ def get(self, __key: KeyType, __default: _T) -> ValueType | _T: ...
52
+
53
+ def get(self, __key: KeyType, __default: Any = None) -> ValueType | Any:
54
+ """Return the value for key if key is in the mapping, else default."""
55
+ return self.__map.get(__key, __default)
56
+
57
+ def add(self, remove_none_values: bool = True, **kwargs: ValueType) -> Self:
58
+ """Add new key-value pairs to the mapping.
59
+
60
+ Args:
61
+ remove_none_values: If True, skip any keys with None values
62
+ **kwargs: Key-value pairs to add
63
+
64
+ Returns:
65
+ Self for method chaining
66
+
67
+ Raises:
68
+ KeyError: If any key already exists
69
+ """
70
+ self.append(kwargs, remove_none_values=remove_none_values)
71
+ return self
72
+
73
+ def append(
74
+ self,
75
+ additional_mappings: Mapping[KeyType, ValueType] | dict[Any, ValueType],
76
+ /,
77
+ *,
78
+ remove_none_values: bool = True,
79
+ ) -> None:
80
+ """Append new key-value pairs from another mapping.
81
+
82
+ Args:
83
+ additional_mappings: Mapping containing key-value pairs to add
84
+ remove_none_values: If True, skip any keys with None values
85
+
86
+ Raises:
87
+ KeyError: If any key already exists
88
+ """
89
+ kvs_to_add = (
90
+ additional_mappings
91
+ if not remove_none_values
92
+ else {k: v for k, v in additional_mappings.items() if v is not None}
93
+ )
94
+ keys_to_add = kvs_to_add.keys()
95
+ conflicting_keys = [k for k in keys_to_add if k in self.__map]
96
+ if conflicting_keys:
97
+ raise KeyError(
98
+ f"{type(self).__qualname__} cannot update existing keys (but new keys can be added). "
99
+ f"Conflicting keys: {', '.join(str(k) for k in conflicting_keys)}"
100
+ )
101
+ self.__map.update(kvs_to_add)
102
+
103
+ def __setitem__(self, key: KeyType, value: ValueType) -> None:
104
+ """Prevent direct item assignment."""
105
+ raise TypeError(
106
+ f"'{type(self).__name__}' object does not support item assignment"
107
+ )
108
+
109
+ def __delitem__(self, key: KeyType) -> None:
110
+ """Prevent item deletion."""
111
+ raise TypeError(
112
+ f"'{type(self).__name__}' object does not support item deletion"
113
+ )
114
+
115
+ def keys(self) -> KeysView[KeyType]:
116
+ """Return a list of keys in the mapping."""
117
+ return KeysView(self.__map)
118
+
119
+ def values(self) -> ValuesView[ValueType]:
120
+ """Return a list of values in the mapping."""
121
+ return ValuesView(self.__map)
122
+
123
+ def items(self) -> ItemsView[KeyType, ValueType]:
124
+ """Return a list of (key, value) pairs in the mapping."""
125
+ return ItemsView(self.__map)
@@ -0,0 +1,4 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class BaseML3MacroStrEnum(StrEnum): ...
File without changes
@@ -0,0 +1,66 @@
1
+ from typing import Any, Callable, TypeVar
2
+
3
+ T = TypeVar("T")
4
+
5
+
6
+ def layered_abstract_class(base_class: type[T]) -> type[T]:
7
+ """
8
+ Decorator to enforce that concrete classes cannot inherit directly from the base class.
9
+ They must inherit from an intermediate abstract class.
10
+
11
+ Args:
12
+ base_class: The base class that should only be inherited by abstract classes
13
+
14
+ Returns:
15
+ The same base class with the enforcement logic added
16
+ """
17
+ original_init_subclass: Callable[..., None] | None = getattr(
18
+ base_class, "__init_subclass__", None
19
+ )
20
+
21
+ def decorator_error(s: str, /) -> TypeError:
22
+ return TypeError(f"[@{layered_abstract_class.__name__}]: {s}")
23
+
24
+ def __init_subclass__(cls: type, **kwargs: Any) -> None:
25
+ # Call original __init_subclass__ if it exists
26
+ if original_init_subclass:
27
+ original_init_subclass(**kwargs)
28
+
29
+ # Check if base_class is a direct parent
30
+ if base_class in cls.__bases__:
31
+ # Check if the class is abstract by looking for any unimplemented abstract methods
32
+ # in the entire inheritance chain (excluding object)
33
+ has_unimplemented_abstract_methods = False
34
+
35
+ # Get all abstract methods from the inheritance chain
36
+ all_abstract_methods = set()
37
+ for base in cls.__mro__[1:]: # Skip the class itself
38
+ if base is object:
39
+ continue
40
+ if hasattr(base, "__abstractmethods__"):
41
+ all_abstract_methods.update(base.__abstractmethods__)
42
+
43
+ # Check if any of these abstract methods are still unimplemented in cls
44
+ if all_abstract_methods:
45
+ # Get the methods that cls has implemented
46
+ implemented_methods = set()
47
+ for name in all_abstract_methods:
48
+ if name in cls.__dict__ and not getattr(
49
+ cls.__dict__[name], "__isabstractmethod__", False
50
+ ):
51
+ implemented_methods.add(name)
52
+
53
+ # If there are still unimplemented abstract methods, the class is abstract
54
+ if all_abstract_methods - implemented_methods:
55
+ has_unimplemented_abstract_methods = True
56
+
57
+ if not has_unimplemented_abstract_methods:
58
+ raise decorator_error(
59
+ f"{cls.__name__} cannot inherit directly from {base_class.__name__}. "
60
+ "It must inherit from an intermediate abstract class."
61
+ )
62
+
63
+ # Add the __init_subclass__ method to the base class
64
+ # We use setattr to avoid mypy's method assignment complaints
65
+ setattr(base_class, "__init_subclass__", classmethod(__init_subclass__))
66
+ return base_class
@@ -0,0 +1,11 @@
1
+ import uuid
2
+
3
+ from ulid import ULID
4
+
5
+
6
+ def short_id() -> str:
7
+ return str(uuid.uuid4())[:6].lower()
8
+
9
+
10
+ def ulid_str() -> str:
11
+ return str(ULID())
@@ -0,0 +1,12 @@
1
+ from itertools import tee
2
+ from typing import Callable, Iterable, TypeVar
3
+
4
+ Item = TypeVar("Item")
5
+
6
+
7
+ # TODO: WRITE UNIT TESTS
8
+ def partition(
9
+ iterable: Iterable[Item], /, *, condition: Callable[[Item], bool]
10
+ ) -> tuple[Iterable[Item], Iterable[Item]]:
11
+ t1, t2 = tee(iterable)
12
+ return filter(condition, t1), filter(lambda x: not condition(x), t2)
@@ -0,0 +1,96 @@
1
+ """Optional value handling utilities with clear callable behavior documentation.
2
+
3
+ Callable Handling Guidelines:
4
+ 1. PREFER returning values from getters when possible
5
+ 2. ONLY return callables when you specifically want deferred execution
6
+ 3. ALWAYS document when getters return callables
7
+ 4. CONSIDER using type hints to clarify callable intent
8
+ """
9
+
10
+ from typing import Callable, Optional, TypeVar
11
+
12
+ ValueType = TypeVar("ValueType")
13
+ Object = TypeVar("Object")
14
+
15
+
16
+ def _get_default(
17
+ default: ValueType | Callable[[], ValueType],
18
+ ) -> ValueType:
19
+ """Get the default value, calling callable defaults if needed."""
20
+ if callable(default):
21
+ return default()
22
+ return default
23
+
24
+
25
+ def get_or_else(
26
+ value: Optional[ValueType],
27
+ default: ValueType | Callable[[], ValueType],
28
+ ) -> ValueType:
29
+ """Return the value if not None, otherwise return the default.
30
+
31
+ Args:
32
+ value: The value to check (returns this if not None)
33
+ default: Value or callable to return if value is None
34
+
35
+ Returns:
36
+ The value if not None, otherwise the default (calling callable defaults)
37
+
38
+ Example:
39
+ >>> get_or_else("value", "default")
40
+ "value"
41
+ >>> get_or_else(None, lambda: "computed")
42
+ "computed"
43
+ """
44
+ if value is None:
45
+ return _get_default(default)
46
+ return value
47
+
48
+
49
+ def obj_get_or_else(
50
+ obj: Optional[Object],
51
+ get_value: Callable[[Object], Optional[ValueType]],
52
+ default: ValueType | Callable[[], ValueType],
53
+ ) -> ValueType:
54
+ """Retrieve a value from an object or return a default.
55
+
56
+ Important Behavior Notes:
57
+ 1. Callable Defaults: If default is callable, it's called when needed
58
+ 2. Getter Results: The getter's return value is returned AS-IS
59
+ - If getter returns a callable, YOU must decide whether to call it
60
+ - This function does NOT automatically call callables returned by getters
61
+ 3. None Handling: Returns default if obj is None OR getter returns None
62
+
63
+ Example Patterns:
64
+ Pattern A - Getter returns values (most common):
65
+ >>> result = obj_get_or_else(person, lambda p: p.name, "Unknown")
66
+ >>> # result is a string, no calling needed
67
+
68
+ Pattern B - Getter returns callables (advanced):
69
+ >>> result = obj_get_or_else(config, lambda c: c.loader, default_loader)
70
+ >>> # result might be callable - check before using!
71
+ >>> final_value = result() if callable(result) else result
72
+
73
+ Args:
74
+ obj: The object to extract value from (None returns default)
75
+ get_value: Function that extracts value from obj (returns None uses default)
76
+ default: Value or callable to return when obj/value is None
77
+
78
+ Returns:
79
+ The extracted value, or default value/callable result
80
+
81
+ Example:
82
+ >>> class Person:
83
+ ... def __init__(self, name):
84
+ ... self.name = name
85
+ >>> person = Person("Alice")
86
+ >>> obj_get_or_else(person, lambda p: p.name, "Unknown")
87
+ "Alice"
88
+ >>> obj_get_or_else(None, lambda p: p.name, "Unknown")
89
+ "Unknown"
90
+ """
91
+ if obj is None:
92
+ return _get_default(default)
93
+ value = get_value(obj)
94
+ if value is None:
95
+ return _get_default(default)
96
+ return value
File without changes
@@ -0,0 +1,77 @@
1
+ """Utility functions for working with Python object references"""
2
+
3
+ from functools import partial
4
+ from typing import Any
5
+
6
+
7
+ def fully_qualified_name(obj: Any) -> str:
8
+ """Get the fully qualified name of a Python object (module.class)."""
9
+
10
+ # ------------------------------------------------------------------
11
+ # Partial functions (explicitly not supported)
12
+ # ------------------------------------------------------------------
13
+ if isinstance(obj, partial):
14
+ raise ValueError("Partial functions are not supported")
15
+
16
+ # ------------------------------------------------------------------
17
+ # Properties
18
+ # ------------------------------------------------------------------
19
+ if isinstance(obj, property):
20
+ # The getter (`fget`) carries the real qualified name.
21
+ fget = getattr(obj, "fget", None)
22
+ if (
23
+ fget is not None
24
+ and hasattr(fget, "__module__")
25
+ and hasattr(fget, "__qualname__")
26
+ ):
27
+ return f"{fget.__module__}.{fget.__qualname__}"
28
+ raise ValueError(f"Cannot determine FQN for property {obj}")
29
+
30
+ # ------------------------------------------------------------------
31
+ # Modules
32
+ # ------------------------------------------------------------------
33
+ if hasattr(obj, "__name__") and not hasattr(obj, "__qualname__"):
34
+ # Built‑in modules (e.g. sys, os, math) – just the name
35
+ if getattr(obj, "__name__", "").startswith(("builtins", "sys", "os", "math")):
36
+ return str(obj.__name__)
37
+ return str(obj.__name__)
38
+
39
+ # ------------------------------------------------------------------
40
+ # Primitive values – not supported
41
+ # ------------------------------------------------------------------
42
+ if obj is None or isinstance(obj, (int, float, str, bool, list, dict, tuple, set)):
43
+ raise ValueError(
44
+ f"Cannot get FQN for primitive value: {obj}. "
45
+ f"Pass the class attribute descriptor instead, e.g., Parent.attr1"
46
+ )
47
+
48
+ # ------------------------------------------------------------------
49
+ # Lambda functions
50
+ # ------------------------------------------------------------------
51
+ if getattr(obj, "__name__", None) == "<lambda>":
52
+ raise ValueError("Cannot get FQN for lambda function")
53
+
54
+ # ------------------------------------------------------------------
55
+ # Bound methods – reject instance methods, allow class/static methods
56
+ # ------------------------------------------------------------------
57
+ if hasattr(obj, "__self__"):
58
+ if getattr(obj, "__module__", None) == "builtins":
59
+ # built‑in function – keep going
60
+ pass
61
+ elif obj.__self__ is not None and not isinstance(obj.__self__, type):
62
+ raise ValueError("Cannot get FQN for bound method")
63
+ # staticmethod / classmethod – continue
64
+
65
+ # ------------------------------------------------------------------
66
+ # Objects without __module__ / __qualname__
67
+ # ------------------------------------------------------------------
68
+ if not hasattr(obj, "__module__") or not hasattr(obj, "__qualname__"):
69
+ raise ValueError(
70
+ f"Object {obj} does not have __module__ and __qualname__ attributes"
71
+ )
72
+
73
+ module = obj.__module__
74
+ qualname = obj.__qualname__
75
+ if not module or not qualname:
76
+ raise ValueError(f"Object {obj} has empty __module__ or __qualname__")
77
+ return f"{module}.{qualname}"
@@ -0,0 +1,50 @@
1
+ from functools import reduce
2
+ from typing import Tuple
3
+
4
+
5
+ def strip_margin(s: str, /) -> str:
6
+ if not isinstance(s, str):
7
+ raise TypeError("Input must be a string")
8
+
9
+ lines: Tuple[str, ...] = tuple(s.splitlines())
10
+
11
+ # Remove empty leading lines (immutable approach)
12
+ first_non_empty_idx = next(
13
+ (i for i, line in enumerate(lines) if line.strip()), len(lines)
14
+ )
15
+ trimmed_lines: Tuple[str, ...] = lines[first_non_empty_idx:]
16
+
17
+ # Remove empty trailing lines (immutable approach)
18
+ last_non_empty_idx = next(
19
+ (i for i, line in enumerate(reversed(trimmed_lines)) if line.strip()),
20
+ len(trimmed_lines),
21
+ )
22
+ trimmed_lines = trimmed_lines[: len(trimmed_lines) - last_non_empty_idx]
23
+
24
+ if not trimmed_lines:
25
+ return ""
26
+
27
+ # Calculate margin length from first line
28
+ first_line: str = trimmed_lines[0]
29
+ margin_length: int = len(first_line) - len(first_line.lstrip())
30
+
31
+ # Verify all lines have consistent margin
32
+ for line in trimmed_lines:
33
+ if not line:
34
+ continue # Skip empty lines
35
+ if len(line) < margin_length:
36
+ raise ValueError(
37
+ f"Line '{line}' is shorter than margin length {margin_length}"
38
+ )
39
+ if line[:margin_length].strip():
40
+ raise ValueError(f"Line '{line}' has non-whitespace in margin area")
41
+
42
+ # Process all lines using reduce
43
+ def process_line(acc: Tuple[str, ...], line: str) -> Tuple[str, ...]:
44
+ return (*acc, line[margin_length:]) if line else (*acc, line)
45
+
46
+ result_lines: Tuple[str, ...] = reduce(
47
+ process_line, trimmed_lines[1:], (trimmed_lines[0][margin_length:],)
48
+ )
49
+
50
+ return "\n".join(result_lines)
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: ml3macro-utils
3
+ Version: 0.0.0
4
+ Summary: ml3macro common utils - immutable data structures, enhanced enums, decorators, and utilities
5
+ Author-email: macro <your-email@example.com>
6
+ Maintainer-email: macro <your-email@example.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://codeberg.org/gxyflow/ml3macro
9
+ Project-URL: Repository, https://codeberg.org/gxyflow/ml3macro
10
+ Project-URL: Issues, https://codeberg.org/gxyflow/ml3macro/issues
11
+ Keywords: ml3macro,utils,immutable,enum,decorators
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: <3.15,>=3.11
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: frozendict>=2.3.0
25
+ Requires-Dist: strenum>=0.4.0
26
+ Requires-Dist: overrides>=7.0.0
27
+ Requires-Dist: python-ulid>=1.0.0
28
+
29
+ # ml3macro utils
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/ml3macro-utils.svg)](https://pypi.org/project/ml3macro-utils/)
32
+ [![Python Version](https://img.shields.io/pypi/pyversions/ml3macro-utils.svg)](https://pypi.org/project/ml3macro-utils/)
33
+
34
+
35
+
36
+ Common utilities and shared code for ml3macro projects.
37
+
38
+ ## Status: In Development
39
+
40
+ This is a new package with general-purpose helper functions/classes/etc for use in my other projects.
41
+
42
+ ## Overview
43
+
44
+ This package contains reusable utilities and helpers used across ml3macro projects:
45
+
46
+ - **Data Structures**: Immutable/constrained collections like `AdditiveMapping`
47
+ - **Enums**: Enhanced enumeration base classes
48
+ - **Decorators**: Class structure enforcement decorators
49
+ - **Identifiers**: Unique ID generation utilities
50
+ - **Functional Helpers**: Iterables processing, optional value handling
51
+ - **Reflection**: Object introspection utilities
52
+ - **Text Processing**: String manipulation helpers
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install ml3macro-utils
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ Import specific utilities as needed:
63
+
64
+ ```python
65
+ from ml3macro.utils.additive_mapping import AdditiveMapping
66
+ from ml3macro.utils.identifier import short_id
67
+ from ml3macro.utils.optional import get_or_else
68
+ ```
69
+
70
+ Or import from the main package:
71
+
72
+ ```python
73
+ from ml3macro.utils import AdditiveMapping, short_id, get_or_else
74
+ ```
75
+
76
+ ## Requirements
77
+
78
+ - Python 3.11+
79
+ - frozendict
80
+ - strenum
81
+ - overrides
82
+ - python-ulid
83
+
84
+ ## License
85
+
86
+ MIT License
87
+
@@ -0,0 +1,15 @@
1
+ ml3macro/utils/__init__.py,sha256=EkR0neMLRYRaImWE9EuGVHwAYSYtSccIJTyfH-UWX5U,79
2
+ ml3macro/utils/additive_mapping.py,sha256=KBTErJU073W8FkDXvnReb037fSfpzRZmfuWca4oKI-A,4003
3
+ ml3macro/utils/base_enum.py,sha256=rVp_NvzNuoYDooIXWzUf12LZT7zBVn7QEzvG5AlM_mk,67
4
+ ml3macro/utils/identifier.py,sha256=7Ib2WxnlwwjQ_ydnJI-51tivcOtgnIUwTIBdT-YdBkE,149
5
+ ml3macro/utils/iterables.py,sha256=H0lBNxx-XF7HQRMb3KLY2LJm2fpl_nQy6dwvV5b4jKs,353
6
+ ml3macro/utils/optional.py,sha256=KsLOUUmGPavLZSV67teNUvyYe5H-o5Vvu5z_CcOg5xE,3216
7
+ ml3macro/utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ ml3macro/utils/reference.py,sha256=DXAxctJAN580C8hSg6lTNuXhmM89j3hb8hnGegltrGg,3470
9
+ ml3macro/utils/strings.py,sha256=6CRXThYlzhxU0Pmbui6kf8tkTycI93ntd_cg6qK3mjE,1700
10
+ ml3macro/utils/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ ml3macro/utils/decorators/layered_abstract_class.py,sha256=WpDuv6NhSzS3QrJVumBjAG2rzD2CI2aJAESjZiiuxJo,2776
12
+ ml3macro_utils-0.0.0.dist-info/METADATA,sha256=NDW1jVFhcHvIxhUIkyWJqWmszluIdB3AEl8100V4ZEU,2640
13
+ ml3macro_utils-0.0.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
14
+ ml3macro_utils-0.0.0.dist-info/top_level.txt,sha256=lfZK6lm0zMRNkTTOFqQzbBU0gf7ed_O_Wat-yy13Gwc,9
15
+ ml3macro_utils-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ ml3macro