Merge pull request #379 from pylast/httpx
This commit is contained in:
commit
4ae6c16f57
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
client = httpx.Client(
|
||||||
|
verify=SSL_CONTEXT,
|
||||||
|
base_url=f"https://{host_name}",
|
||||||
|
headers=HEADERS,
|
||||||
)
|
)
|
||||||
|
|
||||||
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:
|
|
||||||
conn = HTTPSConnection(context=SSL_CONTEXT, host=host_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
conn.request(
|
|
||||||
method="POST",
|
|
||||||
url=f"{host_subdir}{username}",
|
|
||||||
body=data,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise NetworkError(self.network, e) from e
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = conn.getresponse()
|
response = client.post(f"{host_subdir}{username}", data=self.params)
|
||||||
if response.status in [500, 502, 503, 504]:
|
|
||||||
raise WSError(
|
|
||||||
self.network,
|
|
||||||
response.status,
|
|
||||||
"Connection to the API failed with HTTP code "
|
|
||||||
+ str(response.status),
|
|
||||||
)
|
|
||||||
response_text = _unicode(response.read())
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise MalformedResponseError(self.network, e) from e
|
raise NetworkError(self.network, e) from e
|
||||||
|
|
||||||
|
if response.status_code in (500, 502, 503, 504):
|
||||||
|
raise WSError(
|
||||||
|
self.network,
|
||||||
|
response.status_code,
|
||||||
|
f"Connection to the API failed with HTTP code {response.status_code}",
|
||||||
|
)
|
||||||
|
response_text = _unicode(response.read())
|
||||||
|
|
||||||
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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue