From 22210ca35228f0bbcef75a7c14587c4ecb875ab4 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 7 Jul 2021 14:50:15 -0500 Subject: [PATCH 1/4] Python 3.10 support --- setup.py | 3 +++ speedtest.py | 55 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index a617be425..f3d21add4 100644 --- a/setup.py +++ b/setup.py @@ -92,5 +92,8 @@ def find_version(*file_paths): '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 --git a/speedtest.py b/speedtest.py index a33296d3a..186b52929 100755 --- a/speedtest.py +++ b/speedtest.py @@ -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: @@ -36,7 +36,7 @@ gzip = None GZIP_BASE = object -__version__ = '2.1.3' +__version__ = '2.1.4b1' class FakeShutdownEvent(object): @@ -49,6 +49,8 @@ def isSet(): "Dummy method to always return false""" return False + is_set = isSet + # Some global variables we use DEBUG = False @@ -56,6 +58,7 @@ def isSet(): 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 @@ def write(data): 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 @@ def write(data): 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 @@ def print_dots(shutdown_event): 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 @@ def run(self): 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 data(self): 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 @@ def run(self): 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: From dcaf7870809765f72d8db607f769e105a48d4e71 Mon Sep 17 00:00:00 2001 From: edseldim Date: Sat, 2 Jul 2022 17:22:57 -0500 Subject: [PATCH 2/4] Added feature to select custom server from url --- .gitignore | 3 ++ speedtest.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index f24cd9952..28020b2a8 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ pip-log.txt #Mr Developer .mr.developer.cfg + +#Environment +speedtest-env diff --git a/speedtest.py b/speedtest.py index 186b52929..a05fdf436 100755 --- a/speedtest.py +++ b/speedtest.py @@ -29,6 +29,8 @@ import timeit import xml.parsers.expat +import requests + try: import gzip GZIP_BASE = gzip.GzipFile @@ -1237,7 +1239,21 @@ def get_config(self): return self.config - def get_servers(self, servers=None, exclude=None): + def json_to_xml(self,json_url=None): + if json_url: + r = requests.get(json_url) + if r.status_code == 200: + json_data = json.loads(r.text)[0] + message = '\n\n' + try: + message += f'' + except (KeyError,SyntaxError) as e: + pass + message += "\n\n\n" + return message.replace("&","").encode() + + + def get_servers(self, servers=None, exclude=None, custom_server=None): """Retrieve a the list of speedtest.net servers, optionally filtered to servers matching those specified in the ``servers`` argument """ @@ -1270,6 +1286,65 @@ def get_servers(self, servers=None, exclude=None): headers['Accept-Encoding'] = 'gzip' errors = [] + if custom_server: + serversxml = "".encode().join([self.json_to_xml(custom_server)]) + try: + try: + try: + root = ET.fromstring(serversxml) + except ET.ParseError: + e = get_exception() + raise SpeedtestServersError( + 'Malformed speedtest.net server list: %s' % e + ) + 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 + ) + elements = root.getElementsByTagName('server') + except (SyntaxError, xml.parsers.expat.ExpatError): + 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: + continue + + 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')))) + except Exception: + continue + + attrib['d'] = d + + try: + self.servers[d].append(attrib) + except KeyError: + self.servers[d] = [attrib] + + except ServersRetrievalError: + pass + + if (servers or exclude) and not self.servers: + raise NoMatchedServers() + + return self.servers + for url in urls: try: request = build_request( @@ -1299,9 +1374,8 @@ def get_servers(self, servers=None, exclude=None): if int(uh.code) != 200: raise ServersRetrievalError() - serversxml = ''.encode().join(serversxml_list) - + print(serversxml) printer('Servers XML:\n%s' % serversxml, debug=True) try: @@ -1784,6 +1858,7 @@ def parse_args(): help='Show the version number and exit') parser.add_argument('--debug', action='store_true', help=ARG_SUPPRESS, default=ARG_SUPPRESS) + parser.add_argument('--custom',help="Test with a custom server using its link") options = parser.parse_args() if isinstance(options, tuple): @@ -1916,7 +1991,7 @@ def shell(): if not args.mini: printer('Retrieving speedtest.net server list...', quiet) try: - speedtest.get_servers(servers=args.server, exclude=args.exclude) + speedtest.get_servers(servers=args.server, exclude=args.exclude,custom_server=args.custom) except NoMatchedServers: raise SpeedtestCLIError( 'No matched servers: %s' % From aef897c8855d20b93f7777edb5b73eeb01a6300d Mon Sep 17 00:00:00 2001 From: edseldim Date: Sat, 2 Jul 2022 17:28:11 -0500 Subject: [PATCH 3/4] Adding more replacing methods --- speedtest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/speedtest.py b/speedtest.py index a05fdf436..6d9fe6f47 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1250,7 +1250,7 @@ def json_to_xml(self,json_url=None): except (KeyError,SyntaxError) as e: pass message += "\n\n\n" - return message.replace("&","").encode() + return message.replace("&","").replace("%","").encode() def get_servers(self, servers=None, exclude=None, custom_server=None): @@ -1375,7 +1375,6 @@ def get_servers(self, servers=None, exclude=None, custom_server=None): if int(uh.code) != 200: raise ServersRetrievalError() serversxml = ''.encode().join(serversxml_list) - print(serversxml) printer('Servers XML:\n%s' % serversxml, debug=True) try: From ca087bac99f94608f65d050f2401f08aea6d376f Mon Sep 17 00:00:00 2001 From: edseldim Date: Sun, 3 Jul 2022 16:58:15 -0500 Subject: [PATCH 4/4] Added interoperability feature between --custom and --server to choose server from custom link --- speedtest.py | 50 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/speedtest.py b/speedtest.py index 6d9fe6f47..3f046d932 100755 --- a/speedtest.py +++ b/speedtest.py @@ -322,7 +322,7 @@ class SpeedtestConfigError(SpeedtestException): class SpeedtestServersError(SpeedtestException): - """Servers XML is invalid""" + """Servers XML or JSON is invalid""" class ConfigRetrievalError(SpeedtestHTTPError): @@ -1239,18 +1239,35 @@ def get_config(self): return self.config - def json_to_xml(self,json_url=None): - if json_url: - r = requests.get(json_url) - if r.status_code == 200: - json_data = json.loads(r.text)[0] - message = '\n\n' + def json_to_xml(self,data=None, server_id_list=None): + """Converts text data representing a link with json or json text to XML""" + if data: try: - message += f'' - except (KeyError,SyntaxError) as e: - pass - message += "\n\n\n" - return message.replace("&","").replace("%","").encode() + r = requests.get(data) + except requests.exceptions.MissingSchema: + raise SpeedtestServersError("Invalid --custom link") + if r.status_code == 200: + message = '\n\n' + try: + json_data = json.loads(r.text) + if server_id_list and len(server_id_list)>=1: + for server_json in json_data: + if int(server_json["id"]) in server_id_list: + json_data = server_json + try: + message += f'' + except (KeyError,SyntaxError) as e: + pass + else: + json_data = json_data[0] + try: + message += f'' + except (KeyError,SyntaxError) as e: + pass + except json.decoder.JSONDecodeError: + raise SpeedtestServersError("Invalid json data provided by the link") + message += "\n\n\n" + return message.replace("&","").replace("%","").encode() def get_servers(self, servers=None, exclude=None, custom_server=None): @@ -1287,7 +1304,10 @@ def get_servers(self, servers=None, exclude=None, custom_server=None): errors = [] if custom_server: - serversxml = "".encode().join([self.json_to_xml(custom_server)]) + if custom_server and servers: + serversxml = "".encode().join([self.json_to_xml(custom_server,servers)]) + else: + serversxml = "".encode().join([self.json_to_xml(custom_server)]) try: try: try: @@ -1990,7 +2010,9 @@ def shell(): if not args.mini: printer('Retrieving speedtest.net server list...', quiet) try: - speedtest.get_servers(servers=args.server, exclude=args.exclude,custom_server=args.custom) + speedtest.get_servers(servers=args.server, + exclude=args.exclude, + custom_server=args.custom) except NoMatchedServers: raise SpeedtestCLIError( 'No matched servers: %s' %