From: Bernhard Bitsch <bbitsch@ipfire.org>
To: development@lists.ipfire.org
Subject: Re: [PATCH] speedtest-cli: Fix for bug13805 - error message if run on hour or half hour
Date: Tue, 07 Jan 2025 20:50:10 +0100 [thread overview]
Message-ID: <5deda6da-8780-449f-8a79-c08b6258e663@ipfire.org> (raw)
In-Reply-To: <20250106135226.13854-1-adolf.belka@ipfire.org>
[-- Attachment #1: Type: text/plain, Size: 109399 bytes --]
Testeed-by: Bernhard Bitsch <bbitsch(a)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(a)ipfire.org>
> Signed-off-by: Adolf Belka <adolf.belka(a)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(a)ipfire.org> #
> +# Copyright (C) 2007-2025 IPFire Team <info(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)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
> +
next prev parent reply other threads:[~2025-01-07 19:50 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-01-06 13:52 Adolf Belka
2025-01-07 19:50 ` Bernhard Bitsch [this message]
2025-01-07 19:51 ` Bernhard Bitsch
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=5deda6da-8780-449f-8a79-c08b6258e663@ipfire.org \
--to=bbitsch@ipfire.org \
--cc=development@lists.ipfire.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox