mm-std 0.4.15__py3-none-any.whl → 0.4.17__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
@@ -10,14 +10,18 @@ from .concurrency.sync_decorators import synchronized_parameter as synchronized_
10
10
  from .concurrency.sync_scheduler import Scheduler as Scheduler
11
11
  from .concurrency.sync_task_runner import ConcurrentTasks as ConcurrentTasks
12
12
  from .config import BaseConfig as BaseConfig
13
- from .crypto import fernet_decrypt as fernet_decrypt
14
- from .crypto import fernet_encrypt as fernet_encrypt
15
- from .crypto import fernet_generate_key as fernet_generate_key
13
+ from .crypto.fernet import fernet_decrypt as fernet_decrypt
14
+ from .crypto.fernet import fernet_encrypt as fernet_encrypt
15
+ from .crypto.fernet import fernet_generate_key as fernet_generate_key
16
+ from .crypto.openssl import openssl_decrypt as openssl_decrypt
17
+ from .crypto.openssl import openssl_decrypt_base64 as openssl_decrypt_base64
18
+ from .crypto.openssl import openssl_encrypt as openssl_encrypt
19
+ from .crypto.openssl import openssl_encrypt_base64 as openssl_encrypt_base64
16
20
  from .date import parse_date as parse_date
17
21
  from .date import utc_delta as utc_delta
18
22
  from .date import utc_now as utc_now
19
23
  from .date import utc_random as utc_random
20
- from .dict import replace_empty_dict_values as replace_empty_dict_values
24
+ from .dict import replace_empty_dict_entries as replace_empty_dict_entries
21
25
  from .env import get_dotenv as get_dotenv
22
26
  from .http.http_request import http_request as http_request
23
27
  from .http.http_request_sync import http_request_sync as http_request_sync
File without changes
@@ -0,0 +1,207 @@
1
+ from base64 import b64decode, b64encode
2
+ from hashlib import pbkdf2_hmac
3
+ from os import urandom
4
+ from pathlib import Path
5
+
6
+ from cryptography.hazmat.primitives import padding
7
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
8
+
9
+ MAGIC = b"Salted__"
10
+ SALT_SIZE = 8
11
+ KEY_SIZE = 32 # for AES-256
12
+ IV_SIZE = 16
13
+
14
+
15
+ def openssl_encrypt(input_path: Path, output_path: Path, password: str, iterations: int = 1_000_000) -> None:
16
+ """
17
+ Encrypt a file using OpenSSL-compatible AES-256-CBC with PBKDF2 and output in binary format.
18
+
19
+ This function creates encrypted files that are fully compatible with OpenSSL command:
20
+ openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -in message.txt -out message.enc
21
+
22
+ Args:
23
+ input_path: Path to the input file to encrypt
24
+ output_path: Path where the encrypted binary file will be saved
25
+ password: Password for encryption
26
+ iterations: Number of PBKDF2 iterations (minimum 1000, default 1,000,000)
27
+
28
+ Raises:
29
+ ValueError: If iterations < 1000
30
+
31
+ Example:
32
+ >>> from pathlib import Path
33
+ >>> openssl_encrypt(Path("secret.txt"), Path("secret.enc"), "mypassword")
34
+
35
+ # Decrypt with OpenSSL:
36
+ # openssl enc -d -aes-256-cbc -pbkdf2 -iter 1000000 -in secret.enc -out secret_decrypted.txt -pass pass:mypassword
37
+ """
38
+ if iterations < 1000:
39
+ raise ValueError("Iteration count must be at least 1000 for security")
40
+
41
+ data: bytes = input_path.read_bytes()
42
+ salt: bytes = urandom(SALT_SIZE)
43
+
44
+ key_iv: bytes = pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, KEY_SIZE + IV_SIZE)
45
+ key: bytes = key_iv[:KEY_SIZE]
46
+ iv: bytes = key_iv[KEY_SIZE:]
47
+
48
+ padder = padding.PKCS7(algorithms.AES.block_size).padder()
49
+ padded_data: bytes = padder.update(data) + padder.finalize()
50
+
51
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
52
+ encryptor = cipher.encryptor()
53
+ ciphertext: bytes = encryptor.update(padded_data) + encryptor.finalize()
54
+
55
+ output_path.write_bytes(MAGIC + salt + ciphertext)
56
+
57
+
58
+ def openssl_decrypt(input_path: Path, output_path: Path, password: str, iterations: int = 1_000_000) -> None:
59
+ """
60
+ Decrypt a binary file created by OpenSSL-compatible AES-256-CBC with PBKDF2.
61
+
62
+ This function decrypts files that were encrypted with OpenSSL command:
63
+ openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -in message.txt -out message.enc
64
+
65
+ Args:
66
+ input_path: Path to the encrypted binary file
67
+ output_path: Path where the decrypted file will be saved
68
+ password: Password for decryption
69
+ iterations: Number of PBKDF2 iterations (minimum 1000, default 1,000,000)
70
+
71
+ Raises:
72
+ ValueError: If iterations < 1000, invalid file format, or wrong password
73
+
74
+ Example:
75
+ >>> from pathlib import Path
76
+ >>> openssl_decrypt(Path("secret.enc"), Path("secret_decrypted.txt"), "mypassword")
77
+
78
+ # Encrypt with OpenSSL:
79
+ # openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -in secret.txt -out secret.enc -pass pass:mypassword
80
+ """
81
+ if iterations < 1000:
82
+ raise ValueError("Iteration count must be at least 1000 for security")
83
+
84
+ raw: bytes = input_path.read_bytes()
85
+ if not raw.startswith(MAGIC):
86
+ raise ValueError("Invalid file format: missing OpenSSL Salted header")
87
+
88
+ salt: bytes = raw[8:16]
89
+ ciphertext: bytes = raw[16:]
90
+
91
+ key_iv: bytes = pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, KEY_SIZE + IV_SIZE)
92
+ key: bytes = key_iv[:KEY_SIZE]
93
+ iv: bytes = key_iv[KEY_SIZE:]
94
+
95
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
96
+ decryptor = cipher.decryptor()
97
+ padded_plaintext: bytes = decryptor.update(ciphertext) + decryptor.finalize()
98
+
99
+ unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
100
+ try:
101
+ plaintext: bytes = unpadder.update(padded_plaintext) + unpadder.finalize()
102
+ except ValueError as e:
103
+ raise ValueError("Decryption failed: invalid padding or wrong password") from e
104
+
105
+ output_path.write_bytes(plaintext)
106
+
107
+
108
+ def openssl_encrypt_base64(input_path: Path, output_path: Path, password: str, iterations: int = 1_000_000) -> None:
109
+ """
110
+ Encrypt a file using OpenSSL-compatible AES-256-CBC with PBKDF2 and output in base64 format.
111
+
112
+ This function creates encrypted files that are fully compatible with OpenSSL command:
113
+ openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -base64 -in message.txt -out message.enc
114
+
115
+ Args:
116
+ input_path: Path to the input file to encrypt
117
+ output_path: Path where the encrypted base64 file will be saved
118
+ password: Password for encryption
119
+ iterations: Number of PBKDF2 iterations (minimum 1000, default 1,000,000)
120
+
121
+ Raises:
122
+ ValueError: If iterations < 1000
123
+
124
+ Example:
125
+ >>> from pathlib import Path
126
+ >>> openssl_encrypt_base64(Path("secret.txt"), Path("secret.enc"), "mypassword")
127
+
128
+ # Decrypt with OpenSSL:
129
+ # openssl enc -d -aes-256-cbc -pbkdf2 -iter 1000000 -base64 -in secret.enc -out secret_decrypted.txt -pass pass:mypassword
130
+ """
131
+ if iterations < 1000:
132
+ raise ValueError("Iteration count must be at least 1000 for security")
133
+
134
+ data: bytes = input_path.read_bytes()
135
+ salt: bytes = urandom(SALT_SIZE)
136
+
137
+ key_iv: bytes = pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, KEY_SIZE + IV_SIZE)
138
+ key: bytes = key_iv[:KEY_SIZE]
139
+ iv: bytes = key_iv[KEY_SIZE:]
140
+
141
+ padder = padding.PKCS7(algorithms.AES.block_size).padder()
142
+ padded_data: bytes = padder.update(data) + padder.finalize()
143
+
144
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
145
+ encryptor = cipher.encryptor()
146
+ ciphertext: bytes = encryptor.update(padded_data) + encryptor.finalize()
147
+
148
+ # Encode binary data to base64
149
+ binary_data: bytes = MAGIC + salt + ciphertext
150
+ base64_data: str = b64encode(binary_data).decode("ascii")
151
+ output_path.write_text(base64_data)
152
+
153
+
154
+ def openssl_decrypt_base64(input_path: Path, output_path: Path, password: str, iterations: int = 1_000_000) -> None:
155
+ """
156
+ Decrypt a base64-encoded file created by OpenSSL-compatible AES-256-CBC with PBKDF2.
157
+
158
+ This function decrypts files that were encrypted with OpenSSL command:
159
+ openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -base64 -in message.txt -out message.enc
160
+
161
+ Args:
162
+ input_path: Path to the encrypted base64 file
163
+ output_path: Path where the decrypted file will be saved
164
+ password: Password for decryption
165
+ iterations: Number of PBKDF2 iterations (minimum 1000, default 1,000,000)
166
+
167
+ Raises:
168
+ ValueError: If iterations < 1000, invalid file format, or wrong password
169
+
170
+ Example:
171
+ >>> from pathlib import Path
172
+ >>> openssl_decrypt_base64(Path("secret.enc"), Path("secret_decrypted.txt"), "mypassword")
173
+
174
+ # Encrypt with OpenSSL:
175
+ # openssl enc -aes-256-cbc -pbkdf2 -iter 1000000 -salt -base64 -in secret.txt -out secret.enc -pass pass:mypassword
176
+ """
177
+ if iterations < 1000:
178
+ raise ValueError("Iteration count must be at least 1000 for security")
179
+
180
+ # Decode base64 to binary data
181
+ try:
182
+ base64_data: str = input_path.read_text().strip()
183
+ raw: bytes = b64decode(base64_data)
184
+ except Exception as e:
185
+ raise ValueError("Invalid base64 format") from e
186
+
187
+ if not raw.startswith(MAGIC):
188
+ raise ValueError("Invalid file format: missing OpenSSL Salted header")
189
+
190
+ salt: bytes = raw[8:16]
191
+ ciphertext: bytes = raw[16:]
192
+
193
+ key_iv: bytes = pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, KEY_SIZE + IV_SIZE)
194
+ key: bytes = key_iv[:KEY_SIZE]
195
+ iv: bytes = key_iv[KEY_SIZE:]
196
+
197
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
198
+ decryptor = cipher.decryptor()
199
+ padded_plaintext: bytes = decryptor.update(ciphertext) + decryptor.finalize()
200
+
201
+ unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
202
+ try:
203
+ plaintext: bytes = unpadder.update(padded_plaintext) + unpadder.finalize()
204
+ except ValueError as e:
205
+ raise ValueError("Decryption failed: invalid padding or wrong password") from e
206
+
207
+ output_path.write_bytes(plaintext)
mm_std/dict.py CHANGED
@@ -1,16 +1,49 @@
1
- def replace_empty_dict_values(
2
- data: dict[object, object],
3
- defaults: dict[object, object] | None = None,
1
+ from collections import defaultdict
2
+ from collections.abc import Mapping, MutableMapping
3
+ from typing import TypeVar, cast
4
+
5
+ K = TypeVar("K")
6
+ V = TypeVar("V")
7
+ DictType = TypeVar("DictType", bound=MutableMapping[K, V]) # type: ignore[valid-type]
8
+
9
+
10
+ def replace_empty_dict_entries(
11
+ data: DictType,
12
+ defaults: Mapping[K, V] | None = None,
4
13
  zero_is_empty: bool = False,
5
14
  false_is_empty: bool = False,
6
- ) -> dict[object, object]:
7
- """Replace empty values in a dictionary with provided default values, or remove them if no default exists."""
15
+ empty_string_is_empty: bool = True,
16
+ ) -> DictType:
17
+ """
18
+ Replace empty entries in a dictionary with provided default values,
19
+ or remove them if no default is available. Returns the same type as the input dictionary.
20
+ """
8
21
  if defaults is None:
9
22
  defaults = {}
10
- result = {}
23
+
24
+ try:
25
+ if isinstance(data, defaultdict):
26
+ result: MutableMapping[K, V] = defaultdict(data.default_factory)
27
+ else:
28
+ result = data.__class__()
29
+ except Exception:
30
+ result = {}
31
+
11
32
  for key, value in data.items():
12
- if value is None or value == "" or (zero_is_empty and value == 0) or (false_is_empty and value is False):
13
- value = defaults.get(key, None) # noqa: PLW2901
14
- if value is not None:
15
- result[key] = value
16
- return result
33
+ should_replace = (
34
+ value is None
35
+ or (empty_string_is_empty and value == "")
36
+ or (zero_is_empty and value == 0)
37
+ or (false_is_empty and value is False)
38
+ )
39
+
40
+ if should_replace:
41
+ if key in defaults:
42
+ new_value = defaults[key]
43
+ else:
44
+ continue # Skip the key if no default is available
45
+ else:
46
+ new_value = value
47
+
48
+ result[key] = new_value
49
+ return cast(DictType, result)
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-std
3
- Version: 0.4.15
3
+ Version: 0.4.17
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: aiohttp-socks~=0.10.1
6
- Requires-Dist: aiohttp~=3.11.18
7
- Requires-Dist: cryptography~=45.0.2
6
+ Requires-Dist: aiohttp~=3.12.2
7
+ Requires-Dist: cryptography~=45.0.3
8
8
  Requires-Dist: pydantic-settings>=2.9.1
9
- Requires-Dist: pydantic~=2.11.4
9
+ Requires-Dist: pydantic~=2.11.5
10
10
  Requires-Dist: pydash~=8.0.5
11
11
  Requires-Dist: python-dotenv~=1.1.0
12
12
  Requires-Dist: requests[socks]~=2.32.3
@@ -1,9 +1,8 @@
1
- mm_std/__init__.py,sha256=SlfY7KoSiv9YEkeBrjUksIUtLkkG17Ii-DYpC1TGphY,2886
1
+ mm_std/__init__.py,sha256=OpZAZw3BG3wyDnlRR3p2BZO2cZcE-imScPCf2qRWuUk,3189
2
2
  mm_std/command.py,sha256=ze286wjUjg0QSTgIu-2WZks53_Vclg69UaYYgPpQvCU,1283
3
3
  mm_std/config.py,sha256=3-FxFCtOffY2t9vuu9adojwbzTPwdAH2P6qDaZnHLbY,3206
4
- mm_std/crypto.py,sha256=jdk0_TCmeU0pPXMyz9xH6kQHSjjZ9GcGClBwQps5vBo,340
5
4
  mm_std/date.py,sha256=976eEkSONuNqHQBgSRu8hrtH23tJqztbmHFHLdbP2TY,1879
6
- mm_std/dict.py,sha256=6GkhJPXD0LiJDxPcYe6jPdEDw-MN7P7mKu6U5XxwYDk,675
5
+ mm_std/dict.py,sha256=DxFbZnl5KK4vJ4wLH_pCT0PWzfaIL3YyLNpRsVexfjw,1465
7
6
  mm_std/env.py,sha256=5zaR9VeIfObN-4yfgxoFeU5IM1GDeZZj9SuYf7t9sOA,125
8
7
  mm_std/fs.py,sha256=RwarNRJq3tIMG6LVX_g03hasfYpjYFh_O27oVDt5IPQ,291
9
8
  mm_std/json_.py,sha256=YVvROb5egcF1aQ2fXzyWG8Yw0JvYwpNBwtcBzsOADPo,1133
@@ -24,10 +23,13 @@ mm_std/concurrency/async_task_runner.py,sha256=EN7tN2enkVYVgDbhSiAr-_W4o9m9wBXCv
24
23
  mm_std/concurrency/sync_decorators.py,sha256=syCQBOmN7qPO55yzgJB2rbkh10CVww376hmyvs6e5tA,1080
25
24
  mm_std/concurrency/sync_scheduler.py,sha256=j4tBL_cBI1spr0cZplTA7N2CoYsznuORMeRN8rpR6gY,2407
26
25
  mm_std/concurrency/sync_task_runner.py,sha256=s5JPlLYLGQGHIxy4oDS-PN7O9gcy-yPZFoNm8RQwzcw,1780
26
+ mm_std/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ mm_std/crypto/fernet.py,sha256=jdk0_TCmeU0pPXMyz9xH6kQHSjjZ9GcGClBwQps5vBo,340
28
+ mm_std/crypto/openssl.py,sha256=x8LTRG6-jXLucMcsWS746V7B5fGrJ1c27d08rNK5-Ng,8173
27
29
  mm_std/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
30
  mm_std/http/http_request.py,sha256=6bg3t49c3dG0jKRFxhcceeYb5yKrMoZwuyb25zBG3tY,4088
29
31
  mm_std/http/http_request_sync.py,sha256=zXLeDplYWTFIwaD1Ydyg9yTi37WcI-fReLM0mVnuvhM,1835
30
32
  mm_std/http/http_response.py,sha256=7ZllZFPKJ9s6m-18Dfhrm7hwc2XFnyX7ppt0O8gNmlE,3916
31
- mm_std-0.4.15.dist-info/METADATA,sha256=D84z19NQ9EnTxan0l2149jLv-V8ZhMmLJrmkKM4ufRg,415
32
- mm_std-0.4.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
- mm_std-0.4.15.dist-info/RECORD,,
33
+ mm_std-0.4.17.dist-info/METADATA,sha256=0rG7NElzy9ZOLdx3hnag32xev6avidgXTZe46h85waU,414
34
+ mm_std-0.4.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
+ mm_std-0.4.17.dist-info/RECORD,,
File without changes