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