blackboardsync
Advanced tools
| from textwrap import dedent | ||
| body_template = dedent(""" | ||
| <html> | ||
| <head> | ||
| <meta name="viewport" | ||
| content="width=device-width, initial-scale=1" /> | ||
| <script> | ||
| const openShare = () => {{ | ||
| if(navigator.share) {{ | ||
| navigator.share({{ | ||
| title: '{title}', | ||
| text: '{body_text}' | ||
| }}).catch(console.error); | ||
| }} | ||
| }} | ||
| document.addEventListener('DOMContentLoaded', function() {{ | ||
| const shareButton = document.getElementById('share-button'); | ||
| shareButton.style.display = (navigator.share) ? 'block' : 'none'; | ||
| }}); | ||
| </script> | ||
| <style> | ||
| :root {{ color-scheme: light dark; }} | ||
| html {{ height: 100%; background-color: #212121; }} | ||
| body {{ | ||
| height: calc(100% - 8rem); | ||
| display: flex; | ||
| flex-flow: column nowrap; | ||
| align-items: flex-start; | ||
| justify-content: stretch; | ||
| margin: 2rem; | ||
| padding: 2rem; | ||
| border-radius: 1.5rem; | ||
| box-shadow: 0px 2px 10px 2px black; | ||
| background-color: light-dark(white, #212121); | ||
| font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | ||
| line-height: 1.5; | ||
| }} | ||
| header {{ | ||
| display: flex; | ||
| flex-flow: row nowrap; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| width: 100%; | ||
| }} | ||
| header > h2 {{ flex: 1; }} | ||
| main {{ flex: 1; }} | ||
| footer {{ | ||
| display: flex; | ||
| flex-flow: column nowrap; | ||
| align-self: center; | ||
| align-items: center; | ||
| font-size: 0.75rem; | ||
| color: grey; | ||
| }} | ||
| footer > p {{ margin: 0.25rem; }} | ||
| footer > a {{ | ||
| text-decoration: none; | ||
| color: #a02c2c; | ||
| }} | ||
| ul#sharing {{ | ||
| display: flex; | ||
| flex-flow: row nowrap; | ||
| justify-content: center; | ||
| margin: 0; | ||
| padding: 0; | ||
| grid-gap: 2rem; | ||
| }} | ||
| ul#sharing > li {{ list-style-type: none; }} | ||
| ul#sharing svg {{ | ||
| fill: light-dark(black, white); | ||
| cursor: pointer; | ||
| }} | ||
| #share-button {{ display: none; }} | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <header> | ||
| <h2>{title}</h2> | ||
| <ul id="sharing"> | ||
| <li> | ||
| <a href="mailto:?subject={title}&body={body_text}"> | ||
| <svg xmlns="http://www.w3.org/2000/svg" | ||
| viewBox="0 0 16 16" width="16" height="16"> | ||
| <path d="M1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 | ||
| 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 | ||
| 12.25v-8.5C0 2.784.784 2 1.75 2ZM1.5 12.251c0 | ||
| .138.112.25.25.25h12.5a.25.25 0 0 0 | ||
| .25-.25V5.809L8.38 9.397a.75.75 0 0 1-.76 0L1.5 | ||
| 5.809v6.442Zm13-8.181v-.32a.25.25 0 0 | ||
| 0-.25-.25H1.75a.25.25 0 0 | ||
| 0-.25.25v.32L8 7.88Z"></path></svg> | ||
| </a> | ||
| </li> | ||
| <li id="share-button" onClick="openShare()"> | ||
| <svg xmlns="http://www.w3.org/2000/svg" | ||
| viewBox="0 0 16 16" width="16" height="16"> | ||
| <path d="M15 3a3 3 0 0 1-5.175 2.066l-3.92 2.179a2.994 2.994 | ||
| 0 0 1 0 1.51l3.92 2.179a3 3 0 1 1-.73 | ||
| 1.31l-3.92-2.178a3 3 0 1 1 0-4.133l3.92-2.178A3 | ||
| 3 0 1 1 15 3Zm-1.5 10a1.5 1.5 0 1 0-3.001.001A1.5 | ||
| 1.5 0 0 0 13.5 13Zm-9-5a1.5 1.5 0 1 0-3.001.001A1.5 | ||
| 1.5 0 0 0 4.5 8Zm9-5a1.5 1.5 0 1 0-3.001.001A1.5 | ||
| 1.5 0 0 0 13.5 3Z"></path></svg> | ||
| </li> | ||
| </ul> | ||
| </header> | ||
| <main>{body_html}</main> | ||
| <footer> | ||
| <p>You may have to configure your browser to open | ||
| attachments with the right application instead of downloading | ||
| them again.</p> | ||
| <p>Content downloaded by | ||
| <a href="https://bbsync.app">Blackboard Sync</a></p> | ||
| </footer> | ||
| </body> | ||
| </html>""").lstrip() | ||
| def create_body(title: str, body_html: str, body_text: str) -> str: | ||
| return body_template.format(title=title, | ||
| body_html=body_html, | ||
| body_text=body_text) |
@@ -7,7 +7,7 @@ from pathlib import Path | ||
| from .base import FStream | ||
| from .job import DownloadJob | ||
| from .templates import create_body | ||
| from .webdav import WebDavFile, ContentParser | ||
| from .job import DownloadJob | ||
| class ContentBody(FStream): | ||
@@ -18,10 +18,21 @@ """Process the content body to find WebDav files.""" | ||
| job: DownloadJob) -> None: | ||
| parser = ContentParser(content.body or "", job.session.instance_url) | ||
| self.body = parser.body | ||
| self.ignore = False | ||
| if not content.body: | ||
| self.ignore = True | ||
| return | ||
| title = content.title or "Untitled" | ||
| parser = ContentParser(content.body, job.session.instance_url) | ||
| self.body = create_body(title, parser.body, parser.text) | ||
| self.children = [WebDavFile(ln, job) for ln in parser.links] | ||
| def write(self, path: Path, executor: ThreadPoolExecutor) -> None: | ||
| super().write_base(path / f"{path.stem}.html", executor, self.body) | ||
| if self.ignore: | ||
| return | ||
| self.write_base(path / f"{path.stem}.html", executor, self.body) | ||
| for child in self.children: | ||
| child.write(path, executor) |
@@ -40,3 +40,2 @@ import logging | ||
| Handler = Content.get_handler(content.contentHandler) | ||
| self.title = content.title_path_safe.replace('.', '_') | ||
@@ -43,0 +42,0 @@ |
@@ -5,8 +5,5 @@ from pathlib import Path | ||
| from blackboard.blackboard import BBCourseContent | ||
| from blackboard.filters import BBAttachmentFilter | ||
| from bwfilters import BWFilter | ||
| from blackboard.filters import ( | ||
| BBAttachmentFilter, | ||
| BWFilter | ||
| ) | ||
| from .attachment import Attachment | ||
@@ -13,0 +10,0 @@ from .api_path import BBContentPath |
@@ -27,2 +27,3 @@ """ | ||
| from bs4 import BeautifulSoup | ||
| from urllib.parse import unquote | ||
| from typing import List, NamedTuple | ||
@@ -42,18 +43,33 @@ from pathvalidate import sanitize_filename | ||
| class ContentParser: | ||
| def __init__(self, body: str, base_url: str) -> None: | ||
| links = [] | ||
| def __init__(self, body: str, base_url: str, | ||
| *, find_links: bool = True) -> None: | ||
| soup = BeautifulSoup(body, 'html.parser') | ||
| self._links = [] | ||
| for link in soup.find_all('a'): | ||
| if find_links: | ||
| a = self._find_replace(soup, 'a', 'href', base_url) | ||
| img = self._find_replace(soup, 'img', 'src', base_url) | ||
| self._links = [*a, *img] | ||
| self._body = str(soup) | ||
| self._text = soup.text | ||
| def _find_replace(self, soup: BeautifulSoup, | ||
| tag: str, attr: str, base_url: str) -> list[Link]: | ||
| links = [] | ||
| for el in soup.find_all(tag): | ||
| # Add link for later download | ||
| links.append(Link(href=link.get('href'), text=link.text.strip())) | ||
| uri = el.get(attr) | ||
| # Replace for local instance | ||
| if link['href'].startswith(base_url): | ||
| filename = link.text.strip() | ||
| link['href'] = filename | ||
| link.string = filename | ||
| self._links = links | ||
| self.soup = soup | ||
| if uri: | ||
| # Handle url-encoding | ||
| filename = unquote(uri.split('/')[-1]) | ||
| links.append(Link(href=uri, text=filename)) | ||
| # Replace for local instance | ||
| if uri.startswith(base_url): | ||
| el[attr] = filename | ||
| return links | ||
| @property | ||
@@ -65,5 +81,9 @@ def links(self) -> List[Link]: | ||
| def body(self) -> str: | ||
| return str(self.soup) | ||
| return self._body | ||
| @property | ||
| def text(self) -> str: | ||
| return self._text | ||
| def validate_webdav_response(response: Response, | ||
@@ -70,0 +90,0 @@ link: str, base_url: str) -> bool: |
@@ -44,2 +44,2 @@ # Copyright (C) 2024, Jacob Sánchez Pérez | ||
| for future in done: | ||
| error = future.result() | ||
| future.result() |
@@ -222,3 +222,3 @@ """ | ||
| file_handler = logging.FileHandler(log_path) | ||
| file_handler.setLevel(logging.ERROR) | ||
| file_handler.setLevel(logging.WARNING) | ||
@@ -225,0 +225,0 @@ logger.addHandler(file_handler) |
@@ -23,3 +23,2 @@ """ | ||
| import requests | ||
| from pathlib import Path | ||
| from packaging import version | ||
@@ -26,0 +25,0 @@ from importlib.metadata import PackageNotFoundError |
| Metadata-Version: 2.1 | ||
| Name: blackboardsync | ||
| Version: 0.17.4 | ||
| Version: 0.17.5rc1 | ||
| Summary: Sync your blackboard content to your device | ||
@@ -30,2 +30,3 @@ Author-email: Jacob Sánchez <jacobszpz@protonmail.com> | ||
| Requires-Dist: whoisit | ||
| Requires-Dist: bwfilters | ||
| Provides-Extra: test | ||
@@ -32,0 +33,0 @@ Requires-Dist: pytest; extra == "test" |
@@ -13,2 +13,3 @@ pyqt6>=6.7.1 | ||
| whoisit | ||
| bwfilters | ||
@@ -15,0 +16,0 @@ [package] |
@@ -47,2 +47,3 @@ .gitignore | ||
| blackboard_sync/content/job.py | ||
| blackboard_sync/content/templates.py | ||
| blackboard_sync/content/unhandled.py | ||
@@ -81,3 +82,2 @@ blackboard_sync/content/webdav.py | ||
| docs/_static/custom.css | ||
| docs/dev/bb_api.rst | ||
| docs/dev/qt_api.rst | ||
@@ -84,0 +84,0 @@ docs/dev/sync_api.rst |
+14
-0
@@ -9,2 +9,16 @@ # Changelog | ||
| ### Added | ||
| - Images contained within descriptions are also downloaded | ||
| - Body descriptions can be shared via WebShare or email | ||
| ### Fixed | ||
| - Links are now not assumed to have an 'href' attribute | ||
| - Avoid parsing empty descriptions | ||
| - Webdav filenames are URL decoded | ||
| - Documentation build at readthedocs.org | ||
| ### Changed | ||
| - File logging level is now WARNING rather than ERROR | ||
| - Body descriptions have custom styling rather than pure html | ||
| ## [0.17.4] - 2024-10-12 | ||
@@ -11,0 +25,0 @@ |
+2
-1
@@ -18,2 +18,3 @@ # Configuration file for the Sphinx documentation builder. | ||
| from importlib.metadata import version as get_version | ||
| from importlib.metadata import PackageNotFoundError | ||
@@ -29,3 +30,3 @@ from blackboard_sync import __about__ | ||
| # The full version, including alpha/beta/rc tags | ||
| release = get_version("blackboard_sync") | ||
| release = get_version("blackboardsync") | ||
| version = ".".join(release.split('.')[:2]) | ||
@@ -32,0 +33,0 @@ |
@@ -9,5 +9,5 @@ .. _qt_api: | ||
| .. automodule:: blackboard_sync.qt.qt_elements | ||
| .. automodule:: blackboard_sync.qt | ||
| :members: | ||
| :undoc-members: | ||
| :exclude-members: app_logo |
+1
-2
@@ -46,3 +46,3 @@ .. BlackboardSync documentation master file, created by | ||
| $ python3 -m pip install blackboardsync | ||
| $ python3 -m blackboard_sync # notice the underscore | ||
| $ blackboardsync | ||
@@ -60,4 +60,3 @@ | ||
| dev/bb_api | ||
| dev/sync_api | ||
| dev/qt_api |
| -i https://pypi.org/simple | ||
| recommonmark==0.7.1 | ||
| sphinx==7.4.7; python_version >= '3.9' | ||
| sphinx-rtd-theme==2.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' | ||
| sphinx==8.1.3; python_version >= '3.9' | ||
| sphinx-rtd-theme==3.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' | ||
| pyqt6==6.7.1 | ||
| pyqt6-webengine==6.7.0 | ||
| pydantic==2.9.2 | ||
| pyqt5==5.15.11 | ||
| appdirs==1.4.4 | ||
@@ -11,2 +12,7 @@ python-dateutil==2.9.0.post0 | ||
| pathvalidate==3.2.1 | ||
| pyqtwebengine==5.15.7 | ||
| whoisit==3.0.4 | ||
| bblearn==0.3.6 | ||
| bwfilters==0.1.0 | ||
| setuptools==75.1.0 | ||
| setuptools-scm==8.1.0 | ||
| -e . |
+2
-1
@@ -19,3 +19,4 @@ [[source]] | ||
| bblearn = ">=0.3.4.post0" | ||
| whoisit = "*" | ||
| whoisit = ">=3.0.4" | ||
| bwfilters = "==0.1.0" | ||
@@ -22,0 +23,0 @@ [dev-packages] |
+2
-1
| Metadata-Version: 2.1 | ||
| Name: blackboardsync | ||
| Version: 0.17.4 | ||
| Version: 0.17.5rc1 | ||
| Summary: Sync your blackboard content to your device | ||
@@ -30,2 +30,3 @@ Author-email: Jacob Sánchez <jacobszpz@protonmail.com> | ||
| Requires-Dist: whoisit | ||
| Requires-Dist: bwfilters | ||
| Provides-Extra: test | ||
@@ -32,0 +33,0 @@ Requires-Dist: pytest; extra == "test" |
+2
-1
@@ -34,3 +34,4 @@ [build-system] | ||
| "bblearn>=0.3.0", | ||
| "whoisit" | ||
| "whoisit", | ||
| "bwfilters" | ||
| ] | ||
@@ -37,0 +38,0 @@ |
| .. _bb_api: | ||
| Blackboard API Reference | ||
| ======================== | ||
| Blackboard REST API | ||
| ------------------- | ||
| REST API call parameters must be specified in keyword form. | ||
| .. automodule:: blackboard_sync.blackboard.api | ||
| :members: | ||
| :undoc-members: | ||
| Blackboard Data Classes | ||
| ----------------------- | ||
| .. automodule:: blackboard_sync.blackboard.blackboard | ||
| :members: | ||
| :undoc-members: |
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
654257
1.22%4881
2.97%