qcanvas 0.0.5.2a0__py3-none-any.whl → 0.0.5.4a0__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/__main__.py CHANGED
@@ -22,9 +22,9 @@ from qcanvas.util.app_settings import settings
22
22
  from qcanvas.util.constants import app_name, updated_and_needs_restart_return_code
23
23
  from qcanvas.util.course_indexer import DataManager
24
24
  from qcanvas.util.helpers import theme_helper
25
- from qcanvas.util.linkscanner import CanvasFileScanner
26
- from qcanvas.util.linkscanner.canvas_media_object_scanner import CanvasMediaObjectScanner
27
- from qcanvas.util.linkscanner.dropbox_scanner import DropboxScanner
25
+ from qcanvas.util.link_scanner import CanvasFileScanner
26
+ from qcanvas.util.link_scanner.canvas_media_object_scanner import CanvasMediaObjectScanner
27
+ from qcanvas.util.link_scanner.dropbox_scanner import DropboxScanner
28
28
 
29
29
  engine = create_async_engine("sqlite+aiosqlite:///canvas_db.😘", echo=False)
30
30
 
@@ -58,8 +58,9 @@ class LoaderWindow(QMainWindow):
58
58
  @asyncSlot()
59
59
  async def on_init(self) -> None:
60
60
  try:
61
+ all_set = None not in [settings.api_key, settings.canvas_url, settings.panopto_url]
61
62
  # Verify that the canvas urls and api key are valid
62
- if not await CanvasClient.verify_config(settings.canvas_url, settings.api_key):
63
+ if not all_set or not await CanvasClient.verify_config(settings.canvas_url, settings.api_key):
63
64
  # Show the setup dialog
64
65
  self.setup.emit()
65
66
  else:
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import json
3
3
  import logging
4
- from typing import BinaryIO, AsyncIterator
4
+ from typing import BinaryIO, AsyncIterator, Any
5
5
 
6
6
  import gql
7
7
  import httpx
@@ -13,7 +13,7 @@ from tenacity import retry, wait_exponential, wait_random, stop_after_attempt, w
13
13
  import qcanvas.db as db
14
14
  from qcanvas.net.canvas.legacy_canvas_types import LegacyFile, LegacyPage
15
15
  from qcanvas.net.custom_httpx_async_transport import CustomHTTPXAsyncTransport
16
- from qcanvas.net.self_authenticating import SelfAuthenticating, AuthenticationException
16
+ from qcanvas.net.self_authenticating import SelfAuthenticatingWithHttpClient, AuthenticationException
17
17
 
18
18
 
19
19
  class RatelimitedException(Exception):
@@ -48,11 +48,7 @@ def detect_ratelimit_and_raise(response: Response) -> Response:
48
48
  return response
49
49
 
50
50
 
51
- def detect_authentication_needed(response: Response):
52
- return response.url.path == "/login/canvas" or response.status_code == 401
53
-
54
-
55
- class CanvasClient(SelfAuthenticating):
51
+ class CanvasClient(SelfAuthenticatingWithHttpClient):
56
52
  _logger = logging.getLogger("canvas_client")
57
53
  _net_op_sem = asyncio.Semaphore(20)
58
54
 
@@ -73,11 +69,9 @@ class CanvasClient(SelfAuthenticating):
73
69
  return response.is_success
74
70
 
75
71
  def __init__(self, canvas_url: URL, api_key: str, client: httpx.AsyncClient | None = None):
76
- super().__init__()
72
+ super().__init__(client=client or httpx.AsyncClient(timeout=60))
77
73
  self.api_key = api_key
78
74
  self.canvas_url = canvas_url
79
- self.client = client or httpx.AsyncClient(timeout=60)
80
- self.max_retries = 3
81
75
 
82
76
  def get_headers(self) -> dict[str, dict]:
83
77
  return {"headers": {"Authorization": f"Bearer {self.api_key}"}}
@@ -158,13 +152,7 @@ class CanvasClient(SelfAuthenticating):
158
152
  wait=wait_fixed(5) + wait_random(0, 1),
159
153
  retry=retry_if_exception_type(TransportQueryError)
160
154
  )
161
- async def do_graphql_query(self, query: gql.client.DocumentNode, **kwargs):
162
- """
163
- Executes a graphql query and reauthenticates the client if needed
164
- :param query:
165
- :param operation: The operation to execute
166
- :return: The result
167
- """
155
+ async def do_graphql_query(self, query: gql.client.DocumentNode, **kwargs) -> dict[str, Any]:
168
156
  async with self._net_op_sem:
169
157
  gql_transport = CustomHTTPXAsyncTransport(self.client, self.canvas_url.join("api/graphql"),
170
158
  **self.get_headers())
@@ -193,36 +181,13 @@ class CanvasClient(SelfAuthenticating):
193
181
  self._logger.warning("Gave up download of %s", resource.url)
194
182
  raise
195
183
 
196
- async def do_request_and_retry_if_unauthenticated(self, url: URL):
197
- """
198
- Executes a http request or reauthenticate and retries if needed
199
- :param url: The url of the request
200
- :return:
201
- """
202
- retries = 0
203
-
204
- # Make the initial request
205
- response = await self.client.get(url, **self.get_headers())
206
-
207
- # Retry if canvas is trying to get us to reauthenticate
208
- while (await self.reauthenticate_if_needed(response)) and retries < self.max_retries:
209
- response = await self.client.get(url, **self.get_headers())
210
- retries += 1
211
-
212
- return response.text
213
-
214
- async def reauthenticate_if_needed(self, response: Response):
215
- """
216
- Inspects a response and activates reauthentication if the response indicates we need to
217
- :param response: The response to inspect
218
- :return: True if reauthentication was activated, false if not
219
- """
220
-
221
- if detect_authentication_needed(response):
222
- await self.reauthenticate()
223
- return True
184
+ async def do_request_and_retry_if_unauthenticated(self, url: URL, method: str, **kwargs) -> httpx.Response:
185
+ # Add API token to the request
186
+ return await super().do_request_and_retry_if_unauthenticated(url, method, **kwargs, **self.get_headers())
224
187
 
225
- return False
188
+ def detect_authentication_needed(self, response: Response):
189
+ # Canvas will silently redirect to the login page or give a 401 if we are not authenticated
190
+ return response.url.path == "/login/canvas" or response.status_code == 401
226
191
 
227
192
  async def _authenticate(self):
228
193
  token_response = await self.client.get(self.canvas_url.join("login/session_token"), **self.get_headers())
@@ -1,6 +1,10 @@
1
1
  import logging
2
+ from abc import ABC, abstractmethod
2
3
  from asyncio import Lock, Event
3
4
 
5
+ import httpx
6
+ from httpx import URL
7
+
4
8
 
5
9
  class AuthenticationException(Exception):
6
10
  pass
@@ -9,14 +13,16 @@ class AuthenticationException(Exception):
9
13
  # httpx does have an authentication flow mechanism that allows you to also make other requests but I don't know if it
10
14
  # will behave the same way as this does. I also finished this before I found out that existed.
11
15
 
12
- class SelfAuthenticating:
16
+ class SelfAuthenticatingWithHttpClient(ABC):
13
17
  _logger = logging.getLogger(__name__)
14
18
 
15
- def __init__(self):
19
+ def __init__(self, max_retires: int = 3, client: httpx.AsyncClient = httpx.AsyncClient()):
16
20
  self.authentication_lock = Lock()
21
+ self.client = client
17
22
  self.authentication_in_progress: Event | None = None
23
+ self.max_retries = max_retires
18
24
 
19
- async def reauthenticate(self):
25
+ async def reauthenticate(self) -> None:
20
26
  """
21
27
  Re-authenticates the client
22
28
  """
@@ -55,8 +61,48 @@ class SelfAuthenticating:
55
61
  await event.wait()
56
62
  self._logger.info(f"Finished waiting for {self.__class__.__name__}")
57
63
 
64
+ async def do_request_and_retry_if_unauthenticated(self, url: URL, method: str, **kwargs) -> httpx.Response:
65
+ """
66
+ Executes a http request or reauthenticate and retries if needed
67
+ :param url: The url of the request
68
+ :param method: The method the request should use (post, get, etc)
69
+ :return:
70
+ """
71
+ retries = 0
72
+
73
+ async def make_request():
74
+ return await self.client.request(url=url, method=method, **kwargs)
75
+
76
+ # Make the initial request
77
+ response = await make_request()
78
+
79
+ # Retry if canvas is trying to get us to reauthenticate
80
+ while (await self.reauthenticate_if_needed(response)) and retries < self.max_retries:
81
+ response = await make_request()
82
+ retries += 1
83
+
84
+ return response
85
+
86
+ async def reauthenticate_if_needed(self, response: httpx.Response) -> bool:
87
+ """
88
+ Inspects a response and activates reauthentication if the response indicates we need to
89
+ :param response: The response to inspect
90
+ :return: True if reauthentication was activated, false if not
91
+ """
92
+
93
+ if self.detect_authentication_needed(response):
94
+ await self.reauthenticate()
95
+ return True
96
+
97
+ return False
98
+
99
+ @abstractmethod
100
+ def detect_authentication_needed(self, response: httpx.Response) -> bool:
101
+ ...
102
+
103
+ @abstractmethod
58
104
  async def _authenticate(self) -> None:
59
105
  """
60
106
  Authenticates the client
61
107
  """
62
- raise NotImplemented
108
+ ...
qcanvas/ui/main_ui.py CHANGED
@@ -128,7 +128,11 @@ class AppMainWindow(QMainWindow):
128
128
 
129
129
  @asyncSlot()
130
130
  async def open_quick_auth_in_browser(self):
131
+ opening_progress_dialog = QProgressDialog("Opening canvas", None, 0, 0, self)
132
+ opening_progress_dialog.setWindowTitle("Please wait")
133
+ opening_progress_dialog.show()
131
134
  QDesktopServices.openUrl(await self.data_manager.client.get_temp_session_link())
135
+ opening_progress_dialog.close()
132
136
 
133
137
  def closeEvent(self, event):
134
138
  settings.geometry = self.saveGeometry()
@@ -147,6 +151,8 @@ class AppMainWindow(QMainWindow):
147
151
 
148
152
  await self.data_manager.download_resource(resource)
149
153
  QDesktopServices.openUrl(QUrl.fromLocalFile(resource.download_location.absolute()))
154
+ else:
155
+ QDesktopServices.openUrl(url)
150
156
 
151
157
  @asyncSlot(QTreeWidgetItem, int)
152
158
  async def download_file_from_file_pane(self, item: QTreeWidgetItem, _: int):
@@ -234,6 +240,8 @@ class AppMainWindow(QMainWindow):
234
240
  else:
235
241
  self.selected_course = None
236
242
  self.file_viewer.clear()
243
+ self.pages_viewer.clear()
244
+ self.assignment_viewer.clear()
237
245
  self.course_name_label.setText(_no_course_selected_text)
238
246
 
239
247
  @asyncSlot(db.CoursePreferences)
@@ -51,12 +51,12 @@ class SetupDialog(QDialog):
51
51
 
52
52
  # Line edits for the different properties
53
53
  self.canvas_url = QLineEdit(settings.canvas_url or "")
54
- # self.panopto_url = QLineEdit()
54
+ self.panopto_url = QLineEdit()
55
55
  self.canvas_api_key = QLineEdit(settings.api_key or "")
56
56
 
57
57
  # Add the line edits to the dialog
58
58
  self._row("Canvas URL", self.canvas_url)
59
- # self._row("Painopto URL", self.panopto_url)
59
+ self._row("Painopto URL", self.panopto_url)
60
60
  self._row("Canvas API key", self.canvas_api_key)
61
61
 
62
62
  # Add the activity indicator to the dialog
@@ -93,15 +93,15 @@ class SetupDialog(QDialog):
93
93
  if self._operation_sem.acquire(False):
94
94
  try:
95
95
  canvas_url_text = self.ensure_protocol(self.canvas_url.text().strip())
96
- # panopto_url_text = ensure_protocol(self.panopto_url.text().strip())
96
+ panopto_url_text = self.ensure_protocol(self.panopto_url.text().strip())
97
97
  canvas_api_key_text = self.canvas_api_key.text().strip()
98
98
 
99
99
  if not (len(canvas_url_text) > 0 and QUrl(canvas_url_text).isValid()):
100
100
  self._show_invalid_msgbox("Canvas URL")
101
101
  return
102
- # if not (len(panopto_url_text) > 0 and QUrl(panopto_url_text).isValid()):
103
- # invalid("Panopto URL")
104
- # return
102
+ if not (len(panopto_url_text) > 0 and QUrl(panopto_url_text).isValid()):
103
+ self._show_invalid_msgbox("Panopto URL")
104
+ return
105
105
  elif not len(canvas_api_key_text) > 0:
106
106
  self._show_invalid_msgbox("API key")
107
107
  elif not (await self._verify_canvas_config(canvas_url_text, canvas_api_key_text)):
@@ -116,7 +116,7 @@ class SetupDialog(QDialog):
116
116
  # If nothing was wrong, everything should be fine
117
117
  # Save the url and api key
118
118
  settings.canvas_url = canvas_url_text
119
- # settings.panoptp_url = panopto_url_text
119
+ settings.panopto_url = panopto_url_text
120
120
  settings.api_key = canvas_api_key_text
121
121
 
122
122
  self.accept()
@@ -34,4 +34,7 @@ class StatusBarReporter(ProgressReporter):
34
34
  self.status_bar.showMessage("Finished", 5000)
35
35
 
36
36
  def errored(self, context: Any) -> None:
37
- pass
37
+ if self.status_bar.parent() is not None:
38
+ self.status_bar.removeWidget(self.progress_bar)
39
+ self.progress_bar = None
40
+ self.status_bar.showMessage("Synchronisation error!!", 5000)
@@ -11,7 +11,7 @@ from qcanvas.ui.container_item import ContainerItem
11
11
  from qcanvas.util.constants import default_assignments_module_names
12
12
  from qcanvas.util.course_indexer import resource_helpers
13
13
  from qcanvas.util.helpers import canvas_sanitiser
14
- from qcanvas.util.linkscanner import ResourceScanner
14
+ from qcanvas.util.link_scanner import ResourceScanner
15
15
 
16
16
 
17
17
  class LinkTransformer:
@@ -79,6 +79,11 @@ class PageLikeViewer(QWidget):
79
79
  self.model.setHorizontalHeaderLabels([self.header_name])
80
80
  self.tree.expandAll()
81
81
 
82
+ def clear(self):
83
+ self.model.clear()
84
+ self.viewer.clear()
85
+ self.model.setHorizontalHeaderLabels([self.header_name])
86
+
82
87
  @abstractmethod
83
88
  def _internal_fill_tree(self, data: db.Course):
84
89
  ...
@@ -85,6 +85,7 @@ class _AppSettings:
85
85
  auxiliary = QSettings("QCanvas", "ui")
86
86
 
87
87
  canvas_url: MappedSetting[str] = MappedSetting(settings, "canvas_url")
88
+ panopto_url: MappedSetting[str] = MappedSetting(settings, "panopto_url")
88
89
  api_key: MappedSetting[str] = MappedSetting(settings, "api_key")
89
90
 
90
91
  ignored_update: MappedSetting[Version] = MappedSetting(auxiliary, "ignored_update")
@@ -7,6 +7,7 @@ from typing import Sequence
7
7
 
8
8
  from gql import gql
9
9
  from sqlalchemy import select
10
+ from sqlalchemy.dialects.sqlite import insert as sqlite_upsert
10
11
  from sqlalchemy.ext.asyncio.session import async_sessionmaker as AsyncSessionMaker, AsyncSession
11
12
  from sqlalchemy.orm import selectin_polymorphic, selectinload
12
13
 
@@ -16,8 +17,8 @@ import qcanvas.util.course_indexer.conversion_helpers as conv_helper
16
17
  import qcanvas.util.course_indexer.resource_helpers as resource_helper
17
18
  from qcanvas.net.canvas import CanvasClient
18
19
  from qcanvas.util.download_pool import DownloadPool
19
- from qcanvas.util.linkscanner.canvas_link_scanner import canvas_resource_id_prefix
20
- from qcanvas.util.linkscanner.resource_scanner import ResourceScanner
20
+ from qcanvas.util.link_scanner.canvas_link_scanner import canvas_resource_id_prefix
21
+ from qcanvas.util.link_scanner.resource_scanner import ResourceScanner
21
22
  from qcanvas.util.progress_reporter import ProgressReporter, noop_reporter
22
23
  from qcanvas.util.task_pool import TaskPool
23
24
 
@@ -201,28 +202,32 @@ class DataManager:
201
202
  if not self._init_called:
202
203
  raise Exception("Init was not called")
203
204
 
204
- async with self._session_maker.begin() as session:
205
- # Load module pages/files for the courses
206
- await self._load_module_items(g_courses, session, progress_reporter)
205
+ try:
206
+ async with self._session_maker.begin() as session:
207
+ # Load module pages/files for the courses
208
+ await self._load_module_items(g_courses, session, progress_reporter)
207
209
 
208
- # Collect assignments from the courses
209
- assignments = []
210
+ # Collect assignments from the courses
211
+ assignments = []
210
212
 
211
- for g_course in g_courses:
212
- # Create needed data in the session
213
- term = await conv_helper.create_term(g_course, session)
214
- await conv_helper.create_course(g_course, session, term)
215
- await conv_helper.create_modules(g_course, session)
213
+ for g_course in g_courses:
214
+ # Create needed data in the session
215
+ term = await conv_helper.create_term(g_course, session)
216
+ await conv_helper.create_course(g_course, session, term)
217
+ await conv_helper.create_modules(g_course, session)
216
218
 
217
- # Add course assignments to the list
218
- assignments.extend(await conv_helper.create_assignments(g_course, session))
219
+ # Add course assignments to the list
220
+ assignments.extend(await conv_helper.create_assignments(g_course, session))
219
221
 
220
- # Scan assignments for resources
221
- await self._scan_assignments_for_resources(assignments, session, progress_reporter)
222
+ # Scan assignments for resources
223
+ await self._scan_assignments_for_resources(assignments, session, progress_reporter)
222
224
 
223
- # Add all resources back into the session
224
- session.add_all(self._resource_pool.results())
225
- progress_reporter.finished()
225
+ # Add all resources back into the session
226
+ session.add_all(self._resource_pool.results())
227
+ progress_reporter.finished()
228
+ except BaseException as e:
229
+ traceback.print_exc()
230
+ progress_reporter.errored(e)
226
231
 
227
232
  async def _scan_assignments_for_resources(self, assignments: Sequence[db.Assignment], session: AsyncSession,
228
233
  progress_reporter: ProgressReporter):
@@ -256,8 +261,13 @@ class DataManager:
256
261
 
257
262
  # Filter out pages that don't need updating
258
263
  pages_to_update = _prepare_out_of_date_pages_for_loading(g_courses, existing_pages)
264
+
265
+ if len(pages_to_update) == 0:
266
+ return
267
+
259
268
  # Load the content for all the pages that need updating
260
- module_items = await self._load_content_for_pages(pages_to_update, progress_reporter)
269
+ module_items: list[db.ModuleItem] = await self._load_content_for_pages(pages_to_update, progress_reporter)
270
+ module_pages = [item for item in module_items if isinstance(item, db.ModulePage)]
261
271
 
262
272
  # Link the resources found to the pages they were found on and add them to the database
263
273
  await resource_helper.create_module_item_resource_relations(
@@ -267,13 +277,43 @@ class DataManager:
267
277
  resource_pool=self._resource_pool,
268
278
  progress_reporter=progress_reporter,
269
279
  # Collect just the module pages for scanning
270
- items=[item for item in module_items if isinstance(item, db.ModulePage)]
280
+ items=module_pages
271
281
  ),
272
282
  session
273
283
  )
274
284
 
275
- # Add all the module items to the session
276
- session.add_all(module_items)
285
+ # empty inserts/upserts causes an sql error. don't do them
286
+ if len(module_pages) > 0:
287
+ # Add all the module items to the session
288
+ # shitty bandaid fix
289
+ upsert_item = sqlite_upsert(db.ModuleItem).values([self.moduleitem_dict(item) for item in module_pages])
290
+ upsert_item = upsert_item.on_conflict_do_update(
291
+ index_elements=[db.ModuleItem.id],
292
+ set_=dict(name=upsert_item.excluded.name, updated_at=upsert_item.excluded.updated_at,
293
+ position=upsert_item.excluded.position),
294
+
295
+ )
296
+
297
+ upsert_page = sqlite_upsert(db.ModulePage).values([self.page_dict(item) for item in module_pages])
298
+ upsert_page = upsert_page.on_conflict_do_update(
299
+ index_elements=[db.ModulePage.id],
300
+ set_=dict(content=upsert_page.excluded.content)
301
+ )
302
+
303
+ await session.execute(upsert_item)
304
+ await session.execute(upsert_page)
305
+
306
+ session.add_all([item for item in module_items if isinstance(item, db.ModuleFile)])
307
+
308
+ @staticmethod
309
+ def page_dict(page: db.ModulePage) -> dict[str, object]:
310
+ return {"id": page.id, "content": page.content}
311
+
312
+ @staticmethod
313
+ def moduleitem_dict(page: db.ModuleItem) -> dict[str, object]:
314
+ return {"id": page.id, "name": page.name, "updated_at": page.updated_at, "position": page.position,
315
+ "module_id": page.module_id, "course_id": page.course_id, "type": page.type,
316
+ "created_at": page.created_at}
277
317
 
278
318
  def _add_resources_and_pages_to_taskpool(self, existing_pages: Sequence[db.ModuleItem],
279
319
  existing_resources: Sequence[db.Resource]):
@@ -293,6 +333,7 @@ class DataManager:
293
333
  The pages to load
294
334
  Returns
295
335
  -------
336
+ list
296
337
  The list of complete pages with page content loaded.
297
338
  """
298
339
  progress = progress_reporter.section("Loading page content", len(pages))
@@ -7,11 +7,11 @@ from bs4 import Tag, BeautifulSoup
7
7
  from sqlalchemy.ext.asyncio import AsyncSession
8
8
 
9
9
  import qcanvas.db as db
10
- from qcanvas.util.linkscanner import ResourceScanner
10
+ from qcanvas.util.link_scanner import ResourceScanner
11
11
  from qcanvas.util.progress_reporter import ProgressReporter
12
12
  from qcanvas.util.task_pool import TaskPool
13
13
 
14
- _logger = logging.getLogger()
14
+ _logger = logging.getLogger(__name__)
15
15
 
16
16
  resource_elements = ["a", "iframe", "img"]
17
17
 
@@ -63,6 +63,7 @@ async def create_assignment_resource_relations(relations: Sequence[TransientReso
63
63
  )
64
64
 
65
65
 
66
+ # todo change resource system to think of resources as links on a page with a shallow id (that may be the same as the deep id) which links to one or more deep ids
66
67
  async def find_resources_in_pages(link_scanners: Sequence[ResourceScanner], resource_pool: TaskPool[db.Resource],
67
68
  items: Sequence[db.PageLike], progress_reporter: ProgressReporter) -> list[
68
69
  TransientResourceToPageLink]:
@@ -3,7 +3,7 @@ from httpx import URL
3
3
 
4
4
  from qcanvas import db as db
5
5
  from qcanvas.net.canvas import CanvasClient
6
- from qcanvas.util.linkscanner.resource_scanner import ResourceScanner
6
+ from qcanvas.util.link_scanner.resource_scanner import ResourceScanner
7
7
 
8
8
  canvas_resource_id_prefix = "canvas_file"
9
9
 
@@ -5,7 +5,7 @@ from bs4 import Tag, BeautifulSoup
5
5
  from httpx import AsyncClient
6
6
 
7
7
  from qcanvas import db as db
8
- from qcanvas.util.linkscanner import ResourceScanner
8
+ from qcanvas.util.link_scanner import ResourceScanner
9
9
 
10
10
 
11
11
  class CanvasMediaObjectScanner(ResourceScanner):
@@ -3,7 +3,7 @@ from bs4 import Tag
3
3
  from httpx import URL
4
4
 
5
5
  from qcanvas import db as db
6
- from qcanvas.util.linkscanner import ResourceScanner
6
+ from qcanvas.util.link_scanner import ResourceScanner
7
7
 
8
8
 
9
9
  # from httpx import URL
qcanvas/util/task_pool.py CHANGED
@@ -225,9 +225,9 @@ class TaskPool(Generic[T]):
225
225
 
226
226
  return list(filter(filter_func, self._results.values()))
227
227
 
228
- def get_completed_result(self, task_id: object) -> T | None:
228
+ def get_completed_result_or_nothing(self, task_id: object, default: T | None = None) -> T | None:
229
229
  """
230
- Returns the result of an already completed task
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
231
 
232
232
  Returns
233
233
  -------
@@ -246,8 +246,8 @@ class TaskPool(Generic[T]):
246
246
  result = self._results[task_id]
247
247
 
248
248
  if isinstance(result, asyncio.Event):
249
- raise ValueError(f"{task_id} is still in progress")
249
+ return None
250
250
 
251
251
  return result
252
252
  else:
253
- raise KeyError(f"{task_id} has not been started")
253
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qcanvas
3
- Version: 0.0.5.2a0
3
+ Version: 0.0.5.4a0
4
4
  Summary: A canvas client
5
5
  Author: QCanvas
6
6
  Classifier: Operating System :: OS Independent
@@ -8,7 +8,6 @@ Classifier: Programming Language :: Python :: 3
8
8
  Requires-Python: <=3.12,>=3.11
9
9
  Requires-Dist: aiosqlite-custom~=0.19.0
10
10
  Requires-Dist: beautifulsoup4~=4.12.3
11
- Requires-Dist: bs4~=0.0.1
12
11
  Requires-Dist: gql~=3.6.0b0
13
12
  Requires-Dist: httpx~=0.26.0
14
13
  Requires-Dist: packaging~=23.2
@@ -1,5 +1,5 @@
1
1
  qcanvas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- qcanvas/__main__.py,sha256=7a0-bsPIZ7XYEUZNFAGdsVLkyI1j93vC-E_9r_rpvqU,5157
2
+ qcanvas/__main__.py,sha256=zXR8JG5PpiupENn-D5oK3l6dUCuIZKVVAx2JSxy6Lok,5271
3
3
  qcanvas/db/__init__.py,sha256=j5ZiTiLuY0CYRUV2bktc8qE5x-jBNaSaSVNKAF8DKF0,420
4
4
  qcanvas/db/database.py,sha256=pz3wZ2QwxalSruV-5vID3QDImbX9AD-rkGXRqiaIPmw,10515
5
5
  qcanvas/db/db_converter_helper.py,sha256=-Rpe4CvMJBmvYXLw68caJWwEMPvxsYJB800Z8vHyqX4,2169
@@ -9,9 +9,9 @@ qcanvas/icons/main_icon.svg,sha256=26moo6Ki_YSkazCgOZgs4pi2p4qWNLDIWugwLMfAWfs,1
9
9
  qcanvas/icons/rc_icons.py,sha256=F8TS5WzBbKdNOm3uARzOU0O2ODpOlh4OPIddp1B-1P4,10896
10
10
  qcanvas/net/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  qcanvas/net/custom_httpx_async_transport.py,sha256=1OyszJO8TrwDrq-no-angb-AIjgq_GiV67tQHuPxmm0,1227
12
- qcanvas/net/self_authenticating.py,sha256=bF0lq9BgDQg5We4l0Z_JpJhI8ZGC7TTgggMWHUOYw3U,2515
12
+ qcanvas/net/self_authenticating.py,sha256=VkWpp7kxaWQHvqMjW9Dw-oY0aNFOT-6SYkJMnSvCdqU,4136
13
13
  qcanvas/net/canvas/__init__.py,sha256=EPLFN05Vx9yNHDLK-eUp97C6ymtMTnscwdn28OBlNJw,96
14
- qcanvas/net/canvas/canvas_client.py,sha256=eENktdmVnyd6eIh6I4dH-5mU09pfJLo-v1xQ8Oma26k,9674
14
+ qcanvas/net/canvas/canvas_client.py,sha256=yxhk3uxjkZFXkp-99Jmr7_GDqpa5OBtNBpQU6zQSGuc,8778
15
15
  qcanvas/net/canvas/legacy_canvas_types.py,sha256=pKuTBh1m5oSq9e7jmbHzYfKeoPSw0uHAqLwddKZpsr4,3942
16
16
  qcanvas/queries/__init__.py,sha256=tRUwMS1OvYIWRd4WpmAHrDrcelj_dLEaesxh9NngC6s,227
17
17
  qcanvas/queries/all_courses.gql,sha256=8U0_3VOcD66SRCfvhZ9IOTmxVgDPJdMD_LuGPbM9vVU,124
@@ -20,9 +20,9 @@ qcanvas/queries/canvas_course_data.gql,sha256=KJHjdu5sMKld4L6mPGfUrhqRLx_wdgGH8f
20
20
  qcanvas/queries/canvas_course_data.py,sha256=_ZNv9stXAyap2eBbCqmajXVpWr0920ZUcCS6Gtyyo0g,4314
21
21
  qcanvas/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  qcanvas/ui/container_item.py,sha256=MNGL3Z6dNlM59XXWqmXB1Ql3FTOEdA6TY2K9Do1LmTE,285
23
- qcanvas/ui/main_ui.py,sha256=2vpBJ_5DZit8ZYo-fIWk7x9zRarVwO-zu1cnXm5QMMc,9824
24
- qcanvas/ui/setup_dialog.py,sha256=BXDNDd5IjA0l-9teUYsOCYMRXxUl4Ryxx64GsxJX5xU,7261
25
- qcanvas/ui/status_bar_reporter.py,sha256=6Qp0i-vUq9Yxowx0BSB4ieR8BO37iKjRNvPQ1AHUXtI,1318
23
+ qcanvas/ui/main_ui.py,sha256=S6lQ_kqwunQcByCOMZbp2HQu5RjmQjwMeMAnIZebjoA,10188
24
+ qcanvas/ui/setup_dialog.py,sha256=Rh9Zj595-OdPUN31cN8QcY5V2giQ3zPRUg65t6wQ-LY,7270
25
+ qcanvas/ui/status_bar_reporter.py,sha256=uLX_C8r0augOSOwS-HCeDFxuSQKUJ8qYM5RE8kTKklU,1524
26
26
  qcanvas/ui/menu_bar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  qcanvas/ui/menu_bar/grouping_preferences_menu.py,sha256=nSe9qt3pI9WAiKVTOEtHN9JPW3gaDxpU8YiSGGRjR5M,2407
28
28
  qcanvas/ui/menu_bar/theme_selection_menu.py,sha256=WdcFMEO-FRYJm3qKjYbUpRhJPVe0-RhwrVxcyckSr0Q,1248
@@ -30,32 +30,32 @@ qcanvas/ui/viewer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
30
30
  qcanvas/ui/viewer/course_list.py,sha256=jg_t66uyZpWyBITRWBFHaYgYMvWJ5useMljWv4tfOx4,3413
31
31
  qcanvas/ui/viewer/file_list.py,sha256=hqKXrLs4bLHA9himFEuE8d8Pq2TJnvuUGGJgZftiyrA,7869
32
32
  qcanvas/ui/viewer/file_view_tab.py,sha256=DrXwHwIPBoHVjsKU3zUbOvA4tTarp29d4cMYvZnE-O8,2205
33
- qcanvas/ui/viewer/page_list_viewer.py,sha256=-5wtDEX6TMSMNYyCxWAyf2nIBuIEPSgcmSaiDNcWQrQ,5535
33
+ qcanvas/ui/viewer/page_list_viewer.py,sha256=FKFuqgy_yQPS_i3c0AbQk5E3mxtOfgYI7Q81mD28bLI,5678
34
34
  qcanvas/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- qcanvas/util/app_settings.py,sha256=B9hIfl6wQZkBflM1_UwpCEd935k7sckegXXwKMbSnYo,2940
35
+ qcanvas/util/app_settings.py,sha256=G_aq-7FPXmV4pN0cM2VldFS1HwTIzofgezgXt0y-AQ0,3017
36
36
  qcanvas/util/constants.py,sha256=GNRlqG9rnWlW5ijKoJEsyuOWTdrXNSiWL_a5XeEZzI0,259
37
37
  qcanvas/util/download_pool.py,sha256=8MRzdxZMmCibMx1rwh0G8U2SPjITtvDieLyr8drhJ7c,2003
38
38
  qcanvas/util/progress_reporter.py,sha256=wgD8A_mrZ3EZQtrJeCYoQ0TvMIVtdFKDVNTQrbZMoDA,2934
39
39
  qcanvas/util/self_updater.py,sha256=oItk-J-CSN74RGu4P_xtJ4S8vGnaAZL23Bm4jpLbLzg,1655
40
- qcanvas/util/task_pool.py,sha256=kizChooliF7O9w5LtgBd87LHT_M1aGXiPoJ-M8Lg2RY,8159
40
+ qcanvas/util/task_pool.py,sha256=NHrsgY8Srd4I9Iv3qqGLblF2RRqLqbLcYFgvnEGpvaY,8192
41
41
  qcanvas/util/course_indexer/__init__.py,sha256=IqwTfB8KVk7Go_yuBL8NOANgFiBCzjt0z38D4CEgq7w,38
42
42
  qcanvas/util/course_indexer/conversion_helpers.py,sha256=DZwx3Ct-MYzxQwuvqem5c2cARSkr_2SJvz0gaZDJq6c,2644
43
- qcanvas/util/course_indexer/data_manager.py,sha256=AsDP-PLzFquL-d0l9uFyAihcfLbRGuCYoQV4uySk_uE,17045
44
- qcanvas/util/course_indexer/resource_helpers.py,sha256=54XnTbcq8RG1zSDifycSYdpQ007_aQrFFXkibMO7l8k,6794
43
+ qcanvas/util/course_indexer/data_manager.py,sha256=qqcrd5OnZqygpINxVDSJ-AkxOoCDgEiTdZ6w8-oNlNM,18903
44
+ qcanvas/util/course_indexer/resource_helpers.py,sha256=htd3XnrnOEkS8iAxTqAIypgMzqJEcjs1Qte9wmF2ccQ,6966
45
45
  qcanvas/util/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  qcanvas/util/helpers/canvas_sanitiser.py,sha256=c6r6cyJomQ1zn5wFK-LX-ZsZXQZ-A2awBFtCB_JqUtE,1184
47
47
  qcanvas/util/helpers/file_icon_helper.py,sha256=6Rgy5K5wB6Hofsx4Etu6JlurAJEqma32zOLWgRHTf48,925
48
48
  qcanvas/util/helpers/qaction_helper.py,sha256=lQ3eouIgR3w3o1jLNAmu2plAdc6HBBT5qaE4cnW6cX8,667
49
49
  qcanvas/util/helpers/theme_helper.py,sha256=5rvKdYwmPMWDlSWuRBpEcixyq_iBeKDoEfvmiy5ZbLQ,1210
50
- qcanvas/util/linkscanner/__init__.py,sha256=VVZ4L0AwC-TQtX6JQNUcpWH8UhKaCaSS3_7X4i3mipE,97
51
- qcanvas/util/linkscanner/canvas_link_scanner.py,sha256=mRvahbf_sClGTflc9nFKEbi5HltbIfWZu2qE-IhJO2o,1450
52
- qcanvas/util/linkscanner/canvas_media_object_scanner.py,sha256=0PL0buW53YQaOlT-n-UazwHFQS91ivfJN5kiw6tcfa4,2218
53
- qcanvas/util/linkscanner/dropbox_scanner.py,sha256=HB7FhZp3dQwvq1Ptkug7NEJu5yS4UEd4Xy19EZUIZ7k,2033
54
- qcanvas/util/linkscanner/resource_scanner.py,sha256=EY5AzDXG6Ix18YpRkJcMjOPaygird6iD1akaG-yUyHc,1988
50
+ qcanvas/util/link_scanner/__init__.py,sha256=VVZ4L0AwC-TQtX6JQNUcpWH8UhKaCaSS3_7X4i3mipE,97
51
+ qcanvas/util/link_scanner/canvas_link_scanner.py,sha256=M7_zc7AU2pI9RdR8AaC9cyEiNufnu6bNxu4qu7FaLns,1451
52
+ qcanvas/util/link_scanner/canvas_media_object_scanner.py,sha256=8xnDtrdqxIzW-8JuoKXGUoB2usR6S8rzma_QB6rGzaI,2219
53
+ qcanvas/util/link_scanner/dropbox_scanner.py,sha256=_xIOGpPH5voORGPGLCEXcQCWWf-PVSBg8MEm-hcIs3M,2034
54
+ qcanvas/util/link_scanner/resource_scanner.py,sha256=EY5AzDXG6Ix18YpRkJcMjOPaygird6iD1akaG-yUyHc,1988
55
55
  qcanvas/util/tree_util/__init__.py,sha256=yqjNHrD8SiyW_fGPfw7xdS-Rctjdu-RMWSkUxcGSn8k,154
56
56
  qcanvas/util/tree_util/expanding_tree.py,sha256=5UM9kfcHtVavzAc9nwd7lucVnjI_dkVH8qC3GvmBvtc,6622
57
57
  qcanvas/util/tree_util/model_helpers.py,sha256=io1bHOMNgVqkPvGH2Iohug1-wx2MwC_-qlWlpVA0fFE,743
58
58
  qcanvas/util/tree_util/tree_model.py,sha256=JhlYmGqxlul8jUUQ-KtPXIehzgG-UtszHve7Wh-XbtE,2981
59
- qcanvas-0.0.5.2a0.dist-info/METADATA,sha256=sYVc14zPHkIRzrCAslhOP8ABZspmiU-ciMj4sAskgQ4,676
60
- qcanvas-0.0.5.2a0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
61
- qcanvas-0.0.5.2a0.dist-info/RECORD,,
59
+ qcanvas-0.0.5.4a0.dist-info/METADATA,sha256=Fjv5DyBlWKM2P_rQhrJkLUojcKrnQUyvup_TpaYdOdU,650
60
+ qcanvas-0.0.5.4a0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
61
+ qcanvas-0.0.5.4a0.dist-info/RECORD,,
File without changes