From mboxrd@z Thu Jan 1 00:00:00 1970 From: Adolf Belka To: development@lists.ipfire.org Subject: [PATCH] speedtest-cli: Fix for bug13805 - error message if run on hour or half hour Date: Mon, 06 Jan 2025 14:52:26 +0100 Message-ID: <20250106135226.13854-1-adolf.belka@ipfire.org> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="===============2117277164450723600==" List-Id: --===============2117277164450723600== Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable - Created a self consistent patch set out of four patches on the speedtest-cli github site. Slight changes needed in each to allow them to be successfull= y applied in sequence. - Additional comments added to top of the various patches. - Tested out this modified package on my vm testbed and it fixes the bug of speedtest-cli giving an error message if run on the hour or on the half ho= ur. I tested it out with the original system first and it failed with the error = message for 7 half hour tests. With this modified version it ran for 9 half hour s= lots with no problems at all. Tested with the command being run via fcrontab. - None of these patches have ben merged by the speedtest-cli github owner as = the last commit was July 2021 and the patches were proposed in Feb 2023. There has = been no resposne to anything on the speedtest-cli github site by the owner. - I have reviewed all the patches and the content looks fine to me with no co= ncerns from a security point of view although it would be good to get feedback fr= om alternative eyes. - Update of rootfile not required. Fixes: Bug13805 Tested-by: Adolf Belka Signed-off-by: Adolf Belka --- lfs/speedtest-cli | 8 +- .../speedtest-cli-2.1.3-fix_429_errors.patch | 101 + ...edtest-cli-2.1.3-python_3.10_support.patch | 146 ++ ...-2.1.3-python_3.11_updates_and_fixes.patch | 2302 +++++++++++++++++ ...python_3.12_remove_deprecated_method.patch | 27 + 5 files changed, 2582 insertions(+), 2 deletions(-) create mode 100644 src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_err= ors.patch create mode 100644 src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10= _support.patch create mode 100644 src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11= _updates_and_fixes.patch create mode 100644 src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.12= _remove_deprecated_method.patch diff --git a/lfs/speedtest-cli b/lfs/speedtest-cli index 0407c36bc..d0aa96c3c 100644 --- a/lfs/speedtest-cli +++ b/lfs/speedtest-cli @@ -1,7 +1,7 @@ ############################################################################= ### # = # # IPFire.org - A linux based firewall = # -# Copyright (C) 2007-2018 IPFire Team = # +# Copyright (C) 2007-2025 IPFire Team = # # = # # This program is free software: you can redistribute it and/or modify = # # it under the terms of the GNU General Public License as published by = # @@ -34,7 +34,7 @@ DL_FROM =3D $(URL_IPFIRE) DIR_APP =3D $(DIR_SRC)/$(THISAPP) TARGET =3D $(DIR_INFO)/$(THISAPP) PROG =3D speedtest-cli -PAK_VER =3D 5 +PAK_VER =3D 6 =20 DEPS =3D =20 @@ -81,6 +81,10 @@ $(subst %,%_BLAKE2,$(objects)) : $(TARGET) : $(patsubst %,$(DIR_DL)/%,$(objects)) @$(PREBUILD) @rm -rf $(DIR_APP) && cd $(DIR_SRC) && tar zxf $(DIR_DL)/$(DL_FILE) + cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtes= t-cli-2.1.3-python_3.10_support.patch + cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtes= t-cli-2.1.3-python_3.11_updates_and_fixes.patch + cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtes= t-cli-2.1.3-fix_429_errors.patch + cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtes= t-cli-2.1.3-python_3.12_remove_deprecated_method.patch cd $(DIR_APP) && python3 setup.py build cd $(DIR_APP) && python3 setup.py install --root=3D/ @rm -rf $(DIR_APP) diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_errors.pat= ch b/src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_errors.patch new file mode 100644 index 000000000..733550c76 --- /dev/null +++ b/src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_errors.patch @@ -0,0 +1,101 @@ +From 7906c4bdc36b969212526d71e83a2ecea5739704 Mon Sep 17 00:00:00 2001 +From: notmarrco +Date: Fri, 10 Feb 2023 11:51:33 +0100 +Subject: [PATCH 2/2] fix 429 errors + +Use the new json servers list +--- + speedtest.py | 46 +++++++++++----------------------------------- + 1 file changed, 11 insertions(+), 35 deletions(-) + +diff --git a/speedtest.py b/speedtest.py +index 408ce3510..c4929be7b 100755 +--- a/speedtest.py ++++ b/speedtest.py +@@ -18,6 +18,7 @@ + import csv + import datetime + import errno ++import json + import math + import os + import platform +@@ -1301,10 +1302,7 @@ def get_servers(self, servers=3DNone, exclude=3DNone): + ) +=20 + urls =3D [ +- "://www.speedtest.net/speedtest-servers-static.php", +- "http://c.speedtest.net/speedtest-servers-static.php", +- "://www.speedtest.net/speedtest-servers.php", +- "http://c.speedtest.net/speedtest-servers.php", ++ "://www.speedtest.net/api/js/servers", + ] +=20 + headers =3D {} +@@ -1346,56 +1344,34 @@ def get_servers(self, servers=3DNone, exclude=3DNone= ): + printer(f"Servers XML:\n{serversxml}", debug=3DTrue) +=20 + try: +- try: +- try: +- root =3D ET.fromstring(serversxml) +- except ET.ParseError: +- e =3D get_exception() +- raise SpeedtestServersError( +- f"Malformed speedtest.net server list: {e}", +- ) +- elements =3D etree_iter(root, "server") +- except AttributeError: +- try: +- root =3D DOM.parseString(serversxml) +- except ExpatError: +- e =3D get_exception() +- raise SpeedtestServersError( +- f"Malformed speedtest.net server list: {e}", +- ) +- elements =3D root.getElementsByTagName("server") +- except (SyntaxError, xml.parsers.expat.ExpatError): ++ elements =3D json.loads(serversxml) ++ except SyntaxError: + raise ServersRetrievalError() +=20 + for server in elements: +- try: +- attrib =3D server.attrib +- except AttributeError: +- attrib =3D dict(list(server.attributes.items())) +- +- if servers and int(attrib.get("id")) not in servers: ++ if servers and int(server.get("id")) not in servers: + continue +=20 + if ( +- int(attrib.get("id")) in self.config["ignore_server= s"] +- or int(attrib.get("id")) in exclude ++ int(server.get("id")) in self.config["ignore_server= s"] ++ or int(server.get("id")) in exclude + ): + continue +=20 + try: + d =3D distance( + self.lat_lon, +- (float(attrib.get("lat")), float(attrib.get("lo= n"))), ++ (float(server.get("lat")), float(server.get("lo= n"))), + ) + except Exception: + continue +=20 +- attrib["d"] =3D d ++ server["d"] =3D d +=20 + try: +- self.servers[d].append(attrib) ++ self.servers[d].append(server) + except KeyError: +- self.servers[d] =3D [attrib] ++ self.servers[d] =3D [server] +=20 + break +=20 + diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10_suppor= t.patch b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10_support.p= atch new file mode 100644 index 000000000..e3182d284 --- /dev/null +++ b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10_support.patch @@ -0,0 +1,146 @@ +Patch originally from + +From 22210ca35228f0bbcef75a7c14587c4ecb875ab4 Mon Sep 17 00:00:00 2001 +From: Matt Martz +Date: Wed, 7 Jul 2021 14:50:15 -0500 +Subject: [PATCH] Python 3.10 support + +but this changed the version of speedtest to 2.1.4b1 but only in speedtest.p= y not the rest of the package. +This modification by Adolf Belka does everything = the original patch did except for the version change. + +diff -Naur speedtest-cli-2.1.3.orig/setup.py speedtest-cli-2.1.3/setup.py +--- speedtest-cli-2.1.3.orig/setup.py 2021-04-08 15:45:29.000000000 +0200 ++++ speedtest-cli-2.1.3/setup.py 2025-01-05 12:54:36.284847079 +0100 +@@ -92,5 +92,8 @@ + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ++ 'Programming Language :: Python :: 3.8', ++ 'Programming Language :: Python :: 3.9', ++ 'Programming Language :: Python :: 3.10', + ] + ) +diff -Naur speedtest-cli-2.1.3.orig/speedtest.py speedtest-cli-2.1.3/speedte= st.py +--- speedtest-cli-2.1.3.orig/speedtest.py 2021-04-08 15:45:29.000000000 +0200 ++++ speedtest-cli-2.1.3/speedtest.py 2025-01-05 12:55:13.742881499 +0100 +@@ -15,18 +15,18 @@ + # License for the specific language governing permissions and limitations + # under the License. +=20 +-import os +-import re + import csv +-import sys +-import math ++import datetime + import errno ++import math ++import os ++import platform ++import re + import signal + import socket +-import timeit +-import datetime +-import platform ++import sys + import threading ++import timeit + import xml.parsers.expat +=20 + try: +@@ -49,6 +49,8 @@ + "Dummy method to always return false""" + return False +=20 ++ is_set =3D isSet ++ +=20 + # Some global variables we use + DEBUG =3D False +@@ -56,6 +58,7 @@ + PY25PLUS =3D sys.version_info[:2] >=3D (2, 5) + PY26PLUS =3D sys.version_info[:2] >=3D (2, 6) + PY32PLUS =3D sys.version_info[:2] >=3D (3, 2) ++PY310PLUS =3D sys.version_info[:2] >=3D (3, 10) +=20 + # Begin import game to handle Python 2 and Python 3 + try: +@@ -266,17 +269,6 @@ + write(arg) + write(end) +=20 +-if PY32PLUS: +- etree_iter =3D ET.Element.iter +-elif PY25PLUS: +- etree_iter =3D ET_Element.getiterator +- +-if PY26PLUS: +- thread_is_alive =3D threading.Thread.is_alive +-else: +- thread_is_alive =3D threading.Thread.isAlive +- +- + # Exception "constants" to support Python 2 through Python 3 + try: + import ssl +@@ -293,6 +285,23 @@ + ssl =3D None + HTTP_ERRORS =3D (HTTPError, URLError, socket.error, BadStatusLine) +=20 ++if PY32PLUS: ++ etree_iter =3D ET.Element.iter ++elif PY25PLUS: ++ etree_iter =3D ET_Element.getiterator ++ ++if PY26PLUS: ++ thread_is_alive =3D threading.Thread.is_alive ++else: ++ thread_is_alive =3D threading.Thread.isAlive ++ ++ ++def event_is_set(event): ++ try: ++ return event.is_set() ++ except AttributeError: ++ return event.isSet() ++ +=20 + class SpeedtestException(Exception): + """Base exception for this module""" +@@ -769,7 +778,7 @@ + status + """ + def inner(current, total, start=3DFalse, end=3DFalse): +- if shutdown_event.isSet(): ++ if event_is_set(shutdown_event): + return +=20 + sys.stdout.write('.') +@@ -808,7 +817,7 @@ + try: + if (timeit.default_timer() - self.starttime) <=3D self.timeout: + f =3D self._opener(self.request) +- while (not self._shutdown_event.isSet() and ++ while (not event_is_set(self._shutdown_event) and + (timeit.default_timer() - self.starttime) <=3D + self.timeout): + self.result.append(len(f.read(10240))) +@@ -864,7 +873,7 @@ +=20 + def read(self, n=3D10240): + if ((timeit.default_timer() - self.start) <=3D self.timeout and +- not self._shutdown_event.isSet()): ++ not event_is_set(self._shutdown_event)): + chunk =3D self.data.read(n) + self.total.append(len(chunk)) + return chunk +@@ -902,7 +911,7 @@ + request =3D self.request + try: + if ((timeit.default_timer() - self.starttime) <=3D self.timeout= and +- not self._shutdown_event.isSet()): ++ not event_is_set(self._shutdown_event)): + try: + f =3D self._opener(request) + except TypeError: diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11_update= s_and_fixes.patch b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11= _updates_and_fixes.patch new file mode 100644 index 000000000..0ea27d876 --- /dev/null +++ b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11_updates_and_f= ixes.patch @@ -0,0 +1,2302 @@ +Patch originally from + +From d456ed64c70fd0a1081410505daba3aef3e4fa61 Mon Sep 17 00:00:00 2001 +From: Mark Mayo +Date: Mon, 23 Jan 2023 17:03:58 +1300 +Subject: [PATCH 1/2] python 3.11 updates and fixes + +but this patch forgot to also include Python 3.11 in the Classifiers section= . This modified patch by Adolf Belka is the same a= s the original patch but with the inclusion of Python 3.11 in the Classifiers= section in setup.py + +diff -Naur speedtest-cli-2.1.3.orig/setup.py speedtest-cli-2.1.3/setup.py +--- speedtest-cli-2.1.3.orig/setup.py 2025-01-05 13:14:39.515389969 +0100 ++++ speedtest-cli-2.1.3/setup.py 2025-01-05 13:18:21.333439176 +0100 +@@ -15,9 +15,9 @@ + # License for the specific language governing permissions and limitations + # under the License. +=20 ++import codecs + import os + import re +-import codecs +=20 + from setuptools import setup +=20 +@@ -31,16 +31,15 @@ + # Open in Latin-1 so that we avoid encoding errors. + # Use codecs.open for Python 2 compatibility + try: +- f =3D codecs.open(os.path.join(here, *file_paths), 'r', 'latin1') ++ f =3D codecs.open(os.path.join(here, *file_paths), "r", "latin1") + version_file =3D f.read() + f.close() +- except: ++ except Exception: + raise RuntimeError("Unable to find version string.") +=20 + # The version line must have the form + # __version__ =3D 'ver' +- version_match =3D re.search(r"^__version__ =3D ['\"]([^'\"]*)['\"]", +- version_file, re.M) ++ version_match =3D re.search(r"^__version__ =3D ['\"]([^'\"]*)['\"]", ve= rsion_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") +@@ -48,52 +47,54 @@ +=20 + # Get the long description from the relevant file + try: +- f =3D codecs.open('README.rst', encoding=3D'utf-8') ++ f =3D codecs.open("README.rst", encoding=3D"utf-8") + long_description =3D f.read() + f.close() +-except: +- long_description =3D '' ++except Exception: ++ long_description =3D "" +=20 +=20 + setup( +- name=3D'speedtest-cli', +- version=3Dfind_version('speedtest.py'), +- description=3D('Command line interface for testing internet bandwidth u= sing ' +- 'speedtest.net'), ++ name=3D"speedtest-cli", ++ version=3Dfind_version("speedtest.py"), ++ description=3D( ++ "Command line interface for testing internet bandwidth using " "spe= edtest.net" ++ ), + long_description=3Dlong_description, +- keywords=3D'speedtest speedtest.net', +- author=3D'Matt Martz', +- author_email=3D'matt(a)sivel.net', +- url=3D'https://github.com/sivel/speedtest-cli', +- license=3D'Apache License, Version 2.0', +- py_modules=3D['speedtest'], ++ keywords=3D"speedtest speedtest.net", ++ author=3D"Matt Martz", ++ author_email=3D"matt(a)sivel.net", ++ url=3D"https://github.com/sivel/speedtest-cli", ++ license=3D"Apache License, Version 2.0", ++ py_modules=3D["speedtest"], + entry_points=3D{ +- 'console_scripts': [ +- 'speedtest=3Dspeedtest:main', +- 'speedtest-cli=3Dspeedtest:main' +- ] ++ "console_scripts": [ ++ "speedtest=3Dspeedtest:main", ++ "speedtest-cli=3Dspeedtest:main", ++ ], + }, + classifiers=3D[ +- 'Development Status :: 5 - Production/Stable', +- 'Programming Language :: Python', +- 'Environment :: Console', +- 'License :: OSI Approved :: Apache Software License', +- 'Operating System :: OS Independent', +- 'Programming Language :: Python :: 2', +- 'Programming Language :: Python :: 2.4', +- 'Programming Language :: Python :: 2.5', +- 'Programming Language :: Python :: 2.6', +- 'Programming Language :: Python :: 2.7', +- 'Programming Language :: Python :: 3', +- 'Programming Language :: Python :: 3.1', +- 'Programming Language :: Python :: 3.2', +- 'Programming Language :: Python :: 3.3', +- 'Programming Language :: Python :: 3.4', +- 'Programming Language :: Python :: 3.5', +- 'Programming Language :: Python :: 3.6', +- 'Programming Language :: Python :: 3.7', +- 'Programming Language :: Python :: 3.8', +- 'Programming Language :: Python :: 3.9', +- 'Programming Language :: Python :: 3.10', +- ] ++ "Development Status :: 5 - Production/Stable", ++ "Programming Language :: Python", ++ "Environment :: Console", ++ "License :: OSI Approved :: Apache Software License", ++ "Operating System :: OS Independent", ++ "Programming Language :: Python :: 2", ++ "Programming Language :: Python :: 2.4", ++ "Programming Language :: Python :: 2.5", ++ "Programming Language :: Python :: 2.6", ++ "Programming Language :: Python :: 2.7", ++ "Programming Language :: Python :: 3", ++ "Programming Language :: Python :: 3.1", ++ "Programming Language :: Python :: 3.2", ++ "Programming Language :: Python :: 3.3", ++ "Programming Language :: Python :: 3.4", ++ "Programming Language :: Python :: 3.5", ++ "Programming Language :: Python :: 3.6", ++ "Programming Language :: Python :: 3.7", ++ "Programming Language :: Python :: 3.8", ++ "Programming Language :: Python :: 3.9", ++ "Programming Language :: Python :: 3.10" ++ "Programming Language :: Python :: 3.11", ++ ], + ) +diff -Naur speedtest-cli-2.1.3.orig/speedtest.py speedtest-cli-2.1.3/speedte= st.py +--- speedtest-cli-2.1.3.orig/speedtest.py 2025-01-05 13:14:39.655395043 +0100 ++++ speedtest-cli-2.1.3/speedtest.py 2025-01-05 13:17:05.914033926 +0100 +@@ -31,22 +31,23 @@ +=20 + try: + import gzip ++ + GZIP_BASE =3D gzip.GzipFile + except ImportError: + gzip =3D None + GZIP_BASE =3D object +=20 +-__version__ =3D '2.1.3' ++__version__ =3D "2.1.3" +=20 +=20 +-class FakeShutdownEvent(object): ++class FakeShutdownEvent: + """Class to fake a threading.Event.isSet so that users of this module + are not required to register their own threading.Event() + """ +=20 + @staticmethod + def isSet(): +- "Dummy method to always return false""" ++ """Dummy method to always return false""" + return False +=20 + is_set =3D isSet +@@ -71,6 +72,7 @@ +=20 + try: + import xml.etree.ElementTree as ET ++ + try: + from xml.etree.ElementTree import _Element as ET_Element + except ImportError: +@@ -78,23 +80,24 @@ + except ImportError: + from xml.dom import minidom as DOM + from xml.parsers.expat import ExpatError ++ + ET =3D None +=20 + try: +- from urllib2 import (urlopen, Request, HTTPError, URLError, +- AbstractHTTPHandler, ProxyHandler, +- HTTPDefaultErrorHandler, HTTPRedirectHandler, +- HTTPErrorProcessor, OpenerDirector) ++ from urllib2 import (AbstractHTTPHandler, HTTPDefaultErrorHandler, ++ HTTPError, HTTPErrorProcessor, HTTPRedirectHandler, ++ OpenerDirector, ProxyHandler, Request, URLError, ++ urlopen) + except ImportError: +- from urllib.request import (urlopen, Request, HTTPError, URLError, +- AbstractHTTPHandler, ProxyHandler, +- HTTPDefaultErrorHandler, HTTPRedirectHandle= r, +- HTTPErrorProcessor, OpenerDirector) ++ from urllib.request import (AbstractHTTPHandler, HTTPDefaultErrorHandle= r, ++ HTTPError, HTTPErrorProcessor, ++ HTTPRedirectHandler, OpenerDirector, ++ ProxyHandler, Request, URLError, urlopen) +=20 + try: +- from httplib import HTTPConnection, BadStatusLine ++ from httplib import BadStatusLine, HTTPConnection + except ImportError: +- from http.client import HTTPConnection, BadStatusLine ++ from http.client import BadStatusLine, HTTPConnection +=20 + try: + from httplib import HTTPSConnection +@@ -133,51 +136,52 @@ + from md5 import md5 +=20 + try: +- from argparse import ArgumentParser as ArgParser + from argparse import SUPPRESS as ARG_SUPPRESS ++ from argparse import ArgumentParser as ArgParser ++ + PARSER_TYPE_INT =3D int + PARSER_TYPE_STR =3D str + PARSER_TYPE_FLOAT =3D float + except ImportError: +- from optparse import OptionParser as ArgParser + from optparse import SUPPRESS_HELP as ARG_SUPPRESS +- PARSER_TYPE_INT =3D 'int' +- PARSER_TYPE_STR =3D 'string' +- PARSER_TYPE_FLOAT =3D 'float' ++ from optparse import OptionParser as ArgParser ++ ++ PARSER_TYPE_INT =3D "int" ++ PARSER_TYPE_STR =3D "string" ++ PARSER_TYPE_FLOAT =3D "float" +=20 + try: + from cStringIO import StringIO ++ + BytesIO =3D None + except ImportError: + try: + from StringIO import StringIO ++ + BytesIO =3D None + except ImportError: +- from io import StringIO, BytesIO ++ from io import BytesIO, StringIO +=20 + try: + import __builtin__ + except ImportError: + import builtins +- from io import TextIOWrapper, FileIO ++ from io import FileIO, TextIOWrapper +=20 + class _Py3Utf8Output(TextIOWrapper): + """UTF-8 encoded wrapper around stdout for py3, to override + ASCII stdout + """ ++ + def __init__(self, f, **kwargs): +- buf =3D FileIO(f.fileno(), 'w') +- super(_Py3Utf8Output, self).__init__( +- buf, +- encoding=3D'utf8', +- errors=3D'strict' +- ) ++ buf =3D FileIO(f.fileno(), "w") ++ super().__init__(buf, encoding=3D"utf8", errors=3D"strict") +=20 + def write(self, s): +- super(_Py3Utf8Output, self).write(s) ++ super().write(s) + self.flush() +=20 +- _py3_print =3D getattr(builtins, 'print') ++ _py3_print =3D getattr(builtins, "print") + try: + _py3_utf8_stdout =3D _Py3Utf8Output(sys.stdout) + _py3_utf8_stderr =3D _Py3Utf8Output(sys.stderr) +@@ -188,23 +192,24 @@ + _py3_utf8_stderr =3D sys.stderr +=20 + def to_utf8(v): +- """No-op encode to utf-8 for py3""" ++ """No-op encode to utf-8 for py3.""" + return v +=20 + def print_(*args, **kwargs): +- """Wrapper function for py3 to print, with a utf-8 encoded stdout""" +- if kwargs.get('file') =3D=3D sys.stderr: +- kwargs['file'] =3D _py3_utf8_stderr ++ """Wrapper function for py3 to print, with a utf-8 encoded stdout."= "" ++ if kwargs.get("file") =3D=3D sys.stderr: ++ kwargs["file"] =3D _py3_utf8_stderr + else: +- kwargs['file'] =3D kwargs.get('file', _py3_utf8_stdout) ++ kwargs["file"] =3D kwargs.get("file", _py3_utf8_stdout) + _py3_print(*args, **kwargs) ++ + else: + del __builtin__ +=20 + def to_utf8(v): +- """Encode value to utf-8 if possible for py2""" ++ """Encode value to utf-8 if possible for py2.""" + try: +- return v.encode('utf8', 'strict') ++ return v.encode("utf8", "strict") + except AttributeError: + return v +=20 +@@ -223,16 +228,19 @@ + if not isinstance(data, basestring): + data =3D str(data) + # If the file has an encoding, encode unicode with it. +- encoding =3D 'utf8' # Always trust UTF-8 for output +- if (isinstance(fp, file) and +- isinstance(data, unicode) and +- encoding is not None): ++ encoding =3D "utf8" # Always trust UTF-8 for output ++ if ( ++ isinstance(fp, file) ++ and isinstance(data, unicode) ++ and encoding is not None ++ ): + errors =3D getattr(fp, "errors", None) + if errors is None: + errors =3D "strict" + data =3D data.encode(encoding, errors) + fp.write(data) + fp.flush() ++ + want_unicode =3D False + sep =3D kwargs.pop("sep", None) + if sep is not None: +@@ -269,18 +277,23 @@ + write(arg) + write(end) +=20 ++ + # Exception "constants" to support Python 2 through Python 3 + try: + import ssl ++ + try: + CERT_ERROR =3D (ssl.CertificateError,) + except AttributeError: + CERT_ERROR =3D tuple() +=20 + HTTP_ERRORS =3D ( +- (HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) + +- CERT_ERROR +- ) ++ HTTPError, ++ URLError, ++ socket.error, ++ ssl.SSLError, ++ BadStatusLine, ++ ) + CERT_ERROR + except ImportError: + ssl =3D None + HTTP_ERRORS =3D (HTTPError, URLError, socket.error, BadStatusLine) +@@ -373,8 +386,7 @@ + """get_best_server not called or not able to determine best server""" +=20 +=20 +-def create_connection(address, timeout=3D_GLOBAL_DEFAULT_TIMEOUT, +- source_address=3DNone): ++def create_connection(address, timeout=3D_GLOBAL_DEFAULT_TIMEOUT, source_ad= dress=3DNone): + """Connect to *address* and return the socket object. +=20 + Convenience function. Connect to *address* (a 2-tuple ``(host, +@@ -388,7 +400,6 @@ +=20 + Largely vendored from Python 2.7, modified to work with Python 2.4 + """ +- + host, port =3D address + err =3D None + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): +@@ -410,17 +421,17 @@ +=20 + if err is not None: + raise err +- else: +- raise socket.error("getaddrinfo returns an empty list") ++ raise socket.error("getaddrinfo returns an empty list") +=20 +=20 + class SpeedtestHTTPConnection(HTTPConnection): + """Custom HTTPConnection to support source_address across + Python 2.4 - Python 3 + """ ++ + def __init__(self, *args, **kwargs): +- source_address =3D kwargs.pop('source_address', None) +- timeout =3D kwargs.pop('timeout', 10) ++ source_address =3D kwargs.pop("source_address", None) ++ timeout =3D kwargs.pop("timeout", 10) +=20 + self._tunnel_host =3D None +=20 +@@ -435,13 +446,13 @@ + self.sock =3D socket.create_connection( + (self.host, self.port), + self.timeout, +- self.source_address ++ self.source_address, + ) + except (AttributeError, TypeError): + self.sock =3D create_connection( + (self.host, self.port), + self.timeout, +- self.source_address ++ self.source_address, + ) +=20 + if self._tunnel_host: +@@ -449,15 +460,17 @@ +=20 +=20 + if HTTPSConnection: ++ + class SpeedtestHTTPSConnection(HTTPSConnection): + """Custom HTTPSConnection to support source_address across + Python 2.4 - Python 3 + """ ++ + default_port =3D 443 +=20 + def __init__(self, *args, **kwargs): +- source_address =3D kwargs.pop('source_address', None) +- timeout =3D kwargs.pop('timeout', 10) ++ source_address =3D kwargs.pop("source_address", None) ++ timeout =3D kwargs.pop("timeout", 10) +=20 + self._tunnel_host =3D None +=20 +@@ -467,18 +480,18 @@ + self.source_address =3D source_address +=20 + def connect(self): +- "Connect to a host on a given (SSL) port." ++ """Connect to a host on a given (SSL) port.""" + try: + self.sock =3D socket.create_connection( + (self.host, self.port), + self.timeout, +- self.source_address ++ self.source_address, + ) + except (AttributeError, TypeError): + self.sock =3D create_connection( + (self.host, self.port), + self.timeout, +- self.source_address ++ self.source_address, + ) +=20 + if self._tunnel_host: +@@ -487,11 +500,11 @@ + if ssl: + try: + kwargs =3D {} +- if hasattr(ssl, 'SSLContext'): ++ if hasattr(ssl, "SSLContext"): + if self._tunnel_host: +- kwargs['server_hostname'] =3D self._tunnel_host ++ kwargs["server_hostname"] =3D self._tunnel_host + else: +- kwargs['server_hostname'] =3D self.host ++ kwargs["server_hostname"] =3D self.host + self.sock =3D self._context.wrap_socket(self.sock, **kw= args) + except AttributeError: + self.sock =3D ssl.wrap_socket(self.sock) +@@ -505,13 +518,13 @@ + self.sock =3D FakeSocket(self.sock, socket.ssl(self.soc= k)) + except AttributeError: + raise SpeedtestException( +- 'This version of Python does not support HTTPS/SSL ' +- 'functionality' ++ "This version of Python does not support HTTPS/SSL " ++ "functionality", + ) + else: + raise SpeedtestException( +- 'This version of Python does not support HTTPS/SSL ' +- 'functionality' ++ "This version of Python does not support HTTPS/SSL " ++ "functionality", + ) +=20 +=20 +@@ -522,14 +535,13 @@ + Called from ``http(s)_open`` methods of ``SpeedtestHTTPHandler`` or + ``SpeedtestHTTPSHandler`` + """ ++ + def inner(host, **kwargs): +- kwargs.update({ +- 'source_address': source_address, +- 'timeout': timeout +- }) ++ kwargs.update({"source_address": source_address, "timeout": timeout= }) + if context: +- kwargs['context'] =3D context ++ kwargs["context"] =3D context + return connection(host, **kwargs) ++ + return inner +=20 +=20 +@@ -537,6 +549,7 @@ + """Custom ``HTTPHandler`` that can build a ``HTTPConnection`` with the + args we need for ``source_address`` and ``timeout`` + """ ++ + def __init__(self, debuglevel=3D0, source_address=3DNone, timeout=3D10): + AbstractHTTPHandler.__init__(self, debuglevel) + self.source_address =3D source_address +@@ -547,9 +560,9 @@ + _build_connection( + SpeedtestHTTPConnection, + self.source_address, +- self.timeout ++ self.timeout, + ), +- req ++ req, + ) +=20 + http_request =3D AbstractHTTPHandler.do_request_ +@@ -559,8 +572,8 @@ + """Custom ``HTTPSHandler`` that can build a ``HTTPSConnection`` with the + args we need for ``source_address`` and ``timeout`` + """ +- def __init__(self, debuglevel=3D0, context=3DNone, source_address=3DNon= e, +- timeout=3D10): ++ ++ def __init__(self, debuglevel=3D0, context=3DNone, source_address=3DNon= e, timeout=3D10): + AbstractHTTPHandler.__init__(self, debuglevel) + self._context =3D context + self.source_address =3D source_address +@@ -574,7 +587,7 @@ + self.timeout, + context=3Dself._context, + ), +- req ++ req, + ) +=20 + https_request =3D AbstractHTTPHandler.do_request_ +@@ -586,29 +599,25 @@ + ``source_address`` for binding, ``timeout`` and our custom + `User-Agent` + """ +- +- printer('Timeout set to %d' % timeout, debug=3DTrue) ++ printer(f"Timeout set to {timeout}", debug=3DTrue) +=20 + if source_address: + source_address_tuple =3D (source_address, 0) +- printer('Binding to source address: %r' % (source_address_tuple,), +- debug=3DTrue) ++ printer(f"Binding to source address: {source_address_tuple!r}", deb= ug=3DTrue) + else: + source_address_tuple =3D None +=20 + handlers =3D [ + ProxyHandler(), +- SpeedtestHTTPHandler(source_address=3Dsource_address_tuple, +- timeout=3Dtimeout), +- SpeedtestHTTPSHandler(source_address=3Dsource_address_tuple, +- timeout=3Dtimeout), ++ SpeedtestHTTPHandler(source_address=3Dsource_address_tuple, timeout= =3Dtimeout), ++ SpeedtestHTTPSHandler(source_address=3Dsource_address_tuple, timeou= t=3Dtimeout), + HTTPDefaultErrorHandler(), + HTTPRedirectHandler(), +- HTTPErrorProcessor() ++ HTTPErrorProcessor(), + ] +=20 + opener =3D OpenerDirector() +- opener.addheaders =3D [('User-agent', build_user_agent())] ++ opener.addheaders =3D [("User-agent", build_user_agent())] +=20 + for handler in handlers: + opener.add_handler(handler) +@@ -623,12 +632,15 @@ + Largely copied from ``xmlrpclib``/``xmlrpc.client`` and modified + to work for py2.4-py3 + """ ++ + def __init__(self, response): + # response doesn't support tell() and read(), required by + # GzipFile + if not gzip: +- raise SpeedtestHTTPError('HTTP response body is gzip encoded, ' +- 'but gzip support is not available') ++ raise SpeedtestHTTPError( ++ "HTTP response body is gzip encoded, " ++ "but gzip support is not available", ++ ) + IO =3D BytesIO or StringIO + self.io =3D IO() + while 1: +@@ -637,7 +649,7 @@ + break + self.io.write(chunk) + self.io.seek(0) +- gzip.GzipFile.__init__(self, mode=3D'rb', fileobj=3Dself.io) ++ gzip.GzipFile.__init__(self, mode=3D"rb", fileobj=3Dself.io) +=20 + def close(self): + try: +@@ -655,17 +667,15 @@ +=20 + def distance(origin, destination): + """Determine distance between 2 sets of [lat,lon] in km""" +- + lat1, lon1 =3D origin + lat2, lon2 =3D destination + radius =3D 6371 # km +=20 + dlat =3D math.radians(lat2 - lat1) + dlon =3D math.radians(lon2 - lon1) +- a =3D (math.sin(dlat / 2) * math.sin(dlat / 2) + +- math.cos(math.radians(lat1)) * +- math.cos(math.radians(lat2)) * math.sin(dlon / 2) * +- math.sin(dlon / 2)) ++ a =3D math.sin(dlat / 2) * math.sin(dlat / 2) + math.cos( ++ math.radians(lat1), ++ ) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) * math.sin(dlon /= 2) + c =3D 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + d =3D radius * c +=20 +@@ -674,52 +684,47 @@ +=20 + def build_user_agent(): + """Build a Mozilla/5.0 compatible User-Agent string""" +- + ua_tuple =3D ( +- 'Mozilla/5.0', +- '(%s; U; %s; en-us)' % (platform.platform(), +- platform.architecture()[0]), +- 'Python/%s' % platform.python_version(), +- '(KHTML, like Gecko)', +- 'speedtest-cli/%s' % __version__ ++ "Mozilla/5.0", ++ f"({platform.platform()}; U; {platform.architecture()[0]}; en-us)", ++ f"Python/{platform.python_version()}", ++ "(KHTML, like Gecko)", ++ f"speedtest-cli/{__version__}", + ) +- user_agent =3D ' '.join(ua_tuple) +- printer('User-Agent: %s' % user_agent, debug=3DTrue) ++ user_agent =3D " ".join(ua_tuple) ++ printer(f"User-Agent: {user_agent}", debug=3DTrue) + return user_agent +=20 +=20 +-def build_request(url, data=3DNone, headers=3DNone, bump=3D'0', secure=3DFa= lse): ++def build_request(url, data=3DNone, headers=3DNone, bump=3D"0", secure=3DFa= lse): + """Build a urllib2 request object +=20 + This function automatically adds a User-Agent header to all requests +- + """ +- + if not headers: + headers =3D {} +=20 +- if url[0] =3D=3D ':': +- scheme =3D ('http', 'https')[bool(secure)] +- schemed_url =3D '%s%s' % (scheme, url) ++ if url[0] =3D=3D ":": ++ scheme =3D ("http", "https")[bool(secure)] ++ schemed_url =3D f"{scheme}{url}" + else: + schemed_url =3D url +=20 +- if '?' in url: +- delim =3D '&' ++ if "?" in url: ++ delim =3D "&" + else: +- delim =3D '?' ++ delim =3D "?" +=20 + # WHO YOU GONNA CALL? CACHE BUSTERS! +- final_url =3D '%s%sx=3D%s.%s' % (schemed_url, delim, +- int(timeit.time.time() * 1000), +- bump) +- +- headers.update({ +- 'Cache-Control': 'no-cache', +- }) ++ final_url =3D f"{schemed_url}{delim}x=3D{int(timeit.time.time() * 1000)= }.{bump}" ++ ++ headers.update( ++ { ++ "Cache-Control": "no-cache", ++ }, ++ ) +=20 +- printer('%s %s' % (('GET', 'POST')[bool(data)], final_url), +- debug=3DTrue) ++ printer(f"{('GET', 'POST')[bool(data)]} {final_url}", debug=3DTrue) +=20 + return Request(final_url, data=3Ddata, headers=3Dheaders) +=20 +@@ -729,7 +734,6 @@ + establishing a connection with a HTTP/HTTPS request +=20 + """ +- + if opener: + _open =3D opener.open + else: +@@ -738,7 +742,7 @@ + try: + uh =3D _open(request) + if request.get_full_url() !=3D uh.geturl(): +- printer('Redirected to %s' % uh.geturl(), debug=3DTrue) ++ printer(f"Redirected to {uh.geturl()}", debug=3DTrue) + return uh, False + except HTTP_ERRORS: + e =3D get_exception() +@@ -750,13 +754,12 @@ + ``Content-Encoding`` is ``gzip`` otherwise the response itself +=20 + """ +- + try: + getheader =3D response.headers.getheader + except AttributeError: + getheader =3D response.getheader +=20 +- if getheader('content-encoding') =3D=3D 'gzip': ++ if getheader("content-encoding") =3D=3D "gzip": + return GzipDecodedResponse(response) +=20 + return response +@@ -777,14 +780,16 @@ + """Built in callback function used by Thread classes for printing + status + """ ++ + def inner(current, total, start=3DFalse, end=3DFalse): + if event_is_set(shutdown_event): + return +=20 +- sys.stdout.write('.') ++ sys.stdout.write(".") + if current + 1 =3D=3D total and end is True: +- sys.stdout.write('\n') ++ sys.stdout.write("\n") + sys.stdout.flush() ++ + return inner +=20 +=20 +@@ -795,8 +800,7 @@ + class HTTPDownloader(threading.Thread): + """Thread class for retrieving a URL""" +=20 +- def __init__(self, i, request, start, timeout, opener=3DNone, +- shutdown_event=3DNone): ++ def __init__(self, i, request, start, timeout, opener=3DNone, shutdown_= event=3DNone): + threading.Thread.__init__(self) + self.request =3D request + self.result =3D [0] +@@ -817,9 +821,10 @@ + try: + if (timeit.default_timer() - self.starttime) <=3D self.timeout: + f =3D self._opener(self.request) +- while (not event_is_set(self._shutdown_event) and +- (timeit.default_timer() - self.starttime) <=3D +- self.timeout): ++ while ( ++ not event_is_set(self._shutdown_event) ++ and (timeit.default_timer() - self.starttime) <=3D self= .timeout ++ ): + self.result.append(len(f.read(10240))) + if self.result[-1] =3D=3D 0: + break +@@ -830,7 +835,7 @@ + pass +=20 +=20 +-class HTTPUploaderData(object): ++class HTTPUploaderData: + """File like object to improve cutting off the upload once the timeout + has been reached + """ +@@ -850,19 +855,17 @@ + self.total =3D [0] +=20 + def pre_allocate(self): +- chars =3D '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' ++ chars =3D "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + multiplier =3D int(round(int(self.length) / 36.0)) + IO =3D BytesIO or StringIO + try: + self._data =3D IO( +- ('content1=3D%s' % +- (chars * multiplier)[0:int(self.length) - 9] +- ).encode() ++ (f"content1=3D{(chars * multiplier)[0:int(self.length) - 9]= }").encode(), + ) + except MemoryError: + raise SpeedtestCLIError( +- 'Insufficient memory to pre-allocate upload data. Please ' +- 'use --no-pre-allocate' ++ "Insufficient memory to pre-allocate upload data. Please " ++ "use --no-pre-allocate", + ) +=20 + @property +@@ -872,13 +875,13 @@ + return self._data +=20 + def read(self, n=3D10240): +- if ((timeit.default_timer() - self.start) <=3D self.timeout and +- not event_is_set(self._shutdown_event)): ++ if (timeit.default_timer() - self.start) <=3D self.timeout and not = event_is_set( ++ self._shutdown_event, ++ ): + chunk =3D self.data.read(n) + self.total.append(len(chunk)) + return chunk +- else: +- raise SpeedtestUploadTimeout() ++ raise SpeedtestUploadTimeout() +=20 + def __len__(self): + return self.length +@@ -887,8 +890,16 @@ + class HTTPUploader(threading.Thread): + """Thread class for putting a URL""" +=20 +- def __init__(self, i, request, start, size, timeout, opener=3DNone, +- shutdown_event=3DNone): ++ def __init__( ++ self, ++ i, ++ request, ++ start, ++ size, ++ timeout, ++ opener=3DNone, ++ shutdown_event=3DNone, ++ ): + threading.Thread.__init__(self) + self.request =3D request + self.request.data.start =3D self.starttime =3D start +@@ -910,16 +921,19 @@ + def run(self): + request =3D self.request + try: +- if ((timeit.default_timer() - self.starttime) <=3D self.timeout= and +- not event_is_set(self._shutdown_event)): ++ if ( ++ timeit.default_timer() - self.starttime ++ ) <=3D self.timeout and not event_is_set(self._shutdown_event): + try: + f =3D self._opener(request) + except TypeError: + # PY24 expects a string or buffer + # This also causes issues with Ctrl-C, but we will conc= ede + # for the moment that Ctrl-C on PY24 isn't immediate +- request =3D build_request(self.request.get_full_url(), +- data=3Drequest.data.read(self.s= ize)) ++ request =3D build_request( ++ self.request.get_full_url(), ++ data=3Drequest.data.read(self.size), ++ ) + f =3D self._opener(request) + f.read(11) + f.close() +@@ -932,7 +946,7 @@ + self.result =3D 0 +=20 +=20 +-class SpeedtestResults(object): ++class SpeedtestResults: + """Class for holding the results of a speedtest, including: +=20 + Download speed +@@ -945,8 +959,16 @@ + to get a share results image link. + """ +=20 +- def __init__(self, download=3D0, upload=3D0, ping=3D0, server=3DNone, c= lient=3DNone, +- opener=3DNone, secure=3DFalse): ++ def __init__( ++ self, ++ download=3D0, ++ upload=3D0, ++ ping=3D0, ++ server=3DNone, ++ client=3DNone, ++ opener=3DNone, ++ secure=3DFalse, ++ ): + self.download =3D download + self.upload =3D upload + self.ping =3D ping +@@ -957,7 +979,7 @@ + self.client =3D client or {} +=20 + self._share =3D None +- self.timestamp =3D '%sZ' % datetime.datetime.utcnow().isoformat() ++ self.timestamp =3D f"{datetime.datetime.utcnow().isoformat()}Z" + self.bytes_received =3D 0 + self.bytes_sent =3D 0 +=20 +@@ -975,7 +997,6 @@ + """POST data to the speedtest.net API to obtain a share results + link + """ +- + if self._share: + return self._share +=20 +@@ -987,29 +1008,33 @@ + # We use a list instead of a dict because the API expects parameters + # in a certain order + api_data =3D [ +- 'recommendedserverid=3D%s' % self.server['id'], +- 'ping=3D%s' % ping, +- 'screenresolution=3D', +- 'promo=3D', +- 'download=3D%s' % download, +- 'screendpi=3D', +- 'upload=3D%s' % upload, +- 'testmethod=3Dhttp', +- 'hash=3D%s' % md5(('%s-%s-%s-%s' % +- (ping, upload, download, '297aae72')) +- .encode()).hexdigest(), +- 'touchscreen=3Dnone', +- 'startmode=3Dpingselect', +- 'accuracy=3D1', +- 'bytesreceived=3D%s' % self.bytes_received, +- 'bytessent=3D%s' % self.bytes_sent, +- 'serverid=3D%s' % self.server['id'], ++ f"recommendedserverid=3D{self.server['id']}", ++ f"ping=3D{ping}", ++ "screenresolution=3D", ++ "promo=3D", ++ f"download=3D{download}", ++ "screendpi=3D", ++ f"upload=3D{upload}", ++ "testmethod=3Dhttp", ++ "hash=3D%s" ++ % md5( ++ ("%s-%s-%s-%s" % (ping, upload, download, "297aae72")).enco= de(), ++ ).hexdigest(), ++ "touchscreen=3Dnone", ++ "startmode=3Dpingselect", ++ "accuracy=3D1", ++ f"bytesreceived=3D{self.bytes_received}", ++ f"bytessent=3D{self.bytes_sent}", ++ f"serverid=3D{self.server['id']}", + ] +=20 +- headers =3D {'Referer': 'http://c.speedtest.net/flash/speedtest.swf= '} +- request =3D build_request('://www.speedtest.net/api/api.php', +- data=3D'&'.join(api_data).encode(), +- headers=3Dheaders, secure=3Dself._secure) ++ headers =3D {"Referer": "http://c.speedtest.net/flash/speedtest.swf= "} ++ request =3D build_request( ++ "://www.speedtest.net/api/api.php", ++ data=3D"&".join(api_data).encode(), ++ headers=3Dheaders, ++ secure=3Dself._secure, ++ ) + f, e =3D catch_request(request, opener=3Dself._opener) + if e: + raise ShareResultsConnectFailure(e) +@@ -1019,75 +1044,94 @@ + f.close() +=20 + if int(code) !=3D 200: +- raise ShareResultsSubmitFailure('Could not submit results to ' +- 'speedtest.net') ++ raise ShareResultsSubmitFailure( ++ "Could not submit results to " "speedtest.net", ++ ) +=20 + qsargs =3D parse_qs(response.decode()) +- resultid =3D qsargs.get('resultid') ++ resultid =3D qsargs.get("resultid") + if not resultid or len(resultid) !=3D 1: +- raise ShareResultsSubmitFailure('Could not submit results to ' +- 'speedtest.net') ++ raise ShareResultsSubmitFailure( ++ "Could not submit results to " "speedtest.net", ++ ) +=20 +- self._share =3D 'http://www.speedtest.net/result/%s.png' % resultid= [0] ++ self._share =3D f"http://www.speedtest.net/result/{resultid[0]}.png" +=20 + return self._share +=20 + def dict(self): + """Return dictionary of result data""" +- + return { +- 'download': self.download, +- 'upload': self.upload, +- 'ping': self.ping, +- 'server': self.server, +- 'timestamp': self.timestamp, +- 'bytes_sent': self.bytes_sent, +- 'bytes_received': self.bytes_received, +- 'share': self._share, +- 'client': self.client, ++ "download": self.download, ++ "upload": self.upload, ++ "ping": self.ping, ++ "server": self.server, ++ "timestamp": self.timestamp, ++ "bytes_sent": self.bytes_sent, ++ "bytes_received": self.bytes_received, ++ "share": self._share, ++ "client": self.client, + } +=20 + @staticmethod +- def csv_header(delimiter=3D','): ++ def csv_header(delimiter=3D","): + """Return CSV Headers""" +- +- row =3D ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Dista= nce', +- 'Ping', 'Download', 'Upload', 'Share', 'IP Address'] ++ row =3D [ ++ "Server ID", ++ "Sponsor", ++ "Server Name", ++ "Timestamp", ++ "Distance", ++ "Ping", ++ "Download", ++ "Upload", ++ "Share", ++ "IP Address", ++ ] + out =3D StringIO() +- writer =3D csv.writer(out, delimiter=3Ddelimiter, lineterminator=3D= '') ++ writer =3D csv.writer(out, delimiter=3Ddelimiter, lineterminator=3D= "") + writer.writerow([to_utf8(v) for v in row]) + return out.getvalue() +=20 +- def csv(self, delimiter=3D','): ++ def csv(self, delimiter=3D","): + """Return data in CSV format""" +- + data =3D self.dict() + out =3D StringIO() +- writer =3D csv.writer(out, delimiter=3Ddelimiter, lineterminator=3D= '') +- row =3D [data['server']['id'], data['server']['sponsor'], +- data['server']['name'], data['timestamp'], +- data['server']['d'], data['ping'], data['download'], +- data['upload'], self._share or '', self.client['ip']] ++ writer =3D csv.writer(out, delimiter=3Ddelimiter, lineterminator=3D= "") ++ row =3D [ ++ data["server"]["id"], ++ data["server"]["sponsor"], ++ data["server"]["name"], ++ data["timestamp"], ++ data["server"]["d"], ++ data["ping"], ++ data["download"], ++ data["upload"], ++ self._share or "", ++ self.client["ip"], ++ ] + writer.writerow([to_utf8(v) for v in row]) + return out.getvalue() +=20 + def json(self, pretty=3DFalse): + """Return data in JSON format""" +- + kwargs =3D {} + if pretty: +- kwargs.update({ +- 'indent': 4, +- 'sort_keys': True +- }) ++ kwargs.update({"indent": 4, "sort_keys": True}) + return json.dumps(self.dict(), **kwargs) +=20 +=20 +-class Speedtest(object): ++class Speedtest: + """Class for performing standard speedtest.net testing operations""" +=20 +- def __init__(self, config=3DNone, source_address=3DNone, timeout=3D10, +- secure=3DFalse, shutdown_event=3DNone): ++ def __init__( ++ self, ++ config=3DNone, ++ source_address=3DNone, ++ timeout=3D10, ++ secure=3DFalse, ++ shutdown_event=3DNone, ++ ): + self.config =3D {} +=20 + self._source_address =3D source_address +@@ -1110,7 +1154,7 @@ + self._best =3D {} +=20 + self.results =3D SpeedtestResults( +- client=3Dself.config['client'], ++ client=3Dself.config["client"], + opener=3Dself._opener, + secure=3Dsecure, + ) +@@ -1125,12 +1169,14 @@ + """Download the speedtest.net configuration and return only the data + we are interested in + """ +- + headers =3D {} + if gzip: +- headers['Accept-Encoding'] =3D 'gzip' +- request =3D build_request('://www.speedtest.net/speedtest-config.ph= p', +- headers=3Dheaders, secure=3Dself._secure) ++ headers["Accept-Encoding"] =3D "gzip" ++ request =3D build_request( ++ "://www.speedtest.net/speedtest-config.php", ++ headers=3Dheaders, ++ secure=3Dself._secure, ++ ) + uh, e =3D catch_request(request, opener=3Dself._opener) + if e: + raise ConfigRetrievalError(e) +@@ -1151,9 +1197,9 @@ + if int(uh.code) !=3D 200: + return None +=20 +- configxml =3D ''.encode().join(configxml_list) ++ configxml =3D "".encode().join(configxml_list) +=20 +- printer('Config XML:\n%s' % configxml, debug=3DTrue) ++ printer(f"Config XML:\n{configxml}", debug=3DTrue) +=20 + try: + try: +@@ -1161,13 +1207,13 @@ + except ET.ParseError: + e =3D get_exception() + raise SpeedtestConfigError( +- 'Malformed speedtest.net configuration: %s' % e ++ f"Malformed speedtest.net configuration: {e}", + ) +- server_config =3D root.find('server-config').attrib +- download =3D root.find('download').attrib +- upload =3D root.find('upload').attrib ++ server_config =3D root.find("server-config").attrib ++ download =3D root.find("download").attrib ++ upload =3D root.find("upload").attrib + # times =3D root.find('times').attrib +- client =3D root.find('client').attrib ++ client =3D root.find("client").attrib +=20 + except AttributeError: + try: +@@ -1175,65 +1221,61 @@ + except ExpatError: + e =3D get_exception() + raise SpeedtestConfigError( +- 'Malformed speedtest.net configuration: %s' % e ++ f"Malformed speedtest.net configuration: {e}", + ) +- server_config =3D get_attributes_by_tag_name(root, 'server-conf= ig') +- download =3D get_attributes_by_tag_name(root, 'download') +- upload =3D get_attributes_by_tag_name(root, 'upload') ++ server_config =3D get_attributes_by_tag_name(root, "server-conf= ig") ++ download =3D get_attributes_by_tag_name(root, "download") ++ upload =3D get_attributes_by_tag_name(root, "upload") + # times =3D get_attributes_by_tag_name(root, 'times') +- client =3D get_attributes_by_tag_name(root, 'client') ++ client =3D get_attributes_by_tag_name(root, "client") +=20 +- ignore_servers =3D [ +- int(i) for i in server_config['ignoreids'].split(',') if i +- ] ++ ignore_servers =3D [int(i) for i in server_config["ignoreids"].spli= t(",") if i] +=20 +- ratio =3D int(upload['ratio']) +- upload_max =3D int(upload['maxchunkcount']) ++ ratio =3D int(upload["ratio"]) ++ upload_max =3D int(upload["maxchunkcount"]) + up_sizes =3D [32768, 65536, 131072, 262144, 524288, 1048576, 734003= 2] + sizes =3D { +- 'upload': up_sizes[ratio - 1:], +- 'download': [350, 500, 750, 1000, 1500, 2000, 2500, +- 3000, 3500, 4000] ++ "upload": up_sizes[ratio - 1 :], ++ "download": [350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500,= 4000], + } +=20 +- size_count =3D len(sizes['upload']) ++ size_count =3D len(sizes["upload"]) +=20 + upload_count =3D int(math.ceil(upload_max / size_count)) +=20 +- counts =3D { +- 'upload': upload_count, +- 'download': int(download['threadsperurl']) +- } ++ counts =3D {"upload": upload_count, "download": int(download["threa= dsperurl"])} +=20 + threads =3D { +- 'upload': int(upload['threads']), +- 'download': int(server_config['threadcount']) * 2 ++ "upload": int(upload["threads"]), ++ "download": int(server_config["threadcount"]) * 2, + } +=20 + length =3D { +- 'upload': int(upload['testlength']), +- 'download': int(download['testlength']) ++ "upload": int(upload["testlength"]), ++ "download": int(download["testlength"]), + } +=20 +- self.config.update({ +- 'client': client, +- 'ignore_servers': ignore_servers, +- 'sizes': sizes, +- 'counts': counts, +- 'threads': threads, +- 'length': length, +- 'upload_max': upload_count * size_count +- }) ++ self.config.update( ++ { ++ "client": client, ++ "ignore_servers": ignore_servers, ++ "sizes": sizes, ++ "counts": counts, ++ "threads": threads, ++ "length": length, ++ "upload_max": upload_count * size_count, ++ }, ++ ) +=20 + try: +- self.lat_lon =3D (float(client['lat']), float(client['lon'])) ++ self.lat_lon =3D (float(client["lat"]), float(client["lon"])) + except ValueError: + raise SpeedtestConfigError( +- 'Unknown location: lat=3D%r lon=3D%r' % +- (client.get('lat'), client.get('lon')) ++ "Unknown location: lat=3D%r lon=3D%r" ++ % (client.get("lat"), client.get("lon")), + ) +=20 +- printer('Config:\n%r' % self.config, debug=3DTrue) ++ printer(f"Config:\n{self.config!r}", debug=3DTrue) +=20 + return self.config +=20 +@@ -1255,32 +1297,31 @@ + server_list[i] =3D int(s) + except ValueError: + raise InvalidServerIDType( +- '%s is an invalid server type, must be int' % s ++ f"{s} is an invalid server type, must be int", + ) +=20 + urls =3D [ +- '://www.speedtest.net/speedtest-servers-static.php', +- 'http://c.speedtest.net/speedtest-servers-static.php', +- '://www.speedtest.net/speedtest-servers.php', +- 'http://c.speedtest.net/speedtest-servers.php', ++ "://www.speedtest.net/speedtest-servers-static.php", ++ "http://c.speedtest.net/speedtest-servers-static.php", ++ "://www.speedtest.net/speedtest-servers.php", ++ "http://c.speedtest.net/speedtest-servers.php", + ] +=20 + headers =3D {} + if gzip: +- headers['Accept-Encoding'] =3D 'gzip' ++ headers["Accept-Encoding"] =3D "gzip" +=20 + errors =3D [] + for url in urls: + try: + request =3D build_request( +- '%s?threads=3D%s' % (url, +- self.config['threads']['download']), ++ f"{url}?threads=3D{self.config['threads']['download']}", + headers=3Dheaders, +- secure=3Dself._secure ++ secure=3Dself._secure, + ) + uh, e =3D catch_request(request, opener=3Dself._opener) + if e: +- errors.append('%s' % e) ++ errors.append(f"{e}") + raise ServersRetrievalError() +=20 + stream =3D get_response_stream(uh) +@@ -1300,9 +1341,9 @@ + if int(uh.code) !=3D 200: + raise ServersRetrievalError() +=20 +- serversxml =3D ''.encode().join(serversxml_list) ++ serversxml =3D "".encode().join(serversxml_list) +=20 +- printer('Servers XML:\n%s' % serversxml, debug=3DTrue) ++ printer(f"Servers XML:\n{serversxml}", debug=3DTrue) +=20 + try: + try: +@@ -1311,18 +1352,18 @@ + except ET.ParseError: + e =3D get_exception() + raise SpeedtestServersError( +- 'Malformed speedtest.net server list: %s' %= e ++ f"Malformed speedtest.net server list: {e}", + ) +- elements =3D etree_iter(root, 'server') ++ elements =3D etree_iter(root, "server") + except AttributeError: + try: + root =3D DOM.parseString(serversxml) + except ExpatError: + e =3D get_exception() + raise SpeedtestServersError( +- 'Malformed speedtest.net server list: %s' %= e ++ f"Malformed speedtest.net server list: {e}", + ) +- elements =3D root.getElementsByTagName('server') ++ elements =3D root.getElementsByTagName("server") + except (SyntaxError, xml.parsers.expat.ExpatError): + raise ServersRetrievalError() +=20 +@@ -1332,21 +1373,24 @@ + except AttributeError: + attrib =3D dict(list(server.attributes.items())) +=20 +- if servers and int(attrib.get('id')) not in servers: ++ if servers and int(attrib.get("id")) not in servers: + continue +=20 +- if (int(attrib.get('id')) in self.config['ignore_server= s'] +- or int(attrib.get('id')) in exclude): ++ if ( ++ int(attrib.get("id")) in self.config["ignore_server= s"] ++ or int(attrib.get("id")) in exclude ++ ): + continue +=20 + try: +- d =3D distance(self.lat_lon, +- (float(attrib.get('lat')), +- float(attrib.get('lon')))) ++ d =3D distance( ++ self.lat_lon, ++ (float(attrib.get("lat")), float(attrib.get("lo= n"))), ++ ) + except Exception: + continue +=20 +- attrib['d'] =3D d ++ attrib["d"] =3D d +=20 + try: + self.servers[d].append(attrib) +@@ -1367,7 +1411,6 @@ + """Instead of querying for a list of servers, set a link to a + speedtest mini server + """ +- + urlparts =3D urlparse(server) +=20 + name, ext =3D os.path.splitext(urlparts[2]) +@@ -1379,41 +1422,41 @@ + request =3D build_request(url) + uh, e =3D catch_request(request, opener=3Dself._opener) + if e: +- raise SpeedtestMiniConnectFailure('Failed to connect to %s' % +- server) +- else: +- text =3D uh.read() +- uh.close() ++ raise SpeedtestMiniConnectFailure(f"Failed to connect to {serve= r}") ++ text =3D uh.read() ++ uh.close() +=20 +- extension =3D re.findall('upload_?[Ee]xtension: "([^"]+)"', +- text.decode()) ++ extension =3D re.findall('upload_?[Ee]xtension: "([^"]+)"', text.de= code()) + if not extension: +- for ext in ['php', 'asp', 'aspx', 'jsp']: ++ for ext in ["php", "asp", "aspx", "jsp"]: + try: +- f =3D self._opener.open( +- '%s/speedtest/upload.%s' % (url, ext) +- ) ++ f =3D self._opener.open(f"{url}/speedtest/upload.{ext}") + except Exception: + pass + else: + data =3D f.read().strip().decode() +- if (f.code =3D=3D 200 and +- len(data.splitlines()) =3D=3D 1 and +- re.match('size=3D[0-9]', data)): ++ if ( ++ f.code =3D=3D 200 ++ and len(data.splitlines()) =3D=3D 1 ++ and re.match("size=3D[0-9]", data) ++ ): + extension =3D [ext] + break + if not urlparts or not extension: +- raise InvalidSpeedtestMiniServer('Invalid Speedtest Mini Server= : ' +- '%s' % server) ++ raise InvalidSpeedtestMiniServer( ++ "Invalid Speedtest Mini Server: " "%s" % server, ++ ) +=20 +- self.servers =3D [{ +- 'sponsor': 'Speedtest Mini', +- 'name': urlparts[1], +- 'd': 0, +- 'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0= ]), +- 'latency': 0, +- 'id': 0 +- }] ++ self.servers =3D [ ++ { ++ "sponsor": "Speedtest Mini", ++ "name": urlparts[1], ++ "d": 0, ++ "url": f"{url.rstrip('/')}/speedtest/upload.{extension[0]}", ++ "latency": 0, ++ "id": 0, ++ }, ++ ] +=20 + return self.servers +=20 +@@ -1421,7 +1464,6 @@ + """Limit servers to the closest speedtest.net servers based on + geographic distance + """ +- + if not self.servers: + self.get_servers() +=20 +@@ -1434,14 +1476,13 @@ + continue + break +=20 +- printer('Closest Servers:\n%r' % self.closest, debug=3DTrue) ++ printer(f"Closest Servers:\n{self.closest!r}", debug=3DTrue) + return self.closest +=20 + def get_best_server(self, servers=3DNone): + """Perform a speedtest.net "ping" to determine which speedtest.net + server has the lowest latency + """ +- + if not servers: + if not self.closest: + servers =3D self.get_closest_servers() +@@ -1457,39 +1498,38 @@ + results =3D {} + for server in servers: + cum =3D [] +- url =3D os.path.dirname(server['url']) ++ url =3D os.path.dirname(server["url"]) + stamp =3D int(timeit.time.time() * 1000) +- latency_url =3D '%s/latency.txt?x=3D%s' % (url, stamp) ++ latency_url =3D f"{url}/latency.txt?x=3D{stamp}" + for i in range(0, 3): +- this_latency_url =3D '%s.%s' % (latency_url, i) +- printer('%s %s' % ('GET', this_latency_url), +- debug=3DTrue) ++ this_latency_url =3D f"{latency_url}.{i}" ++ printer(f"{'GET'} {this_latency_url}", debug=3DTrue) + urlparts =3D urlparse(latency_url) + try: +- if urlparts[0] =3D=3D 'https': ++ if urlparts[0] =3D=3D "https": + h =3D SpeedtestHTTPSConnection( + urlparts[1], +- source_address=3Dsource_address_tuple ++ source_address=3Dsource_address_tuple, + ) + else: + h =3D SpeedtestHTTPConnection( + urlparts[1], +- source_address=3Dsource_address_tuple ++ source_address=3Dsource_address_tuple, + ) +- headers =3D {'User-Agent': user_agent} +- path =3D '%s?%s' % (urlparts[2], urlparts[4]) ++ headers =3D {"User-Agent": user_agent} ++ path =3D f"{urlparts[2]}?{urlparts[4]}" + start =3D timeit.default_timer() + h.request("GET", path, headers=3Dheaders) + r =3D h.getresponse() +- total =3D (timeit.default_timer() - start) ++ total =3D timeit.default_timer() - start + except HTTP_ERRORS: + e =3D get_exception() +- printer('ERROR: %r' % e, debug=3DTrue) ++ printer(f"ERROR: {e!r}", debug=3DTrue) + cum.append(3600) + continue +=20 + text =3D r.read(9) +- if int(r.status) =3D=3D 200 and text =3D=3D 'test=3Dtest'.e= ncode(): ++ if int(r.status) =3D=3D 200 and text =3D=3D "test=3Dtest".e= ncode(): + cum.append(total) + else: + cum.append(3600) +@@ -1501,16 +1541,17 @@ + try: + fastest =3D sorted(results.keys())[0] + except IndexError: +- raise SpeedtestBestServerFailure('Unable to connect to servers = to ' +- 'test latency.') ++ raise SpeedtestBestServerFailure( ++ "Unable to connect to servers to " "test latency.", ++ ) + best =3D results[fastest] +- best['latency'] =3D fastest ++ best["latency"] =3D fastest +=20 + self.results.ping =3D fastest + self.results.server =3D best +=20 + self._best.update(best) +- printer('Best Server:\n%r' % best, debug=3DTrue) ++ printer(f"Best Server:\n{best!r}", debug=3DTrue) + return best +=20 + def download(self, callback=3Ddo_nothing, threads=3DNone): +@@ -1519,22 +1560,21 @@ + A ``threads`` value of ``None`` will fall back to those dictated + by the speedtest.net configuration + """ +- + urls =3D [] +- for size in self.config['sizes']['download']: +- for _ in range(0, self.config['counts']['download']): +- urls.append('%s/random%sx%s.jpg' % +- (os.path.dirname(self.best['url']), size, size)) ++ for size in self.config["sizes"]["download"]: ++ for _ in range(0, self.config["counts"]["download"]): ++ urls.append( ++ "%s/random%sx%s.jpg" ++ % (os.path.dirname(self.best["url"]), size, size), ++ ) +=20 + request_count =3D len(urls) + requests =3D [] + for i, url in enumerate(urls): +- requests.append( +- build_request(url, bump=3Di, secure=3Dself._secure) +- ) ++ requests.append(build_request(url, bump=3Di, secure=3Dself._sec= ure)) +=20 +- max_threads =3D threads or self.config['threads']['download'] +- in_flight =3D {'threads': 0} ++ max_threads =3D threads or self.config["threads"]["download"] ++ in_flight =3D {"threads": 0} +=20 + def producer(q, requests, request_count): + for i, request in enumerate(requests): +@@ -1542,15 +1582,15 @@ + i, + request, + start, +- self.config['length']['download'], ++ self.config["length"]["download"], + opener=3Dself._opener, +- shutdown_event=3Dself._shutdown_event ++ shutdown_event=3Dself._shutdown_event, + ) +- while in_flight['threads'] >=3D max_threads: ++ while in_flight["threads"] >=3D max_threads: + timeit.time.sleep(0.001) + thread.start() + q.put(thread, True) +- in_flight['threads'] +=3D 1 ++ in_flight["threads"] +=3D 1 + callback(i, request_count, start=3DTrue) +=20 + finished =3D [] +@@ -1561,15 +1601,16 @@ + thread =3D q.get(True) + while _is_alive(thread): + thread.join(timeout=3D0.001) +- in_flight['threads'] -=3D 1 ++ in_flight["threads"] -=3D 1 + finished.append(sum(thread.result)) + callback(thread.i, request_count, end=3DTrue) +=20 + q =3D Queue(max_threads) +- prod_thread =3D threading.Thread(target=3Dproducer, +- args=3D(q, requests, request_count)) +- cons_thread =3D threading.Thread(target=3Dconsumer, +- args=3D(q, request_count)) ++ prod_thread =3D threading.Thread( ++ target=3Dproducer, ++ args=3D(q, requests, request_count), ++ ) ++ cons_thread =3D threading.Thread(target=3Dconsumer, args=3D(q, requ= est_count)) + start =3D timeit.default_timer() + prod_thread.start() + cons_thread.start() +@@ -1581,11 +1622,9 @@ +=20 + stop =3D timeit.default_timer() + self.results.bytes_received =3D sum(finished) +- self.results.download =3D ( +- (self.results.bytes_received / (stop - start)) * 8.0 +- ) ++ self.results.download =3D (self.results.bytes_received / (stop - st= art)) * 8.0 + if self.results.download > 100000: +- self.config['threads']['upload'] =3D 8 ++ self.config["threads"]["upload"] =3D 8 + return self.results.download +=20 + def upload(self, callback=3Ddo_nothing, pre_allocate=3DTrue, threads=3D= None): +@@ -1594,40 +1633,43 @@ + A ``threads`` value of ``None`` will fall back to those dictated + by the speedtest.net configuration + """ +- + sizes =3D [] +=20 +- for size in self.config['sizes']['upload']: +- for _ in range(0, self.config['counts']['upload']): ++ for size in self.config["sizes"]["upload"]: ++ for _ in range(0, self.config["counts"]["upload"]): + sizes.append(size) +=20 + # request_count =3D len(sizes) +- request_count =3D self.config['upload_max'] ++ request_count =3D self.config["upload_max"] +=20 + requests =3D [] +- for i, size in enumerate(sizes): ++ for _, size in enumerate(sizes): + # We set ``0`` for ``start`` and handle setting the actual + # ``start`` in ``HTTPUploader`` to get better measurements + data =3D HTTPUploaderData( + size, + 0, +- self.config['length']['upload'], +- shutdown_event=3Dself._shutdown_event ++ self.config["length"]["upload"], ++ shutdown_event=3Dself._shutdown_event, + ) + if pre_allocate: + data.pre_allocate() +=20 +- headers =3D {'Content-length': size} ++ headers =3D {"Content-length": size} + requests.append( + ( +- build_request(self.best['url'], data, secure=3Dself._se= cure, +- headers=3Dheaders), +- size +- ) ++ build_request( ++ self.best["url"], ++ data, ++ secure=3Dself._secure, ++ headers=3Dheaders, ++ ), ++ size, ++ ), + ) +=20 +- max_threads =3D threads or self.config['threads']['upload'] +- in_flight =3D {'threads': 0} ++ max_threads =3D threads or self.config["threads"]["upload"] ++ in_flight =3D {"threads": 0} +=20 + def producer(q, requests, request_count): + for i, request in enumerate(requests[:request_count]): +@@ -1636,15 +1678,15 @@ + request[0], + start, + request[1], +- self.config['length']['upload'], ++ self.config["length"]["upload"], + opener=3Dself._opener, +- shutdown_event=3Dself._shutdown_event ++ shutdown_event=3Dself._shutdown_event, + ) +- while in_flight['threads'] >=3D max_threads: ++ while in_flight["threads"] >=3D max_threads: + timeit.time.sleep(0.001) + thread.start() + q.put(thread, True) +- in_flight['threads'] +=3D 1 ++ in_flight["threads"] +=3D 1 + callback(i, request_count, start=3DTrue) +=20 + finished =3D [] +@@ -1655,15 +1697,16 @@ + thread =3D q.get(True) + while _is_alive(thread): + thread.join(timeout=3D0.001) +- in_flight['threads'] -=3D 1 ++ in_flight["threads"] -=3D 1 + finished.append(thread.result) + callback(thread.i, request_count, end=3DTrue) +=20 +- q =3D Queue(threads or self.config['threads']['upload']) +- prod_thread =3D threading.Thread(target=3Dproducer, +- args=3D(q, requests, request_count)) +- cons_thread =3D threading.Thread(target=3Dconsumer, +- args=3D(q, request_count)) ++ q =3D Queue(threads or self.config["threads"]["upload"]) ++ prod_thread =3D threading.Thread( ++ target=3Dproducer, ++ args=3D(q, requests, request_count), ++ ) ++ cons_thread =3D threading.Thread(target=3Dconsumer, args=3D(q, requ= est_count)) + start =3D timeit.default_timer() + prod_thread.start() + cons_thread.start() +@@ -1675,9 +1718,7 @@ +=20 + stop =3D timeit.default_timer() + self.results.bytes_sent =3D sum(finished) +- self.results.upload =3D ( +- (self.results.bytes_sent / (stop - start)) * 8.0 +- ) ++ self.results.upload =3D (self.results.bytes_sent / (stop - start)) = * 8.0 + return self.results.upload +=20 +=20 +@@ -1685,24 +1726,24 @@ + """Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded + operations + """ ++ + def inner(signum, frame): + shutdown_event.set() +- printer('\nCancelling...', error=3DTrue) ++ printer("\nCancelling...", error=3DTrue) + sys.exit(0) ++ + return inner +=20 +=20 + def version(): + """Print the version""" +- +- printer('speedtest-cli %s' % __version__) +- printer('Python %s' % sys.version.replace('\n', '')) ++ printer(f"speedtest-cli {__version__}") ++ printer("Python %s" % sys.version.replace("\n", "")) + sys.exit(0) +=20 +=20 +-def csv_header(delimiter=3D','): ++def csv_header(delimiter=3D","): + """Print the CSV Headers""" +- + printer(SpeedtestResults.csv_header(delimiter=3Ddelimiter)) + sys.exit(0) +=20 +@@ -1710,11 +1751,12 @@ + def parse_args(): + """Function to handle building and parsing of command line arguments""" + description =3D ( +- 'Command line interface for testing internet bandwidth using ' +- 'speedtest.net.\n' +- '------------------------------------------------------------' +- '--------------\n' +- 'https://github.com/sivel/speedtest-cli') ++ "Command line interface for testing internet bandwidth using " ++ "speedtest.net.\n" ++ "------------------------------------------------------------" ++ "--------------\n" ++ "https://github.com/sivel/speedtest-cli" ++ ) +=20 + parser =3D ArgParser(description=3Ddescription) + # Give optparse.OptionParser an `add_argument` method for +@@ -1723,67 +1765,134 @@ + parser.add_argument =3D parser.add_option + except AttributeError: + pass +- parser.add_argument('--no-download', dest=3D'download', default=3DTrue, +- action=3D'store_const', const=3DFalse, +- help=3D'Do not perform download test') +- parser.add_argument('--no-upload', dest=3D'upload', default=3DTrue, +- action=3D'store_const', const=3DFalse, +- help=3D'Do not perform upload test') +- parser.add_argument('--single', default=3DFalse, action=3D'store_true', +- help=3D'Only use a single connection instead of ' +- 'multiple. This simulates a typical file ' +- 'transfer.') +- parser.add_argument('--bytes', dest=3D'units', action=3D'store_const', +- const=3D('byte', 8), default=3D('bit', 1), +- help=3D'Display values in bytes instead of bits. Do= es ' +- 'not affect the image generated by --share, no= r ' +- 'output from --json or --csv') +- parser.add_argument('--share', action=3D'store_true', +- help=3D'Generate and provide a URL to the speedtest= .net ' +- 'share results image, not displayed with --csv= ') +- parser.add_argument('--simple', action=3D'store_true', default=3DFalse, +- help=3D'Suppress verbose output, only show basic ' +- 'information') +- parser.add_argument('--csv', action=3D'store_true', default=3DFalse, +- help=3D'Suppress verbose output, only show basic ' +- 'information in CSV format. Speeds listed in ' +- 'bit/s and not affected by --bytes') +- parser.add_argument('--csv-delimiter', default=3D',', type=3DPARSER_TYP= E_STR, +- help=3D'Single character delimiter to use in CSV ' +- 'output. Default ","') +- parser.add_argument('--csv-header', action=3D'store_true', default=3DFa= lse, +- help=3D'Print CSV headers') +- parser.add_argument('--json', action=3D'store_true', default=3DFalse, +- help=3D'Suppress verbose output, only show basic ' +- 'information in JSON format. Speeds listed in ' +- 'bit/s and not affected by --bytes') +- parser.add_argument('--list', action=3D'store_true', +- help=3D'Display a list of speedtest.net servers ' +- 'sorted by distance') +- parser.add_argument('--server', type=3DPARSER_TYPE_INT, action=3D'appen= d', +- help=3D'Specify a server ID to test against. Can be= ' +- 'supplied multiple times') +- parser.add_argument('--exclude', type=3DPARSER_TYPE_INT, action=3D'appe= nd', +- help=3D'Exclude a server from selection. Can be ' +- 'supplied multiple times') +- parser.add_argument('--mini', help=3D'URL of the Speedtest Mini server') +- parser.add_argument('--source', help=3D'Source IP address to bind to') +- parser.add_argument('--timeout', default=3D10, type=3DPARSER_TYPE_FLOAT, +- help=3D'HTTP timeout in seconds. Default 10') +- parser.add_argument('--secure', action=3D'store_true', +- help=3D'Use HTTPS instead of HTTP when communicatin= g ' +- 'with speedtest.net operated servers') +- parser.add_argument('--no-pre-allocate', dest=3D'pre_allocate', +- action=3D'store_const', default=3DTrue, const=3DFal= se, +- help=3D'Do not pre allocate upload data. Pre alloca= tion ' +- 'is enabled by default to improve upload ' +- 'performance. To support systems with ' +- 'insufficient memory, use this option to avoid= a ' +- 'MemoryError') +- parser.add_argument('--version', action=3D'store_true', +- help=3D'Show the version number and exit') +- parser.add_argument('--debug', action=3D'store_true', +- help=3DARG_SUPPRESS, default=3DARG_SUPPRESS) ++ parser.add_argument( ++ "--no-download", ++ dest=3D"download", ++ default=3DTrue, ++ action=3D"store_const", ++ const=3DFalse, ++ help=3D"Do not perform download test", ++ ) ++ parser.add_argument( ++ "--no-upload", ++ dest=3D"upload", ++ default=3DTrue, ++ action=3D"store_const", ++ const=3DFalse, ++ help=3D"Do not perform upload test", ++ ) ++ parser.add_argument( ++ "--single", ++ default=3DFalse, ++ action=3D"store_true", ++ help=3D"Only use a single connection instead of " ++ "multiple. This simulates a typical file " ++ "transfer.", ++ ) ++ parser.add_argument( ++ "--bytes", ++ dest=3D"units", ++ action=3D"store_const", ++ const=3D("byte", 8), ++ default=3D("bit", 1), ++ help=3D"Display values in bytes instead of bits. Does " ++ "not affect the image generated by --share, nor " ++ "output from --json or --csv", ++ ) ++ parser.add_argument( ++ "--share", ++ action=3D"store_true", ++ help=3D"Generate and provide a URL to the speedtest.net " ++ "share results image, not displayed with --csv", ++ ) ++ parser.add_argument( ++ "--simple", ++ action=3D"store_true", ++ default=3DFalse, ++ help=3D"Suppress verbose output, only show basic " "information", ++ ) ++ parser.add_argument( ++ "--csv", ++ action=3D"store_true", ++ default=3DFalse, ++ help=3D"Suppress verbose output, only show basic " ++ "information in CSV format. Speeds listed in " ++ "bit/s and not affected by --bytes", ++ ) ++ parser.add_argument( ++ "--csv-delimiter", ++ default=3D",", ++ type=3DPARSER_TYPE_STR, ++ help=3D"Single character delimiter to use in CSV " 'output. Default= ","', ++ ) ++ parser.add_argument( ++ "--csv-header", ++ action=3D"store_true", ++ default=3DFalse, ++ help=3D"Print CSV headers", ++ ) ++ parser.add_argument( ++ "--json", ++ action=3D"store_true", ++ default=3DFalse, ++ help=3D"Suppress verbose output, only show basic " ++ "information in JSON format. Speeds listed in " ++ "bit/s and not affected by --bytes", ++ ) ++ parser.add_argument( ++ "--list", ++ action=3D"store_true", ++ help=3D"Display a list of speedtest.net servers " "sorted by distan= ce", ++ ) ++ parser.add_argument( ++ "--server", ++ type=3DPARSER_TYPE_INT, ++ action=3D"append", ++ help=3D"Specify a server ID to test against. Can be " "supplied mul= tiple times", ++ ) ++ parser.add_argument( ++ "--exclude", ++ type=3DPARSER_TYPE_INT, ++ action=3D"append", ++ help=3D"Exclude a server from selection. Can be " "supplied multipl= e times", ++ ) ++ parser.add_argument("--mini", help=3D"URL of the Speedtest Mini server") ++ parser.add_argument("--source", help=3D"Source IP address to bind to") ++ parser.add_argument( ++ "--timeout", ++ default=3D10, ++ type=3DPARSER_TYPE_FLOAT, ++ help=3D"HTTP timeout in seconds. Default 10", ++ ) ++ parser.add_argument( ++ "--secure", ++ action=3D"store_true", ++ help=3D"Use HTTPS instead of HTTP when communicating " ++ "with speedtest.net operated servers", ++ ) ++ parser.add_argument( ++ "--no-pre-allocate", ++ dest=3D"pre_allocate", ++ action=3D"store_const", ++ default=3DTrue, ++ const=3DFalse, ++ help=3D"Do not pre allocate upload data. Pre allocation " ++ "is enabled by default to improve upload " ++ "performance. To support systems with " ++ "insufficient memory, use this option to avoid a " ++ "MemoryError", ++ ) ++ parser.add_argument( ++ "--version", ++ action=3D"store_true", ++ help=3D"Show the version number and exit", ++ ) ++ parser.add_argument( ++ "--debug", ++ action=3D"store_true", ++ help=3DARG_SUPPRESS, ++ default=3DARG_SUPPRESS, ++ ) +=20 + options =3D parser.parse_args() + if isinstance(options, tuple): +@@ -1801,32 +1910,30 @@ + with an error stating which module is missing. + """ + optional_args =3D { +- 'json': ('json/simplejson python module', json), +- 'secure': ('SSL support', HTTPSConnection), ++ "json": ("json/simplejson python module", json), ++ "secure": ("SSL support", HTTPSConnection), + } +=20 + for arg, info in optional_args.items(): + if getattr(args, arg, False) and info[1] is None: +- raise SystemExit('%s is not installed. --%s is ' +- 'unavailable' % (info[0], arg)) ++ raise SystemExit(f"{info[0]} is not installed. --{arg} is unava= ilable") +=20 +=20 + def printer(string, quiet=3DFalse, debug=3DFalse, error=3DFalse, **kwargs): + """Helper function print a string with various features""" +- + if debug and not DEBUG: + return +=20 + if debug: + if sys.stdout.isatty(): +- out =3D '\033[1;30mDEBUG: %s\033[0m' % string ++ out =3D f"\x1b[1;30mDEBUG: {string}\x1b[0m" + else: +- out =3D 'DEBUG: %s' % string ++ out =3D f"DEBUG: {string}" + else: + out =3D string +=20 + if error: +- kwargs['file'] =3D sys.stderr ++ kwargs["file"] =3D sys.stderr +=20 + if not quiet: + print_(out, **kwargs) +@@ -1834,7 +1941,6 @@ +=20 + def shell(): + """Run the full speedtest.net test""" +- + global DEBUG + shutdown_event =3D threading.Event() +=20 +@@ -1847,32 +1953,25 @@ + version() +=20 + if not args.download and not args.upload: +- raise SpeedtestCLIError('Cannot supply both --no-download and ' +- '--no-upload') ++ raise SpeedtestCLIError("Cannot supply both --no-download and " "--= no-upload") +=20 + if len(args.csv_delimiter) !=3D 1: +- raise SpeedtestCLIError('--csv-delimiter must be a single character= ') ++ raise SpeedtestCLIError("--csv-delimiter must be a single character= ") +=20 + if args.csv_header: + csv_header(args.csv_delimiter) +=20 + validate_optional_args(args) +=20 +- debug =3D getattr(args, 'debug', False) +- if debug =3D=3D 'SUPPRESSHELP': ++ debug =3D getattr(args, "debug", False) ++ if debug =3D=3D "SUPPRESSHELP": + debug =3D False + if debug: + DEBUG =3D True +=20 +- if args.simple or args.csv or args.json: +- quiet =3D True +- else: +- quiet =3D False ++ quiet =3D args.simple or args.csv or args.json +=20 +- if args.csv or args.json: +- machine_format =3D True +- else: +- machine_format =3D False ++ machine_format =3D args.csv or args.json +=20 + # Don't set a callback if we are running quietly + if quiet or debug: +@@ -1880,28 +1979,30 @@ + else: + callback =3D print_dots(shutdown_event) +=20 +- printer('Retrieving speedtest.net configuration...', quiet) ++ printer("Retrieving speedtest.net configuration...", quiet) + try: + speedtest =3D Speedtest( + source_address=3Dargs.source, + timeout=3Dargs.timeout, +- secure=3Dargs.secure ++ secure=3Dargs.secure, + ) + except (ConfigRetrievalError,) + HTTP_ERRORS: +- printer('Cannot retrieve speedtest configuration', error=3DTrue) ++ printer("Cannot retrieve speedtest configuration", error=3DTrue) + raise SpeedtestCLIError(get_exception()) +=20 + if args.list: + try: + speedtest.get_servers() + except (ServersRetrievalError,) + HTTP_ERRORS: +- printer('Cannot retrieve speedtest server list', error=3DTrue) ++ printer("Cannot retrieve speedtest server list", error=3DTrue) + raise SpeedtestCLIError(get_exception()) +=20 + for _, servers in sorted(speedtest.servers.items()): + for server in servers: +- line =3D ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) ' +- '[%(d)0.2f km]' % server) ++ line =3D ( ++ "%(id)5s) %(sponsor)s (%(name)s, %(country)s) " ++ "[%(d)0.2f km]" % server ++ ) + try: + printer(line) + except IOError: +@@ -1910,104 +2011,109 @@ + raise + sys.exit(0) +=20 +- printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client'], +- quiet) ++ printer( ++ f"Testing from {speedtest.config['client']['isp']} ({speedtest.conf= ig['client']['ip']})...", ++ quiet, ++ ) +=20 + if not args.mini: +- printer('Retrieving speedtest.net server list...', quiet) ++ printer("Retrieving speedtest.net server list...", quiet) + try: + speedtest.get_servers(servers=3Dargs.server, exclude=3Dargs.exc= lude) + except NoMatchedServers: + raise SpeedtestCLIError( +- 'No matched servers: %s' % +- ', '.join('%s' % s for s in args.server) ++ "No matched servers: %s" % ", ".join("%s" % s for s in args= .server), + ) + except (ServersRetrievalError,) + HTTP_ERRORS: +- printer('Cannot retrieve speedtest server list', error=3DTrue) ++ printer("Cannot retrieve speedtest server list", error=3DTrue) + raise SpeedtestCLIError(get_exception()) + except InvalidServerIDType: + raise SpeedtestCLIError( +- '%s is an invalid server type, must ' +- 'be an int' % ', '.join('%s' % s for s in args.server) ++ "%s is an invalid server type, must " ++ "be an int" % ", ".join("%s" % s for s in args.server), + ) +=20 + if args.server and len(args.server) =3D=3D 1: +- printer('Retrieving information for the selected server...', qu= iet) ++ printer("Retrieving information for the selected server...", qu= iet) + else: +- printer('Selecting best server based on ping...', quiet) ++ printer("Selecting best server based on ping...", quiet) + speedtest.get_best_server() + elif args.mini: + speedtest.get_best_server(speedtest.set_mini_server(args.mini)) +=20 + results =3D speedtest.results +=20 +- printer('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' +- '%(latency)s ms' % results.server, quiet) ++ printer( ++ "Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: " ++ "%(latency)s ms" % results.server, ++ quiet, ++ ) +=20 + if args.download: +- printer('Testing download speed', quiet, +- end=3D('', '\n')[bool(debug)]) +- speedtest.download( +- callback=3Dcallback, +- threads=3D(None, 1)[args.single] ++ printer("Testing download speed", quiet, end=3D("", "\n")[bool(debu= g)]) ++ speedtest.download(callback=3Dcallback, threads=3D(None, 1)[args.si= ngle]) ++ printer( ++ "Download: %0.2f M%s/s" ++ % ((results.download / 1000.0 / 1000.0) / args.units[1], args.u= nits[0]), ++ quiet, + ) +- printer('Download: %0.2f M%s/s' % +- ((results.download / 1000.0 / 1000.0) / args.units[1], +- args.units[0]), +- quiet) + else: +- printer('Skipping download test', quiet) ++ printer("Skipping download test", quiet) +=20 + if args.upload: +- printer('Testing upload speed', quiet, +- end=3D('', '\n')[bool(debug)]) ++ printer("Testing upload speed", quiet, end=3D("", "\n")[bool(debug)= ]) + speedtest.upload( + callback=3Dcallback, + pre_allocate=3Dargs.pre_allocate, +- threads=3D(None, 1)[args.single] ++ threads=3D(None, 1)[args.single], ++ ) ++ printer( ++ "Upload: %0.2f M%s/s" ++ % ((results.upload / 1000.0 / 1000.0) / args.units[1], args.uni= ts[0]), ++ quiet, + ) +- printer('Upload: %0.2f M%s/s' % +- ((results.upload / 1000.0 / 1000.0) / args.units[1], +- args.units[0]), +- quiet) + else: +- printer('Skipping upload test', quiet) ++ printer("Skipping upload test", quiet) +=20 +- printer('Results:\n%r' % results.dict(), debug=3DTrue) ++ printer(f"Results:\n{results.dict()!r}", debug=3DTrue) +=20 + if not args.simple and args.share: + results.share() +=20 + if args.simple: +- printer('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' % +- (results.ping, +- (results.download / 1000.0 / 1000.0) / args.units[1], +- args.units[0], +- (results.upload / 1000.0 / 1000.0) / args.units[1], +- args.units[0])) ++ printer( ++ "Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s" ++ % ( ++ results.ping, ++ (results.download / 1000.0 / 1000.0) / args.units[1], ++ args.units[0], ++ (results.upload / 1000.0 / 1000.0) / args.units[1], ++ args.units[0], ++ ), ++ ) + elif args.csv: + printer(results.csv(delimiter=3Dargs.csv_delimiter)) + elif args.json: + printer(results.json()) +=20 + if args.share and not machine_format: +- printer('Share results: %s' % results.share()) ++ printer(f"Share results: {results.share()}") +=20 +=20 + def main(): + try: + shell() + except KeyboardInterrupt: +- printer('\nCancelling...', error=3DTrue) ++ printer("\nCancelling...", error=3DTrue) + except (SpeedtestException, SystemExit): + e =3D get_exception() + # Ignore a successful exit, or argparse exit +- if getattr(e, 'code', 1) not in (0, 2): +- msg =3D '%s' % e ++ if getattr(e, "code", 1) not in (0, 2): ++ msg =3D f"{e}" + if not msg: +- msg =3D '%r' % e +- raise SystemExit('ERROR: %s' % msg) ++ msg =3D f"{e!r}" ++ raise SystemExit(f"ERROR: {msg}") +=20 +=20 +-if __name__ =3D=3D '__main__': ++if __name__ =3D=3D "__main__": + main() +diff -Naur speedtest-cli-2.1.3.orig/tests/scripts/source.py speedtest-cli-2.= 1.3/tests/scripts/source.py +--- speedtest-cli-2.1.3.orig/tests/scripts/source.py 2021-04-08 15:45:29.000= 000000 +0200 ++++ speedtest-cli-2.1.3/tests/scripts/source.py 2025-01-05 13:17:06.01403755= 7 +0100 +@@ -15,23 +15,19 @@ + # License for the specific language governing permissions and limitations + # under the License. +=20 +-import sys + import subprocess ++import sys +=20 +-cmd =3D [sys.executable, 'speedtest.py', '--source', '127.0.0.1'] ++cmd =3D [sys.executable, "speedtest.py", "--source", "127.0.0.1"] +=20 +-p =3D subprocess.Popen( +- cmd, +- stdout=3Dsubprocess.PIPE, +- stderr=3Dsubprocess.PIPE +-) ++p =3D subprocess.Popen(cmd, stdout=3Dsubprocess.PIPE, stderr=3Dsubprocess.P= IPE) +=20 + stdout, stderr =3D p.communicate() +=20 + if p.returncode !=3D 1: +- raise SystemExit('%s did not fail with exit code 1' % ' '.join(cmd)) ++ raise SystemExit(f"{' '.join(cmd)} did not fail with exit code 1") +=20 +-if 'Invalid argument'.encode() not in stderr: ++if "Invalid argument".encode() not in stderr: + raise SystemExit( +- '"Invalid argument" not found in stderr:\n%s' % stderr.decode() ++ f'"Invalid argument" not found in stderr:\n{stderr.decode()}', + ) diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.12_remove= _deprecated_method.patch b/src/patches/speedtest-cli/speedtest-cli-2.1.3-pyth= on_3.12_remove_deprecated_method.patch new file mode 100644 index 000000000..81014dda8 --- /dev/null +++ b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.12_remove_deprec= ated_method.patch @@ -0,0 +1,27 @@ +Patch originally from + +From: Lavender +Date: Mon, 4 Dec 2023 15:45:07 +0000 +Subject: [PATCH] remove deprecated method in python3.12 + +however this does not work together with other patches as none of them have = been merged into speedtest-cli and this one clashed with a previous change. + +Adolf Belka took the original patch and modified = it to this version to work with the other patches. + +diff -Naur speedtest-cli-2.1.3.orig/speedtest.py speedtest-cli-2.1.3/speedte= st.py +--- speedtest-cli-2.1.3.orig/speedtest.py 2025-01-05 13:36:51.090504543 +0100 ++++ speedtest-cli-2.1.3/speedtest.py 2025-01-05 13:42:27.952782400 +0100 +@@ -980,7 +980,12 @@ + self.client =3D client or {} +=20 + self._share =3D None +- self.timestamp =3D f"{datetime.datetime.utcnow().isoformat()}Z" ++ # datetime.datetime.utcnow() is deprecated starting from 3.12 ++ # but datetime.UTC is supported starting from 3.11 ++ if sys.version_info.major >=3D 3 and sys.version_info.minor >=3D 11: ++ self.timestamp =3D f"{datetime.datetime.now(datetime.UTC).isofo= rmat()}Z" ++ else: ++ self.timestamp =3D f"{datetime.datetime.utcnow().isoformat()}Z" + self.bytes_received =3D 0 + self.bytes_sent =3D 0 +=20 --=20 2.47.1 --===============2117277164450723600==--