mm-std 0.5.3__py3-none-any.whl → 0.6.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.
- mm_std/__init__.py +4 -4
- mm_std/dict_utils.py +35 -8
- mm_std/json_utils.py +7 -10
- mm_std/random_utils.py +2 -2
- mm_std/subprocess_utils.py +38 -17
- {mm_std-0.5.3.dist-info → mm_std-0.6.0.dist-info}/METADATA +1 -1
- mm_std-0.6.0.dist-info/RECORD +11 -0
- {mm_std-0.5.3.dist-info → mm_std-0.6.0.dist-info}/WHEEL +1 -1
- mm_std-0.5.3.dist-info/RECORD +0 -11
mm_std/__init__.py
CHANGED
|
@@ -3,19 +3,19 @@ from .dict_utils import replace_empty_dict_entries
|
|
|
3
3
|
from .json_utils import ExtendedJSONEncoder, json_dumps
|
|
4
4
|
from .random_utils import random_datetime, random_decimal
|
|
5
5
|
from .str_utils import parse_lines, str_contains_any, str_ends_with_any, str_starts_with_any
|
|
6
|
-
from .subprocess_utils import
|
|
6
|
+
from .subprocess_utils import CmdResult, run_cmd, run_ssh_cmd # nosec
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
9
|
+
"CmdResult",
|
|
9
10
|
"ExtendedJSONEncoder",
|
|
10
|
-
"ShellResult",
|
|
11
11
|
"json_dumps",
|
|
12
12
|
"parse_date",
|
|
13
13
|
"parse_lines",
|
|
14
14
|
"random_datetime",
|
|
15
15
|
"random_decimal",
|
|
16
16
|
"replace_empty_dict_entries",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"run_cmd",
|
|
18
|
+
"run_ssh_cmd",
|
|
19
19
|
"str_contains_any",
|
|
20
20
|
"str_ends_with_any",
|
|
21
21
|
"str_starts_with_any",
|
mm_std/dict_utils.py
CHANGED
|
@@ -1,22 +1,49 @@
|
|
|
1
|
-
from collections import defaultdict
|
|
1
|
+
from collections import OrderedDict, defaultdict
|
|
2
2
|
from collections.abc import Mapping, MutableMapping
|
|
3
3
|
from decimal import Decimal
|
|
4
|
-
from typing import TypeVar,
|
|
4
|
+
from typing import TypeVar, overload
|
|
5
5
|
|
|
6
6
|
K = TypeVar("K")
|
|
7
7
|
V = TypeVar("V")
|
|
8
|
-
# TypeVar bound to MutableMapping with same K, V as defaults parameter
|
|
9
|
-
# 'type: ignore' needed because mypy can't handle TypeVar bounds with other TypeVars
|
|
10
|
-
DictType = TypeVar("DictType", bound=MutableMapping[K, V]) # type: ignore[valid-type]
|
|
11
8
|
|
|
12
9
|
|
|
10
|
+
@overload
|
|
13
11
|
def replace_empty_dict_entries(
|
|
14
|
-
data:
|
|
12
|
+
data: defaultdict[K, V],
|
|
15
13
|
defaults: Mapping[K, V] | None = None,
|
|
16
14
|
treat_zero_as_empty: bool = False,
|
|
17
15
|
treat_false_as_empty: bool = False,
|
|
18
16
|
treat_empty_string_as_empty: bool = True,
|
|
19
|
-
) ->
|
|
17
|
+
) -> defaultdict[K, V]: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@overload
|
|
21
|
+
def replace_empty_dict_entries(
|
|
22
|
+
data: OrderedDict[K, V],
|
|
23
|
+
defaults: Mapping[K, V] | None = None,
|
|
24
|
+
treat_zero_as_empty: bool = False,
|
|
25
|
+
treat_false_as_empty: bool = False,
|
|
26
|
+
treat_empty_string_as_empty: bool = True,
|
|
27
|
+
) -> OrderedDict[K, V]: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@overload
|
|
31
|
+
def replace_empty_dict_entries(
|
|
32
|
+
data: dict[K, V],
|
|
33
|
+
defaults: Mapping[K, V] | None = None,
|
|
34
|
+
treat_zero_as_empty: bool = False,
|
|
35
|
+
treat_false_as_empty: bool = False,
|
|
36
|
+
treat_empty_string_as_empty: bool = True,
|
|
37
|
+
) -> dict[K, V]: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def replace_empty_dict_entries(
|
|
41
|
+
data: MutableMapping[K, V],
|
|
42
|
+
defaults: Mapping[K, V] | None = None,
|
|
43
|
+
treat_zero_as_empty: bool = False,
|
|
44
|
+
treat_false_as_empty: bool = False,
|
|
45
|
+
treat_empty_string_as_empty: bool = True,
|
|
46
|
+
) -> MutableMapping[K, V]:
|
|
20
47
|
"""
|
|
21
48
|
Replace empty entries in a dictionary with defaults or remove them entirely.
|
|
22
49
|
|
|
@@ -60,4 +87,4 @@ def replace_empty_dict_entries(
|
|
|
60
87
|
new_value = value
|
|
61
88
|
|
|
62
89
|
result[key] = new_value
|
|
63
|
-
return
|
|
90
|
+
return result
|
mm_std/json_utils.py
CHANGED
|
@@ -43,26 +43,23 @@ class ExtendedJSONEncoder(json.JSONEncoder):
|
|
|
43
43
|
serializer: Function that converts objects of this type to JSON-serializable data
|
|
44
44
|
|
|
45
45
|
Raises:
|
|
46
|
-
TypeError: If serializer is not callable
|
|
47
46
|
ValueError: If type_ is a built-in JSON type
|
|
48
47
|
"""
|
|
49
|
-
if not callable(serializer):
|
|
50
|
-
raise TypeError("Serializer must be callable")
|
|
51
48
|
if type_ in (str, int, float, bool, list, dict, type(None)):
|
|
52
49
|
raise ValueError(f"Cannot override built-in JSON type: {type_.__name__}")
|
|
53
50
|
cls._type_handlers[type_] = serializer
|
|
54
51
|
|
|
55
|
-
def default(self,
|
|
52
|
+
def default(self, o: Any) -> Any: # noqa: ANN401
|
|
56
53
|
# Check registered type handlers first
|
|
57
54
|
for type_, handler in self._type_handlers.items():
|
|
58
|
-
if isinstance(
|
|
59
|
-
return handler(
|
|
55
|
+
if isinstance(o, type_):
|
|
56
|
+
return handler(o)
|
|
60
57
|
|
|
61
58
|
# Special case: dataclasses (requires is_dataclass check, not isinstance)
|
|
62
|
-
if is_dataclass(
|
|
63
|
-
return asdict(
|
|
59
|
+
if is_dataclass(o) and not isinstance(o, type):
|
|
60
|
+
return asdict(o) # Don't need recursive serialization
|
|
64
61
|
|
|
65
|
-
return super().default(
|
|
62
|
+
return super().default(o)
|
|
66
63
|
|
|
67
64
|
|
|
68
65
|
def json_dumps(data: Any, type_handlers: dict[type[Any], Callable[[Any], Any]] | None = None, **kwargs: Any) -> str: # noqa: ANN401
|
|
@@ -101,7 +98,7 @@ def _auto_register_optional_types() -> None:
|
|
|
101
98
|
"""Register handlers for optional dependencies if available."""
|
|
102
99
|
# Pydantic models
|
|
103
100
|
try:
|
|
104
|
-
from pydantic import BaseModel # type: ignore[import-not-found]
|
|
101
|
+
from pydantic import BaseModel # type: ignore[import-not-found] # noqa: PLC0415
|
|
105
102
|
|
|
106
103
|
ExtendedJSONEncoder.register(BaseModel, lambda obj: obj.model_dump())
|
|
107
104
|
except ImportError:
|
mm_std/random_utils.py
CHANGED
|
@@ -33,7 +33,7 @@ def random_decimal(from_value: Decimal, to_value: Decimal) -> Decimal:
|
|
|
33
33
|
from_int = int(from_value * multiplier)
|
|
34
34
|
to_int = int(to_value * multiplier)
|
|
35
35
|
|
|
36
|
-
random_int = random.randint(from_int, to_int)
|
|
36
|
+
random_int = random.randint(from_int, to_int) # nosec B311
|
|
37
37
|
return Decimal(random_int) / Decimal(multiplier)
|
|
38
38
|
|
|
39
39
|
|
|
@@ -68,5 +68,5 @@ def random_datetime(
|
|
|
68
68
|
if total_seconds == 0:
|
|
69
69
|
return from_time
|
|
70
70
|
|
|
71
|
-
random_seconds = random.uniform(0, total_seconds)
|
|
71
|
+
random_seconds = random.uniform(0, total_seconds) # nosec B311
|
|
72
72
|
return from_time + timedelta(seconds=random_seconds)
|
mm_std/subprocess_utils.py
CHANGED
|
@@ -6,14 +6,8 @@ TIMEOUT_EXIT_CODE = 255
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
9
|
-
class
|
|
10
|
-
"""Result of
|
|
11
|
-
|
|
12
|
-
Args:
|
|
13
|
-
stdout: Standard output from the command
|
|
14
|
-
stderr: Standard error from the command
|
|
15
|
-
code: Exit code of the command
|
|
16
|
-
"""
|
|
9
|
+
class CmdResult:
|
|
10
|
+
"""Result of command execution."""
|
|
17
11
|
|
|
18
12
|
stdout: str
|
|
19
13
|
stderr: str
|
|
@@ -32,30 +26,57 @@ class ShellResult:
|
|
|
32
26
|
return result
|
|
33
27
|
|
|
34
28
|
|
|
35
|
-
def
|
|
36
|
-
|
|
29
|
+
def run_cmd(
|
|
30
|
+
cmd: str,
|
|
31
|
+
timeout: int | None = 60,
|
|
32
|
+
capture_output: bool = True,
|
|
33
|
+
echo_command: bool = False,
|
|
34
|
+
shell: bool = False,
|
|
35
|
+
) -> CmdResult:
|
|
36
|
+
"""Execute a command.
|
|
37
37
|
|
|
38
38
|
Args:
|
|
39
39
|
cmd: Command to execute
|
|
40
40
|
timeout: Timeout in seconds, None for no timeout
|
|
41
41
|
capture_output: Whether to capture stdout/stderr
|
|
42
42
|
echo_command: Whether to print the command before execution
|
|
43
|
+
shell: If False (default), the command is parsed with shlex.split() and
|
|
44
|
+
executed without shell interpretation. Special characters like
|
|
45
|
+
backticks, $(), pipes (|), redirects (>, <), and wildcards (*) are
|
|
46
|
+
treated as literal text. This is the safe mode for commands with
|
|
47
|
+
user input.
|
|
48
|
+
If True, the command is passed to the shell as-is, enabling pipes,
|
|
49
|
+
redirects, command substitution, and other shell features. Use this
|
|
50
|
+
only for trusted commands that need shell functionality.
|
|
43
51
|
|
|
44
52
|
Returns:
|
|
45
|
-
|
|
53
|
+
CmdResult with stdout, stderr and exit code
|
|
46
54
|
"""
|
|
47
55
|
if echo_command:
|
|
48
56
|
print(cmd) # noqa: T201
|
|
49
57
|
try:
|
|
50
|
-
|
|
58
|
+
if shell:
|
|
59
|
+
process = subprocess.run( # noqa: S602 # nosec
|
|
60
|
+
cmd, timeout=timeout, capture_output=capture_output, shell=True, check=False
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
process = subprocess.run( # noqa: S603 # nosec
|
|
64
|
+
shlex.split(cmd), timeout=timeout, capture_output=capture_output, shell=False, check=False
|
|
65
|
+
)
|
|
51
66
|
stdout = process.stdout.decode("utf-8", errors="replace") if capture_output else ""
|
|
52
67
|
stderr = process.stderr.decode("utf-8", errors="replace") if capture_output else ""
|
|
53
|
-
return
|
|
68
|
+
return CmdResult(stdout=stdout, stderr=stderr, code=process.returncode)
|
|
54
69
|
except subprocess.TimeoutExpired:
|
|
55
|
-
return
|
|
70
|
+
return CmdResult(stdout="", stderr="timeout", code=TIMEOUT_EXIT_CODE)
|
|
56
71
|
|
|
57
72
|
|
|
58
|
-
def
|
|
73
|
+
def run_ssh_cmd(
|
|
74
|
+
host: str,
|
|
75
|
+
cmd: str,
|
|
76
|
+
ssh_key_path: str | None = None,
|
|
77
|
+
timeout: int = 60,
|
|
78
|
+
echo_command: bool = False,
|
|
79
|
+
) -> CmdResult:
|
|
59
80
|
"""Execute a command on remote host via SSH.
|
|
60
81
|
|
|
61
82
|
Args:
|
|
@@ -66,10 +87,10 @@ def ssh_shell(host: str, cmd: str, ssh_key_path: str | None = None, timeout: int
|
|
|
66
87
|
echo_command: Whether to print the command before execution
|
|
67
88
|
|
|
68
89
|
Returns:
|
|
69
|
-
|
|
90
|
+
CmdResult with stdout, stderr and exit code
|
|
70
91
|
"""
|
|
71
92
|
ssh_cmd = "ssh -o 'StrictHostKeyChecking=no' -o 'LogLevel=ERROR'"
|
|
72
93
|
if ssh_key_path:
|
|
73
94
|
ssh_cmd += f" -i {shlex.quote(ssh_key_path)}"
|
|
74
95
|
ssh_cmd += f" {shlex.quote(host)} {shlex.quote(cmd)}"
|
|
75
|
-
return
|
|
96
|
+
return run_cmd(ssh_cmd, timeout=timeout, echo_command=echo_command)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
mm_std/__init__.py,sha256=6VZQzG9evSp0V1HES_OLkbVPP41-ziJ2AwBsSkF4N5M,719
|
|
2
|
+
mm_std/date_utils.py,sha256=aFdIacoNgDSPGeUkZihXZADd86TeHu4hr1uIT9zcqvw,1732
|
|
3
|
+
mm_std/dict_utils.py,sha256=Gq_LYuidf24SWvzNmUx8wVPGuop9263GOxcIflB__uQ,2850
|
|
4
|
+
mm_std/json_utils.py,sha256=SXrqNk5vlUxzJC8TE9yteMl8eTxBcWSQK63CHnQ3ZxI,3983
|
|
5
|
+
mm_std/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
mm_std/random_utils.py,sha256=3q5ZylCqfFHKN3VaDXuy4heo8C1jNbLHNNOFvyYJoZY,2253
|
|
7
|
+
mm_std/str_utils.py,sha256=I6vVC81dGBDTHm7FxW-ka5OlUPjHmgagei7Zjld65lk,1520
|
|
8
|
+
mm_std/subprocess_utils.py,sha256=PsrwPdpfPLozVIFbqBNgIAOIKpj0LAeVcq833YGcHbo,3212
|
|
9
|
+
mm_std-0.6.0.dist-info/METADATA,sha256=kGDjddlSMx1LnxwnME8L0qAS6dLmu8H7wOvxW8u0sxw,74
|
|
10
|
+
mm_std-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
+
mm_std-0.6.0.dist-info/RECORD,,
|
mm_std-0.5.3.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
mm_std/__init__.py,sha256=V1na3dOxX52p44hOwhHFaIgrZEHR3yEjFdmSIcB2n0c,715
|
|
2
|
-
mm_std/date_utils.py,sha256=aFdIacoNgDSPGeUkZihXZADd86TeHu4hr1uIT9zcqvw,1732
|
|
3
|
-
mm_std/dict_utils.py,sha256=GVegQXTIo3tzLGbBkiUSGTJkfaD5WWwz6OQnw9KcXlg,2275
|
|
4
|
-
mm_std/json_utils.py,sha256=3tOv2rowc9B18TpJyGSci1MvPEj5XogRy3qrJ1W_7Bg,4129
|
|
5
|
-
mm_std/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
mm_std/random_utils.py,sha256=x3uNuKjKY8GxYjbnOq0LU1pGXhI2wezpH2K-t9hrhfA,2225
|
|
7
|
-
mm_std/str_utils.py,sha256=I6vVC81dGBDTHm7FxW-ka5OlUPjHmgagei7Zjld65lk,1520
|
|
8
|
-
mm_std/subprocess_utils.py,sha256=6Bkw6ZYHT1NLIYbQUV9LGkBzUl5RtaVzMJiLJrTGq48,2507
|
|
9
|
-
mm_std-0.5.3.dist-info/METADATA,sha256=GbbfchD5CvEJq3n5ZMNJyNPwUwvfOd_bJxj7tc6iGqY,74
|
|
10
|
-
mm_std-0.5.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
-
mm_std-0.5.3.dist-info/RECORD,,
|