Merge pull request #379 from pylast/httpx

This commit is contained in:
Hugo van Kemenade 2022-02-27 16:22:07 +02:00 committed by GitHub
commit 4ae6c16f57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 52 additions and 86 deletions

View file

@ -16,7 +16,7 @@ repos:
hooks: hooks:
- id: blacken-docs - id: blacken-docs
args: [--target-version=py37] args: [--target-version=py37]
additional_dependencies: [black==21.11b1] additional_dependencies: [black==21.12b0]
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.10.1 rev: 5.10.1

View file

@ -32,6 +32,7 @@ keywords =
[options] [options]
packages = find: packages = find:
install_requires = install_requires =
httpx
importlib-metadata;python_version < '3.8' importlib-metadata;python_version < '3.8'
python_requires = >=3.7 python_requires = >=3.7
package_dir = =src package_dir = =src

View file

@ -19,6 +19,8 @@
# #
# https://github.com/pylast/pylast # https://github.com/pylast/pylast
from __future__ import annotations
import collections import collections
import hashlib import hashlib
import html.entities import html.entities
@ -30,10 +32,11 @@ import ssl
import tempfile import tempfile
import time import time
import xml.dom import xml.dom
from http.client import HTTPSConnection
from urllib.parse import quote_plus from urllib.parse import quote_plus
from xml.dom import Node, minidom from xml.dom import Node, minidom
import httpx
try: try:
# Python 3.8+ # Python 3.8+
import importlib.metadata as importlib_metadata import importlib.metadata as importlib_metadata
@ -125,6 +128,12 @@ DELAY_TIME = 0.2
# Python >3.4 has sane defaults # Python >3.4 has sane defaults
SSL_CONTEXT = ssl.create_default_context() SSL_CONTEXT = ssl.create_default_context()
HEADERS = {
"Content-type": "application/x-www-form-urlencoded",
"Accept-Charset": "utf-8",
"User-Agent": f"pylast/{__version__}",
}
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())
@ -187,7 +196,6 @@ class _Network:
self.urls = urls self.urls = urls
self.cache_backend = None self.cache_backend = None
self.proxy_enabled = False
self.proxy = None self.proxy = None
self.last_call_time = 0 self.last_call_time = 0
self.limit_rate = False self.limit_rate = False
@ -387,26 +395,20 @@ class _Network:
return seq return seq
def enable_proxy(self, host, port): def enable_proxy(self, proxy: str | dict) -> None:
"""Enable a default web proxy""" """Enable default web proxy.
Multiple proxies can be passed as a `dict`, see
https://www.python-httpx.org/advanced/#http-proxying
"""
self.proxy = proxy
self.proxy = [host, _number(port)] def disable_proxy(self) -> None:
self.proxy_enabled = True
def disable_proxy(self):
"""Disable using the web proxy""" """Disable using the web proxy"""
self.proxy = None
self.proxy_enabled = False def is_proxy_enabled(self) -> bool:
"""Returns True if web proxy is enabled."""
def is_proxy_enabled(self): return self.proxy is not None
"""Returns True if a web proxy is enabled."""
return self.proxy_enabled
def _get_proxy(self):
"""Returns proxy details."""
return self.proxy
def enable_rate_limit(self): def enable_rate_limit(self):
"""Enables rate limiting for this network""" """Enables rate limiting for this network"""
@ -906,68 +908,41 @@ class _Request:
self.network._delay_call() self.network._delay_call()
username = self.params.pop("username", None) username = self.params.pop("username", None)
username = f"?username={username}" if username is not None else "" username = "" if username is None else f"?username={username}"
data = []
for name in self.params.keys():
data.append("=".join((name, quote_plus(_string(self.params[name])))))
data = "&".join(data)
headers = {
"Content-type": "application/x-www-form-urlencoded",
"Accept-Charset": "utf-8",
"User-Agent": "pylast/" + __version__,
}
(host_name, host_subdir) = self.network.ws_server (host_name, host_subdir) = self.network.ws_server
if self.network.is_proxy_enabled(): if self.network.is_proxy_enabled():
conn = HTTPSConnection( client = httpx.Client(
context=SSL_CONTEXT, verify=SSL_CONTEXT,
host=self.network._get_proxy()[0], base_url=f"https://{host_name}",
port=self.network._get_proxy()[1], headers=HEADERS,
proxies=self.network.proxy,
) )
try:
conn.request(
method="POST",
url=f"https://{host_name}{host_subdir}{username}",
body=data,
headers=headers,
)
except Exception as e:
raise NetworkError(self.network, e) from e
else: else:
conn = HTTPSConnection(context=SSL_CONTEXT, host=host_name) client = httpx.Client(
verify=SSL_CONTEXT,
base_url=f"https://{host_name}",
headers=HEADERS,
)
try: try:
conn.request( response = client.post(f"{host_subdir}{username}", data=self.params)
method="POST",
url=f"{host_subdir}{username}",
body=data,
headers=headers,
)
except Exception as e: except Exception as e:
raise NetworkError(self.network, e) from e raise NetworkError(self.network, e) from e
try: if response.status_code in (500, 502, 503, 504):
response = conn.getresponse()
if response.status in [500, 502, 503, 504]:
raise WSError( raise WSError(
self.network, self.network,
response.status, response.status_code,
"Connection to the API failed with HTTP code " f"Connection to the API failed with HTTP code {response.status_code}",
+ str(response.status),
) )
response_text = _unicode(response.read()) response_text = _unicode(response.read())
except Exception as e:
raise MalformedResponseError(self.network, e) from e
try: try:
self._check_response_for_errors(response_text) self._check_response_for_errors(response_text)
finally: finally:
conn.close() client.close()
return response_text return response_text
def execute(self, cacheable: bool = False) -> xml.dom.minidom.Document: def execute(self, cacheable: bool = False) -> xml.dom.minidom.Document:
@ -1121,7 +1096,7 @@ Image = collections.namedtuple(
def _string_output(func): def _string_output(func):
def r(*args): def r(*args):
return _string(func(*args)) return str(func(*args))
return r return r
@ -2744,18 +2719,10 @@ def md5(text):
def _unicode(text): def _unicode(text):
if isinstance(text, bytes): if isinstance(text, bytes):
return str(text, "utf-8") return str(text, "utf-8")
elif isinstance(text, str):
return text
else: else:
return str(text) return str(text)
def _string(string):
if isinstance(string, str):
return string
return str(string)
def cleanup_nodes(doc): def cleanup_nodes(doc):
""" """
Remove text nodes containing only whitespace Remove text nodes containing only whitespace
@ -2901,7 +2868,7 @@ def _extract_tracks(doc, network):
def _url_safe(text): def _url_safe(text):
"""Does all kinds of tricks on a text to make it safe to use in a URL.""" """Does all kinds of tricks on a text to make it safe to use in a URL."""
return quote_plus(quote_plus(_string(text))).lower() return quote_plus(quote_plus(str(text))).lower()
def _number(string): def _number(string):
@ -2928,7 +2895,7 @@ def _unescape_htmlentity(string):
def _parse_response(response: str) -> xml.dom.minidom.Document: def _parse_response(response: str) -> xml.dom.minidom.Document:
response = _string(response).replace("opensearch:", "") response = str(response).replace("opensearch:", "")
try: try:
doc = minidom.parseString(response) doc = minidom.parseString(response)
except xml.parsers.expat.ExpatError: except xml.parsers.expat.ExpatError:

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
@ -297,13 +296,12 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
def test_proxy(self): def test_proxy(self):
# Arrange # Arrange
host = "https://example.com" proxy = "http://example.com:1234"
port = 1234
# Act / Assert # Act / Assert
self.network.enable_proxy(host, port) self.network.enable_proxy(proxy)
assert self.network.is_proxy_enabled() assert self.network.is_proxy_enabled()
assert self.network._get_proxy() == ["https://example.com", 1234] assert self.network.proxy == "http://example.com:1234"
self.network.disable_proxy() self.network.disable_proxy()
assert not self.network.is_proxy_enabled() assert not self.network.is_proxy_enabled()