Compare commits
36 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
22210ca352 | |
|
|
42e96b13dd | |
|
|
cadc68b5ae | |
|
|
db46af8bcd | |
|
|
c58ad3367b | |
|
|
266e53c256 | |
|
|
7ebb9965dd | |
|
|
2658bd50b4 | |
|
|
81bba6070c | |
|
|
681cdf20a5 | |
|
|
cdf6002865 | |
|
|
9af203652b | |
|
|
2d5a9ef364 | |
|
|
3109fcf407 | |
|
|
69ddff1a11 | |
|
|
fb0569946d | |
|
|
f356c7b02d | |
|
|
6cf43b2ff7 | |
|
|
217ce8eff1 | |
|
|
b43334f1ec | |
|
|
b0b826c870 | |
|
|
9ac1091eae | |
|
|
ca2250f700 | |
|
|
ddb8db0c94 | |
|
|
72bf53affa | |
|
|
a8a3265001 | |
|
|
b2654de410 | |
|
|
72ed585c6f | |
|
|
41e599f9c3 | |
|
|
c7530bb143 | |
|
|
4ceae77401 | |
|
|
9e185e8f88 | |
|
|
9c2977acfc | |
|
|
f8aa20ecdf | |
|
|
8ff923b0fb | |
|
|
35c3ee20ed |
12
.travis.yml
12
.travis.yml
|
|
@ -1,4 +1,6 @@
|
||||||
language: python
|
language: python
|
||||||
|
sudo: required
|
||||||
|
dist: xenial
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
|
|
@ -33,8 +35,13 @@ matrix:
|
||||||
env: TOXENV=py35
|
env: TOXENV=py35
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: TOXENV=py36
|
env: TOXENV=py36
|
||||||
|
- python: 3.7
|
||||||
|
env: TOXENV=py37
|
||||||
|
- python: 3.8-dev
|
||||||
|
env: TOXENV=py38
|
||||||
- python: pypy
|
- python: pypy
|
||||||
env: TOXENV=pypy
|
env: TOXENV=pypy
|
||||||
|
dist: trusty
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- 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;
|
||||||
|
|
@ -42,7 +49,10 @@ before_install:
|
||||||
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
|
||||||
|
|
|
||||||
17
README.rst
17
README.rst
|
|
@ -51,7 +51,8 @@ or
|
||||||
::
|
::
|
||||||
|
|
||||||
git clone https://github.com/sivel/speedtest-cli.git
|
git clone https://github.com/sivel/speedtest-cli.git
|
||||||
python speedtest-cli/setup.py install
|
cd speedtest-cli
|
||||||
|
python setup.py install
|
||||||
|
|
||||||
Just download (Like the way it used to be)
|
Just download (Like the way it used to be)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
@ -74,12 +75,12 @@ Usage
|
||||||
::
|
::
|
||||||
|
|
||||||
$ speedtest-cli -h
|
$ speedtest-cli -h
|
||||||
usage: speedtest-cli [-h] [--no-download] [--no-upload] [--bytes] [--share]
|
usage: speedtest-cli [-h] [--no-download] [--no-upload] [--single] [--bytes]
|
||||||
[--simple] [--csv] [--csv-delimiter CSV_DELIMITER]
|
[--share] [--simple] [--csv]
|
||||||
[--csv-header] [--json] [--list] [--server SERVER]
|
[--csv-delimiter CSV_DELIMITER] [--csv-header] [--json]
|
||||||
[--exclude EXCLUDE] [--mini MINI] [--source SOURCE]
|
[--list] [--server SERVER] [--exclude EXCLUDE]
|
||||||
[--timeout TIMEOUT] [--secure] [--no-pre-allocate]
|
[--mini MINI] [--source SOURCE] [--timeout TIMEOUT]
|
||||||
[--version]
|
[--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.
|
||||||
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
|
||||||
|
|
@ -89,6 +90,8 @@ Usage
|
||||||
-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
|
||||||
--no-upload Do not perform upload test
|
--no-upload Do not perform upload test
|
||||||
|
--single Only use a single connection instead of multiple. This
|
||||||
|
simulates a typical file transfer.
|
||||||
--bytes Display values in bytes instead of bits. Does not
|
--bytes Display values in bytes instead of bits. Does not
|
||||||
affect the image generated by --share, nor output from
|
affect the image generated by --share, nor output from
|
||||||
--json or --csv
|
--json or --csv
|
||||||
|
|
|
||||||
5
setup.py
5
setup.py
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2012-2018 Matt Martz
|
# Copyright 2012 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
|
||||||
|
|
@ -92,5 +92,8 @@ setup(
|
||||||
'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',
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Programming Language :: Python :: 3.9',
|
||||||
|
'Programming Language :: Python :: 3.10',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ Do not perform download test
|
||||||
Do not perform upload test
|
Do not perform upload test
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
|
\fB\-\-single\fR
|
||||||
|
.RS
|
||||||
|
Only use a single connection instead of multiple. This simulates a typical file transfer.
|
||||||
|
.RE
|
||||||
|
|
||||||
\fB\-\-bytes\fR
|
\fB\-\-bytes\fR
|
||||||
.RS
|
.RS
|
||||||
Display values in bytes instead of bits. Does not affect the image generated by \-\-share, nor output from \-\-json or \-\-csv
|
Display values in bytes instead of bits. Does not affect the image generated by \-\-share, nor output from \-\-json or \-\-csv
|
||||||
|
|
|
||||||
320
speedtest.py
320
speedtest.py
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2012-2018 Matt Martz
|
# Copyright 2012 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,18 +15,18 @@
|
||||||
# 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 os
|
|
||||||
import re
|
|
||||||
import csv
|
import csv
|
||||||
import sys
|
import datetime
|
||||||
import math
|
|
||||||
import errno
|
import errno
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import timeit
|
import sys
|
||||||
import datetime
|
|
||||||
import platform
|
|
||||||
import threading
|
import threading
|
||||||
|
import timeit
|
||||||
import xml.parsers.expat
|
import xml.parsers.expat
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -36,7 +36,7 @@ except ImportError:
|
||||||
gzip = None
|
gzip = None
|
||||||
GZIP_BASE = object
|
GZIP_BASE = object
|
||||||
|
|
||||||
__version__ = '2.0.0'
|
__version__ = '2.1.4b1'
|
||||||
|
|
||||||
|
|
||||||
class FakeShutdownEvent(object):
|
class FakeShutdownEvent(object):
|
||||||
|
|
@ -49,10 +49,16 @@ class FakeShutdownEvent(object):
|
||||||
"Dummy method to always return false"""
|
"Dummy method to always return false"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
is_set = isSet
|
||||||
|
|
||||||
|
|
||||||
# Some global variables we use
|
# Some global variables we use
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
_GLOBAL_DEFAULT_TIMEOUT = object()
|
_GLOBAL_DEFAULT_TIMEOUT = object()
|
||||||
|
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
|
# Begin import game to handle Python 2 and Python 3
|
||||||
try:
|
try:
|
||||||
|
|
@ -64,12 +70,14 @@ except ImportError:
|
||||||
json = None
|
json = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import xml.etree.cElementTree as ET
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
try:
|
||||||
|
from xml.etree.ElementTree import _Element as ET_Element
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
pass
|
||||||
|
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:
|
||||||
|
|
@ -84,9 +92,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
|
||||||
|
|
@ -96,6 +104,11 @@ except ImportError:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HTTPSConnection = None
|
HTTPSConnection = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from httplib import FakeSocket
|
||||||
|
except ImportError:
|
||||||
|
FakeSocket = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -256,7 +269,6 @@ else:
|
||||||
write(arg)
|
write(arg)
|
||||||
write(end)
|
write(end)
|
||||||
|
|
||||||
|
|
||||||
# Exception "constants" to support Python 2 through Python 3
|
# Exception "constants" to support Python 2 through Python 3
|
||||||
try:
|
try:
|
||||||
import ssl
|
import ssl
|
||||||
|
|
@ -265,10 +277,30 @@ 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)
|
||||||
|
|
||||||
|
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):
|
class SpeedtestException(Exception):
|
||||||
|
|
@ -284,7 +316,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):
|
||||||
|
|
@ -384,13 +420,13 @@ 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)
|
||||||
|
|
||||||
|
self._tunnel_host = None
|
||||||
|
|
||||||
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):
|
||||||
|
|
@ -408,23 +444,75 @@ class SpeedtestHTTPConnection(HTTPConnection):
|
||||||
self.source_address
|
self.source_address
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self._tunnel_host:
|
||||||
|
self._tunnel()
|
||||||
|
|
||||||
|
|
||||||
if HTTPSConnection:
|
if HTTPSConnection:
|
||||||
class SpeedtestHTTPSConnection(HTTPSConnection,
|
class SpeedtestHTTPSConnection(HTTPSConnection):
|
||||||
SpeedtestHTTPConnection):
|
|
||||||
"""Custom HTTPSConnection to support source_address across
|
"""Custom HTTPSConnection to support source_address across
|
||||||
Python 2.4 - Python 3
|
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)
|
||||||
|
|
||||||
|
self._tunnel_host = None
|
||||||
|
|
||||||
|
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."
|
||||||
|
try:
|
||||||
|
self.sock = socket.create_connection(
|
||||||
|
(self.host, self.port),
|
||||||
|
self.timeout,
|
||||||
|
self.source_address
|
||||||
|
)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
self.sock = create_connection(
|
||||||
|
(self.host, self.port),
|
||||||
|
self.timeout,
|
||||||
|
self.source_address
|
||||||
|
)
|
||||||
|
|
||||||
SpeedtestHTTPConnection.connect(self)
|
if self._tunnel_host:
|
||||||
|
self._tunnel()
|
||||||
|
|
||||||
|
if ssl:
|
||||||
|
try:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if hasattr(ssl, 'SSLContext'):
|
if hasattr(ssl, 'SSLContext'):
|
||||||
|
if 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)
|
self.sock = self._context.wrap_socket(self.sock, **kwargs)
|
||||||
|
except AttributeError:
|
||||||
|
self.sock = ssl.wrap_socket(self.sock)
|
||||||
|
try:
|
||||||
|
self.sock.server_hostname = self.host
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
elif FakeSocket:
|
||||||
|
# Python 2.4/2.5 support
|
||||||
|
try:
|
||||||
|
self.sock = FakeSocket(self.sock, socket.ssl(self.sock))
|
||||||
|
except AttributeError:
|
||||||
|
raise SpeedtestException(
|
||||||
|
'This version of Python does not support HTTPS/SSL '
|
||||||
|
'functionality'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise SpeedtestException(
|
||||||
|
'This version of Python does not support HTTPS/SSL '
|
||||||
|
'functionality'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_connection(connection, source_address, timeout, context=None):
|
def _build_connection(connection, source_address, timeout, context=None):
|
||||||
|
|
@ -589,7 +677,8 @@ def build_user_agent():
|
||||||
|
|
||||||
ua_tuple = (
|
ua_tuple = (
|
||||||
'Mozilla/5.0',
|
'Mozilla/5.0',
|
||||||
'(%s; U; %s; en-us)' % (platform.system(), platform.architecture()[0]),
|
'(%s; U; %s; en-us)' % (platform.platform(),
|
||||||
|
platform.architecture()[0]),
|
||||||
'Python/%s' % platform.python_version(),
|
'Python/%s' % platform.python_version(),
|
||||||
'(KHTML, like Gecko)',
|
'(KHTML, like Gecko)',
|
||||||
'speedtest-cli/%s' % __version__
|
'speedtest-cli/%s' % __version__
|
||||||
|
|
@ -648,6 +737,8 @@ def catch_request(request, opener=None):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
uh = _open(request)
|
uh = _open(request)
|
||||||
|
if request.get_full_url() != uh.geturl():
|
||||||
|
printer('Redirected to %s' % uh.geturl(), debug=True)
|
||||||
return uh, False
|
return uh, False
|
||||||
except HTTP_ERRORS:
|
except HTTP_ERRORS:
|
||||||
e = get_exception()
|
e = get_exception()
|
||||||
|
|
@ -687,7 +778,7 @@ def print_dots(shutdown_event):
|
||||||
status
|
status
|
||||||
"""
|
"""
|
||||||
def inner(current, total, start=False, end=False):
|
def inner(current, total, start=False, end=False):
|
||||||
if shutdown_event.isSet():
|
if event_is_set(shutdown_event):
|
||||||
return
|
return
|
||||||
|
|
||||||
sys.stdout.write('.')
|
sys.stdout.write('.')
|
||||||
|
|
@ -726,7 +817,7 @@ class HTTPDownloader(threading.Thread):
|
||||||
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 self._shutdown_event.isSet() and
|
while (not event_is_set(self._shutdown_event) 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)))
|
||||||
|
|
@ -735,6 +826,8 @@ class HTTPDownloader(threading.Thread):
|
||||||
f.close()
|
f.close()
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
except HTTP_ERRORS:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HTTPUploaderData(object):
|
class HTTPUploaderData(object):
|
||||||
|
|
@ -780,7 +873,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 self._shutdown_event.isSet()):
|
not event_is_set(self._shutdown_event)):
|
||||||
chunk = self.data.read(n)
|
chunk = self.data.read(n)
|
||||||
self.total.append(len(chunk))
|
self.total.append(len(chunk))
|
||||||
return chunk
|
return chunk
|
||||||
|
|
@ -800,7 +893,7 @@ class HTTPUploader(threading.Thread):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.request.data.start = self.starttime = start
|
self.request.data.start = self.starttime = start
|
||||||
self.size = size
|
self.size = size
|
||||||
self.result = None
|
self.result = 0
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.i = i
|
self.i = i
|
||||||
|
|
||||||
|
|
@ -818,7 +911,7 @@ class HTTPUploader(threading.Thread):
|
||||||
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 self._shutdown_event.isSet()):
|
not event_is_set(self._shutdown_event)):
|
||||||
try:
|
try:
|
||||||
f = self._opener(request)
|
f = self._opener(request)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
|
@ -835,6 +928,8 @@ class HTTPUploader(threading.Thread):
|
||||||
self.result = 0
|
self.result = 0
|
||||||
except (IOError, SpeedtestUploadTimeout):
|
except (IOError, SpeedtestUploadTimeout):
|
||||||
self.result = sum(self.request.data.total)
|
self.result = sum(self.request.data.total)
|
||||||
|
except HTTP_ERRORS:
|
||||||
|
self.result = 0
|
||||||
|
|
||||||
|
|
||||||
class SpeedtestResults(object):
|
class SpeedtestResults(object):
|
||||||
|
|
@ -1023,10 +1118,7 @@ class Speedtest(object):
|
||||||
@property
|
@property
|
||||||
def best(self):
|
def best(self):
|
||||||
if not self._best:
|
if not self._best:
|
||||||
raise SpeedtestMissingBestServer(
|
self.get_best_server()
|
||||||
'get_best_server not called or not able to determine best '
|
|
||||||
'server'
|
|
||||||
)
|
|
||||||
return self._best
|
return self._best
|
||||||
|
|
||||||
def get_config(self):
|
def get_config(self):
|
||||||
|
|
@ -1042,16 +1134,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:
|
||||||
try:
|
try:
|
||||||
configxml.append(stream.read(1024))
|
configxml_list.append(stream.read(1024))
|
||||||
except (OSError, EOFError):
|
except (OSError, EOFError):
|
||||||
raise ConfigRetrievalError(get_exception())
|
raise ConfigRetrievalError(get_exception())
|
||||||
if len(configxml[-1]) == 0:
|
if len(configxml_list[-1]) == 0:
|
||||||
break
|
break
|
||||||
stream.close()
|
stream.close()
|
||||||
uh.close()
|
uh.close()
|
||||||
|
|
@ -1059,10 +1151,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
|
||||||
|
|
@ -1070,16 +1170,22 @@ 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')
|
||||||
# times = get_attributes_by_tag_name(root, 'times')
|
# 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 = list(
|
ignore_servers = [
|
||||||
map(int, server_config['ignoreids'].split(','))
|
int(i) for i in server_config['ignoreids'].split(',') if i
|
||||||
)
|
]
|
||||||
|
|
||||||
ratio = int(upload['ratio'])
|
ratio = int(upload['ratio'])
|
||||||
upload_max = int(upload['maxchunkcount'])
|
upload_max = int(upload['maxchunkcount'])
|
||||||
|
|
@ -1119,7 +1225,13 @@ class Speedtest(object):
|
||||||
'upload_max': upload_count * size_count
|
'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'))
|
||||||
|
)
|
||||||
|
|
||||||
printer('Config:\n%r' % self.config, debug=True)
|
printer('Config:\n%r' % self.config, debug=True)
|
||||||
|
|
||||||
|
|
@ -1173,13 +1285,13 @@ class Speedtest(object):
|
||||||
|
|
||||||
stream = get_response_stream(uh)
|
stream = get_response_stream(uh)
|
||||||
|
|
||||||
serversxml = []
|
serversxml_list = []
|
||||||
while 1:
|
while 1:
|
||||||
try:
|
try:
|
||||||
serversxml.append(stream.read(1024))
|
serversxml_list.append(stream.read(1024))
|
||||||
except (OSError, EOFError):
|
except (OSError, EOFError):
|
||||||
raise ServersRetrievalError(get_exception())
|
raise ServersRetrievalError(get_exception())
|
||||||
if len(serversxml[-1]) == 0:
|
if len(serversxml_list[-1]) == 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
stream.close()
|
stream.close()
|
||||||
|
|
@ -1188,15 +1300,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:
|
||||||
elements = root.getiterator('server')
|
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:
|
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()
|
||||||
|
|
@ -1388,8 +1513,12 @@ class Speedtest(object):
|
||||||
printer('Best Server:\n%r' % best, debug=True)
|
printer('Best Server:\n%r' % best, debug=True)
|
||||||
return best
|
return best
|
||||||
|
|
||||||
def download(self, callback=do_nothing):
|
def download(self, callback=do_nothing, threads=None):
|
||||||
"""Test download speed against speedtest.net"""
|
"""Test download speed against speedtest.net
|
||||||
|
|
||||||
|
A ``threads`` value of ``None`` will fall back to those dictated
|
||||||
|
by the speedtest.net configuration
|
||||||
|
"""
|
||||||
|
|
||||||
urls = []
|
urls = []
|
||||||
for size in self.config['sizes']['download']:
|
for size in self.config['sizes']['download']:
|
||||||
|
|
@ -1404,6 +1533,9 @@ class Speedtest(object):
|
||||||
build_request(url, bump=i, secure=self._secure)
|
build_request(url, bump=i, secure=self._secure)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
max_threads = threads or self.config['threads']['download']
|
||||||
|
in_flight = {'threads': 0}
|
||||||
|
|
||||||
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(
|
thread = HTTPDownloader(
|
||||||
|
|
@ -1414,21 +1546,26 @@ class Speedtest(object):
|
||||||
opener=self._opener,
|
opener=self._opener,
|
||||||
shutdown_event=self._shutdown_event
|
shutdown_event=self._shutdown_event
|
||||||
)
|
)
|
||||||
|
while in_flight['threads'] >= max_threads:
|
||||||
|
timeit.time.sleep(0.001)
|
||||||
thread.start()
|
thread.start()
|
||||||
q.put(thread, True)
|
q.put(thread, True)
|
||||||
|
in_flight['threads'] += 1
|
||||||
callback(i, request_count, start=True)
|
callback(i, request_count, start=True)
|
||||||
|
|
||||||
finished = []
|
finished = []
|
||||||
|
|
||||||
def consumer(q, request_count):
|
def consumer(q, request_count):
|
||||||
|
_is_alive = thread_is_alive
|
||||||
while len(finished) < request_count:
|
while len(finished) < request_count:
|
||||||
thread = q.get(True)
|
thread = q.get(True)
|
||||||
while thread.isAlive():
|
while _is_alive(thread):
|
||||||
thread.join(timeout=0.1)
|
thread.join(timeout=0.001)
|
||||||
|
in_flight['threads'] -= 1
|
||||||
finished.append(sum(thread.result))
|
finished.append(sum(thread.result))
|
||||||
callback(thread.i, request_count, end=True)
|
callback(thread.i, request_count, end=True)
|
||||||
|
|
||||||
q = Queue(self.config['threads']['download'])
|
q = Queue(max_threads)
|
||||||
prod_thread = threading.Thread(target=producer,
|
prod_thread = threading.Thread(target=producer,
|
||||||
args=(q, requests, request_count))
|
args=(q, requests, request_count))
|
||||||
cons_thread = threading.Thread(target=consumer,
|
cons_thread = threading.Thread(target=consumer,
|
||||||
|
|
@ -1436,10 +1573,11 @@ class Speedtest(object):
|
||||||
start = timeit.default_timer()
|
start = timeit.default_timer()
|
||||||
prod_thread.start()
|
prod_thread.start()
|
||||||
cons_thread.start()
|
cons_thread.start()
|
||||||
while prod_thread.isAlive():
|
_is_alive = thread_is_alive
|
||||||
prod_thread.join(timeout=0.1)
|
while _is_alive(prod_thread):
|
||||||
while cons_thread.isAlive():
|
prod_thread.join(timeout=0.001)
|
||||||
cons_thread.join(timeout=0.1)
|
while _is_alive(cons_thread):
|
||||||
|
cons_thread.join(timeout=0.001)
|
||||||
|
|
||||||
stop = timeit.default_timer()
|
stop = timeit.default_timer()
|
||||||
self.results.bytes_received = sum(finished)
|
self.results.bytes_received = sum(finished)
|
||||||
|
|
@ -1450,8 +1588,12 @@ class Speedtest(object):
|
||||||
self.config['threads']['upload'] = 8
|
self.config['threads']['upload'] = 8
|
||||||
return self.results.download
|
return self.results.download
|
||||||
|
|
||||||
def upload(self, callback=do_nothing, pre_allocate=True):
|
def upload(self, callback=do_nothing, pre_allocate=True, threads=None):
|
||||||
"""Test upload speed against speedtest.net"""
|
"""Test upload speed against speedtest.net
|
||||||
|
|
||||||
|
A ``threads`` value of ``None`` will fall back to those dictated
|
||||||
|
by the speedtest.net configuration
|
||||||
|
"""
|
||||||
|
|
||||||
sizes = []
|
sizes = []
|
||||||
|
|
||||||
|
|
@ -1474,13 +1616,19 @@ class Speedtest(object):
|
||||||
)
|
)
|
||||||
if pre_allocate:
|
if pre_allocate:
|
||||||
data.pre_allocate()
|
data.pre_allocate()
|
||||||
|
|
||||||
|
headers = {'Content-length': size}
|
||||||
requests.append(
|
requests.append(
|
||||||
(
|
(
|
||||||
build_request(self.best['url'], data, secure=self._secure),
|
build_request(self.best['url'], data, secure=self._secure,
|
||||||
|
headers=headers),
|
||||||
size
|
size
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
max_threads = threads or self.config['threads']['upload']
|
||||||
|
in_flight = {'threads': 0}
|
||||||
|
|
||||||
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(
|
thread = HTTPUploader(
|
||||||
|
|
@ -1492,21 +1640,26 @@ class Speedtest(object):
|
||||||
opener=self._opener,
|
opener=self._opener,
|
||||||
shutdown_event=self._shutdown_event
|
shutdown_event=self._shutdown_event
|
||||||
)
|
)
|
||||||
|
while in_flight['threads'] >= max_threads:
|
||||||
|
timeit.time.sleep(0.001)
|
||||||
thread.start()
|
thread.start()
|
||||||
q.put(thread, True)
|
q.put(thread, True)
|
||||||
|
in_flight['threads'] += 1
|
||||||
callback(i, request_count, start=True)
|
callback(i, request_count, start=True)
|
||||||
|
|
||||||
finished = []
|
finished = []
|
||||||
|
|
||||||
def consumer(q, request_count):
|
def consumer(q, request_count):
|
||||||
|
_is_alive = thread_is_alive
|
||||||
while len(finished) < request_count:
|
while len(finished) < request_count:
|
||||||
thread = q.get(True)
|
thread = q.get(True)
|
||||||
while thread.isAlive():
|
while _is_alive(thread):
|
||||||
thread.join(timeout=0.1)
|
thread.join(timeout=0.001)
|
||||||
|
in_flight['threads'] -= 1
|
||||||
finished.append(thread.result)
|
finished.append(thread.result)
|
||||||
callback(thread.i, request_count, end=True)
|
callback(thread.i, request_count, end=True)
|
||||||
|
|
||||||
q = Queue(self.config['threads']['upload'])
|
q = Queue(threads or self.config['threads']['upload'])
|
||||||
prod_thread = threading.Thread(target=producer,
|
prod_thread = threading.Thread(target=producer,
|
||||||
args=(q, requests, request_count))
|
args=(q, requests, request_count))
|
||||||
cons_thread = threading.Thread(target=consumer,
|
cons_thread = threading.Thread(target=consumer,
|
||||||
|
|
@ -1514,9 +1667,10 @@ class Speedtest(object):
|
||||||
start = timeit.default_timer()
|
start = timeit.default_timer()
|
||||||
prod_thread.start()
|
prod_thread.start()
|
||||||
cons_thread.start()
|
cons_thread.start()
|
||||||
while prod_thread.isAlive():
|
_is_alive = thread_is_alive
|
||||||
|
while _is_alive(prod_thread):
|
||||||
prod_thread.join(timeout=0.1)
|
prod_thread.join(timeout=0.1)
|
||||||
while cons_thread.isAlive():
|
while _is_alive(cons_thread):
|
||||||
cons_thread.join(timeout=0.1)
|
cons_thread.join(timeout=0.1)
|
||||||
|
|
||||||
stop = timeit.default_timer()
|
stop = timeit.default_timer()
|
||||||
|
|
@ -1541,7 +1695,8 @@ def ctrl_c(shutdown_event):
|
||||||
def version():
|
def version():
|
||||||
"""Print the version"""
|
"""Print the version"""
|
||||||
|
|
||||||
printer(__version__)
|
printer('speedtest-cli %s' % __version__)
|
||||||
|
printer('Python %s' % sys.version.replace('\n', ''))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1574,6 +1729,10 @@ def parse_args():
|
||||||
parser.add_argument('--no-upload', dest='upload', default=True,
|
parser.add_argument('--no-upload', dest='upload', default=True,
|
||||||
action='store_const', const=False,
|
action='store_const', const=False,
|
||||||
help='Do not perform upload test')
|
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',
|
parser.add_argument('--bytes', dest='units', action='store_const',
|
||||||
const=('byte', 8), default=('bit', 1),
|
const=('byte', 8), default=('bit', 1),
|
||||||
help='Display values in bytes instead of bits. Does '
|
help='Display values in bytes instead of bits. Does '
|
||||||
|
|
@ -1788,7 +1947,10 @@ def shell():
|
||||||
if args.download:
|
if args.download:
|
||||||
printer('Testing download speed', quiet,
|
printer('Testing download speed', quiet,
|
||||||
end=('', '\n')[bool(debug)])
|
end=('', '\n')[bool(debug)])
|
||||||
speedtest.download(callback=callback)
|
speedtest.download(
|
||||||
|
callback=callback,
|
||||||
|
threads=(None, 1)[args.single]
|
||||||
|
)
|
||||||
printer('Download: %0.2f M%s/s' %
|
printer('Download: %0.2f M%s/s' %
|
||||||
((results.download / 1000.0 / 1000.0) / args.units[1],
|
((results.download / 1000.0 / 1000.0) / args.units[1],
|
||||||
args.units[0]),
|
args.units[0]),
|
||||||
|
|
@ -1799,7 +1961,11 @@ def shell():
|
||||||
if args.upload:
|
if args.upload:
|
||||||
printer('Testing upload speed', quiet,
|
printer('Testing upload speed', quiet,
|
||||||
end=('', '\n')[bool(debug)])
|
end=('', '\n')[bool(debug)])
|
||||||
speedtest.upload(callback=callback, pre_allocate=args.pre_allocate)
|
speedtest.upload(
|
||||||
|
callback=callback,
|
||||||
|
pre_allocate=args.pre_allocate,
|
||||||
|
threads=(None, 1)[args.single]
|
||||||
|
)
|
||||||
printer('Upload: %0.2f M%s/s' %
|
printer('Upload: %0.2f M%s/s' %
|
||||||
((results.upload / 1000.0 / 1000.0) / args.units[1],
|
((results.upload / 1000.0 / 1000.0) / args.units[1],
|
||||||
args.units[0]),
|
args.units[0]),
|
||||||
|
|
@ -1809,6 +1975,9 @@ def shell():
|
||||||
|
|
||||||
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:
|
||||||
printer('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,
|
||||||
|
|
@ -1819,8 +1988,6 @@ def shell():
|
||||||
elif args.csv:
|
elif args.csv:
|
||||||
printer(results.csv(delimiter=args.csv_delimiter))
|
printer(results.csv(delimiter=args.csv_delimiter))
|
||||||
elif args.json:
|
elif args.json:
|
||||||
if args.share:
|
|
||||||
results.share()
|
|
||||||
printer(results.json())
|
printer(results.json())
|
||||||
|
|
||||||
if args.share and not machine_format:
|
if args.share and not machine_format:
|
||||||
|
|
@ -1836,7 +2003,10 @@ def main():
|
||||||
e = get_exception()
|
e = get_exception()
|
||||||
# Ignore a successful exit, or argparse exit
|
# Ignore a successful exit, or argparse exit
|
||||||
if getattr(e, 'code', 1) not in (0, 2):
|
if getattr(e, 'code', 1) not in (0, 2):
|
||||||
raise SystemExit('ERROR: %s' % e)
|
msg = '%s' % e
|
||||||
|
if not msg:
|
||||||
|
msg = '%r' % e
|
||||||
|
raise SystemExit('ERROR: %s' % msg)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue