qcanvas 0.0.5.7a0__py3-none-any.whl → 1.0.3.post1__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.
Potentially problematic release.
This version of qcanvas might be problematic. Click here for more details.
- qcanvas/app_start/__init__.py +47 -0
- qcanvas/backend_connectors/__init__.py +2 -0
- qcanvas/backend_connectors/frontend_resource_manager.py +63 -0
- qcanvas/backend_connectors/qcanvas_task_master.py +28 -0
- qcanvas/icons/__init__.py +6 -0
- qcanvas/icons/file-download-failed.svg +6 -0
- qcanvas/icons/file-downloaded.svg +6 -0
- qcanvas/icons/file-not-downloaded.svg +6 -0
- qcanvas/icons/file-unknown.svg +6 -0
- qcanvas/icons/icons.qrc +4 -0
- qcanvas/icons/main_icon.svg +7 -7
- qcanvas/icons/rc_icons.py +580 -214
- qcanvas/icons/sync.svg +6 -6
- qcanvas/run.py +29 -0
- qcanvas/ui/course_viewer/__init__.py +2 -0
- qcanvas/ui/course_viewer/content_tree.py +123 -0
- qcanvas/ui/course_viewer/course_tree.py +93 -0
- qcanvas/ui/course_viewer/course_viewer.py +62 -0
- qcanvas/ui/course_viewer/tabs/__init__.py +3 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +168 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +104 -0
- qcanvas/ui/course_viewer/tabs/content_tab.py +96 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +68 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +70 -0
- qcanvas/ui/course_viewer/tabs/page_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +36 -0
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +74 -0
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +176 -0
- qcanvas/ui/course_viewer/tabs/util.py +1 -0
- qcanvas/ui/main_ui/course_viewer_container.py +52 -0
- qcanvas/ui/main_ui/options/__init__.py +3 -0
- qcanvas/ui/main_ui/options/quick_sync_option.py +25 -0
- qcanvas/ui/main_ui/options/sync_on_start_option.py +25 -0
- qcanvas/ui/main_ui/qcanvas_window.py +192 -0
- qcanvas/ui/main_ui/status_bar_progress_display.py +153 -0
- qcanvas/ui/memory_tree/__init__.py +2 -0
- qcanvas/ui/memory_tree/_tree_memory.py +66 -0
- qcanvas/ui/memory_tree/memory_tree_widget.py +133 -0
- qcanvas/ui/memory_tree/memory_tree_widget_item.py +19 -0
- qcanvas/ui/setup/__init__.py +2 -0
- qcanvas/ui/setup/setup_checker.py +17 -0
- qcanvas/ui/setup/setup_dialog.py +212 -0
- qcanvas/util/__init__.py +2 -0
- qcanvas/util/basic_fonts.py +12 -0
- qcanvas/util/fe_resource_manager.py +23 -0
- qcanvas/util/html_cleaner.py +25 -0
- qcanvas/util/layouts.py +52 -0
- qcanvas/util/logs.py +6 -0
- qcanvas/util/paths.py +41 -0
- qcanvas/util/settings/__init__.py +9 -0
- qcanvas/util/settings/_client_settings.py +29 -0
- qcanvas/util/settings/_mapped_setting.py +63 -0
- qcanvas/util/settings/_ui_settings.py +34 -0
- qcanvas/util/ui_tools.py +41 -0
- qcanvas/util/url_checker.py +13 -0
- qcanvas-1.0.3.post1.dist-info/METADATA +59 -0
- qcanvas-1.0.3.post1.dist-info/RECORD +64 -0
- {qcanvas-0.0.5.7a0.dist-info → qcanvas-1.0.3.post1.dist-info}/WHEEL +1 -1
- qcanvas-1.0.3.post1.dist-info/entry_points.txt +3 -0
- qcanvas/__main__.py +0 -155
- qcanvas/db/__init__.py +0 -5
- qcanvas/db/database.py +0 -338
- qcanvas/db/db_converter_helper.py +0 -81
- qcanvas/net/canvas/__init__.py +0 -2
- qcanvas/net/canvas/canvas_client.py +0 -209
- qcanvas/net/canvas/legacy_canvas_types.py +0 -124
- qcanvas/net/custom_httpx_async_transport.py +0 -34
- qcanvas/net/self_authenticating.py +0 -108
- qcanvas/queries/__init__.py +0 -4
- qcanvas/queries/all_courses.gql +0 -7
- qcanvas/queries/all_courses.py +0 -108
- qcanvas/queries/canvas_course_data.gql +0 -51
- qcanvas/queries/canvas_course_data.py +0 -143
- qcanvas/ui/container_item.py +0 -11
- qcanvas/ui/main_ui.py +0 -251
- qcanvas/ui/menu_bar/__init__.py +0 -0
- qcanvas/ui/menu_bar/grouping_preferences_menu.py +0 -61
- qcanvas/ui/menu_bar/theme_selection_menu.py +0 -39
- qcanvas/ui/setup_dialog.py +0 -190
- qcanvas/ui/status_bar_reporter.py +0 -40
- qcanvas/ui/viewer/__init__.py +0 -0
- qcanvas/ui/viewer/course_list.py +0 -96
- qcanvas/ui/viewer/file_list.py +0 -195
- qcanvas/ui/viewer/file_view_tab.py +0 -62
- qcanvas/ui/viewer/page_list_viewer.py +0 -150
- qcanvas/util/app_settings.py +0 -98
- qcanvas/util/constants.py +0 -5
- qcanvas/util/course_indexer/__init__.py +0 -1
- qcanvas/util/course_indexer/conversion_helpers.py +0 -78
- qcanvas/util/course_indexer/data_manager.py +0 -447
- qcanvas/util/course_indexer/resource_helpers.py +0 -191
- qcanvas/util/download_pool.py +0 -58
- qcanvas/util/helpers/__init__.py +0 -0
- qcanvas/util/helpers/canvas_sanitiser.py +0 -47
- qcanvas/util/helpers/file_icon_helper.py +0 -34
- qcanvas/util/helpers/qaction_helper.py +0 -25
- qcanvas/util/helpers/theme_helper.py +0 -48
- qcanvas/util/link_scanner/__init__.py +0 -2
- qcanvas/util/link_scanner/canvas_link_scanner.py +0 -41
- qcanvas/util/link_scanner/canvas_media_object_scanner.py +0 -60
- qcanvas/util/link_scanner/dropbox_scanner.py +0 -68
- qcanvas/util/link_scanner/resource_scanner.py +0 -69
- qcanvas/util/progress_reporter.py +0 -101
- qcanvas/util/self_updater.py +0 -55
- qcanvas/util/task_pool.py +0 -253
- qcanvas/util/tree_util/__init__.py +0 -3
- qcanvas/util/tree_util/expanding_tree.py +0 -165
- qcanvas/util/tree_util/model_helpers.py +0 -36
- qcanvas/util/tree_util/tree_model.py +0 -85
- qcanvas-0.0.5.7a0.dist-info/METADATA +0 -21
- qcanvas-0.0.5.7a0.dist-info/RECORD +0 -62
- /qcanvas/{net → ui/main_ui}/__init__.py +0 -0
qcanvas/util/task_pool.py
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import logging
|
|
3
|
-
from typing import TypeVar, Generic, Callable
|
|
4
|
-
|
|
5
|
-
T = TypeVar("T")
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TaskPool(Generic[T]):
|
|
9
|
-
"""
|
|
10
|
-
A TaskPool is a utility that receives submitted tasks which have an identity and which should be executed no more
|
|
11
|
-
than once. It can be configured to wait for the task to finish when started, wait for the task to finish when it
|
|
12
|
-
is in progress, and record the result of the task when it finishes and return it when a task with the same ID is
|
|
13
|
-
submitted again.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
_logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
def __init__(self, remember_result: bool = True, wait_if_in_progress: bool = True,
|
|
19
|
-
wait_if_just_started: bool = True, restart_if_finished: bool = False):
|
|
20
|
-
"""
|
|
21
|
-
Parameters
|
|
22
|
-
----------
|
|
23
|
-
remember_result : bool
|
|
24
|
-
Whether to store the result of submitted tasks
|
|
25
|
-
|
|
26
|
-
wait_if_just_started : bool
|
|
27
|
-
Whether to await a newly started task or return immediately
|
|
28
|
-
|
|
29
|
-
wait_if_in_progress : bool
|
|
30
|
-
Whether to await a task that is in progress or return immediately
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
if not wait_if_in_progress and remember_result:
|
|
34
|
-
raise ValueError("Can't remember result without waiting")
|
|
35
|
-
|
|
36
|
-
self._results: dict[object, asyncio.Event | T | None] = {}
|
|
37
|
-
self._semaphore = asyncio.Semaphore()
|
|
38
|
-
self._remember_result: bool = remember_result
|
|
39
|
-
self._wait_if_in_progress: bool = wait_if_in_progress
|
|
40
|
-
self._wait_if_just_started: bool = wait_if_just_started
|
|
41
|
-
self._restart_if_finished: bool = restart_if_finished
|
|
42
|
-
|
|
43
|
-
def add_values(self, results: dict[object, T]) -> None:
|
|
44
|
-
"""
|
|
45
|
-
Adds the specified values from the dictionary as stored results.
|
|
46
|
-
|
|
47
|
-
Parameters
|
|
48
|
-
----------
|
|
49
|
-
results
|
|
50
|
-
The results to store, where the key is the ID and the value is the result
|
|
51
|
-
|
|
52
|
-
Returns
|
|
53
|
-
-------
|
|
54
|
-
None
|
|
55
|
-
"""
|
|
56
|
-
self._results.update(**results)
|
|
57
|
-
|
|
58
|
-
async def submit(self, task_id: object, func, **kwargs) -> T | None:
|
|
59
|
-
"""
|
|
60
|
-
Submits a task and executes it. Depending on configuration, waits for it and returns the result of it.
|
|
61
|
-
|
|
62
|
-
Parameters
|
|
63
|
-
----------
|
|
64
|
-
task_id: object
|
|
65
|
-
The identity of the task. E.g. "task_get_file_1" or "task_get_file_2"
|
|
66
|
-
|
|
67
|
-
func : lambda
|
|
68
|
-
The function to execute.
|
|
69
|
-
Should be provided in the form of a lambda which executes some async function without using the await keyword.
|
|
70
|
-
E.g::
|
|
71
|
-
# Right
|
|
72
|
-
taskpool.submit(..., func=lambda: some_async_function(some_val))
|
|
73
|
-
# Wrong
|
|
74
|
-
taskpool.submit(..., func=lambda: async some_async_function(some_val))
|
|
75
|
-
# ^^^^^
|
|
76
|
-
kwargs
|
|
77
|
-
Extra arguments for the lambda
|
|
78
|
-
Returns
|
|
79
|
-
-------
|
|
80
|
-
T | None
|
|
81
|
-
None, if the configuration states that we should not wait for the function to finish or not to remember the result.
|
|
82
|
-
Returns the result of the function otherwise.
|
|
83
|
-
"""
|
|
84
|
-
sem = self._semaphore
|
|
85
|
-
await sem.acquire()
|
|
86
|
-
|
|
87
|
-
if task_id in self._results.keys():
|
|
88
|
-
if not self._wait_if_in_progress and isinstance(self._results[task_id], asyncio.Event):
|
|
89
|
-
self._logger.debug("Task %s in progress but configured to not wait, returning None.", task_id)
|
|
90
|
-
sem.release()
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
if not isinstance(self._results[task_id], asyncio.Event):
|
|
94
|
-
if self._restart_if_finished:
|
|
95
|
-
self._logger.debug("Task %s already finished but configured to restart if finished, restarting.",
|
|
96
|
-
task_id)
|
|
97
|
-
# start_task releases the semaphore, no need to do it here
|
|
98
|
-
return await self._start_task(task_id, func, **kwargs)
|
|
99
|
-
|
|
100
|
-
self._logger.debug("Task %s already finished, returning.", task_id)
|
|
101
|
-
sem.release()
|
|
102
|
-
return self._results[task_id]
|
|
103
|
-
|
|
104
|
-
self._logger.debug("Task %s in progress. Waiting.", task_id)
|
|
105
|
-
|
|
106
|
-
event: asyncio.Event = self._results[task_id]
|
|
107
|
-
sem.release()
|
|
108
|
-
|
|
109
|
-
await event.wait()
|
|
110
|
-
|
|
111
|
-
self._logger.debug("Finished waiting for %s.", task_id)
|
|
112
|
-
|
|
113
|
-
return self._results[task_id]
|
|
114
|
-
else:
|
|
115
|
-
# start_task releases the semaphore, no need to do it here
|
|
116
|
-
return await self._start_task(task_id, func, **kwargs)
|
|
117
|
-
|
|
118
|
-
async def _start_task(self, task_id: object, func, **kwargs):
|
|
119
|
-
"""
|
|
120
|
-
Starts a task and **releases the results list semaphore**
|
|
121
|
-
|
|
122
|
-
Parameters
|
|
123
|
-
----------
|
|
124
|
-
task
|
|
125
|
-
The task to start
|
|
126
|
-
task_id
|
|
127
|
-
The ID of the task
|
|
128
|
-
event
|
|
129
|
-
The event that the task is attached to
|
|
130
|
-
|
|
131
|
-
Returns
|
|
132
|
-
-------
|
|
133
|
-
T
|
|
134
|
-
The result of the task
|
|
135
|
-
"""
|
|
136
|
-
sem = self._semaphore
|
|
137
|
-
|
|
138
|
-
self._logger.debug("Task %s started.", task_id)
|
|
139
|
-
|
|
140
|
-
event = asyncio.Event()
|
|
141
|
-
self._results[task_id] = event
|
|
142
|
-
sem.release()
|
|
143
|
-
|
|
144
|
-
if not self._wait_if_just_started:
|
|
145
|
-
# noinspection PyAsyncCall
|
|
146
|
-
asyncio.create_task(self._handle_task(func, task_id, event, func_args=kwargs))
|
|
147
|
-
return None
|
|
148
|
-
|
|
149
|
-
return await self._handle_task(func, task_id, event, func_args=kwargs)
|
|
150
|
-
|
|
151
|
-
async def _handle_task(self, func: Callable, task_id: object, event: asyncio.Event, func_args: dict) -> T:
|
|
152
|
-
"""
|
|
153
|
-
Handles the specified task
|
|
154
|
-
|
|
155
|
-
Parameters
|
|
156
|
-
----------
|
|
157
|
-
func
|
|
158
|
-
The task to handle
|
|
159
|
-
task_id
|
|
160
|
-
The ID of the task
|
|
161
|
-
event
|
|
162
|
-
The event that the task is attached to
|
|
163
|
-
|
|
164
|
-
Returns
|
|
165
|
-
-------
|
|
166
|
-
T
|
|
167
|
-
The result of the task
|
|
168
|
-
"""
|
|
169
|
-
sem = self._semaphore
|
|
170
|
-
|
|
171
|
-
result = await func(**func_args)
|
|
172
|
-
|
|
173
|
-
if isinstance(result, asyncio.Event):
|
|
174
|
-
self._logger.warning("Result was of type asyncio.Event, this will break things!")
|
|
175
|
-
|
|
176
|
-
async with sem:
|
|
177
|
-
self._logger.debug("Task %s finished", task_id)
|
|
178
|
-
# Record this task as done, storing the result if configured to
|
|
179
|
-
self._results[task_id] = result if self._remember_result else None
|
|
180
|
-
event.set()
|
|
181
|
-
|
|
182
|
-
return result
|
|
183
|
-
|
|
184
|
-
async def wait_if_in_progress(self, task_id: object):
|
|
185
|
-
"""
|
|
186
|
-
Waits for a task if it is in progress. Returns immediately otherwise.
|
|
187
|
-
|
|
188
|
-
Parameters
|
|
189
|
-
----------
|
|
190
|
-
task_id
|
|
191
|
-
The task id to wait for
|
|
192
|
-
"""
|
|
193
|
-
sem = self._semaphore
|
|
194
|
-
|
|
195
|
-
await sem.acquire()
|
|
196
|
-
|
|
197
|
-
if task_id in self._results and isinstance(self._results[task_id], asyncio.Event):
|
|
198
|
-
event: asyncio.Event = self._results[task_id]
|
|
199
|
-
sem.release()
|
|
200
|
-
|
|
201
|
-
await event.wait()
|
|
202
|
-
|
|
203
|
-
def clear(self) -> None:
|
|
204
|
-
"""
|
|
205
|
-
Deletes the stored results
|
|
206
|
-
|
|
207
|
-
Returns
|
|
208
|
-
-------
|
|
209
|
-
None
|
|
210
|
-
"""
|
|
211
|
-
self._results.clear()
|
|
212
|
-
|
|
213
|
-
def results(self) -> list[T]:
|
|
214
|
-
"""
|
|
215
|
-
Gets the results of all currently completed tasks
|
|
216
|
-
|
|
217
|
-
Returns
|
|
218
|
-
-------
|
|
219
|
-
list[T]
|
|
220
|
-
The results of all currently completed tasks, excluding Nones
|
|
221
|
-
"""
|
|
222
|
-
|
|
223
|
-
def filter_func(it):
|
|
224
|
-
return not isinstance(it, asyncio.Event) and it is not None
|
|
225
|
-
|
|
226
|
-
return list(filter(filter_func, self._results.values()))
|
|
227
|
-
|
|
228
|
-
def get_completed_result_or_nothing(self, task_id: object, default: T | None = None) -> T | None:
|
|
229
|
-
"""
|
|
230
|
-
Returns the result of an already completed task, or nothing at all if result with that id exists or is still in progress
|
|
231
|
-
|
|
232
|
-
Returns
|
|
233
|
-
-------
|
|
234
|
-
object
|
|
235
|
-
The result of the task
|
|
236
|
-
|
|
237
|
-
Raises
|
|
238
|
-
------
|
|
239
|
-
ValueError
|
|
240
|
-
If the task is still in progress
|
|
241
|
-
|
|
242
|
-
KeyError
|
|
243
|
-
If the task has not been started yet or does not exist
|
|
244
|
-
"""
|
|
245
|
-
if task_id in self._results:
|
|
246
|
-
result = self._results[task_id]
|
|
247
|
-
|
|
248
|
-
if isinstance(result, asyncio.Event):
|
|
249
|
-
return None
|
|
250
|
-
|
|
251
|
-
return result
|
|
252
|
-
else:
|
|
253
|
-
return None
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
from typing import Any, TypeVar, Sequence, Optional
|
|
2
|
-
|
|
3
|
-
from PySide6.QtCore import QModelIndex, Slot, QItemSelectionModel
|
|
4
|
-
from PySide6.QtWidgets import QTreeView, QWidget
|
|
5
|
-
|
|
6
|
-
from .model_helpers import HasChildren, HasParent
|
|
7
|
-
from .tree_model import TreeModel
|
|
8
|
-
|
|
9
|
-
T = TypeVar("T")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ExpandingTreeView(QTreeView):
|
|
13
|
-
"""
|
|
14
|
-
Provides a way to retain the collapsed or expanded state of tree items after a model reset. Expects the tree's
|
|
15
|
-
model to implement `AbstractItemModelHasListRoot` and any children to implement `HasChildren`. Also provides
|
|
16
|
-
some methods that can be used to get a model index for an arbitrary object in a tree. These will NOT work when
|
|
17
|
-
duplicate objects are present in the tree. Does NOT support multi-selection.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
def __init__(self, parent: Optional[QWidget] = None):
|
|
21
|
-
"""
|
|
22
|
-
Constructor
|
|
23
|
-
:param session_factory: The session that will be used to update an item's expanded/collapsed state when it is changed
|
|
24
|
-
:param parent: The parent of this treeview
|
|
25
|
-
"""
|
|
26
|
-
super().__init__(parent)
|
|
27
|
-
self._connect_expanded_listeners()
|
|
28
|
-
|
|
29
|
-
def reexpand(self) -> None:
|
|
30
|
-
"""
|
|
31
|
-
Re-expands all items in the tree that have children based on their saved collapsed/expanded state.
|
|
32
|
-
"""
|
|
33
|
-
self._disconnect_expanded_listeners()
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
root: Sequence[Any] = self.model().get_root()
|
|
37
|
-
|
|
38
|
-
# Go through every item in the root and reexpand it and any children it has
|
|
39
|
-
for index, item in enumerate(root):
|
|
40
|
-
if isinstance(item, HasChildren):
|
|
41
|
-
self._reexpand_recur(item, self.model().index(index, 0, QModelIndex()))
|
|
42
|
-
finally:
|
|
43
|
-
self._connect_expanded_listeners()
|
|
44
|
-
|
|
45
|
-
def _connect_expanded_listeners(self):
|
|
46
|
-
self.expanded.connect(self.tree_item_expanded)
|
|
47
|
-
self.collapsed.connect(self.tree_item_collapsed)
|
|
48
|
-
|
|
49
|
-
def _disconnect_expanded_listeners(self):
|
|
50
|
-
self.expanded.disconnect(self.tree_item_expanded)
|
|
51
|
-
self.collapsed.disconnect(self.tree_item_collapsed)
|
|
52
|
-
|
|
53
|
-
def _reexpand_recur(self, item: HasChildren, index: QModelIndex) -> None:
|
|
54
|
-
"""
|
|
55
|
-
Internal function to expand an item in the tree. Recurs to any children it has.
|
|
56
|
-
:param item: The object to expand in the tree
|
|
57
|
-
:param index: The index of that object
|
|
58
|
-
"""
|
|
59
|
-
# Sanity check
|
|
60
|
-
if not isinstance(item, HasChildren):
|
|
61
|
-
raise TypeError("item must be an instance of HasChildren")
|
|
62
|
-
|
|
63
|
-
# Expand this item
|
|
64
|
-
self.setExpanded(index, not item.collapsed)
|
|
65
|
-
|
|
66
|
-
for child_index, child in enumerate(item.get_children()):
|
|
67
|
-
# Check if the child item can also have children and be collapsed. Also check that it actually has
|
|
68
|
-
# any children.
|
|
69
|
-
if isinstance(child, HasChildren) and len(child.get_children()) > 0:
|
|
70
|
-
# Expand it and any children it has
|
|
71
|
-
self._reexpand_recur(child, self.model().index(child_index, 0, index))
|
|
72
|
-
|
|
73
|
-
@Slot()
|
|
74
|
-
def tree_item_expanded(self, index: QModelIndex) -> None:
|
|
75
|
-
"""
|
|
76
|
-
Slot that is connected to the tree's `expanded` signal
|
|
77
|
-
:param index: The index that has been expanded
|
|
78
|
-
"""
|
|
79
|
-
item = self.model().get_item(index)
|
|
80
|
-
|
|
81
|
-
# Update the item's state
|
|
82
|
-
if isinstance(item, HasChildren):
|
|
83
|
-
item.collapsed = False
|
|
84
|
-
|
|
85
|
-
@Slot()
|
|
86
|
-
def tree_item_collapsed(self, index: QModelIndex) -> None:
|
|
87
|
-
"""
|
|
88
|
-
Slot that is connected to the tree's `collapsed` signal
|
|
89
|
-
:param index: The index that was collapsed
|
|
90
|
-
"""
|
|
91
|
-
item = self.model().get_item(index)
|
|
92
|
-
|
|
93
|
-
# Update the item's state
|
|
94
|
-
if isinstance(item, HasChildren):
|
|
95
|
-
item.collapsed = True
|
|
96
|
-
|
|
97
|
-
def get_path_of_indexes_for_item(self, item: Any) -> list[int]:
|
|
98
|
-
"""
|
|
99
|
-
Gets the path of numerical indexes for an arbitrary object in a tree.
|
|
100
|
-
For example, second child of the first item would be [0, 1].
|
|
101
|
-
|
|
102
|
-
:param item: The item to find the path for. The item must belong to the tree
|
|
103
|
-
:return: The path to the item as a list of indexes to follow
|
|
104
|
-
"""
|
|
105
|
-
path: list[int] = []
|
|
106
|
-
|
|
107
|
-
# Keep going until we reach the root
|
|
108
|
-
while isinstance(item, HasParent) and item.parent is not None:
|
|
109
|
-
# Add the index of the item to the path and then go to the parent of that child until there is no parent
|
|
110
|
-
path.insert(0, item.index_of_self)
|
|
111
|
-
item = item.parent
|
|
112
|
-
|
|
113
|
-
# Insert the index of the item belonging to the root list to the path
|
|
114
|
-
path.insert(0, self.model().get_root().index(item))
|
|
115
|
-
|
|
116
|
-
return path
|
|
117
|
-
|
|
118
|
-
def select_object(self, item: Any) -> None:
|
|
119
|
-
"""
|
|
120
|
-
Selects an arbitrary item that belongs to the tree. Will not work for branches that have duplicate items.
|
|
121
|
-
:param item: The item to select. Must belong to the tree
|
|
122
|
-
"""
|
|
123
|
-
self.select_item_by_path(self.get_path_of_indexes_for_item(item))
|
|
124
|
-
|
|
125
|
-
def select_item_by_path(self, item_path: list) -> None:
|
|
126
|
-
"""
|
|
127
|
-
Selects an item based on its path
|
|
128
|
-
:param item_path: The path of the item
|
|
129
|
-
"""
|
|
130
|
-
selection_model = self.selectionModel()
|
|
131
|
-
|
|
132
|
-
# Convert the path to a model index
|
|
133
|
-
# Clear previous selection and select the whole row
|
|
134
|
-
selection_model.select(self.get_model_index_for_path(item_path),
|
|
135
|
-
QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows)
|
|
136
|
-
|
|
137
|
-
def get_model_index_for_path(self, item_path: list[int]) -> QModelIndex:
|
|
138
|
-
"""
|
|
139
|
-
Converts an item's path into a QModelIndex
|
|
140
|
-
:param item_path: The path of the item to convert
|
|
141
|
-
:return: The path as a QModelIndex
|
|
142
|
-
"""
|
|
143
|
-
model: TreeModel[Any] = self.model()
|
|
144
|
-
# Get the first index
|
|
145
|
-
item_index = item_path.pop(0)
|
|
146
|
-
# Create the base QModelIndex
|
|
147
|
-
model_index = model.index(item_index, 0, QModelIndex())
|
|
148
|
-
# Get the children of the first item
|
|
149
|
-
children = model.get_root()[item_index]
|
|
150
|
-
|
|
151
|
-
while len(item_path) > 0:
|
|
152
|
-
# Get the next item
|
|
153
|
-
item_index = item_path.pop(0)
|
|
154
|
-
# Update the QModelIndex
|
|
155
|
-
model_index = model.index(item_index, 0, model_index)
|
|
156
|
-
|
|
157
|
-
if len(item_path) > 0:
|
|
158
|
-
# Sanity check
|
|
159
|
-
if not isinstance(children, HasChildren):
|
|
160
|
-
raise TypeError("Parent object does not have any children but a child was expected")
|
|
161
|
-
|
|
162
|
-
# Go to the children of this child next
|
|
163
|
-
children = children.children[item_index]
|
|
164
|
-
|
|
165
|
-
return model_index
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
from typing import Sequence, Any
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class HasColumnData:
|
|
5
|
-
def get_column_data(self, column: int, role: int) -> str | None:
|
|
6
|
-
raise NotImplementedError()
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class HasText:
|
|
10
|
-
@property
|
|
11
|
-
def text(self) -> str:
|
|
12
|
-
raise NotImplementedError()
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class HasParent:
|
|
16
|
-
@property
|
|
17
|
-
def parent(self) -> Any:
|
|
18
|
-
raise NotImplementedError()
|
|
19
|
-
|
|
20
|
-
@property
|
|
21
|
-
def index_of_self(self) -> int:
|
|
22
|
-
raise NotImplementedError()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class HasChildren:
|
|
26
|
-
@property
|
|
27
|
-
def collapsed(self) -> bool:
|
|
28
|
-
raise NotImplementedError()
|
|
29
|
-
|
|
30
|
-
@collapsed.setter
|
|
31
|
-
def collapsed(self, value: bool):
|
|
32
|
-
raise NotImplementedError()
|
|
33
|
-
|
|
34
|
-
@property
|
|
35
|
-
def children(self) -> Sequence[HasColumnData]:
|
|
36
|
-
raise NotImplementedError()
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
from typing import Any, Sequence, TypeVar, Generic, Optional
|
|
2
|
-
|
|
3
|
-
from PySide6.QtCore import QAbstractItemModel, QModelIndex, QPersistentModelIndex
|
|
4
|
-
from PySide6.QtWidgets import QWidget
|
|
5
|
-
|
|
6
|
-
from .model_helpers import HasColumnData, HasParent, HasChildren
|
|
7
|
-
|
|
8
|
-
T = TypeVar("T")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# todo comment this
|
|
12
|
-
class TreeModel(QAbstractItemModel, Generic[T]):
|
|
13
|
-
def __init__(self, parent: Optional[QWidget] = None):
|
|
14
|
-
super().__init__(parent)
|
|
15
|
-
self.root: Sequence[T] = []
|
|
16
|
-
|
|
17
|
-
def get_item(self, index: QModelIndex | QPersistentModelIndex) -> object:
|
|
18
|
-
if index.isValid():
|
|
19
|
-
return index.internalPointer()
|
|
20
|
-
|
|
21
|
-
return self.root
|
|
22
|
-
|
|
23
|
-
def data(self, index: QModelIndex | QPersistentModelIndex, role: int = ...) -> Any:
|
|
24
|
-
if not index.isValid():
|
|
25
|
-
return None
|
|
26
|
-
|
|
27
|
-
item = self.get_item(index)
|
|
28
|
-
|
|
29
|
-
if isinstance(item, HasColumnData):
|
|
30
|
-
return item.get_column_data(index.column(), role)
|
|
31
|
-
else:
|
|
32
|
-
return None
|
|
33
|
-
|
|
34
|
-
def index(self, row: int, column: int, parent: QModelIndex | QPersistentModelIndex = ...) -> QModelIndex:
|
|
35
|
-
if parent.isValid() and parent.column() != 0:
|
|
36
|
-
return QModelIndex()
|
|
37
|
-
|
|
38
|
-
parent_item = self.get_item(parent)
|
|
39
|
-
|
|
40
|
-
if isinstance(parent_item, HasChildren):
|
|
41
|
-
parent_list = parent_item.children
|
|
42
|
-
elif isinstance(parent_item, Sequence):
|
|
43
|
-
parent_list = parent_item
|
|
44
|
-
else:
|
|
45
|
-
raise TypeError(
|
|
46
|
-
f"Expected parent of item to be Sequence or HasChildren, actually {parent_item.__class__}")
|
|
47
|
-
|
|
48
|
-
if row > len(parent_list) or row < 0:
|
|
49
|
-
return QModelIndex()
|
|
50
|
-
|
|
51
|
-
return self.createIndex(row, column, parent_list[row])
|
|
52
|
-
|
|
53
|
-
def parent(self, child: QModelIndex | QPersistentModelIndex = QModelIndex()) -> QModelIndex:
|
|
54
|
-
if not child.isValid():
|
|
55
|
-
return QModelIndex()
|
|
56
|
-
|
|
57
|
-
child_item = self.get_item(child)
|
|
58
|
-
|
|
59
|
-
# We don't need to handle the root items here because... they are the root and have no parents
|
|
60
|
-
|
|
61
|
-
if isinstance(child_item, HasParent) and child_item.parent is not None:
|
|
62
|
-
return self.createIndex(child_item.index_of_self, 0, child_item.parent)
|
|
63
|
-
|
|
64
|
-
return QModelIndex()
|
|
65
|
-
|
|
66
|
-
def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
|
|
67
|
-
if parent.isValid() and parent.column() > 0:
|
|
68
|
-
return 0
|
|
69
|
-
|
|
70
|
-
parent_item = self.get_item(parent)
|
|
71
|
-
|
|
72
|
-
if isinstance(parent_item, HasChildren):
|
|
73
|
-
return len(parent_item.children)
|
|
74
|
-
elif isinstance(parent_item, Sequence):
|
|
75
|
-
return len(parent_item)
|
|
76
|
-
else:
|
|
77
|
-
return 0
|
|
78
|
-
|
|
79
|
-
def get_root(self) -> Sequence[T]:
|
|
80
|
-
"""
|
|
81
|
-
Returns the list of root level items (i.e. items which have no parents) for the tree
|
|
82
|
-
:return: List of root level items (i.e. items which have no parents) for the tree
|
|
83
|
-
:return: List of root level items (i.e. items which have no parents) for the tree
|
|
84
|
-
"""
|
|
85
|
-
return self.root
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: qcanvas
|
|
3
|
-
Version: 0.0.5.7a0
|
|
4
|
-
Summary: A canvas client
|
|
5
|
-
Author: QCanvas
|
|
6
|
-
Classifier: Operating System :: OS Independent
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Requires-Python: <=3.12,>=3.11
|
|
9
|
-
Requires-Dist: aiosqlite-custom~=0.19.0
|
|
10
|
-
Requires-Dist: beautifulsoup4~=4.12.3
|
|
11
|
-
Requires-Dist: gql~=3.6.0b0
|
|
12
|
-
Requires-Dist: httpx~=0.26.0
|
|
13
|
-
Requires-Dist: packaging~=23.2
|
|
14
|
-
Requires-Dist: pydantic~=2.6.0
|
|
15
|
-
Requires-Dist: pyqtdarktheme~=2.1.0
|
|
16
|
-
Requires-Dist: pyside6~=6.6.1
|
|
17
|
-
Requires-Dist: qasync~=0.27.1
|
|
18
|
-
Requires-Dist: qenerate-custom~=0.6.3
|
|
19
|
-
Requires-Dist: requests~=2.31.0
|
|
20
|
-
Requires-Dist: sqlalchemy~=2.0.25
|
|
21
|
-
Requires-Dist: tenacity~=8.2.3
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
qcanvas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
qcanvas/__main__.py,sha256=FOj1TahcG6GSDppkpe09X2Grddc0sXu5Ql5YUABbG5w,5192
|
|
3
|
-
qcanvas/db/__init__.py,sha256=j5ZiTiLuY0CYRUV2bktc8qE5x-jBNaSaSVNKAF8DKF0,420
|
|
4
|
-
qcanvas/db/database.py,sha256=jKSExpMuq1x7GppeDW2mRHJznLGHeexf76wgvnBnNWQ,10557
|
|
5
|
-
qcanvas/db/db_converter_helper.py,sha256=-Rpe4CvMJBmvYXLw68caJWwEMPvxsYJB800Z8vHyqX4,2169
|
|
6
|
-
qcanvas/icons/__init__.py,sha256=nknAcvd97UBIYO1dR4atTCY3PHudbPprPT7FPyhqugc,35
|
|
7
|
-
qcanvas/icons/icons.qrc,sha256=_wGNdTTOuygPLeFMJxA3Q2HleqJKKkzW38VEhWJ76Rg,110
|
|
8
|
-
qcanvas/icons/main_icon.svg,sha256=26moo6Ki_YSkazCgOZgs4pi2p4qWNLDIWugwLMfAWfs,16104
|
|
9
|
-
qcanvas/icons/rc_icons.py,sha256=F8TS5WzBbKdNOm3uARzOU0O2ODpOlh4OPIddp1B-1P4,10896
|
|
10
|
-
qcanvas/icons/sync.svg,sha256=ArqfW_jH-eEjDnxyVPBBo2OOoDNja1OfN3KiptPs258,2835
|
|
11
|
-
qcanvas/net/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
qcanvas/net/custom_httpx_async_transport.py,sha256=1OyszJO8TrwDrq-no-angb-AIjgq_GiV67tQHuPxmm0,1227
|
|
13
|
-
qcanvas/net/self_authenticating.py,sha256=VkWpp7kxaWQHvqMjW9Dw-oY0aNFOT-6SYkJMnSvCdqU,4136
|
|
14
|
-
qcanvas/net/canvas/__init__.py,sha256=EPLFN05Vx9yNHDLK-eUp97C6ymtMTnscwdn28OBlNJw,96
|
|
15
|
-
qcanvas/net/canvas/canvas_client.py,sha256=yxhk3uxjkZFXkp-99Jmr7_GDqpa5OBtNBpQU6zQSGuc,8778
|
|
16
|
-
qcanvas/net/canvas/legacy_canvas_types.py,sha256=pKuTBh1m5oSq9e7jmbHzYfKeoPSw0uHAqLwddKZpsr4,3942
|
|
17
|
-
qcanvas/queries/__init__.py,sha256=tRUwMS1OvYIWRd4WpmAHrDrcelj_dLEaesxh9NngC6s,227
|
|
18
|
-
qcanvas/queries/all_courses.gql,sha256=8U0_3VOcD66SRCfvhZ9IOTmxVgDPJdMD_LuGPbM9vVU,124
|
|
19
|
-
qcanvas/queries/all_courses.py,sha256=-DpMy7eDFUeYp3ffZrwXp15qo_t6Lfx8K-rFi3OK1DM,2751
|
|
20
|
-
qcanvas/queries/canvas_course_data.gql,sha256=KJHjdu5sMKld4L6mPGfUrhqRLx_wdgGH8fwTnmRFGrI,1115
|
|
21
|
-
qcanvas/queries/canvas_course_data.py,sha256=_ZNv9stXAyap2eBbCqmajXVpWr0920ZUcCS6Gtyyo0g,4314
|
|
22
|
-
qcanvas/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
qcanvas/ui/container_item.py,sha256=MNGL3Z6dNlM59XXWqmXB1Ql3FTOEdA6TY2K9Do1LmTE,285
|
|
24
|
-
qcanvas/ui/main_ui.py,sha256=4OKDbY3l4f6lxOAiN6v9_yy1Ak1F8pyZ-lCySsqJABI,10219
|
|
25
|
-
qcanvas/ui/setup_dialog.py,sha256=Rh9Zj595-OdPUN31cN8QcY5V2giQ3zPRUg65t6wQ-LY,7270
|
|
26
|
-
qcanvas/ui/status_bar_reporter.py,sha256=uLX_C8r0augOSOwS-HCeDFxuSQKUJ8qYM5RE8kTKklU,1524
|
|
27
|
-
qcanvas/ui/menu_bar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
qcanvas/ui/menu_bar/grouping_preferences_menu.py,sha256=nSe9qt3pI9WAiKVTOEtHN9JPW3gaDxpU8YiSGGRjR5M,2407
|
|
29
|
-
qcanvas/ui/menu_bar/theme_selection_menu.py,sha256=WdcFMEO-FRYJm3qKjYbUpRhJPVe0-RhwrVxcyckSr0Q,1248
|
|
30
|
-
qcanvas/ui/viewer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
-
qcanvas/ui/viewer/course_list.py,sha256=umJr_zUUPuL4sGNfcaiDl-Vsodned5egTxEydAKdiCw,3421
|
|
32
|
-
qcanvas/ui/viewer/file_list.py,sha256=hqKXrLs4bLHA9himFEuE8d8Pq2TJnvuUGGJgZftiyrA,7869
|
|
33
|
-
qcanvas/ui/viewer/file_view_tab.py,sha256=DrXwHwIPBoHVjsKU3zUbOvA4tTarp29d4cMYvZnE-O8,2205
|
|
34
|
-
qcanvas/ui/viewer/page_list_viewer.py,sha256=FKFuqgy_yQPS_i3c0AbQk5E3mxtOfgYI7Q81mD28bLI,5678
|
|
35
|
-
qcanvas/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
qcanvas/util/app_settings.py,sha256=G_aq-7FPXmV4pN0cM2VldFS1HwTIzofgezgXt0y-AQ0,3017
|
|
37
|
-
qcanvas/util/constants.py,sha256=GNRlqG9rnWlW5ijKoJEsyuOWTdrXNSiWL_a5XeEZzI0,259
|
|
38
|
-
qcanvas/util/download_pool.py,sha256=8MRzdxZMmCibMx1rwh0G8U2SPjITtvDieLyr8drhJ7c,2003
|
|
39
|
-
qcanvas/util/progress_reporter.py,sha256=wgD8A_mrZ3EZQtrJeCYoQ0TvMIVtdFKDVNTQrbZMoDA,2934
|
|
40
|
-
qcanvas/util/self_updater.py,sha256=oItk-J-CSN74RGu4P_xtJ4S8vGnaAZL23Bm4jpLbLzg,1655
|
|
41
|
-
qcanvas/util/task_pool.py,sha256=NHrsgY8Srd4I9Iv3qqGLblF2RRqLqbLcYFgvnEGpvaY,8192
|
|
42
|
-
qcanvas/util/course_indexer/__init__.py,sha256=IqwTfB8KVk7Go_yuBL8NOANgFiBCzjt0z38D4CEgq7w,38
|
|
43
|
-
qcanvas/util/course_indexer/conversion_helpers.py,sha256=DZwx3Ct-MYzxQwuvqem5c2cARSkr_2SJvz0gaZDJq6c,2644
|
|
44
|
-
qcanvas/util/course_indexer/data_manager.py,sha256=-bqw28YZNoq-BcT3LsJl5RZS5tTpgk85wmfR2bgSCPc,18872
|
|
45
|
-
qcanvas/util/course_indexer/resource_helpers.py,sha256=htd3XnrnOEkS8iAxTqAIypgMzqJEcjs1Qte9wmF2ccQ,6966
|
|
46
|
-
qcanvas/util/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
|
-
qcanvas/util/helpers/canvas_sanitiser.py,sha256=c6r6cyJomQ1zn5wFK-LX-ZsZXQZ-A2awBFtCB_JqUtE,1184
|
|
48
|
-
qcanvas/util/helpers/file_icon_helper.py,sha256=6Rgy5K5wB6Hofsx4Etu6JlurAJEqma32zOLWgRHTf48,925
|
|
49
|
-
qcanvas/util/helpers/qaction_helper.py,sha256=lQ3eouIgR3w3o1jLNAmu2plAdc6HBBT5qaE4cnW6cX8,667
|
|
50
|
-
qcanvas/util/helpers/theme_helper.py,sha256=_qnXIMSeuXW3a8mincehHRtJgTv2Rm93Q3y2N7dW3Zo,1290
|
|
51
|
-
qcanvas/util/link_scanner/__init__.py,sha256=VVZ4L0AwC-TQtX6JQNUcpWH8UhKaCaSS3_7X4i3mipE,97
|
|
52
|
-
qcanvas/util/link_scanner/canvas_link_scanner.py,sha256=M7_zc7AU2pI9RdR8AaC9cyEiNufnu6bNxu4qu7FaLns,1451
|
|
53
|
-
qcanvas/util/link_scanner/canvas_media_object_scanner.py,sha256=8xnDtrdqxIzW-8JuoKXGUoB2usR6S8rzma_QB6rGzaI,2219
|
|
54
|
-
qcanvas/util/link_scanner/dropbox_scanner.py,sha256=_xIOGpPH5voORGPGLCEXcQCWWf-PVSBg8MEm-hcIs3M,2034
|
|
55
|
-
qcanvas/util/link_scanner/resource_scanner.py,sha256=EY5AzDXG6Ix18YpRkJcMjOPaygird6iD1akaG-yUyHc,1988
|
|
56
|
-
qcanvas/util/tree_util/__init__.py,sha256=yqjNHrD8SiyW_fGPfw7xdS-Rctjdu-RMWSkUxcGSn8k,154
|
|
57
|
-
qcanvas/util/tree_util/expanding_tree.py,sha256=5UM9kfcHtVavzAc9nwd7lucVnjI_dkVH8qC3GvmBvtc,6622
|
|
58
|
-
qcanvas/util/tree_util/model_helpers.py,sha256=io1bHOMNgVqkPvGH2Iohug1-wx2MwC_-qlWlpVA0fFE,743
|
|
59
|
-
qcanvas/util/tree_util/tree_model.py,sha256=JhlYmGqxlul8jUUQ-KtPXIehzgG-UtszHve7Wh-XbtE,2981
|
|
60
|
-
qcanvas-0.0.5.7a0.dist-info/METADATA,sha256=y1oIx74ntPhF74IYe1uVsBwXWefTp9wNjpcV1QPrlgo,650
|
|
61
|
-
qcanvas-0.0.5.7a0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
|
|
62
|
-
qcanvas-0.0.5.7a0.dist-info/RECORD,,
|
|
File without changes
|