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 |