Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2017 The LUCI Authors. All rights reserved. | |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 | |
| 4 # that can be found in the LICENSE file. | |
| 5 | |
| 6 # NOTE: This was imported from Chromium's "tools/build" at revision: | |
| 7 # 65976b6e2a612439681dc42830e90dbcdf550f40 | |
| 8 | |
| 9 import argparse | |
| 10 import json | |
| 11 import logging | |
| 12 import os | |
| 13 import sys | |
| 14 import time | |
| 15 | |
| 16 import requests | |
| 17 import requests.adapters | |
| 18 import requests.models | |
| 19 from requests.packages.urllib3.util.retry import Retry | |
| 20 | |
| 21 | |
| 22 class Output(object): | |
| 23 """Output is a file-like object which writes content to a sink. | |
| 24 | |
| 25 If a prefix is supplied, it will validate and discard that prefix before | |
| 26 writing output. If the prefix did not validate, a ValueError will be raised. | |
| 27 """ | |
| 28 | |
| 29 def __init__(self, sink, prefix=None): | |
| 30 self.total = 0 | |
| 31 | |
| 32 self._sink = sink | |
| 33 self._prefix = prefix | |
| 34 self._prefix_idx = 0 | |
| 35 self._prefix_buf = [] | |
| 36 | |
| 37 def write(self, content): | |
|
iannucci
2017/05/12 00:53:52
assert len(prefix) < CHUNK_SIZE, use iter() interf
dnj
2017/05/12 02:15:04
Done.
| |
| 38 self.total += len(content) | |
| 39 | |
| 40 # If we still have prefix remaining, read and validate it. | |
| 41 if self._prefix and self._prefix_idx < len(self._prefix): | |
| 42 prefix_part = self._prefix[self._prefix_idx:][:len(content)] | |
| 43 d = content[:len(prefix_part)] | |
| 44 self._prefix_buf.append(d) | |
| 45 self._prefix_idx += len(prefix_part) | |
| 46 | |
| 47 if d != prefix_part: | |
| 48 total_prefix = ''.join(self._prefix_buf) | |
| 49 raise ValueError( | |
| 50 'Expected prefix was not observed: [%s] != [%s]...' % ( | |
| 51 total_prefix, self._prefix[:len(total_prefix)])) | |
| 52 content = content[len(prefix_part):] | |
| 53 | |
| 54 if content: | |
| 55 self._sink.write(content) | |
| 56 | |
| 57 | |
| 58 def _download(url, outfile, headers, transient_retry, strip_prefix): | |
| 59 s = requests.Session() | |
| 60 if transient_retry: | |
| 61 # See http://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html | |
| 62 retry = Retry( | |
| 63 total=10, | |
| 64 connect=5, | |
| 65 read=5, | |
| 66 redirect=5, | |
| 67 status_forcelist=range(500, 600), | |
| 68 backoff_factor=0.2, | |
| 69 ) | |
| 70 print retry | |
| 71 s.mount(url, requests.adapters.HTTPAdapter(max_retries=retry)) | |
| 72 | |
| 73 | |
| 74 logging.info('Connecting...') | |
| 75 r = s.get(url, headers=headers, stream=True) | |
| 76 if r.status_code != requests.codes.ok: | |
| 77 r.raise_for_status() | |
| 78 | |
| 79 if outfile: | |
| 80 fd = open(outfile, 'wb') | |
| 81 else: | |
| 82 fd = sys.stdout | |
| 83 with fd: | |
| 84 out = Output(fd, prefix=strip_prefix) | |
| 85 logging.info('Downloading...') | |
| 86 for chunk in r.iter_content(1024*1024): | |
| 87 out.write(chunk) | |
| 88 logging.info('Downloaded %.1f MB so far', out.total / 1024 / 1024) | |
| 89 return r.status_code, out.total | |
| 90 | |
| 91 | |
| 92 def main(): | |
| 93 parser = argparse.ArgumentParser( | |
| 94 description='Get a url and print its document.', | |
| 95 prog='./runit.py pycurl.py') | |
| 96 parser.add_argument('url', help='the url to fetch') | |
|
iannucci
2017/05/12 00:53:52
urlparse validate (up in the recipe)?
dnj
2017/05/12 02:15:04
Done.
| |
| 97 parser.add_argument('--status-json', metavar='PATH', action='store', | |
|
iannucci
2017/05/12 00:53:52
required=True ?
dnj
2017/05/12 02:15:04
Done.
| |
| 98 help='Write HTTP status result JSON. If set, all complete HTTP ' | |
| 99 'responses will exit with 0, regardless of their status code.') | |
| 100 parser.add_argument('--no-transient-retry', action='store_true', | |
| 101 help='Do not perform automatic retries on transient failures.') | |
| 102 parser.add_argument('--headers-json', action='store', | |
| 103 help='A json file containing any headers to include with the request.') | |
| 104 parser.add_argument('--outfile', help='write output to this file') | |
| 105 parser.add_argument('--strip-prefix', action='store', | |
| 106 help='Expect this string at the beginning of the response, and strip it.') | |
| 107 | |
| 108 args = parser.parse_args() | |
| 109 | |
| 110 headers = None | |
| 111 if args.headers_json: | |
| 112 with open(args.headers_json, 'r') as json_file: | |
| 113 headers = json.load(json_file) | |
| 114 | |
| 115 status = {} | |
| 116 try: | |
| 117 status_code, size = _download( | |
| 118 args.url, args.outfile, headers, not args.no_transient_retry, | |
| 119 args.strip_prefix) | |
| 120 status = { | |
| 121 'status_code': status_code, | |
| 122 'success': True, | |
| 123 'size': size, | |
| 124 } | |
| 125 except requests.HTTPError as e: | |
| 126 if not args.status_json: | |
| 127 raise | |
| 128 status = { | |
| 129 'status_code': e.response.status_code, | |
| 130 'success': False, | |
| 131 } | |
| 132 | |
| 133 if args.status_json: | |
| 134 with open(args.status_json, 'w') as fd: | |
| 135 json.dump(status, fd) | |
| 136 return 0 | |
| 137 | |
| 138 | |
| 139 if __name__ == '__main__': | |
| 140 logging.basicConfig() | |
| 141 logging.getLogger().setLevel(logging.INFO) | |
| 142 logging.getLogger("requests").setLevel(logging.DEBUG) | |
| 143 sys.exit(main()) | |
| 144 | |
| 145 | |
| 146 ## | |
| 147 # The following section is read by "vpython" and used to construct the | |
| 148 # VirtualEnv for this tool. | |
| 149 # | |
| 150 # These imports were lifted from "/bootstrap/venv.cfg". | |
| 151 ## | |
| 152 # [VPYTHON:BEGIN] | |
| 153 # | |
| 154 # wheel: < | |
| 155 # name: "infra/python/wheels/cryptography/${platform}_${py_version}_${py_abi}" | |
| 156 # version: "version:1.8.1" | |
| 157 # > | |
| 158 # | |
| 159 # wheel: < | |
| 160 # name: "infra/python/wheels/appdirs-py2_py3" | |
| 161 # version: "version:1.4.3" | |
| 162 # > | |
| 163 # | |
| 164 # wheel: < | |
| 165 # name: "infra/python/wheels/asn1crypto-py2_py3" | |
| 166 # version: "version:0.22.0" | |
| 167 # > | |
| 168 # | |
| 169 # wheel: < | |
| 170 # name: "infra/python/wheels/enum34-py2" | |
| 171 # version: "version:1.1.6" | |
| 172 # > | |
| 173 # | |
| 174 # wheel: < | |
| 175 # name: "infra/python/wheels/cffi/${platform}_${py_version}_${py_abi}" | |
| 176 # version: "version:1.10.0" | |
| 177 # > | |
| 178 # | |
| 179 # wheel: < | |
| 180 # name: "infra/python/wheels/idna-py2_py3" | |
| 181 # version: "version:2.5" | |
| 182 # > | |
| 183 # | |
| 184 # wheel: < | |
| 185 # name: "infra/python/wheels/ipaddress-py2" | |
| 186 # version: "version:1.0.18" | |
| 187 # > | |
| 188 # | |
| 189 # wheel: < | |
| 190 # name: "infra/python/wheels/packaging-py2_py3" | |
| 191 # version: "version:16.8" | |
| 192 # > | |
| 193 # | |
| 194 # wheel: < | |
| 195 # name: "infra/python/wheels/pyasn1-py2_py3" | |
| 196 # version: "version:0.2.3" | |
| 197 # > | |
| 198 # | |
| 199 # wheel: < | |
| 200 # name: "infra/python/wheels/pycparser-py2_py3" | |
| 201 # version: "version:2.17" | |
| 202 # > | |
| 203 # | |
| 204 # wheel: < | |
| 205 # name: "infra/python/wheels/pyopenssl-py2_py3" | |
| 206 # version: "version:17.0.0" | |
| 207 # > | |
| 208 # | |
| 209 # wheel: < | |
| 210 # name: "infra/python/wheels/pyparsing-py2_py3" | |
| 211 # version: "version:2.2.0" | |
| 212 # > | |
| 213 # | |
| 214 # wheel: < | |
| 215 # name: "infra/python/wheels/setuptools-py2_py3" | |
| 216 # version: "version:34.3.2" | |
| 217 # > | |
| 218 # | |
| 219 # wheel: < | |
| 220 # name: "infra/python/wheels/six-py2_py3" | |
| 221 # version: "version:1.10.0" | |
| 222 # > | |
| 223 # | |
| 224 # wheel: < | |
| 225 # name: "infra/python/wheels/requests-py2_py3" | |
| 226 # version: "version:2.13.0" | |
| 227 # > | |
| 228 # | |
| 229 # [VPYTHON:END] | |
| 230 ## | |
| OLD | NEW |