winipedia-utils 0.1.39__tar.gz → 0.1.41__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 (100) hide show
  1. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/PKG-INFO +1 -1
  2. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/pyproject.toml +1 -1
  3. winipedia_utils-0.1.41/winipedia_utils/pyside/core/py_qiodevice.py +476 -0
  4. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/base/base.py +70 -12
  5. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/pages/base/base.py +19 -3
  6. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/pages/browser.py +9 -3
  7. winipedia_utils-0.1.41/winipedia_utils/pyside/ui/pages/player.py +89 -0
  8. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/widgets/browser.py +96 -19
  9. winipedia_utils-0.1.41/winipedia_utils/pyside/ui/widgets/clickable_widget.py +57 -0
  10. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/widgets/media_player.py +186 -37
  11. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/widgets/notification.py +29 -8
  12. winipedia_utils-0.1.39/winipedia_utils/pyside/core/py_qiodevice.py +0 -93
  13. winipedia_utils-0.1.39/winipedia_utils/pyside/ui/pages/player.py +0 -27
  14. winipedia_utils-0.1.39/winipedia_utils/pyside/ui/widgets/clickable_widget.py +0 -29
  15. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/LICENSE +0 -0
  16. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/README.md +0 -0
  17. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/__init__.py +0 -0
  18. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/concurrent/__init__.py +0 -0
  19. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/concurrent/concurrent.py +0 -0
  20. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/concurrent/multiprocessing.py +0 -0
  21. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/concurrent/multithreading.py +0 -0
  22. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/consts.py +0 -0
  23. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/data/__init__.py +0 -0
  24. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/data/dataframe.py +0 -0
  25. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/django/__init__.py +0 -0
  26. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/django/bulk.py +0 -0
  27. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/django/command.py +0 -0
  28. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/django/database.py +0 -0
  29. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/git/__init__.py +0 -0
  30. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/git/gitignore/__init__.py +0 -0
  31. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/git/gitignore/gitignore.py +0 -0
  32. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/git/pre_commit/__init__.py +0 -0
  33. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/git/pre_commit/config.py +0 -0
  34. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/git/pre_commit/hooks.py +0 -0
  35. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/git/pre_commit/run_hooks.py +0 -0
  36. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/iterating/__init__.py +0 -0
  37. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/iterating/iterate.py +0 -0
  38. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/logging/__init__.py +0 -0
  39. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/logging/ansi.py +0 -0
  40. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/logging/config.py +0 -0
  41. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/logging/logger.py +0 -0
  42. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/modules/__init__.py +0 -0
  43. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/modules/class_.py +0 -0
  44. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/modules/function.py +0 -0
  45. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/modules/module.py +0 -0
  46. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/modules/package.py +0 -0
  47. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/oop/__init__.py +0 -0
  48. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/oop/mixins/__init__.py +0 -0
  49. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/oop/mixins/meta.py +0 -0
  50. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/oop/mixins/mixin.py +0 -0
  51. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/os/__init__.py +0 -0
  52. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/os/os.py +0 -0
  53. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/projects/__init__.py +0 -0
  54. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/projects/poetry/__init__.py +0 -0
  55. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/projects/poetry/config.py +0 -0
  56. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/projects/poetry/poetry.py +0 -0
  57. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/projects/project.py +0 -0
  58. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/py.typed +0 -0
  59. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/__init__.py +0 -0
  60. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/core/__init__.py +0 -0
  61. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/__init__.py +0 -0
  62. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/base/__init__.py +0 -0
  63. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/pages/__init__.py +0 -0
  64. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/pages/base/__init__.py +0 -0
  65. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/widgets/__init__.py +0 -0
  66. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/windows/__init__.py +0 -0
  67. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/windows/base/__init__.py +0 -0
  68. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/pyside/ui/windows/base/base.py +0 -0
  69. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/resources/__init__.py +0 -0
  70. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/resources/svgs/__init__.py +0 -0
  71. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/resources/svgs/download_arrow.svg +0 -0
  72. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/resources/svgs/exit_fullscreen_icon.svg +0 -0
  73. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/resources/svgs/fullscreen_icon.svg +0 -0
  74. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/resources/svgs/pause_icon.svg +0 -0
  75. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/resources/svgs/play_icon.svg +0 -0
  76. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/resources/svgs/svg.py +0 -0
  77. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/security/__init__.py +0 -0
  78. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/security/cryptography.py +0 -0
  79. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/security/keyring.py +0 -0
  80. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/setup.py +0 -0
  81. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/__init__.py +0 -0
  82. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/assertions.py +0 -0
  83. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/convention.py +0 -0
  84. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/create_tests.py +0 -0
  85. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/fixtures.py +0 -0
  86. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/__init__.py +0 -0
  87. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/__init__.py +0 -0
  88. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/fixtures/__init__.py +0 -0
  89. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/fixtures/fixture.py +0 -0
  90. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/fixtures/scopes/__init__.py +0 -0
  91. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +0 -0
  92. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/fixtures/scopes/function.py +0 -0
  93. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/fixtures/scopes/module.py +0 -0
  94. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/fixtures/scopes/package.py +0 -0
  95. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/fixtures/scopes/session.py +0 -0
  96. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/utils/__init__.py +0 -0
  97. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/base/utils/utils.py +0 -0
  98. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/testing/tests/conftest.py +0 -0
  99. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/text/__init__.py +0 -0
  100. {winipedia_utils-0.1.39 → winipedia_utils-0.1.41}/winipedia_utils/text/string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: winipedia-utils
3
- Version: 0.1.39
3
+ Version: 0.1.41
4
4
  Summary: A package with many utility functions
5
5
  License: MIT
6
6
  Author: Winipedia
@@ -1,7 +1,7 @@
1
1
  # Project section
2
2
  [project]
3
3
  name = "winipedia-utils"
4
- version = "0.1.39"
4
+ version = "0.1.41"
5
5
  description = "A package with many utility functions"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.12,<3.14"
@@ -0,0 +1,476 @@
1
+ """PySide6 QIODevice wrapper."""
2
+
3
+ import os
4
+ from collections.abc import Generator
5
+ from functools import partial
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
10
+ from PySide6.QtCore import QFile, QIODevice
11
+
12
+
13
+ class PyQIODevice(QIODevice):
14
+ """PySide6 QIODevice wrapper with enhanced functionality.
15
+
16
+ A wrapper class that provides a Python-friendly interface to PySide6's QIODevice,
17
+ allowing for easier integration with Python code while maintaining all the
18
+ original functionality.
19
+
20
+ Args:
21
+ QIODevice: The base QIODevice class from PySide6.
22
+ """
23
+
24
+ def __init__(self, q_device: QIODevice, *args: Any, **kwargs: Any) -> None:
25
+ """Initialize the PyQIODevice wrapper.
26
+
27
+ Args:
28
+ q_device: The QIODevice instance to wrap.
29
+ *args: Additional positional arguments passed to parent constructor.
30
+ **kwargs: Additional keyword arguments passed to parent constructor.
31
+ """
32
+ super().__init__(*args, **kwargs)
33
+ self.q_device = q_device
34
+
35
+ def atEnd(self) -> bool: # noqa: N802
36
+ """Check if the device is at the end of data.
37
+
38
+ Returns:
39
+ True if the device is at the end, False otherwise.
40
+ """
41
+ return self.q_device.atEnd()
42
+
43
+ def bytesAvailable(self) -> int: # noqa: N802
44
+ """Get the number of bytes available for reading.
45
+
46
+ Returns:
47
+ The number of bytes available for reading.
48
+ """
49
+ return self.q_device.bytesAvailable()
50
+
51
+ def bytesToWrite(self) -> int: # noqa: N802
52
+ """Get the number of bytes waiting to be written.
53
+
54
+ Returns:
55
+ The number of bytes waiting to be written.
56
+ """
57
+ return self.q_device.bytesToWrite()
58
+
59
+ def canReadLine(self) -> bool: # noqa: N802
60
+ """Check if a complete line can be read from the device.
61
+
62
+ Returns:
63
+ True if a complete line can be read, False otherwise.
64
+ """
65
+ return self.q_device.canReadLine()
66
+
67
+ def close(self) -> None:
68
+ """Close the device and release resources.
69
+
70
+ Closes the underlying QIODevice and calls the parent close method.
71
+ """
72
+ self.q_device.close()
73
+ return super().close()
74
+
75
+ def isSequential(self) -> bool: # noqa: N802
76
+ """Check if the device is sequential.
77
+
78
+ Returns:
79
+ True if the device is sequential, False if it supports random access.
80
+ """
81
+ return self.q_device.isSequential()
82
+
83
+ def open(self, mode: QIODevice.OpenModeFlag) -> bool:
84
+ """Open the device with the specified mode.
85
+
86
+ Args:
87
+ mode: The open mode flag specifying how to open the device.
88
+
89
+ Returns:
90
+ True if the device was opened successfully, False otherwise.
91
+ """
92
+ self.q_device.open(mode)
93
+ return super().open(mode)
94
+
95
+ def pos(self) -> int:
96
+ """Get the current position in the device.
97
+
98
+ Returns:
99
+ The current position in the device.
100
+ """
101
+ return self.q_device.pos()
102
+
103
+ def readData(self, maxlen: int) -> bytes: # noqa: N802
104
+ """Read data from the device.
105
+
106
+ Args:
107
+ maxlen: The maximum number of bytes to read.
108
+
109
+ Returns:
110
+ The data read from the device as bytes.
111
+ """
112
+ return bytes(self.q_device.read(maxlen).data())
113
+
114
+ def readLineData(self, maxlen: int) -> object: # noqa: N802
115
+ """Read a line from the device.
116
+
117
+ Args:
118
+ maxlen: The maximum number of bytes to read.
119
+
120
+ Returns:
121
+ The line data read from the device.
122
+ """
123
+ return self.q_device.readLine(maxlen)
124
+
125
+ def reset(self) -> bool:
126
+ """Reset the device to its initial state.
127
+
128
+ Returns:
129
+ True if the device was reset successfully, False otherwise.
130
+ """
131
+ return self.q_device.reset()
132
+
133
+ def seek(self, pos: int) -> bool:
134
+ """Seek to a specific position in the device.
135
+
136
+ Args:
137
+ pos: The position to seek to.
138
+
139
+ Returns:
140
+ True if the seek operation was successful, False otherwise.
141
+ """
142
+ return self.q_device.seek(pos)
143
+
144
+ def size(self) -> int:
145
+ """Get the size of the device.
146
+
147
+ Returns:
148
+ The size of the device in bytes.
149
+ """
150
+ return self.q_device.size()
151
+
152
+ def skipData(self, maxSize: int) -> int: # noqa: N802, N803
153
+ """Skip data in the device.
154
+
155
+ Args:
156
+ maxSize: The maximum number of bytes to skip.
157
+
158
+ Returns:
159
+ The actual number of bytes skipped.
160
+ """
161
+ return self.q_device.skip(maxSize)
162
+
163
+ def waitForBytesWritten(self, msecs: int) -> bool: # noqa: N802
164
+ """Wait for bytes to be written to the device.
165
+
166
+ Args:
167
+ msecs: The maximum time to wait in milliseconds.
168
+
169
+ Returns:
170
+ True if bytes were written within the timeout, False otherwise.
171
+ """
172
+ return self.q_device.waitForBytesWritten(msecs)
173
+
174
+ def waitForReadyRead(self, msecs: int) -> bool: # noqa: N802
175
+ """Wait for the device to be ready for reading.
176
+
177
+ Args:
178
+ msecs: The maximum time to wait in milliseconds.
179
+
180
+ Returns:
181
+ True if the device became ready within the timeout, False otherwise.
182
+ """
183
+ return self.q_device.waitForReadyRead(msecs)
184
+
185
+ def writeData(self, data: bytes | bytearray | memoryview, len: int) -> int: # noqa: A002, ARG002, N802
186
+ """Write data to the device.
187
+
188
+ Args:
189
+ data: The data to write to the device.
190
+ len: The length parameter (unused in this implementation).
191
+
192
+ Returns:
193
+ The number of bytes actually written.
194
+ """
195
+ return self.q_device.write(data)
196
+
197
+
198
+ class PyQFile(PyQIODevice):
199
+ """QFile wrapper with enhanced Python integration.
200
+
201
+ A specialized PyQIODevice wrapper for file operations, providing a more
202
+ Python-friendly interface to PySide6's QFile functionality.
203
+ """
204
+
205
+ def __init__(self, path: Path, *args: Any, **kwargs: Any) -> None:
206
+ """Initialize the PyQFile with a file path.
207
+
208
+ Args:
209
+ path: The file path to open.
210
+ *args: Additional positional arguments passed to parent constructor.
211
+ **kwargs: Additional keyword arguments passed to parent constructor.
212
+ """
213
+ super().__init__(QFile(path), *args, **kwargs)
214
+ self.q_device: QFile
215
+
216
+
217
+ class EncryptedPyQFile(PyQFile):
218
+ """Encrypted file wrapper using AES-GCM encryption.
219
+
220
+ Provides transparent encryption/decryption for file operations using
221
+ AES-GCM (Galois/Counter Mode) encryption. Data is encrypted in chunks
222
+ for efficient streaming operations.
223
+ """
224
+
225
+ NONCE_SIZE = 12
226
+ CIPHER_SIZE = 64 * 1024
227
+ TAG_SIZE = 16
228
+ CHUNK_SIZE = CIPHER_SIZE + NONCE_SIZE + TAG_SIZE
229
+ CHUNK_OVERHEAD = NONCE_SIZE + TAG_SIZE
230
+
231
+ def __init__(self, path: Path, aes_gcm: AESGCM, *args: Any, **kwargs: Any) -> None:
232
+ """Initialize the encrypted file wrapper.
233
+
234
+ Args:
235
+ path: The file path to open.
236
+ aes_gcm: The AES-GCM cipher instance for encryption/decryption.
237
+ *args: Additional positional arguments passed to parent constructor.
238
+ **kwargs: Additional keyword arguments passed to parent constructor.
239
+ """
240
+ super().__init__(path, *args, **kwargs)
241
+ self.q_device: QFile
242
+ self.aes_gcm = aes_gcm
243
+ self.dec_size = self.size()
244
+
245
+ def readData(self, maxlen: int) -> bytes: # noqa: N802
246
+ """Read and decrypt data from the encrypted file.
247
+
248
+ Reads encrypted chunks from the file, decrypts them, and returns
249
+ the requested portion of decrypted data.
250
+
251
+ Args:
252
+ maxlen: The maximum number of decrypted bytes to read.
253
+
254
+ Returns:
255
+ The decrypted data as bytes.
256
+ """
257
+ # where we are in the encrypted data
258
+ dec_pos = self.pos()
259
+ # where we are in the decrypted data
260
+ enc_pos = self.get_encrypted_pos(dec_pos)
261
+
262
+ # get the chunk start and end
263
+ chunk_start = self.get_chunk_start(enc_pos)
264
+ chunk_end = self.get_chunk_end(enc_pos, maxlen)
265
+ new_maxlen = chunk_end - chunk_start
266
+
267
+ # read the chunk
268
+ self.seek(chunk_start)
269
+ enc_data = super().readData(new_maxlen)
270
+ # decrypt the chunk
271
+ dec_data = self.decrypt_data(enc_data)
272
+
273
+ # get the start and end of the requested data in the decrypted data
274
+ dec_chunk_start = self.get_decrypted_pos(chunk_start + self.NONCE_SIZE)
275
+
276
+ req_data_start = dec_pos - dec_chunk_start
277
+ req_data_end = req_data_start + maxlen
278
+
279
+ dec_pos += maxlen
280
+ self.seek(dec_pos)
281
+
282
+ return dec_data[req_data_start:req_data_end]
283
+
284
+ def writeData(self, data: bytes | bytearray | memoryview, len: int) -> int: # noqa: A002, ARG002, N802
285
+ """Encrypt and write data to the file.
286
+
287
+ Args:
288
+ data: The data to encrypt and write.
289
+ len: The length parameter (unused in this implementation).
290
+
291
+ Returns:
292
+ The number of bytes actually written.
293
+ """
294
+ encrypted_data = self.encrypt_data(bytes(data))
295
+ encrypted_len = encrypted_data.__len__()
296
+ return super().writeData(encrypted_data, encrypted_len)
297
+
298
+ def size(self) -> int:
299
+ """Get the decrypted size of the file.
300
+
301
+ Calculates the decrypted size based on the encrypted file size
302
+ and chunk structure.
303
+
304
+ Returns:
305
+ The decrypted size of the file in bytes.
306
+ """
307
+ self.enc_size = super().size()
308
+ self.num_chunks = self.enc_size // self.CHUNK_SIZE + 1
309
+ self.dec_size = self.num_chunks * self.CIPHER_SIZE
310
+ return self.dec_size
311
+
312
+ def get_decrypted_pos(self, enc_pos: int) -> int:
313
+ """Convert encrypted file position to decrypted position.
314
+
315
+ Args:
316
+ enc_pos: The position in the encrypted file.
317
+
318
+ Returns:
319
+ The corresponding position in the decrypted data.
320
+ """
321
+ if enc_pos >= self.enc_size:
322
+ return self.dec_size
323
+
324
+ num_chunks_before = enc_pos // self.CHUNK_SIZE
325
+ last_enc_chunk_start = num_chunks_before * self.CHUNK_SIZE
326
+ last_dec_chunk_start = num_chunks_before * self.CIPHER_SIZE
327
+
328
+ enc_bytes_to_move = enc_pos - last_enc_chunk_start
329
+
330
+ return last_dec_chunk_start + enc_bytes_to_move - self.NONCE_SIZE
331
+
332
+ def get_encrypted_pos(self, dec_pos: int) -> int:
333
+ """Convert decrypted position to encrypted file position.
334
+
335
+ Args:
336
+ dec_pos: The position in the decrypted data.
337
+
338
+ Returns:
339
+ The corresponding position in the encrypted file.
340
+ """
341
+ if dec_pos >= self.dec_size:
342
+ return self.enc_size
343
+ num_chunks_before = dec_pos // self.CIPHER_SIZE
344
+ last_dec_chunk_start = num_chunks_before * self.CIPHER_SIZE
345
+ last_enc_chunk_start = num_chunks_before * self.CHUNK_SIZE
346
+
347
+ dec_bytes_to_move = dec_pos - last_dec_chunk_start
348
+
349
+ return last_enc_chunk_start + self.NONCE_SIZE + dec_bytes_to_move
350
+
351
+ def get_chunk_start(self, pos: int) -> int:
352
+ """Get the start position of the chunk containing the given position.
353
+
354
+ Args:
355
+ pos: The position to find the chunk start for.
356
+
357
+ Returns:
358
+ The start position of the chunk.
359
+ """
360
+ return pos // self.CHUNK_SIZE * self.CHUNK_SIZE
361
+
362
+ def get_chunk_end(self, pos: int, maxlen: int) -> int:
363
+ """Get the end position of the chunk range for the given position and length.
364
+
365
+ Args:
366
+ pos: The starting position.
367
+ maxlen: The maximum length to read.
368
+
369
+ Returns:
370
+ The end position of the chunk range.
371
+ """
372
+ return (pos + maxlen) // self.CHUNK_SIZE * self.CHUNK_SIZE + self.CHUNK_SIZE
373
+
374
+ @classmethod
375
+ def chunk_generator(
376
+ cls, data: bytes, *, is_encrypted: bool
377
+ ) -> Generator[bytes, None, None]:
378
+ """Generate chunks from data.
379
+
380
+ Args:
381
+ data: The data to split into chunks.
382
+ is_encrypted: Whether the data is encrypted (affects chunk size).
383
+
384
+ Yields:
385
+ Chunks of data of appropriate size.
386
+ """
387
+ size = cls.CHUNK_SIZE if is_encrypted else cls.CIPHER_SIZE
388
+ for i in range(0, len(data), size):
389
+ yield data[i : i + size]
390
+
391
+ def encrypt_data(self, data: bytes) -> bytes:
392
+ """Encrypt data using AES-GCM.
393
+
394
+ Args:
395
+ data: The data to encrypt.
396
+
397
+ Returns:
398
+ The encrypted data with nonce and authentication tag.
399
+ """
400
+ return self.encrypt_data_static(data, self.aes_gcm)
401
+
402
+ @classmethod
403
+ def encrypt_data_static(cls, data: bytes, aes_gcm: AESGCM) -> bytes:
404
+ """Encrypt data using AES-GCM (static method).
405
+
406
+ Args:
407
+ data: The data to encrypt.
408
+ aes_gcm: The AES-GCM cipher instance for encryption/decryption.
409
+
410
+ Returns:
411
+ The encrypted data with nonce and authentication tag.
412
+ """
413
+ decrypted_chunks = cls.chunk_generator(data, is_encrypted=False)
414
+ encrypted_chunks = map(
415
+ partial(cls.encrypt_chunk_static, aes_gcm=aes_gcm), decrypted_chunks
416
+ )
417
+ return b"".join(encrypted_chunks)
418
+
419
+ @classmethod
420
+ def encrypt_chunk_static(cls, data: bytes, aes_gcm: AESGCM) -> bytes:
421
+ """Encrypt a single chunk using AES-GCM (static method).
422
+
423
+ Args:
424
+ data: The chunk data to encrypt.
425
+ aes_gcm: The AES-GCM cipher instance for encryption/decryption.
426
+
427
+ Returns:
428
+ The encrypted chunk with nonce and authentication tag.
429
+ """
430
+ nonce = os.urandom(12)
431
+ aad = cls.__name__.encode()
432
+ return nonce + aes_gcm.encrypt(nonce, data, aad)
433
+
434
+ def decrypt_data(self, data: bytes) -> bytes:
435
+ """Decrypt data using AES-GCM.
436
+
437
+ Args:
438
+ data: The encrypted data to decrypt.
439
+
440
+ Returns:
441
+ The decrypted data as bytes.
442
+ """
443
+ return self.decrypt_data_static(data, self.aes_gcm)
444
+
445
+ @classmethod
446
+ def decrypt_data_static(cls, data: bytes, aes_gcm: AESGCM) -> bytes:
447
+ """Decrypt data using AES-GCM (static method).
448
+
449
+ Args:
450
+ data: The encrypted data to decrypt.
451
+ aes_gcm: The AES-GCM cipher instance for encryption/decryption.
452
+
453
+ Returns:
454
+ The decrypted data as bytes.
455
+ """
456
+ encrypted_chunks = cls.chunk_generator(data, is_encrypted=True)
457
+ decrypted_chunks = map(
458
+ partial(cls.decrypt_chunk_static, aes_gcm=aes_gcm), encrypted_chunks
459
+ )
460
+ return b"".join(decrypted_chunks)
461
+
462
+ @classmethod
463
+ def decrypt_chunk_static(cls, data: bytes, aes_gcm: AESGCM) -> bytes:
464
+ """Decrypt a single chunk using AES-GCM (static method).
465
+
466
+ Args:
467
+ data: The encrypted chunk data to decrypt.
468
+ aes_gcm: The AES-GCM cipher instance for encryption/decryption.
469
+
470
+ Returns:
471
+ The decrypted chunk data as bytes.
472
+ """
473
+ nonce = data[: cls.NONCE_SIZE]
474
+ cipher_and_tag = data[cls.NONCE_SIZE :]
475
+ aad = cls.__name__.encode()
476
+ return aes_gcm.decrypt(nonce, cipher_and_tag, aad)
@@ -46,24 +46,43 @@ class Base(metaclass=QABCImplementationLoggingMeta):
46
46
 
47
47
  @abstractmethod
48
48
  def base_setup(self) -> None:
49
- """Get the Qt object of the UI."""
49
+ """Setup the base Qt object of the UI.
50
+
51
+ This method should initialize the core Qt components required
52
+ for the UI to function properly.
53
+ """
50
54
 
51
55
  @abstractmethod
52
56
  def setup(self) -> None:
53
- """Setup the UI."""
57
+ """Setup the main UI components.
58
+
59
+ This method should contain the primary UI initialization logic.
60
+ """
54
61
 
55
62
  @abstractmethod
56
63
  def pre_setup(self) -> None:
57
- """Setup the UI."""
64
+ """Setup operations to run before main setup.
65
+
66
+ This method should contain any initialization that needs to happen
67
+ before the main setup method is called.
68
+ """
58
69
 
59
70
  @abstractmethod
60
71
  def post_setup(self) -> None:
61
- """Setup the UI."""
72
+ """Setup operations to run after main setup.
73
+
74
+ This method should contain any finalization that needs to happen
75
+ after the main setup method is called.
76
+ """
62
77
 
63
78
  @classmethod
64
79
  @final
65
80
  def get_display_name(cls) -> str:
66
- """Get the display name of the UI."""
81
+ """Get the display name of the UI.
82
+
83
+ Returns:
84
+ The human-readable display name derived from the class name.
85
+ """
67
86
  return " ".join(split_on_uppercase(cls.__name__))
68
87
 
69
88
  @classmethod
@@ -72,7 +91,11 @@ class Base(metaclass=QABCImplementationLoggingMeta):
72
91
  """Get all subclasses of the UI.
73
92
 
74
93
  Args:
75
- package: The package to search for subclasses in.
94
+ package: The package to search for subclasses in. If None,
95
+ searches in the main package.
96
+
97
+ Returns:
98
+ A sorted list of all non-abstract subclasses.
76
99
  """
77
100
  if package is None:
78
101
  # find the main package
@@ -85,19 +108,31 @@ class Base(metaclass=QABCImplementationLoggingMeta):
85
108
 
86
109
  @final
87
110
  def set_current_page(self, page_cls: type["BasePage"]) -> None:
88
- """Set the current page."""
111
+ """Set the current page in the stack.
112
+
113
+ Args:
114
+ page_cls: The page class to set as current.
115
+ """
89
116
  self.get_stack().setCurrentWidget(self.get_page(page_cls))
90
117
 
91
118
  @final
92
119
  def get_stack(self) -> QStackedWidget:
93
- """Get the stack of the window."""
120
+ """Get the stack widget of the window.
121
+
122
+ Returns:
123
+ The QStackedWidget containing all pages.
124
+ """
94
125
  window = cast("BaseWindow", (getattr(self, "window", lambda: None)()))
95
126
 
96
127
  return window.stack
97
128
 
98
129
  @final
99
130
  def get_stack_pages(self) -> list["BasePage"]:
100
- """Get all the pages."""
131
+ """Get all pages from the stack.
132
+
133
+ Returns:
134
+ A list of all pages currently in the stack.
135
+ """
101
136
  # Import here to avoid circular import
102
137
 
103
138
  stack = self.get_stack()
@@ -106,7 +141,14 @@ class Base(metaclass=QABCImplementationLoggingMeta):
106
141
 
107
142
  @final
108
143
  def get_page[T: "BasePage"](self, page_cls: type[T]) -> T:
109
- """Get the page."""
144
+ """Get a specific page from the stack.
145
+
146
+ Args:
147
+ page_cls: The class of the page to retrieve.
148
+
149
+ Returns:
150
+ The page instance of the specified class.
151
+ """
110
152
  page = next(
111
153
  page for page in self.get_stack_pages() if page.__class__ is page_cls
112
154
  )
@@ -115,13 +157,29 @@ class Base(metaclass=QABCImplementationLoggingMeta):
115
157
  @classmethod
116
158
  @final
117
159
  def get_svg_icon(cls, svg_name: str, package: ModuleType | None = None) -> QIcon:
118
- """Get the Qicon for a svg."""
160
+ """Get a QIcon for an SVG file.
161
+
162
+ Args:
163
+ svg_name: The name of the SVG file.
164
+ package: The package to search for the SVG in. If None,
165
+ searches in the main package.
166
+
167
+ Returns:
168
+ A QIcon created from the SVG file.
169
+ """
119
170
  return QIcon(str(get_svg_path(svg_name, package=package)))
120
171
 
121
172
  @classmethod
122
173
  @final
123
174
  def get_page_static[T: "BasePage"](cls, page_cls: type[T]) -> T:
124
- """Get the page."""
175
+ """Get a page statically from the main window.
176
+
177
+ Args:
178
+ page_cls: The class of the page to retrieve.
179
+
180
+ Returns:
181
+ The page instance of the specified class from the main window.
182
+ """
125
183
  from winipedia_utils.pyside.ui.windows.base.base import Base as BaseWindow
126
184
 
127
185
  top_level_widgets = QApplication.topLevelWidgets()
@@ -25,7 +25,11 @@ class Base(BaseUI, QWidget):
25
25
 
26
26
  @final
27
27
  def base_setup(self) -> None:
28
- """Get the Qt object of the UI."""
28
+ """Setup the base Qt object of the UI.
29
+
30
+ Initializes the main vertical layout, adds a horizontal layout for the top row,
31
+ and sets up the menu dropdown button.
32
+ """
29
33
  self.v_layout = QVBoxLayout()
30
34
  self.setLayout(self.v_layout)
31
35
 
@@ -37,7 +41,11 @@ class Base(BaseUI, QWidget):
37
41
 
38
42
  @final
39
43
  def add_menu_dropdown_button(self) -> None:
40
- """Add a dropdown menu that leadds to each page."""
44
+ """Add a dropdown menu that leads to each page.
45
+
46
+ Creates a menu button with a dropdown containing actions for all available
47
+ page subclasses. Each action connects to the set_current_page method.
48
+ """
41
49
  self.menu_button = QPushButton("Menu")
42
50
  self.menu_button.setSizePolicy(
43
51
  QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum
@@ -57,7 +65,15 @@ class Base(BaseUI, QWidget):
57
65
  def add_to_page_button(
58
66
  self, to_page_cls: type["Base"], layout: QLayout
59
67
  ) -> QPushButton:
60
- """Add a button to go to the page."""
68
+ """Add a button to go to the specified page.
69
+
70
+ Args:
71
+ to_page_cls: The page class to navigate to when button is clicked.
72
+ layout: The layout to add the button to.
73
+
74
+ Returns:
75
+ The created QPushButton widget.
76
+ """
61
77
  button = QPushButton(to_page_cls.get_display_name())
62
78
 
63
79
  # connect to open page on click
@@ -14,11 +14,17 @@ class Browser(BasePage):
14
14
 
15
15
  @final
16
16
  def setup(self) -> None:
17
- """Setup the UI."""
18
- # add a download button in the top right
17
+ """Setup the UI.
18
+
19
+ Initializes the browser page by adding a browser widget to the layout.
20
+ """
19
21
  self.add_brwoser()
20
22
 
21
23
  @final
22
24
  def add_brwoser(self) -> None:
23
- """Add a browser to surfe the web."""
25
+ """Add a browser to surf the web.
26
+
27
+ Creates and adds a BrowserWidget instance to the vertical layout,
28
+ enabling web browsing functionality within the page.
29
+ """
24
30
  self.browser = BrowserWidget(self.v_layout)