qcanvas 0.0.5.7a0__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.
- 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.post0.dist-info/METADATA +61 -0
- qcanvas-1.0.3.post0.dist-info/RECORD +64 -0
- {qcanvas-0.0.5.7a0.dist-info → qcanvas-1.0.3.post0.dist-info}/WHEEL +1 -1
- qcanvas-1.0.3.post0.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
|
@@ -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
|
-
...
|
qcanvas/queries/__init__.py
DELETED
qcanvas/queries/all_courses.gql
DELETED
qcanvas/queries/all_courses.py
DELETED
|
@@ -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
|
-
}
|