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