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.
Files changed (25) hide show
  1. {utilitz-0.9.2 → utilitz-0.9.4}/PKG-INFO +2 -2
  2. {utilitz-0.9.2 → utilitz-0.9.4}/README.md +1 -1
  3. {utilitz-0.9.2 → utilitz-0.9.4}/pyproject.toml +1 -1
  4. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/decryptor.py +77 -42
  5. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/encryptor.py +40 -22
  6. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/input.py +9 -5
  7. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/output.py +5 -6
  8. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/sys.py +19 -4
  9. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/windows/__init__.py +2 -0
  10. utilitz-0.9.4/src/utilitz/windows/context_menu.py +716 -0
  11. utilitz-0.9.4/src/utilitz/windows/environment.py +339 -0
  12. utilitz-0.9.2/src/utilitz/windows/context_menu.py +0 -345
  13. {utilitz-0.9.2 → utilitz-0.9.4}/.gitignore +0 -0
  14. {utilitz-0.9.2 → utilitz-0.9.4}/LICENSE +0 -0
  15. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/__init__.py +0 -0
  16. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/__init__.py +0 -0
  17. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/crypto/_utils.py +0 -0
  18. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/excel/__init__.py +0 -0
  19. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/excel/legacy.py +0 -0
  20. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/excel/reader.py +0 -0
  21. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/io.py +0 -0
  22. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/path.py +0 -0
  23. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/regex/__init__.py +0 -0
  24. {utilitz-0.9.2 → utilitz-0.9.4}/src/utilitz/regex/core.py +0 -0
  25. {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.2
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: **0.9.1**
39
+ Current release: see package metadata.
40
40
 
41
41
  ## 🚀 Installation
42
42
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  It is built with a modular approach, keeping the core library lightweight while providing powerful specialized modules through optional dependencies.
6
6
 
7
- Current release: **0.9.1**
7
+ Current release: see package metadata.
8
8
 
9
9
  ## 🚀 Installation
10
10
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "utilitz"
7
- version = "0.9.2"
7
+ version = "0.9.4"
8
8
  authors = [
9
9
  { name = "Artitzco", email = "artitzco@proton.me" },
10
10
  ]
@@ -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
- if self.decrypted_content is None:
240
- raise ValueError("No decrypted content has been generated.")
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
- encoding = self.metadata.get("encoding", "utf-8")
248
- return self.decrypted_content.decode(encoding)
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
- if self.decrypted_content is None:
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 = self.metadata.get("filename")
299
+ filename = metadata.get("filename")
273
300
  if not filename:
274
- raise ValueError(
275
- "No filename was stored in the decrypted metadata. "
276
- "Provide file_path explicitly."
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 = self.metadata.get("filename")
311
+ filename = metadata.get("filename")
283
312
  if not filename:
284
313
  raise ValueError(
285
- "No filename was stored in the decrypted metadata. "
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(self.decrypted_content)
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
- if self.decrypted_content is None:
324
- raise ValueError("No decrypted content has been generated.")
325
- if self.kind != "directory":
326
- raise ValueError("The decrypted content is not a directory archive.")
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(self.decrypted_content)
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(self.decrypted_content, temp_extract_dir)
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
- if self.decrypted_content is None:
380
- raise ValueError("No decrypted content has been generated.")
381
- if self.kind != "value":
382
- raise ValueError("The decrypted content is not a Python value.")
383
- if self.metadata.get("serializer") != "pickle":
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(self.decrypted_content)
416
+ return pickle.loads(content)
386
417
 
387
418
  def __str__(self) -> str:
388
- state = "idle" if self.content is None else "ready"
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
- "None"
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(content={content_part}, "
404
- f"decrypted_content={decrypted_part}, "
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 to_bytes(self) -> bytes:
208
- if self.output is None:
209
- raise ValueError("No encrypted output has been generated.")
210
- return self.output.to_bytes()
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 to_string(self, encoding: str = "utf-8") -> str:
213
- if self.output is None:
214
- raise ValueError("No encrypted output has been generated.")
215
- return self.output.to_string(encoding=encoding)
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 to_clipboard(self, encoding: str = "utf-8") -> str:
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 to the system clipboard as text.
234
+ Copy the encrypted output, or the input when no output exists, as text.
220
235
  """
221
- if self.output is None:
222
- raise ValueError("No encrypted output has been generated.")
223
- return self.output.to_clipboard(encoding=encoding)
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
- if self.output is None:
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
- state = "idle"
252
- if self.input is not None:
253
- state = "ready"
254
- if self.output is not None:
255
- state = f"{state}, encrypted"
256
- return f"Encryptor<{state}>"
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
- return f"CryptoInput<{self.kind}, {self.size} bytes>"
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
- f"CryptoOutput<{self.size} bytes, hash={self.content_hash[:12]}>"
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):
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from .context_menu import ContextMenu
4
+ from .environment import UserEnvironment
4
5
 
5
6
  __all__ = [
6
7
  "ContextMenu",
8
+ "UserEnvironment",
7
9
  ]