Compare commits

..

50 Commits

Author SHA1 Message Date
Adam Cruz
e715ef3008 Update 'README.rst' 2018-06-10 06:11:23 +08:00
Adam Cruz
9384255634 Update 'README.rst' 2018-06-10 06:10:39 +08:00
Matt Martz
72ed585c6f Bump to v2.0.2 2018-05-24 11:06:29 -05:00
Matt Martz
41e599f9c3 Ensure we are utilizing the context created by HTTPSConnection, or falling back to ssl. Fixes #517 2018-05-24 09:37:39 -05:00
Matt Martz
c7530bb143 Bump to 2.0.1 for release 2018-05-23 15:26:20 -05:00
Matt Martz
4ceae77401 Handle virtualenv and tox versions for py2.6/3.3 2018-05-22 16:38:10 -05:00
Matt Martz
9e185e8f88 Properly handle older HTTPSConnection. Fixes #513 2018-05-22 15:28:00 -05:00
Matt Martz
9c2977acfc Gracefully handle XML parsing errors. Fixes #490 #491 2018-03-09 09:46:10 -06:00
Matt Martz
f8aa20ecdf Move results.share() to ensure csv and json have access to it. Fixes #483 2018-02-20 14:59:08 -06:00
Matt Martz
8ff923b0fb Bump to 2.0.1a 2018-02-13 16:22:23 -06:00
Matt Martz
35c3ee20ed Exit with nicer error if lat/lon is not valid 2018-02-13 16:21:57 -06:00
Matt Martz
0a7823db7a Tested through 3.7 2018-02-05 16:33:07 -06:00
Matt Martz
27a8301192 Replace downloads badge with travis 2018-02-05 16:33:01 -06:00
Matt Martz
ee2e647b9b Remove deprecated speedtest_cli.py 2018-02-05 16:25:59 -06:00
Matt Martz
831c079113 Bump for release 2018-02-05 16:17:03 -06:00
Matt Martz
4f4c1dd8d1 Update man page 2018-02-05 16:16:51 -06:00
Matt Martz
2c847a1849 Add some guard code for places where sys.stdout and stderr are replaced with some other incompatible object 2018-01-26 15:52:06 -06:00
Matt Martz
e1bab1ab55 Only add terminal colors with DEBUG if stdout is a tty 2018-01-08 16:57:26 -06:00
Matt Martz
48a3d33ae4 Bump to beta 2018-01-03 09:16:51 -06:00
Matt Martz
c16ffd4ae7 Catch OSError and EOFError while reading from gzip stream 2018-01-02 18:32:03 -06:00
Matt Martz
9848481d06 Use the printer everywhere, leaving print_ to only be used within printer 2018-01-02 18:22:16 -06:00
Matt Martz
4737a69f10 Add a few additional tests, specifically around --source 2018-01-02 17:16:52 -06:00
Matt Martz
6381ba3742 Eliminate SHUTDOWN_EVENT global 2018-01-02 16:07:46 -06:00
Matt Martz
fa2e15ee08 Skipping test should be quiet 2017-12-11 09:36:03 -06:00
Matt Martz
eab354603f Don't display ERROR: 2 when argparse exits with non-0 2017-12-11 09:35:41 -06:00
Matt Martz
e80ccc4647 update README for usage changes 2017-11-23 10:30:31 -06:00
Matt Martz
5fbe593fc8 Get travis working properly again 2017-11-23 10:18:16 -06:00
Matt Martz
f70cc86222 No bare except 2017-11-23 10:16:23 -06:00
Matt Martz
5c061da8e0 Move the majority of the csv_header functionality to SpeedtestResults 2017-11-23 10:16:23 -06:00
Matt Martz
4457fe9fb8 Support csv-delimiter for csv-header 2017-11-23 10:16:17 -06:00
Matt Martz
b27f69d1ad Output a different message when only 1 server is provided 2017-11-23 10:15:48 -06:00
Matt Martz
5a9f82a20a Add additional information to machine parsable outputs 2017-11-23 10:15:46 -06:00
Matt Martz
3cb44f5630 Attempt to catch MemoryError if possible 2017-11-23 10:15:09 -06:00
Matt Martz
16054cc3bc Print errors to stderr 2017-11-23 10:15:09 -06:00
Matt Martz
d9642b2047 Always flush in py2 print_ 2017-11-23 10:15:09 -06:00
Matt Martz
f3a607feb2 Allow timeout to be a float 2017-11-23 10:15:09 -06:00
Matt Martz
6bfa5922c3 Add option to exclude servers, and allow --server and --exclude to be specified multiple times 2017-11-23 10:15:09 -06:00
Matt Martz
ca72d40033 Create a getter for Speedtest.best to raise an exception is get_best_server has not found a best server 2017-11-23 10:15:09 -06:00
Matt Martz
3ebb9734a2 Indicate speedtest-cli supports python 3.6, and ensure py3.2 has an appropriate setuptools version 2017-11-23 10:15:09 -06:00
Matt Martz
8854d82049 More and better debugging 2017-11-23 10:15:09 -06:00
Matt Martz
f2a97baf1e Revert "Test failing --source"
This reverts commit be7d7f6a1c.
2017-11-23 10:15:09 -06:00
Matt Martz
6531677346 Test failing --source 2017-11-23 10:15:09 -06:00
Matt Martz
6556be190a Switch to using matrix for travis 2017-11-23 10:15:09 -06:00
Matt Martz
2fe34ecf4e Remove debug print 2017-11-23 10:15:09 -06:00
Matt Martz
0e585cbf64 Docstrings and version bump 2017-11-23 10:15:07 -06:00
Matt Martz
2fe369fdf8 Remove SCHEME global 2017-11-23 10:14:35 -06:00
Matt Martz
b33c7533df flake8 fixes 2017-11-23 10:14:35 -06:00
Matt Martz
fe864f6dce Use vendored create_connection when socket doesn't have it, or socket.create_connection is too old 2017-11-23 10:14:35 -06:00
Matt Martz
10b3b09f02 Don't override socket.socket for binding, eliminiate globals SOURCE and USER_AGENT 2017-11-23 10:14:35 -06:00
Matt Martz
20e5d12a5c Support csv-delimiter for csv-header 2017-10-16 09:28:35 -05:00
7 changed files with 259 additions and 114 deletions

View File

@ -37,13 +37,15 @@ matrix:
env: TOXENV=pypy env: TOXENV=pypy
before_install: before_install:
- pyenv versions
- if [[ $(echo "$TOXENV" | egrep -c "py35") != 0 ]]; then pyenv global system 3.5; fi; - if [[ $(echo "$TOXENV" | egrep -c "py35") != 0 ]]; then pyenv global system 3.5; fi;
install: install:
- if [[ $(echo "$TOXENV" | egrep -c "py32") != 0 ]]; then pip install setuptools==17.1.1; fi; - if [[ $(echo "$TOXENV" | egrep -c "py32") != 0 ]]; then pip install setuptools==17.1.1; fi;
- if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") != 0 ]]; then pip install virtualenv==1.7.2 tox==1.3; fi; - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") != 0 ]]; then pip install virtualenv==1.7.2 tox==1.3; fi;
- if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") == 0 ]]; then pip install tox; fi; - if [[ $(echo "$TOXENV" | egrep -c "(py26|py33)") != 0 ]]; then pip install virtualenv==15.2.0 tox==2.9.1; fi;
- if [[ $(echo "$TOXENV" | egrep -c "(py2[456]|py3[123])") == 0 ]]; then pip install tox; fi;
script: script:
- tox - tox

View File

@ -1,3 +1,7 @@
#Usage
$ curl -s https://git.spectre5.com/adamcruz/speedtest-cli-pub/raw/branch/master/speedtest.py | python -
speedtest-cli speedtest-cli
============= =============
@ -7,9 +11,9 @@ speedtest.net
.. image:: https://img.shields.io/pypi/v/speedtest-cli.svg .. image:: https://img.shields.io/pypi/v/speedtest-cli.svg
:target: https://pypi.python.org/pypi/speedtest-cli/ :target: https://pypi.python.org/pypi/speedtest-cli/
:alt: Latest Version :alt: Latest Version
.. image:: https://img.shields.io/pypi/dm/speedtest-cli.svg .. image:: https://img.shields.io/travis/sivel/speedtest-cli.svg
:target: https://pypi.python.org/pypi/speedtest-cli/ :target: https://pypi.python.org/pypi/speedtest-cli/
:alt: Downloads :alt: Travis
.. image:: https://img.shields.io/pypi/l/speedtest-cli.svg .. image:: https://img.shields.io/pypi/l/speedtest-cli.svg
:target: https://pypi.python.org/pypi/speedtest-cli/ :target: https://pypi.python.org/pypi/speedtest-cli/
:alt: License :alt: License
@ -17,7 +21,7 @@ speedtest.net
Versions Versions
-------- --------
speedtest-cli works with Python 2.4-3.6 speedtest-cli works with Python 2.4-3.7
.. image:: https://img.shields.io/pypi/pyversions/speedtest-cli.svg .. image:: https://img.shields.io/pypi/pyversions/speedtest-cli.svg
:target: https://pypi.python.org/pypi/speedtest-cli/ :target: https://pypi.python.org/pypi/speedtest-cli/
@ -77,13 +81,14 @@ Usage
usage: speedtest-cli [-h] [--no-download] [--no-upload] [--bytes] [--share] usage: speedtest-cli [-h] [--no-download] [--no-upload] [--bytes] [--share]
[--simple] [--csv] [--csv-delimiter CSV_DELIMITER] [--simple] [--csv] [--csv-delimiter CSV_DELIMITER]
[--csv-header] [--json] [--list] [--server SERVER] [--csv-header] [--json] [--list] [--server SERVER]
[--mini MINI] [--source SOURCE] [--timeout TIMEOUT] [--exclude EXCLUDE] [--mini MINI] [--source SOURCE]
[--secure] [--no-pre-allocate] [--version] [--timeout TIMEOUT] [--secure] [--no-pre-allocate]
[--version]
Command line interface for testing internet bandwidth using speedtest.net. Command line interface for testing internet bandwidth using speedtest.net.
-------------------------------------------------------------------------- --------------------------------------------------------------------------
https://github.com/sivel/speedtest-cli https://github.com/sivel/speedtest-cli
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
--no-download Do not perform download test --no-download Do not perform download test
@ -106,7 +111,10 @@ Usage
affected by --bytes affected by --bytes
--list Display a list of speedtest.net servers sorted by --list Display a list of speedtest.net servers sorted by
distance distance
--server SERVER Specify a server ID to test against --server SERVER Specify a server ID to test against. Can be supplied
multiple times
--exclude EXCLUDE Exclude a server from selection. Can be supplied
multiple times
--mini MINI URL of the Speedtest Mini server --mini MINI URL of the Speedtest Mini server
--source SOURCE Source IP address to bind to --source SOURCE Source IP address to bind to
--timeout TIMEOUT HTTP timeout in seconds. Default 10 --timeout TIMEOUT HTTP timeout in seconds. Default 10

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2012-2016 Matt Martz # Copyright 2012-2018 Matt Martz
# All Rights Reserved. # All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -66,7 +66,7 @@ setup(
author_email='matt@sivel.net', author_email='matt@sivel.net',
url='https://github.com/sivel/speedtest-cli', url='https://github.com/sivel/speedtest-cli',
license='Apache License, Version 2.0', license='Apache License, Version 2.0',
py_modules=['speedtest', 'speedtest_cli'], py_modules=['speedtest'],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'speedtest=speedtest:main', 'speedtest=speedtest:main',
@ -91,5 +91,6 @@ setup(
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
] ]
) )

View File

@ -1,4 +1,4 @@
.TH "speedtest-cli" 1 "2014-04-23" "speedtest-cli" .TH "speedtest-cli" 1 "2018-01-05" "speedtest-cli"
.SH NAME .SH NAME
speedtest\-cli \- Command line interface for testing internet bandwidth using speedtest.net speedtest\-cli \- Command line interface for testing internet bandwidth using speedtest.net
.SH SYNOPSIS .SH SYNOPSIS
@ -23,14 +23,24 @@ Displays usage for the tool.
.B Options .B Options
\fB\-\-no\-download\fR
.RS
Do not perform download test
.RE
\fB\-\-no\-upload\fR
.RS
Do not perform upload test
.RE
\fB\-\-bytes\fR \fB\-\-bytes\fR
.RS .RS
Display values in bytes instead of bits. Does not affect the image generated by \-\-share Display values in bytes instead of bits. Does not affect the image generated by \-\-share, nor output from \-\-json or \-\-csv
.RE .RE
\fB\-\-share\fR \fB\-\-share\fR
.RS .RS
Generate and provide a URL to the speedtest.net share results image Generate and provide a URL to the speedtest.net share results image, not displayed with \-\-csv
.RE .RE
\fB\-\-simple\fR \fB\-\-simple\fR
@ -43,12 +53,12 @@ Suppress verbose output, only show basic information
Suppress verbose output, only show basic information in CSV format. Speeds listed in bit/s and not affected by \-\-bytes Suppress verbose output, only show basic information in CSV format. Speeds listed in bit/s and not affected by \-\-bytes
.RE .RE
\fB\-\-csv-delimiter CSV_DELIMITER\fR \fB\-\-csv\-delimiter CSV_DELIMITER\fR
.RS .RS
Single character delimiter to use in CSV output. Default "," Single character delimiter to use in CSV output. Default ","
.RE .RE
\fB\-\-csv-header\fR \fB\-\-csv\-header\fR
.RS .RS
Print CSV headers Print CSV headers
.RE .RE
@ -65,7 +75,12 @@ Display a list of speedtest.net servers sorted by distance
\fB\-\-server SERVER\fR \fB\-\-server SERVER\fR
.RS .RS
Specify a server ID to test against Specify a server ID to test against. Can be supplied multiple times
.RE
\fB\-\-exclude EXCLUDE\fR
.RS
Exclude a server from selection. Can be supplied multiple times
.RE .RE
\fB\-\-mini MINI\fR \fB\-\-mini MINI\fR
@ -88,6 +103,11 @@ HTTP timeout in seconds. Default 10
Use HTTPS instead of HTTP when communicating with speedtest.net operated servers Use HTTPS instead of HTTP when communicating with speedtest.net operated servers
.RE .RE
\fB\-\-no\-pre\-allocate\fR
.RS
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
.RE
\fB\-\-version\fR \fB\-\-version\fR
.RS .RS
Show the version number and exit Show the version number and exit

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2012-2016 Matt Martz # Copyright 2012-2018 Matt Martz
# All Rights Reserved. # All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -36,7 +36,7 @@ except ImportError:
gzip = None gzip = None
GZIP_BASE = object GZIP_BASE = object
__version__ = '2.0.0a' __version__ = '2.0.2'
class FakeShutdownEvent(object): class FakeShutdownEvent(object):
@ -51,7 +51,6 @@ class FakeShutdownEvent(object):
# Some global variables we use # Some global variables we use
SHUTDOWN_EVENT = FakeShutdownEvent()
DEBUG = False DEBUG = False
_GLOBAL_DEFAULT_TIMEOUT = object() _GLOBAL_DEFAULT_TIMEOUT = object()
@ -71,6 +70,7 @@ except ImportError:
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
except ImportError: except ImportError:
from xml.dom import minidom as DOM from xml.dom import minidom as DOM
from xml.parsers.expat import ExpatError
ET = None ET = None
try: try:
@ -85,9 +85,9 @@ except ImportError:
HTTPErrorProcessor, OpenerDirector) HTTPErrorProcessor, OpenerDirector)
try: try:
from httplib import HTTPConnection from httplib import HTTPConnection, BadStatusLine
except ImportError: except ImportError:
from http.client import HTTPConnection from http.client import HTTPConnection, BadStatusLine
try: try:
from httplib import HTTPSConnection from httplib import HTTPSConnection
@ -166,8 +166,14 @@ except ImportError:
self.flush() self.flush()
_py3_print = getattr(builtins, 'print') _py3_print = getattr(builtins, 'print')
_py3_utf8_stdout = _Py3Utf8Output(sys.stdout) try:
_py3_utf8_stderr = _Py3Utf8Output(sys.stderr) _py3_utf8_stdout = _Py3Utf8Output(sys.stdout)
_py3_utf8_stderr = _Py3Utf8Output(sys.stderr)
except OSError:
# sys.stdout/sys.stderr is not a compatible stdout/stderr object
# just use it and hope things go ok
_py3_utf8_stdout = sys.stdout
_py3_utf8_stderr = sys.stderr
def to_utf8(v): def to_utf8(v):
"""No-op encode to utf-8 for py3""" """No-op encode to utf-8 for py3"""
@ -260,10 +266,13 @@ try:
except AttributeError: except AttributeError:
CERT_ERROR = tuple() CERT_ERROR = tuple()
HTTP_ERRORS = ((HTTPError, URLError, socket.error, ssl.SSLError) + HTTP_ERRORS = (
CERT_ERROR) (HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) +
CERT_ERROR
)
except ImportError: except ImportError:
HTTP_ERRORS = (HTTPError, URLError, socket.error) ssl = None
HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
class SpeedtestException(Exception): class SpeedtestException(Exception):
@ -279,7 +288,11 @@ class SpeedtestHTTPError(SpeedtestException):
class SpeedtestConfigError(SpeedtestException): class SpeedtestConfigError(SpeedtestException):
"""Configuration provided is invalid""" """Configuration XML is invalid"""
class SpeedtestServersError(SpeedtestException):
"""Servers XML is invalid"""
class ConfigRetrievalError(SpeedtestHTTPError): class ConfigRetrievalError(SpeedtestHTTPError):
@ -379,13 +392,11 @@ class SpeedtestHTTPConnection(HTTPConnection):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
source_address = kwargs.pop('source_address', None) source_address = kwargs.pop('source_address', None)
context = kwargs.pop('context', None)
timeout = kwargs.pop('timeout', 10) timeout = kwargs.pop('timeout', 10)
HTTPConnection.__init__(self, *args, **kwargs) HTTPConnection.__init__(self, *args, **kwargs)
self.source_address = source_address self.source_address = source_address
self._context = context
self.timeout = timeout self.timeout = timeout
def connect(self): def connect(self):
@ -410,16 +421,28 @@ if HTTPSConnection:
"""Custom HTTPSConnection to support source_address across """Custom HTTPSConnection to support source_address across
Python 2.4 - Python 3 Python 2.4 - Python 3
""" """
def __init__(self, *args, **kwargs):
source_address = kwargs.pop('source_address', None)
timeout = kwargs.pop('timeout', 10)
HTTPSConnection.__init__(self, *args, **kwargs)
self.timeout = timeout
self.source_address = source_address
def connect(self): def connect(self):
"Connect to a host on a given (SSL) port." "Connect to a host on a given (SSL) port."
SpeedtestHTTPConnection.connect(self) SpeedtestHTTPConnection.connect(self)
kwargs = {} kwargs = {}
if hasattr(ssl, 'SSLContext'): if ssl:
kwargs['server_hostname'] = self.host if hasattr(ssl, 'SSLContext'):
kwargs['server_hostname'] = self.host
self.sock = self._context.wrap_socket(self.sock, **kwargs) try:
self.sock = self._context.wrap_socket(self.sock, **kwargs)
except AttributeError:
self.sock = ssl.wrap_socket(self.sock, **kwargs)
def _build_connection(connection, source_address, timeout, context=None): def _build_connection(connection, source_address, timeout, context=None):
@ -677,18 +700,19 @@ def get_attributes_by_tag_name(dom, tag_name):
return dict(list(elem.attributes.items())) return dict(list(elem.attributes.items()))
def print_dots(current, total, start=False, end=False): def print_dots(shutdown_event):
"""Built in callback function used by Thread classes for printing """Built in callback function used by Thread classes for printing
status status
""" """
def inner(current, total, start=False, end=False):
if shutdown_event.isSet():
return
if SHUTDOWN_EVENT.isSet(): sys.stdout.write('.')
return if current + 1 == total and end is True:
sys.stdout.write('\n')
sys.stdout.write('.') sys.stdout.flush()
if current + 1 == total and end is True: return inner
sys.stdout.write('\n')
sys.stdout.flush()
def do_nothing(*args, **kwargs): def do_nothing(*args, **kwargs):
@ -698,7 +722,8 @@ def do_nothing(*args, **kwargs):
class HTTPDownloader(threading.Thread): class HTTPDownloader(threading.Thread):
"""Thread class for retrieving a URL""" """Thread class for retrieving a URL"""
def __init__(self, i, request, start, timeout, opener=None): def __init__(self, i, request, start, timeout, opener=None,
shutdown_event=None):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.request = request self.request = request
self.result = [0] self.result = [0]
@ -710,11 +735,16 @@ class HTTPDownloader(threading.Thread):
else: else:
self._opener = urlopen self._opener = urlopen
if shutdown_event:
self._shutdown_event = shutdown_event
else:
self._shutdown_event = FakeShutdownEvent()
def run(self): def run(self):
try: try:
if (timeit.default_timer() - self.starttime) <= self.timeout: if (timeit.default_timer() - self.starttime) <= self.timeout:
f = self._opener(self.request) f = self._opener(self.request)
while (not SHUTDOWN_EVENT.isSet() and while (not self._shutdown_event.isSet() and
(timeit.default_timer() - self.starttime) <= (timeit.default_timer() - self.starttime) <=
self.timeout): self.timeout):
self.result.append(len(f.read(10240))) self.result.append(len(f.read(10240)))
@ -730,11 +760,16 @@ class HTTPUploaderData(object):
has been reached has been reached
""" """
def __init__(self, length, start, timeout): def __init__(self, length, start, timeout, shutdown_event=None):
self.length = length self.length = length
self.start = start self.start = start
self.timeout = timeout self.timeout = timeout
if shutdown_event:
self._shutdown_event = shutdown_event
else:
self._shutdown_event = FakeShutdownEvent()
self._data = None self._data = None
self.total = [0] self.total = [0]
@ -763,7 +798,7 @@ class HTTPUploaderData(object):
def read(self, n=10240): def read(self, n=10240):
if ((timeit.default_timer() - self.start) <= self.timeout and if ((timeit.default_timer() - self.start) <= self.timeout and
not SHUTDOWN_EVENT.isSet()): not self._shutdown_event.isSet()):
chunk = self.data.read(n) chunk = self.data.read(n)
self.total.append(len(chunk)) self.total.append(len(chunk))
return chunk return chunk
@ -777,7 +812,8 @@ class HTTPUploaderData(object):
class HTTPUploader(threading.Thread): class HTTPUploader(threading.Thread):
"""Thread class for putting a URL""" """Thread class for putting a URL"""
def __init__(self, i, request, start, size, timeout, opener=None): def __init__(self, i, request, start, size, timeout, opener=None,
shutdown_event=None):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.request = request self.request = request
self.request.data.start = self.starttime = start self.request.data.start = self.starttime = start
@ -791,11 +827,16 @@ class HTTPUploader(threading.Thread):
else: else:
self._opener = urlopen self._opener = urlopen
if shutdown_event:
self._shutdown_event = shutdown_event
else:
self._shutdown_event = FakeShutdownEvent()
def run(self): def run(self):
request = self.request request = self.request
try: try:
if ((timeit.default_timer() - self.starttime) <= self.timeout and if ((timeit.default_timer() - self.starttime) <= self.timeout and
not SHUTDOWN_EVENT.isSet()): not self._shutdown_event.isSet()):
try: try:
f = self._opener(request) f = self._opener(request)
except TypeError: except TypeError:
@ -969,7 +1010,7 @@ class Speedtest(object):
"""Class for performing standard speedtest.net testing operations""" """Class for performing standard speedtest.net testing operations"""
def __init__(self, config=None, source_address=None, timeout=10, def __init__(self, config=None, source_address=None, timeout=10,
secure=False): secure=False, shutdown_event=None):
self.config = {} self.config = {}
self._source_address = source_address self._source_address = source_address
@ -978,6 +1019,11 @@ class Speedtest(object):
self._secure = secure self._secure = secure
if shutdown_event:
self._shutdown_event = shutdown_event
else:
self._shutdown_event = FakeShutdownEvent()
self.get_config() self.get_config()
if config is not None: if config is not None:
self.config.update(config) self.config.update(config)
@ -1014,13 +1060,16 @@ class Speedtest(object):
uh, e = catch_request(request, opener=self._opener) uh, e = catch_request(request, opener=self._opener)
if e: if e:
raise ConfigRetrievalError(e) raise ConfigRetrievalError(e)
configxml = [] configxml_list = []
stream = get_response_stream(uh) stream = get_response_stream(uh)
while 1: while 1:
configxml.append(stream.read(1024)) try:
if len(configxml[-1]) == 0: configxml_list.append(stream.read(1024))
except (OSError, EOFError):
raise ConfigRetrievalError(get_exception())
if len(configxml_list[-1]) == 0:
break break
stream.close() stream.close()
uh.close() uh.close()
@ -1028,10 +1077,18 @@ class Speedtest(object):
if int(uh.code) != 200: if int(uh.code) != 200:
return None return None
printer('Config XML:\n%s' % ''.encode().join(configxml), debug=True) configxml = ''.encode().join(configxml_list)
printer('Config XML:\n%s' % configxml, debug=True)
try: try:
root = ET.fromstring(''.encode().join(configxml)) try:
root = ET.fromstring(configxml)
except ET.ParseError:
e = get_exception()
raise SpeedtestConfigError(
'Malformed speedtest.net configuration: %s' % e
)
server_config = root.find('server-config').attrib server_config = root.find('server-config').attrib
download = root.find('download').attrib download = root.find('download').attrib
upload = root.find('upload').attrib upload = root.find('upload').attrib
@ -1039,7 +1096,13 @@ class Speedtest(object):
client = root.find('client').attrib client = root.find('client').attrib
except AttributeError: except AttributeError:
root = DOM.parseString(''.join(configxml)) try:
root = DOM.parseString(configxml)
except ExpatError:
e = get_exception()
raise SpeedtestConfigError(
'Malformed speedtest.net configuration: %s' % e
)
server_config = get_attributes_by_tag_name(root, 'server-config') server_config = get_attributes_by_tag_name(root, 'server-config')
download = get_attributes_by_tag_name(root, 'download') download = get_attributes_by_tag_name(root, 'download')
upload = get_attributes_by_tag_name(root, 'upload') upload = get_attributes_by_tag_name(root, 'upload')
@ -1088,7 +1151,13 @@ class Speedtest(object):
'upload_max': upload_count * size_count 'upload_max': upload_count * size_count
}) })
self.lat_lon = (float(client['lat']), float(client['lon'])) try:
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'))
)
printer('Config:\n%r' % self.config, debug=True) printer('Config:\n%r' % self.config, debug=True)
@ -1142,10 +1211,13 @@ class Speedtest(object):
stream = get_response_stream(uh) stream = get_response_stream(uh)
serversxml = [] serversxml_list = []
while 1: while 1:
serversxml.append(stream.read(1024)) try:
if len(serversxml[-1]) == 0: serversxml_list.append(stream.read(1024))
except (OSError, EOFError):
raise ServersRetrievalError(get_exception())
if len(serversxml_list[-1]) == 0:
break break
stream.close() stream.close()
@ -1154,15 +1226,28 @@ class Speedtest(object):
if int(uh.code) != 200: if int(uh.code) != 200:
raise ServersRetrievalError() raise ServersRetrievalError()
printer('Servers XML:\n%s' % ''.encode().join(serversxml), serversxml = ''.encode().join(serversxml_list)
debug=True)
printer('Servers XML:\n%s' % serversxml, debug=True)
try: try:
try: try:
root = ET.fromstring(''.encode().join(serversxml)) try:
root = ET.fromstring(serversxml)
except ET.ParseError:
e = get_exception()
raise SpeedtestServersError(
'Malformed speedtest.net server list: %s' % e
)
elements = root.getiterator('server') elements = root.getiterator('server')
except AttributeError: except AttributeError:
root = DOM.parseString(''.join(serversxml)) try:
root = DOM.parseString(serversxml)
except ExpatError:
e = get_exception()
raise SpeedtestServersError(
'Malformed speedtest.net server list: %s' % e
)
elements = root.getElementsByTagName('server') elements = root.getElementsByTagName('server')
except (SyntaxError, xml.parsers.expat.ExpatError): except (SyntaxError, xml.parsers.expat.ExpatError):
raise ServersRetrievalError() raise ServersRetrievalError()
@ -1372,9 +1457,14 @@ class Speedtest(object):
def producer(q, requests, request_count): def producer(q, requests, request_count):
for i, request in enumerate(requests): for i, request in enumerate(requests):
thread = HTTPDownloader(i, request, start, thread = HTTPDownloader(
self.config['length']['download'], i,
opener=self._opener) request,
start,
self.config['length']['download'],
opener=self._opener,
shutdown_event=self._shutdown_event
)
thread.start() thread.start()
q.put(thread, True) q.put(thread, True)
callback(i, request_count, start=True) callback(i, request_count, start=True)
@ -1427,7 +1517,12 @@ class Speedtest(object):
for i, size in enumerate(sizes): for i, size in enumerate(sizes):
# We set ``0`` for ``start`` and handle setting the actual # We set ``0`` for ``start`` and handle setting the actual
# ``start`` in ``HTTPUploader`` to get better measurements # ``start`` in ``HTTPUploader`` to get better measurements
data = HTTPUploaderData(size, 0, self.config['length']['upload']) data = HTTPUploaderData(
size,
0,
self.config['length']['upload'],
shutdown_event=self._shutdown_event
)
if pre_allocate: if pre_allocate:
data.pre_allocate() data.pre_allocate()
requests.append( requests.append(
@ -1439,9 +1534,15 @@ class Speedtest(object):
def producer(q, requests, request_count): def producer(q, requests, request_count):
for i, request in enumerate(requests[:request_count]): for i, request in enumerate(requests[:request_count]):
thread = HTTPUploader(i, request[0], start, request[1], thread = HTTPUploader(
self.config['length']['upload'], i,
opener=self._opener) request[0],
start,
request[1],
self.config['length']['upload'],
opener=self._opener,
shutdown_event=self._shutdown_event
)
thread.start() thread.start()
q.put(thread, True) q.put(thread, True)
callback(i, request_count, start=True) callback(i, request_count, start=True)
@ -1477,27 +1578,28 @@ class Speedtest(object):
return self.results.upload return self.results.upload
def ctrl_c(signum, frame): def ctrl_c(shutdown_event):
"""Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded """Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded
operations operations
""" """
def inner(signum, frame):
SHUTDOWN_EVENT.set() shutdown_event.set()
print_('\nCancelling...') printer('\nCancelling...', error=True)
sys.exit(0) sys.exit(0)
return inner
def version(): def version():
"""Print the version""" """Print the version"""
print_(__version__) printer(__version__)
sys.exit(0) sys.exit(0)
def csv_header(delimiter=','): def csv_header(delimiter=','):
"""Print the CSV Headers""" """Print the CSV Headers"""
print_(SpeedtestResults.csv_header(delimiter=delimiter)) printer(SpeedtestResults.csv_header(delimiter=delimiter))
sys.exit(0) sys.exit(0)
@ -1602,13 +1704,16 @@ def validate_optional_args(args):
def printer(string, quiet=False, debug=False, error=False, **kwargs): def printer(string, quiet=False, debug=False, error=False, **kwargs):
"""Helper function to print a string only when not quiet""" """Helper function print a string with various features"""
if debug and not DEBUG: if debug and not DEBUG:
return return
if debug: if debug:
out = '\033[1;30mDEBUG: %s\033[0m' % string if sys.stdout.isatty():
out = '\033[1;30mDEBUG: %s\033[0m' % string
else:
out = 'DEBUG: %s' % string
else: else:
out = string out = string
@ -1622,10 +1727,10 @@ def printer(string, quiet=False, debug=False, error=False, **kwargs):
def shell(): def shell():
"""Run the full speedtest.net test""" """Run the full speedtest.net test"""
global SHUTDOWN_EVENT, DEBUG global DEBUG
SHUTDOWN_EVENT = threading.Event() shutdown_event = threading.Event()
signal.signal(signal.SIGINT, ctrl_c) signal.signal(signal.SIGINT, ctrl_c(shutdown_event))
args = parse_args() args = parse_args()
@ -1665,7 +1770,7 @@ def shell():
if quiet or debug: if quiet or debug:
callback = do_nothing callback = do_nothing
else: else:
callback = print_dots callback = print_dots(shutdown_event)
printer('Retrieving speedtest.net configuration...', quiet) printer('Retrieving speedtest.net configuration...', quiet)
try: try:
@ -1690,7 +1795,7 @@ def shell():
line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) ' line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) '
'[%(d)0.2f km]' % server) '[%(d)0.2f km]' % server)
try: try:
print_(line) printer(line)
except IOError: except IOError:
e = get_exception() e = get_exception()
if e.errno != errno.EPIPE: if e.errno != errno.EPIPE:
@ -1740,7 +1845,7 @@ def shell():
args.units[0]), args.units[0]),
quiet) quiet)
else: else:
printer('Skipping download test') printer('Skipping download test', quiet)
if args.upload: if args.upload:
printer('Testing upload speed', quiet, printer('Testing upload speed', quiet,
@ -1751,23 +1856,24 @@ def shell():
args.units[0]), args.units[0]),
quiet) quiet)
else: else:
printer('Skipping upload test') printer('Skipping upload test', quiet)
printer('Results:\n%r' % results.dict(), debug=True) printer('Results:\n%r' % results.dict(), debug=True)
if not args.simple and args.share:
results.share()
if args.simple: if args.simple:
print_('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' % printer('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' %
(results.ping, (results.ping,
(results.download / 1000.0 / 1000.0) / args.units[1], (results.download / 1000.0 / 1000.0) / args.units[1],
args.units[0], args.units[0],
(results.upload / 1000.0 / 1000.0) / args.units[1], (results.upload / 1000.0 / 1000.0) / args.units[1],
args.units[0])) args.units[0]))
elif args.csv: elif args.csv:
print_(results.csv(delimiter=args.csv_delimiter)) printer(results.csv(delimiter=args.csv_delimiter))
elif args.json: elif args.json:
if args.share: printer(results.json())
results.share()
print_(results.json())
if args.share and not machine_format: if args.share and not machine_format:
printer('Share results: %s' % results.share()) printer('Share results: %s' % results.share())
@ -1777,10 +1883,11 @@ def main():
try: try:
shell() shell()
except KeyboardInterrupt: except KeyboardInterrupt:
print_('\nCancelling...') printer('\nCancelling...', error=True)
except (SpeedtestException, SystemExit): except (SpeedtestException, SystemExit):
e = get_exception() e = get_exception()
if getattr(e, 'code', 1) != 0: # Ignore a successful exit, or argparse exit
if getattr(e, 'code', 1) not in (0, 2):
raise SystemExit('ERROR: %s' % e) raise SystemExit('ERROR: %s' % e)

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2012-2016 Matt Martz # Copyright 2018 Matt Martz
# All Rights Reserved. # All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -15,20 +15,23 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import warnings import sys
import subprocess
DEPRECATED_MSG = ('The file speedtest_cli.py has been deprecated in favor of ' cmd = [sys.executable, 'speedtest.py', '--source', '127.0.0.1']
'speedtest.py\nand is available for download at:\n\n'
'https://raw.githubusercontent.com/sivel/speedtest-cli/'
'master/speedtest.py')
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
if __name__ == '__main__': stdout, stderr = p.communicate()
raise SystemExit(DEPRECATED_MSG)
else: if p.returncode != 1:
try: raise SystemExit('%s did not fail with exit code 1' % ' '.join(cmd))
from speedtest import *
except ImportError: if 'Invalid argument'.encode() not in stderr:
raise SystemExit(DEPRECATED_MSG) raise SystemExit(
else: '"Invalid argument" not found in stderr:\n%s' % stderr.decode()
warnings.warn(DEPRECATED_MSG, UserWarning) )

View File

@ -6,6 +6,8 @@ commands =
{envpython} -V {envpython} -V
{envpython} -m compileall speedtest.py {envpython} -m compileall speedtest.py
{envpython} speedtest.py {envpython} speedtest.py
{envpython} speedtest.py --source 172.17.0.1
{envpython} tests/scripts/source.py
[testenv:flake8] [testenv:flake8]
basepython=python basepython=python
@ -19,3 +21,5 @@ commands =
pypy -V pypy -V
pypy -m compileall speedtest.py pypy -m compileall speedtest.py
pypy speedtest.py pypy speedtest.py
pypy speedtest.py --source 172.17.0.1
pypy tests/scripts/source.py