comparison venv/lib/python2.7/site-packages/pip/download.py @ 0:d67268158946 draft

planemo upload commit a3f181f5f126803c654b3a66dd4e83a48f7e203b
author bcclaywell
date Mon, 12 Oct 2015 17:43:33 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:d67268158946
1 from __future__ import absolute_import
2
3 import cgi
4 import email.utils
5 import hashlib
6 import getpass
7 import json
8 import logging
9 import mimetypes
10 import os
11 import platform
12 import re
13 import shutil
14 import sys
15 import tempfile
16
17 from pip._vendor.six.moves.urllib import parse as urllib_parse
18 from pip._vendor.six.moves.urllib import request as urllib_request
19
20 import pip
21
22 from pip.exceptions import InstallationError, HashMismatch
23 from pip.models import PyPI
24 from pip.utils import (splitext, rmtree, format_size, display_path,
25 backup_dir, ask_path_exists, unpack_file,
26 call_subprocess)
27 from pip.utils.filesystem import check_path_owner
28 from pip.utils.logging import indent_log
29 from pip.utils.ui import DownloadProgressBar, DownloadProgressSpinner
30 from pip.locations import write_delete_marker_file
31 from pip.vcs import vcs
32 from pip._vendor import requests, six
33 from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter
34 from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
35 from pip._vendor.requests.models import Response
36 from pip._vendor.requests.structures import CaseInsensitiveDict
37 from pip._vendor.requests.packages import urllib3
38 from pip._vendor.cachecontrol import CacheControlAdapter
39 from pip._vendor.cachecontrol.caches import FileCache
40 from pip._vendor.lockfile import LockError
41 from pip._vendor.six.moves import xmlrpc_client
42
43
44 __all__ = ['get_file_content',
45 'is_url', 'url_to_path', 'path_to_url',
46 'is_archive_file', 'unpack_vcs_link',
47 'unpack_file_url', 'is_vcs_url', 'is_file_url',
48 'unpack_http_url', 'unpack_url']
49
50
51 logger = logging.getLogger(__name__)
52
53
54 def user_agent():
55 """
56 Return a string representing the user agent.
57 """
58 data = {
59 "installer": {"name": "pip", "version": pip.__version__},
60 "python": platform.python_version(),
61 "implementation": {
62 "name": platform.python_implementation(),
63 },
64 }
65
66 if data["implementation"]["name"] == 'CPython':
67 data["implementation"]["version"] = platform.python_version()
68 elif data["implementation"]["name"] == 'PyPy':
69 if sys.pypy_version_info.releaselevel == 'final':
70 pypy_version_info = sys.pypy_version_info[:3]
71 else:
72 pypy_version_info = sys.pypy_version_info
73 data["implementation"]["version"] = ".".join(
74 [str(x) for x in pypy_version_info]
75 )
76 elif data["implementation"]["name"] == 'Jython':
77 # Complete Guess
78 data["implementation"]["version"] = platform.python_version()
79 elif data["implementation"]["name"] == 'IronPython':
80 # Complete Guess
81 data["implementation"]["version"] = platform.python_version()
82
83 if sys.platform.startswith("linux"):
84 distro = dict(filter(
85 lambda x: x[1],
86 zip(["name", "version", "id"], platform.linux_distribution()),
87 ))
88 libc = dict(filter(
89 lambda x: x[1],
90 zip(["lib", "version"], platform.libc_ver()),
91 ))
92 if libc:
93 distro["libc"] = libc
94 if distro:
95 data["distro"] = distro
96
97 if sys.platform.startswith("darwin") and platform.mac_ver()[0]:
98 data["distro"] = {"name": "OS X", "version": platform.mac_ver()[0]}
99
100 if platform.system():
101 data.setdefault("system", {})["name"] = platform.system()
102
103 if platform.release():
104 data.setdefault("system", {})["release"] = platform.release()
105
106 if platform.machine():
107 data["cpu"] = platform.machine()
108
109 return "{data[installer][name]}/{data[installer][version]} {json}".format(
110 data=data,
111 json=json.dumps(data, separators=(",", ":"), sort_keys=True),
112 )
113
114
115 class MultiDomainBasicAuth(AuthBase):
116
117 def __init__(self, prompting=True):
118 self.prompting = prompting
119 self.passwords = {}
120
121 def __call__(self, req):
122 parsed = urllib_parse.urlparse(req.url)
123
124 # Get the netloc without any embedded credentials
125 netloc = parsed.netloc.rsplit("@", 1)[-1]
126
127 # Set the url of the request to the url without any credentials
128 req.url = urllib_parse.urlunparse(parsed[:1] + (netloc,) + parsed[2:])
129
130 # Use any stored credentials that we have for this netloc
131 username, password = self.passwords.get(netloc, (None, None))
132
133 # Extract credentials embedded in the url if we have none stored
134 if username is None:
135 username, password = self.parse_credentials(parsed.netloc)
136
137 if username or password:
138 # Store the username and password
139 self.passwords[netloc] = (username, password)
140
141 # Send the basic auth with this request
142 req = HTTPBasicAuth(username or "", password or "")(req)
143
144 # Attach a hook to handle 401 responses
145 req.register_hook("response", self.handle_401)
146
147 return req
148
149 def handle_401(self, resp, **kwargs):
150 # We only care about 401 responses, anything else we want to just
151 # pass through the actual response
152 if resp.status_code != 401:
153 return resp
154
155 # We are not able to prompt the user so simple return the response
156 if not self.prompting:
157 return resp
158
159 parsed = urllib_parse.urlparse(resp.url)
160
161 # Prompt the user for a new username and password
162 username = six.moves.input("User for %s: " % parsed.netloc)
163 password = getpass.getpass("Password: ")
164
165 # Store the new username and password to use for future requests
166 if username or password:
167 self.passwords[parsed.netloc] = (username, password)
168
169 # Consume content and release the original connection to allow our new
170 # request to reuse the same one.
171 resp.content
172 resp.raw.release_conn()
173
174 # Add our new username and password to the request
175 req = HTTPBasicAuth(username or "", password or "")(resp.request)
176
177 # Send our new request
178 new_resp = resp.connection.send(req, **kwargs)
179 new_resp.history.append(resp)
180
181 return new_resp
182
183 def parse_credentials(self, netloc):
184 if "@" in netloc:
185 userinfo = netloc.rsplit("@", 1)[0]
186 if ":" in userinfo:
187 return userinfo.split(":", 1)
188 return userinfo, None
189 return None, None
190
191
192 class LocalFSAdapter(BaseAdapter):
193
194 def send(self, request, stream=None, timeout=None, verify=None, cert=None,
195 proxies=None):
196 pathname = url_to_path(request.url)
197
198 resp = Response()
199 resp.status_code = 200
200 resp.url = request.url
201
202 try:
203 stats = os.stat(pathname)
204 except OSError as exc:
205 resp.status_code = 404
206 resp.raw = exc
207 else:
208 modified = email.utils.formatdate(stats.st_mtime, usegmt=True)
209 content_type = mimetypes.guess_type(pathname)[0] or "text/plain"
210 resp.headers = CaseInsensitiveDict({
211 "Content-Type": content_type,
212 "Content-Length": stats.st_size,
213 "Last-Modified": modified,
214 })
215
216 resp.raw = open(pathname, "rb")
217 resp.close = resp.raw.close
218
219 return resp
220
221 def close(self):
222 pass
223
224
225 class SafeFileCache(FileCache):
226 """
227 A file based cache which is safe to use even when the target directory may
228 not be accessible or writable.
229 """
230
231 def __init__(self, *args, **kwargs):
232 super(SafeFileCache, self).__init__(*args, **kwargs)
233
234 # Check to ensure that the directory containing our cache directory
235 # is owned by the user current executing pip. If it does not exist
236 # we will check the parent directory until we find one that does exist.
237 # If it is not owned by the user executing pip then we will disable
238 # the cache and log a warning.
239 if not check_path_owner(self.directory):
240 logger.warning(
241 "The directory '%s' or its parent directory is not owned by "
242 "the current user and the cache has been disabled. Please "
243 "check the permissions and owner of that directory. If "
244 "executing pip with sudo, you may want sudo's -H flag.",
245 self.directory,
246 )
247
248 # Set our directory to None to disable the Cache
249 self.directory = None
250
251 def get(self, *args, **kwargs):
252 # If we don't have a directory, then the cache should be a no-op.
253 if self.directory is None:
254 return
255
256 try:
257 return super(SafeFileCache, self).get(*args, **kwargs)
258 except (LockError, OSError, IOError):
259 # We intentionally silence this error, if we can't access the cache
260 # then we can just skip caching and process the request as if
261 # caching wasn't enabled.
262 pass
263
264 def set(self, *args, **kwargs):
265 # If we don't have a directory, then the cache should be a no-op.
266 if self.directory is None:
267 return
268
269 try:
270 return super(SafeFileCache, self).set(*args, **kwargs)
271 except (LockError, OSError, IOError):
272 # We intentionally silence this error, if we can't access the cache
273 # then we can just skip caching and process the request as if
274 # caching wasn't enabled.
275 pass
276
277 def delete(self, *args, **kwargs):
278 # If we don't have a directory, then the cache should be a no-op.
279 if self.directory is None:
280 return
281
282 try:
283 return super(SafeFileCache, self).delete(*args, **kwargs)
284 except (LockError, OSError, IOError):
285 # We intentionally silence this error, if we can't access the cache
286 # then we can just skip caching and process the request as if
287 # caching wasn't enabled.
288 pass
289
290
291 class InsecureHTTPAdapter(HTTPAdapter):
292
293 def cert_verify(self, conn, url, verify, cert):
294 conn.cert_reqs = 'CERT_NONE'
295 conn.ca_certs = None
296
297
298 class PipSession(requests.Session):
299
300 timeout = None
301
302 def __init__(self, *args, **kwargs):
303 retries = kwargs.pop("retries", 0)
304 cache = kwargs.pop("cache", None)
305 insecure_hosts = kwargs.pop("insecure_hosts", [])
306
307 super(PipSession, self).__init__(*args, **kwargs)
308
309 # Attach our User Agent to the request
310 self.headers["User-Agent"] = user_agent()
311
312 # Attach our Authentication handler to the session
313 self.auth = MultiDomainBasicAuth()
314
315 # Create our urllib3.Retry instance which will allow us to customize
316 # how we handle retries.
317 retries = urllib3.Retry(
318 # Set the total number of retries that a particular request can
319 # have.
320 total=retries,
321
322 # A 503 error from PyPI typically means that the Fastly -> Origin
323 # connection got interupted in some way. A 503 error in general
324 # is typically considered a transient error so we'll go ahead and
325 # retry it.
326 status_forcelist=[503],
327
328 # Add a small amount of back off between failed requests in
329 # order to prevent hammering the service.
330 backoff_factor=0.25,
331 )
332
333 # We want to _only_ cache responses on securely fetched origins. We do
334 # this because we can't validate the response of an insecurely fetched
335 # origin, and we don't want someone to be able to poison the cache and
336 # require manual evication from the cache to fix it.
337 if cache:
338 secure_adapter = CacheControlAdapter(
339 cache=SafeFileCache(cache),
340 max_retries=retries,
341 )
342 else:
343 secure_adapter = HTTPAdapter(max_retries=retries)
344
345 # Our Insecure HTTPAdapter disables HTTPS validation. It does not
346 # support caching (see above) so we'll use it for all http:// URLs as
347 # well as any https:// host that we've marked as ignoring TLS errors
348 # for.
349 insecure_adapter = InsecureHTTPAdapter(max_retries=retries)
350
351 self.mount("https://", secure_adapter)
352 self.mount("http://", insecure_adapter)
353
354 # Enable file:// urls
355 self.mount("file://", LocalFSAdapter())
356
357 # We want to use a non-validating adapter for any requests which are
358 # deemed insecure.
359 for host in insecure_hosts:
360 self.mount("https://{0}/".format(host), insecure_adapter)
361
362 def request(self, method, url, *args, **kwargs):
363 # Allow setting a default timeout on a session
364 kwargs.setdefault("timeout", self.timeout)
365
366 # Dispatch the actual request
367 return super(PipSession, self).request(method, url, *args, **kwargs)
368
369
370 def get_file_content(url, comes_from=None, session=None):
371 """Gets the content of a file; it may be a filename, file: URL, or
372 http: URL. Returns (location, content). Content is unicode."""
373 if session is None:
374 raise TypeError(
375 "get_file_content() missing 1 required keyword argument: 'session'"
376 )
377
378 match = _scheme_re.search(url)
379 if match:
380 scheme = match.group(1).lower()
381 if (scheme == 'file' and comes_from and
382 comes_from.startswith('http')):
383 raise InstallationError(
384 'Requirements file %s references URL %s, which is local'
385 % (comes_from, url))
386 if scheme == 'file':
387 path = url.split(':', 1)[1]
388 path = path.replace('\\', '/')
389 match = _url_slash_drive_re.match(path)
390 if match:
391 path = match.group(1) + ':' + path.split('|', 1)[1]
392 path = urllib_parse.unquote(path)
393 if path.startswith('/'):
394 path = '/' + path.lstrip('/')
395 url = path
396 else:
397 # FIXME: catch some errors
398 resp = session.get(url)
399 resp.raise_for_status()
400
401 if six.PY3:
402 return resp.url, resp.text
403 else:
404 return resp.url, resp.content
405 try:
406 with open(url) as f:
407 content = f.read()
408 except IOError as exc:
409 raise InstallationError(
410 'Could not open requirements file: %s' % str(exc)
411 )
412 return url, content
413
414
415 _scheme_re = re.compile(r'^(http|https|file):', re.I)
416 _url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I)
417
418
419 def is_url(name):
420 """Returns true if the name looks like a URL"""
421 if ':' not in name:
422 return False
423 scheme = name.split(':', 1)[0].lower()
424 return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes
425
426
427 def url_to_path(url):
428 """
429 Convert a file: URL to a path.
430 """
431 assert url.startswith('file:'), (
432 "You can only turn file: urls into filenames (not %r)" % url)
433
434 _, netloc, path, _, _ = urllib_parse.urlsplit(url)
435
436 # if we have a UNC path, prepend UNC share notation
437 if netloc:
438 netloc = '\\\\' + netloc
439
440 path = urllib_request.url2pathname(netloc + path)
441 return path
442
443
444 def path_to_url(path):
445 """
446 Convert a path to a file: URL. The path will be made absolute and have
447 quoted path parts.
448 """
449 path = os.path.normpath(os.path.abspath(path))
450 url = urllib_parse.urljoin('file:', urllib_request.pathname2url(path))
451 return url
452
453
454 def is_archive_file(name):
455 """Return True if `name` is a considered as an archive file."""
456 archives = (
457 '.zip', '.tar.gz', '.tar.bz2', '.tgz', '.tar', '.whl'
458 )
459 ext = splitext(name)[1].lower()
460 if ext in archives:
461 return True
462 return False
463
464
465 def unpack_vcs_link(link, location, only_download=False):
466 vcs_backend = _get_used_vcs_backend(link)
467 if only_download:
468 vcs_backend.export(location)
469 else:
470 vcs_backend.unpack(location)
471
472
473 def _get_used_vcs_backend(link):
474 for backend in vcs.backends:
475 if link.scheme in backend.schemes:
476 vcs_backend = backend(link.url)
477 return vcs_backend
478
479
480 def is_vcs_url(link):
481 return bool(_get_used_vcs_backend(link))
482
483
484 def is_file_url(link):
485 return link.url.lower().startswith('file:')
486
487
488 def _check_hash(download_hash, link):
489 if download_hash.digest_size != hashlib.new(link.hash_name).digest_size:
490 logger.critical(
491 "Hash digest size of the package %d (%s) doesn't match the "
492 "expected hash name %s!",
493 download_hash.digest_size, link, link.hash_name,
494 )
495 raise HashMismatch('Hash name mismatch for package %s' % link)
496 if download_hash.hexdigest() != link.hash:
497 logger.critical(
498 "Hash of the package %s (%s) doesn't match the expected hash %s!",
499 link, download_hash.hexdigest(), link.hash,
500 )
501 raise HashMismatch(
502 'Bad %s hash for package %s' % (link.hash_name, link)
503 )
504
505
506 def _get_hash_from_file(target_file, link):
507 try:
508 download_hash = hashlib.new(link.hash_name)
509 except (ValueError, TypeError):
510 logger.warning(
511 "Unsupported hash name %s for package %s", link.hash_name, link,
512 )
513 return None
514
515 with open(target_file, 'rb') as fp:
516 while True:
517 chunk = fp.read(4096)
518 if not chunk:
519 break
520 download_hash.update(chunk)
521 return download_hash
522
523
524 def _progress_indicator(iterable, *args, **kwargs):
525 return iterable
526
527
528 def _download_url(resp, link, content_file):
529 download_hash = None
530 if link.hash and link.hash_name:
531 try:
532 download_hash = hashlib.new(link.hash_name)
533 except ValueError:
534 logger.warning(
535 "Unsupported hash name %s for package %s",
536 link.hash_name, link,
537 )
538
539 try:
540 total_length = int(resp.headers['content-length'])
541 except (ValueError, KeyError, TypeError):
542 total_length = 0
543
544 cached_resp = getattr(resp, "from_cache", False)
545
546 if logger.getEffectiveLevel() > logging.INFO:
547 show_progress = False
548 elif cached_resp:
549 show_progress = False
550 elif total_length > (40 * 1000):
551 show_progress = True
552 elif not total_length:
553 show_progress = True
554 else:
555 show_progress = False
556
557 show_url = link.show_url
558
559 def resp_read(chunk_size):
560 try:
561 # Special case for urllib3.
562 for chunk in resp.raw.stream(
563 chunk_size,
564 # We use decode_content=False here because we do
565 # want urllib3 to mess with the raw bytes we get
566 # from the server. If we decompress inside of
567 # urllib3 then we cannot verify the checksum
568 # because the checksum will be of the compressed
569 # file. This breakage will only occur if the
570 # server adds a Content-Encoding header, which
571 # depends on how the server was configured:
572 # - Some servers will notice that the file isn't a
573 # compressible file and will leave the file alone
574 # and with an empty Content-Encoding
575 # - Some servers will notice that the file is
576 # already compressed and will leave the file
577 # alone and will add a Content-Encoding: gzip
578 # header
579 # - Some servers won't notice anything at all and
580 # will take a file that's already been compressed
581 # and compress it again and set the
582 # Content-Encoding: gzip header
583 #
584 # By setting this not to decode automatically we
585 # hope to eliminate problems with the second case.
586 decode_content=False):
587 yield chunk
588 except AttributeError:
589 # Standard file-like object.
590 while True:
591 chunk = resp.raw.read(chunk_size)
592 if not chunk:
593 break
594 yield chunk
595
596 progress_indicator = _progress_indicator
597
598 if link.netloc == PyPI.netloc:
599 url = show_url
600 else:
601 url = link.url_without_fragment
602
603 if show_progress: # We don't show progress on cached responses
604 if total_length:
605 logger.info(
606 "Downloading %s (%s)", url, format_size(total_length),
607 )
608 progress_indicator = DownloadProgressBar(
609 max=total_length,
610 ).iter
611 else:
612 logger.info("Downloading %s", url)
613 progress_indicator = DownloadProgressSpinner().iter
614 elif cached_resp:
615 logger.info("Using cached %s", url)
616 else:
617 logger.info("Downloading %s", url)
618
619 logger.debug('Downloading from URL %s', link)
620
621 for chunk in progress_indicator(resp_read(4096), 4096):
622 if download_hash is not None:
623 download_hash.update(chunk)
624 content_file.write(chunk)
625 if link.hash and link.hash_name:
626 _check_hash(download_hash, link)
627 return download_hash
628
629
630 def _copy_file(filename, location, content_type, link):
631 copy = True
632 download_location = os.path.join(location, link.filename)
633 if os.path.exists(download_location):
634 response = ask_path_exists(
635 'The file %s exists. (i)gnore, (w)ipe, (b)ackup ' %
636 display_path(download_location), ('i', 'w', 'b'))
637 if response == 'i':
638 copy = False
639 elif response == 'w':
640 logger.warning('Deleting %s', display_path(download_location))
641 os.remove(download_location)
642 elif response == 'b':
643 dest_file = backup_dir(download_location)
644 logger.warning(
645 'Backing up %s to %s',
646 display_path(download_location),
647 display_path(dest_file),
648 )
649 shutil.move(download_location, dest_file)
650 if copy:
651 shutil.copy(filename, download_location)
652 logger.info('Saved %s', display_path(download_location))
653
654
655 def unpack_http_url(link, location, download_dir=None, session=None):
656 if session is None:
657 raise TypeError(
658 "unpack_http_url() missing 1 required keyword argument: 'session'"
659 )
660
661 temp_dir = tempfile.mkdtemp('-unpack', 'pip-')
662
663 # If a download dir is specified, is the file already downloaded there?
664 already_downloaded_path = None
665 if download_dir:
666 already_downloaded_path = _check_download_dir(link, download_dir)
667
668 if already_downloaded_path:
669 from_path = already_downloaded_path
670 content_type = mimetypes.guess_type(from_path)[0]
671 else:
672 # let's download to a tmp dir
673 from_path, content_type = _download_http_url(link, session, temp_dir)
674
675 # unpack the archive to the build dir location. even when only downloading
676 # archives, they have to be unpacked to parse dependencies
677 unpack_file(from_path, location, content_type, link)
678
679 # a download dir is specified; let's copy the archive there
680 if download_dir and not already_downloaded_path:
681 _copy_file(from_path, download_dir, content_type, link)
682
683 if not already_downloaded_path:
684 os.unlink(from_path)
685 rmtree(temp_dir)
686
687
688 def unpack_file_url(link, location, download_dir=None):
689 """Unpack link into location.
690 If download_dir is provided and link points to a file, make a copy
691 of the link file inside download_dir."""
692
693 link_path = url_to_path(link.url_without_fragment)
694
695 # If it's a url to a local directory
696 if os.path.isdir(link_path):
697 if os.path.isdir(location):
698 rmtree(location)
699 shutil.copytree(link_path, location, symlinks=True)
700 if download_dir:
701 logger.info('Link is a directory, ignoring download_dir')
702 return
703
704 # if link has a hash, let's confirm it matches
705 if link.hash:
706 link_path_hash = _get_hash_from_file(link_path, link)
707 _check_hash(link_path_hash, link)
708
709 # If a download dir is specified, is the file already there and valid?
710 already_downloaded_path = None
711 if download_dir:
712 already_downloaded_path = _check_download_dir(link, download_dir)
713
714 if already_downloaded_path:
715 from_path = already_downloaded_path
716 else:
717 from_path = link_path
718
719 content_type = mimetypes.guess_type(from_path)[0]
720
721 # unpack the archive to the build dir location. even when only downloading
722 # archives, they have to be unpacked to parse dependencies
723 unpack_file(from_path, location, content_type, link)
724
725 # a download dir is specified and not already downloaded
726 if download_dir and not already_downloaded_path:
727 _copy_file(from_path, download_dir, content_type, link)
728
729
730 def _copy_dist_from_dir(link_path, location):
731 """Copy distribution files in `link_path` to `location`.
732
733 Invoked when user requests to install a local directory. E.g.:
734
735 pip install .
736 pip install ~/dev/git-repos/python-prompt-toolkit
737
738 """
739
740 # Note: This is currently VERY SLOW if you have a lot of data in the
741 # directory, because it copies everything with `shutil.copytree`.
742 # What it should really do is build an sdist and install that.
743 # See https://github.com/pypa/pip/issues/2195
744
745 if os.path.isdir(location):
746 rmtree(location)
747
748 # build an sdist
749 setup_py = 'setup.py'
750 sdist_args = [sys.executable]
751 sdist_args.append('-c')
752 sdist_args.append(
753 "import setuptools, tokenize;__file__=%r;"
754 "exec(compile(getattr(tokenize, 'open', open)(__file__).read()"
755 ".replace('\\r\\n', '\\n'), __file__, 'exec'))" % setup_py)
756 sdist_args.append('sdist')
757 sdist_args += ['--dist-dir', location]
758 logger.info('Running setup.py sdist for %s', link_path)
759
760 with indent_log():
761 call_subprocess(sdist_args, cwd=link_path, show_stdout=False)
762
763 # unpack sdist into `location`
764 sdist = os.path.join(location, os.listdir(location)[0])
765 logger.info('Unpacking sdist %s into %s', sdist, location)
766 unpack_file(sdist, location, content_type=None, link=None)
767
768
769 class PipXmlrpcTransport(xmlrpc_client.Transport):
770 """Provide a `xmlrpclib.Transport` implementation via a `PipSession`
771 object.
772 """
773 def __init__(self, index_url, session, use_datetime=False):
774 xmlrpc_client.Transport.__init__(self, use_datetime)
775 index_parts = urllib_parse.urlparse(index_url)
776 self._scheme = index_parts.scheme
777 self._session = session
778
779 def request(self, host, handler, request_body, verbose=False):
780 parts = (self._scheme, host, handler, None, None, None)
781 url = urllib_parse.urlunparse(parts)
782 try:
783 headers = {'Content-Type': 'text/xml'}
784 response = self._session.post(url, data=request_body,
785 headers=headers, stream=True)
786 response.raise_for_status()
787 self.verbose = verbose
788 return self.parse_response(response.raw)
789 except requests.HTTPError as exc:
790 logger.critical(
791 "HTTP error %s while getting %s",
792 exc.response.status_code, url,
793 )
794 raise
795
796
797 def unpack_url(link, location, download_dir=None,
798 only_download=False, session=None):
799 """Unpack link.
800 If link is a VCS link:
801 if only_download, export into download_dir and ignore location
802 else unpack into location
803 for other types of link:
804 - unpack into location
805 - if download_dir, copy the file into download_dir
806 - if only_download, mark location for deletion
807 """
808 # non-editable vcs urls
809 if is_vcs_url(link):
810 unpack_vcs_link(link, location, only_download)
811
812 # file urls
813 elif is_file_url(link):
814 unpack_file_url(link, location, download_dir)
815 if only_download:
816 write_delete_marker_file(location)
817
818 # http urls
819 else:
820 if session is None:
821 session = PipSession()
822
823 unpack_http_url(
824 link,
825 location,
826 download_dir,
827 session,
828 )
829 if only_download:
830 write_delete_marker_file(location)
831
832
833 def _download_http_url(link, session, temp_dir):
834 """Download link url into temp_dir using provided session"""
835 target_url = link.url.split('#', 1)[0]
836 try:
837 resp = session.get(
838 target_url,
839 # We use Accept-Encoding: identity here because requests
840 # defaults to accepting compressed responses. This breaks in
841 # a variety of ways depending on how the server is configured.
842 # - Some servers will notice that the file isn't a compressible
843 # file and will leave the file alone and with an empty
844 # Content-Encoding
845 # - Some servers will notice that the file is already
846 # compressed and will leave the file alone and will add a
847 # Content-Encoding: gzip header
848 # - Some servers won't notice anything at all and will take
849 # a file that's already been compressed and compress it again
850 # and set the Content-Encoding: gzip header
851 # By setting this to request only the identity encoding We're
852 # hoping to eliminate the third case. Hopefully there does not
853 # exist a server which when given a file will notice it is
854 # already compressed and that you're not asking for a
855 # compressed file and will then decompress it before sending
856 # because if that's the case I don't think it'll ever be
857 # possible to make this work.
858 headers={"Accept-Encoding": "identity"},
859 stream=True,
860 )
861 resp.raise_for_status()
862 except requests.HTTPError as exc:
863 logger.critical(
864 "HTTP error %s while getting %s", exc.response.status_code, link,
865 )
866 raise
867
868 content_type = resp.headers.get('content-type', '')
869 filename = link.filename # fallback
870 # Have a look at the Content-Disposition header for a better guess
871 content_disposition = resp.headers.get('content-disposition')
872 if content_disposition:
873 type, params = cgi.parse_header(content_disposition)
874 # We use ``or`` here because we don't want to use an "empty" value
875 # from the filename param.
876 filename = params.get('filename') or filename
877 ext = splitext(filename)[1]
878 if not ext:
879 ext = mimetypes.guess_extension(content_type)
880 if ext:
881 filename += ext
882 if not ext and link.url != resp.url:
883 ext = os.path.splitext(resp.url)[1]
884 if ext:
885 filename += ext
886 file_path = os.path.join(temp_dir, filename)
887 with open(file_path, 'wb') as content_file:
888 _download_url(resp, link, content_file)
889 return file_path, content_type
890
891
892 def _check_download_dir(link, download_dir):
893 """ Check download_dir for previously downloaded file with correct hash
894 If a correct file is found return its path else None
895 """
896 download_path = os.path.join(download_dir, link.filename)
897 if os.path.exists(download_path):
898 # If already downloaded, does its hash match?
899 logger.info('File was already downloaded %s', download_path)
900 if link.hash:
901 download_hash = _get_hash_from_file(download_path, link)
902 try:
903 _check_hash(download_hash, link)
904 except HashMismatch:
905 logger.warning(
906 'Previously-downloaded file %s has bad hash, '
907 're-downloading.',
908 download_path
909 )
910 os.unlink(download_path)
911 return None
912 return download_path
913 return None