utilitz 0.9.2__tar.gz → 0.9.4__tar.gz
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.
- {utilitz-0.9.2 → utilitz-0.9.4}/PKG-INFO +2 -2
- {utilitz-0.9.2 → utilitz-0.9.4}/README.md +1 -1
- {utilitz-0.9.2 → utilitz-0.9.4}/pyproject.toml +1 -1
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/decryptor.py +77 -42
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/encryptor.py +40 -22
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/input.py +9 -5
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/output.py +5 -6
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/sys.py +19 -4
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/windows/__init__.py +2 -0
- utilitz-0.9.4/src/utilitz/windows/context_menu.py +716 -0
- utilitz-0.9.4/src/utilitz/windows/environment.py +339 -0
- utilitz-0.9.2/src/utilitz/windows/context_menu.py +0 -345
- {utilitz-0.9.2 → utilitz-0.9.4}/.gitignore +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/LICENSE +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/__init__.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/__init__.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/_utils.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/excel/__init__.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/excel/legacy.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/excel/reader.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/io.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/path.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/regex/__init__.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/regex/core.py +0 -0
- {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/regex/patterns.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: utilitz
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Simple Python utility functions to save and load data
|
|
5
5
|
Project-URL: Homepage, https://github.com/artitzco/utilitz
|
|
6
6
|
Project-URL: Issues, https://github.com/artitzco/utilitz/issues
|
|
@@ -36,7 +36,7 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
|
|
37
37
|
It is built with a modular approach, keeping the core library lightweight while providing powerful specialized modules through optional dependencies.
|
|
38
38
|
|
|
39
|
-
Current release:
|
|
39
|
+
Current release: see package metadata.
|
|
40
40
|
|
|
41
41
|
## 🚀 Installation
|
|
42
42
|
|
|
@@ -235,17 +235,45 @@ class Decryptor:
|
|
|
235
235
|
finally:
|
|
236
236
|
del password, password_bytes, parts, config, salt, ciphertext, kdf, key
|
|
237
237
|
|
|
238
|
+
def _portable_document(self) -> tuple[str | None, dict[str, Any], bytes] | None:
|
|
239
|
+
if self.content is None or not self.content.startswith(DOCUMENT_MAGIC + b":"):
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
payload = base64.urlsafe_b64decode(
|
|
244
|
+
self.content[len(DOCUMENT_MAGIC) + 1 :].strip()
|
|
245
|
+
)
|
|
246
|
+
document_header, document_content = _unpack_document_payload(payload)
|
|
247
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
248
|
+
raise ValueError("Invalid portable document format.") from exc
|
|
249
|
+
|
|
250
|
+
kind = document_header.get("kind")
|
|
251
|
+
metadata = document_header.get("metadata", {})
|
|
252
|
+
return kind, metadata if isinstance(metadata, dict) else {}, document_content
|
|
253
|
+
|
|
254
|
+
def _available_content(self) -> tuple[bytes, str | None, dict[str, Any], bool]:
|
|
255
|
+
if self.decrypted_content is not None:
|
|
256
|
+
return self.decrypted_content, self.kind, self.metadata, True
|
|
257
|
+
|
|
258
|
+
portable_document = self._portable_document()
|
|
259
|
+
if portable_document is not None:
|
|
260
|
+
kind, metadata, content = portable_document
|
|
261
|
+
return content, kind, metadata, True
|
|
262
|
+
|
|
263
|
+
if self.content is not None:
|
|
264
|
+
return self.content, None, {}, False
|
|
265
|
+
|
|
266
|
+
raise ValueError("No encrypted, portable, or decrypted content is available.")
|
|
267
|
+
|
|
238
268
|
def to_bytes(self) -> bytes:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return self.decrypted_content
|
|
269
|
+
content, _, _, _ = self._available_content()
|
|
270
|
+
return content
|
|
242
271
|
|
|
243
272
|
def to_string(self, encoding: str | None = None) -> str:
|
|
244
|
-
if self.decrypted_content is None:
|
|
245
|
-
raise ValueError("No decrypted content has been generated.")
|
|
246
273
|
if encoding is None:
|
|
247
|
-
|
|
248
|
-
|
|
274
|
+
_, _, metadata, _ = self._available_content()
|
|
275
|
+
encoding = metadata.get("encoding", "utf-8")
|
|
276
|
+
return self.to_bytes().decode(encoding)
|
|
249
277
|
|
|
250
278
|
def to_clipboard(self, encoding: str | None = None) -> str:
|
|
251
279
|
"""
|
|
@@ -265,24 +293,25 @@ class Decryptor:
|
|
|
265
293
|
If ``file_path`` is a directory, the stored filename is appended.
|
|
266
294
|
Set ``create_parent=True`` to create missing parent directories.
|
|
267
295
|
"""
|
|
268
|
-
|
|
269
|
-
raise ValueError("No decrypted content has been generated.")
|
|
296
|
+
content, _, metadata, has_document = self._available_content()
|
|
270
297
|
|
|
271
298
|
if file_path is None:
|
|
272
|
-
filename =
|
|
299
|
+
filename = metadata.get("filename")
|
|
273
300
|
if not filename:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
301
|
+
if has_document:
|
|
302
|
+
raise ValueError(
|
|
303
|
+
"No filename was stored in the document metadata. "
|
|
304
|
+
"Provide file_path explicitly."
|
|
305
|
+
)
|
|
306
|
+
raise ValueError("Provide file_path to write encrypted content.")
|
|
278
307
|
file_path = filename
|
|
279
308
|
|
|
280
309
|
path = os.path.abspath(os.path.expanduser(file_path))
|
|
281
310
|
if os.path.isdir(path):
|
|
282
|
-
filename =
|
|
311
|
+
filename = metadata.get("filename")
|
|
283
312
|
if not filename:
|
|
284
313
|
raise ValueError(
|
|
285
|
-
"No filename was stored in the
|
|
314
|
+
"No filename was stored in the document metadata. "
|
|
286
315
|
"Provide file_path explicitly."
|
|
287
316
|
)
|
|
288
317
|
path = os.path.join(path, filename)
|
|
@@ -302,7 +331,7 @@ class Decryptor:
|
|
|
302
331
|
)
|
|
303
332
|
|
|
304
333
|
with open(path, "wb") as file_obj:
|
|
305
|
-
file_obj.write(
|
|
334
|
+
file_obj.write(content)
|
|
306
335
|
|
|
307
336
|
return path
|
|
308
337
|
|
|
@@ -320,13 +349,14 @@ class Decryptor:
|
|
|
320
349
|
root folder. Set ``exact_path=True`` to treat it as the final folder
|
|
321
350
|
path, and ``create_parent=True`` to create missing parent directories.
|
|
322
351
|
"""
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
352
|
+
content, kind, _, has_document = self._available_content()
|
|
353
|
+
if not has_document:
|
|
354
|
+
raise ValueError("No decrypted or portable document content has been generated.")
|
|
355
|
+
if kind != "directory":
|
|
356
|
+
raise ValueError("The document content is not a directory archive.")
|
|
327
357
|
|
|
328
358
|
try:
|
|
329
|
-
root_name = _contains_single_root(
|
|
359
|
+
root_name = _contains_single_root(content)
|
|
330
360
|
except zipfile.BadZipFile as exc:
|
|
331
361
|
raise ValueError("Invalid directory archive.") from exc
|
|
332
362
|
|
|
@@ -364,7 +394,7 @@ class Decryptor:
|
|
|
364
394
|
|
|
365
395
|
temp_extract_dir = tempfile.mkdtemp(prefix=".tmp_extract_")
|
|
366
396
|
try:
|
|
367
|
-
_safe_extract_zip(
|
|
397
|
+
_safe_extract_zip(content, temp_extract_dir)
|
|
368
398
|
|
|
369
399
|
extracted_root = os.path.join(temp_extract_dir, root_name)
|
|
370
400
|
if not os.path.isdir(extracted_root):
|
|
@@ -376,31 +406,36 @@ class Decryptor:
|
|
|
376
406
|
return final_dir_path
|
|
377
407
|
|
|
378
408
|
def to_value(self) -> Any:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
409
|
+
content, kind, metadata, has_document = self._available_content()
|
|
410
|
+
if not has_document:
|
|
411
|
+
raise ValueError("No decrypted or portable document content has been generated.")
|
|
412
|
+
if kind != "value":
|
|
413
|
+
raise ValueError("The document content is not a Python value.")
|
|
414
|
+
if metadata.get("serializer") != "pickle":
|
|
384
415
|
raise ValueError("Unsupported value serializer.")
|
|
385
|
-
return pickle.loads(
|
|
416
|
+
return pickle.loads(content)
|
|
386
417
|
|
|
387
418
|
def __str__(self) -> str:
|
|
388
|
-
|
|
389
|
-
if self.decrypted_content is not None:
|
|
390
|
-
state = f"{state}, decrypted"
|
|
391
|
-
return f"Decryptor<{state}>"
|
|
392
|
-
|
|
393
|
-
def __repr__(self) -> str:
|
|
394
|
-
if self.content is None and self.decrypted_content is None:
|
|
395
|
-
return "Decryptor(content=None, decrypted_content=None)"
|
|
396
|
-
content_part = "None" if self.content is None else f"{len(self.content)} bytes"
|
|
419
|
+
content_part = "none" if self.content is None else f"{len(self.content)} bytes"
|
|
397
420
|
decrypted_part = (
|
|
398
|
-
"
|
|
421
|
+
"none"
|
|
399
422
|
if self.decrypted_content is None
|
|
400
423
|
else f"{len(self.decrypted_content)} bytes"
|
|
401
424
|
)
|
|
425
|
+
metadata_keys = ", ".join(sorted(self.metadata)) or "none"
|
|
426
|
+
return (
|
|
427
|
+
"Decryptor\n"
|
|
428
|
+
f" has_content: {self.has_content}\n"
|
|
429
|
+
f" content: {content_part}\n"
|
|
430
|
+
f" has_decrypted_content: {self.has_decrypted_content}\n"
|
|
431
|
+
f" decrypted_content: {decrypted_part}\n"
|
|
432
|
+
f" kind: {self.kind}\n"
|
|
433
|
+
f" metadata keys: {metadata_keys}\n"
|
|
434
|
+
f" key_env_varname: {self.key_env_varname}"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def __repr__(self) -> str:
|
|
402
438
|
return (
|
|
403
|
-
f"Decryptor(
|
|
404
|
-
f"
|
|
405
|
-
f"kind={self.kind!r})"
|
|
439
|
+
f"Decryptor(has_content={self.has_content}, "
|
|
440
|
+
f"has_decrypted_content={self.has_decrypted_content}, kind={self.kind!r})"
|
|
406
441
|
)
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from .decryptor import Decryptor
|
|
9
9
|
from .input import CryptoInput
|
|
10
|
+
from . import _utils
|
|
10
11
|
from ._utils import (
|
|
11
12
|
CRYPT_MAGIC,
|
|
12
13
|
DOCUMENT_MAGIC,
|
|
@@ -204,23 +205,37 @@ class Encryptor:
|
|
|
204
205
|
finally:
|
|
205
206
|
del password, password_bytes, salt, kdf, key, document_payload, ciphertext, config, encrypted_content
|
|
206
207
|
|
|
207
|
-
def
|
|
208
|
-
if self.output is None:
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
def _exportable_content(self) -> bytes:
|
|
209
|
+
if self.output is not None:
|
|
210
|
+
return self.output.to_bytes()
|
|
211
|
+
if self.input is not None:
|
|
212
|
+
return self.input.to_bytes()
|
|
213
|
+
raise ValueError("No crypto input or encrypted output is available.")
|
|
211
214
|
|
|
212
|
-
def
|
|
213
|
-
if self.output is None:
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
def _portable_content(self) -> bytes:
|
|
216
|
+
if self.output is not None:
|
|
217
|
+
return self.output.to_bytes()
|
|
218
|
+
if self.input is not None:
|
|
219
|
+
return DOCUMENT_MAGIC + b":" + base64.urlsafe_b64encode(
|
|
220
|
+
_build_document_payload(self.input)
|
|
221
|
+
)
|
|
222
|
+
raise ValueError("No crypto input or encrypted output is available.")
|
|
223
|
+
|
|
224
|
+
def to_bytes(self, *, portable: bool = False) -> bytes:
|
|
225
|
+
if portable:
|
|
226
|
+
return self._portable_content()
|
|
227
|
+
return self._exportable_content()
|
|
216
228
|
|
|
217
|
-
def
|
|
229
|
+
def to_string(self, encoding: str = "utf-8", *, portable: bool = False) -> str:
|
|
230
|
+
return self.to_bytes(portable=portable).decode(encoding)
|
|
231
|
+
|
|
232
|
+
def to_clipboard(self, encoding: str = "utf-8", *, portable: bool = False) -> str:
|
|
218
233
|
"""
|
|
219
|
-
Copy the encrypted output
|
|
234
|
+
Copy the encrypted output, or the input when no output exists, as text.
|
|
220
235
|
"""
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
236
|
+
return _utils.copy_text_to_clipboard(
|
|
237
|
+
self.to_string(encoding=encoding, portable=portable)
|
|
238
|
+
)
|
|
224
239
|
|
|
225
240
|
def to_file(
|
|
226
241
|
self,
|
|
@@ -229,10 +244,9 @@ class Encryptor:
|
|
|
229
244
|
encoding: str = "utf-8",
|
|
230
245
|
overwrite: bool = False,
|
|
231
246
|
binary: bool = False,
|
|
247
|
+
portable: bool = False,
|
|
232
248
|
) -> str:
|
|
233
|
-
|
|
234
|
-
raise ValueError("No encrypted output has been generated.")
|
|
235
|
-
return self.output.to_file(
|
|
249
|
+
return CryptoOutput.create(self.to_bytes(portable=portable)).to_file(
|
|
236
250
|
file_path,
|
|
237
251
|
encoding=encoding,
|
|
238
252
|
overwrite=overwrite,
|
|
@@ -248,9 +262,13 @@ class Encryptor:
|
|
|
248
262
|
return f"Encryptor(has_input={self.has_input}, has_output={self.has_output})"
|
|
249
263
|
|
|
250
264
|
def __str__(self) -> str:
|
|
251
|
-
|
|
252
|
-
if self.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
265
|
+
input_part = "none" if self.input is None else f"{self.input.kind}, {self.input.size} bytes"
|
|
266
|
+
output_part = "none" if self.output is None else f"{self.output.size} bytes"
|
|
267
|
+
return (
|
|
268
|
+
"Encryptor\n"
|
|
269
|
+
f" has_input: {self.has_input}\n"
|
|
270
|
+
f" input: {input_part}\n"
|
|
271
|
+
f" has_output: {self.has_output}\n"
|
|
272
|
+
f" output: {output_part}\n"
|
|
273
|
+
f" key_env_varname: {self.key_env_varname}"
|
|
274
|
+
)
|
|
@@ -136,13 +136,17 @@ class CryptoInput:
|
|
|
136
136
|
return self.content
|
|
137
137
|
|
|
138
138
|
def __repr__(self) -> str:
|
|
139
|
-
return (
|
|
140
|
-
f"CryptoInput(kind={self.kind!r}, size={self.size}, "
|
|
141
|
-
f"content_hash={self.content_hash!r}, metadata={self.metadata!r})"
|
|
142
|
-
)
|
|
139
|
+
return f"CryptoInput(kind={self.kind!r}, size={self.size}, hash={self.content_hash[:12]!r})"
|
|
143
140
|
|
|
144
141
|
def __str__(self) -> str:
|
|
145
|
-
|
|
142
|
+
metadata_keys = ", ".join(sorted(self.metadata)) or "none"
|
|
143
|
+
return (
|
|
144
|
+
"CryptoInput\n"
|
|
145
|
+
f" kind: {self.kind}\n"
|
|
146
|
+
f" size: {self.size} bytes\n"
|
|
147
|
+
f" hash: {self.content_hash[:12]}\n"
|
|
148
|
+
f" metadata keys: {metadata_keys}"
|
|
149
|
+
)
|
|
146
150
|
|
|
147
151
|
@classmethod
|
|
148
152
|
def from_string(
|
|
@@ -78,13 +78,12 @@ class CryptoOutput:
|
|
|
78
78
|
return path
|
|
79
79
|
|
|
80
80
|
def __repr__(self) -> str:
|
|
81
|
-
return (
|
|
82
|
-
f"CryptoOutput(size={self.size}, "
|
|
83
|
-
f"created_at={self.created_at.isoformat()!r}, "
|
|
84
|
-
f"content_hash={self.content_hash!r})"
|
|
85
|
-
)
|
|
81
|
+
return f"CryptoOutput(size={self.size}, hash={self.content_hash[:12]!r})"
|
|
86
82
|
|
|
87
83
|
def __str__(self) -> str:
|
|
88
84
|
return (
|
|
89
|
-
|
|
85
|
+
"CryptoOutput\n"
|
|
86
|
+
f" size: {self.size} bytes\n"
|
|
87
|
+
f" created_at: {self.created_at.isoformat()}\n"
|
|
88
|
+
f" hash: {self.content_hash[:12]}"
|
|
90
89
|
)
|
|
@@ -16,7 +16,7 @@ from datetime import datetime
|
|
|
16
16
|
import time
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class MonitorActivity:
|
|
19
|
+
class MonitorActivity:
|
|
20
20
|
|
|
21
21
|
def __init__(self):
|
|
22
22
|
_check_gui()
|
|
@@ -50,9 +50,24 @@ class MonitorActivity:
|
|
|
50
50
|
self._keyboard_listener.start()
|
|
51
51
|
self._mouse_listener.start()
|
|
52
52
|
|
|
53
|
-
def stop(self):
|
|
54
|
-
self._keyboard_listener.stop()
|
|
55
|
-
self._mouse_listener.stop()
|
|
53
|
+
def stop(self):
|
|
54
|
+
self._keyboard_listener.stop()
|
|
55
|
+
self._mouse_listener.stop()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def inactive_seconds(self):
|
|
59
|
+
return time.monotonic() - self.last_activity_time
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return f"MonitorActivity(inactive_seconds={self.inactive_seconds:.3f})"
|
|
63
|
+
|
|
64
|
+
def __str__(self):
|
|
65
|
+
return (
|
|
66
|
+
"MonitorActivity\n"
|
|
67
|
+
f" inactive_seconds: {self.inactive_seconds:.3f}\n"
|
|
68
|
+
f" keyboard_listener: {type(self._keyboard_listener).__name__}\n"
|
|
69
|
+
f" mouse_listener: {type(self._mouse_listener).__name__}"
|
|
70
|
+
)
|
|
56
71
|
|
|
57
72
|
|
|
58
73
|
def monitor_keep_alive(seconds, key='ctrl', verbose=1, tolerance=0.1):
|