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 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 ShellResult, shell, ssh_shell # nosec
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
- "shell",
18
- "ssh_shell",
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, cast
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: DictType,
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
- ) -> DictType:
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 cast(DictType, result)
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, obj: Any) -> Any: # noqa: ANN401
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(obj, type_):
59
- return handler(obj)
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(obj) and not isinstance(obj, type):
63
- return asdict(obj) # Don't need recursive serialization
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(obj)
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)
@@ -6,14 +6,8 @@ TIMEOUT_EXIT_CODE = 255
6
6
 
7
7
 
8
8
  @dataclass
9
- class ShellResult:
10
- """Result of shell command execution.
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 shell(cmd: str, timeout: int | None = 60, capture_output: bool = True, echo_command: bool = False) -> ShellResult:
36
- """Execute a shell command.
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
- ShellResult with stdout, stderr and exit code
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
- process = subprocess.run(cmd, timeout=timeout, capture_output=capture_output, shell=True, check=False) # noqa: S602 # nosec
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 ShellResult(stdout=stdout, stderr=stderr, code=process.returncode)
68
+ return CmdResult(stdout=stdout, stderr=stderr, code=process.returncode)
54
69
  except subprocess.TimeoutExpired:
55
- return ShellResult(stdout="", stderr="timeout", code=TIMEOUT_EXIT_CODE)
70
+ return CmdResult(stdout="", stderr="timeout", code=TIMEOUT_EXIT_CODE)
56
71
 
57
72
 
58
- def ssh_shell(host: str, cmd: str, ssh_key_path: str | None = None, timeout: int = 60, echo_command: bool = False) -> ShellResult:
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
- ShellResult with stdout, stderr and exit code
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 shell(ssh_cmd, timeout=timeout, echo_command=echo_command)
96
+ return run_cmd(ssh_cmd, timeout=timeout, echo_command=echo_command)
@@ -1,4 +1,4 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-std
3
- Version: 0.5.3
3
+ Version: 0.6.0
4
4
  Requires-Python: >=3.13
@@ -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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,