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,47 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
I dedicate this file to removing the random crap canvas puts in its data.
|
|
3
|
-
Like random NBSPs. Gotta love those fuckers.
|
|
4
|
-
Thanks instructure btw for you state of the art WYSIWYG dogshit editor.
|
|
5
|
-
"""
|
|
6
|
-
from bs4 import BeautifulSoup
|
|
7
|
-
|
|
8
|
-
NBSP = " "
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def remove_garbage_from_title(smelly_canvas_title: str) -> str:
|
|
12
|
-
"""
|
|
13
|
-
Removes trailing tabs, spaces and NBSPs from smelly canvas titles.
|
|
14
|
-
Parameters
|
|
15
|
-
----------
|
|
16
|
-
smelly_canvas_title
|
|
17
|
-
|
|
18
|
-
Returns
|
|
19
|
-
-------
|
|
20
|
-
str
|
|
21
|
-
Clean title that is not smelly and has no NBSPs.
|
|
22
|
-
"""
|
|
23
|
-
return (smelly_canvas_title
|
|
24
|
-
.strip(f"\t {NBSP}") # remove trailing garbage
|
|
25
|
-
.replace(NBSP, " ") # remove any other NBSPs
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def remove_stylesheets_from_html(smelly_html: str) -> str:
|
|
30
|
-
"""
|
|
31
|
-
Removes all stylesheet links from `smelly_html`.
|
|
32
|
-
|
|
33
|
-
Parameters
|
|
34
|
-
----------
|
|
35
|
-
smelly_html
|
|
36
|
-
The html to remove style sheets from
|
|
37
|
-
Returns
|
|
38
|
-
-------
|
|
39
|
-
The non-smelly html with all stylesheet links removed
|
|
40
|
-
"""
|
|
41
|
-
bs = BeautifulSoup(smelly_html, "html.parser")
|
|
42
|
-
|
|
43
|
-
# remove links
|
|
44
|
-
for ele in bs.find_all("link", {"rel": "stylesheet"}):
|
|
45
|
-
ele.decompose()
|
|
46
|
-
|
|
47
|
-
return str(bs)
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
from PySide6.QtCore import QMimeDatabase
|
|
2
|
-
from PySide6.QtGui import QIcon
|
|
3
|
-
from PySide6.QtWidgets import QApplication, QStyle
|
|
4
|
-
|
|
5
|
-
_mime_database = QMimeDatabase()
|
|
6
|
-
_default_icon = None
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def icon_for_filename(file_name: str) -> QIcon:
|
|
10
|
-
"""
|
|
11
|
-
Gets the icon for a filename, based on its extension
|
|
12
|
-
Parameters
|
|
13
|
-
----------
|
|
14
|
-
file_name
|
|
15
|
-
The name of the file
|
|
16
|
-
Returns
|
|
17
|
-
-------
|
|
18
|
-
QIcon
|
|
19
|
-
The icon for the file
|
|
20
|
-
"""
|
|
21
|
-
global _default_icon
|
|
22
|
-
|
|
23
|
-
for mime_type in _mime_database.mimeTypesForFileName(file_name):
|
|
24
|
-
icon = QIcon.fromTheme(mime_type.iconName())
|
|
25
|
-
|
|
26
|
-
# Return the appropriate icon if it's found
|
|
27
|
-
if not icon.isNull():
|
|
28
|
-
return icon
|
|
29
|
-
|
|
30
|
-
# Cache the default icon, used when the icon for a file is unknown/not found
|
|
31
|
-
if _default_icon is None:
|
|
32
|
-
_default_icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
|
|
33
|
-
|
|
34
|
-
return _default_icon
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
from PySide6.QtGui import QAction, QKeySequence
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def create_qaction(name: str, shortcut: QKeySequence | None = None, parent: Any = None, triggered: Any = None,
|
|
7
|
-
checkable: bool | None = None, checked: bool | None = None) -> QAction:
|
|
8
|
-
action = QAction(name)
|
|
9
|
-
|
|
10
|
-
if shortcut is not None:
|
|
11
|
-
action.setShortcut(shortcut)
|
|
12
|
-
|
|
13
|
-
if parent is not None:
|
|
14
|
-
action.setParent(parent)
|
|
15
|
-
|
|
16
|
-
if triggered is not None:
|
|
17
|
-
action.triggered.connect(triggered)
|
|
18
|
-
|
|
19
|
-
if checkable is not None:
|
|
20
|
-
action.setCheckable(checkable)
|
|
21
|
-
|
|
22
|
-
if checked is not None:
|
|
23
|
-
action.setChecked(checked)
|
|
24
|
-
|
|
25
|
-
return action
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
|
|
3
|
-
import qdarktheme
|
|
4
|
-
|
|
5
|
-
from qcanvas.util.app_settings import settings
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def apply_selected_theme() -> None:
|
|
9
|
-
"""
|
|
10
|
-
Applies the selected theme from the app's settings
|
|
11
|
-
"""
|
|
12
|
-
if settings.theme != "native":
|
|
13
|
-
qdarktheme.setup_theme(
|
|
14
|
-
settings.theme,
|
|
15
|
-
custom_colors=_get_colours()
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
red_theme = {
|
|
20
|
-
"primary": "e21d31",
|
|
21
|
-
"[light]": {"foreground": "480910", "background": "fcf8f8"},
|
|
22
|
-
"[dark]": {"foreground": "fbdfe2", "background": "231f1f"}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _get_colours() -> dict:
|
|
27
|
-
now = datetime.now()
|
|
28
|
-
|
|
29
|
-
if now.year >= 2025:
|
|
30
|
-
print("I certainly hope not")
|
|
31
|
-
|
|
32
|
-
if now.month == 3 and now.day == 17:
|
|
33
|
-
# And this is on the weekend...
|
|
34
|
-
return {
|
|
35
|
-
"[dark]": {"primary": "08ff00"},
|
|
36
|
-
"[light]": {"primary": "06c200"}
|
|
37
|
-
}
|
|
38
|
-
elif now.month == 2 and now.day == 14:
|
|
39
|
-
print("Why are you looking at canvas? Don't you have something better to do?")
|
|
40
|
-
|
|
41
|
-
# Nobody will ever see this because uni starts around the 20th
|
|
42
|
-
# Too bad, I kinda liked the theme
|
|
43
|
-
return red_theme
|
|
44
|
-
elif now.month == 8 and now.day == 20:
|
|
45
|
-
# Some random day... I just wanted to see the red theme
|
|
46
|
-
return red_theme
|
|
47
|
-
else:
|
|
48
|
-
return {"primary": "FF804F"}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from bs4 import Tag
|
|
2
|
-
from httpx import URL
|
|
3
|
-
|
|
4
|
-
from qcanvas import db as db
|
|
5
|
-
from qcanvas.net.canvas import CanvasClient
|
|
6
|
-
from qcanvas.util.link_scanner.resource_scanner import ResourceScanner
|
|
7
|
-
|
|
8
|
-
canvas_resource_id_prefix = "canvas_file"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CanvasFileScanner(ResourceScanner):
|
|
12
|
-
_canvas_client: CanvasClient
|
|
13
|
-
|
|
14
|
-
def __init__(self, canvas_client: CanvasClient):
|
|
15
|
-
self._canvas_client = canvas_client
|
|
16
|
-
|
|
17
|
-
def accepts_link(self, link: Tag) -> bool:
|
|
18
|
-
if link.name not in ["a", "img"]:
|
|
19
|
-
return False
|
|
20
|
-
|
|
21
|
-
return "data-api-returntype" in link.attrs.keys() and link["data-api-returntype"] == "File"
|
|
22
|
-
|
|
23
|
-
async def extract_resource(self, link: Tag, file_id: str) -> db.Resource:
|
|
24
|
-
return db.convert_legacy_file(await self._canvas_client.get_file_from_endpoint(link["data-api-endpoint"]))
|
|
25
|
-
|
|
26
|
-
def extract_id(self, link: Tag) -> str:
|
|
27
|
-
# https://canvas.newcastle.edu.au/courses/27716/files/5975585/...
|
|
28
|
-
# --------------------------------- Extract this part ^^^^^^^
|
|
29
|
-
return URL(link["data-api-endpoint"]).path.rsplit('/', 2)[-1]
|
|
30
|
-
|
|
31
|
-
async def download(self, resource):
|
|
32
|
-
path = resource.download_location
|
|
33
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
-
|
|
35
|
-
with open(path, "wb") as file:
|
|
36
|
-
async for progress in self._canvas_client.download_file(resource, file):
|
|
37
|
-
yield progress
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def name(self) -> str:
|
|
41
|
-
return canvas_resource_id_prefix
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from bs4 import Tag, BeautifulSoup
|
|
5
|
-
from httpx import AsyncClient
|
|
6
|
-
|
|
7
|
-
from qcanvas import db as db
|
|
8
|
-
from qcanvas.util.link_scanner import ResourceScanner
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CanvasMediaObjectScanner(ResourceScanner):
|
|
12
|
-
|
|
13
|
-
def __init__(self, client: AsyncClient):
|
|
14
|
-
self.client = client
|
|
15
|
-
|
|
16
|
-
@property
|
|
17
|
-
def name(self) -> str:
|
|
18
|
-
return "canvas_media_object"
|
|
19
|
-
|
|
20
|
-
def accepts_link(self, link: Tag) -> bool:
|
|
21
|
-
return (
|
|
22
|
-
link.name == "iframe"
|
|
23
|
-
and "data-media-type" in link.attrs.keys()
|
|
24
|
-
and link.attrs["data-media-type"] == "video"
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
async def extract_resource(self, link: Tag, file_id: str) -> db.Resource:
|
|
28
|
-
# Get the page for the embedded player (I could not find another way to get the needed data from canvas)
|
|
29
|
-
response = (await self.client.get(link.attrs["src"])).text
|
|
30
|
-
# Parse the HTML response
|
|
31
|
-
doc = BeautifulSoup(response, "html.parser")
|
|
32
|
-
media_data: None | dict[str, Any] = None
|
|
33
|
-
|
|
34
|
-
# Find all script tags (one of them has the data we are interested in)
|
|
35
|
-
for script_tag in doc.find_all("script", {}):
|
|
36
|
-
body = script_tag.text.strip()
|
|
37
|
-
|
|
38
|
-
# If the tag content starts with this then it has the data we want
|
|
39
|
-
if "INST = {" in body:
|
|
40
|
-
# Find the data that we are interested in (is on a line that starts with "ENV = ")
|
|
41
|
-
line: str = next(filter(lambda x: x.strip().startswith("ENV ="), script_tag.text.splitlines()))
|
|
42
|
-
# Parse the json embedded in the script tag
|
|
43
|
-
media_data = json.loads(line.lstrip("ENV = ").rstrip(";"))["media_object"]
|
|
44
|
-
break
|
|
45
|
-
|
|
46
|
-
if media_data is None:
|
|
47
|
-
raise Exception("Could not extract media info")
|
|
48
|
-
|
|
49
|
-
# The highest quality stream is the first
|
|
50
|
-
media_source = media_data["media_sources"][0]
|
|
51
|
-
|
|
52
|
-
return db.Resource(
|
|
53
|
-
id=file_id,
|
|
54
|
-
url=media_source["src"],
|
|
55
|
-
file_name=media_data["title"],
|
|
56
|
-
file_size=int(media_source["size"]) * 1024 # Seems to be recorded in KiB, not bytes
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
def extract_id(self, link: Tag) -> str:
|
|
60
|
-
return link.attrs["data-media-id"]
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import httpx
|
|
2
|
-
from bs4 import Tag
|
|
3
|
-
from httpx import URL
|
|
4
|
-
|
|
5
|
-
from qcanvas import db as db
|
|
6
|
-
from qcanvas.util.link_scanner import ResourceScanner
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# from httpx import URL
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def parse_content_disposition(header: str) -> dict[str, str | None]:
|
|
13
|
-
bad_chars = "\" \t"
|
|
14
|
-
result = {}
|
|
15
|
-
|
|
16
|
-
for statement in header.split(";"):
|
|
17
|
-
split = statement.split("=", 2)
|
|
18
|
-
|
|
19
|
-
result[split[0].strip(bad_chars)] = None if len(split) == 1 else split[1].strip(bad_chars)
|
|
20
|
-
|
|
21
|
-
return result
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class DropboxScanner(ResourceScanner):
|
|
25
|
-
def __init__(self, client: httpx.AsyncClient):
|
|
26
|
-
self.client = client
|
|
27
|
-
|
|
28
|
-
def accepts_link(self, link: Tag) -> bool:
|
|
29
|
-
if link.name != "a":
|
|
30
|
-
return False
|
|
31
|
-
|
|
32
|
-
if "href" in link.attrs:
|
|
33
|
-
url = URL(link.attrs["href"])
|
|
34
|
-
|
|
35
|
-
return url.host == "www.dropbox.com" and url.path.split("/", 2)[1] == "s"
|
|
36
|
-
else:
|
|
37
|
-
return False
|
|
38
|
-
|
|
39
|
-
async def extract_resource(self, link: Tag, file_id: str) -> db.Resource:
|
|
40
|
-
url = URL(link.attrs["href"]).copy_set_param("dl", 1)
|
|
41
|
-
|
|
42
|
-
req = self.client.build_request(
|
|
43
|
-
method="GET",
|
|
44
|
-
url=url
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
# The following code essentially starts downloading the file, reads the headers and then stops downloading it,
|
|
48
|
-
# just to ge the size of the file
|
|
49
|
-
resp = await self.client.send(req, follow_redirects=True, stream=True)
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
resp.raise_for_status()
|
|
53
|
-
|
|
54
|
-
filename = parse_content_disposition(resp.headers["content-disposition"])["filename"]
|
|
55
|
-
size = int(resp.headers["content-length"])
|
|
56
|
-
|
|
57
|
-
return db.Resource(id=file_id, url=str(url), file_name=filename, file_size=size)
|
|
58
|
-
finally:
|
|
59
|
-
await resp.aclose()
|
|
60
|
-
|
|
61
|
-
def extract_id(self, link: Tag) -> str:
|
|
62
|
-
# https://www.dropbox.com/s/vwk48ajl9nw6pqh/Lab1_ENGG1500_robot.pdf?dl=0
|
|
63
|
-
# ------- Extract this part ^^^^^^^^^^^^^^^
|
|
64
|
-
return URL(link.attrs["href"]).path.split("/", 3)[2]
|
|
65
|
-
|
|
66
|
-
@property
|
|
67
|
-
def name(self) -> str:
|
|
68
|
-
return "dropbox"
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import AsyncIterator
|
|
3
|
-
|
|
4
|
-
import httpx
|
|
5
|
-
from bs4 import Tag
|
|
6
|
-
|
|
7
|
-
import qcanvas.db as db
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ResourceScanner(ABC):
|
|
11
|
-
"""
|
|
12
|
-
A resource scanner extracts resources from canvas pages.
|
|
13
|
-
The resource may be an embedded video, a file or anything that will be of use to the user.
|
|
14
|
-
Each scanner should be responsible for only 1 type of resource.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
@abstractmethod
|
|
19
|
-
def name(self) -> str:
|
|
20
|
-
"""
|
|
21
|
-
The name of the resource scanner.
|
|
22
|
-
Will be attached to the resource id externally.
|
|
23
|
-
"""
|
|
24
|
-
...
|
|
25
|
-
|
|
26
|
-
@abstractmethod
|
|
27
|
-
def accepts_link(self, link: Tag) -> bool:
|
|
28
|
-
"""
|
|
29
|
-
Whether this resource scanner accepts the specified link
|
|
30
|
-
"""
|
|
31
|
-
...
|
|
32
|
-
|
|
33
|
-
@abstractmethod
|
|
34
|
-
async def extract_resource(self, link: Tag, file_id: str) -> db.Resource:
|
|
35
|
-
"""
|
|
36
|
-
Extract information about the resource in the specified tag
|
|
37
|
-
Parameters
|
|
38
|
-
----------
|
|
39
|
-
link
|
|
40
|
-
The element that links to the resource
|
|
41
|
-
file_id
|
|
42
|
-
The id of the file (as produced from `extract_id`)
|
|
43
|
-
Returns
|
|
44
|
-
-------
|
|
45
|
-
The resource
|
|
46
|
-
"""
|
|
47
|
-
...
|
|
48
|
-
|
|
49
|
-
@abstractmethod
|
|
50
|
-
def extract_id(self, link: Tag) -> str:
|
|
51
|
-
"""
|
|
52
|
-
Extracts a unique id from a file link
|
|
53
|
-
"""
|
|
54
|
-
...
|
|
55
|
-
|
|
56
|
-
async def download(self, resource: db.Resource) -> AsyncIterator[int]:
|
|
57
|
-
yield 0
|
|
58
|
-
|
|
59
|
-
download_destination = resource.download_location
|
|
60
|
-
download_destination.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
-
|
|
62
|
-
with open(download_destination, "wb") as file:
|
|
63
|
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
64
|
-
async with client.stream(method='get', url=resource.url) as resp:
|
|
65
|
-
resp.raise_for_status()
|
|
66
|
-
|
|
67
|
-
async for chunk in resp.aiter_bytes():
|
|
68
|
-
file.write(chunk)
|
|
69
|
-
yield resp.num_bytes_downloaded
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import itertools
|
|
2
|
-
from abc import ABC, abstractmethod
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ProgressSection:
|
|
7
|
-
"""
|
|
8
|
-
Convenience class for reporting on a section of a larger task
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
def __init__(self, section_name: str, total_progress: int, reporter: "ProgressReporter"):
|
|
12
|
-
self.total_progress = total_progress
|
|
13
|
-
self.reporter = reporter
|
|
14
|
-
self._counter = itertools.count(1) # Start at 1 instead of 0
|
|
15
|
-
self.reporter.section_started(section_name, total_progress)
|
|
16
|
-
|
|
17
|
-
def increment_progress(self, *args):
|
|
18
|
-
self.reporter.progress(next(self._counter), self.total_progress)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ProgressReporter(ABC):
|
|
22
|
-
"""
|
|
23
|
-
A progress reporter is passed to a function which then uses it to report the progress for whatever task it performs to another place.
|
|
24
|
-
Only 1 section should be active at a time.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
@abstractmethod
|
|
28
|
-
def section_started(self, section_name: str, total_progress: int) -> None:
|
|
29
|
-
"""
|
|
30
|
-
Signals that a new section of a task has started
|
|
31
|
-
Parameters
|
|
32
|
-
----------
|
|
33
|
-
section_name
|
|
34
|
-
The name of the new task section
|
|
35
|
-
"""
|
|
36
|
-
...
|
|
37
|
-
|
|
38
|
-
@abstractmethod
|
|
39
|
-
def progress(self, current_progress: int, total_progress: int) -> None:
|
|
40
|
-
"""
|
|
41
|
-
Updates the current progress of the current task
|
|
42
|
-
Parameters
|
|
43
|
-
----------
|
|
44
|
-
current_progress
|
|
45
|
-
The current amount of progress, e.g. the number of bytes of a file that have been downloaded
|
|
46
|
-
total_progress
|
|
47
|
-
The total or final amount of progress, e.g. the size of a download
|
|
48
|
-
"""
|
|
49
|
-
...
|
|
50
|
-
|
|
51
|
-
@abstractmethod
|
|
52
|
-
def finished(self) -> None:
|
|
53
|
-
"""
|
|
54
|
-
Signals that the task is finished and there is nothing left to do
|
|
55
|
-
"""
|
|
56
|
-
...
|
|
57
|
-
|
|
58
|
-
@abstractmethod
|
|
59
|
-
def errored(self, context: Any) -> None:
|
|
60
|
-
"""
|
|
61
|
-
Signals that the task could not be completed
|
|
62
|
-
Parameters
|
|
63
|
-
----------
|
|
64
|
-
context
|
|
65
|
-
Any information about why the task failed
|
|
66
|
-
"""
|
|
67
|
-
...
|
|
68
|
-
|
|
69
|
-
def section(self, section_name: str, total_progress: int) -> ProgressSection:
|
|
70
|
-
"""
|
|
71
|
-
Creates a ProgressSection for this reporter, for reporting on a section of a larger task
|
|
72
|
-
Parameters
|
|
73
|
-
----------
|
|
74
|
-
section_name
|
|
75
|
-
The name of the section
|
|
76
|
-
total_progress
|
|
77
|
-
The total or final amount of progress, e.g. the size of a download
|
|
78
|
-
Returns
|
|
79
|
-
-------
|
|
80
|
-
ProgressSection
|
|
81
|
-
The created section
|
|
82
|
-
"""
|
|
83
|
-
return ProgressSection(section_name, total_progress, self)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class _NoopReporter(ProgressReporter):
|
|
87
|
-
|
|
88
|
-
def section_started(self, section_name: str, total_progress: int) -> None:
|
|
89
|
-
pass
|
|
90
|
-
|
|
91
|
-
def progress(self, current_progress: int, total: int) -> None:
|
|
92
|
-
pass
|
|
93
|
-
|
|
94
|
-
def finished(self) -> None:
|
|
95
|
-
pass
|
|
96
|
-
|
|
97
|
-
def errored(self, context: Any) -> None:
|
|
98
|
-
pass
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
noop_reporter = _NoopReporter()
|
qcanvas/util/self_updater.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
from importlib.metadata import version
|
|
5
|
-
|
|
6
|
-
import httpx
|
|
7
|
-
from packaging.version import Version
|
|
8
|
-
|
|
9
|
-
from qcanvas.util.constants import package_name
|
|
10
|
-
|
|
11
|
-
# When true, signals that the program should be restarted when it closes next
|
|
12
|
-
restart_flag = False
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
async def do_update() -> None:
|
|
16
|
-
"""
|
|
17
|
-
Updates the qcanvas package and sets the restart flag.
|
|
18
|
-
The restart flag is passed back to the launcher script as a return code.
|
|
19
|
-
"""
|
|
20
|
-
global restart_flag
|
|
21
|
-
await asyncio.to_thread(os.system, f"pip install --upgrade {package_name}")
|
|
22
|
-
restart_flag = True
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async def get_newer_version() -> tuple[Version | None, Version | None] | None:
|
|
26
|
-
"""
|
|
27
|
-
Check for a newer version of qcanvas
|
|
28
|
-
Returns
|
|
29
|
-
-------
|
|
30
|
-
tuple
|
|
31
|
-
A tuple, where the first item is the latest version and the second item is the installed version. If the installed
|
|
32
|
-
version is up-to-date, then the first item is None
|
|
33
|
-
"""
|
|
34
|
-
latest_version = await get_latest_version()
|
|
35
|
-
installed_version = Version(version(package_name))
|
|
36
|
-
|
|
37
|
-
print(f"latest = {latest_version}, installed = {installed_version}")
|
|
38
|
-
|
|
39
|
-
if installed_version < latest_version:
|
|
40
|
-
return latest_version, installed_version
|
|
41
|
-
else:
|
|
42
|
-
return None, installed_version
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
async def get_latest_version() -> Version:
|
|
46
|
-
"""
|
|
47
|
-
Retrieves the latest version of the package from pypi
|
|
48
|
-
Returns
|
|
49
|
-
-------
|
|
50
|
-
Version
|
|
51
|
-
The latest version of qcanvas on pypi
|
|
52
|
-
"""
|
|
53
|
-
async with httpx.AsyncClient() as client:
|
|
54
|
-
data = json.loads((await client.get(f"https://pypi.org/pypi/{package_name}/json")).text)
|
|
55
|
-
return Version(data["info"]["version"])
|