mm-std 0.5.4__py3-none-any.whl → 0.7.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
@@ -1,24 +1,21 @@
1
- from .date_utils import parse_date, utc_delta, utc_now
2
- from .dict_utils import replace_empty_dict_entries
3
- from .json_utils import ExtendedJSONEncoder, json_dumps
4
- from .random_utils import random_datetime, random_decimal
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
1
+ """mm-std: Python utilities for common data manipulation tasks."""
7
2
 
8
- __all__ = [
9
- "ExtendedJSONEncoder",
10
- "ShellResult",
11
- "json_dumps",
12
- "parse_date",
13
- "parse_lines",
14
- "random_datetime",
15
- "random_decimal",
16
- "replace_empty_dict_entries",
17
- "shell",
18
- "ssh_shell",
19
- "str_contains_any",
20
- "str_ends_with_any",
21
- "str_starts_with_any",
22
- "utc_delta",
23
- "utc_now",
24
- ]
3
+ from .date_utils import parse_datetime as parse_datetime
4
+ from .date_utils import utc_from_timestamp as utc_from_timestamp
5
+ from .date_utils import utc_now as utc_now
6
+ from .date_utils import utc_now_offset as utc_now_offset
7
+ from .dict_utils import compact_dict as compact_dict
8
+ from .json_utils import ExtendedJSONEncoder as ExtendedJSONEncoder
9
+ from .json_utils import json_dumps as json_dumps
10
+ from .random_utils import random_datetime as random_datetime
11
+ from .random_utils import random_datetime_offset as random_datetime_offset
12
+ from .random_utils import random_decimal as random_decimal
13
+ from .str_utils import parse_lines as parse_lines
14
+ from .str_utils import str_contains_any as str_contains_any
15
+ from .str_utils import str_ends_with_any as str_ends_with_any
16
+ from .str_utils import str_starts_with_any as str_starts_with_any
17
+
18
+ # B404: re-exporting subprocess utilities with documented security considerations
19
+ from .subprocess_utils import CmdResult as CmdResult # nosec
20
+ from .subprocess_utils import run_cmd as run_cmd # nosec
21
+ from .subprocess_utils import run_ssh_cmd as run_ssh_cmd # nosec
mm_std/date_utils.py CHANGED
@@ -1,3 +1,5 @@
1
+ """UTC-focused datetime operations and flexible date parsing."""
2
+
1
3
  from datetime import UTC, datetime, timedelta
2
4
 
3
5
 
@@ -6,37 +8,38 @@ def utc_now() -> datetime:
6
8
  return datetime.now(UTC)
7
9
 
8
10
 
9
- def utc_delta(
10
- *,
11
- days: int | None = None,
12
- hours: int | None = None,
13
- minutes: int | None = None,
14
- seconds: int | None = None,
11
+ def utc_now_offset(
12
+ *, days: int | None = None, hours: int | None = None, minutes: int | None = None, seconds: int | None = None
15
13
  ) -> datetime:
16
14
  """Get UTC time shifted by the specified delta.
17
15
 
18
16
  Use negative values to get time in the past.
19
17
  """
20
18
  params = {}
21
- if days:
19
+ if days is not None:
22
20
  params["days"] = days
23
- if hours:
21
+ if hours is not None:
24
22
  params["hours"] = hours
25
- if minutes:
23
+ if minutes is not None:
26
24
  params["minutes"] = minutes
27
- if seconds:
25
+ if seconds is not None:
28
26
  params["seconds"] = seconds
29
27
  return datetime.now(UTC) + timedelta(**params)
30
28
 
31
29
 
32
- def parse_date(value: str, ignore_tz: bool = False) -> datetime:
30
+ def utc_from_timestamp(timestamp: float) -> datetime:
31
+ """Create UTC datetime from Unix timestamp."""
32
+ return datetime.fromtimestamp(timestamp, UTC)
33
+
34
+
35
+ def parse_datetime(date_str: str, ignore_tz: bool = False) -> datetime:
33
36
  """Parse date string in various formats, with timezone handling.
34
37
 
35
38
  Converts 'Z' suffix to '+00:00' for ISO format compatibility.
36
39
  Use ignore_tz=True to strip timezone info from the result.
37
40
  """
38
- if value.lower().endswith("z"):
39
- value = value[:-1] + "+00:00"
41
+ if date_str.lower().endswith("z"):
42
+ date_str = date_str[:-1] + "+00:00"
40
43
  date_formats = [
41
44
  "%Y-%m-%d %H:%M:%S.%f%z",
42
45
  "%Y-%m-%dT%H:%M:%S.%f%z",
@@ -53,10 +56,10 @@ def parse_date(value: str, ignore_tz: bool = False) -> datetime:
53
56
 
54
57
  for fmt in date_formats:
55
58
  try:
56
- dt = datetime.strptime(value, fmt) # noqa: DTZ007
59
+ dt = datetime.strptime(date_str, fmt) # noqa: DTZ007 - timezone deliberately ignored when ignore_tz=True
57
60
  if ignore_tz and dt.tzinfo is not None:
58
61
  dt = dt.replace(tzinfo=None)
59
- return dt # noqa: TRY300
62
+ return dt # noqa: TRY300 - return in try block is intentional for parse flow
60
63
  except ValueError:
61
64
  continue
62
- raise ValueError(f"Time data '{value}' does not match any known format.")
65
+ raise ValueError(f"Time data '{date_str}' does not match any known format.")
mm_std/dict_utils.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Dictionary manipulation utilities with type preservation."""
2
+
1
3
  from collections import OrderedDict, defaultdict
2
4
  from collections.abc import Mapping, MutableMapping
3
5
  from decimal import Decimal
@@ -8,8 +10,8 @@ V = TypeVar("V")
8
10
 
9
11
 
10
12
  @overload
11
- def replace_empty_dict_entries(
12
- data: defaultdict[K, V],
13
+ def compact_dict(
14
+ mapping: defaultdict[K, V],
13
15
  defaults: Mapping[K, V] | None = None,
14
16
  treat_zero_as_empty: bool = False,
15
17
  treat_false_as_empty: bool = False,
@@ -18,8 +20,8 @@ def replace_empty_dict_entries(
18
20
 
19
21
 
20
22
  @overload
21
- def replace_empty_dict_entries(
22
- data: OrderedDict[K, V],
23
+ def compact_dict(
24
+ mapping: OrderedDict[K, V],
23
25
  defaults: Mapping[K, V] | None = None,
24
26
  treat_zero_as_empty: bool = False,
25
27
  treat_false_as_empty: bool = False,
@@ -28,8 +30,8 @@ def replace_empty_dict_entries(
28
30
 
29
31
 
30
32
  @overload
31
- def replace_empty_dict_entries(
32
- data: dict[K, V],
33
+ def compact_dict(
34
+ mapping: dict[K, V],
33
35
  defaults: Mapping[K, V] | None = None,
34
36
  treat_zero_as_empty: bool = False,
35
37
  treat_false_as_empty: bool = False,
@@ -37,15 +39,14 @@ def replace_empty_dict_entries(
37
39
  ) -> dict[K, V]: ...
38
40
 
39
41
 
40
- def replace_empty_dict_entries(
41
- data: MutableMapping[K, V],
42
+ def compact_dict(
43
+ mapping: MutableMapping[K, V],
42
44
  defaults: Mapping[K, V] | None = None,
43
45
  treat_zero_as_empty: bool = False,
44
46
  treat_false_as_empty: bool = False,
45
47
  treat_empty_string_as_empty: bool = True,
46
48
  ) -> MutableMapping[K, V]:
47
- """
48
- Replace empty entries in a dictionary with defaults or remove them entirely.
49
+ """Replace empty entries in a dictionary with defaults or remove them entirely.
49
50
 
50
51
  Preserves the exact type of the input mapping:
51
52
  - dict[str, int] → dict[str, int]
@@ -53,7 +54,7 @@ def replace_empty_dict_entries(
53
54
  - OrderedDict[str, str] → OrderedDict[str, str]
54
55
 
55
56
  Args:
56
- data: The dictionary to process
57
+ mapping: The dictionary to process
57
58
  defaults: Default values to use for empty entries. If None or key not found, empty entries are removed
58
59
  treat_zero_as_empty: Treat 0 as empty value
59
60
  treat_false_as_empty: Treat False as empty value
@@ -61,16 +62,17 @@ def replace_empty_dict_entries(
61
62
 
62
63
  Returns:
63
64
  New dictionary of the same concrete type with empty entries replaced or removed
65
+
64
66
  """
65
67
  if defaults is None:
66
68
  defaults = {}
67
69
 
68
- if isinstance(data, defaultdict):
69
- result: MutableMapping[K, V] = defaultdict(data.default_factory)
70
+ if isinstance(mapping, defaultdict):
71
+ result: MutableMapping[K, V] = defaultdict(mapping.default_factory)
70
72
  else:
71
- result = data.__class__()
73
+ result = mapping.__class__()
72
74
 
73
- for key, value in data.items():
75
+ for key, value in mapping.items():
74
76
  should_replace = (
75
77
  value is None
76
78
  or (treat_false_as_empty and value is False)
mm_std/json_utils.py CHANGED
@@ -1,4 +1,4 @@
1
- from __future__ import annotations
1
+ """Extended JSON encoder with support for Python types."""
2
2
 
3
3
  import json
4
4
  from collections.abc import Callable
@@ -35,47 +35,50 @@ class ExtendedJSONEncoder(json.JSONEncoder):
35
35
  }
36
36
 
37
37
  @classmethod
38
- def register(cls, type_: type[Any], serializer: Callable[[Any], Any]) -> None:
38
+ def register(cls, type_: type[Any], handler: Callable[[Any], Any]) -> None:
39
39
  """Register a custom type with its serialization function.
40
40
 
41
41
  Args:
42
42
  type_: The type to register
43
- serializer: Function that converts objects of this type to JSON-serializable data
43
+ handler: Function that converts objects of this type to JSON-serializable data
44
44
 
45
45
  Raises:
46
46
  ValueError: If type_ is a built-in JSON type
47
+
47
48
  """
48
49
  if type_ in (str, int, float, bool, list, dict, type(None)):
49
50
  raise ValueError(f"Cannot override built-in JSON type: {type_.__name__}")
50
- cls._type_handlers[type_] = serializer
51
+ cls._type_handlers[type_] = handler
51
52
 
52
- def default(self, obj: Any) -> Any: # noqa: ANN401
53
+ def default(self, o: Any) -> Any: # noqa: ANN401 - Any required for generic JSON encoding
54
+ """Encode object to JSON-serializable format."""
53
55
  # Check registered type handlers first
54
56
  for type_, handler in self._type_handlers.items():
55
- if isinstance(obj, type_):
56
- return handler(obj)
57
+ if isinstance(o, type_):
58
+ return handler(o)
57
59
 
58
60
  # Special case: dataclasses (requires is_dataclass check, not isinstance)
59
- if is_dataclass(obj) and not isinstance(obj, type):
60
- return asdict(obj) # Don't need recursive serialization
61
+ if is_dataclass(o) and not isinstance(o, type):
62
+ return asdict(o) # Don't need recursive serialization
61
63
 
62
- return super().default(obj)
64
+ return super().default(o)
63
65
 
64
66
 
65
- def json_dumps(data: Any, type_handlers: dict[type[Any], Callable[[Any], Any]] | None = None, **kwargs: Any) -> str: # noqa: ANN401
67
+ def json_dumps(obj: Any, type_handlers: dict[type[Any], Callable[[Any], Any]] | None = None, **kwargs: Any) -> str: # noqa: ANN401 - Any required for generic type handler
66
68
  """Serialize object to JSON with extended type support.
67
69
 
68
70
  Unlike standard json.dumps, uses ExtendedJSONEncoder which automatically handles
69
71
  UUID, Decimal, Path, datetime, dataclasses, enums, pydantic models, and other Python types.
70
72
 
71
73
  Args:
72
- data: Object to serialize to JSON
74
+ obj: Object to serialize to JSON
73
75
  type_handlers: Optional additional type handlers for this call only.
74
76
  These handlers take precedence over default ones.
75
77
  **kwargs: Additional arguments passed to json.dumps
76
78
 
77
79
  Returns:
78
80
  JSON string representation
81
+
79
82
  """
80
83
  if type_handlers:
81
84
  # Type narrowing for mypy
@@ -83,7 +86,7 @@ def json_dumps(data: Any, type_handlers: dict[type[Any], Callable[[Any], Any]] |
83
86
 
84
87
  class TemporaryEncoder(ExtendedJSONEncoder):
85
88
  _type_handlers: ClassVar[dict[type[Any], Callable[[Any], Any]]] = {
86
- **ExtendedJSONEncoder._type_handlers, # noqa: SLF001
89
+ **ExtendedJSONEncoder._type_handlers, # noqa: SLF001 - accessing class internals for type handler inheritance
87
90
  **handlers,
88
91
  }
89
92
 
@@ -91,14 +94,14 @@ def json_dumps(data: Any, type_handlers: dict[type[Any], Callable[[Any], Any]] |
91
94
  else:
92
95
  encoder_cls = ExtendedJSONEncoder
93
96
 
94
- return json.dumps(data, cls=encoder_cls, **kwargs)
97
+ return json.dumps(obj, cls=encoder_cls, **kwargs)
95
98
 
96
99
 
97
100
  def _auto_register_optional_types() -> None:
98
101
  """Register handlers for optional dependencies if available."""
99
102
  # Pydantic models
100
103
  try:
101
- from pydantic import BaseModel # type: ignore[import-not-found] # noqa: PLC0415
104
+ from pydantic import BaseModel # noqa: PLC0415 - optional pydantic import at runtime
102
105
 
103
106
  ExtendedJSONEncoder.register(BaseModel, lambda obj: obj.model_dump())
104
107
  except ImportError:
mm_std/random_utils.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Type-safe random generation for decimals and datetimes."""
2
+
1
3
  import random
2
4
  from datetime import datetime, timedelta
3
5
  from decimal import Decimal
@@ -18,6 +20,7 @@ def random_decimal(from_value: Decimal, to_value: Decimal) -> Decimal:
18
20
 
19
21
  Raises:
20
22
  ValueError: If from_value > to_value
23
+
21
24
  """
22
25
  if from_value > to_value:
23
26
  raise ValueError("from_value must be <= to_value")
@@ -37,14 +40,33 @@ def random_decimal(from_value: Decimal, to_value: Decimal) -> Decimal:
37
40
  return Decimal(random_int) / Decimal(multiplier)
38
41
 
39
42
 
40
- def random_datetime(
41
- from_time: datetime,
42
- *,
43
- hours: int = 0,
44
- minutes: int = 0,
45
- seconds: int = 0,
46
- ) -> datetime:
47
- """Generate a random datetime within a specified time range.
43
+ def random_datetime(from_time: datetime, to_time: datetime) -> datetime:
44
+ """Generate a random datetime between from_time and to_time.
45
+
46
+ Args:
47
+ from_time: Minimum datetime (inclusive)
48
+ to_time: Maximum datetime (inclusive)
49
+
50
+ Returns:
51
+ Random datetime in the specified range
52
+
53
+ Raises:
54
+ ValueError: If from_time > to_time
55
+
56
+ """
57
+ if from_time > to_time:
58
+ raise ValueError("from_time must be <= to_time")
59
+
60
+ delta = (to_time - from_time).total_seconds()
61
+ if delta == 0:
62
+ return from_time
63
+
64
+ random_seconds = random.uniform(0, delta) # nosec B311
65
+ return from_time + timedelta(seconds=random_seconds)
66
+
67
+
68
+ def random_datetime_offset(from_time: datetime, *, hours: int = 0, minutes: int = 0, seconds: int = 0) -> datetime:
69
+ """Generate a random datetime within a specified offset from base time.
48
70
 
49
71
  Returns a random datetime between from_time and from_time + offset,
50
72
  where offset is calculated from the provided hours, minutes, and seconds.
@@ -60,13 +82,10 @@ def random_datetime(
60
82
 
61
83
  Raises:
62
84
  ValueError: If any offset value is negative
85
+
63
86
  """
64
87
  if hours < 0 or minutes < 0 or seconds < 0:
65
- raise ValueError("Range values must be non-negative")
88
+ raise ValueError("Offset values must be non-negative")
66
89
 
67
90
  total_seconds = hours * 3600 + minutes * 60 + seconds
68
- if total_seconds == 0:
69
- return from_time
70
-
71
- random_seconds = random.uniform(0, total_seconds) # nosec B311
72
- return from_time + timedelta(seconds=random_seconds)
91
+ return random_datetime(from_time, from_time + timedelta(seconds=total_seconds))
mm_std/str_utils.py CHANGED
@@ -1,3 +1,5 @@
1
+ """String matching utilities and multiline text parsing."""
2
+
1
3
  from collections.abc import Iterable
2
4
 
3
5
 
@@ -16,12 +18,7 @@ def str_contains_any(value: str, substrings: Iterable[str]) -> bool:
16
18
  return any(substring in value for substring in substrings)
17
19
 
18
20
 
19
- def parse_lines(
20
- text: str,
21
- lowercase: bool = False,
22
- remove_comments: bool = False,
23
- deduplicate: bool = False,
24
- ) -> list[str]:
21
+ def parse_lines(text: str, lowercase: bool = False, remove_comments: bool = False, deduplicate: bool = False) -> list[str]:
25
22
  """Parse multiline text into a list of cleaned lines.
26
23
 
27
24
  Args:
@@ -32,6 +29,7 @@ def parse_lines(
32
29
 
33
30
  Returns:
34
31
  List of non-empty, stripped lines after applying specified transformations
32
+
35
33
  """
36
34
  if lowercase:
37
35
  text = text.lower()
@@ -1,19 +1,16 @@
1
+ """Safe shell command execution with result handling."""
2
+
1
3
  import shlex
2
4
  import subprocess # nosec
3
5
  from dataclasses import dataclass
4
6
 
5
7
  TIMEOUT_EXIT_CODE = 255
8
+ """Exit code returned when command execution times out."""
6
9
 
7
10
 
8
11
  @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
- """
12
+ class CmdResult:
13
+ """Result of command execution."""
17
14
 
18
15
  stdout: str
19
16
  stderr: str
@@ -31,31 +28,66 @@ class ShellResult:
31
28
  result += self.stderr
32
29
  return result
33
30
 
31
+ @property
32
+ def is_success(self) -> bool:
33
+ """True if command completed successfully (exit code 0)."""
34
+ return self.code == 0
35
+
36
+ @property
37
+ def is_timeout(self) -> bool:
38
+ """True if command timed out."""
39
+ return self.code == TIMEOUT_EXIT_CODE
40
+
34
41
 
35
- def shell(cmd: str, timeout: int | None = 60, capture_output: bool = True, echo_command: bool = False) -> ShellResult:
36
- """Execute a shell command.
42
+ def run_cmd(
43
+ cmd: str, timeout: int | None = 60, capture_output: bool = True, echo_command: bool = False, shell: bool = False
44
+ ) -> CmdResult:
45
+ """Execute a command.
37
46
 
38
47
  Args:
39
48
  cmd: Command to execute
40
49
  timeout: Timeout in seconds, None for no timeout
41
50
  capture_output: Whether to capture stdout/stderr
42
51
  echo_command: Whether to print the command before execution
52
+ shell: If False (default), the command is parsed with shlex.split() and
53
+ executed without shell interpretation. Special characters like
54
+ backticks, $(), pipes (|), redirects (>, <), and wildcards (*) are
55
+ treated as literal text. This is the safe mode for commands with
56
+ user input.
57
+ If True, the command is passed to the shell as-is, enabling pipes,
58
+ redirects, command substitution, and other shell features. Use this
59
+ only for trusted commands that need shell functionality.
43
60
 
44
61
  Returns:
45
- ShellResult with stdout, stderr and exit code
62
+ CmdResult with stdout, stderr and exit code
63
+
46
64
  """
47
65
  if echo_command:
48
- print(cmd) # noqa: T201
66
+ print(cmd) # noqa: T201 - print is intentional for echo_command feature
49
67
  try:
50
- process = subprocess.run(cmd, timeout=timeout, capture_output=capture_output, shell=True, check=False) # noqa: S602 # nosec
68
+ if shell:
69
+ process = subprocess.run( # noqa: S602 # nosec - shell=True required for pipe support
70
+ cmd, timeout=timeout, capture_output=capture_output, shell=True, check=False
71
+ )
72
+ else:
73
+ process = subprocess.run( # noqa: S603 # nosec - subprocess with shell=False is safe
74
+ shlex.split(cmd), timeout=timeout, capture_output=capture_output, shell=False, check=False
75
+ )
51
76
  stdout = process.stdout.decode("utf-8", errors="replace") if capture_output else ""
52
77
  stderr = process.stderr.decode("utf-8", errors="replace") if capture_output else ""
53
- return ShellResult(stdout=stdout, stderr=stderr, code=process.returncode)
78
+ return CmdResult(stdout=stdout, stderr=stderr, code=process.returncode)
54
79
  except subprocess.TimeoutExpired:
55
- return ShellResult(stdout="", stderr="timeout", code=TIMEOUT_EXIT_CODE)
80
+ return CmdResult(stdout="", stderr="timeout", code=TIMEOUT_EXIT_CODE)
56
81
 
57
82
 
58
- def ssh_shell(host: str, cmd: str, ssh_key_path: str | None = None, timeout: int = 60, echo_command: bool = False) -> ShellResult:
83
+ def run_ssh_cmd(
84
+ host: str,
85
+ cmd: str,
86
+ ssh_key_path: str | None = None,
87
+ timeout: int = 60,
88
+ echo_command: bool = False,
89
+ strict_host_key_checking: bool | None = None,
90
+ ) -> CmdResult:
59
91
  """Execute a command on remote host via SSH.
60
92
 
61
93
  Args:
@@ -64,12 +96,18 @@ def ssh_shell(host: str, cmd: str, ssh_key_path: str | None = None, timeout: int
64
96
  ssh_key_path: Path to SSH private key file
65
97
  timeout: Timeout in seconds
66
98
  echo_command: Whether to print the command before execution
99
+ strict_host_key_checking: If True/False, explicitly set StrictHostKeyChecking.
100
+ If None, leave SSH defaults unchanged.
67
101
 
68
102
  Returns:
69
- ShellResult with stdout, stderr and exit code
103
+ CmdResult with stdout, stderr and exit code
104
+
70
105
  """
71
- ssh_cmd = "ssh -o 'StrictHostKeyChecking=no' -o 'LogLevel=ERROR'"
106
+ ssh_cmd = "ssh -o 'LogLevel=ERROR'"
107
+ if strict_host_key_checking is not None:
108
+ option_value = "yes" if strict_host_key_checking else "no"
109
+ ssh_cmd += f" -o 'StrictHostKeyChecking={option_value}'"
72
110
  if ssh_key_path:
73
111
  ssh_cmd += f" -i {shlex.quote(ssh_key_path)}"
74
112
  ssh_cmd += f" {shlex.quote(host)} {shlex.quote(cmd)}"
75
- return shell(ssh_cmd, timeout=timeout, echo_command=echo_command)
113
+ return run_cmd(ssh_cmd, timeout=timeout, echo_command=echo_command)
@@ -0,0 +1,4 @@
1
+ Metadata-Version: 2.4
2
+ Name: mm-std
3
+ Version: 0.7.0
4
+ Requires-Python: >=3.14
@@ -0,0 +1,11 @@
1
+ mm_std/__init__.py,sha256=7hmdftEHF19ef6jtwVDH3J7vEZLV3j5ZgIksZTLd_l0,1161
2
+ mm_std/date_utils.py,sha256=cGdTEXwsIQS6ZPTvjQRlkTTIf0yL2GCj_uYa8i0pzps,2117
3
+ mm_std/dict_utils.py,sha256=bddxRbemL7UZ9rp7k2bkBDsYX-NbVXv8CPL7ESjPxXc,2882
4
+ mm_std/json_utils.py,sha256=lOlleRdI9mdrB-s0nAfxiaQ5LTcbfEea2uObh0W-dzs,4196
5
+ mm_std/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ mm_std/random_utils.py,sha256=oGZhOg6rIft3SRpWv2lB6IAyvdWEN08-7BVrC5_vfoc,2889
7
+ mm_std/str_utils.py,sha256=p6hxxoe3xiuPSqb1vc3TbNfUoSwdW6z_OJG_3Qx7ITA,1563
8
+ mm_std/subprocess_utils.py,sha256=Sr8ENMg29CkhgpJi5PaW7h7tEviaEFurVJKcfR16itE,4057
9
+ mm_std-0.7.0.dist-info/METADATA,sha256=scnEov8zndpkmqWOgIde_iVjbb-arHejeuiKDt7Csbg,74
10
+ mm_std-0.7.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ mm_std-0.7.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,4 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: mm-std
3
- Version: 0.5.4
4
- Requires-Python: >=3.13
@@ -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=Gq_LYuidf24SWvzNmUx8wVPGuop9263GOxcIflB__uQ,2850
4
- mm_std/json_utils.py,sha256=NuDomTThfCkJBCmfQ6vkr7dvChKGsuLoAyLW0TON_dQ,3997
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=6Bkw6ZYHT1NLIYbQUV9LGkBzUl5RtaVzMJiLJrTGq48,2507
9
- mm_std-0.5.4.dist-info/METADATA,sha256=SnanvJ0gBE6Y_5-dSOkUaH5BVV482k3KPXFhuUKeV5Q,74
10
- mm_std-0.5.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- mm_std-0.5.4.dist-info/RECORD,,