From mboxrd@z Thu Jan 1 00:00:00 1970 From: Bernhard Bitsch To: development@lists.ipfire.org Subject: Re: [PATCH] speedtest-cli: Fix for bug13805 - error message if run on hour or half hour Date: Tue, 07 Jan 2025 20:51:42 +0100 Message-ID: <6a3451f5-a44a-411e-8359-bfb50208b245@ipfire.org> In-Reply-To: <20250106135226.13854-1-adolf.belka@ipfire.org> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="===============8398293249517758309==" List-Id: --===============8398293249517758309== Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Tested-by: Bernhard Bitsch Am 06.01.2025 um 14:52 schrieb Adolf Belka: > - 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 successf= ully 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= hour. I > tested it out with the original system first and it failed with the err= or message > for 7 half hour tests. With this modified version it ran for 9 half hou= r slots 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 a= s the last > commit was July 2021 and the patches were proposed in Feb 2023. There h= as 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 = concerns > from a security point of view although it would be good to get feedback= from > alternative eyes. > - Update of rootfile not required. >=20 > 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_= errors.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 >=20 > 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/speedt= est-cli-2.1.3-python_3.10_support.patch > + cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedt= est-cli-2.1.3-python_3.11_updates_and_fixes.patch > + cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedt= est-cli-2.1.3-fix_429_errors.patch > + cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedt= est-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.p= atch 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=3DNon= e): > + ) > + > + 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", > + ] > + > + headers =3D {} > +@@ -1346,56 +1344,34 @@ def get_servers(self, servers=3DNone, exclude=3DNo= ne): > + printer(f"Servers XML:\n{serversxml}", debug=3DTrue) > + > + 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() > + > + 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 > + > + if ( > +- int(attrib.get("id")) in self.config["ignore_serv= ers"] > +- or int(attrib.get("id")) in exclude > ++ int(server.get("id")) in self.config["ignore_serv= ers"] > ++ or int(server.get("id")) in exclude > + ): > + continue > + > + try: > + d =3D distance( > + self.lat_lon, > +- (float(attrib.get("lat")), float(attrib.get("= lon"))), > ++ (float(server.get("lat")), float(server.get("= lon"))), > + ) > + except Exception: > + continue > + > +- attrib["d"] =3D d > ++ server["d"] =3D d > + > + try: > +- self.servers[d].append(attrib) > ++ self.servers[d].append(server) > + except KeyError: > +- self.servers[d] =3D [attrib] > ++ self.servers[d] =3D [server] > + > + break > + > + > diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10_supp= ort.patch b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10_support= .patch > new file mode 100644 > index 000000000..e3182d284 > --- /dev/null > +++ b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10_support.pat= ch > @@ -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= .py not the rest of the package. > +This modification by Adolf Belka does everythin= g 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/speed= test.py > +--- speedtest-cli-2.1.3.orig/speedtest.py 2021-04-08 15:45:29.000000000 +0= 200 > ++++ 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 limitati= ons > + # under the License. > + > +-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 > + > + try: > +@@ -49,6 +49,8 @@ > + "Dummy method to always return false""" > + return False > + > ++ is_set =3D isSet > ++ > + > + # 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) > + > + # Begin import game to handle Python 2 and Python 3 > + try: > +@@ -266,17 +269,6 @@ > + write(arg) > + write(end) > + > +-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) > + > ++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() > ++ > + > + 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 > + > + sys.stdout.write('.') > +@@ -808,7 +817,7 @@ > + try: > + if (timeit.default_timer() - self.starttime) <=3D self.timeou= t: > + 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 @@ > + > + 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.timeo= ut 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_upda= tes_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= _fixes.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 secti= on. This modified patch by Adolf Belka is the same= as the original patch but with the inclusion of Python 3.11 in the Classifie= rs 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 limitati= ons > + # under the License. > + > ++import codecs > + import os > + import re > +-import codecs > + > + from setuptools import setup > + > +@@ -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.") > + > + # 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 ['\"]([^'\"]*)['\"]", = version_file, re.M) > + if version_match: > + return version_match.group(1) > + raise RuntimeError("Unable to find version string.") > +@@ -48,52 +47,54 @@ > + > + # 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 "" > + > + > + setup( > +- name=3D'speedtest-cli', > +- version=3Dfind_version('speedtest.py'), > +- description=3D('Command line interface for testing internet bandwidth= using ' > +- 'speedtest.net'), > ++ name=3D"speedtest-cli", > ++ version=3Dfind_version("speedtest.py"), > ++ description=3D( > ++ "Command line interface for testing internet bandwidth using " "s= peedtest.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/speed= test.py > +--- speedtest-cli-2.1.3.orig/speedtest.py 2025-01-05 13:14:39.655395043 +0= 100 > ++++ speedtest-cli-2.1.3/speedtest.py 2025-01-05 13:17:05.914033926 +0100 > +@@ -31,22 +31,23 @@ > + > + try: > + import gzip > ++ > + GZIP_BASE =3D gzip.GzipFile > + except ImportError: > + gzip =3D None > + GZIP_BASE =3D object > + > +-__version__ =3D '2.1.3' > ++__version__ =3D "2.1.3" > + > + > +-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() > + """ > + > + @staticmethod > + def isSet(): > +- "Dummy method to always return false""" > ++ """Dummy method to always return false""" > + return False > + > + is_set =3D isSet > +@@ -71,6 +72,7 @@ > + > + 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 > + > + try: > +- from urllib2 import (urlopen, Request, HTTPError, URLError, > +- AbstractHTTPHandler, ProxyHandler, > +- HTTPDefaultErrorHandler, HTTPRedirectHandler, > +- HTTPErrorProcessor, OpenerDirector) > ++ from urllib2 import (AbstractHTTPHandler, HTTPDefaultErrorHandler, > ++ HTTPError, HTTPErrorProcessor, HTTPRedirectHandl= er, > ++ OpenerDirector, ProxyHandler, Request, URLError, > ++ urlopen) > + except ImportError: > +- from urllib.request import (urlopen, Request, HTTPError, URLError, > +- AbstractHTTPHandler, ProxyHandler, > +- HTTPDefaultErrorHandler, HTTPRedirectHand= ler, > +- HTTPErrorProcessor, OpenerDirector) > ++ from urllib.request import (AbstractHTTPHandler, HTTPDefaultErrorHand= ler, > ++ HTTPError, HTTPErrorProcessor, > ++ HTTPRedirectHandler, OpenerDirector, > ++ ProxyHandler, Request, URLError, urlopen) > + > + 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 > + > + try: > + from httplib import HTTPSConnection > +@@ -133,51 +136,52 @@ > + from md5 import md5 > + > + 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" > + > + 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 > + > + try: > + import __builtin__ > + except ImportError: > + import builtins > +- from io import TextIOWrapper, FileIO > ++ from io import FileIO, TextIOWrapper > + > + 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") > + > + def write(self, s): > +- super(_Py3Utf8Output, self).write(s) > ++ super().write(s) > + self.flush() > + > +- _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 > + > + def to_utf8(v): > +- """No-op encode to utf-8 for py3""" > ++ """No-op encode to utf-8 for py3.""" > + return v > + > + 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__ > + > + 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 > + > +@@ -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) > + > ++ > + # Exception "constants" to support Python 2 through Python 3 > + try: > + import ssl > ++ > + try: > + CERT_ERROR =3D (ssl.CertificateError,) > + except AttributeError: > + CERT_ERROR =3D tuple() > + > + 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""" > + > + > +-def create_connection(address, timeout=3D_GLOBAL_DEFAULT_TIMEOUT, > +- source_address=3DNone): > ++def create_connection(address, timeout=3D_GLOBAL_DEFAULT_TIMEOUT, source_= address=3DNone): > + """Connect to *address* and return the socket object. > + > + Convenience function. Connect to *address* (a 2-tuple ``(host, > +@@ -388,7 +400,6 @@ > + > + 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 @@ > + > + if err is not None: > + raise err > +- else: > +- raise socket.error("getaddrinfo returns an empty list") > ++ raise socket.error("getaddrinfo returns an empty list") > + > + > + 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) > + > + self._tunnel_host =3D None > + > +@@ -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, > + ) > + > + if self._tunnel_host: > +@@ -449,15 +460,17 @@ > + > + > + if HTTPSConnection: > ++ > + class SpeedtestHTTPSConnection(HTTPSConnection): > + """Custom HTTPSConnection to support source_address across > + Python 2.4 - Python 3 > + """ > ++ > + default_port =3D 443 > + > + 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) > + > + self._tunnel_host =3D None > + > +@@ -467,18 +480,18 @@ > + self.source_address =3D source_address > + > + 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, > + ) > + > + 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_ho= st > ++ kwargs["server_hostname"] =3D self._tunnel_ho= st > + else: > +- kwargs['server_hostname'] =3D self.host > ++ kwargs["server_hostname"] =3D self.host > + self.sock =3D self._context.wrap_socket(self.sock, **= kwargs) > + except AttributeError: > + self.sock =3D ssl.wrap_socket(self.sock) > +@@ -505,13 +518,13 @@ > + self.sock =3D FakeSocket(self.sock, socket.ssl(self.s= ock)) > + except AttributeError: > + raise SpeedtestException( > +- 'This version of Python does not support HTTPS/SS= L ' > +- 'functionality' > ++ "This version of Python does not support HTTPS/SS= L " > ++ "functionality", > + ) > + else: > + raise SpeedtestException( > +- 'This version of Python does not support HTTPS/SSL ' > +- 'functionality' > ++ "This version of Python does not support HTTPS/SSL " > ++ "functionality", > + ) > + > + > +@@ -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": timeo= ut}) > + if context: > +- kwargs['context'] =3D context > ++ kwargs["context"] =3D context > + return connection(host, **kwargs) > ++ > + return inner > + > + > +@@ -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=3D1= 0): > + 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, > + ) > + > + 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=3DN= one, > +- timeout=3D10): > ++ > ++ def __init__(self, debuglevel=3D0, context=3DNone, source_address=3DN= one, 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, > + ) > + > + 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) > + > + 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}", d= ebug=3DTrue) > + else: > + source_address_tuple =3D None > + > + 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, timeo= ut=3Dtimeout), > ++ SpeedtestHTTPSHandler(source_address=3Dsource_address_tuple, time= out=3Dtimeout), > + HTTPDefaultErrorHandler(), > + HTTPRedirectHandler(), > +- HTTPErrorProcessor() > ++ HTTPErrorProcessor(), > + ] > + > + opener =3D OpenerDirector() > +- opener.addheaders =3D [('User-agent', build_user_agent())] > ++ opener.addheaders =3D [("User-agent", build_user_agent())] > + > + 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) > + > + def close(self): > + try: > +@@ -655,17 +667,15 @@ > + > + 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 > + > + 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 > + > +@@ -674,52 +684,47 @@ > + > + 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 > + > + > +-def build_request(url, data=3DNone, headers=3DNone, bump=3D'0', secure=3D= False): > ++def build_request(url, data=3DNone, headers=3DNone, bump=3D"0", secure=3D= False): > + """Build a urllib2 request object > + > + This function automatically adds a User-Agent header to all requests > +- > + """ > +- > + if not headers: > + headers =3D {} > + > +- 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 > + > +- if '?' in url: > +- delim =3D '&' > ++ if "?" in url: > ++ delim =3D "&" > + else: > +- delim =3D '?' > ++ delim =3D "?" > + > + # 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() * 100= 0)}.{bump}" > ++ > ++ headers.update( > ++ { > ++ "Cache-Control": "no-cache", > ++ }, > ++ ) > + > +- printer('%s %s' % (('GET', 'POST')[bool(data)], final_url), > +- debug=3DTrue) > ++ printer(f"{('GET', 'POST')[bool(data)]} {final_url}", debug=3DTrue) > + > + return Request(final_url, data=3Ddata, headers=3Dheaders) > + > +@@ -729,7 +734,6 @@ > + establishing a connection with a HTTP/HTTPS request > + > + """ > +- > + 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 > + > + """ > +- > + try: > + getheader =3D response.headers.getheader > + except AttributeError: > + getheader =3D response.getheader > + > +- if getheader('content-encoding') =3D=3D 'gzip': > ++ if getheader("content-encoding") =3D=3D "gzip": > + return GzipDecodedResponse(response) > + > + 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 > + > +- 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 > + > + > +@@ -795,8 +800,7 @@ > + class HTTPDownloader(threading.Thread): > + """Thread class for retrieving a URL""" > + > +- def __init__(self, i, request, start, timeout, opener=3DNone, > +- shutdown_event=3DNone): > ++ def __init__(self, i, request, start, timeout, opener=3DNone, shutdow= n_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.timeou= t: > + 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 se= lf.timeout > ++ ): > + self.result.append(len(f.read(10240))) > + if self.result[-1] =3D=3D 0: > + break > +@@ -830,7 +835,7 @@ > + pass > + > + > +-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] > + > + 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", > + ) > + > + @property > +@@ -872,13 +875,13 @@ > + return self._data > + > + 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 no= t event_is_set( > ++ self._shutdown_event, > ++ ): > + chunk =3D self.data.read(n) > + self.total.append(len(chunk)) > + return chunk > +- else: > +- raise SpeedtestUploadTimeout() > ++ raise SpeedtestUploadTimeout() > + > + def __len__(self): > + return self.length > +@@ -887,8 +890,16 @@ > + class HTTPUploader(threading.Thread): > + """Thread class for putting a URL""" > + > +- 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.timeo= ut 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 co= ncede > + # 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= .size)) > ++ 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 > + > + > +-class SpeedtestResults(object): > ++class SpeedtestResults: > + """Class for holding the results of a speedtest, including: > + > + Download speed > +@@ -945,8 +959,16 @@ > + to get a share results image link. > + """ > + > +- def __init__(self, download=3D0, upload=3D0, ping=3D0, server=3DNone,= client=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 {} > + > + 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 > + > +@@ -975,7 +997,6 @@ > + """POST data to the speedtest.net API to obtain a share results > + link > + """ > +- > + if self._share: > + return self._share > + > +@@ -987,29 +1008,33 @@ > + # We use a list instead of a dict because the API expects paramet= ers > + # 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")).en= code(), > ++ ).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']}", > + ] > + > +- headers =3D {'Referer': 'http://c.speedtest.net/flash/speedtest.s= wf'} > +- 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.s= wf"} > ++ 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() > + > + if int(code) !=3D 200: > +- raise ShareResultsSubmitFailure('Could not submit results to ' > +- 'speedtest.net') > ++ raise ShareResultsSubmitFailure( > ++ "Could not submit results to " "speedtest.net", > ++ ) > + > + 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", > ++ ) > + > +- self._share =3D 'http://www.speedtest.net/result/%s.png' % result= id[0] > ++ self._share =3D f"http://www.speedtest.net/result/{resultid[0]}.p= ng" > + > + return self._share > + > + 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, > + } > + > + @staticmethod > +- def csv_header(delimiter=3D','): > ++ def csv_header(delimiter=3D","): > + """Return CSV Headers""" > +- > +- row =3D ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Dis= tance', > +- '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() > + > +- 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() > + > + 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) > + > + > +-class Speedtest(object): > ++class Speedtest: > + """Class for performing standard speedtest.net testing operations""" > + > +- 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 {} > + > + self._source_address =3D source_address > +@@ -1110,7 +1154,7 @@ > + self._best =3D {} > + > + 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 d= ata > + we are interested in > + """ > +- > + headers =3D {} > + if gzip: > +- headers['Accept-Encoding'] =3D 'gzip' > +- request =3D build_request('://www.speedtest.net/speedtest-config.= php', > +- 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 > + > +- configxml =3D ''.encode().join(configxml_list) > ++ configxml =3D "".encode().join(configxml_list) > + > +- printer('Config XML:\n%s' % configxml, debug=3DTrue) > ++ printer(f"Config XML:\n{configxml}", debug=3DTrue) > + > + 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 > + > + 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-co= nfig') > +- 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-co= nfig") > ++ 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") > + > +- 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"].sp= lit(",") if i] > + > +- 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, 7340= 032] > + 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, 350= 0, 4000], > + } > + > +- size_count =3D len(sizes['upload']) > ++ size_count =3D len(sizes["upload"]) > + > + upload_count =3D int(math.ceil(upload_max / size_count)) > + > +- counts =3D { > +- 'upload': upload_count, > +- 'download': int(download['threadsperurl']) > +- } > ++ counts =3D {"upload": upload_count, "download": int(download["thr= eadsperurl"])} > + > + threads =3D { > +- 'upload': int(upload['threads']), > +- 'download': int(server_config['threadcount']) * 2 > ++ "upload": int(upload["threads"]), > ++ "download": int(server_config["threadcount"]) * 2, > + } > + > + length =3D { > +- 'upload': int(upload['testlength']), > +- 'download': int(download['testlength']) > ++ "upload": int(upload["testlength"]), > ++ "download": int(download["testlength"]), > + } > + > +- 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, > ++ }, > ++ ) > + > + 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")), > + ) > + > +- printer('Config:\n%r' % self.config, debug=3DTrue) > ++ printer(f"Config:\n{self.config!r}", debug=3DTrue) > + > + return self.config > + > +@@ -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", > + ) > + > + 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", > + ] > + > + headers =3D {} > + if gzip: > +- headers['Accept-Encoding'] =3D 'gzip' > ++ headers["Accept-Encoding"] =3D "gzip" > + > + 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() > + > + stream =3D get_response_stream(uh) > +@@ -1300,9 +1341,9 @@ > + if int(uh.code) !=3D 200: > + raise ServersRetrievalError() > + > +- serversxml =3D ''.encode().join(serversxml_list) > ++ serversxml =3D "".encode().join(serversxml_list) > + > +- printer('Servers XML:\n%s' % serversxml, debug=3DTrue) > ++ printer(f"Servers XML:\n{serversxml}", debug=3DTrue) > + > + 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() > + > +@@ -1332,21 +1373,24 @@ > + except AttributeError: > + attrib =3D dict(list(server.attributes.items())) > + > +- if servers and int(attrib.get('id')) not in servers: > ++ if servers and int(attrib.get("id")) not in servers: > + continue > + > +- if (int(attrib.get('id')) in self.config['ignore_serv= ers'] > +- or int(attrib.get('id')) in exclude): > ++ if ( > ++ int(attrib.get("id")) in self.config["ignore_serv= ers"] > ++ or int(attrib.get("id")) in exclude > ++ ): > + continue > + > + 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("= lon"))), > ++ ) > + except Exception: > + continue > + > +- attrib['d'] =3D d > ++ attrib["d"] =3D d > + > + 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) > + > + 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 {ser= ver}") > ++ text =3D uh.read() > ++ uh.close() > + > +- extension =3D re.findall('upload_?[Ee]xtension: "([^"]+)"', > +- text.decode()) > ++ extension =3D re.findall('upload_?[Ee]xtension: "([^"]+)"', text.= decode()) > + 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 Serv= er: ' > +- '%s' % server) > ++ raise InvalidSpeedtestMiniServer( > ++ "Invalid Speedtest Mini Server: " "%s" % server, > ++ ) > + > +- 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, > ++ }, > ++ ] > + > + return self.servers > + > +@@ -1421,7 +1464,6 @@ > + """Limit servers to the closest speedtest.net servers based on > + geographic distance > + """ > +- > + if not self.servers: > + self.get_servers() > + > +@@ -1434,14 +1476,13 @@ > + continue > + break > + > +- printer('Closest Servers:\n%r' % self.closest, debug=3DTrue) > ++ printer(f"Closest Servers:\n{self.closest!r}", debug=3DTrue) > + return self.closest > + > + 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 > + > + text =3D r.read(9) > +- if int(r.status) =3D=3D 200 and text =3D=3D 'test=3Dtest'= .encode(): > ++ if int(r.status) =3D=3D 200 and text =3D=3D "test=3Dtest"= .encode(): > + 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 server= s 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 > + > + self.results.ping =3D fastest > + self.results.server =3D best > + > + self._best.update(best) > +- printer('Best Server:\n%r' % best, debug=3DTrue) > ++ printer(f"Best Server:\n{best!r}", debug=3DTrue) > + return best > + > + 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, siz= e)) > ++ 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), > ++ ) > + > + 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._s= ecure)) > + > +- 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} > + > + 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) > + > + 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) > + > + 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, re= quest_count)) > + start =3D timeit.default_timer() > + prod_thread.start() > + cons_thread.start() > +@@ -1581,11 +1622,9 @@ > + > + 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 - = start)) * 8.0 > + if self.results.download > 100000: > +- self.config['threads']['upload'] =3D 8 > ++ self.config["threads"]["upload"] =3D 8 > + return self.results.download > + > + def upload(self, callback=3Ddo_nothing, pre_allocate=3DTrue, threads= =3DNone): > +@@ -1594,40 +1633,43 @@ > + A ``threads`` value of ``None`` will fall back to those dictated > + by the speedtest.net configuration > + """ > +- > + sizes =3D [] > + > +- 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) > + > + # request_count =3D len(sizes) > +- request_count =3D self.config['upload_max'] > ++ request_count =3D self.config["upload_max"] > + > + 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() > + > +- headers =3D {'Content-length': size} > ++ headers =3D {"Content-length": size} > + requests.append( > + ( > +- build_request(self.best['url'], data, secure=3Dself._= secure, > +- headers=3Dheaders), > +- size > +- ) > ++ build_request( > ++ self.best["url"], > ++ data, > ++ secure=3Dself._secure, > ++ headers=3Dheaders, > ++ ), > ++ size, > ++ ), > + ) > + > +- 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} > + > + 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) > + > + 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) > + > +- 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, re= quest_count)) > + start =3D timeit.default_timer() > + prod_thread.start() > + cons_thread.start() > +@@ -1675,9 +1718,7 @@ > + > + 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 > + > + > +@@ -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 > + > + > + 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) > + > + > +-def csv_header(delimiter=3D','): > ++def csv_header(delimiter=3D","): > + """Print the CSV Headers""" > +- > + printer(SpeedtestResults.csv_header(delimiter=3Ddelimiter)) > + sys.exit(0) > + > +@@ -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" > ++ ) > + > + 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=3DTru= e, > +- 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 speedte= st.net ' > +- 'share results image, not displayed with --c= sv') > +- parser.add_argument('--simple', action=3D'store_true', default=3DFals= e, > +- 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_T= YPE_STR, > +- help=3D'Single character delimiter to use in CSV ' > +- 'output. Default ","') > +- parser.add_argument('--csv-header', action=3D'store_true', default=3D= False, > +- 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 i= n ' > +- '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'app= end', > +- help=3D'Specify a server ID to test against. Can = be ' > +- 'supplied multiple times') > +- parser.add_argument('--exclude', type=3DPARSER_TYPE_INT, action=3D'ap= pend', > +- help=3D'Exclude a server from selection. Can be ' > +- 'supplied multiple times') > +- parser.add_argument('--mini', help=3D'URL of the Speedtest Mini serve= r') > +- parser.add_argument('--source', help=3D'Source IP address to bind to') > +- parser.add_argument('--timeout', default=3D10, type=3DPARSER_TYPE_FLO= AT, > +- help=3D'HTTP timeout in seconds. Default 10') > +- parser.add_argument('--secure', action=3D'store_true', > +- help=3D'Use HTTPS instead of HTTP when communicat= ing ' > +- 'with speedtest.net operated servers') > +- parser.add_argument('--no-pre-allocate', dest=3D'pre_allocate', > +- action=3D'store_const', default=3DTrue, const=3DF= alse, > +- help=3D'Do not pre allocate upload data. Pre allo= cation ' > +- 'is enabled by default to improve upload ' > +- 'performance. To support systems with ' > +- 'insufficient memory, use this option to avo= id 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. Defau= lt ","', > ++ ) > ++ 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 dist= ance", > ++ ) > ++ parser.add_argument( > ++ "--server", > ++ type=3DPARSER_TYPE_INT, > ++ action=3D"append", > ++ help=3D"Specify a server ID to test against. Can be " "supplied m= ultiple times", > ++ ) > ++ parser.add_argument( > ++ "--exclude", > ++ type=3DPARSER_TYPE_INT, > ++ action=3D"append", > ++ help=3D"Exclude a server from selection. Can be " "supplied multi= ple times", > ++ ) > ++ parser.add_argument("--mini", help=3D"URL of the Speedtest Mini serve= r") > ++ 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, > ++ ) > + > + 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), > + } > + > + 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 una= vailable") > + > + > + def printer(string, quiet=3DFalse, debug=3DFalse, error=3DFalse, **kwargs= ): > + """Helper function print a string with various features""" > +- > + if debug and not DEBUG: > + return > + > + 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 > + > + if error: > +- kwargs['file'] =3D sys.stderr > ++ kwargs["file"] =3D sys.stderr > + > + if not quiet: > + print_(out, **kwargs) > +@@ -1834,7 +1941,6 @@ > + > + def shell(): > + """Run the full speedtest.net test""" > +- > + global DEBUG > + shutdown_event =3D threading.Event() > + > +@@ -1847,32 +1953,25 @@ > + version() > + > + 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") > + > + if len(args.csv_delimiter) !=3D 1: > +- raise SpeedtestCLIError('--csv-delimiter must be a single charact= er') > ++ raise SpeedtestCLIError("--csv-delimiter must be a single charact= er") > + > + if args.csv_header: > + csv_header(args.csv_delimiter) > + > + validate_optional_args(args) > + > +- 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 > + > +- 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 > + > +- if args.csv or args.json: > +- machine_format =3D True > +- else: > +- machine_format =3D False > ++ machine_format =3D args.csv or args.json > + > + # 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) > + > +- 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()) > + > + 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()) > + > + 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) > + > +- printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client= '], > +- quiet) > ++ printer( > ++ f"Testing from {speedtest.config['client']['isp']} ({speedtest.co= nfig['client']['ip']})...", > ++ quiet, > ++ ) > + > + 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.e= xclude) > + 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 ar= gs.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), > + ) > + > + if args.server and len(args.server) =3D=3D 1: > +- printer('Retrieving information for the selected server...', = quiet) > ++ printer("Retrieving information for the selected server...", = quiet) > + 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)) > + > + results =3D speedtest.results > + > +- 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, > ++ ) > + > + 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(de= bug)]) > ++ speedtest.download(callback=3Dcallback, threads=3D(None, 1)[args.= single]) > ++ printer( > ++ "Download: %0.2f M%s/s" > ++ % ((results.download / 1000.0 / 1000.0) / args.units[1], args= .units[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) > + > + if args.upload: > +- printer('Testing upload speed', quiet, > +- end=3D('', '\n')[bool(debug)]) > ++ printer("Testing upload speed", quiet, end=3D("", "\n")[bool(debu= g)]) > + 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.u= nits[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) > + > +- printer('Results:\n%r' % results.dict(), debug=3DTrue) > ++ printer(f"Results:\n{results.dict()!r}", debug=3DTrue) > + > + if not args.simple and args.share: > + results.share() > + > + 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()) > + > + if args.share and not machine_format: > +- printer('Share results: %s' % results.share()) > ++ printer(f"Share results: {results.share()}") > + > + > + 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}") > + > + > +-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.0= 00000000 +0200 > ++++ speedtest-cli-2.1.3/tests/scripts/source.py 2025-01-05 13:17:06.014037= 557 +0100 > +@@ -15,23 +15,19 @@ > + # License for the specific language governing permissions and limitati= ons > + # under the License. > + > +-import sys > + import subprocess > ++import sys > + > +-cmd =3D [sys.executable, 'speedtest.py', '--source', '127.0.0.1'] > ++cmd =3D [sys.executable, "speedtest.py", "--source", "127.0.0.1"] > + > +-p =3D subprocess.Popen( > +- cmd, > +- stdout=3Dsubprocess.PIPE, > +- stderr=3Dsubprocess.PIPE > +-) > ++p =3D subprocess.Popen(cmd, stdout=3Dsubprocess.PIPE, stderr=3Dsubprocess= .PIPE) > + > + stdout, stderr =3D p.communicate() > + > + 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") > + > +-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_remo= ve_deprecated_method.patch b/src/patches/speedtest-cli/speedtest-cli-2.1.3-py= thon_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_depr= ecated_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 hav= e been merged into speedtest-cli and this one clashed with a previous change. > + > +Adolf Belka took the original patch and modifie= d 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/speed= test.py > +--- speedtest-cli-2.1.3.orig/speedtest.py 2025-01-05 13:36:51.090504543 +0= 100 > ++++ speedtest-cli-2.1.3/speedtest.py 2025-01-05 13:42:27.952782400 +0100 > +@@ -980,7 +980,12 @@ > + self.client =3D client or {} > + > + 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).iso= format()}Z" > ++ else: > ++ self.timestamp =3D f"{datetime.datetime.utcnow().isoformat()}= Z" > + self.bytes_received =3D 0 > + self.bytes_sent =3D 0 > + --===============8398293249517758309==--