qcanvas 0.0.5.6a0__py3-none-any.whl → 1.0.3.post0__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.

Files changed (114) hide show
  1. qcanvas/app_start/__init__.py +47 -0
  2. qcanvas/backend_connectors/__init__.py +2 -0
  3. qcanvas/backend_connectors/frontend_resource_manager.py +63 -0
  4. qcanvas/backend_connectors/qcanvas_task_master.py +28 -0
  5. qcanvas/icons/__init__.py +6 -0
  6. qcanvas/icons/file-download-failed.svg +6 -0
  7. qcanvas/icons/file-downloaded.svg +6 -0
  8. qcanvas/icons/file-not-downloaded.svg +6 -0
  9. qcanvas/icons/file-unknown.svg +6 -0
  10. qcanvas/icons/icons.qrc +4 -0
  11. qcanvas/icons/main_icon.svg +7 -7
  12. qcanvas/icons/rc_icons.py +580 -214
  13. qcanvas/icons/sync.svg +7 -0
  14. qcanvas/run.py +29 -0
  15. qcanvas/ui/course_viewer/__init__.py +2 -0
  16. qcanvas/ui/course_viewer/content_tree.py +123 -0
  17. qcanvas/ui/course_viewer/course_tree.py +93 -0
  18. qcanvas/ui/course_viewer/course_viewer.py +62 -0
  19. qcanvas/ui/course_viewer/tabs/__init__.py +3 -0
  20. qcanvas/ui/course_viewer/tabs/assignment_tab/__init__.py +1 -0
  21. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +168 -0
  22. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +104 -0
  23. qcanvas/ui/course_viewer/tabs/content_tab.py +96 -0
  24. qcanvas/ui/course_viewer/tabs/mail_tab/__init__.py +1 -0
  25. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +68 -0
  26. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +70 -0
  27. qcanvas/ui/course_viewer/tabs/page_tab/__init__.py +1 -0
  28. qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +36 -0
  29. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +74 -0
  30. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +176 -0
  31. qcanvas/ui/course_viewer/tabs/util.py +1 -0
  32. qcanvas/ui/main_ui/course_viewer_container.py +52 -0
  33. qcanvas/ui/main_ui/options/__init__.py +3 -0
  34. qcanvas/ui/main_ui/options/quick_sync_option.py +25 -0
  35. qcanvas/ui/main_ui/options/sync_on_start_option.py +25 -0
  36. qcanvas/ui/main_ui/qcanvas_window.py +192 -0
  37. qcanvas/ui/main_ui/status_bar_progress_display.py +153 -0
  38. qcanvas/ui/memory_tree/__init__.py +2 -0
  39. qcanvas/ui/memory_tree/_tree_memory.py +66 -0
  40. qcanvas/ui/memory_tree/memory_tree_widget.py +133 -0
  41. qcanvas/ui/memory_tree/memory_tree_widget_item.py +19 -0
  42. qcanvas/ui/setup/__init__.py +2 -0
  43. qcanvas/ui/setup/setup_checker.py +17 -0
  44. qcanvas/ui/setup/setup_dialog.py +212 -0
  45. qcanvas/util/__init__.py +2 -0
  46. qcanvas/util/basic_fonts.py +12 -0
  47. qcanvas/util/fe_resource_manager.py +23 -0
  48. qcanvas/util/html_cleaner.py +25 -0
  49. qcanvas/util/layouts.py +52 -0
  50. qcanvas/util/logs.py +6 -0
  51. qcanvas/util/paths.py +41 -0
  52. qcanvas/util/settings/__init__.py +9 -0
  53. qcanvas/util/settings/_client_settings.py +29 -0
  54. qcanvas/util/settings/_mapped_setting.py +63 -0
  55. qcanvas/util/settings/_ui_settings.py +34 -0
  56. qcanvas/util/ui_tools.py +41 -0
  57. qcanvas/util/url_checker.py +13 -0
  58. qcanvas-1.0.3.post0.dist-info/METADATA +61 -0
  59. qcanvas-1.0.3.post0.dist-info/RECORD +64 -0
  60. {qcanvas-0.0.5.6a0.dist-info → qcanvas-1.0.3.post0.dist-info}/WHEEL +1 -1
  61. qcanvas-1.0.3.post0.dist-info/entry_points.txt +3 -0
  62. qcanvas/__main__.py +0 -155
  63. qcanvas/db/__init__.py +0 -5
  64. qcanvas/db/database.py +0 -337
  65. qcanvas/db/db_converter_helper.py +0 -81
  66. qcanvas/net/canvas/__init__.py +0 -2
  67. qcanvas/net/canvas/canvas_client.py +0 -209
  68. qcanvas/net/canvas/legacy_canvas_types.py +0 -124
  69. qcanvas/net/custom_httpx_async_transport.py +0 -34
  70. qcanvas/net/self_authenticating.py +0 -108
  71. qcanvas/queries/__init__.py +0 -4
  72. qcanvas/queries/all_courses.gql +0 -7
  73. qcanvas/queries/all_courses.py +0 -108
  74. qcanvas/queries/canvas_course_data.gql +0 -51
  75. qcanvas/queries/canvas_course_data.py +0 -143
  76. qcanvas/ui/container_item.py +0 -11
  77. qcanvas/ui/main_ui.py +0 -249
  78. qcanvas/ui/menu_bar/__init__.py +0 -0
  79. qcanvas/ui/menu_bar/grouping_preferences_menu.py +0 -61
  80. qcanvas/ui/menu_bar/theme_selection_menu.py +0 -39
  81. qcanvas/ui/setup_dialog.py +0 -190
  82. qcanvas/ui/status_bar_reporter.py +0 -40
  83. qcanvas/ui/viewer/__init__.py +0 -0
  84. qcanvas/ui/viewer/course_list.py +0 -96
  85. qcanvas/ui/viewer/file_list.py +0 -195
  86. qcanvas/ui/viewer/file_view_tab.py +0 -62
  87. qcanvas/ui/viewer/page_list_viewer.py +0 -150
  88. qcanvas/util/app_settings.py +0 -98
  89. qcanvas/util/constants.py +0 -5
  90. qcanvas/util/course_indexer/__init__.py +0 -1
  91. qcanvas/util/course_indexer/conversion_helpers.py +0 -78
  92. qcanvas/util/course_indexer/data_manager.py +0 -447
  93. qcanvas/util/course_indexer/resource_helpers.py +0 -191
  94. qcanvas/util/download_pool.py +0 -58
  95. qcanvas/util/helpers/__init__.py +0 -0
  96. qcanvas/util/helpers/canvas_sanitiser.py +0 -47
  97. qcanvas/util/helpers/file_icon_helper.py +0 -34
  98. qcanvas/util/helpers/qaction_helper.py +0 -25
  99. qcanvas/util/helpers/theme_helper.py +0 -45
  100. qcanvas/util/link_scanner/__init__.py +0 -2
  101. qcanvas/util/link_scanner/canvas_link_scanner.py +0 -41
  102. qcanvas/util/link_scanner/canvas_media_object_scanner.py +0 -60
  103. qcanvas/util/link_scanner/dropbox_scanner.py +0 -68
  104. qcanvas/util/link_scanner/resource_scanner.py +0 -69
  105. qcanvas/util/progress_reporter.py +0 -101
  106. qcanvas/util/self_updater.py +0 -55
  107. qcanvas/util/task_pool.py +0 -253
  108. qcanvas/util/tree_util/__init__.py +0 -3
  109. qcanvas/util/tree_util/expanding_tree.py +0 -165
  110. qcanvas/util/tree_util/model_helpers.py +0 -36
  111. qcanvas/util/tree_util/tree_model.py +0 -85
  112. qcanvas-0.0.5.6a0.dist-info/METADATA +0 -21
  113. qcanvas-0.0.5.6a0.dist-info/RECORD +0 -61
  114. /qcanvas/{net → ui/main_ui}/__init__.py +0 -0
@@ -1,209 +0,0 @@
1
- import asyncio
2
- import json
3
- import logging
4
- from typing import BinaryIO, AsyncIterator, Any
5
-
6
- import gql
7
- import httpx
8
- from gql.transport.exceptions import TransportQueryError
9
- from httpx import URL, Response
10
- from tenacity import retry, wait_exponential, wait_random, stop_after_attempt, wait_fixed, \
11
- retry_if_exception_type
12
-
13
- import qcanvas.db as db
14
- from qcanvas.net.canvas.legacy_canvas_types import LegacyFile, LegacyPage
15
- from qcanvas.net.custom_httpx_async_transport import CustomHTTPXAsyncTransport
16
- from qcanvas.net.self_authenticating import SelfAuthenticatingWithHttpClient, AuthenticationException
17
-
18
-
19
- class RatelimitedException(Exception):
20
- def __init__(self):
21
- super().__init__("Canvas is ratelimiting me yay")
22
-
23
-
24
- class UnauthenticatedException(Exception):
25
- def __init__(self):
26
- super().__init__("Trust me bro i'm authenticated")
27
-
28
-
29
- def detect_unauthenticated_and_raise(response: Response) -> Response:
30
- """
31
- Detects if a response is redirecting us to the login page or is giving us a 401 (unauthorised)
32
- :param response: The response to check
33
- :return: True if reauthentication is needed
34
- """
35
- if response.url.path == "/login/canvas" or response.status_code == 401:
36
- raise UnauthenticatedException()
37
-
38
- return response
39
-
40
-
41
- def detect_ratelimit_and_raise(response: Response) -> Response:
42
- # Who the FUCK decided to use 403 instead of 429?? With this stupid message??
43
- # Fuck you instructure, learn to code
44
- # And the newline at the end for some fucking reason is the cherry on top...
45
- if response.status_code == 403 and response.text == "403 Forbidden (Rate Limit Exceeded)\n":
46
- raise RatelimitedException()
47
-
48
- return response
49
-
50
-
51
- class CanvasClient(SelfAuthenticatingWithHttpClient):
52
- _logger = logging.getLogger("canvas_client")
53
- _net_op_sem = asyncio.Semaphore(20)
54
-
55
- @staticmethod
56
- async def verify_config(canvas_url: str, api_key: str) -> bool:
57
- """
58
- Makes a request to canvas to verify that the url and key are correct.
59
- :param canvas_url: The canvas url to verify
60
- :param api_key: The api key to verify
61
- :return: True if everything looks ok, false if the api key or url is wrong
62
- """
63
- client = httpx.AsyncClient()
64
- # Make a request to an endpoint that returns very little/no data (for students at least) to check if everything
65
- # is working
66
- response = await client.get(url=URL(canvas_url).join("api/v1/accounts"),
67
- headers={"Authorization": f"Bearer {api_key}"}, timeout=10)
68
-
69
- return response.is_success
70
-
71
- def __init__(self, canvas_url: URL, api_key: str, client: httpx.AsyncClient | None = None):
72
- super().__init__(client=client or httpx.AsyncClient(timeout=60))
73
- self.api_key = api_key
74
- self.canvas_url = canvas_url
75
-
76
- def get_headers(self) -> dict[str, dict]:
77
- return {"headers": {"Authorization": f"Bearer {self.api_key}"}}
78
-
79
- @retry(
80
- wait=wait_exponential(exp_base=1.2, max=10) + wait_random(0, 1),
81
- retry=retry_if_exception_type(RatelimitedException),
82
- stop=stop_after_attempt(8)
83
- )
84
- async def get_courses(self):
85
- with self._net_op_sem:
86
- return await self.client.get(self.canvas_url.join("api/v1/courses"))
87
-
88
- @retry(
89
- wait=wait_exponential(exp_base=1.2, max=10) + wait_random(0, 1),
90
- retry=retry_if_exception_type(RatelimitedException),
91
- stop=stop_after_attempt(8)
92
- )
93
- async def get_page(self, page_id: str | int, course_id: str | int) -> LegacyPage:
94
- async with self._net_op_sem:
95
- response = detect_ratelimit_and_raise(
96
- await self.client.get(self.canvas_url.join(f"api/v1/courses/{course_id}/pages/{page_id}"),
97
- **self.get_headers()))
98
-
99
- return LegacyPage.from_dict(json.loads(response.text))
100
-
101
- @retry(
102
- wait=wait_exponential(exp_base=1.2, max=10) + wait_random(0, 1),
103
- retry=retry_if_exception_type(RatelimitedException),
104
- stop=stop_after_attempt(8)
105
- )
106
- async def get_file(self, file_id: str | int, course_id: str | int) -> LegacyFile:
107
- async with self._net_op_sem:
108
- response = detect_ratelimit_and_raise(
109
- await self.client.get(self.canvas_url.join(f"api/v1/courses/{course_id}/files/{file_id}"),
110
- **self.get_headers()))
111
-
112
- response.raise_for_status()
113
-
114
- return LegacyFile.from_dict(json.loads(response.text))
115
-
116
- @retry(
117
- wait=wait_exponential(exp_base=1.2, max=10) + wait_random(0, 1),
118
- retry=retry_if_exception_type(RatelimitedException),
119
- stop=stop_after_attempt(8)
120
- )
121
- async def get_file_from_endpoint(self, endpoint_url: str) -> LegacyFile:
122
- async with self._net_op_sem:
123
- response = detect_ratelimit_and_raise(await self.client.get(endpoint_url, **self.get_headers()))
124
-
125
- response.raise_for_status()
126
-
127
- return LegacyFile.from_dict(json.loads(response.text))
128
-
129
- @retry(
130
- wait=wait_exponential(exp_base=1.2, max=10) + wait_random(0, 1),
131
- retry=retry_if_exception_type(RatelimitedException),
132
- stop=stop_after_attempt(8)
133
- )
134
- async def get_temp_session_link(self) -> str | None:
135
- """
136
- Gets the link which will authenticate a browser/open canvas using a 'legacy session token'
137
- Returns
138
- -------
139
- str | None
140
- The url as a string if the request succeeded, None if it didn't
141
- """
142
-
143
- token_response = await self.client.get(self.canvas_url.join("login/session_token"), **self.get_headers())
144
-
145
- if token_response.is_success:
146
- return json.loads(token_response.text)["session_url"]
147
- else:
148
- return None
149
-
150
- @retry(
151
- stop=stop_after_attempt(3),
152
- wait=wait_fixed(5) + wait_random(0, 1),
153
- retry=retry_if_exception_type(TransportQueryError)
154
- )
155
- async def do_graphql_query(self, query: gql.client.DocumentNode, **kwargs) -> dict[str, Any]:
156
- async with self._net_op_sem:
157
- gql_transport = CustomHTTPXAsyncTransport(self.client, self.canvas_url.join("api/graphql"),
158
- **self.get_headers())
159
- gql_client = gql.Client(transport=gql_transport, execute_timeout=60)
160
-
161
- return await gql_client.execute_async(query, variable_values=kwargs)
162
-
163
- async def download_file(self, resource: db.Resource, download_destination: BinaryIO) -> AsyncIterator[int]:
164
- yield 0
165
- retries = 0
166
-
167
- while retries < self.max_retries:
168
- async with self.client.stream(method='GET', url=resource.url, cookies=self.client.cookies,
169
- follow_redirects=True) as resp:
170
- if await self.reauthenticate_if_needed(resp):
171
- retries += 1
172
- self._logger.warning("Retrying download of %s", resource.url)
173
- continue
174
-
175
- async for chunk in resp.aiter_bytes():
176
- download_destination.write(chunk)
177
- yield resp.num_bytes_downloaded
178
-
179
- return
180
-
181
- self._logger.warning("Gave up download of %s", resource.url)
182
- raise
183
-
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())
187
-
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
191
-
192
- async def _authenticate(self):
193
- token_response = await self.client.get(self.canvas_url.join("login/session_token"), **self.get_headers())
194
-
195
- if token_response.is_success:
196
- session_url = json.loads(token_response.text)["session_url"]
197
- self._logger.debug("Got token response for authentication")
198
-
199
- if session_url is not None:
200
- # Headers are not needed here
201
- req = await self.client.get(session_url)
202
- if req.status_code != 302:
203
- self._logger.error("Error when activating session from request")
204
- raise AuthenticationException("Authentication was not successful")
205
- else:
206
- raise AuthenticationException("Token response body was malformed")
207
- else:
208
- self._logger.error("Authentication failed, API key may be invalid")
209
- raise AuthenticationException("Authentication failed, check your API key")
@@ -1,124 +0,0 @@
1
- from typing import Any, TypeVar, Type, cast
2
-
3
- T = TypeVar("T")
4
-
5
-
6
- def from_bool(x: Any) -> bool:
7
- assert isinstance(x, bool)
8
- return x
9
-
10
-
11
- def from_str(x: Any) -> str:
12
- assert isinstance(x, str)
13
- return x
14
-
15
-
16
- def from_int(x: Any) -> int:
17
- assert isinstance(x, int) and not isinstance(x, bool)
18
- return x
19
-
20
-
21
- def to_class(c: Type[T], x: Any) -> dict:
22
- assert isinstance(x, c)
23
- return cast(Any, x).to_dict()
24
-
25
-
26
- class LegacyPage:
27
- published: bool
28
- hide_from_students: bool
29
- locked_for_user: bool
30
- body: str | None
31
-
32
- def __init__(self, published: bool, hide_from_students: bool, locked_for_user: bool, body: str | None) -> None:
33
- self.published = published
34
- self.hide_from_students = hide_from_students
35
- self.locked_for_user = locked_for_user
36
- self.body = body
37
-
38
- @staticmethod
39
- def from_dict(obj: Any) -> 'LegacyPage':
40
- assert isinstance(obj, dict)
41
- published = from_bool(obj.get("published"))
42
- hide_from_students = from_bool(obj.get("hide_from_students"))
43
- locked_for_user = from_bool(obj.get("locked_for_user"))
44
- body = None if locked_for_user else from_str(obj.get("body"))
45
- return LegacyPage(published, hide_from_students, locked_for_user, body)
46
-
47
- def to_dict(self) -> dict:
48
- result: dict = {}
49
- result["published"] = from_bool(self.published)
50
- result["hide_from_students"] = from_bool(self.hide_from_students)
51
- result["locked_for_user"] = from_bool(self.locked_for_user)
52
- result["body"] = from_str(self.body)
53
- return result
54
-
55
-
56
- class LegacyFile:
57
- id: int
58
- uuid: str
59
- display_name: str
60
- filename: str
61
- url: str
62
- size: int
63
- locked: bool
64
- hidden: bool
65
- hidden_for_user: bool
66
- locked_for_user: bool
67
-
68
- def __init__(self, id: int, uuid: str, display_name: str, filename: str, url: str, size: int, locked: bool,
69
- hidden: bool, hidden_for_user: bool, locked_for_user: bool) -> None:
70
- self.id = id
71
- self.uuid = uuid
72
- self.display_name = display_name
73
- self.filename = filename
74
- self.url = url
75
- self.size = size
76
- self.locked = locked
77
- self.hidden = hidden
78
- self.hidden_for_user = hidden_for_user
79
- self.locked_for_user = locked_for_user
80
-
81
- @staticmethod
82
- def from_dict(obj: Any) -> 'LegacyFile':
83
- assert isinstance(obj, dict)
84
- id = from_int(obj.get("id"))
85
- uuid = from_str(obj.get("uuid"))
86
- display_name = from_str(obj.get("display_name"))
87
- filename = from_str(obj.get("filename"))
88
- url = from_str(obj.get("url"))
89
- size = from_int(obj.get("size"))
90
- locked = from_bool(obj.get("locked"))
91
- hidden = from_bool(obj.get("hidden"))
92
- hidden_for_user = from_bool(obj.get("hidden_for_user"))
93
- locked_for_user = from_bool(obj.get("locked_for_user"))
94
- return LegacyFile(id, uuid, display_name, filename, url, size, locked, hidden, hidden_for_user, locked_for_user)
95
-
96
- def to_dict(self) -> dict:
97
- result: dict = {}
98
- result["id"] = from_int(self.id)
99
- result["uuid"] = from_str(self.uuid)
100
- result["display_name"] = from_str(self.display_name)
101
- result["filename"] = from_str(self.filename)
102
- result["url"] = from_str(self.url)
103
- result["size"] = from_int(self.size)
104
- result["locked"] = from_bool(self.locked)
105
- result["hidden"] = from_bool(self.hidden)
106
- result["hidden_for_user"] = from_bool(self.hidden_for_user)
107
- result["locked_for_user"] = from_bool(self.locked_for_user)
108
- return result
109
-
110
-
111
- def legacy_page_from_dict(s: Any) -> LegacyPage:
112
- return LegacyPage.from_dict(s)
113
-
114
-
115
- def legacy_page_to_dict(x: LegacyPage) -> Any:
116
- return to_class(LegacyPage, x)
117
-
118
-
119
- def legacy_file_from_dict(s: Any) -> LegacyFile:
120
- return LegacyFile.from_dict(s)
121
-
122
-
123
- def legacy_file_to_dict(x: LegacyFile) -> Any:
124
- return to_class(LegacyFile, x)
@@ -1,34 +0,0 @@
1
- from typing import Union, Optional, Dict, Any
2
-
3
- import httpx
4
- from gql.transport.httpx import HTTPXAsyncTransport
5
- from graphql import DocumentNode
6
-
7
-
8
- class CustomHTTPXAsyncTransport(HTTPXAsyncTransport):
9
- """:ref:`Async Transport <async_transports>` used to execute GraphQL queries
10
- on remote servers.
11
-
12
- The transport uses the httpx library with anyio.
13
- """
14
-
15
- def __init__(self, client: httpx.AsyncClient, url: Union[str, httpx.URL], headers: dict[str, Any] | None = None,
16
- **kwargs):
17
- super().__init__(url=url, **kwargs)
18
- self.client = client
19
- self.headers = headers or {}
20
-
21
- async def connect(self):
22
- pass
23
-
24
- async def close(self):
25
- """Do not close the client. We want to keep it."""
26
- pass
27
-
28
- def _prepare_request(self, document: DocumentNode, variable_values: Optional[Dict[str, Any]] = None,
29
- operation_name: Optional[str] = None, extra_args: Optional[Dict[str, Any]] = None,
30
- upload_files: bool = False) -> Dict[str, Any]:
31
- result = super()._prepare_request(document, variable_values, operation_name, extra_args, upload_files)
32
- result["headers"] = self.headers
33
-
34
- return result
@@ -1,108 +0,0 @@
1
- import logging
2
- from abc import ABC, abstractmethod
3
- from asyncio import Lock, Event
4
-
5
- import httpx
6
- from httpx import URL
7
-
8
-
9
- class AuthenticationException(Exception):
10
- pass
11
-
12
-
13
- # httpx does have an authentication flow mechanism that allows you to also make other requests but I don't know if it
14
- # will behave the same way as this does. I also finished this before I found out that existed.
15
-
16
- class SelfAuthenticatingWithHttpClient(ABC):
17
- _logger = logging.getLogger(__name__)
18
-
19
- def __init__(self, max_retires: int = 3, client: httpx.AsyncClient = httpx.AsyncClient()):
20
- self.authentication_lock = Lock()
21
- self.client = client
22
- self.authentication_in_progress: Event | None = None
23
- self.max_retries = max_retires
24
-
25
- async def reauthenticate(self) -> None:
26
- """
27
- Re-authenticates the client
28
- """
29
-
30
- # Acquire the lock as to prevent multiple threads from modifying self.authentication_in_progress.
31
- # Not that will ever happen using asyncio alone because python async is not actually multi-processed
32
- await self.authentication_lock.acquire()
33
-
34
- # If there's no authentication already in progress
35
- if self.authentication_in_progress is None:
36
- # Update authentication_in_progress and release the lock
37
- self.authentication_in_progress = Event()
38
- self.authentication_lock.release()
39
-
40
- # Start the authentication
41
- self._logger.info(f"Authenticating {self.__class__.__name__}")
42
- await self._authenticate()
43
- self._logger.info(f"Finished authenticating {self.__class__.__name__}")
44
- # Update the event to unblock other coroutines
45
- self.authentication_in_progress.set()
46
-
47
- # Delete authentication_in_progress to indicate that authentication is no longer in progress
48
- # This lock is probably not needed (due to not actually being multi-processed) but shouldn't hurt
49
- await self.authentication_lock.acquire()
50
- self.authentication_in_progress = None
51
- self.authentication_lock.release()
52
- # Authentication is already in progress
53
- else:
54
- # Get a reference to the event
55
- event = self.authentication_in_progress
56
- # Release the lock as we are not going to access it anymore
57
- self.authentication_lock.release()
58
-
59
- # Wait for the reauthentication to finish
60
- self._logger.info(f"Waiting for {self.__class__.__name__}")
61
- await event.wait()
62
- self._logger.info(f"Finished waiting for {self.__class__.__name__}")
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
104
- async def _authenticate(self) -> None:
105
- """
106
- Authenticates the client
107
- """
108
- ...
@@ -1,4 +0,0 @@
1
- from .all_courses import AllCoursesQueryData
2
- from .canvas_course_data import Term, Assignment, AssignmentConnection, ModuleItemInterface, File, Page, \
3
- ModuleItem, \
4
- Module, ModuleConnection, CanvasCourseData as Course
@@ -1,7 +0,0 @@
1
- # qenerate: plugin=pydantic_v1
2
-
3
- query AllCourses($detailed: Boolean!) {
4
- allCourses {
5
- ...CanvasCourseData
6
- }
7
- }
@@ -1,108 +0,0 @@
1
- """
2
- Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
- """
4
- from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
- from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
- from enum import Enum # noqa: F401 # pylint: disable=W0611
7
- from typing import ( # noqa: F401 # pylint: disable=W0611
8
- Any,
9
- Optional,
10
- Union,
11
- )
12
-
13
- from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
- BaseModel,
15
- Extra,
16
- Field,
17
- Json,
18
- )
19
-
20
- from qcanvas.queries.canvas_course_data import CanvasCourseData
21
-
22
- DEFINITION = """
23
- fragment CanvasCourseData on Course {
24
- _id
25
- # id
26
- name
27
- courseNickname
28
- term {
29
- endAt
30
- startAt
31
- name
32
- id
33
- }
34
- assignmentsConnection @include(if: $detailed) {
35
- nodes {
36
- description
37
- courseId
38
- dueAt
39
- createdAt
40
- id
41
- name
42
- position
43
- updatedAt
44
- }
45
- }
46
- modulesConnection @include(if: $detailed) {
47
- nodes {
48
- name
49
- id
50
- moduleItems {
51
- content {
52
- ... on File {
53
- _id
54
- displayName
55
- createdAt
56
- updatedAt
57
- url
58
- size
59
- mimeClass
60
- contentType
61
- }
62
- ... on Page {
63
- _id
64
- title
65
- updatedAt
66
- createdAt
67
- }
68
- }
69
- }
70
- }
71
- }
72
- }
73
-
74
- query AllCourses($detailed: Boolean!) {
75
- allCourses {
76
- ...CanvasCourseData
77
- }
78
- }
79
- """
80
-
81
-
82
- class ConfiguredBaseModel(BaseModel):
83
- class Config:
84
- smart_union = True
85
- extra = Extra.forbid
86
-
87
-
88
- class AllCoursesQueryData(ConfiguredBaseModel):
89
- all_courses: Optional[list[CanvasCourseData]] = Field(..., alias="allCourses")
90
-
91
-
92
- def query(query_func: Callable, **kwargs: Any) -> AllCoursesQueryData:
93
- """
94
- This is a convenience function which queries and parses the data into
95
- concrete types. It should be compatible with most GQL clients.
96
- You do not have to use it to consume the generated data classes.
97
- Alternatively, you can also mime and alternate the behavior
98
- of this function in the caller.
99
-
100
- Parameters:
101
- query_func (Callable): Function which queries your GQL Server
102
- kwargs: optional arguments that will be passed to the query function
103
-
104
- Returns:
105
- AllCoursesQueryData: queried data parsed into generated classes
106
- """
107
- raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
108
- return AllCoursesQueryData(**raw_data)
@@ -1,51 +0,0 @@
1
- # qenerate: plugin=pydantic_v1
2
-
3
- fragment CanvasCourseData on Course {
4
- _id
5
- name
6
- courseNickname
7
- term {
8
- endAt
9
- startAt
10
- name
11
- id
12
- }
13
- assignmentsConnection @include(if: $detailed) {
14
- nodes {
15
- description
16
- courseId
17
- dueAt
18
- createdAt
19
- id
20
- name
21
- position
22
- updatedAt
23
- }
24
- }
25
- modulesConnection @include(if: $detailed) {
26
- nodes {
27
- name
28
- id
29
- moduleItems {
30
- content {
31
- ... on File {
32
- _id
33
- displayName
34
- createdAt
35
- updatedAt
36
- url
37
- size
38
- mimeClass
39
- contentType
40
- }
41
- ... on Page {
42
- _id
43
- title
44
- updatedAt
45
- createdAt
46
- }
47
- }
48
- }
49
- }
50
- }
51
- }