diff --git a/pylib/anki/httpclient.py b/pylib/anki/httpclient.py new file mode 100644 index 000000000..95bb82d49 --- /dev/null +++ b/pylib/anki/httpclient.py @@ -0,0 +1,79 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +""" +Wrapper for requests that adds hooks for tracking upload/download progress. + +The hooks http_data_did_send and http_data_did_receive will be called for each +chunk or partial read, on the thread that is running the request. +""" + +import io +import os +from typing import Any, Dict, Optional + +import requests +from requests import Response + +from anki import hooks + +HTTP_BUF_SIZE = 64 * 1024 + + +class AnkiRequestsClient: + + verify = True + timeout = 60 + + def __init__(self) -> None: + self.session = requests.Session() + + def post(self, url: str, data: Any, headers: Optional[Dict[str, str]]) -> Response: + data = _MonitoringFile(data) # pytype: disable=wrong-arg-types + headers["User-Agent"] = self._agentName() + return self.session.post( + url, + data=data, + headers=headers, + stream=True, + timeout=self.timeout, + verify=self.verify, + ) # pytype: disable=wrong-arg-types + + def get(self, url, headers=None) -> Response: + if headers is None: + headers = {} + headers["User-Agent"] = self._agentName() + return self.session.get( + url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify + ) + + def streamContent(self, resp) -> bytes: + resp.raise_for_status() + + buf = io.BytesIO() + for chunk in resp.iter_content(chunk_size=HTTP_BUF_SIZE): + hooks.http_data_did_receive(len(chunk)) + buf.write(chunk) + return buf.getvalue() + + def _agentName(self) -> str: + from anki import version + + return "Anki {}".format(version) + + +# allow user to accept invalid certs in work/school settings +if os.environ.get("ANKI_NOVERIFYSSL"): + AnkiRequestsClient.verify = False + + import warnings + + warnings.filterwarnings("ignore") + + +class _MonitoringFile(io.BufferedReader): + def read(self, size=-1) -> bytes: + data = io.BufferedReader.read(self, HTTP_BUF_SIZE) + hooks.http_data_did_send(len(data)) + return data diff --git a/pylib/anki/sync.py b/pylib/anki/sync.py index 7ce3a4f4c..d138c9965 100644 --- a/pylib/anki/sync.py +++ b/pylib/anki/sync.py @@ -11,20 +11,15 @@ import random import sqlite3 from typing import Any, Dict, List, Optional, Tuple, Union -import requests - import anki from anki.consts import * from anki.db import DB, DBError from anki.utils import checksum, devMode, ids2str, intTime, platDesc, versionWithBuild from . import hooks +from .httpclient import AnkiRequestsClient from .lang import ngettext -# syncing vars -HTTP_TIMEOUT = 90 -HTTP_BUF_SIZE = 64 * 1024 - class UnexpectedSchemaChange(Exception): pass @@ -464,69 +459,6 @@ from notes where %s""" self.col.conf = conf -# Wrapper for requests that tracks upload/download progress -########################################################################## - - -class AnkiRequestsClient: - - verify = True - timeout = 60 - - def __init__(self) -> None: - self.session = requests.Session() - - def post(self, url, data, headers) -> Any: - data = _MonitoringFile(data) # pytype: disable=wrong-arg-types - headers["User-Agent"] = self._agentName() - return self.session.post( - url, - data=data, - headers=headers, - stream=True, - timeout=self.timeout, - verify=self.verify, - ) # pytype: disable=wrong-arg-types - - def get(self, url, headers=None) -> requests.models.Response: - if headers is None: - headers = {} - headers["User-Agent"] = self._agentName() - return self.session.get( - url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify - ) - - def streamContent(self, resp) -> bytes: - resp.raise_for_status() - - buf = io.BytesIO() - for chunk in resp.iter_content(chunk_size=HTTP_BUF_SIZE): - hooks.http_data_did_receive(len(chunk)) - buf.write(chunk) - return buf.getvalue() - - def _agentName(self) -> str: - from anki import version - - return "Anki {}".format(version) - - -# allow user to accept invalid certs in work/school settings -if os.environ.get("ANKI_NOVERIFYSSL"): - AnkiRequestsClient.verify = False - - import warnings - - warnings.filterwarnings("ignore") - - -class _MonitoringFile(io.BufferedReader): - def read(self, size=-1) -> bytes: - data = io.BufferedReader.read(self, HTTP_BUF_SIZE) - hooks.http_data_did_send(len(data)) - return data - - # HTTP syncing tools ########################################################################## diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index b6d4b46db..f84573b99 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -17,8 +17,8 @@ from send2trash import send2trash import aqt import aqt.forms +from anki.httpclient import AnkiRequestsClient from anki.lang import _, ngettext -from anki.sync import AnkiRequestsClient from anki.utils import intTime from aqt.downloader import download from aqt.qt import * diff --git a/qt/aqt/downloader.py b/qt/aqt/downloader.py index 379f2588e..234b14bca 100644 --- a/qt/aqt/downloader.py +++ b/qt/aqt/downloader.py @@ -7,8 +7,8 @@ import time import aqt from anki import hooks +from anki.httpclient import AnkiRequestsClient from anki.lang import _ -from anki.sync import AnkiRequestsClient from aqt.qt import * diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 86e861a83..b0ece2782 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -20,9 +20,9 @@ from bs4 import BeautifulSoup import aqt import aqt.sound from anki.hooks import runFilter +from anki.httpclient import AnkiRequestsClient from anki.lang import _ from anki.notes import Note -from anki.sync import AnkiRequestsClient from anki.utils import checksum, isWin, namedtmp, stripHTMLMedia from aqt import AnkiQt, gui_hooks from aqt.qt import *