speedtest-cli: Fix for bug13805 - error message if run on hour or half hour

Message ID 20250106135226.13854-1-adolf.belka@ipfire.org
State Staged
Commit 2dc005c68b24f7e3d5d59258da26c9ff6e25ffdc
Headers
Series speedtest-cli: Fix for bug13805 - error message if run on hour or half hour |

Commit Message

Adolf Belka Jan. 6, 2025, 1:52 p.m. UTC
  - 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 successfully 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 error message
   for 7 half hour tests. With this modified version it ran for 9 half hour 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 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 concerns
   from a security point of view although it would be good to get feedback from
   alternative eyes.
- Update of rootfile not required.

Fixes: Bug13805
Tested-by: Adolf Belka <adolf.belka@ipfire.org>
Signed-off-by: Adolf Belka <adolf.belka@ipfire.org>
---
 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
  

Comments

Bernhard Bitsch Jan. 7, 2025, 7:50 p.m. UTC | #1
Testeed-by: Bernhard Bitsch <bbitsch@ipfire.org>

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 successfully 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 error message
>     for 7 half hour tests. With this modified version it ran for 9 half hour 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 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 concerns
>     from a security point of view although it would be good to get feedback from
>     alternative eyes.
> - Update of rootfile not required.
> 
> Fixes: Bug13805
> Tested-by: Adolf Belka <adolf.belka@ipfire.org>
> Signed-off-by: Adolf Belka <adolf.belka@ipfire.org>
> ---
>   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
> 
> 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  <info@ipfire.org>                     #
> +# Copyright (C) 2007-2025  IPFire Team  <info@ipfire.org>                     #
>   #                                                                             #
>   # 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    = $(URL_IPFIRE)
>   DIR_APP    = $(DIR_SRC)/$(THISAPP)
>   TARGET     = $(DIR_INFO)/$(THISAPP)
>   PROG       = speedtest-cli
> -PAK_VER    = 5
> +PAK_VER    = 6
>   
>   DEPS       =
>   
> @@ -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/speedtest-cli-2.1.3-python_3.10_support.patch
> +	cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11_updates_and_fixes.patch
> +	cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_errors.patch
> +	cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtest-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=/
>   	@rm -rf $(DIR_APP)
> diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_errors.patch 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 <marrco@wohecha.fr>
> +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=None, exclude=None):
> +                     )
> +
> +         urls = [
> +-            "://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 = {}
> +@@ -1346,56 +1344,34 @@ def get_servers(self, servers=None, exclude=None):
> +                 printer(f"Servers XML:\n{serversxml}", debug=True)
> +
> +                 try:
> +-                    try:
> +-                        try:
> +-                            root = ET.fromstring(serversxml)
> +-                        except ET.ParseError:
> +-                            e = get_exception()
> +-                            raise SpeedtestServersError(
> +-                                f"Malformed speedtest.net server list: {e}",
> +-                            )
> +-                        elements = etree_iter(root, "server")
> +-                    except AttributeError:
> +-                        try:
> +-                            root = DOM.parseString(serversxml)
> +-                        except ExpatError:
> +-                            e = get_exception()
> +-                            raise SpeedtestServersError(
> +-                                f"Malformed speedtest.net server list: {e}",
> +-                            )
> +-                        elements = root.getElementsByTagName("server")
> +-                except (SyntaxError, xml.parsers.expat.ExpatError):
> ++                    elements = json.loads(serversxml)
> ++                except SyntaxError:
> +                     raise ServersRetrievalError()
> +
> +                 for server in elements:
> +-                    try:
> +-                        attrib = server.attrib
> +-                    except AttributeError:
> +-                        attrib = 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_servers"]
> +-                        or int(attrib.get("id")) in exclude
> ++                        int(server.get("id")) in self.config["ignore_servers"]
> ++                        or int(server.get("id")) in exclude
> +                     ):
> +                         continue
> +
> +                     try:
> +                         d = 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"] = d
> ++                    server["d"] = d
> +
> +                     try:
> +-                        self.servers[d].append(attrib)
> ++                        self.servers[d].append(server)
> +                     except KeyError:
> +-                        self.servers[d] = [attrib]
> ++                        self.servers[d] = [server]
> +
> +                 break
> +
> +
> diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10_support.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.patch
> @@ -0,0 +1,146 @@
> +Patch originally from
> +
> +From 22210ca35228f0bbcef75a7c14587c4ecb875ab4 Mon Sep 17 00:00:00 2001
> +From: Matt Martz <matt@sivel.net>
> +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 <adolf.belka@ipfire.org> 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/speedtest.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.
> +
> +-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 = isSet
> ++
> +
> + # Some global variables we use
> + DEBUG = False
> +@@ -56,6 +58,7 @@
> + PY25PLUS = sys.version_info[:2] >= (2, 5)
> + PY26PLUS = sys.version_info[:2] >= (2, 6)
> + PY32PLUS = sys.version_info[:2] >= (3, 2)
> ++PY310PLUS = sys.version_info[:2] >= (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 = ET.Element.iter
> +-elif PY25PLUS:
> +-    etree_iter = ET_Element.getiterator
> +-
> +-if PY26PLUS:
> +-    thread_is_alive = threading.Thread.is_alive
> +-else:
> +-    thread_is_alive = threading.Thread.isAlive
> +-
> +-
> + # Exception "constants" to support Python 2 through Python 3
> + try:
> +     import ssl
> +@@ -293,6 +285,23 @@
> +     ssl = None
> +     HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
> +
> ++if PY32PLUS:
> ++    etree_iter = ET.Element.iter
> ++elif PY25PLUS:
> ++    etree_iter = ET_Element.getiterator
> ++
> ++if PY26PLUS:
> ++    thread_is_alive = threading.Thread.is_alive
> ++else:
> ++    thread_is_alive = 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=False, end=False):
> +-        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) <= self.timeout:
> +                 f = 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) <=
> +                         self.timeout):
> +                     self.result.append(len(f.read(10240)))
> +@@ -864,7 +873,7 @@
> +
> +     def read(self, n=10240):
> +         if ((timeit.default_timer() - self.start) <= self.timeout and
> +-                not self._shutdown_event.isSet()):
> ++                not event_is_set(self._shutdown_event)):
> +             chunk = self.data.read(n)
> +             self.total.append(len(chunk))
> +             return chunk
> +@@ -902,7 +911,7 @@
> +         request = self.request
> +         try:
> +             if ((timeit.default_timer() - self.starttime) <= self.timeout and
> +-                    not self._shutdown_event.isSet()):
> ++                    not event_is_set(self._shutdown_event)):
> +                 try:
> +                     f = self._opener(request)
> +                 except TypeError:
> diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11_updates_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 <mark@there.co.nz>
> +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 <adolf.belka@ipfire.org> is the same as 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.
> +
> ++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 = codecs.open(os.path.join(here, *file_paths), 'r', 'latin1')
> ++        f = codecs.open(os.path.join(here, *file_paths), "r", "latin1")
> +         version_file = f.read()
> +         f.close()
> +-    except:
> ++    except Exception:
> +         raise RuntimeError("Unable to find version string.")
> +
> +     # The version line must have the form
> +     # __version__ = 'ver'
> +-    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
> +-                              version_file, re.M)
> ++    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 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 = codecs.open('README.rst', encoding='utf-8')
> ++    f = codecs.open("README.rst", encoding="utf-8")
> +     long_description = f.read()
> +     f.close()
> +-except:
> +-    long_description = ''
> ++except Exception:
> ++    long_description = ""
> +
> +
> + setup(
> +-    name='speedtest-cli',
> +-    version=find_version('speedtest.py'),
> +-    description=('Command line interface for testing internet bandwidth using '
> +-                 'speedtest.net'),
> ++    name="speedtest-cli",
> ++    version=find_version("speedtest.py"),
> ++    description=(
> ++        "Command line interface for testing internet bandwidth using " "speedtest.net"
> ++    ),
> +     long_description=long_description,
> +-    keywords='speedtest speedtest.net',
> +-    author='Matt Martz',
> +-    author_email='matt@sivel.net',
> +-    url='https://github.com/sivel/speedtest-cli',
> +-    license='Apache License, Version 2.0',
> +-    py_modules=['speedtest'],
> ++    keywords="speedtest speedtest.net",
> ++    author="Matt Martz",
> ++    author_email="matt@sivel.net",
> ++    url="https://github.com/sivel/speedtest-cli",
> ++    license="Apache License, Version 2.0",
> ++    py_modules=["speedtest"],
> +     entry_points={
> +-        'console_scripts': [
> +-            'speedtest=speedtest:main',
> +-            'speedtest-cli=speedtest:main'
> +-        ]
> ++        "console_scripts": [
> ++            "speedtest=speedtest:main",
> ++            "speedtest-cli=speedtest:main",
> ++        ],
> +     },
> +     classifiers=[
> +-        '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/speedtest.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 @@
> +
> + try:
> +     import gzip
> ++
> +     GZIP_BASE = gzip.GzipFile
> + except ImportError:
> +     gzip = None
> +     GZIP_BASE = object
> +
> +-__version__ = '2.1.3'
> ++__version__ = "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 = 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 = None
> +
> + 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, HTTPRedirectHandler,
> +-                                HTTPErrorProcessor, OpenerDirector)
> ++    from urllib.request import (AbstractHTTPHandler, HTTPDefaultErrorHandler,
> ++                                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 = int
> +     PARSER_TYPE_STR = str
> +     PARSER_TYPE_FLOAT = float
> + except ImportError:
> +-    from optparse import OptionParser as ArgParser
> +     from optparse import SUPPRESS_HELP as ARG_SUPPRESS
> +-    PARSER_TYPE_INT = 'int'
> +-    PARSER_TYPE_STR = 'string'
> +-    PARSER_TYPE_FLOAT = 'float'
> ++    from optparse import OptionParser as ArgParser
> ++
> ++    PARSER_TYPE_INT = "int"
> ++    PARSER_TYPE_STR = "string"
> ++    PARSER_TYPE_FLOAT = "float"
> +
> + try:
> +     from cStringIO import StringIO
> ++
> +     BytesIO = None
> + except ImportError:
> +     try:
> +         from StringIO import StringIO
> ++
> +         BytesIO = 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 = FileIO(f.fileno(), 'w')
> +-            super(_Py3Utf8Output, self).__init__(
> +-                buf,
> +-                encoding='utf8',
> +-                errors='strict'
> +-            )
> ++            buf = FileIO(f.fileno(), "w")
> ++            super().__init__(buf, encoding="utf8", errors="strict")
> +
> +         def write(self, s):
> +-            super(_Py3Utf8Output, self).write(s)
> ++            super().write(s)
> +             self.flush()
> +
> +-    _py3_print = getattr(builtins, 'print')
> ++    _py3_print = getattr(builtins, "print")
> +     try:
> +         _py3_utf8_stdout = _Py3Utf8Output(sys.stdout)
> +         _py3_utf8_stderr = _Py3Utf8Output(sys.stderr)
> +@@ -188,23 +192,24 @@
> +         _py3_utf8_stderr = 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') == sys.stderr:
> +-            kwargs['file'] = _py3_utf8_stderr
> ++        """Wrapper function for py3 to print, with a utf-8 encoded stdout."""
> ++        if kwargs.get("file") == sys.stderr:
> ++            kwargs["file"] = _py3_utf8_stderr
> +         else:
> +-            kwargs['file'] = kwargs.get('file', _py3_utf8_stdout)
> ++            kwargs["file"] = 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 = str(data)
> +             # If the file has an encoding, encode unicode with it.
> +-            encoding = 'utf8'  # Always trust UTF-8 for output
> +-            if (isinstance(fp, file) and
> +-                    isinstance(data, unicode) and
> +-                    encoding is not None):
> ++            encoding = "utf8"  # Always trust UTF-8 for output
> ++            if (
> ++                isinstance(fp, file)
> ++                and isinstance(data, unicode)
> ++                and encoding is not None
> ++            ):
> +                 errors = getattr(fp, "errors", None)
> +                 if errors is None:
> +                     errors = "strict"
> +                 data = data.encode(encoding, errors)
> +             fp.write(data)
> +             fp.flush()
> ++
> +         want_unicode = False
> +         sep = 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 = (ssl.CertificateError,)
> +     except AttributeError:
> +         CERT_ERROR = tuple()
> +
> +     HTTP_ERRORS = (
> +-        (HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) +
> +-        CERT_ERROR
> +-    )
> ++        HTTPError,
> ++        URLError,
> ++        socket.error,
> ++        ssl.SSLError,
> ++        BadStatusLine,
> ++    ) + CERT_ERROR
> + except ImportError:
> +     ssl = None
> +     HTTP_ERRORS = (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=_GLOBAL_DEFAULT_TIMEOUT,
> +-                      source_address=None):
> ++def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None):
> +     """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 = address
> +     err = 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 = kwargs.pop('source_address', None)
> +-        timeout = kwargs.pop('timeout', 10)
> ++        source_address = kwargs.pop("source_address", None)
> ++        timeout = kwargs.pop("timeout", 10)
> +
> +         self._tunnel_host = None
> +
> +@@ -435,13 +446,13 @@
> +             self.sock = socket.create_connection(
> +                 (self.host, self.port),
> +                 self.timeout,
> +-                self.source_address
> ++                self.source_address,
> +             )
> +         except (AttributeError, TypeError):
> +             self.sock = 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 = 443
> +
> +         def __init__(self, *args, **kwargs):
> +-            source_address = kwargs.pop('source_address', None)
> +-            timeout = kwargs.pop('timeout', 10)
> ++            source_address = kwargs.pop("source_address", None)
> ++            timeout = kwargs.pop("timeout", 10)
> +
> +             self._tunnel_host = None
> +
> +@@ -467,18 +480,18 @@
> +             self.source_address = 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 = socket.create_connection(
> +                     (self.host, self.port),
> +                     self.timeout,
> +-                    self.source_address
> ++                    self.source_address,
> +                 )
> +             except (AttributeError, TypeError):
> +                 self.sock = 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 = {}
> +-                    if hasattr(ssl, 'SSLContext'):
> ++                    if hasattr(ssl, "SSLContext"):
> +                         if self._tunnel_host:
> +-                            kwargs['server_hostname'] = self._tunnel_host
> ++                            kwargs["server_hostname"] = self._tunnel_host
> +                         else:
> +-                            kwargs['server_hostname'] = self.host
> ++                            kwargs["server_hostname"] = self.host
> +                     self.sock = self._context.wrap_socket(self.sock, **kwargs)
> +                 except AttributeError:
> +                     self.sock = ssl.wrap_socket(self.sock)
> +@@ -505,13 +518,13 @@
> +                     self.sock = FakeSocket(self.sock, socket.ssl(self.sock))
> +                 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",
> +                 )
> +
> +
> +@@ -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'] = context
> ++            kwargs["context"] = 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=0, source_address=None, timeout=10):
> +         AbstractHTTPHandler.__init__(self, debuglevel)
> +         self.source_address = source_address
> +@@ -547,9 +560,9 @@
> +             _build_connection(
> +                 SpeedtestHTTPConnection,
> +                 self.source_address,
> +-                self.timeout
> ++                self.timeout,
> +             ),
> +-            req
> ++            req,
> +         )
> +
> +     http_request = 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=0, context=None, source_address=None,
> +-                 timeout=10):
> ++
> ++    def __init__(self, debuglevel=0, context=None, source_address=None, timeout=10):
> +         AbstractHTTPHandler.__init__(self, debuglevel)
> +         self._context = context
> +         self.source_address = source_address
> +@@ -574,7 +587,7 @@
> +                 self.timeout,
> +                 context=self._context,
> +             ),
> +-            req
> ++            req,
> +         )
> +
> +     https_request = AbstractHTTPHandler.do_request_
> +@@ -586,29 +599,25 @@
> +     ``source_address`` for binding, ``timeout`` and our custom
> +     `User-Agent`
> +     """
> +-
> +-    printer('Timeout set to %d' % timeout, debug=True)
> ++    printer(f"Timeout set to {timeout}", debug=True)
> +
> +     if source_address:
> +         source_address_tuple = (source_address, 0)
> +-        printer('Binding to source address: %r' % (source_address_tuple,),
> +-                debug=True)
> ++        printer(f"Binding to source address: {source_address_tuple!r}", debug=True)
> +     else:
> +         source_address_tuple = None
> +
> +     handlers = [
> +         ProxyHandler(),
> +-        SpeedtestHTTPHandler(source_address=source_address_tuple,
> +-                             timeout=timeout),
> +-        SpeedtestHTTPSHandler(source_address=source_address_tuple,
> +-                              timeout=timeout),
> ++        SpeedtestHTTPHandler(source_address=source_address_tuple, timeout=timeout),
> ++        SpeedtestHTTPSHandler(source_address=source_address_tuple, timeout=timeout),
> +         HTTPDefaultErrorHandler(),
> +         HTTPRedirectHandler(),
> +-        HTTPErrorProcessor()
> ++        HTTPErrorProcessor(),
> +     ]
> +
> +     opener = OpenerDirector()
> +-    opener.addheaders = [('User-agent', build_user_agent())]
> ++    opener.addheaders = [("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 = BytesIO or StringIO
> +         self.io = IO()
> +         while 1:
> +@@ -637,7 +649,7 @@
> +                 break
> +             self.io.write(chunk)
> +         self.io.seek(0)
> +-        gzip.GzipFile.__init__(self, mode='rb', fileobj=self.io)
> ++        gzip.GzipFile.__init__(self, mode="rb", fileobj=self.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 = origin
> +     lat2, lon2 = destination
> +     radius = 6371  # km
> +
> +     dlat = math.radians(lat2 - lat1)
> +     dlon = math.radians(lon2 - lon1)
> +-    a = (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 = 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 = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
> +     d = radius * c
> +
> +@@ -674,52 +684,47 @@
> +
> + def build_user_agent():
> +     """Build a Mozilla/5.0 compatible User-Agent string"""
> +-
> +     ua_tuple = (
> +-        '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 = ' '.join(ua_tuple)
> +-    printer('User-Agent: %s' % user_agent, debug=True)
> ++    user_agent = " ".join(ua_tuple)
> ++    printer(f"User-Agent: {user_agent}", debug=True)
> +     return user_agent
> +
> +
> +-def build_request(url, data=None, headers=None, bump='0', secure=False):
> ++def build_request(url, data=None, headers=None, bump="0", secure=False):
> +     """Build a urllib2 request object
> +
> +     This function automatically adds a User-Agent header to all requests
> +-
> +     """
> +-
> +     if not headers:
> +         headers = {}
> +
> +-    if url[0] == ':':
> +-        scheme = ('http', 'https')[bool(secure)]
> +-        schemed_url = '%s%s' % (scheme, url)
> ++    if url[0] == ":":
> ++        scheme = ("http", "https")[bool(secure)]
> ++        schemed_url = f"{scheme}{url}"
> +     else:
> +         schemed_url = url
> +
> +-    if '?' in url:
> +-        delim = '&'
> ++    if "?" in url:
> ++        delim = "&"
> +     else:
> +-        delim = '?'
> ++        delim = "?"
> +
> +     # WHO YOU GONNA CALL? CACHE BUSTERS!
> +-    final_url = '%s%sx=%s.%s' % (schemed_url, delim,
> +-                                 int(timeit.time.time() * 1000),
> +-                                 bump)
> +-
> +-    headers.update({
> +-        'Cache-Control': 'no-cache',
> +-    })
> ++    final_url = f"{schemed_url}{delim}x={int(timeit.time.time() * 1000)}.{bump}"
> ++
> ++    headers.update(
> ++        {
> ++            "Cache-Control": "no-cache",
> ++        },
> ++    )
> +
> +-    printer('%s %s' % (('GET', 'POST')[bool(data)], final_url),
> +-            debug=True)
> ++    printer(f"{('GET', 'POST')[bool(data)]} {final_url}", debug=True)
> +
> +     return Request(final_url, data=data, headers=headers)
> +
> +@@ -729,7 +734,6 @@
> +     establishing a connection with a HTTP/HTTPS request
> +
> +     """
> +-
> +     if opener:
> +         _open = opener.open
> +     else:
> +@@ -738,7 +742,7 @@
> +     try:
> +         uh = _open(request)
> +         if request.get_full_url() != uh.geturl():
> +-            printer('Redirected to %s' % uh.geturl(), debug=True)
> ++            printer(f"Redirected to {uh.geturl()}", debug=True)
> +         return uh, False
> +     except HTTP_ERRORS:
> +         e = get_exception()
> +@@ -750,13 +754,12 @@
> +     ``Content-Encoding`` is ``gzip`` otherwise the response itself
> +
> +     """
> +-
> +     try:
> +         getheader = response.headers.getheader
> +     except AttributeError:
> +         getheader = response.getheader
> +
> +-    if getheader('content-encoding') == 'gzip':
> ++    if getheader("content-encoding") == "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=False, end=False):
> +         if event_is_set(shutdown_event):
> +             return
> +
> +-        sys.stdout.write('.')
> ++        sys.stdout.write(".")
> +         if current + 1 == 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=None,
> +-                 shutdown_event=None):
> ++    def __init__(self, i, request, start, timeout, opener=None, shutdown_event=None):
> +         threading.Thread.__init__(self)
> +         self.request = request
> +         self.result = [0]
> +@@ -817,9 +821,10 @@
> +         try:
> +             if (timeit.default_timer() - self.starttime) <= self.timeout:
> +                 f = self._opener(self.request)
> +-                while (not event_is_set(self._shutdown_event) and
> +-                        (timeit.default_timer() - self.starttime) <=
> +-                        self.timeout):
> ++                while (
> ++                    not event_is_set(self._shutdown_event)
> ++                    and (timeit.default_timer() - self.starttime) <= self.timeout
> ++                ):
> +                     self.result.append(len(f.read(10240)))
> +                     if self.result[-1] == 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 = [0]
> +
> +     def pre_allocate(self):
> +-        chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
> ++        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
> +         multiplier = int(round(int(self.length) / 36.0))
> +         IO = BytesIO or StringIO
> +         try:
> +             self._data = IO(
> +-                ('content1=%s' %
> +-                 (chars * multiplier)[0:int(self.length) - 9]
> +-                 ).encode()
> ++                (f"content1={(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=10240):
> +-        if ((timeit.default_timer() - self.start) <= self.timeout and
> +-                not event_is_set(self._shutdown_event)):
> ++        if (timeit.default_timer() - self.start) <= self.timeout and not event_is_set(
> ++            self._shutdown_event,
> ++        ):
> +             chunk = 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=None,
> +-                 shutdown_event=None):
> ++    def __init__(
> ++        self,
> ++        i,
> ++        request,
> ++        start,
> ++        size,
> ++        timeout,
> ++        opener=None,
> ++        shutdown_event=None,
> ++    ):
> +         threading.Thread.__init__(self)
> +         self.request = request
> +         self.request.data.start = self.starttime = start
> +@@ -910,16 +921,19 @@
> +     def run(self):
> +         request = self.request
> +         try:
> +-            if ((timeit.default_timer() - self.starttime) <= self.timeout and
> +-                    not event_is_set(self._shutdown_event)):
> ++            if (
> ++                timeit.default_timer() - self.starttime
> ++            ) <= self.timeout and not event_is_set(self._shutdown_event):
> +                 try:
> +                     f = self._opener(request)
> +                 except TypeError:
> +                     # PY24 expects a string or buffer
> +                     # This also causes issues with Ctrl-C, but we will concede
> +                     # for the moment that Ctrl-C on PY24 isn't immediate
> +-                    request = build_request(self.request.get_full_url(),
> +-                                            data=request.data.read(self.size))
> ++                    request = build_request(
> ++                        self.request.get_full_url(),
> ++                        data=request.data.read(self.size),
> ++                    )
> +                     f = self._opener(request)
> +                 f.read(11)
> +                 f.close()
> +@@ -932,7 +946,7 @@
> +             self.result = 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=0, upload=0, ping=0, server=None, client=None,
> +-                 opener=None, secure=False):
> ++    def __init__(
> ++        self,
> ++        download=0,
> ++        upload=0,
> ++        ping=0,
> ++        server=None,
> ++        client=None,
> ++        opener=None,
> ++        secure=False,
> ++    ):
> +         self.download = download
> +         self.upload = upload
> +         self.ping = ping
> +@@ -957,7 +979,7 @@
> +         self.client = client or {}
> +
> +         self._share = None
> +-        self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat()
> ++        self.timestamp = f"{datetime.datetime.utcnow().isoformat()}Z"
> +         self.bytes_received = 0
> +         self.bytes_sent = 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 parameters
> +         # in a certain order
> +         api_data = [
> +-            'recommendedserverid=%s' % self.server['id'],
> +-            'ping=%s' % ping,
> +-            'screenresolution=',
> +-            'promo=',
> +-            'download=%s' % download,
> +-            'screendpi=',
> +-            'upload=%s' % upload,
> +-            'testmethod=http',
> +-            'hash=%s' % md5(('%s-%s-%s-%s' %
> +-                             (ping, upload, download, '297aae72'))
> +-                            .encode()).hexdigest(),
> +-            'touchscreen=none',
> +-            'startmode=pingselect',
> +-            'accuracy=1',
> +-            'bytesreceived=%s' % self.bytes_received,
> +-            'bytessent=%s' % self.bytes_sent,
> +-            'serverid=%s' % self.server['id'],
> ++            f"recommendedserverid={self.server['id']}",
> ++            f"ping={ping}",
> ++            "screenresolution=",
> ++            "promo=",
> ++            f"download={download}",
> ++            "screendpi=",
> ++            f"upload={upload}",
> ++            "testmethod=http",
> ++            "hash=%s"
> ++            % md5(
> ++                ("%s-%s-%s-%s" % (ping, upload, download, "297aae72")).encode(),
> ++            ).hexdigest(),
> ++            "touchscreen=none",
> ++            "startmode=pingselect",
> ++            "accuracy=1",
> ++            f"bytesreceived={self.bytes_received}",
> ++            f"bytessent={self.bytes_sent}",
> ++            f"serverid={self.server['id']}",
> +         ]
> +
> +-        headers = {'Referer': 'http://c.speedtest.net/flash/speedtest.swf'}
> +-        request = build_request('://www.speedtest.net/api/api.php',
> +-                                data='&'.join(api_data).encode(),
> +-                                headers=headers, secure=self._secure)
> ++        headers = {"Referer": "http://c.speedtest.net/flash/speedtest.swf"}
> ++        request = build_request(
> ++            "://www.speedtest.net/api/api.php",
> ++            data="&".join(api_data).encode(),
> ++            headers=headers,
> ++            secure=self._secure,
> ++        )
> +         f, e = catch_request(request, opener=self._opener)
> +         if e:
> +             raise ShareResultsConnectFailure(e)
> +@@ -1019,75 +1044,94 @@
> +         f.close()
> +
> +         if int(code) != 200:
> +-            raise ShareResultsSubmitFailure('Could not submit results to '
> +-                                            'speedtest.net')
> ++            raise ShareResultsSubmitFailure(
> ++                "Could not submit results to " "speedtest.net",
> ++            )
> +
> +         qsargs = parse_qs(response.decode())
> +-        resultid = qsargs.get('resultid')
> ++        resultid = qsargs.get("resultid")
> +         if not resultid or len(resultid) != 1:
> +-            raise ShareResultsSubmitFailure('Could not submit results to '
> +-                                            'speedtest.net')
> ++            raise ShareResultsSubmitFailure(
> ++                "Could not submit results to " "speedtest.net",
> ++            )
> +
> +-        self._share = 'http://www.speedtest.net/result/%s.png' % resultid[0]
> ++        self._share = f"http://www.speedtest.net/result/{resultid[0]}.png"
> +
> +         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=','):
> ++    def csv_header(delimiter=","):
> +         """Return CSV Headers"""
> +-
> +-        row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance',
> +-               'Ping', 'Download', 'Upload', 'Share', 'IP Address']
> ++        row = [
> ++            "Server ID",
> ++            "Sponsor",
> ++            "Server Name",
> ++            "Timestamp",
> ++            "Distance",
> ++            "Ping",
> ++            "Download",
> ++            "Upload",
> ++            "Share",
> ++            "IP Address",
> ++        ]
> +         out = StringIO()
> +-        writer = csv.writer(out, delimiter=delimiter, lineterminator='')
> ++        writer = csv.writer(out, delimiter=delimiter, lineterminator="")
> +         writer.writerow([to_utf8(v) for v in row])
> +         return out.getvalue()
> +
> +-    def csv(self, delimiter=','):
> ++    def csv(self, delimiter=","):
> +         """Return data in CSV format"""
> +-
> +         data = self.dict()
> +         out = StringIO()
> +-        writer = csv.writer(out, delimiter=delimiter, lineterminator='')
> +-        row = [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 = csv.writer(out, delimiter=delimiter, lineterminator="")
> ++        row = [
> ++            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=False):
> +         """Return data in JSON format"""
> +-
> +         kwargs = {}
> +         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=None, source_address=None, timeout=10,
> +-                 secure=False, shutdown_event=None):
> ++    def __init__(
> ++        self,
> ++        config=None,
> ++        source_address=None,
> ++        timeout=10,
> ++        secure=False,
> ++        shutdown_event=None,
> ++    ):
> +         self.config = {}
> +
> +         self._source_address = source_address
> +@@ -1110,7 +1154,7 @@
> +         self._best = {}
> +
> +         self.results = SpeedtestResults(
> +-            client=self.config['client'],
> ++            client=self.config["client"],
> +             opener=self._opener,
> +             secure=secure,
> +         )
> +@@ -1125,12 +1169,14 @@
> +         """Download the speedtest.net configuration and return only the data
> +         we are interested in
> +         """
> +-
> +         headers = {}
> +         if gzip:
> +-            headers['Accept-Encoding'] = 'gzip'
> +-        request = build_request('://www.speedtest.net/speedtest-config.php',
> +-                                headers=headers, secure=self._secure)
> ++            headers["Accept-Encoding"] = "gzip"
> ++        request = build_request(
> ++            "://www.speedtest.net/speedtest-config.php",
> ++            headers=headers,
> ++            secure=self._secure,
> ++        )
> +         uh, e = catch_request(request, opener=self._opener)
> +         if e:
> +             raise ConfigRetrievalError(e)
> +@@ -1151,9 +1197,9 @@
> +         if int(uh.code) != 200:
> +             return None
> +
> +-        configxml = ''.encode().join(configxml_list)
> ++        configxml = "".encode().join(configxml_list)
> +
> +-        printer('Config XML:\n%s' % configxml, debug=True)
> ++        printer(f"Config XML:\n{configxml}", debug=True)
> +
> +         try:
> +             try:
> +@@ -1161,13 +1207,13 @@
> +             except ET.ParseError:
> +                 e = get_exception()
> +                 raise SpeedtestConfigError(
> +-                    'Malformed speedtest.net configuration: %s' % e
> ++                    f"Malformed speedtest.net configuration: {e}",
> +                 )
> +-            server_config = root.find('server-config').attrib
> +-            download = root.find('download').attrib
> +-            upload = root.find('upload').attrib
> ++            server_config = root.find("server-config").attrib
> ++            download = root.find("download").attrib
> ++            upload = root.find("upload").attrib
> +             # times = root.find('times').attrib
> +-            client = root.find('client').attrib
> ++            client = root.find("client").attrib
> +
> +         except AttributeError:
> +             try:
> +@@ -1175,65 +1221,61 @@
> +             except ExpatError:
> +                 e = get_exception()
> +                 raise SpeedtestConfigError(
> +-                    'Malformed speedtest.net configuration: %s' % e
> ++                    f"Malformed speedtest.net configuration: {e}",
> +                 )
> +-            server_config = get_attributes_by_tag_name(root, 'server-config')
> +-            download = get_attributes_by_tag_name(root, 'download')
> +-            upload = get_attributes_by_tag_name(root, 'upload')
> ++            server_config = get_attributes_by_tag_name(root, "server-config")
> ++            download = get_attributes_by_tag_name(root, "download")
> ++            upload = get_attributes_by_tag_name(root, "upload")
> +             # times = get_attributes_by_tag_name(root, 'times')
> +-            client = get_attributes_by_tag_name(root, 'client')
> ++            client = get_attributes_by_tag_name(root, "client")
> +
> +-        ignore_servers = [
> +-            int(i) for i in server_config['ignoreids'].split(',') if i
> +-        ]
> ++        ignore_servers = [int(i) for i in server_config["ignoreids"].split(",") if i]
> +
> +-        ratio = int(upload['ratio'])
> +-        upload_max = int(upload['maxchunkcount'])
> ++        ratio = int(upload["ratio"])
> ++        upload_max = int(upload["maxchunkcount"])
> +         up_sizes = [32768, 65536, 131072, 262144, 524288, 1048576, 7340032]
> +         sizes = {
> +-            '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],
> +         }
> +
> +-        size_count = len(sizes['upload'])
> ++        size_count = len(sizes["upload"])
> +
> +         upload_count = int(math.ceil(upload_max / size_count))
> +
> +-        counts = {
> +-            'upload': upload_count,
> +-            'download': int(download['threadsperurl'])
> +-        }
> ++        counts = {"upload": upload_count, "download": int(download["threadsperurl"])}
> +
> +         threads = {
> +-            'upload': int(upload['threads']),
> +-            'download': int(server_config['threadcount']) * 2
> ++            "upload": int(upload["threads"]),
> ++            "download": int(server_config["threadcount"]) * 2,
> +         }
> +
> +         length = {
> +-            '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 = (float(client['lat']), float(client['lon']))
> ++            self.lat_lon = (float(client["lat"]), float(client["lon"]))
> +         except ValueError:
> +             raise SpeedtestConfigError(
> +-                'Unknown location: lat=%r lon=%r' %
> +-                (client.get('lat'), client.get('lon'))
> ++                "Unknown location: lat=%r lon=%r"
> ++                % (client.get("lat"), client.get("lon")),
> +             )
> +
> +-        printer('Config:\n%r' % self.config, debug=True)
> ++        printer(f"Config:\n{self.config!r}", debug=True)
> +
> +         return self.config
> +
> +@@ -1255,32 +1297,31 @@
> +                     server_list[i] = 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 = [
> +-            '://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 = {}
> +         if gzip:
> +-            headers['Accept-Encoding'] = 'gzip'
> ++            headers["Accept-Encoding"] = "gzip"
> +
> +         errors = []
> +         for url in urls:
> +             try:
> +                 request = build_request(
> +-                    '%s?threads=%s' % (url,
> +-                                       self.config['threads']['download']),
> ++                    f"{url}?threads={self.config['threads']['download']}",
> +                     headers=headers,
> +-                    secure=self._secure
> ++                    secure=self._secure,
> +                 )
> +                 uh, e = catch_request(request, opener=self._opener)
> +                 if e:
> +-                    errors.append('%s' % e)
> ++                    errors.append(f"{e}")
> +                     raise ServersRetrievalError()
> +
> +                 stream = get_response_stream(uh)
> +@@ -1300,9 +1341,9 @@
> +                 if int(uh.code) != 200:
> +                     raise ServersRetrievalError()
> +
> +-                serversxml = ''.encode().join(serversxml_list)
> ++                serversxml = "".encode().join(serversxml_list)
> +
> +-                printer('Servers XML:\n%s' % serversxml, debug=True)
> ++                printer(f"Servers XML:\n{serversxml}", debug=True)
> +
> +                 try:
> +                     try:
> +@@ -1311,18 +1352,18 @@
> +                         except ET.ParseError:
> +                             e = get_exception()
> +                             raise SpeedtestServersError(
> +-                                'Malformed speedtest.net server list: %s' % e
> ++                                f"Malformed speedtest.net server list: {e}",
> +                             )
> +-                        elements = etree_iter(root, 'server')
> ++                        elements = etree_iter(root, "server")
> +                     except AttributeError:
> +                         try:
> +                             root = DOM.parseString(serversxml)
> +                         except ExpatError:
> +                             e = get_exception()
> +                             raise SpeedtestServersError(
> +-                                'Malformed speedtest.net server list: %s' % e
> ++                                f"Malformed speedtest.net server list: {e}",
> +                             )
> +-                        elements = root.getElementsByTagName('server')
> ++                        elements = root.getElementsByTagName("server")
> +                 except (SyntaxError, xml.parsers.expat.ExpatError):
> +                     raise ServersRetrievalError()
> +
> +@@ -1332,21 +1373,24 @@
> +                     except AttributeError:
> +                         attrib = 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_servers']
> +-                            or int(attrib.get('id')) in exclude):
> ++                    if (
> ++                        int(attrib.get("id")) in self.config["ignore_servers"]
> ++                        or int(attrib.get("id")) in exclude
> ++                    ):
> +                         continue
> +
> +                     try:
> +-                        d = distance(self.lat_lon,
> +-                                     (float(attrib.get('lat')),
> +-                                      float(attrib.get('lon'))))
> ++                        d = distance(
> ++                            self.lat_lon,
> ++                            (float(attrib.get("lat")), float(attrib.get("lon"))),
> ++                        )
> +                     except Exception:
> +                         continue
> +
> +-                    attrib['d'] = d
> ++                    attrib["d"] = 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 = urlparse(server)
> +
> +         name, ext = os.path.splitext(urlparts[2])
> +@@ -1379,41 +1422,41 @@
> +         request = build_request(url)
> +         uh, e = catch_request(request, opener=self._opener)
> +         if e:
> +-            raise SpeedtestMiniConnectFailure('Failed to connect to %s' %
> +-                                              server)
> +-        else:
> +-            text = uh.read()
> +-            uh.close()
> ++            raise SpeedtestMiniConnectFailure(f"Failed to connect to {server}")
> ++        text = uh.read()
> ++        uh.close()
> +
> +-        extension = re.findall('upload_?[Ee]xtension: "([^"]+)"',
> +-                               text.decode())
> ++        extension = 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 = self._opener.open(
> +-                        '%s/speedtest/upload.%s' % (url, ext)
> +-                    )
> ++                    f = self._opener.open(f"{url}/speedtest/upload.{ext}")
> +                 except Exception:
> +                     pass
> +                 else:
> +                     data = f.read().strip().decode()
> +-                    if (f.code == 200 and
> +-                            len(data.splitlines()) == 1 and
> +-                            re.match('size=[0-9]', data)):
> ++                    if (
> ++                        f.code == 200
> ++                        and len(data.splitlines()) == 1
> ++                        and re.match("size=[0-9]", data)
> ++                    ):
> +                         extension = [ext]
> +                         break
> +         if not urlparts or not extension:
> +-            raise InvalidSpeedtestMiniServer('Invalid Speedtest Mini Server: '
> +-                                             '%s' % server)
> ++            raise InvalidSpeedtestMiniServer(
> ++                "Invalid Speedtest Mini Server: " "%s" % server,
> ++            )
> +
> +-        self.servers = [{
> +-            'sponsor': 'Speedtest Mini',
> +-            'name': urlparts[1],
> +-            'd': 0,
> +-            'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]),
> +-            'latency': 0,
> +-            'id': 0
> +-        }]
> ++        self.servers = [
> ++            {
> ++                "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=True)
> ++        printer(f"Closest Servers:\n{self.closest!r}", debug=True)
> +         return self.closest
> +
> +     def get_best_server(self, servers=None):
> +         """Perform a speedtest.net "ping" to determine which speedtest.net
> +         server has the lowest latency
> +         """
> +-
> +         if not servers:
> +             if not self.closest:
> +                 servers = self.get_closest_servers()
> +@@ -1457,39 +1498,38 @@
> +         results = {}
> +         for server in servers:
> +             cum = []
> +-            url = os.path.dirname(server['url'])
> ++            url = os.path.dirname(server["url"])
> +             stamp = int(timeit.time.time() * 1000)
> +-            latency_url = '%s/latency.txt?x=%s' % (url, stamp)
> ++            latency_url = f"{url}/latency.txt?x={stamp}"
> +             for i in range(0, 3):
> +-                this_latency_url = '%s.%s' % (latency_url, i)
> +-                printer('%s %s' % ('GET', this_latency_url),
> +-                        debug=True)
> ++                this_latency_url = f"{latency_url}.{i}"
> ++                printer(f"{'GET'} {this_latency_url}", debug=True)
> +                 urlparts = urlparse(latency_url)
> +                 try:
> +-                    if urlparts[0] == 'https':
> ++                    if urlparts[0] == "https":
> +                         h = SpeedtestHTTPSConnection(
> +                             urlparts[1],
> +-                            source_address=source_address_tuple
> ++                            source_address=source_address_tuple,
> +                         )
> +                     else:
> +                         h = SpeedtestHTTPConnection(
> +                             urlparts[1],
> +-                            source_address=source_address_tuple
> ++                            source_address=source_address_tuple,
> +                         )
> +-                    headers = {'User-Agent': user_agent}
> +-                    path = '%s?%s' % (urlparts[2], urlparts[4])
> ++                    headers = {"User-Agent": user_agent}
> ++                    path = f"{urlparts[2]}?{urlparts[4]}"
> +                     start = timeit.default_timer()
> +                     h.request("GET", path, headers=headers)
> +                     r = h.getresponse()
> +-                    total = (timeit.default_timer() - start)
> ++                    total = timeit.default_timer() - start
> +                 except HTTP_ERRORS:
> +                     e = get_exception()
> +-                    printer('ERROR: %r' % e, debug=True)
> ++                    printer(f"ERROR: {e!r}", debug=True)
> +                     cum.append(3600)
> +                     continue
> +
> +                 text = r.read(9)
> +-                if int(r.status) == 200 and text == 'test=test'.encode():
> ++                if int(r.status) == 200 and text == "test=test".encode():
> +                     cum.append(total)
> +                 else:
> +                     cum.append(3600)
> +@@ -1501,16 +1541,17 @@
> +         try:
> +             fastest = 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 = results[fastest]
> +-        best['latency'] = fastest
> ++        best["latency"] = fastest
> +
> +         self.results.ping = fastest
> +         self.results.server = best
> +
> +         self._best.update(best)
> +-        printer('Best Server:\n%r' % best, debug=True)
> ++        printer(f"Best Server:\n{best!r}", debug=True)
> +         return best
> +
> +     def download(self, callback=do_nothing, threads=None):
> +@@ -1519,22 +1560,21 @@
> +         A ``threads`` value of ``None`` will fall back to those dictated
> +         by the speedtest.net configuration
> +         """
> +-
> +         urls = []
> +-        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),
> ++                )
> +
> +         request_count = len(urls)
> +         requests = []
> +         for i, url in enumerate(urls):
> +-            requests.append(
> +-                build_request(url, bump=i, secure=self._secure)
> +-            )
> ++            requests.append(build_request(url, bump=i, secure=self._secure))
> +
> +-        max_threads = threads or self.config['threads']['download']
> +-        in_flight = {'threads': 0}
> ++        max_threads = threads or self.config["threads"]["download"]
> ++        in_flight = {"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=self._opener,
> +-                    shutdown_event=self._shutdown_event
> ++                    shutdown_event=self._shutdown_event,
> +                 )
> +-                while in_flight['threads'] >= max_threads:
> ++                while in_flight["threads"] >= max_threads:
> +                     timeit.time.sleep(0.001)
> +                 thread.start()
> +                 q.put(thread, True)
> +-                in_flight['threads'] += 1
> ++                in_flight["threads"] += 1
> +                 callback(i, request_count, start=True)
> +
> +         finished = []
> +@@ -1561,15 +1601,16 @@
> +                 thread = q.get(True)
> +                 while _is_alive(thread):
> +                     thread.join(timeout=0.001)
> +-                in_flight['threads'] -= 1
> ++                in_flight["threads"] -= 1
> +                 finished.append(sum(thread.result))
> +                 callback(thread.i, request_count, end=True)
> +
> +         q = Queue(max_threads)
> +-        prod_thread = threading.Thread(target=producer,
> +-                                       args=(q, requests, request_count))
> +-        cons_thread = threading.Thread(target=consumer,
> +-                                       args=(q, request_count))
> ++        prod_thread = threading.Thread(
> ++            target=producer,
> ++            args=(q, requests, request_count),
> ++        )
> ++        cons_thread = threading.Thread(target=consumer, args=(q, request_count))
> +         start = timeit.default_timer()
> +         prod_thread.start()
> +         cons_thread.start()
> +@@ -1581,11 +1622,9 @@
> +
> +         stop = timeit.default_timer()
> +         self.results.bytes_received = sum(finished)
> +-        self.results.download = (
> +-            (self.results.bytes_received / (stop - start)) * 8.0
> +-        )
> ++        self.results.download = (self.results.bytes_received / (stop - start)) * 8.0
> +         if self.results.download > 100000:
> +-            self.config['threads']['upload'] = 8
> ++            self.config["threads"]["upload"] = 8
> +         return self.results.download
> +
> +     def upload(self, callback=do_nothing, pre_allocate=True, threads=None):
> +@@ -1594,40 +1633,43 @@
> +         A ``threads`` value of ``None`` will fall back to those dictated
> +         by the speedtest.net configuration
> +         """
> +-
> +         sizes = []
> +
> +-        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 = len(sizes)
> +-        request_count = self.config['upload_max']
> ++        request_count = self.config["upload_max"]
> +
> +         requests = []
> +-        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 = HTTPUploaderData(
> +                 size,
> +                 0,
> +-                self.config['length']['upload'],
> +-                shutdown_event=self._shutdown_event
> ++                self.config["length"]["upload"],
> ++                shutdown_event=self._shutdown_event,
> +             )
> +             if pre_allocate:
> +                 data.pre_allocate()
> +
> +-            headers = {'Content-length': size}
> ++            headers = {"Content-length": size}
> +             requests.append(
> +                 (
> +-                    build_request(self.best['url'], data, secure=self._secure,
> +-                                  headers=headers),
> +-                    size
> +-                )
> ++                    build_request(
> ++                        self.best["url"],
> ++                        data,
> ++                        secure=self._secure,
> ++                        headers=headers,
> ++                    ),
> ++                    size,
> ++                ),
> +             )
> +
> +-        max_threads = threads or self.config['threads']['upload']
> +-        in_flight = {'threads': 0}
> ++        max_threads = threads or self.config["threads"]["upload"]
> ++        in_flight = {"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=self._opener,
> +-                    shutdown_event=self._shutdown_event
> ++                    shutdown_event=self._shutdown_event,
> +                 )
> +-                while in_flight['threads'] >= max_threads:
> ++                while in_flight["threads"] >= max_threads:
> +                     timeit.time.sleep(0.001)
> +                 thread.start()
> +                 q.put(thread, True)
> +-                in_flight['threads'] += 1
> ++                in_flight["threads"] += 1
> +                 callback(i, request_count, start=True)
> +
> +         finished = []
> +@@ -1655,15 +1697,16 @@
> +                 thread = q.get(True)
> +                 while _is_alive(thread):
> +                     thread.join(timeout=0.001)
> +-                in_flight['threads'] -= 1
> ++                in_flight["threads"] -= 1
> +                 finished.append(thread.result)
> +                 callback(thread.i, request_count, end=True)
> +
> +-        q = Queue(threads or self.config['threads']['upload'])
> +-        prod_thread = threading.Thread(target=producer,
> +-                                       args=(q, requests, request_count))
> +-        cons_thread = threading.Thread(target=consumer,
> +-                                       args=(q, request_count))
> ++        q = Queue(threads or self.config["threads"]["upload"])
> ++        prod_thread = threading.Thread(
> ++            target=producer,
> ++            args=(q, requests, request_count),
> ++        )
> ++        cons_thread = threading.Thread(target=consumer, args=(q, request_count))
> +         start = timeit.default_timer()
> +         prod_thread.start()
> +         cons_thread.start()
> +@@ -1675,9 +1718,7 @@
> +
> +         stop = timeit.default_timer()
> +         self.results.bytes_sent = sum(finished)
> +-        self.results.upload = (
> +-            (self.results.bytes_sent / (stop - start)) * 8.0
> +-        )
> ++        self.results.upload = (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=True)
> ++        printer("\nCancelling...", error=True)
> +         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=','):
> ++def csv_header(delimiter=","):
> +     """Print the CSV Headers"""
> +-
> +     printer(SpeedtestResults.csv_header(delimiter=delimiter))
> +     sys.exit(0)
> +
> +@@ -1710,11 +1751,12 @@
> + def parse_args():
> +     """Function to handle building and parsing of command line arguments"""
> +     description = (
> +-        '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 = ArgParser(description=description)
> +     # Give optparse.OptionParser an `add_argument` method for
> +@@ -1723,67 +1765,134 @@
> +         parser.add_argument = parser.add_option
> +     except AttributeError:
> +         pass
> +-    parser.add_argument('--no-download', dest='download', default=True,
> +-                        action='store_const', const=False,
> +-                        help='Do not perform download test')
> +-    parser.add_argument('--no-upload', dest='upload', default=True,
> +-                        action='store_const', const=False,
> +-                        help='Do not perform upload test')
> +-    parser.add_argument('--single', default=False, action='store_true',
> +-                        help='Only use a single connection instead of '
> +-                             'multiple. This simulates a typical file '
> +-                             'transfer.')
> +-    parser.add_argument('--bytes', dest='units', action='store_const',
> +-                        const=('byte', 8), default=('bit', 1),
> +-                        help='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='store_true',
> +-                        help='Generate and provide a URL to the speedtest.net '
> +-                             'share results image, not displayed with --csv')
> +-    parser.add_argument('--simple', action='store_true', default=False,
> +-                        help='Suppress verbose output, only show basic '
> +-                             'information')
> +-    parser.add_argument('--csv', action='store_true', default=False,
> +-                        help='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=',', type=PARSER_TYPE_STR,
> +-                        help='Single character delimiter to use in CSV '
> +-                             'output. Default ","')
> +-    parser.add_argument('--csv-header', action='store_true', default=False,
> +-                        help='Print CSV headers')
> +-    parser.add_argument('--json', action='store_true', default=False,
> +-                        help='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='store_true',
> +-                        help='Display a list of speedtest.net servers '
> +-                             'sorted by distance')
> +-    parser.add_argument('--server', type=PARSER_TYPE_INT, action='append',
> +-                        help='Specify a server ID to test against. Can be '
> +-                             'supplied multiple times')
> +-    parser.add_argument('--exclude', type=PARSER_TYPE_INT, action='append',
> +-                        help='Exclude a server from selection. Can be '
> +-                             'supplied multiple times')
> +-    parser.add_argument('--mini', help='URL of the Speedtest Mini server')
> +-    parser.add_argument('--source', help='Source IP address to bind to')
> +-    parser.add_argument('--timeout', default=10, type=PARSER_TYPE_FLOAT,
> +-                        help='HTTP timeout in seconds. Default 10')
> +-    parser.add_argument('--secure', action='store_true',
> +-                        help='Use HTTPS instead of HTTP when communicating '
> +-                             'with speedtest.net operated servers')
> +-    parser.add_argument('--no-pre-allocate', dest='pre_allocate',
> +-                        action='store_const', default=True, const=False,
> +-                        help='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='store_true',
> +-                        help='Show the version number and exit')
> +-    parser.add_argument('--debug', action='store_true',
> +-                        help=ARG_SUPPRESS, default=ARG_SUPPRESS)
> ++    parser.add_argument(
> ++        "--no-download",
> ++        dest="download",
> ++        default=True,
> ++        action="store_const",
> ++        const=False,
> ++        help="Do not perform download test",
> ++    )
> ++    parser.add_argument(
> ++        "--no-upload",
> ++        dest="upload",
> ++        default=True,
> ++        action="store_const",
> ++        const=False,
> ++        help="Do not perform upload test",
> ++    )
> ++    parser.add_argument(
> ++        "--single",
> ++        default=False,
> ++        action="store_true",
> ++        help="Only use a single connection instead of "
> ++        "multiple. This simulates a typical file "
> ++        "transfer.",
> ++    )
> ++    parser.add_argument(
> ++        "--bytes",
> ++        dest="units",
> ++        action="store_const",
> ++        const=("byte", 8),
> ++        default=("bit", 1),
> ++        help="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="store_true",
> ++        help="Generate and provide a URL to the speedtest.net "
> ++        "share results image, not displayed with --csv",
> ++    )
> ++    parser.add_argument(
> ++        "--simple",
> ++        action="store_true",
> ++        default=False,
> ++        help="Suppress verbose output, only show basic " "information",
> ++    )
> ++    parser.add_argument(
> ++        "--csv",
> ++        action="store_true",
> ++        default=False,
> ++        help="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=",",
> ++        type=PARSER_TYPE_STR,
> ++        help="Single character delimiter to use in CSV " 'output. Default ","',
> ++    )
> ++    parser.add_argument(
> ++        "--csv-header",
> ++        action="store_true",
> ++        default=False,
> ++        help="Print CSV headers",
> ++    )
> ++    parser.add_argument(
> ++        "--json",
> ++        action="store_true",
> ++        default=False,
> ++        help="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="store_true",
> ++        help="Display a list of speedtest.net servers " "sorted by distance",
> ++    )
> ++    parser.add_argument(
> ++        "--server",
> ++        type=PARSER_TYPE_INT,
> ++        action="append",
> ++        help="Specify a server ID to test against. Can be " "supplied multiple times",
> ++    )
> ++    parser.add_argument(
> ++        "--exclude",
> ++        type=PARSER_TYPE_INT,
> ++        action="append",
> ++        help="Exclude a server from selection. Can be " "supplied multiple times",
> ++    )
> ++    parser.add_argument("--mini", help="URL of the Speedtest Mini server")
> ++    parser.add_argument("--source", help="Source IP address to bind to")
> ++    parser.add_argument(
> ++        "--timeout",
> ++        default=10,
> ++        type=PARSER_TYPE_FLOAT,
> ++        help="HTTP timeout in seconds. Default 10",
> ++    )
> ++    parser.add_argument(
> ++        "--secure",
> ++        action="store_true",
> ++        help="Use HTTPS instead of HTTP when communicating "
> ++        "with speedtest.net operated servers",
> ++    )
> ++    parser.add_argument(
> ++        "--no-pre-allocate",
> ++        dest="pre_allocate",
> ++        action="store_const",
> ++        default=True,
> ++        const=False,
> ++        help="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="store_true",
> ++        help="Show the version number and exit",
> ++    )
> ++    parser.add_argument(
> ++        "--debug",
> ++        action="store_true",
> ++        help=ARG_SUPPRESS,
> ++        default=ARG_SUPPRESS,
> ++    )
> +
> +     options = parser.parse_args()
> +     if isinstance(options, tuple):
> +@@ -1801,32 +1910,30 @@
> +     with an error stating which module is missing.
> +     """
> +     optional_args = {
> +-        '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 unavailable")
> +
> +
> + def printer(string, quiet=False, debug=False, error=False, **kwargs):
> +     """Helper function print a string with various features"""
> +-
> +     if debug and not DEBUG:
> +         return
> +
> +     if debug:
> +         if sys.stdout.isatty():
> +-            out = '\033[1;30mDEBUG: %s\033[0m' % string
> ++            out = f"\x1b[1;30mDEBUG: {string}\x1b[0m"
> +         else:
> +-            out = 'DEBUG: %s' % string
> ++            out = f"DEBUG: {string}"
> +     else:
> +         out = string
> +
> +     if error:
> +-        kwargs['file'] = sys.stderr
> ++        kwargs["file"] = sys.stderr
> +
> +     if not quiet:
> +         print_(out, **kwargs)
> +@@ -1834,7 +1941,6 @@
> +
> + def shell():
> +     """Run the full speedtest.net test"""
> +-
> +     global DEBUG
> +     shutdown_event = 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) != 1:
> +-        raise SpeedtestCLIError('--csv-delimiter must be a single character')
> ++        raise SpeedtestCLIError("--csv-delimiter must be a single character")
> +
> +     if args.csv_header:
> +         csv_header(args.csv_delimiter)
> +
> +     validate_optional_args(args)
> +
> +-    debug = getattr(args, 'debug', False)
> +-    if debug == 'SUPPRESSHELP':
> ++    debug = getattr(args, "debug", False)
> ++    if debug == "SUPPRESSHELP":
> +         debug = False
> +     if debug:
> +         DEBUG = True
> +
> +-    if args.simple or args.csv or args.json:
> +-        quiet = True
> +-    else:
> +-        quiet = False
> ++    quiet = args.simple or args.csv or args.json
> +
> +-    if args.csv or args.json:
> +-        machine_format = True
> +-    else:
> +-        machine_format = False
> ++    machine_format = 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 = print_dots(shutdown_event)
> +
> +-    printer('Retrieving speedtest.net configuration...', quiet)
> ++    printer("Retrieving speedtest.net configuration...", quiet)
> +     try:
> +         speedtest = Speedtest(
> +             source_address=args.source,
> +             timeout=args.timeout,
> +-            secure=args.secure
> ++            secure=args.secure,
> +         )
> +     except (ConfigRetrievalError,) + HTTP_ERRORS:
> +-        printer('Cannot retrieve speedtest configuration', error=True)
> ++        printer("Cannot retrieve speedtest configuration", error=True)
> +         raise SpeedtestCLIError(get_exception())
> +
> +     if args.list:
> +         try:
> +             speedtest.get_servers()
> +         except (ServersRetrievalError,) + HTTP_ERRORS:
> +-            printer('Cannot retrieve speedtest server list', error=True)
> ++            printer("Cannot retrieve speedtest server list", error=True)
> +             raise SpeedtestCLIError(get_exception())
> +
> +         for _, servers in sorted(speedtest.servers.items()):
> +             for server in servers:
> +-                line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) '
> +-                        '[%(d)0.2f km]' % server)
> ++                line = (
> ++                    "%(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.config['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=args.server, exclude=args.exclude)
> +         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=True)
> ++            printer("Cannot retrieve speedtest server list", error=True)
> +             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) == 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 = 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=('', '\n')[bool(debug)])
> +-        speedtest.download(
> +-            callback=callback,
> +-            threads=(None, 1)[args.single]
> ++        printer("Testing download speed", quiet, end=("", "\n")[bool(debug)])
> ++        speedtest.download(callback=callback, threads=(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=('', '\n')[bool(debug)])
> ++        printer("Testing upload speed", quiet, end=("", "\n")[bool(debug)])
> +         speedtest.upload(
> +             callback=callback,
> +             pre_allocate=args.pre_allocate,
> +-            threads=(None, 1)[args.single]
> ++            threads=(None, 1)[args.single],
> ++        )
> ++        printer(
> ++            "Upload: %0.2f M%s/s"
> ++            % ((results.upload / 1000.0 / 1000.0) / args.units[1], args.units[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=True)
> ++    printer(f"Results:\n{results.dict()!r}", debug=True)
> +
> +     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=args.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=True)
> ++        printer("\nCancelling...", error=True)
> +     except (SpeedtestException, SystemExit):
> +         e = get_exception()
> +         # Ignore a successful exit, or argparse exit
> +-        if getattr(e, 'code', 1) not in (0, 2):
> +-            msg = '%s' % e
> ++        if getattr(e, "code", 1) not in (0, 2):
> ++            msg = f"{e}"
> +             if not msg:
> +-                msg = '%r' % e
> +-            raise SystemExit('ERROR: %s' % msg)
> ++                msg = f"{e!r}"
> ++            raise SystemExit(f"ERROR: {msg}")
> +
> +
> +-if __name__ == '__main__':
> ++if __name__ == "__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.000000000 +0200
> ++++ speedtest-cli-2.1.3/tests/scripts/source.py	2025-01-05 13:17:06.014037557 +0100
> +@@ -15,23 +15,19 @@
> + #    License for the specific language governing permissions and limitations
> + #    under the License.
> +
> +-import sys
> + import subprocess
> ++import sys
> +
> +-cmd = [sys.executable, 'speedtest.py', '--source', '127.0.0.1']
> ++cmd = [sys.executable, "speedtest.py", "--source", "127.0.0.1"]
> +
> +-p = subprocess.Popen(
> +-    cmd,
> +-    stdout=subprocess.PIPE,
> +-    stderr=subprocess.PIPE
> +-)
> ++p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
> +
> + stdout, stderr = p.communicate()
> +
> + if p.returncode != 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_remove_deprecated_method.patch b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_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_deprecated_method.patch
> @@ -0,0 +1,27 @@
> +Patch originally from
> +
> +From: Lavender <keqing.hu@icloud.com>
> +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 <adolf.belka@ipfire.org> 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/speedtest.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 = client or {}
> +
> +         self._share = None
> +-        self.timestamp = 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 >= 3 and sys.version_info.minor >= 11:
> ++            self.timestamp = f"{datetime.datetime.now(datetime.UTC).isoformat()}Z"
> ++        else:
> ++            self.timestamp = f"{datetime.datetime.utcnow().isoformat()}Z"
> +         self.bytes_received = 0
> +         self.bytes_sent = 0
> +
  
Bernhard Bitsch Jan. 7, 2025, 7:51 p.m. UTC | #2
Tested-by: Bernhard Bitsch <bbitsch@ipfire.org>

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 successfully 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 error message
>     for 7 half hour tests. With this modified version it ran for 9 half hour 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 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 concerns
>     from a security point of view although it would be good to get feedback from
>     alternative eyes.
> - Update of rootfile not required.
> 
> Fixes: Bug13805
> Tested-by: Adolf Belka <adolf.belka@ipfire.org>
> Signed-off-by: Adolf Belka <adolf.belka@ipfire.org>
> ---
>   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
> 
> 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  <info@ipfire.org>                     #
> +# Copyright (C) 2007-2025  IPFire Team  <info@ipfire.org>                     #
>   #                                                                             #
>   # 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    = $(URL_IPFIRE)
>   DIR_APP    = $(DIR_SRC)/$(THISAPP)
>   TARGET     = $(DIR_INFO)/$(THISAPP)
>   PROG       = speedtest-cli
> -PAK_VER    = 5
> +PAK_VER    = 6
>   
>   DEPS       =
>   
> @@ -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/speedtest-cli-2.1.3-python_3.10_support.patch
> +	cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11_updates_and_fixes.patch
> +	cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_errors.patch
> +	cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtest-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=/
>   	@rm -rf $(DIR_APP)
> diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_errors.patch 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 <marrco@wohecha.fr>
> +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=None, exclude=None):
> +                     )
> +
> +         urls = [
> +-            "://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 = {}
> +@@ -1346,56 +1344,34 @@ def get_servers(self, servers=None, exclude=None):
> +                 printer(f"Servers XML:\n{serversxml}", debug=True)
> +
> +                 try:
> +-                    try:
> +-                        try:
> +-                            root = ET.fromstring(serversxml)
> +-                        except ET.ParseError:
> +-                            e = get_exception()
> +-                            raise SpeedtestServersError(
> +-                                f"Malformed speedtest.net server list: {e}",
> +-                            )
> +-                        elements = etree_iter(root, "server")
> +-                    except AttributeError:
> +-                        try:
> +-                            root = DOM.parseString(serversxml)
> +-                        except ExpatError:
> +-                            e = get_exception()
> +-                            raise SpeedtestServersError(
> +-                                f"Malformed speedtest.net server list: {e}",
> +-                            )
> +-                        elements = root.getElementsByTagName("server")
> +-                except (SyntaxError, xml.parsers.expat.ExpatError):
> ++                    elements = json.loads(serversxml)
> ++                except SyntaxError:
> +                     raise ServersRetrievalError()
> +
> +                 for server in elements:
> +-                    try:
> +-                        attrib = server.attrib
> +-                    except AttributeError:
> +-                        attrib = 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_servers"]
> +-                        or int(attrib.get("id")) in exclude
> ++                        int(server.get("id")) in self.config["ignore_servers"]
> ++                        or int(server.get("id")) in exclude
> +                     ):
> +                         continue
> +
> +                     try:
> +                         d = 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"] = d
> ++                    server["d"] = d
> +
> +                     try:
> +-                        self.servers[d].append(attrib)
> ++                        self.servers[d].append(server)
> +                     except KeyError:
> +-                        self.servers[d] = [attrib]
> ++                        self.servers[d] = [server]
> +
> +                 break
> +
> +
> diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10_support.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.patch
> @@ -0,0 +1,146 @@
> +Patch originally from
> +
> +From 22210ca35228f0bbcef75a7c14587c4ecb875ab4 Mon Sep 17 00:00:00 2001
> +From: Matt Martz <matt@sivel.net>
> +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 <adolf.belka@ipfire.org> 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/speedtest.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.
> +
> +-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 = isSet
> ++
> +
> + # Some global variables we use
> + DEBUG = False
> +@@ -56,6 +58,7 @@
> + PY25PLUS = sys.version_info[:2] >= (2, 5)
> + PY26PLUS = sys.version_info[:2] >= (2, 6)
> + PY32PLUS = sys.version_info[:2] >= (3, 2)
> ++PY310PLUS = sys.version_info[:2] >= (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 = ET.Element.iter
> +-elif PY25PLUS:
> +-    etree_iter = ET_Element.getiterator
> +-
> +-if PY26PLUS:
> +-    thread_is_alive = threading.Thread.is_alive
> +-else:
> +-    thread_is_alive = threading.Thread.isAlive
> +-
> +-
> + # Exception "constants" to support Python 2 through Python 3
> + try:
> +     import ssl
> +@@ -293,6 +285,23 @@
> +     ssl = None
> +     HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
> +
> ++if PY32PLUS:
> ++    etree_iter = ET.Element.iter
> ++elif PY25PLUS:
> ++    etree_iter = ET_Element.getiterator
> ++
> ++if PY26PLUS:
> ++    thread_is_alive = threading.Thread.is_alive
> ++else:
> ++    thread_is_alive = 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=False, end=False):
> +-        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) <= self.timeout:
> +                 f = 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) <=
> +                         self.timeout):
> +                     self.result.append(len(f.read(10240)))
> +@@ -864,7 +873,7 @@
> +
> +     def read(self, n=10240):
> +         if ((timeit.default_timer() - self.start) <= self.timeout and
> +-                not self._shutdown_event.isSet()):
> ++                not event_is_set(self._shutdown_event)):
> +             chunk = self.data.read(n)
> +             self.total.append(len(chunk))
> +             return chunk
> +@@ -902,7 +911,7 @@
> +         request = self.request
> +         try:
> +             if ((timeit.default_timer() - self.starttime) <= self.timeout and
> +-                    not self._shutdown_event.isSet()):
> ++                    not event_is_set(self._shutdown_event)):
> +                 try:
> +                     f = self._opener(request)
> +                 except TypeError:
> diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11_updates_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 <mark@there.co.nz>
> +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 <adolf.belka@ipfire.org> is the same as 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.
> +
> ++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 = codecs.open(os.path.join(here, *file_paths), 'r', 'latin1')
> ++        f = codecs.open(os.path.join(here, *file_paths), "r", "latin1")
> +         version_file = f.read()
> +         f.close()
> +-    except:
> ++    except Exception:
> +         raise RuntimeError("Unable to find version string.")
> +
> +     # The version line must have the form
> +     # __version__ = 'ver'
> +-    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
> +-                              version_file, re.M)
> ++    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 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 = codecs.open('README.rst', encoding='utf-8')
> ++    f = codecs.open("README.rst", encoding="utf-8")
> +     long_description = f.read()
> +     f.close()
> +-except:
> +-    long_description = ''
> ++except Exception:
> ++    long_description = ""
> +
> +
> + setup(
> +-    name='speedtest-cli',
> +-    version=find_version('speedtest.py'),
> +-    description=('Command line interface for testing internet bandwidth using '
> +-                 'speedtest.net'),
> ++    name="speedtest-cli",
> ++    version=find_version("speedtest.py"),
> ++    description=(
> ++        "Command line interface for testing internet bandwidth using " "speedtest.net"
> ++    ),
> +     long_description=long_description,
> +-    keywords='speedtest speedtest.net',
> +-    author='Matt Martz',
> +-    author_email='matt@sivel.net',
> +-    url='https://github.com/sivel/speedtest-cli',
> +-    license='Apache License, Version 2.0',
> +-    py_modules=['speedtest'],
> ++    keywords="speedtest speedtest.net",
> ++    author="Matt Martz",
> ++    author_email="matt@sivel.net",
> ++    url="https://github.com/sivel/speedtest-cli",
> ++    license="Apache License, Version 2.0",
> ++    py_modules=["speedtest"],
> +     entry_points={
> +-        'console_scripts': [
> +-            'speedtest=speedtest:main',
> +-            'speedtest-cli=speedtest:main'
> +-        ]
> ++        "console_scripts": [
> ++            "speedtest=speedtest:main",
> ++            "speedtest-cli=speedtest:main",
> ++        ],
> +     },
> +     classifiers=[
> +-        '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/speedtest.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 @@
> +
> + try:
> +     import gzip
> ++
> +     GZIP_BASE = gzip.GzipFile
> + except ImportError:
> +     gzip = None
> +     GZIP_BASE = object
> +
> +-__version__ = '2.1.3'
> ++__version__ = "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 = 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 = None
> +
> + 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, HTTPRedirectHandler,
> +-                                HTTPErrorProcessor, OpenerDirector)
> ++    from urllib.request import (AbstractHTTPHandler, HTTPDefaultErrorHandler,
> ++                                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 = int
> +     PARSER_TYPE_STR = str
> +     PARSER_TYPE_FLOAT = float
> + except ImportError:
> +-    from optparse import OptionParser as ArgParser
> +     from optparse import SUPPRESS_HELP as ARG_SUPPRESS
> +-    PARSER_TYPE_INT = 'int'
> +-    PARSER_TYPE_STR = 'string'
> +-    PARSER_TYPE_FLOAT = 'float'
> ++    from optparse import OptionParser as ArgParser
> ++
> ++    PARSER_TYPE_INT = "int"
> ++    PARSER_TYPE_STR = "string"
> ++    PARSER_TYPE_FLOAT = "float"
> +
> + try:
> +     from cStringIO import StringIO
> ++
> +     BytesIO = None
> + except ImportError:
> +     try:
> +         from StringIO import StringIO
> ++
> +         BytesIO = 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 = FileIO(f.fileno(), 'w')
> +-            super(_Py3Utf8Output, self).__init__(
> +-                buf,
> +-                encoding='utf8',
> +-                errors='strict'
> +-            )
> ++            buf = FileIO(f.fileno(), "w")
> ++            super().__init__(buf, encoding="utf8", errors="strict")
> +
> +         def write(self, s):
> +-            super(_Py3Utf8Output, self).write(s)
> ++            super().write(s)
> +             self.flush()
> +
> +-    _py3_print = getattr(builtins, 'print')
> ++    _py3_print = getattr(builtins, "print")
> +     try:
> +         _py3_utf8_stdout = _Py3Utf8Output(sys.stdout)
> +         _py3_utf8_stderr = _Py3Utf8Output(sys.stderr)
> +@@ -188,23 +192,24 @@
> +         _py3_utf8_stderr = 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') == sys.stderr:
> +-            kwargs['file'] = _py3_utf8_stderr
> ++        """Wrapper function for py3 to print, with a utf-8 encoded stdout."""
> ++        if kwargs.get("file") == sys.stderr:
> ++            kwargs["file"] = _py3_utf8_stderr
> +         else:
> +-            kwargs['file'] = kwargs.get('file', _py3_utf8_stdout)
> ++            kwargs["file"] = 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 = str(data)
> +             # If the file has an encoding, encode unicode with it.
> +-            encoding = 'utf8'  # Always trust UTF-8 for output
> +-            if (isinstance(fp, file) and
> +-                    isinstance(data, unicode) and
> +-                    encoding is not None):
> ++            encoding = "utf8"  # Always trust UTF-8 for output
> ++            if (
> ++                isinstance(fp, file)
> ++                and isinstance(data, unicode)
> ++                and encoding is not None
> ++            ):
> +                 errors = getattr(fp, "errors", None)
> +                 if errors is None:
> +                     errors = "strict"
> +                 data = data.encode(encoding, errors)
> +             fp.write(data)
> +             fp.flush()
> ++
> +         want_unicode = False
> +         sep = 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 = (ssl.CertificateError,)
> +     except AttributeError:
> +         CERT_ERROR = tuple()
> +
> +     HTTP_ERRORS = (
> +-        (HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) +
> +-        CERT_ERROR
> +-    )
> ++        HTTPError,
> ++        URLError,
> ++        socket.error,
> ++        ssl.SSLError,
> ++        BadStatusLine,
> ++    ) + CERT_ERROR
> + except ImportError:
> +     ssl = None
> +     HTTP_ERRORS = (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=_GLOBAL_DEFAULT_TIMEOUT,
> +-                      source_address=None):
> ++def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None):
> +     """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 = address
> +     err = 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 = kwargs.pop('source_address', None)
> +-        timeout = kwargs.pop('timeout', 10)
> ++        source_address = kwargs.pop("source_address", None)
> ++        timeout = kwargs.pop("timeout", 10)
> +
> +         self._tunnel_host = None
> +
> +@@ -435,13 +446,13 @@
> +             self.sock = socket.create_connection(
> +                 (self.host, self.port),
> +                 self.timeout,
> +-                self.source_address
> ++                self.source_address,
> +             )
> +         except (AttributeError, TypeError):
> +             self.sock = 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 = 443
> +
> +         def __init__(self, *args, **kwargs):
> +-            source_address = kwargs.pop('source_address', None)
> +-            timeout = kwargs.pop('timeout', 10)
> ++            source_address = kwargs.pop("source_address", None)
> ++            timeout = kwargs.pop("timeout", 10)
> +
> +             self._tunnel_host = None
> +
> +@@ -467,18 +480,18 @@
> +             self.source_address = 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 = socket.create_connection(
> +                     (self.host, self.port),
> +                     self.timeout,
> +-                    self.source_address
> ++                    self.source_address,
> +                 )
> +             except (AttributeError, TypeError):
> +                 self.sock = 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 = {}
> +-                    if hasattr(ssl, 'SSLContext'):
> ++                    if hasattr(ssl, "SSLContext"):
> +                         if self._tunnel_host:
> +-                            kwargs['server_hostname'] = self._tunnel_host
> ++                            kwargs["server_hostname"] = self._tunnel_host
> +                         else:
> +-                            kwargs['server_hostname'] = self.host
> ++                            kwargs["server_hostname"] = self.host
> +                     self.sock = self._context.wrap_socket(self.sock, **kwargs)
> +                 except AttributeError:
> +                     self.sock = ssl.wrap_socket(self.sock)
> +@@ -505,13 +518,13 @@
> +                     self.sock = FakeSocket(self.sock, socket.ssl(self.sock))
> +                 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",
> +                 )
> +
> +
> +@@ -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'] = context
> ++            kwargs["context"] = 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=0, source_address=None, timeout=10):
> +         AbstractHTTPHandler.__init__(self, debuglevel)
> +         self.source_address = source_address
> +@@ -547,9 +560,9 @@
> +             _build_connection(
> +                 SpeedtestHTTPConnection,
> +                 self.source_address,
> +-                self.timeout
> ++                self.timeout,
> +             ),
> +-            req
> ++            req,
> +         )
> +
> +     http_request = 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=0, context=None, source_address=None,
> +-                 timeout=10):
> ++
> ++    def __init__(self, debuglevel=0, context=None, source_address=None, timeout=10):
> +         AbstractHTTPHandler.__init__(self, debuglevel)
> +         self._context = context
> +         self.source_address = source_address
> +@@ -574,7 +587,7 @@
> +                 self.timeout,
> +                 context=self._context,
> +             ),
> +-            req
> ++            req,
> +         )
> +
> +     https_request = AbstractHTTPHandler.do_request_
> +@@ -586,29 +599,25 @@
> +     ``source_address`` for binding, ``timeout`` and our custom
> +     `User-Agent`
> +     """
> +-
> +-    printer('Timeout set to %d' % timeout, debug=True)
> ++    printer(f"Timeout set to {timeout}", debug=True)
> +
> +     if source_address:
> +         source_address_tuple = (source_address, 0)
> +-        printer('Binding to source address: %r' % (source_address_tuple,),
> +-                debug=True)
> ++        printer(f"Binding to source address: {source_address_tuple!r}", debug=True)
> +     else:
> +         source_address_tuple = None
> +
> +     handlers = [
> +         ProxyHandler(),
> +-        SpeedtestHTTPHandler(source_address=source_address_tuple,
> +-                             timeout=timeout),
> +-        SpeedtestHTTPSHandler(source_address=source_address_tuple,
> +-                              timeout=timeout),
> ++        SpeedtestHTTPHandler(source_address=source_address_tuple, timeout=timeout),
> ++        SpeedtestHTTPSHandler(source_address=source_address_tuple, timeout=timeout),
> +         HTTPDefaultErrorHandler(),
> +         HTTPRedirectHandler(),
> +-        HTTPErrorProcessor()
> ++        HTTPErrorProcessor(),
> +     ]
> +
> +     opener = OpenerDirector()
> +-    opener.addheaders = [('User-agent', build_user_agent())]
> ++    opener.addheaders = [("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 = BytesIO or StringIO
> +         self.io = IO()
> +         while 1:
> +@@ -637,7 +649,7 @@
> +                 break
> +             self.io.write(chunk)
> +         self.io.seek(0)
> +-        gzip.GzipFile.__init__(self, mode='rb', fileobj=self.io)
> ++        gzip.GzipFile.__init__(self, mode="rb", fileobj=self.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 = origin
> +     lat2, lon2 = destination
> +     radius = 6371  # km
> +
> +     dlat = math.radians(lat2 - lat1)
> +     dlon = math.radians(lon2 - lon1)
> +-    a = (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 = 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 = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
> +     d = radius * c
> +
> +@@ -674,52 +684,47 @@
> +
> + def build_user_agent():
> +     """Build a Mozilla/5.0 compatible User-Agent string"""
> +-
> +     ua_tuple = (
> +-        '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 = ' '.join(ua_tuple)
> +-    printer('User-Agent: %s' % user_agent, debug=True)
> ++    user_agent = " ".join(ua_tuple)
> ++    printer(f"User-Agent: {user_agent}", debug=True)
> +     return user_agent
> +
> +
> +-def build_request(url, data=None, headers=None, bump='0', secure=False):
> ++def build_request(url, data=None, headers=None, bump="0", secure=False):
> +     """Build a urllib2 request object
> +
> +     This function automatically adds a User-Agent header to all requests
> +-
> +     """
> +-
> +     if not headers:
> +         headers = {}
> +
> +-    if url[0] == ':':
> +-        scheme = ('http', 'https')[bool(secure)]
> +-        schemed_url = '%s%s' % (scheme, url)
> ++    if url[0] == ":":
> ++        scheme = ("http", "https")[bool(secure)]
> ++        schemed_url = f"{scheme}{url}"
> +     else:
> +         schemed_url = url
> +
> +-    if '?' in url:
> +-        delim = '&'
> ++    if "?" in url:
> ++        delim = "&"
> +     else:
> +-        delim = '?'
> ++        delim = "?"
> +
> +     # WHO YOU GONNA CALL? CACHE BUSTERS!
> +-    final_url = '%s%sx=%s.%s' % (schemed_url, delim,
> +-                                 int(timeit.time.time() * 1000),
> +-                                 bump)
> +-
> +-    headers.update({
> +-        'Cache-Control': 'no-cache',
> +-    })
> ++    final_url = f"{schemed_url}{delim}x={int(timeit.time.time() * 1000)}.{bump}"
> ++
> ++    headers.update(
> ++        {
> ++            "Cache-Control": "no-cache",
> ++        },
> ++    )
> +
> +-    printer('%s %s' % (('GET', 'POST')[bool(data)], final_url),
> +-            debug=True)
> ++    printer(f"{('GET', 'POST')[bool(data)]} {final_url}", debug=True)
> +
> +     return Request(final_url, data=data, headers=headers)
> +
> +@@ -729,7 +734,6 @@
> +     establishing a connection with a HTTP/HTTPS request
> +
> +     """
> +-
> +     if opener:
> +         _open = opener.open
> +     else:
> +@@ -738,7 +742,7 @@
> +     try:
> +         uh = _open(request)
> +         if request.get_full_url() != uh.geturl():
> +-            printer('Redirected to %s' % uh.geturl(), debug=True)
> ++            printer(f"Redirected to {uh.geturl()}", debug=True)
> +         return uh, False
> +     except HTTP_ERRORS:
> +         e = get_exception()
> +@@ -750,13 +754,12 @@
> +     ``Content-Encoding`` is ``gzip`` otherwise the response itself
> +
> +     """
> +-
> +     try:
> +         getheader = response.headers.getheader
> +     except AttributeError:
> +         getheader = response.getheader
> +
> +-    if getheader('content-encoding') == 'gzip':
> ++    if getheader("content-encoding") == "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=False, end=False):
> +         if event_is_set(shutdown_event):
> +             return
> +
> +-        sys.stdout.write('.')
> ++        sys.stdout.write(".")
> +         if current + 1 == 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=None,
> +-                 shutdown_event=None):
> ++    def __init__(self, i, request, start, timeout, opener=None, shutdown_event=None):
> +         threading.Thread.__init__(self)
> +         self.request = request
> +         self.result = [0]
> +@@ -817,9 +821,10 @@
> +         try:
> +             if (timeit.default_timer() - self.starttime) <= self.timeout:
> +                 f = self._opener(self.request)
> +-                while (not event_is_set(self._shutdown_event) and
> +-                        (timeit.default_timer() - self.starttime) <=
> +-                        self.timeout):
> ++                while (
> ++                    not event_is_set(self._shutdown_event)
> ++                    and (timeit.default_timer() - self.starttime) <= self.timeout
> ++                ):
> +                     self.result.append(len(f.read(10240)))
> +                     if self.result[-1] == 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 = [0]
> +
> +     def pre_allocate(self):
> +-        chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
> ++        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
> +         multiplier = int(round(int(self.length) / 36.0))
> +         IO = BytesIO or StringIO
> +         try:
> +             self._data = IO(
> +-                ('content1=%s' %
> +-                 (chars * multiplier)[0:int(self.length) - 9]
> +-                 ).encode()
> ++                (f"content1={(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=10240):
> +-        if ((timeit.default_timer() - self.start) <= self.timeout and
> +-                not event_is_set(self._shutdown_event)):
> ++        if (timeit.default_timer() - self.start) <= self.timeout and not event_is_set(
> ++            self._shutdown_event,
> ++        ):
> +             chunk = 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=None,
> +-                 shutdown_event=None):
> ++    def __init__(
> ++        self,
> ++        i,
> ++        request,
> ++        start,
> ++        size,
> ++        timeout,
> ++        opener=None,
> ++        shutdown_event=None,
> ++    ):
> +         threading.Thread.__init__(self)
> +         self.request = request
> +         self.request.data.start = self.starttime = start
> +@@ -910,16 +921,19 @@
> +     def run(self):
> +         request = self.request
> +         try:
> +-            if ((timeit.default_timer() - self.starttime) <= self.timeout and
> +-                    not event_is_set(self._shutdown_event)):
> ++            if (
> ++                timeit.default_timer() - self.starttime
> ++            ) <= self.timeout and not event_is_set(self._shutdown_event):
> +                 try:
> +                     f = self._opener(request)
> +                 except TypeError:
> +                     # PY24 expects a string or buffer
> +                     # This also causes issues with Ctrl-C, but we will concede
> +                     # for the moment that Ctrl-C on PY24 isn't immediate
> +-                    request = build_request(self.request.get_full_url(),
> +-                                            data=request.data.read(self.size))
> ++                    request = build_request(
> ++                        self.request.get_full_url(),
> ++                        data=request.data.read(self.size),
> ++                    )
> +                     f = self._opener(request)
> +                 f.read(11)
> +                 f.close()
> +@@ -932,7 +946,7 @@
> +             self.result = 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=0, upload=0, ping=0, server=None, client=None,
> +-                 opener=None, secure=False):
> ++    def __init__(
> ++        self,
> ++        download=0,
> ++        upload=0,
> ++        ping=0,
> ++        server=None,
> ++        client=None,
> ++        opener=None,
> ++        secure=False,
> ++    ):
> +         self.download = download
> +         self.upload = upload
> +         self.ping = ping
> +@@ -957,7 +979,7 @@
> +         self.client = client or {}
> +
> +         self._share = None
> +-        self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat()
> ++        self.timestamp = f"{datetime.datetime.utcnow().isoformat()}Z"
> +         self.bytes_received = 0
> +         self.bytes_sent = 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 parameters
> +         # in a certain order
> +         api_data = [
> +-            'recommendedserverid=%s' % self.server['id'],
> +-            'ping=%s' % ping,
> +-            'screenresolution=',
> +-            'promo=',
> +-            'download=%s' % download,
> +-            'screendpi=',
> +-            'upload=%s' % upload,
> +-            'testmethod=http',
> +-            'hash=%s' % md5(('%s-%s-%s-%s' %
> +-                             (ping, upload, download, '297aae72'))
> +-                            .encode()).hexdigest(),
> +-            'touchscreen=none',
> +-            'startmode=pingselect',
> +-            'accuracy=1',
> +-            'bytesreceived=%s' % self.bytes_received,
> +-            'bytessent=%s' % self.bytes_sent,
> +-            'serverid=%s' % self.server['id'],
> ++            f"recommendedserverid={self.server['id']}",
> ++            f"ping={ping}",
> ++            "screenresolution=",
> ++            "promo=",
> ++            f"download={download}",
> ++            "screendpi=",
> ++            f"upload={upload}",
> ++            "testmethod=http",
> ++            "hash=%s"
> ++            % md5(
> ++                ("%s-%s-%s-%s" % (ping, upload, download, "297aae72")).encode(),
> ++            ).hexdigest(),
> ++            "touchscreen=none",
> ++            "startmode=pingselect",
> ++            "accuracy=1",
> ++            f"bytesreceived={self.bytes_received}",
> ++            f"bytessent={self.bytes_sent}",
> ++            f"serverid={self.server['id']}",
> +         ]
> +
> +-        headers = {'Referer': 'http://c.speedtest.net/flash/speedtest.swf'}
> +-        request = build_request('://www.speedtest.net/api/api.php',
> +-                                data='&'.join(api_data).encode(),
> +-                                headers=headers, secure=self._secure)
> ++        headers = {"Referer": "http://c.speedtest.net/flash/speedtest.swf"}
> ++        request = build_request(
> ++            "://www.speedtest.net/api/api.php",
> ++            data="&".join(api_data).encode(),
> ++            headers=headers,
> ++            secure=self._secure,
> ++        )
> +         f, e = catch_request(request, opener=self._opener)
> +         if e:
> +             raise ShareResultsConnectFailure(e)
> +@@ -1019,75 +1044,94 @@
> +         f.close()
> +
> +         if int(code) != 200:
> +-            raise ShareResultsSubmitFailure('Could not submit results to '
> +-                                            'speedtest.net')
> ++            raise ShareResultsSubmitFailure(
> ++                "Could not submit results to " "speedtest.net",
> ++            )
> +
> +         qsargs = parse_qs(response.decode())
> +-        resultid = qsargs.get('resultid')
> ++        resultid = qsargs.get("resultid")
> +         if not resultid or len(resultid) != 1:
> +-            raise ShareResultsSubmitFailure('Could not submit results to '
> +-                                            'speedtest.net')
> ++            raise ShareResultsSubmitFailure(
> ++                "Could not submit results to " "speedtest.net",
> ++            )
> +
> +-        self._share = 'http://www.speedtest.net/result/%s.png' % resultid[0]
> ++        self._share = f"http://www.speedtest.net/result/{resultid[0]}.png"
> +
> +         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=','):
> ++    def csv_header(delimiter=","):
> +         """Return CSV Headers"""
> +-
> +-        row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance',
> +-               'Ping', 'Download', 'Upload', 'Share', 'IP Address']
> ++        row = [
> ++            "Server ID",
> ++            "Sponsor",
> ++            "Server Name",
> ++            "Timestamp",
> ++            "Distance",
> ++            "Ping",
> ++            "Download",
> ++            "Upload",
> ++            "Share",
> ++            "IP Address",
> ++        ]
> +         out = StringIO()
> +-        writer = csv.writer(out, delimiter=delimiter, lineterminator='')
> ++        writer = csv.writer(out, delimiter=delimiter, lineterminator="")
> +         writer.writerow([to_utf8(v) for v in row])
> +         return out.getvalue()
> +
> +-    def csv(self, delimiter=','):
> ++    def csv(self, delimiter=","):
> +         """Return data in CSV format"""
> +-
> +         data = self.dict()
> +         out = StringIO()
> +-        writer = csv.writer(out, delimiter=delimiter, lineterminator='')
> +-        row = [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 = csv.writer(out, delimiter=delimiter, lineterminator="")
> ++        row = [
> ++            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=False):
> +         """Return data in JSON format"""
> +-
> +         kwargs = {}
> +         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=None, source_address=None, timeout=10,
> +-                 secure=False, shutdown_event=None):
> ++    def __init__(
> ++        self,
> ++        config=None,
> ++        source_address=None,
> ++        timeout=10,
> ++        secure=False,
> ++        shutdown_event=None,
> ++    ):
> +         self.config = {}
> +
> +         self._source_address = source_address
> +@@ -1110,7 +1154,7 @@
> +         self._best = {}
> +
> +         self.results = SpeedtestResults(
> +-            client=self.config['client'],
> ++            client=self.config["client"],
> +             opener=self._opener,
> +             secure=secure,
> +         )
> +@@ -1125,12 +1169,14 @@
> +         """Download the speedtest.net configuration and return only the data
> +         we are interested in
> +         """
> +-
> +         headers = {}
> +         if gzip:
> +-            headers['Accept-Encoding'] = 'gzip'
> +-        request = build_request('://www.speedtest.net/speedtest-config.php',
> +-                                headers=headers, secure=self._secure)
> ++            headers["Accept-Encoding"] = "gzip"
> ++        request = build_request(
> ++            "://www.speedtest.net/speedtest-config.php",
> ++            headers=headers,
> ++            secure=self._secure,
> ++        )
> +         uh, e = catch_request(request, opener=self._opener)
> +         if e:
> +             raise ConfigRetrievalError(e)
> +@@ -1151,9 +1197,9 @@
> +         if int(uh.code) != 200:
> +             return None
> +
> +-        configxml = ''.encode().join(configxml_list)
> ++        configxml = "".encode().join(configxml_list)
> +
> +-        printer('Config XML:\n%s' % configxml, debug=True)
> ++        printer(f"Config XML:\n{configxml}", debug=True)
> +
> +         try:
> +             try:
> +@@ -1161,13 +1207,13 @@
> +             except ET.ParseError:
> +                 e = get_exception()
> +                 raise SpeedtestConfigError(
> +-                    'Malformed speedtest.net configuration: %s' % e
> ++                    f"Malformed speedtest.net configuration: {e}",
> +                 )
> +-            server_config = root.find('server-config').attrib
> +-            download = root.find('download').attrib
> +-            upload = root.find('upload').attrib
> ++            server_config = root.find("server-config").attrib
> ++            download = root.find("download").attrib
> ++            upload = root.find("upload").attrib
> +             # times = root.find('times').attrib
> +-            client = root.find('client').attrib
> ++            client = root.find("client").attrib
> +
> +         except AttributeError:
> +             try:
> +@@ -1175,65 +1221,61 @@
> +             except ExpatError:
> +                 e = get_exception()
> +                 raise SpeedtestConfigError(
> +-                    'Malformed speedtest.net configuration: %s' % e
> ++                    f"Malformed speedtest.net configuration: {e}",
> +                 )
> +-            server_config = get_attributes_by_tag_name(root, 'server-config')
> +-            download = get_attributes_by_tag_name(root, 'download')
> +-            upload = get_attributes_by_tag_name(root, 'upload')
> ++            server_config = get_attributes_by_tag_name(root, "server-config")
> ++            download = get_attributes_by_tag_name(root, "download")
> ++            upload = get_attributes_by_tag_name(root, "upload")
> +             # times = get_attributes_by_tag_name(root, 'times')
> +-            client = get_attributes_by_tag_name(root, 'client')
> ++            client = get_attributes_by_tag_name(root, "client")
> +
> +-        ignore_servers = [
> +-            int(i) for i in server_config['ignoreids'].split(',') if i
> +-        ]
> ++        ignore_servers = [int(i) for i in server_config["ignoreids"].split(",") if i]
> +
> +-        ratio = int(upload['ratio'])
> +-        upload_max = int(upload['maxchunkcount'])
> ++        ratio = int(upload["ratio"])
> ++        upload_max = int(upload["maxchunkcount"])
> +         up_sizes = [32768, 65536, 131072, 262144, 524288, 1048576, 7340032]
> +         sizes = {
> +-            '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],
> +         }
> +
> +-        size_count = len(sizes['upload'])
> ++        size_count = len(sizes["upload"])
> +
> +         upload_count = int(math.ceil(upload_max / size_count))
> +
> +-        counts = {
> +-            'upload': upload_count,
> +-            'download': int(download['threadsperurl'])
> +-        }
> ++        counts = {"upload": upload_count, "download": int(download["threadsperurl"])}
> +
> +         threads = {
> +-            'upload': int(upload['threads']),
> +-            'download': int(server_config['threadcount']) * 2
> ++            "upload": int(upload["threads"]),
> ++            "download": int(server_config["threadcount"]) * 2,
> +         }
> +
> +         length = {
> +-            '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 = (float(client['lat']), float(client['lon']))
> ++            self.lat_lon = (float(client["lat"]), float(client["lon"]))
> +         except ValueError:
> +             raise SpeedtestConfigError(
> +-                'Unknown location: lat=%r lon=%r' %
> +-                (client.get('lat'), client.get('lon'))
> ++                "Unknown location: lat=%r lon=%r"
> ++                % (client.get("lat"), client.get("lon")),
> +             )
> +
> +-        printer('Config:\n%r' % self.config, debug=True)
> ++        printer(f"Config:\n{self.config!r}", debug=True)
> +
> +         return self.config
> +
> +@@ -1255,32 +1297,31 @@
> +                     server_list[i] = 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 = [
> +-            '://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 = {}
> +         if gzip:
> +-            headers['Accept-Encoding'] = 'gzip'
> ++            headers["Accept-Encoding"] = "gzip"
> +
> +         errors = []
> +         for url in urls:
> +             try:
> +                 request = build_request(
> +-                    '%s?threads=%s' % (url,
> +-                                       self.config['threads']['download']),
> ++                    f"{url}?threads={self.config['threads']['download']}",
> +                     headers=headers,
> +-                    secure=self._secure
> ++                    secure=self._secure,
> +                 )
> +                 uh, e = catch_request(request, opener=self._opener)
> +                 if e:
> +-                    errors.append('%s' % e)
> ++                    errors.append(f"{e}")
> +                     raise ServersRetrievalError()
> +
> +                 stream = get_response_stream(uh)
> +@@ -1300,9 +1341,9 @@
> +                 if int(uh.code) != 200:
> +                     raise ServersRetrievalError()
> +
> +-                serversxml = ''.encode().join(serversxml_list)
> ++                serversxml = "".encode().join(serversxml_list)
> +
> +-                printer('Servers XML:\n%s' % serversxml, debug=True)
> ++                printer(f"Servers XML:\n{serversxml}", debug=True)
> +
> +                 try:
> +                     try:
> +@@ -1311,18 +1352,18 @@
> +                         except ET.ParseError:
> +                             e = get_exception()
> +                             raise SpeedtestServersError(
> +-                                'Malformed speedtest.net server list: %s' % e
> ++                                f"Malformed speedtest.net server list: {e}",
> +                             )
> +-                        elements = etree_iter(root, 'server')
> ++                        elements = etree_iter(root, "server")
> +                     except AttributeError:
> +                         try:
> +                             root = DOM.parseString(serversxml)
> +                         except ExpatError:
> +                             e = get_exception()
> +                             raise SpeedtestServersError(
> +-                                'Malformed speedtest.net server list: %s' % e
> ++                                f"Malformed speedtest.net server list: {e}",
> +                             )
> +-                        elements = root.getElementsByTagName('server')
> ++                        elements = root.getElementsByTagName("server")
> +                 except (SyntaxError, xml.parsers.expat.ExpatError):
> +                     raise ServersRetrievalError()
> +
> +@@ -1332,21 +1373,24 @@
> +                     except AttributeError:
> +                         attrib = 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_servers']
> +-                            or int(attrib.get('id')) in exclude):
> ++                    if (
> ++                        int(attrib.get("id")) in self.config["ignore_servers"]
> ++                        or int(attrib.get("id")) in exclude
> ++                    ):
> +                         continue
> +
> +                     try:
> +-                        d = distance(self.lat_lon,
> +-                                     (float(attrib.get('lat')),
> +-                                      float(attrib.get('lon'))))
> ++                        d = distance(
> ++                            self.lat_lon,
> ++                            (float(attrib.get("lat")), float(attrib.get("lon"))),
> ++                        )
> +                     except Exception:
> +                         continue
> +
> +-                    attrib['d'] = d
> ++                    attrib["d"] = 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 = urlparse(server)
> +
> +         name, ext = os.path.splitext(urlparts[2])
> +@@ -1379,41 +1422,41 @@
> +         request = build_request(url)
> +         uh, e = catch_request(request, opener=self._opener)
> +         if e:
> +-            raise SpeedtestMiniConnectFailure('Failed to connect to %s' %
> +-                                              server)
> +-        else:
> +-            text = uh.read()
> +-            uh.close()
> ++            raise SpeedtestMiniConnectFailure(f"Failed to connect to {server}")
> ++        text = uh.read()
> ++        uh.close()
> +
> +-        extension = re.findall('upload_?[Ee]xtension: "([^"]+)"',
> +-                               text.decode())
> ++        extension = 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 = self._opener.open(
> +-                        '%s/speedtest/upload.%s' % (url, ext)
> +-                    )
> ++                    f = self._opener.open(f"{url}/speedtest/upload.{ext}")
> +                 except Exception:
> +                     pass
> +                 else:
> +                     data = f.read().strip().decode()
> +-                    if (f.code == 200 and
> +-                            len(data.splitlines()) == 1 and
> +-                            re.match('size=[0-9]', data)):
> ++                    if (
> ++                        f.code == 200
> ++                        and len(data.splitlines()) == 1
> ++                        and re.match("size=[0-9]", data)
> ++                    ):
> +                         extension = [ext]
> +                         break
> +         if not urlparts or not extension:
> +-            raise InvalidSpeedtestMiniServer('Invalid Speedtest Mini Server: '
> +-                                             '%s' % server)
> ++            raise InvalidSpeedtestMiniServer(
> ++                "Invalid Speedtest Mini Server: " "%s" % server,
> ++            )
> +
> +-        self.servers = [{
> +-            'sponsor': 'Speedtest Mini',
> +-            'name': urlparts[1],
> +-            'd': 0,
> +-            'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]),
> +-            'latency': 0,
> +-            'id': 0
> +-        }]
> ++        self.servers = [
> ++            {
> ++                "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=True)
> ++        printer(f"Closest Servers:\n{self.closest!r}", debug=True)
> +         return self.closest
> +
> +     def get_best_server(self, servers=None):
> +         """Perform a speedtest.net "ping" to determine which speedtest.net
> +         server has the lowest latency
> +         """
> +-
> +         if not servers:
> +             if not self.closest:
> +                 servers = self.get_closest_servers()
> +@@ -1457,39 +1498,38 @@
> +         results = {}
> +         for server in servers:
> +             cum = []
> +-            url = os.path.dirname(server['url'])
> ++            url = os.path.dirname(server["url"])
> +             stamp = int(timeit.time.time() * 1000)
> +-            latency_url = '%s/latency.txt?x=%s' % (url, stamp)
> ++            latency_url = f"{url}/latency.txt?x={stamp}"
> +             for i in range(0, 3):
> +-                this_latency_url = '%s.%s' % (latency_url, i)
> +-                printer('%s %s' % ('GET', this_latency_url),
> +-                        debug=True)
> ++                this_latency_url = f"{latency_url}.{i}"
> ++                printer(f"{'GET'} {this_latency_url}", debug=True)
> +                 urlparts = urlparse(latency_url)
> +                 try:
> +-                    if urlparts[0] == 'https':
> ++                    if urlparts[0] == "https":
> +                         h = SpeedtestHTTPSConnection(
> +                             urlparts[1],
> +-                            source_address=source_address_tuple
> ++                            source_address=source_address_tuple,
> +                         )
> +                     else:
> +                         h = SpeedtestHTTPConnection(
> +                             urlparts[1],
> +-                            source_address=source_address_tuple
> ++                            source_address=source_address_tuple,
> +                         )
> +-                    headers = {'User-Agent': user_agent}
> +-                    path = '%s?%s' % (urlparts[2], urlparts[4])
> ++                    headers = {"User-Agent": user_agent}
> ++                    path = f"{urlparts[2]}?{urlparts[4]}"
> +                     start = timeit.default_timer()
> +                     h.request("GET", path, headers=headers)
> +                     r = h.getresponse()
> +-                    total = (timeit.default_timer() - start)
> ++                    total = timeit.default_timer() - start
> +                 except HTTP_ERRORS:
> +                     e = get_exception()
> +-                    printer('ERROR: %r' % e, debug=True)
> ++                    printer(f"ERROR: {e!r}", debug=True)
> +                     cum.append(3600)
> +                     continue
> +
> +                 text = r.read(9)
> +-                if int(r.status) == 200 and text == 'test=test'.encode():
> ++                if int(r.status) == 200 and text == "test=test".encode():
> +                     cum.append(total)
> +                 else:
> +                     cum.append(3600)
> +@@ -1501,16 +1541,17 @@
> +         try:
> +             fastest = 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 = results[fastest]
> +-        best['latency'] = fastest
> ++        best["latency"] = fastest
> +
> +         self.results.ping = fastest
> +         self.results.server = best
> +
> +         self._best.update(best)
> +-        printer('Best Server:\n%r' % best, debug=True)
> ++        printer(f"Best Server:\n{best!r}", debug=True)
> +         return best
> +
> +     def download(self, callback=do_nothing, threads=None):
> +@@ -1519,22 +1560,21 @@
> +         A ``threads`` value of ``None`` will fall back to those dictated
> +         by the speedtest.net configuration
> +         """
> +-
> +         urls = []
> +-        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),
> ++                )
> +
> +         request_count = len(urls)
> +         requests = []
> +         for i, url in enumerate(urls):
> +-            requests.append(
> +-                build_request(url, bump=i, secure=self._secure)
> +-            )
> ++            requests.append(build_request(url, bump=i, secure=self._secure))
> +
> +-        max_threads = threads or self.config['threads']['download']
> +-        in_flight = {'threads': 0}
> ++        max_threads = threads or self.config["threads"]["download"]
> ++        in_flight = {"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=self._opener,
> +-                    shutdown_event=self._shutdown_event
> ++                    shutdown_event=self._shutdown_event,
> +                 )
> +-                while in_flight['threads'] >= max_threads:
> ++                while in_flight["threads"] >= max_threads:
> +                     timeit.time.sleep(0.001)
> +                 thread.start()
> +                 q.put(thread, True)
> +-                in_flight['threads'] += 1
> ++                in_flight["threads"] += 1
> +                 callback(i, request_count, start=True)
> +
> +         finished = []
> +@@ -1561,15 +1601,16 @@
> +                 thread = q.get(True)
> +                 while _is_alive(thread):
> +                     thread.join(timeout=0.001)
> +-                in_flight['threads'] -= 1
> ++                in_flight["threads"] -= 1
> +                 finished.append(sum(thread.result))
> +                 callback(thread.i, request_count, end=True)
> +
> +         q = Queue(max_threads)
> +-        prod_thread = threading.Thread(target=producer,
> +-                                       args=(q, requests, request_count))
> +-        cons_thread = threading.Thread(target=consumer,
> +-                                       args=(q, request_count))
> ++        prod_thread = threading.Thread(
> ++            target=producer,
> ++            args=(q, requests, request_count),
> ++        )
> ++        cons_thread = threading.Thread(target=consumer, args=(q, request_count))
> +         start = timeit.default_timer()
> +         prod_thread.start()
> +         cons_thread.start()
> +@@ -1581,11 +1622,9 @@
> +
> +         stop = timeit.default_timer()
> +         self.results.bytes_received = sum(finished)
> +-        self.results.download = (
> +-            (self.results.bytes_received / (stop - start)) * 8.0
> +-        )
> ++        self.results.download = (self.results.bytes_received / (stop - start)) * 8.0
> +         if self.results.download > 100000:
> +-            self.config['threads']['upload'] = 8
> ++            self.config["threads"]["upload"] = 8
> +         return self.results.download
> +
> +     def upload(self, callback=do_nothing, pre_allocate=True, threads=None):
> +@@ -1594,40 +1633,43 @@
> +         A ``threads`` value of ``None`` will fall back to those dictated
> +         by the speedtest.net configuration
> +         """
> +-
> +         sizes = []
> +
> +-        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 = len(sizes)
> +-        request_count = self.config['upload_max']
> ++        request_count = self.config["upload_max"]
> +
> +         requests = []
> +-        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 = HTTPUploaderData(
> +                 size,
> +                 0,
> +-                self.config['length']['upload'],
> +-                shutdown_event=self._shutdown_event
> ++                self.config["length"]["upload"],
> ++                shutdown_event=self._shutdown_event,
> +             )
> +             if pre_allocate:
> +                 data.pre_allocate()
> +
> +-            headers = {'Content-length': size}
> ++            headers = {"Content-length": size}
> +             requests.append(
> +                 (
> +-                    build_request(self.best['url'], data, secure=self._secure,
> +-                                  headers=headers),
> +-                    size
> +-                )
> ++                    build_request(
> ++                        self.best["url"],
> ++                        data,
> ++                        secure=self._secure,
> ++                        headers=headers,
> ++                    ),
> ++                    size,
> ++                ),
> +             )
> +
> +-        max_threads = threads or self.config['threads']['upload']
> +-        in_flight = {'threads': 0}
> ++        max_threads = threads or self.config["threads"]["upload"]
> ++        in_flight = {"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=self._opener,
> +-                    shutdown_event=self._shutdown_event
> ++                    shutdown_event=self._shutdown_event,
> +                 )
> +-                while in_flight['threads'] >= max_threads:
> ++                while in_flight["threads"] >= max_threads:
> +                     timeit.time.sleep(0.001)
> +                 thread.start()
> +                 q.put(thread, True)
> +-                in_flight['threads'] += 1
> ++                in_flight["threads"] += 1
> +                 callback(i, request_count, start=True)
> +
> +         finished = []
> +@@ -1655,15 +1697,16 @@
> +                 thread = q.get(True)
> +                 while _is_alive(thread):
> +                     thread.join(timeout=0.001)
> +-                in_flight['threads'] -= 1
> ++                in_flight["threads"] -= 1
> +                 finished.append(thread.result)
> +                 callback(thread.i, request_count, end=True)
> +
> +-        q = Queue(threads or self.config['threads']['upload'])
> +-        prod_thread = threading.Thread(target=producer,
> +-                                       args=(q, requests, request_count))
> +-        cons_thread = threading.Thread(target=consumer,
> +-                                       args=(q, request_count))
> ++        q = Queue(threads or self.config["threads"]["upload"])
> ++        prod_thread = threading.Thread(
> ++            target=producer,
> ++            args=(q, requests, request_count),
> ++        )
> ++        cons_thread = threading.Thread(target=consumer, args=(q, request_count))
> +         start = timeit.default_timer()
> +         prod_thread.start()
> +         cons_thread.start()
> +@@ -1675,9 +1718,7 @@
> +
> +         stop = timeit.default_timer()
> +         self.results.bytes_sent = sum(finished)
> +-        self.results.upload = (
> +-            (self.results.bytes_sent / (stop - start)) * 8.0
> +-        )
> ++        self.results.upload = (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=True)
> ++        printer("\nCancelling...", error=True)
> +         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=','):
> ++def csv_header(delimiter=","):
> +     """Print the CSV Headers"""
> +-
> +     printer(SpeedtestResults.csv_header(delimiter=delimiter))
> +     sys.exit(0)
> +
> +@@ -1710,11 +1751,12 @@
> + def parse_args():
> +     """Function to handle building and parsing of command line arguments"""
> +     description = (
> +-        '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 = ArgParser(description=description)
> +     # Give optparse.OptionParser an `add_argument` method for
> +@@ -1723,67 +1765,134 @@
> +         parser.add_argument = parser.add_option
> +     except AttributeError:
> +         pass
> +-    parser.add_argument('--no-download', dest='download', default=True,
> +-                        action='store_const', const=False,
> +-                        help='Do not perform download test')
> +-    parser.add_argument('--no-upload', dest='upload', default=True,
> +-                        action='store_const', const=False,
> +-                        help='Do not perform upload test')
> +-    parser.add_argument('--single', default=False, action='store_true',
> +-                        help='Only use a single connection instead of '
> +-                             'multiple. This simulates a typical file '
> +-                             'transfer.')
> +-    parser.add_argument('--bytes', dest='units', action='store_const',
> +-                        const=('byte', 8), default=('bit', 1),
> +-                        help='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='store_true',
> +-                        help='Generate and provide a URL to the speedtest.net '
> +-                             'share results image, not displayed with --csv')
> +-    parser.add_argument('--simple', action='store_true', default=False,
> +-                        help='Suppress verbose output, only show basic '
> +-                             'information')
> +-    parser.add_argument('--csv', action='store_true', default=False,
> +-                        help='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=',', type=PARSER_TYPE_STR,
> +-                        help='Single character delimiter to use in CSV '
> +-                             'output. Default ","')
> +-    parser.add_argument('--csv-header', action='store_true', default=False,
> +-                        help='Print CSV headers')
> +-    parser.add_argument('--json', action='store_true', default=False,
> +-                        help='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='store_true',
> +-                        help='Display a list of speedtest.net servers '
> +-                             'sorted by distance')
> +-    parser.add_argument('--server', type=PARSER_TYPE_INT, action='append',
> +-                        help='Specify a server ID to test against. Can be '
> +-                             'supplied multiple times')
> +-    parser.add_argument('--exclude', type=PARSER_TYPE_INT, action='append',
> +-                        help='Exclude a server from selection. Can be '
> +-                             'supplied multiple times')
> +-    parser.add_argument('--mini', help='URL of the Speedtest Mini server')
> +-    parser.add_argument('--source', help='Source IP address to bind to')
> +-    parser.add_argument('--timeout', default=10, type=PARSER_TYPE_FLOAT,
> +-                        help='HTTP timeout in seconds. Default 10')
> +-    parser.add_argument('--secure', action='store_true',
> +-                        help='Use HTTPS instead of HTTP when communicating '
> +-                             'with speedtest.net operated servers')
> +-    parser.add_argument('--no-pre-allocate', dest='pre_allocate',
> +-                        action='store_const', default=True, const=False,
> +-                        help='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='store_true',
> +-                        help='Show the version number and exit')
> +-    parser.add_argument('--debug', action='store_true',
> +-                        help=ARG_SUPPRESS, default=ARG_SUPPRESS)
> ++    parser.add_argument(
> ++        "--no-download",
> ++        dest="download",
> ++        default=True,
> ++        action="store_const",
> ++        const=False,
> ++        help="Do not perform download test",
> ++    )
> ++    parser.add_argument(
> ++        "--no-upload",
> ++        dest="upload",
> ++        default=True,
> ++        action="store_const",
> ++        const=False,
> ++        help="Do not perform upload test",
> ++    )
> ++    parser.add_argument(
> ++        "--single",
> ++        default=False,
> ++        action="store_true",
> ++        help="Only use a single connection instead of "
> ++        "multiple. This simulates a typical file "
> ++        "transfer.",
> ++    )
> ++    parser.add_argument(
> ++        "--bytes",
> ++        dest="units",
> ++        action="store_const",
> ++        const=("byte", 8),
> ++        default=("bit", 1),
> ++        help="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="store_true",
> ++        help="Generate and provide a URL to the speedtest.net "
> ++        "share results image, not displayed with --csv",
> ++    )
> ++    parser.add_argument(
> ++        "--simple",
> ++        action="store_true",
> ++        default=False,
> ++        help="Suppress verbose output, only show basic " "information",
> ++    )
> ++    parser.add_argument(
> ++        "--csv",
> ++        action="store_true",
> ++        default=False,
> ++        help="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=",",
> ++        type=PARSER_TYPE_STR,
> ++        help="Single character delimiter to use in CSV " 'output. Default ","',
> ++    )
> ++    parser.add_argument(
> ++        "--csv-header",
> ++        action="store_true",
> ++        default=False,
> ++        help="Print CSV headers",
> ++    )
> ++    parser.add_argument(
> ++        "--json",
> ++        action="store_true",
> ++        default=False,
> ++        help="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="store_true",
> ++        help="Display a list of speedtest.net servers " "sorted by distance",
> ++    )
> ++    parser.add_argument(
> ++        "--server",
> ++        type=PARSER_TYPE_INT,
> ++        action="append",
> ++        help="Specify a server ID to test against. Can be " "supplied multiple times",
> ++    )
> ++    parser.add_argument(
> ++        "--exclude",
> ++        type=PARSER_TYPE_INT,
> ++        action="append",
> ++        help="Exclude a server from selection. Can be " "supplied multiple times",
> ++    )
> ++    parser.add_argument("--mini", help="URL of the Speedtest Mini server")
> ++    parser.add_argument("--source", help="Source IP address to bind to")
> ++    parser.add_argument(
> ++        "--timeout",
> ++        default=10,
> ++        type=PARSER_TYPE_FLOAT,
> ++        help="HTTP timeout in seconds. Default 10",
> ++    )
> ++    parser.add_argument(
> ++        "--secure",
> ++        action="store_true",
> ++        help="Use HTTPS instead of HTTP when communicating "
> ++        "with speedtest.net operated servers",
> ++    )
> ++    parser.add_argument(
> ++        "--no-pre-allocate",
> ++        dest="pre_allocate",
> ++        action="store_const",
> ++        default=True,
> ++        const=False,
> ++        help="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="store_true",
> ++        help="Show the version number and exit",
> ++    )
> ++    parser.add_argument(
> ++        "--debug",
> ++        action="store_true",
> ++        help=ARG_SUPPRESS,
> ++        default=ARG_SUPPRESS,
> ++    )
> +
> +     options = parser.parse_args()
> +     if isinstance(options, tuple):
> +@@ -1801,32 +1910,30 @@
> +     with an error stating which module is missing.
> +     """
> +     optional_args = {
> +-        '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 unavailable")
> +
> +
> + def printer(string, quiet=False, debug=False, error=False, **kwargs):
> +     """Helper function print a string with various features"""
> +-
> +     if debug and not DEBUG:
> +         return
> +
> +     if debug:
> +         if sys.stdout.isatty():
> +-            out = '\033[1;30mDEBUG: %s\033[0m' % string
> ++            out = f"\x1b[1;30mDEBUG: {string}\x1b[0m"
> +         else:
> +-            out = 'DEBUG: %s' % string
> ++            out = f"DEBUG: {string}"
> +     else:
> +         out = string
> +
> +     if error:
> +-        kwargs['file'] = sys.stderr
> ++        kwargs["file"] = sys.stderr
> +
> +     if not quiet:
> +         print_(out, **kwargs)
> +@@ -1834,7 +1941,6 @@
> +
> + def shell():
> +     """Run the full speedtest.net test"""
> +-
> +     global DEBUG
> +     shutdown_event = 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) != 1:
> +-        raise SpeedtestCLIError('--csv-delimiter must be a single character')
> ++        raise SpeedtestCLIError("--csv-delimiter must be a single character")
> +
> +     if args.csv_header:
> +         csv_header(args.csv_delimiter)
> +
> +     validate_optional_args(args)
> +
> +-    debug = getattr(args, 'debug', False)
> +-    if debug == 'SUPPRESSHELP':
> ++    debug = getattr(args, "debug", False)
> ++    if debug == "SUPPRESSHELP":
> +         debug = False
> +     if debug:
> +         DEBUG = True
> +
> +-    if args.simple or args.csv or args.json:
> +-        quiet = True
> +-    else:
> +-        quiet = False
> ++    quiet = args.simple or args.csv or args.json
> +
> +-    if args.csv or args.json:
> +-        machine_format = True
> +-    else:
> +-        machine_format = False
> ++    machine_format = 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 = print_dots(shutdown_event)
> +
> +-    printer('Retrieving speedtest.net configuration...', quiet)
> ++    printer("Retrieving speedtest.net configuration...", quiet)
> +     try:
> +         speedtest = Speedtest(
> +             source_address=args.source,
> +             timeout=args.timeout,
> +-            secure=args.secure
> ++            secure=args.secure,
> +         )
> +     except (ConfigRetrievalError,) + HTTP_ERRORS:
> +-        printer('Cannot retrieve speedtest configuration', error=True)
> ++        printer("Cannot retrieve speedtest configuration", error=True)
> +         raise SpeedtestCLIError(get_exception())
> +
> +     if args.list:
> +         try:
> +             speedtest.get_servers()
> +         except (ServersRetrievalError,) + HTTP_ERRORS:
> +-            printer('Cannot retrieve speedtest server list', error=True)
> ++            printer("Cannot retrieve speedtest server list", error=True)
> +             raise SpeedtestCLIError(get_exception())
> +
> +         for _, servers in sorted(speedtest.servers.items()):
> +             for server in servers:
> +-                line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) '
> +-                        '[%(d)0.2f km]' % server)
> ++                line = (
> ++                    "%(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.config['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=args.server, exclude=args.exclude)
> +         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=True)
> ++            printer("Cannot retrieve speedtest server list", error=True)
> +             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) == 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 = 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=('', '\n')[bool(debug)])
> +-        speedtest.download(
> +-            callback=callback,
> +-            threads=(None, 1)[args.single]
> ++        printer("Testing download speed", quiet, end=("", "\n")[bool(debug)])
> ++        speedtest.download(callback=callback, threads=(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=('', '\n')[bool(debug)])
> ++        printer("Testing upload speed", quiet, end=("", "\n")[bool(debug)])
> +         speedtest.upload(
> +             callback=callback,
> +             pre_allocate=args.pre_allocate,
> +-            threads=(None, 1)[args.single]
> ++            threads=(None, 1)[args.single],
> ++        )
> ++        printer(
> ++            "Upload: %0.2f M%s/s"
> ++            % ((results.upload / 1000.0 / 1000.0) / args.units[1], args.units[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=True)
> ++    printer(f"Results:\n{results.dict()!r}", debug=True)
> +
> +     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=args.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=True)
> ++        printer("\nCancelling...", error=True)
> +     except (SpeedtestException, SystemExit):
> +         e = get_exception()
> +         # Ignore a successful exit, or argparse exit
> +-        if getattr(e, 'code', 1) not in (0, 2):
> +-            msg = '%s' % e
> ++        if getattr(e, "code", 1) not in (0, 2):
> ++            msg = f"{e}"
> +             if not msg:
> +-                msg = '%r' % e
> +-            raise SystemExit('ERROR: %s' % msg)
> ++                msg = f"{e!r}"
> ++            raise SystemExit(f"ERROR: {msg}")
> +
> +
> +-if __name__ == '__main__':
> ++if __name__ == "__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.000000000 +0200
> ++++ speedtest-cli-2.1.3/tests/scripts/source.py	2025-01-05 13:17:06.014037557 +0100
> +@@ -15,23 +15,19 @@
> + #    License for the specific language governing permissions and limitations
> + #    under the License.
> +
> +-import sys
> + import subprocess
> ++import sys
> +
> +-cmd = [sys.executable, 'speedtest.py', '--source', '127.0.0.1']
> ++cmd = [sys.executable, "speedtest.py", "--source", "127.0.0.1"]
> +
> +-p = subprocess.Popen(
> +-    cmd,
> +-    stdout=subprocess.PIPE,
> +-    stderr=subprocess.PIPE
> +-)
> ++p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
> +
> + stdout, stderr = p.communicate()
> +
> + if p.returncode != 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_remove_deprecated_method.patch b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_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_deprecated_method.patch
> @@ -0,0 +1,27 @@
> +Patch originally from
> +
> +From: Lavender <keqing.hu@icloud.com>
> +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 <adolf.belka@ipfire.org> 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/speedtest.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 = client or {}
> +
> +         self._share = None
> +-        self.timestamp = 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 >= 3 and sys.version_info.minor >= 11:
> ++            self.timestamp = f"{datetime.datetime.now(datetime.UTC).isoformat()}Z"
> ++        else:
> ++            self.timestamp = f"{datetime.datetime.utcnow().isoformat()}Z"
> +         self.bytes_received = 0
> +         self.bytes_sent = 0
> +
  

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  <info@ipfire.org>                     #
+# Copyright (C) 2007-2025  IPFire Team  <info@ipfire.org>                     #
 #                                                                             #
 # 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    = $(URL_IPFIRE)
 DIR_APP    = $(DIR_SRC)/$(THISAPP)
 TARGET     = $(DIR_INFO)/$(THISAPP)
 PROG       = speedtest-cli
-PAK_VER    = 5
+PAK_VER    = 6
 
 DEPS       =
 
@@ -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/speedtest-cli-2.1.3-python_3.10_support.patch
+	cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11_updates_and_fixes.patch
+	cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_errors.patch
+	cd $(DIR_APP) && patch -Np1 < $(DIR_SRC)/src/patches/speedtest-cli/speedtest-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=/
 	@rm -rf $(DIR_APP)
diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-fix_429_errors.patch 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 <marrco@wohecha.fr>
+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=None, exclude=None):
+                     )
+ 
+         urls = [
+-            "://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 = {}
+@@ -1346,56 +1344,34 @@ def get_servers(self, servers=None, exclude=None):
+                 printer(f"Servers XML:\n{serversxml}", debug=True)
+ 
+                 try:
+-                    try:
+-                        try:
+-                            root = ET.fromstring(serversxml)
+-                        except ET.ParseError:
+-                            e = get_exception()
+-                            raise SpeedtestServersError(
+-                                f"Malformed speedtest.net server list: {e}",
+-                            )
+-                        elements = etree_iter(root, "server")
+-                    except AttributeError:
+-                        try:
+-                            root = DOM.parseString(serversxml)
+-                        except ExpatError:
+-                            e = get_exception()
+-                            raise SpeedtestServersError(
+-                                f"Malformed speedtest.net server list: {e}",
+-                            )
+-                        elements = root.getElementsByTagName("server")
+-                except (SyntaxError, xml.parsers.expat.ExpatError):
++                    elements = json.loads(serversxml)
++                except SyntaxError:
+                     raise ServersRetrievalError()
+ 
+                 for server in elements:
+-                    try:
+-                        attrib = server.attrib
+-                    except AttributeError:
+-                        attrib = 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_servers"]
+-                        or int(attrib.get("id")) in exclude
++                        int(server.get("id")) in self.config["ignore_servers"]
++                        or int(server.get("id")) in exclude
+                     ):
+                         continue
+ 
+                     try:
+                         d = 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"] = d
++                    server["d"] = d
+ 
+                     try:
+-                        self.servers[d].append(attrib)
++                        self.servers[d].append(server)
+                     except KeyError:
+-                        self.servers[d] = [attrib]
++                        self.servers[d] = [server]
+ 
+                 break
+ 
+
diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.10_support.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.patch
@@ -0,0 +1,146 @@ 
+Patch originally from
+
+From 22210ca35228f0bbcef75a7c14587c4ecb875ab4 Mon Sep 17 00:00:00 2001
+From: Matt Martz <matt@sivel.net>
+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 <adolf.belka@ipfire.org> 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/speedtest.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.
+ 
+-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 = isSet
++
+ 
+ # Some global variables we use
+ DEBUG = False
+@@ -56,6 +58,7 @@
+ PY25PLUS = sys.version_info[:2] >= (2, 5)
+ PY26PLUS = sys.version_info[:2] >= (2, 6)
+ PY32PLUS = sys.version_info[:2] >= (3, 2)
++PY310PLUS = sys.version_info[:2] >= (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 = ET.Element.iter
+-elif PY25PLUS:
+-    etree_iter = ET_Element.getiterator
+-
+-if PY26PLUS:
+-    thread_is_alive = threading.Thread.is_alive
+-else:
+-    thread_is_alive = threading.Thread.isAlive
+-
+-
+ # Exception "constants" to support Python 2 through Python 3
+ try:
+     import ssl
+@@ -293,6 +285,23 @@
+     ssl = None
+     HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
+ 
++if PY32PLUS:
++    etree_iter = ET.Element.iter
++elif PY25PLUS:
++    etree_iter = ET_Element.getiterator
++
++if PY26PLUS:
++    thread_is_alive = threading.Thread.is_alive
++else:
++    thread_is_alive = 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=False, end=False):
+-        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) <= self.timeout:
+                 f = 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) <=
+                         self.timeout):
+                     self.result.append(len(f.read(10240)))
+@@ -864,7 +873,7 @@
+ 
+     def read(self, n=10240):
+         if ((timeit.default_timer() - self.start) <= self.timeout and
+-                not self._shutdown_event.isSet()):
++                not event_is_set(self._shutdown_event)):
+             chunk = self.data.read(n)
+             self.total.append(len(chunk))
+             return chunk
+@@ -902,7 +911,7 @@
+         request = self.request
+         try:
+             if ((timeit.default_timer() - self.starttime) <= self.timeout and
+-                    not self._shutdown_event.isSet()):
++                    not event_is_set(self._shutdown_event)):
+                 try:
+                     f = self._opener(request)
+                 except TypeError:
diff --git a/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_3.11_updates_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 <mark@there.co.nz>
+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 <adolf.belka@ipfire.org> is the same as 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.
+ 
++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 = codecs.open(os.path.join(here, *file_paths), 'r', 'latin1')
++        f = codecs.open(os.path.join(here, *file_paths), "r", "latin1")
+         version_file = f.read()
+         f.close()
+-    except:
++    except Exception:
+         raise RuntimeError("Unable to find version string.")
+ 
+     # The version line must have the form
+     # __version__ = 'ver'
+-    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
+-                              version_file, re.M)
++    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 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 = codecs.open('README.rst', encoding='utf-8')
++    f = codecs.open("README.rst", encoding="utf-8")
+     long_description = f.read()
+     f.close()
+-except:
+-    long_description = ''
++except Exception:
++    long_description = ""
+ 
+ 
+ setup(
+-    name='speedtest-cli',
+-    version=find_version('speedtest.py'),
+-    description=('Command line interface for testing internet bandwidth using '
+-                 'speedtest.net'),
++    name="speedtest-cli",
++    version=find_version("speedtest.py"),
++    description=(
++        "Command line interface for testing internet bandwidth using " "speedtest.net"
++    ),
+     long_description=long_description,
+-    keywords='speedtest speedtest.net',
+-    author='Matt Martz',
+-    author_email='matt@sivel.net',
+-    url='https://github.com/sivel/speedtest-cli',
+-    license='Apache License, Version 2.0',
+-    py_modules=['speedtest'],
++    keywords="speedtest speedtest.net",
++    author="Matt Martz",
++    author_email="matt@sivel.net",
++    url="https://github.com/sivel/speedtest-cli",
++    license="Apache License, Version 2.0",
++    py_modules=["speedtest"],
+     entry_points={
+-        'console_scripts': [
+-            'speedtest=speedtest:main',
+-            'speedtest-cli=speedtest:main'
+-        ]
++        "console_scripts": [
++            "speedtest=speedtest:main",
++            "speedtest-cli=speedtest:main",
++        ],
+     },
+     classifiers=[
+-        '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/speedtest.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 @@
+ 
+ try:
+     import gzip
++
+     GZIP_BASE = gzip.GzipFile
+ except ImportError:
+     gzip = None
+     GZIP_BASE = object
+ 
+-__version__ = '2.1.3'
++__version__ = "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 = 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 = None
+ 
+ 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, HTTPRedirectHandler,
+-                                HTTPErrorProcessor, OpenerDirector)
++    from urllib.request import (AbstractHTTPHandler, HTTPDefaultErrorHandler,
++                                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 = int
+     PARSER_TYPE_STR = str
+     PARSER_TYPE_FLOAT = float
+ except ImportError:
+-    from optparse import OptionParser as ArgParser
+     from optparse import SUPPRESS_HELP as ARG_SUPPRESS
+-    PARSER_TYPE_INT = 'int'
+-    PARSER_TYPE_STR = 'string'
+-    PARSER_TYPE_FLOAT = 'float'
++    from optparse import OptionParser as ArgParser
++
++    PARSER_TYPE_INT = "int"
++    PARSER_TYPE_STR = "string"
++    PARSER_TYPE_FLOAT = "float"
+ 
+ try:
+     from cStringIO import StringIO
++
+     BytesIO = None
+ except ImportError:
+     try:
+         from StringIO import StringIO
++
+         BytesIO = 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 = FileIO(f.fileno(), 'w')
+-            super(_Py3Utf8Output, self).__init__(
+-                buf,
+-                encoding='utf8',
+-                errors='strict'
+-            )
++            buf = FileIO(f.fileno(), "w")
++            super().__init__(buf, encoding="utf8", errors="strict")
+ 
+         def write(self, s):
+-            super(_Py3Utf8Output, self).write(s)
++            super().write(s)
+             self.flush()
+ 
+-    _py3_print = getattr(builtins, 'print')
++    _py3_print = getattr(builtins, "print")
+     try:
+         _py3_utf8_stdout = _Py3Utf8Output(sys.stdout)
+         _py3_utf8_stderr = _Py3Utf8Output(sys.stderr)
+@@ -188,23 +192,24 @@
+         _py3_utf8_stderr = 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') == sys.stderr:
+-            kwargs['file'] = _py3_utf8_stderr
++        """Wrapper function for py3 to print, with a utf-8 encoded stdout."""
++        if kwargs.get("file") == sys.stderr:
++            kwargs["file"] = _py3_utf8_stderr
+         else:
+-            kwargs['file'] = kwargs.get('file', _py3_utf8_stdout)
++            kwargs["file"] = 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 = str(data)
+             # If the file has an encoding, encode unicode with it.
+-            encoding = 'utf8'  # Always trust UTF-8 for output
+-            if (isinstance(fp, file) and
+-                    isinstance(data, unicode) and
+-                    encoding is not None):
++            encoding = "utf8"  # Always trust UTF-8 for output
++            if (
++                isinstance(fp, file)
++                and isinstance(data, unicode)
++                and encoding is not None
++            ):
+                 errors = getattr(fp, "errors", None)
+                 if errors is None:
+                     errors = "strict"
+                 data = data.encode(encoding, errors)
+             fp.write(data)
+             fp.flush()
++
+         want_unicode = False
+         sep = 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 = (ssl.CertificateError,)
+     except AttributeError:
+         CERT_ERROR = tuple()
+ 
+     HTTP_ERRORS = (
+-        (HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) +
+-        CERT_ERROR
+-    )
++        HTTPError,
++        URLError,
++        socket.error,
++        ssl.SSLError,
++        BadStatusLine,
++    ) + CERT_ERROR
+ except ImportError:
+     ssl = None
+     HTTP_ERRORS = (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=_GLOBAL_DEFAULT_TIMEOUT,
+-                      source_address=None):
++def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None):
+     """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 = address
+     err = 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 = kwargs.pop('source_address', None)
+-        timeout = kwargs.pop('timeout', 10)
++        source_address = kwargs.pop("source_address", None)
++        timeout = kwargs.pop("timeout", 10)
+ 
+         self._tunnel_host = None
+ 
+@@ -435,13 +446,13 @@
+             self.sock = socket.create_connection(
+                 (self.host, self.port),
+                 self.timeout,
+-                self.source_address
++                self.source_address,
+             )
+         except (AttributeError, TypeError):
+             self.sock = 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 = 443
+ 
+         def __init__(self, *args, **kwargs):
+-            source_address = kwargs.pop('source_address', None)
+-            timeout = kwargs.pop('timeout', 10)
++            source_address = kwargs.pop("source_address", None)
++            timeout = kwargs.pop("timeout", 10)
+ 
+             self._tunnel_host = None
+ 
+@@ -467,18 +480,18 @@
+             self.source_address = 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 = socket.create_connection(
+                     (self.host, self.port),
+                     self.timeout,
+-                    self.source_address
++                    self.source_address,
+                 )
+             except (AttributeError, TypeError):
+                 self.sock = 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 = {}
+-                    if hasattr(ssl, 'SSLContext'):
++                    if hasattr(ssl, "SSLContext"):
+                         if self._tunnel_host:
+-                            kwargs['server_hostname'] = self._tunnel_host
++                            kwargs["server_hostname"] = self._tunnel_host
+                         else:
+-                            kwargs['server_hostname'] = self.host
++                            kwargs["server_hostname"] = self.host
+                     self.sock = self._context.wrap_socket(self.sock, **kwargs)
+                 except AttributeError:
+                     self.sock = ssl.wrap_socket(self.sock)
+@@ -505,13 +518,13 @@
+                     self.sock = FakeSocket(self.sock, socket.ssl(self.sock))
+                 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",
+                 )
+ 
+ 
+@@ -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'] = context
++            kwargs["context"] = 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=0, source_address=None, timeout=10):
+         AbstractHTTPHandler.__init__(self, debuglevel)
+         self.source_address = source_address
+@@ -547,9 +560,9 @@
+             _build_connection(
+                 SpeedtestHTTPConnection,
+                 self.source_address,
+-                self.timeout
++                self.timeout,
+             ),
+-            req
++            req,
+         )
+ 
+     http_request = 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=0, context=None, source_address=None,
+-                 timeout=10):
++
++    def __init__(self, debuglevel=0, context=None, source_address=None, timeout=10):
+         AbstractHTTPHandler.__init__(self, debuglevel)
+         self._context = context
+         self.source_address = source_address
+@@ -574,7 +587,7 @@
+                 self.timeout,
+                 context=self._context,
+             ),
+-            req
++            req,
+         )
+ 
+     https_request = AbstractHTTPHandler.do_request_
+@@ -586,29 +599,25 @@
+     ``source_address`` for binding, ``timeout`` and our custom
+     `User-Agent`
+     """
+-
+-    printer('Timeout set to %d' % timeout, debug=True)
++    printer(f"Timeout set to {timeout}", debug=True)
+ 
+     if source_address:
+         source_address_tuple = (source_address, 0)
+-        printer('Binding to source address: %r' % (source_address_tuple,),
+-                debug=True)
++        printer(f"Binding to source address: {source_address_tuple!r}", debug=True)
+     else:
+         source_address_tuple = None
+ 
+     handlers = [
+         ProxyHandler(),
+-        SpeedtestHTTPHandler(source_address=source_address_tuple,
+-                             timeout=timeout),
+-        SpeedtestHTTPSHandler(source_address=source_address_tuple,
+-                              timeout=timeout),
++        SpeedtestHTTPHandler(source_address=source_address_tuple, timeout=timeout),
++        SpeedtestHTTPSHandler(source_address=source_address_tuple, timeout=timeout),
+         HTTPDefaultErrorHandler(),
+         HTTPRedirectHandler(),
+-        HTTPErrorProcessor()
++        HTTPErrorProcessor(),
+     ]
+ 
+     opener = OpenerDirector()
+-    opener.addheaders = [('User-agent', build_user_agent())]
++    opener.addheaders = [("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 = BytesIO or StringIO
+         self.io = IO()
+         while 1:
+@@ -637,7 +649,7 @@
+                 break
+             self.io.write(chunk)
+         self.io.seek(0)
+-        gzip.GzipFile.__init__(self, mode='rb', fileobj=self.io)
++        gzip.GzipFile.__init__(self, mode="rb", fileobj=self.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 = origin
+     lat2, lon2 = destination
+     radius = 6371  # km
+ 
+     dlat = math.radians(lat2 - lat1)
+     dlon = math.radians(lon2 - lon1)
+-    a = (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 = 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 = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
+     d = radius * c
+ 
+@@ -674,52 +684,47 @@
+ 
+ def build_user_agent():
+     """Build a Mozilla/5.0 compatible User-Agent string"""
+-
+     ua_tuple = (
+-        '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 = ' '.join(ua_tuple)
+-    printer('User-Agent: %s' % user_agent, debug=True)
++    user_agent = " ".join(ua_tuple)
++    printer(f"User-Agent: {user_agent}", debug=True)
+     return user_agent
+ 
+ 
+-def build_request(url, data=None, headers=None, bump='0', secure=False):
++def build_request(url, data=None, headers=None, bump="0", secure=False):
+     """Build a urllib2 request object
+ 
+     This function automatically adds a User-Agent header to all requests
+-
+     """
+-
+     if not headers:
+         headers = {}
+ 
+-    if url[0] == ':':
+-        scheme = ('http', 'https')[bool(secure)]
+-        schemed_url = '%s%s' % (scheme, url)
++    if url[0] == ":":
++        scheme = ("http", "https")[bool(secure)]
++        schemed_url = f"{scheme}{url}"
+     else:
+         schemed_url = url
+ 
+-    if '?' in url:
+-        delim = '&'
++    if "?" in url:
++        delim = "&"
+     else:
+-        delim = '?'
++        delim = "?"
+ 
+     # WHO YOU GONNA CALL? CACHE BUSTERS!
+-    final_url = '%s%sx=%s.%s' % (schemed_url, delim,
+-                                 int(timeit.time.time() * 1000),
+-                                 bump)
+-
+-    headers.update({
+-        'Cache-Control': 'no-cache',
+-    })
++    final_url = f"{schemed_url}{delim}x={int(timeit.time.time() * 1000)}.{bump}"
++
++    headers.update(
++        {
++            "Cache-Control": "no-cache",
++        },
++    )
+ 
+-    printer('%s %s' % (('GET', 'POST')[bool(data)], final_url),
+-            debug=True)
++    printer(f"{('GET', 'POST')[bool(data)]} {final_url}", debug=True)
+ 
+     return Request(final_url, data=data, headers=headers)
+ 
+@@ -729,7 +734,6 @@
+     establishing a connection with a HTTP/HTTPS request
+ 
+     """
+-
+     if opener:
+         _open = opener.open
+     else:
+@@ -738,7 +742,7 @@
+     try:
+         uh = _open(request)
+         if request.get_full_url() != uh.geturl():
+-            printer('Redirected to %s' % uh.geturl(), debug=True)
++            printer(f"Redirected to {uh.geturl()}", debug=True)
+         return uh, False
+     except HTTP_ERRORS:
+         e = get_exception()
+@@ -750,13 +754,12 @@
+     ``Content-Encoding`` is ``gzip`` otherwise the response itself
+ 
+     """
+-
+     try:
+         getheader = response.headers.getheader
+     except AttributeError:
+         getheader = response.getheader
+ 
+-    if getheader('content-encoding') == 'gzip':
++    if getheader("content-encoding") == "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=False, end=False):
+         if event_is_set(shutdown_event):
+             return
+ 
+-        sys.stdout.write('.')
++        sys.stdout.write(".")
+         if current + 1 == 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=None,
+-                 shutdown_event=None):
++    def __init__(self, i, request, start, timeout, opener=None, shutdown_event=None):
+         threading.Thread.__init__(self)
+         self.request = request
+         self.result = [0]
+@@ -817,9 +821,10 @@
+         try:
+             if (timeit.default_timer() - self.starttime) <= self.timeout:
+                 f = self._opener(self.request)
+-                while (not event_is_set(self._shutdown_event) and
+-                        (timeit.default_timer() - self.starttime) <=
+-                        self.timeout):
++                while (
++                    not event_is_set(self._shutdown_event)
++                    and (timeit.default_timer() - self.starttime) <= self.timeout
++                ):
+                     self.result.append(len(f.read(10240)))
+                     if self.result[-1] == 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 = [0]
+ 
+     def pre_allocate(self):
+-        chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
++        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+         multiplier = int(round(int(self.length) / 36.0))
+         IO = BytesIO or StringIO
+         try:
+             self._data = IO(
+-                ('content1=%s' %
+-                 (chars * multiplier)[0:int(self.length) - 9]
+-                 ).encode()
++                (f"content1={(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=10240):
+-        if ((timeit.default_timer() - self.start) <= self.timeout and
+-                not event_is_set(self._shutdown_event)):
++        if (timeit.default_timer() - self.start) <= self.timeout and not event_is_set(
++            self._shutdown_event,
++        ):
+             chunk = 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=None,
+-                 shutdown_event=None):
++    def __init__(
++        self,
++        i,
++        request,
++        start,
++        size,
++        timeout,
++        opener=None,
++        shutdown_event=None,
++    ):
+         threading.Thread.__init__(self)
+         self.request = request
+         self.request.data.start = self.starttime = start
+@@ -910,16 +921,19 @@
+     def run(self):
+         request = self.request
+         try:
+-            if ((timeit.default_timer() - self.starttime) <= self.timeout and
+-                    not event_is_set(self._shutdown_event)):
++            if (
++                timeit.default_timer() - self.starttime
++            ) <= self.timeout and not event_is_set(self._shutdown_event):
+                 try:
+                     f = self._opener(request)
+                 except TypeError:
+                     # PY24 expects a string or buffer
+                     # This also causes issues with Ctrl-C, but we will concede
+                     # for the moment that Ctrl-C on PY24 isn't immediate
+-                    request = build_request(self.request.get_full_url(),
+-                                            data=request.data.read(self.size))
++                    request = build_request(
++                        self.request.get_full_url(),
++                        data=request.data.read(self.size),
++                    )
+                     f = self._opener(request)
+                 f.read(11)
+                 f.close()
+@@ -932,7 +946,7 @@
+             self.result = 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=0, upload=0, ping=0, server=None, client=None,
+-                 opener=None, secure=False):
++    def __init__(
++        self,
++        download=0,
++        upload=0,
++        ping=0,
++        server=None,
++        client=None,
++        opener=None,
++        secure=False,
++    ):
+         self.download = download
+         self.upload = upload
+         self.ping = ping
+@@ -957,7 +979,7 @@
+         self.client = client or {}
+ 
+         self._share = None
+-        self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat()
++        self.timestamp = f"{datetime.datetime.utcnow().isoformat()}Z"
+         self.bytes_received = 0
+         self.bytes_sent = 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 parameters
+         # in a certain order
+         api_data = [
+-            'recommendedserverid=%s' % self.server['id'],
+-            'ping=%s' % ping,
+-            'screenresolution=',
+-            'promo=',
+-            'download=%s' % download,
+-            'screendpi=',
+-            'upload=%s' % upload,
+-            'testmethod=http',
+-            'hash=%s' % md5(('%s-%s-%s-%s' %
+-                             (ping, upload, download, '297aae72'))
+-                            .encode()).hexdigest(),
+-            'touchscreen=none',
+-            'startmode=pingselect',
+-            'accuracy=1',
+-            'bytesreceived=%s' % self.bytes_received,
+-            'bytessent=%s' % self.bytes_sent,
+-            'serverid=%s' % self.server['id'],
++            f"recommendedserverid={self.server['id']}",
++            f"ping={ping}",
++            "screenresolution=",
++            "promo=",
++            f"download={download}",
++            "screendpi=",
++            f"upload={upload}",
++            "testmethod=http",
++            "hash=%s"
++            % md5(
++                ("%s-%s-%s-%s" % (ping, upload, download, "297aae72")).encode(),
++            ).hexdigest(),
++            "touchscreen=none",
++            "startmode=pingselect",
++            "accuracy=1",
++            f"bytesreceived={self.bytes_received}",
++            f"bytessent={self.bytes_sent}",
++            f"serverid={self.server['id']}",
+         ]
+ 
+-        headers = {'Referer': 'http://c.speedtest.net/flash/speedtest.swf'}
+-        request = build_request('://www.speedtest.net/api/api.php',
+-                                data='&'.join(api_data).encode(),
+-                                headers=headers, secure=self._secure)
++        headers = {"Referer": "http://c.speedtest.net/flash/speedtest.swf"}
++        request = build_request(
++            "://www.speedtest.net/api/api.php",
++            data="&".join(api_data).encode(),
++            headers=headers,
++            secure=self._secure,
++        )
+         f, e = catch_request(request, opener=self._opener)
+         if e:
+             raise ShareResultsConnectFailure(e)
+@@ -1019,75 +1044,94 @@
+         f.close()
+ 
+         if int(code) != 200:
+-            raise ShareResultsSubmitFailure('Could not submit results to '
+-                                            'speedtest.net')
++            raise ShareResultsSubmitFailure(
++                "Could not submit results to " "speedtest.net",
++            )
+ 
+         qsargs = parse_qs(response.decode())
+-        resultid = qsargs.get('resultid')
++        resultid = qsargs.get("resultid")
+         if not resultid or len(resultid) != 1:
+-            raise ShareResultsSubmitFailure('Could not submit results to '
+-                                            'speedtest.net')
++            raise ShareResultsSubmitFailure(
++                "Could not submit results to " "speedtest.net",
++            )
+ 
+-        self._share = 'http://www.speedtest.net/result/%s.png' % resultid[0]
++        self._share = f"http://www.speedtest.net/result/{resultid[0]}.png"
+ 
+         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=','):
++    def csv_header(delimiter=","):
+         """Return CSV Headers"""
+-
+-        row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance',
+-               'Ping', 'Download', 'Upload', 'Share', 'IP Address']
++        row = [
++            "Server ID",
++            "Sponsor",
++            "Server Name",
++            "Timestamp",
++            "Distance",
++            "Ping",
++            "Download",
++            "Upload",
++            "Share",
++            "IP Address",
++        ]
+         out = StringIO()
+-        writer = csv.writer(out, delimiter=delimiter, lineterminator='')
++        writer = csv.writer(out, delimiter=delimiter, lineterminator="")
+         writer.writerow([to_utf8(v) for v in row])
+         return out.getvalue()
+ 
+-    def csv(self, delimiter=','):
++    def csv(self, delimiter=","):
+         """Return data in CSV format"""
+-
+         data = self.dict()
+         out = StringIO()
+-        writer = csv.writer(out, delimiter=delimiter, lineterminator='')
+-        row = [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 = csv.writer(out, delimiter=delimiter, lineterminator="")
++        row = [
++            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=False):
+         """Return data in JSON format"""
+-
+         kwargs = {}
+         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=None, source_address=None, timeout=10,
+-                 secure=False, shutdown_event=None):
++    def __init__(
++        self,
++        config=None,
++        source_address=None,
++        timeout=10,
++        secure=False,
++        shutdown_event=None,
++    ):
+         self.config = {}
+ 
+         self._source_address = source_address
+@@ -1110,7 +1154,7 @@
+         self._best = {}
+ 
+         self.results = SpeedtestResults(
+-            client=self.config['client'],
++            client=self.config["client"],
+             opener=self._opener,
+             secure=secure,
+         )
+@@ -1125,12 +1169,14 @@
+         """Download the speedtest.net configuration and return only the data
+         we are interested in
+         """
+-
+         headers = {}
+         if gzip:
+-            headers['Accept-Encoding'] = 'gzip'
+-        request = build_request('://www.speedtest.net/speedtest-config.php',
+-                                headers=headers, secure=self._secure)
++            headers["Accept-Encoding"] = "gzip"
++        request = build_request(
++            "://www.speedtest.net/speedtest-config.php",
++            headers=headers,
++            secure=self._secure,
++        )
+         uh, e = catch_request(request, opener=self._opener)
+         if e:
+             raise ConfigRetrievalError(e)
+@@ -1151,9 +1197,9 @@
+         if int(uh.code) != 200:
+             return None
+ 
+-        configxml = ''.encode().join(configxml_list)
++        configxml = "".encode().join(configxml_list)
+ 
+-        printer('Config XML:\n%s' % configxml, debug=True)
++        printer(f"Config XML:\n{configxml}", debug=True)
+ 
+         try:
+             try:
+@@ -1161,13 +1207,13 @@
+             except ET.ParseError:
+                 e = get_exception()
+                 raise SpeedtestConfigError(
+-                    'Malformed speedtest.net configuration: %s' % e
++                    f"Malformed speedtest.net configuration: {e}",
+                 )
+-            server_config = root.find('server-config').attrib
+-            download = root.find('download').attrib
+-            upload = root.find('upload').attrib
++            server_config = root.find("server-config").attrib
++            download = root.find("download").attrib
++            upload = root.find("upload").attrib
+             # times = root.find('times').attrib
+-            client = root.find('client').attrib
++            client = root.find("client").attrib
+ 
+         except AttributeError:
+             try:
+@@ -1175,65 +1221,61 @@
+             except ExpatError:
+                 e = get_exception()
+                 raise SpeedtestConfigError(
+-                    'Malformed speedtest.net configuration: %s' % e
++                    f"Malformed speedtest.net configuration: {e}",
+                 )
+-            server_config = get_attributes_by_tag_name(root, 'server-config')
+-            download = get_attributes_by_tag_name(root, 'download')
+-            upload = get_attributes_by_tag_name(root, 'upload')
++            server_config = get_attributes_by_tag_name(root, "server-config")
++            download = get_attributes_by_tag_name(root, "download")
++            upload = get_attributes_by_tag_name(root, "upload")
+             # times = get_attributes_by_tag_name(root, 'times')
+-            client = get_attributes_by_tag_name(root, 'client')
++            client = get_attributes_by_tag_name(root, "client")
+ 
+-        ignore_servers = [
+-            int(i) for i in server_config['ignoreids'].split(',') if i
+-        ]
++        ignore_servers = [int(i) for i in server_config["ignoreids"].split(",") if i]
+ 
+-        ratio = int(upload['ratio'])
+-        upload_max = int(upload['maxchunkcount'])
++        ratio = int(upload["ratio"])
++        upload_max = int(upload["maxchunkcount"])
+         up_sizes = [32768, 65536, 131072, 262144, 524288, 1048576, 7340032]
+         sizes = {
+-            '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],
+         }
+ 
+-        size_count = len(sizes['upload'])
++        size_count = len(sizes["upload"])
+ 
+         upload_count = int(math.ceil(upload_max / size_count))
+ 
+-        counts = {
+-            'upload': upload_count,
+-            'download': int(download['threadsperurl'])
+-        }
++        counts = {"upload": upload_count, "download": int(download["threadsperurl"])}
+ 
+         threads = {
+-            'upload': int(upload['threads']),
+-            'download': int(server_config['threadcount']) * 2
++            "upload": int(upload["threads"]),
++            "download": int(server_config["threadcount"]) * 2,
+         }
+ 
+         length = {
+-            '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 = (float(client['lat']), float(client['lon']))
++            self.lat_lon = (float(client["lat"]), float(client["lon"]))
+         except ValueError:
+             raise SpeedtestConfigError(
+-                'Unknown location: lat=%r lon=%r' %
+-                (client.get('lat'), client.get('lon'))
++                "Unknown location: lat=%r lon=%r"
++                % (client.get("lat"), client.get("lon")),
+             )
+ 
+-        printer('Config:\n%r' % self.config, debug=True)
++        printer(f"Config:\n{self.config!r}", debug=True)
+ 
+         return self.config
+ 
+@@ -1255,32 +1297,31 @@
+                     server_list[i] = 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 = [
+-            '://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 = {}
+         if gzip:
+-            headers['Accept-Encoding'] = 'gzip'
++            headers["Accept-Encoding"] = "gzip"
+ 
+         errors = []
+         for url in urls:
+             try:
+                 request = build_request(
+-                    '%s?threads=%s' % (url,
+-                                       self.config['threads']['download']),
++                    f"{url}?threads={self.config['threads']['download']}",
+                     headers=headers,
+-                    secure=self._secure
++                    secure=self._secure,
+                 )
+                 uh, e = catch_request(request, opener=self._opener)
+                 if e:
+-                    errors.append('%s' % e)
++                    errors.append(f"{e}")
+                     raise ServersRetrievalError()
+ 
+                 stream = get_response_stream(uh)
+@@ -1300,9 +1341,9 @@
+                 if int(uh.code) != 200:
+                     raise ServersRetrievalError()
+ 
+-                serversxml = ''.encode().join(serversxml_list)
++                serversxml = "".encode().join(serversxml_list)
+ 
+-                printer('Servers XML:\n%s' % serversxml, debug=True)
++                printer(f"Servers XML:\n{serversxml}", debug=True)
+ 
+                 try:
+                     try:
+@@ -1311,18 +1352,18 @@
+                         except ET.ParseError:
+                             e = get_exception()
+                             raise SpeedtestServersError(
+-                                'Malformed speedtest.net server list: %s' % e
++                                f"Malformed speedtest.net server list: {e}",
+                             )
+-                        elements = etree_iter(root, 'server')
++                        elements = etree_iter(root, "server")
+                     except AttributeError:
+                         try:
+                             root = DOM.parseString(serversxml)
+                         except ExpatError:
+                             e = get_exception()
+                             raise SpeedtestServersError(
+-                                'Malformed speedtest.net server list: %s' % e
++                                f"Malformed speedtest.net server list: {e}",
+                             )
+-                        elements = root.getElementsByTagName('server')
++                        elements = root.getElementsByTagName("server")
+                 except (SyntaxError, xml.parsers.expat.ExpatError):
+                     raise ServersRetrievalError()
+ 
+@@ -1332,21 +1373,24 @@
+                     except AttributeError:
+                         attrib = 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_servers']
+-                            or int(attrib.get('id')) in exclude):
++                    if (
++                        int(attrib.get("id")) in self.config["ignore_servers"]
++                        or int(attrib.get("id")) in exclude
++                    ):
+                         continue
+ 
+                     try:
+-                        d = distance(self.lat_lon,
+-                                     (float(attrib.get('lat')),
+-                                      float(attrib.get('lon'))))
++                        d = distance(
++                            self.lat_lon,
++                            (float(attrib.get("lat")), float(attrib.get("lon"))),
++                        )
+                     except Exception:
+                         continue
+ 
+-                    attrib['d'] = d
++                    attrib["d"] = 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 = urlparse(server)
+ 
+         name, ext = os.path.splitext(urlparts[2])
+@@ -1379,41 +1422,41 @@
+         request = build_request(url)
+         uh, e = catch_request(request, opener=self._opener)
+         if e:
+-            raise SpeedtestMiniConnectFailure('Failed to connect to %s' %
+-                                              server)
+-        else:
+-            text = uh.read()
+-            uh.close()
++            raise SpeedtestMiniConnectFailure(f"Failed to connect to {server}")
++        text = uh.read()
++        uh.close()
+ 
+-        extension = re.findall('upload_?[Ee]xtension: "([^"]+)"',
+-                               text.decode())
++        extension = 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 = self._opener.open(
+-                        '%s/speedtest/upload.%s' % (url, ext)
+-                    )
++                    f = self._opener.open(f"{url}/speedtest/upload.{ext}")
+                 except Exception:
+                     pass
+                 else:
+                     data = f.read().strip().decode()
+-                    if (f.code == 200 and
+-                            len(data.splitlines()) == 1 and
+-                            re.match('size=[0-9]', data)):
++                    if (
++                        f.code == 200
++                        and len(data.splitlines()) == 1
++                        and re.match("size=[0-9]", data)
++                    ):
+                         extension = [ext]
+                         break
+         if not urlparts or not extension:
+-            raise InvalidSpeedtestMiniServer('Invalid Speedtest Mini Server: '
+-                                             '%s' % server)
++            raise InvalidSpeedtestMiniServer(
++                "Invalid Speedtest Mini Server: " "%s" % server,
++            )
+ 
+-        self.servers = [{
+-            'sponsor': 'Speedtest Mini',
+-            'name': urlparts[1],
+-            'd': 0,
+-            'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]),
+-            'latency': 0,
+-            'id': 0
+-        }]
++        self.servers = [
++            {
++                "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=True)
++        printer(f"Closest Servers:\n{self.closest!r}", debug=True)
+         return self.closest
+ 
+     def get_best_server(self, servers=None):
+         """Perform a speedtest.net "ping" to determine which speedtest.net
+         server has the lowest latency
+         """
+-
+         if not servers:
+             if not self.closest:
+                 servers = self.get_closest_servers()
+@@ -1457,39 +1498,38 @@
+         results = {}
+         for server in servers:
+             cum = []
+-            url = os.path.dirname(server['url'])
++            url = os.path.dirname(server["url"])
+             stamp = int(timeit.time.time() * 1000)
+-            latency_url = '%s/latency.txt?x=%s' % (url, stamp)
++            latency_url = f"{url}/latency.txt?x={stamp}"
+             for i in range(0, 3):
+-                this_latency_url = '%s.%s' % (latency_url, i)
+-                printer('%s %s' % ('GET', this_latency_url),
+-                        debug=True)
++                this_latency_url = f"{latency_url}.{i}"
++                printer(f"{'GET'} {this_latency_url}", debug=True)
+                 urlparts = urlparse(latency_url)
+                 try:
+-                    if urlparts[0] == 'https':
++                    if urlparts[0] == "https":
+                         h = SpeedtestHTTPSConnection(
+                             urlparts[1],
+-                            source_address=source_address_tuple
++                            source_address=source_address_tuple,
+                         )
+                     else:
+                         h = SpeedtestHTTPConnection(
+                             urlparts[1],
+-                            source_address=source_address_tuple
++                            source_address=source_address_tuple,
+                         )
+-                    headers = {'User-Agent': user_agent}
+-                    path = '%s?%s' % (urlparts[2], urlparts[4])
++                    headers = {"User-Agent": user_agent}
++                    path = f"{urlparts[2]}?{urlparts[4]}"
+                     start = timeit.default_timer()
+                     h.request("GET", path, headers=headers)
+                     r = h.getresponse()
+-                    total = (timeit.default_timer() - start)
++                    total = timeit.default_timer() - start
+                 except HTTP_ERRORS:
+                     e = get_exception()
+-                    printer('ERROR: %r' % e, debug=True)
++                    printer(f"ERROR: {e!r}", debug=True)
+                     cum.append(3600)
+                     continue
+ 
+                 text = r.read(9)
+-                if int(r.status) == 200 and text == 'test=test'.encode():
++                if int(r.status) == 200 and text == "test=test".encode():
+                     cum.append(total)
+                 else:
+                     cum.append(3600)
+@@ -1501,16 +1541,17 @@
+         try:
+             fastest = 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 = results[fastest]
+-        best['latency'] = fastest
++        best["latency"] = fastest
+ 
+         self.results.ping = fastest
+         self.results.server = best
+ 
+         self._best.update(best)
+-        printer('Best Server:\n%r' % best, debug=True)
++        printer(f"Best Server:\n{best!r}", debug=True)
+         return best
+ 
+     def download(self, callback=do_nothing, threads=None):
+@@ -1519,22 +1560,21 @@
+         A ``threads`` value of ``None`` will fall back to those dictated
+         by the speedtest.net configuration
+         """
+-
+         urls = []
+-        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),
++                )
+ 
+         request_count = len(urls)
+         requests = []
+         for i, url in enumerate(urls):
+-            requests.append(
+-                build_request(url, bump=i, secure=self._secure)
+-            )
++            requests.append(build_request(url, bump=i, secure=self._secure))
+ 
+-        max_threads = threads or self.config['threads']['download']
+-        in_flight = {'threads': 0}
++        max_threads = threads or self.config["threads"]["download"]
++        in_flight = {"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=self._opener,
+-                    shutdown_event=self._shutdown_event
++                    shutdown_event=self._shutdown_event,
+                 )
+-                while in_flight['threads'] >= max_threads:
++                while in_flight["threads"] >= max_threads:
+                     timeit.time.sleep(0.001)
+                 thread.start()
+                 q.put(thread, True)
+-                in_flight['threads'] += 1
++                in_flight["threads"] += 1
+                 callback(i, request_count, start=True)
+ 
+         finished = []
+@@ -1561,15 +1601,16 @@
+                 thread = q.get(True)
+                 while _is_alive(thread):
+                     thread.join(timeout=0.001)
+-                in_flight['threads'] -= 1
++                in_flight["threads"] -= 1
+                 finished.append(sum(thread.result))
+                 callback(thread.i, request_count, end=True)
+ 
+         q = Queue(max_threads)
+-        prod_thread = threading.Thread(target=producer,
+-                                       args=(q, requests, request_count))
+-        cons_thread = threading.Thread(target=consumer,
+-                                       args=(q, request_count))
++        prod_thread = threading.Thread(
++            target=producer,
++            args=(q, requests, request_count),
++        )
++        cons_thread = threading.Thread(target=consumer, args=(q, request_count))
+         start = timeit.default_timer()
+         prod_thread.start()
+         cons_thread.start()
+@@ -1581,11 +1622,9 @@
+ 
+         stop = timeit.default_timer()
+         self.results.bytes_received = sum(finished)
+-        self.results.download = (
+-            (self.results.bytes_received / (stop - start)) * 8.0
+-        )
++        self.results.download = (self.results.bytes_received / (stop - start)) * 8.0
+         if self.results.download > 100000:
+-            self.config['threads']['upload'] = 8
++            self.config["threads"]["upload"] = 8
+         return self.results.download
+ 
+     def upload(self, callback=do_nothing, pre_allocate=True, threads=None):
+@@ -1594,40 +1633,43 @@
+         A ``threads`` value of ``None`` will fall back to those dictated
+         by the speedtest.net configuration
+         """
+-
+         sizes = []
+ 
+-        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 = len(sizes)
+-        request_count = self.config['upload_max']
++        request_count = self.config["upload_max"]
+ 
+         requests = []
+-        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 = HTTPUploaderData(
+                 size,
+                 0,
+-                self.config['length']['upload'],
+-                shutdown_event=self._shutdown_event
++                self.config["length"]["upload"],
++                shutdown_event=self._shutdown_event,
+             )
+             if pre_allocate:
+                 data.pre_allocate()
+ 
+-            headers = {'Content-length': size}
++            headers = {"Content-length": size}
+             requests.append(
+                 (
+-                    build_request(self.best['url'], data, secure=self._secure,
+-                                  headers=headers),
+-                    size
+-                )
++                    build_request(
++                        self.best["url"],
++                        data,
++                        secure=self._secure,
++                        headers=headers,
++                    ),
++                    size,
++                ),
+             )
+ 
+-        max_threads = threads or self.config['threads']['upload']
+-        in_flight = {'threads': 0}
++        max_threads = threads or self.config["threads"]["upload"]
++        in_flight = {"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=self._opener,
+-                    shutdown_event=self._shutdown_event
++                    shutdown_event=self._shutdown_event,
+                 )
+-                while in_flight['threads'] >= max_threads:
++                while in_flight["threads"] >= max_threads:
+                     timeit.time.sleep(0.001)
+                 thread.start()
+                 q.put(thread, True)
+-                in_flight['threads'] += 1
++                in_flight["threads"] += 1
+                 callback(i, request_count, start=True)
+ 
+         finished = []
+@@ -1655,15 +1697,16 @@
+                 thread = q.get(True)
+                 while _is_alive(thread):
+                     thread.join(timeout=0.001)
+-                in_flight['threads'] -= 1
++                in_flight["threads"] -= 1
+                 finished.append(thread.result)
+                 callback(thread.i, request_count, end=True)
+ 
+-        q = Queue(threads or self.config['threads']['upload'])
+-        prod_thread = threading.Thread(target=producer,
+-                                       args=(q, requests, request_count))
+-        cons_thread = threading.Thread(target=consumer,
+-                                       args=(q, request_count))
++        q = Queue(threads or self.config["threads"]["upload"])
++        prod_thread = threading.Thread(
++            target=producer,
++            args=(q, requests, request_count),
++        )
++        cons_thread = threading.Thread(target=consumer, args=(q, request_count))
+         start = timeit.default_timer()
+         prod_thread.start()
+         cons_thread.start()
+@@ -1675,9 +1718,7 @@
+ 
+         stop = timeit.default_timer()
+         self.results.bytes_sent = sum(finished)
+-        self.results.upload = (
+-            (self.results.bytes_sent / (stop - start)) * 8.0
+-        )
++        self.results.upload = (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=True)
++        printer("\nCancelling...", error=True)
+         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=','):
++def csv_header(delimiter=","):
+     """Print the CSV Headers"""
+-
+     printer(SpeedtestResults.csv_header(delimiter=delimiter))
+     sys.exit(0)
+ 
+@@ -1710,11 +1751,12 @@
+ def parse_args():
+     """Function to handle building and parsing of command line arguments"""
+     description = (
+-        '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 = ArgParser(description=description)
+     # Give optparse.OptionParser an `add_argument` method for
+@@ -1723,67 +1765,134 @@
+         parser.add_argument = parser.add_option
+     except AttributeError:
+         pass
+-    parser.add_argument('--no-download', dest='download', default=True,
+-                        action='store_const', const=False,
+-                        help='Do not perform download test')
+-    parser.add_argument('--no-upload', dest='upload', default=True,
+-                        action='store_const', const=False,
+-                        help='Do not perform upload test')
+-    parser.add_argument('--single', default=False, action='store_true',
+-                        help='Only use a single connection instead of '
+-                             'multiple. This simulates a typical file '
+-                             'transfer.')
+-    parser.add_argument('--bytes', dest='units', action='store_const',
+-                        const=('byte', 8), default=('bit', 1),
+-                        help='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='store_true',
+-                        help='Generate and provide a URL to the speedtest.net '
+-                             'share results image, not displayed with --csv')
+-    parser.add_argument('--simple', action='store_true', default=False,
+-                        help='Suppress verbose output, only show basic '
+-                             'information')
+-    parser.add_argument('--csv', action='store_true', default=False,
+-                        help='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=',', type=PARSER_TYPE_STR,
+-                        help='Single character delimiter to use in CSV '
+-                             'output. Default ","')
+-    parser.add_argument('--csv-header', action='store_true', default=False,
+-                        help='Print CSV headers')
+-    parser.add_argument('--json', action='store_true', default=False,
+-                        help='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='store_true',
+-                        help='Display a list of speedtest.net servers '
+-                             'sorted by distance')
+-    parser.add_argument('--server', type=PARSER_TYPE_INT, action='append',
+-                        help='Specify a server ID to test against. Can be '
+-                             'supplied multiple times')
+-    parser.add_argument('--exclude', type=PARSER_TYPE_INT, action='append',
+-                        help='Exclude a server from selection. Can be '
+-                             'supplied multiple times')
+-    parser.add_argument('--mini', help='URL of the Speedtest Mini server')
+-    parser.add_argument('--source', help='Source IP address to bind to')
+-    parser.add_argument('--timeout', default=10, type=PARSER_TYPE_FLOAT,
+-                        help='HTTP timeout in seconds. Default 10')
+-    parser.add_argument('--secure', action='store_true',
+-                        help='Use HTTPS instead of HTTP when communicating '
+-                             'with speedtest.net operated servers')
+-    parser.add_argument('--no-pre-allocate', dest='pre_allocate',
+-                        action='store_const', default=True, const=False,
+-                        help='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='store_true',
+-                        help='Show the version number and exit')
+-    parser.add_argument('--debug', action='store_true',
+-                        help=ARG_SUPPRESS, default=ARG_SUPPRESS)
++    parser.add_argument(
++        "--no-download",
++        dest="download",
++        default=True,
++        action="store_const",
++        const=False,
++        help="Do not perform download test",
++    )
++    parser.add_argument(
++        "--no-upload",
++        dest="upload",
++        default=True,
++        action="store_const",
++        const=False,
++        help="Do not perform upload test",
++    )
++    parser.add_argument(
++        "--single",
++        default=False,
++        action="store_true",
++        help="Only use a single connection instead of "
++        "multiple. This simulates a typical file "
++        "transfer.",
++    )
++    parser.add_argument(
++        "--bytes",
++        dest="units",
++        action="store_const",
++        const=("byte", 8),
++        default=("bit", 1),
++        help="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="store_true",
++        help="Generate and provide a URL to the speedtest.net "
++        "share results image, not displayed with --csv",
++    )
++    parser.add_argument(
++        "--simple",
++        action="store_true",
++        default=False,
++        help="Suppress verbose output, only show basic " "information",
++    )
++    parser.add_argument(
++        "--csv",
++        action="store_true",
++        default=False,
++        help="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=",",
++        type=PARSER_TYPE_STR,
++        help="Single character delimiter to use in CSV " 'output. Default ","',
++    )
++    parser.add_argument(
++        "--csv-header",
++        action="store_true",
++        default=False,
++        help="Print CSV headers",
++    )
++    parser.add_argument(
++        "--json",
++        action="store_true",
++        default=False,
++        help="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="store_true",
++        help="Display a list of speedtest.net servers " "sorted by distance",
++    )
++    parser.add_argument(
++        "--server",
++        type=PARSER_TYPE_INT,
++        action="append",
++        help="Specify a server ID to test against. Can be " "supplied multiple times",
++    )
++    parser.add_argument(
++        "--exclude",
++        type=PARSER_TYPE_INT,
++        action="append",
++        help="Exclude a server from selection. Can be " "supplied multiple times",
++    )
++    parser.add_argument("--mini", help="URL of the Speedtest Mini server")
++    parser.add_argument("--source", help="Source IP address to bind to")
++    parser.add_argument(
++        "--timeout",
++        default=10,
++        type=PARSER_TYPE_FLOAT,
++        help="HTTP timeout in seconds. Default 10",
++    )
++    parser.add_argument(
++        "--secure",
++        action="store_true",
++        help="Use HTTPS instead of HTTP when communicating "
++        "with speedtest.net operated servers",
++    )
++    parser.add_argument(
++        "--no-pre-allocate",
++        dest="pre_allocate",
++        action="store_const",
++        default=True,
++        const=False,
++        help="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="store_true",
++        help="Show the version number and exit",
++    )
++    parser.add_argument(
++        "--debug",
++        action="store_true",
++        help=ARG_SUPPRESS,
++        default=ARG_SUPPRESS,
++    )
+ 
+     options = parser.parse_args()
+     if isinstance(options, tuple):
+@@ -1801,32 +1910,30 @@
+     with an error stating which module is missing.
+     """
+     optional_args = {
+-        '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 unavailable")
+ 
+ 
+ def printer(string, quiet=False, debug=False, error=False, **kwargs):
+     """Helper function print a string with various features"""
+-
+     if debug and not DEBUG:
+         return
+ 
+     if debug:
+         if sys.stdout.isatty():
+-            out = '\033[1;30mDEBUG: %s\033[0m' % string
++            out = f"\x1b[1;30mDEBUG: {string}\x1b[0m"
+         else:
+-            out = 'DEBUG: %s' % string
++            out = f"DEBUG: {string}"
+     else:
+         out = string
+ 
+     if error:
+-        kwargs['file'] = sys.stderr
++        kwargs["file"] = sys.stderr
+ 
+     if not quiet:
+         print_(out, **kwargs)
+@@ -1834,7 +1941,6 @@
+ 
+ def shell():
+     """Run the full speedtest.net test"""
+-
+     global DEBUG
+     shutdown_event = 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) != 1:
+-        raise SpeedtestCLIError('--csv-delimiter must be a single character')
++        raise SpeedtestCLIError("--csv-delimiter must be a single character")
+ 
+     if args.csv_header:
+         csv_header(args.csv_delimiter)
+ 
+     validate_optional_args(args)
+ 
+-    debug = getattr(args, 'debug', False)
+-    if debug == 'SUPPRESSHELP':
++    debug = getattr(args, "debug", False)
++    if debug == "SUPPRESSHELP":
+         debug = False
+     if debug:
+         DEBUG = True
+ 
+-    if args.simple or args.csv or args.json:
+-        quiet = True
+-    else:
+-        quiet = False
++    quiet = args.simple or args.csv or args.json
+ 
+-    if args.csv or args.json:
+-        machine_format = True
+-    else:
+-        machine_format = False
++    machine_format = 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 = print_dots(shutdown_event)
+ 
+-    printer('Retrieving speedtest.net configuration...', quiet)
++    printer("Retrieving speedtest.net configuration...", quiet)
+     try:
+         speedtest = Speedtest(
+             source_address=args.source,
+             timeout=args.timeout,
+-            secure=args.secure
++            secure=args.secure,
+         )
+     except (ConfigRetrievalError,) + HTTP_ERRORS:
+-        printer('Cannot retrieve speedtest configuration', error=True)
++        printer("Cannot retrieve speedtest configuration", error=True)
+         raise SpeedtestCLIError(get_exception())
+ 
+     if args.list:
+         try:
+             speedtest.get_servers()
+         except (ServersRetrievalError,) + HTTP_ERRORS:
+-            printer('Cannot retrieve speedtest server list', error=True)
++            printer("Cannot retrieve speedtest server list", error=True)
+             raise SpeedtestCLIError(get_exception())
+ 
+         for _, servers in sorted(speedtest.servers.items()):
+             for server in servers:
+-                line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) '
+-                        '[%(d)0.2f km]' % server)
++                line = (
++                    "%(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.config['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=args.server, exclude=args.exclude)
+         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=True)
++            printer("Cannot retrieve speedtest server list", error=True)
+             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) == 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 = 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=('', '\n')[bool(debug)])
+-        speedtest.download(
+-            callback=callback,
+-            threads=(None, 1)[args.single]
++        printer("Testing download speed", quiet, end=("", "\n")[bool(debug)])
++        speedtest.download(callback=callback, threads=(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=('', '\n')[bool(debug)])
++        printer("Testing upload speed", quiet, end=("", "\n")[bool(debug)])
+         speedtest.upload(
+             callback=callback,
+             pre_allocate=args.pre_allocate,
+-            threads=(None, 1)[args.single]
++            threads=(None, 1)[args.single],
++        )
++        printer(
++            "Upload: %0.2f M%s/s"
++            % ((results.upload / 1000.0 / 1000.0) / args.units[1], args.units[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=True)
++    printer(f"Results:\n{results.dict()!r}", debug=True)
+ 
+     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=args.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=True)
++        printer("\nCancelling...", error=True)
+     except (SpeedtestException, SystemExit):
+         e = get_exception()
+         # Ignore a successful exit, or argparse exit
+-        if getattr(e, 'code', 1) not in (0, 2):
+-            msg = '%s' % e
++        if getattr(e, "code", 1) not in (0, 2):
++            msg = f"{e}"
+             if not msg:
+-                msg = '%r' % e
+-            raise SystemExit('ERROR: %s' % msg)
++                msg = f"{e!r}"
++            raise SystemExit(f"ERROR: {msg}")
+ 
+ 
+-if __name__ == '__main__':
++if __name__ == "__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.000000000 +0200
++++ speedtest-cli-2.1.3/tests/scripts/source.py	2025-01-05 13:17:06.014037557 +0100
+@@ -15,23 +15,19 @@
+ #    License for the specific language governing permissions and limitations
+ #    under the License.
+ 
+-import sys
+ import subprocess
++import sys
+ 
+-cmd = [sys.executable, 'speedtest.py', '--source', '127.0.0.1']
++cmd = [sys.executable, "speedtest.py", "--source", "127.0.0.1"]
+ 
+-p = subprocess.Popen(
+-    cmd,
+-    stdout=subprocess.PIPE,
+-    stderr=subprocess.PIPE
+-)
++p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ 
+ stdout, stderr = p.communicate()
+ 
+ if p.returncode != 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_remove_deprecated_method.patch b/src/patches/speedtest-cli/speedtest-cli-2.1.3-python_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_deprecated_method.patch
@@ -0,0 +1,27 @@ 
+Patch originally from
+
+From: Lavender <keqing.hu@icloud.com>
+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 <adolf.belka@ipfire.org> 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/speedtest.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 = client or {}
+ 
+         self._share = None
+-        self.timestamp = 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 >= 3 and sys.version_info.minor >= 11:
++            self.timestamp = f"{datetime.datetime.now(datetime.UTC).isoformat()}Z"
++        else:
++            self.timestamp = f"{datetime.datetime.utcnow().isoformat()}Z"
+         self.bytes_received = 0
+         self.bytes_sent = 0
+