Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
jajbshjahavahh
GitHub Repository: jajbshjahavahh/gojo-satoru
Path: blob/master/speed.py
1863 views
1
import csv
2
import datetime
3
import errno
4
import math
5
import os
6
import platform
7
import re
8
import signal
9
import socket
10
import sys
11
import threading
12
import timeit
13
import xml.parsers.expat
14
15
try:
16
import gzip
17
GZIP_BASE = gzip.GzipFile
18
except ImportError:
19
gzip = None
20
GZIP_BASE = object
21
22
__version__ = '2.1.4b1'
23
24
25
class FakeShutdownEvent(object):
26
"""Class to fake a threading.Event.isSet so that users of this module
27
are not required to register their own threading.Event()
28
"""
29
30
@staticmethod
31
def isSet():
32
"Dummy method to always return false"""
33
return False
34
35
is_set = isSet
36
37
38
# Some global variables we use
39
DEBUG = False
40
_GLOBAL_DEFAULT_TIMEOUT = object()
41
PY25PLUS = sys.version_info[:2] >= (2, 5)
42
PY26PLUS = sys.version_info[:2] >= (2, 6)
43
PY32PLUS = sys.version_info[:2] >= (3, 2)
44
PY310PLUS = sys.version_info[:2] >= (3, 10)
45
46
# Begin import game to handle Python 2 and Python 3
47
try:
48
import json
49
except ImportError:
50
try:
51
import simplejson as json
52
except ImportError:
53
json = None
54
55
try:
56
import xml.etree.ElementTree as ET
57
try:
58
from xml.etree.ElementTree import _Element as ET_Element
59
except ImportError:
60
pass
61
except ImportError:
62
from xml.dom import minidom as DOM
63
from xml.parsers.expat import ExpatError
64
ET = None
65
66
try:
67
from urllib2 import (urlopen, Request, HTTPError, URLError,
68
AbstractHTTPHandler, ProxyHandler,
69
HTTPDefaultErrorHandler, HTTPRedirectHandler,
70
HTTPErrorProcessor, OpenerDirector)
71
except ImportError:
72
from urllib.request import (urlopen, Request, HTTPError, URLError,
73
AbstractHTTPHandler, ProxyHandler,
74
HTTPDefaultErrorHandler, HTTPRedirectHandler,
75
HTTPErrorProcessor, OpenerDirector)
76
77
try:
78
from httplib import HTTPConnection, BadStatusLine
79
except ImportError:
80
from http.client import HTTPConnection, BadStatusLine
81
82
try:
83
from httplib import HTTPSConnection
84
except ImportError:
85
try:
86
from http.client import HTTPSConnection
87
except ImportError:
88
HTTPSConnection = None
89
90
try:
91
from httplib import FakeSocket
92
except ImportError:
93
FakeSocket = None
94
95
try:
96
from Queue import Queue
97
except ImportError:
98
from queue import Queue
99
100
try:
101
from urlparse import urlparse
102
except ImportError:
103
from urllib.parse import urlparse
104
105
try:
106
from urlparse import parse_qs
107
except ImportError:
108
try:
109
from urllib.parse import parse_qs
110
except ImportError:
111
from cgi import parse_qs
112
113
try:
114
from hashlib import md5
115
except ImportError:
116
from md5 import md5
117
118
try:
119
from argparse import ArgumentParser as ArgParser
120
from argparse import SUPPRESS as ARG_SUPPRESS
121
PARSER_TYPE_INT = int
122
PARSER_TYPE_STR = str
123
PARSER_TYPE_FLOAT = float
124
except ImportError:
125
from optparse import OptionParser as ArgParser
126
from optparse import SUPPRESS_HELP as ARG_SUPPRESS
127
PARSER_TYPE_INT = 'int'
128
PARSER_TYPE_STR = 'string'
129
PARSER_TYPE_FLOAT = 'float'
130
131
try:
132
from cStringIO import StringIO
133
BytesIO = None
134
except ImportError:
135
try:
136
from StringIO import StringIO
137
BytesIO = None
138
except ImportError:
139
from io import StringIO, BytesIO
140
141
try:
142
import __builtin__
143
except ImportError:
144
import builtins
145
from io import TextIOWrapper, FileIO
146
147
class _Py3Utf8Output(TextIOWrapper):
148
"""UTF-8 encoded wrapper around stdout for py3, to override
149
ASCII stdout
150
"""
151
def __init__(self, f, **kwargs):
152
buf = FileIO(f.fileno(), 'w')
153
super(_Py3Utf8Output, self).__init__(
154
buf,
155
encoding='utf8',
156
errors='strict'
157
)
158
159
def write(self, s):
160
super(_Py3Utf8Output, self).write(s)
161
self.flush()
162
163
_py3_print = getattr(builtins, 'print')
164
try:
165
_py3_utf8_stdout = _Py3Utf8Output(sys.stdout)
166
_py3_utf8_stderr = _Py3Utf8Output(sys.stderr)
167
except OSError:
168
# sys.stdout/sys.stderr is not a compatible stdout/stderr object
169
# just use it and hope things go ok
170
_py3_utf8_stdout = sys.stdout
171
_py3_utf8_stderr = sys.stderr
172
173
def to_utf8(v):
174
"""No-op encode to utf-8 for py3"""
175
return v
176
177
def print_(*args, **kwargs):
178
"""Wrapper function for py3 to print, with a utf-8 encoded stdout"""
179
if kwargs.get('file') == sys.stderr:
180
kwargs['file'] = _py3_utf8_stderr
181
else:
182
kwargs['file'] = kwargs.get('file', _py3_utf8_stdout)
183
_py3_print(*args, **kwargs)
184
else:
185
del __builtin__
186
187
def to_utf8(v):
188
"""Encode value to utf-8 if possible for py2"""
189
try:
190
return v.encode('utf8', 'strict')
191
except AttributeError:
192
return v
193
194
def print_(*args, **kwargs):
195
"""The new-style print function for Python 2.4 and 2.5.
196
197
Taken from https://pypi.python.org/pypi/six/
198
199
Modified to set encoding to UTF-8 always, and to flush after write
200
"""
201
fp = kwargs.pop("file", sys.stdout)
202
if fp is None:
203
return
204
205
def write(data):
206
if not isinstance(data, basestring):
207
data = str(data)
208
# If the file has an encoding, encode unicode with it.
209
encoding = 'utf8' # Always trust UTF-8 for output
210
if (isinstance(fp, file) and
211
isinstance(data, unicode) and
212
encoding is not None):
213
errors = getattr(fp, "errors", None)
214
if errors is None:
215
errors = "strict"
216
data = data.encode(encoding, errors)
217
fp.write(data)
218
fp.flush()
219
want_unicode = False
220
sep = kwargs.pop("sep", None)
221
if sep is not None:
222
if isinstance(sep, unicode):
223
want_unicode = True
224
elif not isinstance(sep, str):
225
raise TypeError("sep must be None or a string")
226
end = kwargs.pop("end", None)
227
if end is not None:
228
if isinstance(end, unicode):
229
want_unicode = True
230
elif not isinstance(end, str):
231
raise TypeError("end must be None or a string")
232
if kwargs:
233
raise TypeError("invalid keyword arguments to print()")
234
if not want_unicode:
235
for arg in args:
236
if isinstance(arg, unicode):
237
want_unicode = True
238
break
239
if want_unicode:
240
newline = unicode("\n")
241
space = unicode(" ")
242
else:
243
newline = "\n"
244
space = " "
245
if sep is None:
246
sep = space
247
if end is None:
248
end = newline
249
for i, arg in enumerate(args):
250
if i:
251
write(sep)
252
write(arg)
253
write(end)
254
255
# Exception "constants" to support Python 2 through Python 3
256
try:
257
import ssl
258
try:
259
CERT_ERROR = (ssl.CertificateError,)
260
except AttributeError:
261
CERT_ERROR = tuple()
262
263
HTTP_ERRORS = (
264
(HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) +
265
CERT_ERROR
266
)
267
except ImportError:
268
ssl = None
269
HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
270
271
if PY32PLUS:
272
etree_iter = ET.Element.iter
273
elif PY25PLUS:
274
etree_iter = ET_Element.getiterator
275
276
if PY26PLUS:
277
thread_is_alive = threading.Thread.is_alive
278
else:
279
thread_is_alive = threading.Thread.isAlive
280
281
282
def event_is_set(event):
283
try:
284
return event.is_set()
285
except AttributeError:
286
return event.isSet()
287
288
289
class SpeedtestException(Exception):
290
"""Base exception for this module"""
291
292
293
class SpeedtestCLIError(SpeedtestException):
294
"""Generic exception for raising errors during CLI operation"""
295
296
297
class SpeedtestHTTPError(SpeedtestException):
298
"""Base HTTP exception for this module"""
299
300
301
class SpeedtestConfigError(SpeedtestException):
302
"""Configuration XML is invalid"""
303
304
305
class SpeedtestServersError(SpeedtestException):
306
"""Servers XML is invalid"""
307
308
309
class ConfigRetrievalError(SpeedtestHTTPError):
310
"""Could not retrieve config.php"""
311
312
313
class ServersRetrievalError(SpeedtestHTTPError):
314
"""Could not retrieve speedtest-servers.php"""
315
316
317
class InvalidServerIDType(SpeedtestException):
318
"""Server ID used for filtering was not an integer"""
319
320
321
class NoMatchedServers(SpeedtestException):
322
"""No servers matched when filtering"""
323
324
325
class SpeedtestMiniConnectFailure(SpeedtestException):
326
"""Could not connect to the provided speedtest mini server"""
327
328
329
class InvalidSpeedtestMiniServer(SpeedtestException):
330
"""Server provided as a speedtest mini server does not actually appear
331
to be a speedtest mini server
332
"""
333
334
335
class ShareResultsConnectFailure(SpeedtestException):
336
"""Could not connect to speedtest.net API to POST results"""
337
338
339
class ShareResultsSubmitFailure(SpeedtestException):
340
"""Unable to successfully POST results to speedtest.net API after
341
connection
342
"""
343
344
345
class SpeedtestUploadTimeout(SpeedtestException):
346
"""testlength configuration reached during upload
347
Used to ensure the upload halts when no additional data should be sent
348
"""
349
350
351
class SpeedtestBestServerFailure(SpeedtestException):
352
"""Unable to determine best server"""
353
354
355
class SpeedtestMissingBestServer(SpeedtestException):
356
"""get_best_server not called or not able to determine best server"""
357
358
359
def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
360
source_address=None):
361
"""Connect to *address* and return the socket object.
362
363
Convenience function. Connect to *address* (a 2-tuple ``(host,
364
port)``) and return the socket object. Passing the optional
365
*timeout* parameter will set the timeout on the socket instance
366
before attempting to connect. If no *timeout* is supplied, the
367
global default timeout setting returned by :func:`getdefaulttimeout`
368
is used. If *source_address* is set it must be a tuple of (host, port)
369
for the socket to bind as a source address before making the connection.
370
An host of '' or port 0 tells the OS to use the default.
371
372
Largely vendored from Python 2.7, modified to work with Python 2.4
373
"""
374
375
host, port = address
376
err = None
377
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
378
af, socktype, proto, canonname, sa = res
379
sock = None
380
try:
381
sock = socket.socket(af, socktype, proto)
382
if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
383
sock.settimeout(float(timeout))
384
if source_address:
385
sock.bind(source_address)
386
sock.connect(sa)
387
return sock
388
389
except socket.error:
390
err = get_exception()
391
if sock is not None:
392
sock.close()
393
394
if err is not None:
395
raise err
396
else:
397
raise socket.error("getaddrinfo returns an empty list")
398
399
400
class SpeedtestHTTPConnection(HTTPConnection):
401
"""Custom HTTPConnection to support source_address across
402
Python 2.4 - Python 3
403
"""
404
def __init__(self, *args, **kwargs):
405
source_address = kwargs.pop('source_address', None)
406
timeout = kwargs.pop('timeout', 10)
407
408
self._tunnel_host = None
409
410
HTTPConnection.__init__(self, *args, **kwargs)
411
412
self.source_address = source_address
413
self.timeout = timeout
414
415
def connect(self):
416
"""Connect to the host and port specified in __init__."""
417
try:
418
self.sock = socket.create_connection(
419
(self.host, self.port),
420
self.timeout,
421
self.source_address
422
)
423
except (AttributeError, TypeError):
424
self.sock = create_connection(
425
(self.host, self.port),
426
self.timeout,
427
self.source_address
428
)
429
430
if self._tunnel_host:
431
self._tunnel()
432
433
434
if HTTPSConnection:
435
class SpeedtestHTTPSConnection(HTTPSConnection):
436
"""Custom HTTPSConnection to support source_address across
437
Python 2.4 - Python 3
438
"""
439
default_port = 443
440
441
def __init__(self, *args, **kwargs):
442
source_address = kwargs.pop('source_address', None)
443
timeout = kwargs.pop('timeout', 10)
444
445
self._tunnel_host = None
446
447
HTTPSConnection.__init__(self, *args, **kwargs)
448
449
self.timeout = timeout
450
self.source_address = source_address
451
452
def connect(self):
453
"Connect to a host on a given (SSL) port."
454
try:
455
self.sock = socket.create_connection(
456
(self.host, self.port),
457
self.timeout,
458
self.source_address
459
)
460
except (AttributeError, TypeError):
461
self.sock = create_connection(
462
(self.host, self.port),
463
self.timeout,
464
self.source_address
465
)
466
467
if self._tunnel_host:
468
self._tunnel()
469
470
if ssl:
471
try:
472
kwargs = {}
473
if hasattr(ssl, 'SSLContext'):
474
if self._tunnel_host:
475
kwargs['server_hostname'] = self._tunnel_host
476
else:
477
kwargs['server_hostname'] = self.host
478
self.sock = self._context.wrap_socket(self.sock, **kwargs)
479
except AttributeError:
480
self.sock = ssl.wrap_socket(self.sock)
481
try:
482
self.sock.server_hostname = self.host
483
except AttributeError:
484
pass
485
elif FakeSocket:
486
# Python 2.4/2.5 support
487
try:
488
self.sock = FakeSocket(self.sock, socket.ssl(self.sock))
489
except AttributeError:
490
raise SpeedtestException(
491
'This version of Python does not support HTTPS/SSL '
492
'functionality'
493
)
494
else:
495
raise SpeedtestException(
496
'This version of Python does not support HTTPS/SSL '
497
'functionality'
498
)
499
500
501
def _build_connection(connection, source_address, timeout, context=None):
502
"""Cross Python 2.4 - Python 3 callable to build an ``HTTPConnection`` or
503
``HTTPSConnection`` with the args we need
504
505
Called from ``http(s)_open`` methods of ``SpeedtestHTTPHandler`` or
506
``SpeedtestHTTPSHandler``
507
"""
508
def inner(host, **kwargs):
509
kwargs.update({
510
'source_address': source_address,
511
'timeout': timeout
512
})
513
if context:
514
kwargs['context'] = context
515
return connection(host, **kwargs)
516
return inner
517
518
519
class SpeedtestHTTPHandler(AbstractHTTPHandler):
520
"""Custom ``HTTPHandler`` that can build a ``HTTPConnection`` with the
521
args we need for ``source_address`` and ``timeout``
522
"""
523
def __init__(self, debuglevel=0, source_address=None, timeout=10):
524
AbstractHTTPHandler.__init__(self, debuglevel)
525
self.source_address = source_address
526
self.timeout = timeout
527
528
def http_open(self, req):
529
return self.do_open(
530
_build_connection(
531
SpeedtestHTTPConnection,
532
self.source_address,
533
self.timeout
534
),
535
req
536
)
537
538
http_request = AbstractHTTPHandler.do_request_
539
540
541
class SpeedtestHTTPSHandler(AbstractHTTPHandler):
542
"""Custom ``HTTPSHandler`` that can build a ``HTTPSConnection`` with the
543
args we need for ``source_address`` and ``timeout``
544
"""
545
def __init__(self, debuglevel=0, context=None, source_address=None,
546
timeout=10):
547
AbstractHTTPHandler.__init__(self, debuglevel)
548
self._context = context
549
self.source_address = source_address
550
self.timeout = timeout
551
552
def https_open(self, req):
553
return self.do_open(
554
_build_connection(
555
SpeedtestHTTPSConnection,
556
self.source_address,
557
self.timeout,
558
context=self._context,
559
),
560
req
561
)
562
563
https_request = AbstractHTTPHandler.do_request_
564
565
566
def build_opener(source_address=None, timeout=10):
567
"""Function similar to ``urllib2.build_opener`` that will build
568
an ``OpenerDirector`` with the explicit handlers we want,
569
``source_address`` for binding, ``timeout`` and our custom
570
`User-Agent`
571
"""
572
573
printer('Timeout set to %d' % timeout, debug=True)
574
575
if source_address:
576
source_address_tuple = (source_address, 0)
577
printer('Binding to source address: %r' % (source_address_tuple,),
578
debug=True)
579
else:
580
source_address_tuple = None
581
582
handlers = [
583
ProxyHandler(),
584
SpeedtestHTTPHandler(source_address=source_address_tuple,
585
timeout=timeout),
586
SpeedtestHTTPSHandler(source_address=source_address_tuple,
587
timeout=timeout),
588
HTTPDefaultErrorHandler(),
589
HTTPRedirectHandler(),
590
HTTPErrorProcessor()
591
]
592
593
opener = OpenerDirector()
594
opener.addheaders = [('User-agent', build_user_agent())]
595
596
for handler in handlers:
597
opener.add_handler(handler)
598
599
return opener
600
601
602
class GzipDecodedResponse(GZIP_BASE):
603
"""A file-like object to decode a response encoded with the gzip
604
method, as described in RFC 1952.
605
606
Largely copied from ``xmlrpclib``/``xmlrpc.client`` and modified
607
to work for py2.4-py3
608
"""
609
def __init__(self, response):
610
# response doesn't support tell() and read(), required by
611
# GzipFile
612
if not gzip:
613
raise SpeedtestHTTPError('HTTP response body is gzip encoded, '
614
'but gzip support is not available')
615
IO = BytesIO or StringIO
616
self.io = IO()
617
while 1:
618
chunk = response.read(1024)
619
if len(chunk) == 0:
620
break
621
self.io.write(chunk)
622
self.io.seek(0)
623
gzip.GzipFile.__init__(self, mode='rb', fileobj=self.io)
624
625
def close(self):
626
try:
627
gzip.GzipFile.close(self)
628
finally:
629
self.io.close()
630
631
632
def get_exception():
633
"""Helper function to work with py2.4-py3 for getting the current
634
exception in a try/except block
635
"""
636
return sys.exc_info()[1]
637
638
639
def distance(origin, destination):
640
"""Determine distance between 2 sets of [lat,lon] in km"""
641
642
lat1, lon1 = origin
643
lat2, lon2 = destination
644
radius = 6371 # km
645
646
dlat = math.radians(lat2 - lat1)
647
dlon = math.radians(lon2 - lon1)
648
a = (math.sin(dlat / 2) * math.sin(dlat / 2) +
649
math.cos(math.radians(lat1)) *
650
math.cos(math.radians(lat2)) * math.sin(dlon / 2) *
651
math.sin(dlon / 2))
652
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
653
d = radius * c
654
655
return d
656
657
658
def build_user_agent():
659
"""Build a Mozilla/5.0 compatible User-Agent string"""
660
661
ua_tuple = (
662
'Mozilla/5.0',
663
'(%s; U; %s; en-us)' % (platform.platform(),
664
platform.architecture()[0]),
665
'Python/%s' % platform.python_version(),
666
'(KHTML, like Gecko)',
667
'speedtest-cli/%s' % __version__
668
)
669
user_agent = ' '.join(ua_tuple)
670
printer('User-Agent: %s' % user_agent, debug=True)
671
return user_agent
672
673
674
def build_request(url, data=None, headers=None, bump='0', secure=False):
675
"""Build a urllib2 request object
676
677
This function automatically adds a User-Agent header to all requests
678
679
"""
680
681
if not headers:
682
headers = {}
683
684
if url[0] == ':':
685
scheme = ('http', 'https')[bool(secure)]
686
schemed_url = '%s%s' % (scheme, url)
687
else:
688
schemed_url = url
689
690
if '?' in url:
691
delim = '&'
692
else:
693
delim = '?'
694
695
# WHO YOU GONNA CALL? CACHE BUSTERS!
696
final_url = '%s%sx=%s.%s' % (schemed_url, delim,
697
int(timeit.time.time() * 1000),
698
bump)
699
700
headers.update({
701
'Cache-Control': 'no-cache',
702
})
703
704
printer('%s %s' % (('GET', 'POST')[bool(data)], final_url),
705
debug=True)
706
707
return Request(final_url, data=data, headers=headers)
708
709
710
def catch_request(request, opener=None):
711
"""Helper function to catch common exceptions encountered when
712
establishing a connection with a HTTP/HTTPS request
713
714
"""
715
716
if opener:
717
_open = opener.open
718
else:
719
_open = urlopen
720
721
try:
722
uh = _open(request)
723
if request.get_full_url() != uh.geturl():
724
printer('Redirected to %s' % uh.geturl(), debug=True)
725
return uh, False
726
except HTTP_ERRORS:
727
e = get_exception()
728
return None, e
729
730
731
def get_response_stream(response):
732
"""Helper function to return either a Gzip reader if
733
``Content-Encoding`` is ``gzip`` otherwise the response itself
734
735
"""
736
737
try:
738
getheader = response.headers.getheader
739
except AttributeError:
740
getheader = response.getheader
741
742
if getheader('content-encoding') == 'gzip':
743
return GzipDecodedResponse(response)
744
745
return response
746
747
748
def get_attributes_by_tag_name(dom, tag_name):
749
"""Retrieve an attribute from an XML document and return it in a
750
consistent format
751
752
Only used with xml.dom.minidom, which is likely only to be used
753
with python versions older than 2.5
754
"""
755
elem = dom.getElementsByTagName(tag_name)[0]
756
return dict(list(elem.attributes.items()))
757
758
759
def print_dots(shutdown_event):
760
"""Built in callback function used by Thread classes for printing
761
status
762
"""
763
def inner(current, total, start=False, end=False):
764
if event_is_set(shutdown_event):
765
return
766
767
sys.stdout.write('.')
768
if current + 1 == total and end is True:
769
sys.stdout.write('\n')
770
sys.stdout.flush()
771
return inner
772
773
774
def do_nothing(*args, **kwargs):
775
pass
776
777
778
class HTTPDownloader(threading.Thread):
779
"""Thread class for retrieving a URL"""
780
781
def __init__(self, i, request, start, timeout, opener=None,
782
shutdown_event=None):
783
threading.Thread.__init__(self)
784
self.request = request
785
self.result = [0]
786
self.starttime = start
787
self.timeout = timeout
788
self.i = i
789
if opener:
790
self._opener = opener.open
791
else:
792
self._opener = urlopen
793
794
if shutdown_event:
795
self._shutdown_event = shutdown_event
796
else:
797
self._shutdown_event = FakeShutdownEvent()
798
799
def run(self):
800
try:
801
if (timeit.default_timer() - self.starttime) <= self.timeout:
802
f = self._opener(self.request)
803
while (not event_is_set(self._shutdown_event) and
804
(timeit.default_timer() - self.starttime) <=
805
self.timeout):
806
self.result.append(len(f.read(10240)))
807
if self.result[-1] == 0:
808
break
809
f.close()
810
except IOError:
811
pass
812
except HTTP_ERRORS:
813
pass
814
815
816
class HTTPUploaderData(object):
817
"""File like object to improve cutting off the upload once the timeout
818
has been reached
819
"""
820
821
def __init__(self, length, start, timeout, shutdown_event=None):
822
self.length = length
823
self.start = start
824
self.timeout = timeout
825
826
if shutdown_event:
827
self._shutdown_event = shutdown_event
828
else:
829
self._shutdown_event = FakeShutdownEvent()
830
831
self._data = None
832
833
self.total = [0]
834
835
def pre_allocate(self):
836
chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
837
multiplier = int(round(int(self.length) / 36.0))
838
IO = BytesIO or StringIO
839
try:
840
self._data = IO(
841
('content1=%s' %
842
(chars * multiplier)[0:int(self.length) - 9]
843
).encode()
844
)
845
except MemoryError:
846
raise SpeedtestCLIError(
847
'Insufficient memory to pre-allocate upload data. Please '
848
'use --no-pre-allocate'
849
)
850
851
@property
852
def data(self):
853
if not self._data:
854
self.pre_allocate()
855
return self._data
856
857
def read(self, n=10240):
858
if ((timeit.default_timer() - self.start) <= self.timeout and
859
not event_is_set(self._shutdown_event)):
860
chunk = self.data.read(n)
861
self.total.append(len(chunk))
862
return chunk
863
else:
864
raise SpeedtestUploadTimeout()
865
866
def __len__(self):
867
return self.length
868
869
870
class HTTPUploader(threading.Thread):
871
"""Thread class for putting a URL"""
872
873
def __init__(self, i, request, start, size, timeout, opener=None,
874
shutdown_event=None):
875
threading.Thread.__init__(self)
876
self.request = request
877
self.request.data.start = self.starttime = start
878
self.size = size
879
self.result = 0
880
self.timeout = timeout
881
self.i = i
882
883
if opener:
884
self._opener = opener.open
885
else:
886
self._opener = urlopen
887
888
if shutdown_event:
889
self._shutdown_event = shutdown_event
890
else:
891
self._shutdown_event = FakeShutdownEvent()
892
893
def run(self):
894
request = self.request
895
try:
896
if ((timeit.default_timer() - self.starttime) <= self.timeout and
897
not event_is_set(self._shutdown_event)):
898
try:
899
f = self._opener(request)
900
except TypeError:
901
# PY24 expects a string or buffer
902
# This also causes issues with Ctrl-C, but we will concede
903
# for the moment that Ctrl-C on PY24 isn't immediate
904
request = build_request(self.request.get_full_url(),
905
data=request.data.read(self.size))
906
f = self._opener(request)
907
f.read(11)
908
f.close()
909
self.result = sum(self.request.data.total)
910
else:
911
self.result = 0
912
except (IOError, SpeedtestUploadTimeout):
913
self.result = sum(self.request.data.total)
914
except HTTP_ERRORS:
915
self.result = 0
916
917
918
class SpeedtestResults(object):
919
"""Class for holding the results of a speedtest, including:
920
921
Download speed
922
Upload speed
923
Ping/Latency to test server
924
Data about server that the test was run against
925
926
Additionally this class can return a result data as a dictionary or CSV,
927
as well as submit a POST of the result data to the speedtest.net API
928
to get a share results image link.
929
"""
930
931
def __init__(self, download=0, upload=0, ping=0, server=None, client=None,
932
opener=None, secure=False):
933
self.download = download
934
self.upload = upload
935
self.ping = ping
936
if server is None:
937
self.server = {}
938
else:
939
self.server = server
940
self.client = client or {}
941
942
self._share = None
943
self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat()
944
self.bytes_received = 0
945
self.bytes_sent = 0
946
947
if opener:
948
self._opener = opener
949
else:
950
self._opener = build_opener()
951
952
self._secure = secure
953
954
def __repr__(self):
955
return repr(self.dict())
956
957
def share(self):
958
"""POST data to the speedtest.net API to obtain a share results
959
link
960
"""
961
962
if self._share:
963
return self._share
964
965
download = int(round(self.download / 1000.0, 0))
966
ping = int(round(self.ping, 0))
967
upload = int(round(self.upload / 1000.0, 0))
968
969
# Build the request to send results back to speedtest.net
970
# We use a list instead of a dict because the API expects parameters
971
# in a certain order
972
api_data = [
973
'recommendedserverid=%s' % self.server['id'],
974
'ping=%s' % ping,
975
'screenresolution=',
976
'promo=',
977
'download=%s' % download,
978
'screendpi=',
979
'upload=%s' % upload,
980
'testmethod=http',
981
'hash=%s' % md5(('%s-%s-%s-%s' %
982
(ping, upload, download, '297aae72'))
983
.encode()).hexdigest(),
984
'touchscreen=none',
985
'startmode=pingselect',
986
'accuracy=1',
987
'bytesreceived=%s' % self.bytes_received,
988
'bytessent=%s' % self.bytes_sent,
989
'serverid=%s' % self.server['id'],
990
]
991
992
headers = {'Referer': 'http://c.speedtest.net/flash/speedtest.swf'}
993
request = build_request('://www.speedtest.net/api/api.php',
994
data='&'.join(api_data).encode(),
995
headers=headers, secure=self._secure)
996
f, e = catch_request(request, opener=self._opener)
997
if e:
998
raise ShareResultsConnectFailure(e)
999
1000
response = f.read()
1001
code = f.code
1002
f.close()
1003
1004
if int(code) != 200:
1005
raise ShareResultsSubmitFailure('Could not submit results to '
1006
'speedtest.net')
1007
1008
qsargs = parse_qs(response.decode())
1009
resultid = qsargs.get('resultid')
1010
if not resultid or len(resultid) != 1:
1011
raise ShareResultsSubmitFailure('Could not submit results to '
1012
'speedtest.net')
1013
1014
self._share = 'http://www.speedtest.net/result/%s.png' % resultid[0]
1015
1016
return self._share
1017
1018
def dict(self):
1019
"""Return dictionary of result data"""
1020
1021
return {
1022
'download': self.download,
1023
'upload': self.upload,
1024
'ping': self.ping,
1025
'server': self.server,
1026
'timestamp': self.timestamp,
1027
'bytes_sent': self.bytes_sent,
1028
'bytes_received': self.bytes_received,
1029
'share': self._share,
1030
'client': self.client,
1031
}
1032
1033
@staticmethod
1034
def csv_header(delimiter=','):
1035
"""Return CSV Headers"""
1036
1037
row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance',
1038
'Ping', 'Download', 'Upload', 'Share', 'IP Address']
1039
out = StringIO()
1040
writer = csv.writer(out, delimiter=delimiter, lineterminator='')
1041
writer.writerow([to_utf8(v) for v in row])
1042
return out.getvalue()
1043
1044
def csv(self, delimiter=','):
1045
"""Return data in CSV format"""
1046
1047
data = self.dict()
1048
out = StringIO()
1049
writer = csv.writer(out, delimiter=delimiter, lineterminator='')
1050
row = [data['server']['id'], data['server']['sponsor'],
1051
data['server']['name'], data['timestamp'],
1052
data['server']['d'], data['ping'], data['download'],
1053
data['upload'], self._share or '', self.client['ip']]
1054
writer.writerow([to_utf8(v) for v in row])
1055
return out.getvalue()
1056
1057
def json(self, pretty=False):
1058
"""Return data in JSON format"""
1059
1060
kwargs = {}
1061
if pretty:
1062
kwargs.update({
1063
'indent': 4,
1064
'sort_keys': True
1065
})
1066
return json.dumps(self.dict(), **kwargs)
1067
1068
1069
class Speedtest(object):
1070
"""Class for performing standard speedtest.net testing operations"""
1071
1072
def __init__(self, config=None, source_address=None, timeout=10,
1073
secure=False, shutdown_event=None):
1074
self.config = {}
1075
1076
self._source_address = source_address
1077
self._timeout = timeout
1078
self._opener = build_opener(source_address, timeout)
1079
1080
self._secure = secure
1081
1082
if shutdown_event:
1083
self._shutdown_event = shutdown_event
1084
else:
1085
self._shutdown_event = FakeShutdownEvent()
1086
1087
self.get_config()
1088
if config is not None:
1089
self.config.update(config)
1090
1091
self.servers = {}
1092
self.closest = []
1093
self._best = {}
1094
1095
self.results = SpeedtestResults(
1096
client=self.config['client'],
1097
opener=self._opener,
1098
secure=secure,
1099
)
1100
1101
@property
1102
def best(self):
1103
if not self._best:
1104
self.get_best_server()
1105
return self._best
1106
1107
def get_config(self):
1108
"""Download the speedtest.net configuration and return only the data
1109
we are interested in
1110
"""
1111
1112
headers = {}
1113
if gzip:
1114
headers['Accept-Encoding'] = 'gzip'
1115
request = build_request('://www.speedtest.net/speedtest-config.php',
1116
headers=headers, secure=self._secure)
1117
uh, e = catch_request(request, opener=self._opener)
1118
if e:
1119
raise ConfigRetrievalError(e)
1120
configxml_list = []
1121
1122
stream = get_response_stream(uh)
1123
1124
while 1:
1125
try:
1126
configxml_list.append(stream.read(1024))
1127
except (OSError, EOFError):
1128
raise ConfigRetrievalError(get_exception())
1129
if len(configxml_list[-1]) == 0:
1130
break
1131
stream.close()
1132
uh.close()
1133
1134
if int(uh.code) != 200:
1135
return None
1136
1137
configxml = ''.encode().join(configxml_list)
1138
1139
printer('Config XML:\n%s' % configxml, debug=True)
1140
1141
try:
1142
try:
1143
root = ET.fromstring(configxml)
1144
except ET.ParseError:
1145
e = get_exception()
1146
raise SpeedtestConfigError(
1147
'Malformed speedtest.net configuration: %s' % e
1148
)
1149
server_config = root.find('server-config').attrib
1150
download = root.find('download').attrib
1151
upload = root.find('upload').attrib
1152
# times = root.find('times').attrib
1153
client = root.find('client').attrib
1154
1155
except AttributeError:
1156
try:
1157
root = DOM.parseString(configxml)
1158
except ExpatError:
1159
e = get_exception()
1160
raise SpeedtestConfigError(
1161
'Malformed speedtest.net configuration: %s' % e
1162
)
1163
server_config = get_attributes_by_tag_name(root, 'server-config')
1164
download = get_attributes_by_tag_name(root, 'download')
1165
upload = get_attributes_by_tag_name(root, 'upload')
1166
# times = get_attributes_by_tag_name(root, 'times')
1167
client = get_attributes_by_tag_name(root, 'client')
1168
1169
ignore_servers = [
1170
int(i) for i in server_config['ignoreids'].split(',') if i
1171
]
1172
1173
ratio = int(upload['ratio'])
1174
upload_max = int(upload['maxchunkcount'])
1175
up_sizes = [32768, 65536, 131072, 262144, 524288, 1048576, 7340032]
1176
sizes = {
1177
'upload': up_sizes[ratio - 1:],
1178
'download': [350, 500, 750, 1000, 1500, 2000, 2500,
1179
3000, 3500, 4000]
1180
}
1181
1182
size_count = len(sizes['upload'])
1183
1184
upload_count = int(math.ceil(upload_max / size_count))
1185
1186
counts = {
1187
'upload': upload_count,
1188
'download': int(download['threadsperurl'])
1189
}
1190
1191
threads = {
1192
'upload': int(upload['threads']),
1193
'download': int(server_config['threadcount']) * 2
1194
}
1195
1196
length = {
1197
'upload': int(upload['testlength']),
1198
'download': int(download['testlength'])
1199
}
1200
1201
self.config.update({
1202
'client': client,
1203
'ignore_servers': ignore_servers,
1204
'sizes': sizes,
1205
'counts': counts,
1206
'threads': threads,
1207
'length': length,
1208
'upload_max': upload_count * size_count
1209
})
1210
1211
try:
1212
self.lat_lon = (float(client['lat']), float(client['lon']))
1213
except ValueError:
1214
raise SpeedtestConfigError(
1215
'Unknown location: lat=%r lon=%r' %
1216
(client.get('lat'), client.get('lon'))
1217
)
1218
1219
printer('Config:\n%r' % self.config, debug=True)
1220
1221
return self.config
1222
1223
def get_servers(self, servers=None, exclude=None):
1224
"""Retrieve a the list of speedtest.net servers, optionally filtered
1225
to servers matching those specified in the ``servers`` argument
1226
"""
1227
if servers is None:
1228
servers = []
1229
1230
if exclude is None:
1231
exclude = []
1232
1233
self.servers.clear()
1234
1235
for server_list in (servers, exclude):
1236
for i, s in enumerate(server_list):
1237
try:
1238
server_list[i] = int(s)
1239
except ValueError:
1240
raise InvalidServerIDType(
1241
'%s is an invalid server type, must be int' % s
1242
)
1243
1244
urls = [
1245
'://www.speedtest.net/speedtest-servers-static.php',
1246
'http://c.speedtest.net/speedtest-servers-static.php',
1247
'://www.speedtest.net/speedtest-servers.php',
1248
'http://c.speedtest.net/speedtest-servers.php',
1249
]
1250
1251
headers = {}
1252
if gzip:
1253
headers['Accept-Encoding'] = 'gzip'
1254
1255
errors = []
1256
for url in urls:
1257
try:
1258
request = build_request(
1259
'%s?threads=%s' % (url,
1260
self.config['threads']['download']),
1261
headers=headers,
1262
secure=self._secure
1263
)
1264
uh, e = catch_request(request, opener=self._opener)
1265
if e:
1266
errors.append('%s' % e)
1267
raise ServersRetrievalError()
1268
1269
stream = get_response_stream(uh)
1270
1271
serversxml_list = []
1272
while 1:
1273
try:
1274
serversxml_list.append(stream.read(1024))
1275
except (OSError, EOFError):
1276
raise ServersRetrievalError(get_exception())
1277
if len(serversxml_list[-1]) == 0:
1278
break
1279
1280
stream.close()
1281
uh.close()
1282
1283
if int(uh.code) != 200:
1284
raise ServersRetrievalError()
1285
1286
serversxml = ''.encode().join(serversxml_list)
1287
1288
printer('Servers XML:\n%s' % serversxml, debug=True)
1289
1290
try:
1291
try:
1292
try:
1293
root = ET.fromstring(serversxml)
1294
except ET.ParseError:
1295
e = get_exception()
1296
raise SpeedtestServersError(
1297
'Malformed speedtest.net server list: %s' % e
1298
)
1299
elements = etree_iter(root, 'server')
1300
except AttributeError:
1301
try:
1302
root = DOM.parseString(serversxml)
1303
except ExpatError:
1304
e = get_exception()
1305
raise SpeedtestServersError(
1306
'Malformed speedtest.net server list: %s' % e
1307
)
1308
elements = root.getElementsByTagName('server')
1309
except (SyntaxError, xml.parsers.expat.ExpatError):
1310
raise ServersRetrievalError()
1311
1312
for server in elements:
1313
try:
1314
attrib = server.attrib
1315
except AttributeError:
1316
attrib = dict(list(server.attributes.items()))
1317
1318
if servers and int(attrib.get('id')) not in servers:
1319
continue
1320
1321
if (int(attrib.get('id')) in self.config['ignore_servers']
1322
or int(attrib.get('id')) in exclude):
1323
continue
1324
1325
try:
1326
d = distance(self.lat_lon,
1327
(float(attrib.get('lat')),
1328
float(attrib.get('lon'))))
1329
except Exception:
1330
continue
1331
1332
attrib['d'] = d
1333
1334
try:
1335
self.servers[d].append(attrib)
1336
except KeyError:
1337
self.servers[d] = [attrib]
1338
1339
break
1340
1341
except ServersRetrievalError:
1342
continue
1343
1344
if (servers or exclude) and not self.servers:
1345
raise NoMatchedServers()
1346
1347
return self.servers
1348
1349
def set_mini_server(self, server):
1350
"""Instead of querying for a list of servers, set a link to a
1351
speedtest mini server
1352
"""
1353
1354
urlparts = urlparse(server)
1355
1356
name, ext = os.path.splitext(urlparts[2])
1357
if ext:
1358
url = os.path.dirname(server)
1359
else:
1360
url = server
1361
1362
request = build_request(url)
1363
uh, e = catch_request(request, opener=self._opener)
1364
if e:
1365
raise SpeedtestMiniConnectFailure('Failed to connect to %s' %
1366
server)
1367
else:
1368
text = uh.read()
1369
uh.close()
1370
1371
extension = re.findall('upload_?[Ee]xtension: "([^"]+)"',
1372
text.decode())
1373
if not extension:
1374
for ext in ['php', 'asp', 'aspx', 'jsp']:
1375
try:
1376
f = self._opener.open(
1377
'%s/speedtest/upload.%s' % (url, ext)
1378
)
1379
except Exception:
1380
pass
1381
else:
1382
data = f.read().strip().decode()
1383
if (f.code == 200 and
1384
len(data.splitlines()) == 1 and
1385
re.match('size=[0-9]', data)):
1386
extension = [ext]
1387
break
1388
if not urlparts or not extension:
1389
raise InvalidSpeedtestMiniServer('Invalid Speedtest Mini Server: '
1390
'%s' % server)
1391
1392
self.servers = [{
1393
'sponsor': 'Speedtest Mini',
1394
'name': urlparts[1],
1395
'd': 0,
1396
'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]),
1397
'latency': 0,
1398
'id': 0
1399
}]
1400
1401
return self.servers
1402
1403
def get_closest_servers(self, limit=5):
1404
"""Limit servers to the closest speedtest.net servers based on
1405
geographic distance
1406
"""
1407
1408
if not self.servers:
1409
self.get_servers()
1410
1411
for d in sorted(self.servers.keys()):
1412
for s in self.servers[d]:
1413
self.closest.append(s)
1414
if len(self.closest) == limit:
1415
break
1416
else:
1417
continue
1418
break
1419
1420
printer('Closest Servers:\n%r' % self.closest, debug=True)
1421
return self.closest
1422
1423
def get_best_server(self, servers=None):
1424
"""Perform a speedtest.net "ping" to determine which speedtest.net
1425
server has the lowest latency
1426
"""
1427
1428
if not servers:
1429
if not self.closest:
1430
servers = self.get_closest_servers()
1431
servers = self.closest
1432
1433
if self._source_address:
1434
source_address_tuple = (self._source_address, 0)
1435
else:
1436
source_address_tuple = None
1437
1438
user_agent = build_user_agent()
1439
1440
results = {}
1441
for server in servers:
1442
cum = []
1443
url = os.path.dirname(server['url'])
1444
stamp = int(timeit.time.time() * 1000)
1445
latency_url = '%s/latency.txt?x=%s' % (url, stamp)
1446
for i in range(0, 3):
1447
this_latency_url = '%s.%s' % (latency_url, i)
1448
printer('%s %s' % ('GET', this_latency_url),
1449
debug=True)
1450
urlparts = urlparse(latency_url)
1451
try:
1452
if urlparts[0] == 'https':
1453
h = SpeedtestHTTPSConnection(
1454
urlparts[1],
1455
source_address=source_address_tuple
1456
)
1457
else:
1458
h = SpeedtestHTTPConnection(
1459
urlparts[1],
1460
source_address=source_address_tuple
1461
)
1462
headers = {'User-Agent': user_agent}
1463
path = '%s?%s' % (urlparts[2], urlparts[4])
1464
start = timeit.default_timer()
1465
h.request("GET", path, headers=headers)
1466
r = h.getresponse()
1467
total = (timeit.default_timer() - start)
1468
except HTTP_ERRORS:
1469
e = get_exception()
1470
printer('ERROR: %r' % e, debug=True)
1471
cum.append(3600)
1472
continue
1473
1474
text = r.read(9)
1475
if int(r.status) == 200 and text == 'test=test'.encode():
1476
cum.append(total)
1477
else:
1478
cum.append(3600)
1479
h.close()
1480
1481
avg = round((sum(cum) / 6) * 1000.0, 3)
1482
results[avg] = server
1483
1484
try:
1485
fastest = sorted(results.keys())[0]
1486
except IndexError:
1487
raise SpeedtestBestServerFailure('Unable to connect to servers to '
1488
'test latency.')
1489
best = results[fastest]
1490
best['latency'] = fastest
1491
1492
self.results.ping = fastest
1493
self.results.server = best
1494
1495
self._best.update(best)
1496
printer('Best Server:\n%r' % best, debug=True)
1497
return best
1498
1499
def download(self, callback=do_nothing, threads=None):
1500
"""Test download speed against speedtest.net
1501
1502
A ``threads`` value of ``None`` will fall back to those dictated
1503
by the speedtest.net configuration
1504
"""
1505
1506
urls = []
1507
for size in self.config['sizes']['download']:
1508
for _ in range(0, self.config['counts']['download']):
1509
urls.append('%s/random%sx%s.jpg' %
1510
(os.path.dirname(self.best['url']), size, size))
1511
1512
request_count = len(urls)
1513
requests = []
1514
for i, url in enumerate(urls):
1515
requests.append(
1516
build_request(url, bump=i, secure=self._secure)
1517
)
1518
1519
max_threads = threads or self.config['threads']['download']
1520
in_flight = {'threads': 0}
1521
1522
def producer(q, requests, request_count):
1523
for i, request in enumerate(requests):
1524
thread = HTTPDownloader(
1525
i,
1526
request,
1527
start,
1528
self.config['length']['download'],
1529
opener=self._opener,
1530
shutdown_event=self._shutdown_event
1531
)
1532
while in_flight['threads'] >= max_threads:
1533
timeit.time.sleep(0.001)
1534
thread.start()
1535
q.put(thread, True)
1536
in_flight['threads'] += 1
1537
callback(i, request_count, start=True)
1538
1539
finished = []
1540
1541
def consumer(q, request_count):
1542
_is_alive = thread_is_alive
1543
while len(finished) < request_count:
1544
thread = q.get(True)
1545
while _is_alive(thread):
1546
thread.join(timeout=0.001)
1547
in_flight['threads'] -= 1
1548
finished.append(sum(thread.result))
1549
callback(thread.i, request_count, end=True)
1550
1551
q = Queue(max_threads)
1552
prod_thread = threading.Thread(target=producer,
1553
args=(q, requests, request_count))
1554
cons_thread = threading.Thread(target=consumer,
1555
args=(q, request_count))
1556
start = timeit.default_timer()
1557
prod_thread.start()
1558
cons_thread.start()
1559
_is_alive = thread_is_alive
1560
while _is_alive(prod_thread):
1561
prod_thread.join(timeout=0.001)
1562
while _is_alive(cons_thread):
1563
cons_thread.join(timeout=0.001)
1564
1565
stop = timeit.default_timer()
1566
self.results.bytes_received = sum(finished)
1567
self.results.download = (
1568
(self.results.bytes_received / (stop - start)) * 8.0
1569
)
1570
if self.results.download > 100000:
1571
self.config['threads']['upload'] = 8
1572
return self.results.download
1573
1574
def upload(self, callback=do_nothing, pre_allocate=True, threads=None):
1575
"""Test upload speed against speedtest.net
1576
1577
A ``threads`` value of ``None`` will fall back to those dictated
1578
by the speedtest.net configuration
1579
"""
1580
1581
sizes = []
1582
1583
for size in self.config['sizes']['upload']:
1584
for _ in range(0, self.config['counts']['upload']):
1585
sizes.append(size)
1586
1587
# request_count = len(sizes)
1588
request_count = self.config['upload_max']
1589
1590
requests = []
1591
for i, size in enumerate(sizes):
1592
# We set ``0`` for ``start`` and handle setting the actual
1593
# ``start`` in ``HTTPUploader`` to get better measurements
1594
data = HTTPUploaderData(
1595
size,
1596
0,
1597
self.config['length']['upload'],
1598
shutdown_event=self._shutdown_event
1599
)
1600
if pre_allocate:
1601
data.pre_allocate()
1602
1603
headers = {'Content-length': size}
1604
requests.append(
1605
(
1606
build_request(self.best['url'], data, secure=self._secure,
1607
headers=headers),
1608
size
1609
)
1610
)
1611
1612
max_threads = threads or self.config['threads']['upload']
1613
in_flight = {'threads': 0}
1614
1615
def producer(q, requests, request_count):
1616
for i, request in enumerate(requests[:request_count]):
1617
thread = HTTPUploader(
1618
i,
1619
request[0],
1620
start,
1621
request[1],
1622
self.config['length']['upload'],
1623
opener=self._opener,
1624
shutdown_event=self._shutdown_event
1625
)
1626
while in_flight['threads'] >= max_threads:
1627
timeit.time.sleep(0.001)
1628
thread.start()
1629
q.put(thread, True)
1630
in_flight['threads'] += 1
1631
callback(i, request_count, start=True)
1632
1633
finished = []
1634
1635
def consumer(q, request_count):
1636
_is_alive = thread_is_alive
1637
while len(finished) < request_count:
1638
thread = q.get(True)
1639
while _is_alive(thread):
1640
thread.join(timeout=0.001)
1641
in_flight['threads'] -= 1
1642
finished.append(thread.result)
1643
callback(thread.i, request_count, end=True)
1644
1645
q = Queue(threads or self.config['threads']['upload'])
1646
prod_thread = threading.Thread(target=producer,
1647
args=(q, requests, request_count))
1648
cons_thread = threading.Thread(target=consumer,
1649
args=(q, request_count))
1650
start = timeit.default_timer()
1651
prod_thread.start()
1652
cons_thread.start()
1653
_is_alive = thread_is_alive
1654
while _is_alive(prod_thread):
1655
prod_thread.join(timeout=0.1)
1656
while _is_alive(cons_thread):
1657
cons_thread.join(timeout=0.1)
1658
1659
stop = timeit.default_timer()
1660
self.results.bytes_sent = sum(finished)
1661
self.results.upload = (
1662
(self.results.bytes_sent / (stop - start)) * 8.0
1663
)
1664
return self.results.upload
1665
1666
1667
def ctrl_c(shutdown_event):
1668
"""Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded
1669
operations
1670
"""
1671
def inner(signum, frame):
1672
shutdown_event.set()
1673
printer('\nCancelling...', error=True)
1674
sys.exit(0)
1675
return inner
1676
1677
1678
def version():
1679
"""Print the version"""
1680
1681
printer('speedtest-cli %s' % __version__)
1682
printer('Python %s' % sys.version.replace('\n', ''))
1683
sys.exit(0)
1684
1685
1686
def csv_header(delimiter=','):
1687
"""Print the CSV Headers"""
1688
1689
printer(SpeedtestResults.csv_header(delimiter=delimiter))
1690
sys.exit(0)
1691
1692
1693
def parse_args():
1694
"""Function to handle building and parsing of command line arguments"""
1695
description = (
1696
'Command line interface for testing internet bandwidth using '
1697
'speedtest.net.\n'
1698
'------------------------------------------------------------'
1699
'--------------\n'
1700
'https://github.com/sivel/speedtest-cli')
1701
1702
parser = ArgParser(description=description)
1703
# Give optparse.OptionParser an `add_argument` method for
1704
# compatibility with argparse.ArgumentParser
1705
try:
1706
parser.add_argument = parser.add_option
1707
except AttributeError:
1708
pass
1709
parser.add_argument('--no-download', dest='download', default=True,
1710
action='store_const', const=False,
1711
help='Do not perform download test')
1712
parser.add_argument('--no-upload', dest='upload', default=True,
1713
action='store_const', const=False,
1714
help='Do not perform upload test')
1715
parser.add_argument('--single', default=False, action='store_true',
1716
help='Only use a single connection instead of '
1717
'multiple. This simulates a typical file '
1718
'transfer.')
1719
parser.add_argument('--bytes', dest='units', action='store_const',
1720
const=('byte', 8), default=('bit', 1),
1721
help='Display values in bytes instead of bits. Does '
1722
'not affect the image generated by --share, nor '
1723
'output from --json or --csv')
1724
parser.add_argument('--share', action='store_true',
1725
help='Generate and provide a URL to the speedtest.net '
1726
'share results image, not displayed with --csv')
1727
parser.add_argument('--simple', action='store_true', default=False,
1728
help='Suppress verbose output, only show basic '
1729
'information')
1730
parser.add_argument('--csv', action='store_true', default=False,
1731
help='Suppress verbose output, only show basic '
1732
'information in CSV format. Speeds listed in '
1733
'bit/s and not affected by --bytes')
1734
parser.add_argument('--csv-delimiter', default=',', type=PARSER_TYPE_STR,
1735
help='Single character delimiter to use in CSV '
1736
'output. Default ","')
1737
parser.add_argument('--csv-header', action='store_true', default=False,
1738
help='Print CSV headers')
1739
parser.add_argument('--json', action='store_true', default=False,
1740
help='Suppress verbose output, only show basic '
1741
'information in JSON format. Speeds listed in '
1742
'bit/s and not affected by --bytes')
1743
parser.add_argument('--list', action='store_true',
1744
help='Display a list of speedtest.net servers '
1745
'sorted by distance')
1746
parser.add_argument('--server', type=PARSER_TYPE_INT, action='append',
1747
help='Specify a server ID to test against. Can be '
1748
'supplied multiple times')
1749
parser.add_argument('--exclude', type=PARSER_TYPE_INT, action='append',
1750
help='Exclude a server from selection. Can be '
1751
'supplied multiple times')
1752
parser.add_argument('--mini', help='URL of the Speedtest Mini server')
1753
parser.add_argument('--source', help='Source IP address to bind to')
1754
parser.add_argument('--timeout', default=10, type=PARSER_TYPE_FLOAT,
1755
help='HTTP timeout in seconds. Default 10')
1756
parser.add_argument('--secure', action='store_true',
1757
help='Use HTTPS instead of HTTP when communicating '
1758
'with speedtest.net operated servers')
1759
parser.add_argument('--no-pre-allocate', dest='pre_allocate',
1760
action='store_const', default=True, const=False,
1761
help='Do not pre allocate upload data. Pre allocation '
1762
'is enabled by default to improve upload '
1763
'performance. To support systems with '
1764
'insufficient memory, use this option to avoid a '
1765
'MemoryError')
1766
parser.add_argument('--version', action='store_true',
1767
help='Show the version number and exit')
1768
parser.add_argument('--debug', action='store_true',
1769
help=ARG_SUPPRESS, default=ARG_SUPPRESS)
1770
1771
options = parser.parse_args()
1772
if isinstance(options, tuple):
1773
args = options[0]
1774
else:
1775
args = options
1776
return args
1777
1778
1779
def validate_optional_args(args):
1780
"""Check if an argument was provided that depends on a module that may
1781
not be part of the Python standard library.
1782
1783
If such an argument is supplied, and the module does not exist, exit
1784
with an error stating which module is missing.
1785
"""
1786
optional_args = {
1787
'json': ('json/simplejson python module', json),
1788
'secure': ('SSL support', HTTPSConnection),
1789
}
1790
1791
for arg, info in optional_args.items():
1792
if getattr(args, arg, False) and info[1] is None:
1793
raise SystemExit('%s is not installed. --%s is '
1794
'unavailable' % (info[0], arg))
1795
1796
1797
def printer(string, quiet=False, debug=False, error=False, **kwargs):
1798
"""Helper function print a string with various features"""
1799
1800
if debug and not DEBUG:
1801
return
1802
1803
if debug:
1804
if sys.stdout.isatty():
1805
out = '\033[1;30mDEBUG: %s\033[0m' % string
1806
else:
1807
out = 'DEBUG: %s' % string
1808
else:
1809
out = string
1810
1811
if error:
1812
kwargs['file'] = sys.stderr
1813
1814
if not quiet:
1815
print_(out, **kwargs)
1816
1817
1818
def shell():
1819
"""Run the full speedtest.net test"""
1820
1821
global DEBUG
1822
shutdown_event = threading.Event()
1823
1824
signal.signal(signal.SIGINT, ctrl_c(shutdown_event))
1825
1826
args = parse_args()
1827
1828
# Print the version and exit
1829
if args.version:
1830
version()
1831
1832
if not args.download and not args.upload:
1833
raise SpeedtestCLIError('Cannot supply both --no-download and '
1834
'--no-upload')
1835
1836
if len(args.csv_delimiter) != 1:
1837
raise SpeedtestCLIError('--csv-delimiter must be a single character')
1838
1839
if args.csv_header:
1840
csv_header(args.csv_delimiter)
1841
1842
validate_optional_args(args)
1843
1844
debug = getattr(args, 'debug', False)
1845
if debug == 'SUPPRESSHELP':
1846
debug = False
1847
if debug:
1848
DEBUG = True
1849
1850
if args.simple or args.csv or args.json:
1851
quiet = True
1852
else:
1853
quiet = False
1854
1855
if args.csv or args.json:
1856
machine_format = True
1857
else:
1858
machine_format = False
1859
1860
# Don't set a callback if we are running quietly
1861
if quiet or debug:
1862
callback = do_nothing
1863
else:
1864
callback = print_dots(shutdown_event)
1865
1866
printer('*• SPEEDTEST.NET*\n\n', quiet)
1867
try:
1868
speedtest = Speedtest(
1869
source_address=args.source,
1870
timeout=args.timeout,
1871
secure=args.secure
1872
)
1873
except (ConfigRetrievalError,) + HTTP_ERRORS:
1874
printer('Cannot retrieve speedtest configuration', error=True)
1875
raise SpeedtestCLIError(get_exception())
1876
1877
if args.list:
1878
try:
1879
speedtest.get_servers()
1880
except (ServersRetrievalError,) + HTTP_ERRORS:
1881
printer('Cannot retrieve speedtest server list', error=True)
1882
raise SpeedtestCLIError(get_exception())
1883
1884
for _, servers in sorted(speedtest.servers.items()):
1885
for server in servers:
1886
line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) '
1887
'[%(d)0.2f km]' % server)
1888
try:
1889
printer(line)
1890
except IOError:
1891
e = get_exception()
1892
if e.errno != errno.EPIPE:
1893
raise
1894
sys.exit(0)
1895
1896
printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client'],
1897
quiet)
1898
1899
if not args.mini:
1900
printer('Retrieving speedtest.net server list...', quiet)
1901
try:
1902
speedtest.get_servers(servers=args.server, exclude=args.exclude)
1903
except NoMatchedServers:
1904
raise SpeedtestCLIError(
1905
'No matched servers: %s' %
1906
', '.join('%s' % s for s in args.server)
1907
)
1908
except (ServersRetrievalError,) + HTTP_ERRORS:
1909
printer('Cannot retrieve speedtest server list', error=True)
1910
raise SpeedtestCLIError(get_exception())
1911
except InvalidServerIDType:
1912
raise SpeedtestCLIError(
1913
'%s is an invalid server type, must '
1914
'be an int' % ', '.join('%s' % s for s in args.server)
1915
)
1916
1917
if args.server and len(args.server) == 1:
1918
printer('Retrieving information for the selected server...', quiet)
1919
else:
1920
printer('Selecting best server based on ping...', quiet)
1921
speedtest.get_best_server()
1922
elif args.mini:
1923
speedtest.get_best_server(speedtest.set_mini_server(args.mini))
1924
1925
results = speedtest.results
1926
1927
printer('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: '
1928
'%(latency)s ms' % results.server, quiet)
1929
1930
if args.download:
1931
printer('Testing download speed\n', quiet,
1932
end=('', '\n')[bool(debug)])
1933
speedtest.download(
1934
callback=callback,
1935
threads=(None, 1)[args.single]
1936
)
1937
printer('Download: %0.2f M%s/s' %
1938
((results.download / 1000.0 / 1000.0) / args.units[1],
1939
args.units[0]),
1940
quiet)
1941
else:
1942
printer('Skipping download test', quiet)
1943
1944
if args.upload:
1945
printer('Testing upload speed\n', quiet,
1946
end=('', '\n')[bool(debug)])
1947
speedtest.upload(
1948
callback=callback,
1949
pre_allocate=args.pre_allocate,
1950
threads=(None, 1)[args.single]
1951
)
1952
printer('Upload: %0.2f M%s/s' %
1953
((results.upload / 1000.0 / 1000.0) / args.units[1],
1954
args.units[0]),
1955
quiet)
1956
else:
1957
printer('Skipping upload test', quiet)
1958
1959
printer('Results:\n%r' % results.dict(), debug=True)
1960
1961
if not args.simple and args.share:
1962
results.share()
1963
1964
if args.simple:
1965
printer('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' %
1966
(results.ping,
1967
(results.download / 1000.0 / 1000.0) / args.units[1],
1968
args.units[0],
1969
(results.upload / 1000.0 / 1000.0) / args.units[1],
1970
args.units[0]))
1971
elif args.csv:
1972
printer(results.csv(delimiter=args.csv_delimiter))
1973
elif args.json:
1974
printer(results.json())
1975
1976
if args.share and not machine_format:
1977
printer('Share results: %s' % results.share())
1978
1979
1980
def main():
1981
try:
1982
shell()
1983
except KeyboardInterrupt:
1984
printer('\nCancelling...', error=True)
1985
except (SpeedtestException, SystemExit):
1986
e = get_exception()
1987
# Ignore a successful exit, or argparse exit
1988
if getattr(e, 'code', 1) not in (0, 2):
1989
msg = '%s' % e
1990
if not msg:
1991
msg = '%r' % e
1992
raise SystemExit('ERROR: %s' % msg)
1993
1994
1995
if __name__ == '__main__':
1996
main()
1997
1998