mm-std 0.5.1__py3-none-any.whl → 0.5.3__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
@@ -2,15 +2,20 @@ from .date_utils import parse_date, utc_delta, utc_now
2
2
  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
- from .str_utils import str_contains_any, str_ends_with_any, str_starts_with_any
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
7
 
7
8
  __all__ = [
8
9
  "ExtendedJSONEncoder",
10
+ "ShellResult",
9
11
  "json_dumps",
10
12
  "parse_date",
13
+ "parse_lines",
11
14
  "random_datetime",
12
15
  "random_decimal",
13
16
  "replace_empty_dict_entries",
17
+ "shell",
18
+ "ssh_shell",
14
19
  "str_contains_any",
15
20
  "str_ends_with_any",
16
21
  "str_starts_with_any",
mm_std/str_utils.py CHANGED
@@ -14,3 +14,32 @@ def str_ends_with_any(value: str, suffixes: Iterable[str]) -> bool:
14
14
  def str_contains_any(value: str, substrings: Iterable[str]) -> bool:
15
15
  """Check if string contains any of the given substrings."""
16
16
  return any(substring in value for substring in substrings)
17
+
18
+
19
+ def parse_lines(
20
+ text: str,
21
+ lowercase: bool = False,
22
+ remove_comments: bool = False,
23
+ deduplicate: bool = False,
24
+ ) -> list[str]:
25
+ """Parse multiline text into a list of cleaned lines.
26
+
27
+ Args:
28
+ text: Input text to parse
29
+ lowercase: Convert all lines to lowercase
30
+ remove_comments: Remove everything after '#' character in each line
31
+ deduplicate: Remove duplicate lines while preserving order
32
+
33
+ Returns:
34
+ List of non-empty, stripped lines after applying specified transformations
35
+ """
36
+ if lowercase:
37
+ text = text.lower()
38
+ result = [line.strip() for line in text.split("\n") if line.strip()]
39
+ if remove_comments:
40
+ result = [line.split("#")[0].strip() for line in result]
41
+ result = [line for line in result if line]
42
+ if deduplicate:
43
+ result = list(dict.fromkeys(result))
44
+
45
+ return result
@@ -0,0 +1,75 @@
1
+ import shlex
2
+ import subprocess # nosec
3
+ from dataclasses import dataclass
4
+
5
+ TIMEOUT_EXIT_CODE = 255
6
+
7
+
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
+ """
17
+
18
+ stdout: str
19
+ stderr: str
20
+ code: int
21
+
22
+ @property
23
+ def combined_output(self) -> str:
24
+ """Combined stdout and stderr output."""
25
+ result = ""
26
+ if self.stdout:
27
+ result += self.stdout
28
+ if self.stderr:
29
+ if result:
30
+ result += "\n"
31
+ result += self.stderr
32
+ return result
33
+
34
+
35
+ def shell(cmd: str, timeout: int | None = 60, capture_output: bool = True, echo_command: bool = False) -> ShellResult:
36
+ """Execute a shell command.
37
+
38
+ Args:
39
+ cmd: Command to execute
40
+ timeout: Timeout in seconds, None for no timeout
41
+ capture_output: Whether to capture stdout/stderr
42
+ echo_command: Whether to print the command before execution
43
+
44
+ Returns:
45
+ ShellResult with stdout, stderr and exit code
46
+ """
47
+ if echo_command:
48
+ print(cmd) # noqa: T201
49
+ try:
50
+ process = subprocess.run(cmd, timeout=timeout, capture_output=capture_output, shell=True, check=False) # noqa: S602 # nosec
51
+ stdout = process.stdout.decode("utf-8", errors="replace") if capture_output else ""
52
+ stderr = process.stderr.decode("utf-8", errors="replace") if capture_output else ""
53
+ return ShellResult(stdout=stdout, stderr=stderr, code=process.returncode)
54
+ except subprocess.TimeoutExpired:
55
+ return ShellResult(stdout="", stderr="timeout", code=TIMEOUT_EXIT_CODE)
56
+
57
+
58
+ def ssh_shell(host: str, cmd: str, ssh_key_path: str | None = None, timeout: int = 60, echo_command: bool = False) -> ShellResult:
59
+ """Execute a command on remote host via SSH.
60
+
61
+ Args:
62
+ host: Remote host to connect to
63
+ cmd: Command to execute on remote host
64
+ ssh_key_path: Path to SSH private key file
65
+ timeout: Timeout in seconds
66
+ echo_command: Whether to print the command before execution
67
+
68
+ Returns:
69
+ ShellResult with stdout, stderr and exit code
70
+ """
71
+ ssh_cmd = "ssh -o 'StrictHostKeyChecking=no' -o 'LogLevel=ERROR'"
72
+ if ssh_key_path:
73
+ ssh_cmd += f" -i {shlex.quote(ssh_key_path)}"
74
+ ssh_cmd += f" {shlex.quote(host)} {shlex.quote(cmd)}"
75
+ return shell(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.1
3
+ Version: 0.5.3
4
4
  Requires-Python: >=3.13
@@ -0,0 +1,11 @@
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,,
@@ -1,10 +0,0 @@
1
- mm_std/__init__.py,sha256=n464fjIQFV1uGqpDFkcDnyYcSz4IzhFz5FCVPINK160,565
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=Mn6AJzYTRZgxgtDwZGSnsm1CV0aL6IdvO0TNCTDydMU,631
8
- mm_std-0.5.1.dist-info/METADATA,sha256=ys0mlpNRxc1SifL57cRqBhlDVXmZPg76vbmz31vzbu4,74
9
- mm_std-0.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- mm_std-0.5.1.dist-info/RECORD,,
File without changes