Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/distlib/index.py @ 0:26e78fe6e8c4 draft
"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
| author | shellac | 
|---|---|
| date | Sat, 02 May 2020 07:14:21 -0400 | 
| parents | |
| children | 
   comparison
  equal
  deleted
  inserted
  replaced
| -1:000000000000 | 0:26e78fe6e8c4 | 
|---|---|
| 1 # -*- coding: utf-8 -*- | |
| 2 # | |
| 3 # Copyright (C) 2013 Vinay Sajip. | |
| 4 # Licensed to the Python Software Foundation under a contributor agreement. | |
| 5 # See LICENSE.txt and CONTRIBUTORS.txt. | |
| 6 # | |
| 7 import hashlib | |
| 8 import logging | |
| 9 import os | |
| 10 import shutil | |
| 11 import subprocess | |
| 12 import tempfile | |
| 13 try: | |
| 14 from threading import Thread | |
| 15 except ImportError: | |
| 16 from dummy_threading import Thread | |
| 17 | |
| 18 from . import DistlibException | |
| 19 from .compat import (HTTPBasicAuthHandler, Request, HTTPPasswordMgr, | |
| 20 urlparse, build_opener, string_types) | |
| 21 from .util import cached_property, zip_dir, ServerProxy | |
| 22 | |
| 23 logger = logging.getLogger(__name__) | |
| 24 | |
| 25 DEFAULT_INDEX = 'https://pypi.org/pypi' | |
| 26 DEFAULT_REALM = 'pypi' | |
| 27 | |
| 28 class PackageIndex(object): | |
| 29 """ | |
| 30 This class represents a package index compatible with PyPI, the Python | |
| 31 Package Index. | |
| 32 """ | |
| 33 | |
| 34 boundary = b'----------ThIs_Is_tHe_distlib_index_bouNdaRY_$' | |
| 35 | |
| 36 def __init__(self, url=None): | |
| 37 """ | |
| 38 Initialise an instance. | |
| 39 | |
| 40 :param url: The URL of the index. If not specified, the URL for PyPI is | |
| 41 used. | |
| 42 """ | |
| 43 self.url = url or DEFAULT_INDEX | |
| 44 self.read_configuration() | |
| 45 scheme, netloc, path, params, query, frag = urlparse(self.url) | |
| 46 if params or query or frag or scheme not in ('http', 'https'): | |
| 47 raise DistlibException('invalid repository: %s' % self.url) | |
| 48 self.password_handler = None | |
| 49 self.ssl_verifier = None | |
| 50 self.gpg = None | |
| 51 self.gpg_home = None | |
| 52 with open(os.devnull, 'w') as sink: | |
| 53 # Use gpg by default rather than gpg2, as gpg2 insists on | |
| 54 # prompting for passwords | |
| 55 for s in ('gpg', 'gpg2'): | |
| 56 try: | |
| 57 rc = subprocess.check_call([s, '--version'], stdout=sink, | |
| 58 stderr=sink) | |
| 59 if rc == 0: | |
| 60 self.gpg = s | |
| 61 break | |
| 62 except OSError: | |
| 63 pass | |
| 64 | |
| 65 def _get_pypirc_command(self): | |
| 66 """ | |
| 67 Get the distutils command for interacting with PyPI configurations. | |
| 68 :return: the command. | |
| 69 """ | |
| 70 from distutils.core import Distribution | |
| 71 from distutils.config import PyPIRCCommand | |
| 72 d = Distribution() | |
| 73 return PyPIRCCommand(d) | |
| 74 | |
| 75 def read_configuration(self): | |
| 76 """ | |
| 77 Read the PyPI access configuration as supported by distutils, getting | |
| 78 PyPI to do the actual work. This populates ``username``, ``password``, | |
| 79 ``realm`` and ``url`` attributes from the configuration. | |
| 80 """ | |
| 81 # get distutils to do the work | |
| 82 c = self._get_pypirc_command() | |
| 83 c.repository = self.url | |
| 84 cfg = c._read_pypirc() | |
| 85 self.username = cfg.get('username') | |
| 86 self.password = cfg.get('password') | |
| 87 self.realm = cfg.get('realm', 'pypi') | |
| 88 self.url = cfg.get('repository', self.url) | |
| 89 | |
| 90 def save_configuration(self): | |
| 91 """ | |
| 92 Save the PyPI access configuration. You must have set ``username`` and | |
| 93 ``password`` attributes before calling this method. | |
| 94 | |
| 95 Again, distutils is used to do the actual work. | |
| 96 """ | |
| 97 self.check_credentials() | |
| 98 # get distutils to do the work | |
| 99 c = self._get_pypirc_command() | |
| 100 c._store_pypirc(self.username, self.password) | |
| 101 | |
| 102 def check_credentials(self): | |
| 103 """ | |
| 104 Check that ``username`` and ``password`` have been set, and raise an | |
| 105 exception if not. | |
| 106 """ | |
| 107 if self.username is None or self.password is None: | |
| 108 raise DistlibException('username and password must be set') | |
| 109 pm = HTTPPasswordMgr() | |
| 110 _, netloc, _, _, _, _ = urlparse(self.url) | |
| 111 pm.add_password(self.realm, netloc, self.username, self.password) | |
| 112 self.password_handler = HTTPBasicAuthHandler(pm) | |
| 113 | |
| 114 def register(self, metadata): | |
| 115 """ | |
| 116 Register a distribution on PyPI, using the provided metadata. | |
| 117 | |
| 118 :param metadata: A :class:`Metadata` instance defining at least a name | |
| 119 and version number for the distribution to be | |
| 120 registered. | |
| 121 :return: The HTTP response received from PyPI upon submission of the | |
| 122 request. | |
| 123 """ | |
| 124 self.check_credentials() | |
| 125 metadata.validate() | |
| 126 d = metadata.todict() | |
| 127 d[':action'] = 'verify' | |
| 128 request = self.encode_request(d.items(), []) | |
| 129 response = self.send_request(request) | |
| 130 d[':action'] = 'submit' | |
| 131 request = self.encode_request(d.items(), []) | |
| 132 return self.send_request(request) | |
| 133 | |
| 134 def _reader(self, name, stream, outbuf): | |
| 135 """ | |
| 136 Thread runner for reading lines of from a subprocess into a buffer. | |
| 137 | |
| 138 :param name: The logical name of the stream (used for logging only). | |
| 139 :param stream: The stream to read from. This will typically a pipe | |
| 140 connected to the output stream of a subprocess. | |
| 141 :param outbuf: The list to append the read lines to. | |
| 142 """ | |
| 143 while True: | |
| 144 s = stream.readline() | |
| 145 if not s: | |
| 146 break | |
| 147 s = s.decode('utf-8').rstrip() | |
| 148 outbuf.append(s) | |
| 149 logger.debug('%s: %s' % (name, s)) | |
| 150 stream.close() | |
| 151 | |
| 152 def get_sign_command(self, filename, signer, sign_password, | |
| 153 keystore=None): | |
| 154 """ | |
| 155 Return a suitable command for signing a file. | |
| 156 | |
| 157 :param filename: The pathname to the file to be signed. | |
| 158 :param signer: The identifier of the signer of the file. | |
| 159 :param sign_password: The passphrase for the signer's | |
| 160 private key used for signing. | |
| 161 :param keystore: The path to a directory which contains the keys | |
| 162 used in verification. If not specified, the | |
| 163 instance's ``gpg_home`` attribute is used instead. | |
| 164 :return: The signing command as a list suitable to be | |
| 165 passed to :class:`subprocess.Popen`. | |
| 166 """ | |
| 167 cmd = [self.gpg, '--status-fd', '2', '--no-tty'] | |
| 168 if keystore is None: | |
| 169 keystore = self.gpg_home | |
| 170 if keystore: | |
| 171 cmd.extend(['--homedir', keystore]) | |
| 172 if sign_password is not None: | |
| 173 cmd.extend(['--batch', '--passphrase-fd', '0']) | |
| 174 td = tempfile.mkdtemp() | |
| 175 sf = os.path.join(td, os.path.basename(filename) + '.asc') | |
| 176 cmd.extend(['--detach-sign', '--armor', '--local-user', | |
| 177 signer, '--output', sf, filename]) | |
| 178 logger.debug('invoking: %s', ' '.join(cmd)) | |
| 179 return cmd, sf | |
| 180 | |
| 181 def run_command(self, cmd, input_data=None): | |
| 182 """ | |
| 183 Run a command in a child process , passing it any input data specified. | |
| 184 | |
| 185 :param cmd: The command to run. | |
| 186 :param input_data: If specified, this must be a byte string containing | |
| 187 data to be sent to the child process. | |
| 188 :return: A tuple consisting of the subprocess' exit code, a list of | |
| 189 lines read from the subprocess' ``stdout``, and a list of | |
| 190 lines read from the subprocess' ``stderr``. | |
| 191 """ | |
| 192 kwargs = { | |
| 193 'stdout': subprocess.PIPE, | |
| 194 'stderr': subprocess.PIPE, | |
| 195 } | |
| 196 if input_data is not None: | |
| 197 kwargs['stdin'] = subprocess.PIPE | |
| 198 stdout = [] | |
| 199 stderr = [] | |
| 200 p = subprocess.Popen(cmd, **kwargs) | |
| 201 # We don't use communicate() here because we may need to | |
| 202 # get clever with interacting with the command | |
| 203 t1 = Thread(target=self._reader, args=('stdout', p.stdout, stdout)) | |
| 204 t1.start() | |
| 205 t2 = Thread(target=self._reader, args=('stderr', p.stderr, stderr)) | |
| 206 t2.start() | |
| 207 if input_data is not None: | |
| 208 p.stdin.write(input_data) | |
| 209 p.stdin.close() | |
| 210 | |
| 211 p.wait() | |
| 212 t1.join() | |
| 213 t2.join() | |
| 214 return p.returncode, stdout, stderr | |
| 215 | |
| 216 def sign_file(self, filename, signer, sign_password, keystore=None): | |
| 217 """ | |
| 218 Sign a file. | |
| 219 | |
| 220 :param filename: The pathname to the file to be signed. | |
| 221 :param signer: The identifier of the signer of the file. | |
| 222 :param sign_password: The passphrase for the signer's | |
| 223 private key used for signing. | |
| 224 :param keystore: The path to a directory which contains the keys | |
| 225 used in signing. If not specified, the instance's | |
| 226 ``gpg_home`` attribute is used instead. | |
| 227 :return: The absolute pathname of the file where the signature is | |
| 228 stored. | |
| 229 """ | |
| 230 cmd, sig_file = self.get_sign_command(filename, signer, sign_password, | |
| 231 keystore) | |
| 232 rc, stdout, stderr = self.run_command(cmd, | |
| 233 sign_password.encode('utf-8')) | |
| 234 if rc != 0: | |
| 235 raise DistlibException('sign command failed with error ' | |
| 236 'code %s' % rc) | |
| 237 return sig_file | |
| 238 | |
| 239 def upload_file(self, metadata, filename, signer=None, sign_password=None, | |
| 240 filetype='sdist', pyversion='source', keystore=None): | |
| 241 """ | |
| 242 Upload a release file to the index. | |
| 243 | |
| 244 :param metadata: A :class:`Metadata` instance defining at least a name | |
| 245 and version number for the file to be uploaded. | |
| 246 :param filename: The pathname of the file to be uploaded. | |
| 247 :param signer: The identifier of the signer of the file. | |
| 248 :param sign_password: The passphrase for the signer's | |
| 249 private key used for signing. | |
| 250 :param filetype: The type of the file being uploaded. This is the | |
| 251 distutils command which produced that file, e.g. | |
| 252 ``sdist`` or ``bdist_wheel``. | |
| 253 :param pyversion: The version of Python which the release relates | |
| 254 to. For code compatible with any Python, this would | |
| 255 be ``source``, otherwise it would be e.g. ``3.2``. | |
| 256 :param keystore: The path to a directory which contains the keys | |
| 257 used in signing. If not specified, the instance's | |
| 258 ``gpg_home`` attribute is used instead. | |
| 259 :return: The HTTP response received from PyPI upon submission of the | |
| 260 request. | |
| 261 """ | |
| 262 self.check_credentials() | |
| 263 if not os.path.exists(filename): | |
| 264 raise DistlibException('not found: %s' % filename) | |
| 265 metadata.validate() | |
| 266 d = metadata.todict() | |
| 267 sig_file = None | |
| 268 if signer: | |
| 269 if not self.gpg: | |
| 270 logger.warning('no signing program available - not signed') | |
| 271 else: | |
| 272 sig_file = self.sign_file(filename, signer, sign_password, | |
| 273 keystore) | |
| 274 with open(filename, 'rb') as f: | |
| 275 file_data = f.read() | |
| 276 md5_digest = hashlib.md5(file_data).hexdigest() | |
| 277 sha256_digest = hashlib.sha256(file_data).hexdigest() | |
| 278 d.update({ | |
| 279 ':action': 'file_upload', | |
| 280 'protocol_version': '1', | |
| 281 'filetype': filetype, | |
| 282 'pyversion': pyversion, | |
| 283 'md5_digest': md5_digest, | |
| 284 'sha256_digest': sha256_digest, | |
| 285 }) | |
| 286 files = [('content', os.path.basename(filename), file_data)] | |
| 287 if sig_file: | |
| 288 with open(sig_file, 'rb') as f: | |
| 289 sig_data = f.read() | |
| 290 files.append(('gpg_signature', os.path.basename(sig_file), | |
| 291 sig_data)) | |
| 292 shutil.rmtree(os.path.dirname(sig_file)) | |
| 293 request = self.encode_request(d.items(), files) | |
| 294 return self.send_request(request) | |
| 295 | |
| 296 def upload_documentation(self, metadata, doc_dir): | |
| 297 """ | |
| 298 Upload documentation to the index. | |
| 299 | |
| 300 :param metadata: A :class:`Metadata` instance defining at least a name | |
| 301 and version number for the documentation to be | |
| 302 uploaded. | |
| 303 :param doc_dir: The pathname of the directory which contains the | |
| 304 documentation. This should be the directory that | |
| 305 contains the ``index.html`` for the documentation. | |
| 306 :return: The HTTP response received from PyPI upon submission of the | |
| 307 request. | |
| 308 """ | |
| 309 self.check_credentials() | |
| 310 if not os.path.isdir(doc_dir): | |
| 311 raise DistlibException('not a directory: %r' % doc_dir) | |
| 312 fn = os.path.join(doc_dir, 'index.html') | |
| 313 if not os.path.exists(fn): | |
| 314 raise DistlibException('not found: %r' % fn) | |
| 315 metadata.validate() | |
| 316 name, version = metadata.name, metadata.version | |
| 317 zip_data = zip_dir(doc_dir).getvalue() | |
| 318 fields = [(':action', 'doc_upload'), | |
| 319 ('name', name), ('version', version)] | |
| 320 files = [('content', name, zip_data)] | |
| 321 request = self.encode_request(fields, files) | |
| 322 return self.send_request(request) | |
| 323 | |
| 324 def get_verify_command(self, signature_filename, data_filename, | |
| 325 keystore=None): | |
| 326 """ | |
| 327 Return a suitable command for verifying a file. | |
| 328 | |
| 329 :param signature_filename: The pathname to the file containing the | |
| 330 signature. | |
| 331 :param data_filename: The pathname to the file containing the | |
| 332 signed data. | |
| 333 :param keystore: The path to a directory which contains the keys | |
| 334 used in verification. If not specified, the | |
| 335 instance's ``gpg_home`` attribute is used instead. | |
| 336 :return: The verifying command as a list suitable to be | |
| 337 passed to :class:`subprocess.Popen`. | |
| 338 """ | |
| 339 cmd = [self.gpg, '--status-fd', '2', '--no-tty'] | |
| 340 if keystore is None: | |
| 341 keystore = self.gpg_home | |
| 342 if keystore: | |
| 343 cmd.extend(['--homedir', keystore]) | |
| 344 cmd.extend(['--verify', signature_filename, data_filename]) | |
| 345 logger.debug('invoking: %s', ' '.join(cmd)) | |
| 346 return cmd | |
| 347 | |
| 348 def verify_signature(self, signature_filename, data_filename, | |
| 349 keystore=None): | |
| 350 """ | |
| 351 Verify a signature for a file. | |
| 352 | |
| 353 :param signature_filename: The pathname to the file containing the | |
| 354 signature. | |
| 355 :param data_filename: The pathname to the file containing the | |
| 356 signed data. | |
| 357 :param keystore: The path to a directory which contains the keys | |
| 358 used in verification. If not specified, the | |
| 359 instance's ``gpg_home`` attribute is used instead. | |
| 360 :return: True if the signature was verified, else False. | |
| 361 """ | |
| 362 if not self.gpg: | |
| 363 raise DistlibException('verification unavailable because gpg ' | |
| 364 'unavailable') | |
| 365 cmd = self.get_verify_command(signature_filename, data_filename, | |
| 366 keystore) | |
| 367 rc, stdout, stderr = self.run_command(cmd) | |
| 368 if rc not in (0, 1): | |
| 369 raise DistlibException('verify command failed with error ' | |
| 370 'code %s' % rc) | |
| 371 return rc == 0 | |
| 372 | |
| 373 def download_file(self, url, destfile, digest=None, reporthook=None): | |
| 374 """ | |
| 375 This is a convenience method for downloading a file from an URL. | |
| 376 Normally, this will be a file from the index, though currently | |
| 377 no check is made for this (i.e. a file can be downloaded from | |
| 378 anywhere). | |
| 379 | |
| 380 The method is just like the :func:`urlretrieve` function in the | |
| 381 standard library, except that it allows digest computation to be | |
| 382 done during download and checking that the downloaded data | |
| 383 matched any expected value. | |
| 384 | |
| 385 :param url: The URL of the file to be downloaded (assumed to be | |
| 386 available via an HTTP GET request). | |
| 387 :param destfile: The pathname where the downloaded file is to be | |
| 388 saved. | |
| 389 :param digest: If specified, this must be a (hasher, value) | |
| 390 tuple, where hasher is the algorithm used (e.g. | |
| 391 ``'md5'``) and ``value`` is the expected value. | |
| 392 :param reporthook: The same as for :func:`urlretrieve` in the | |
| 393 standard library. | |
| 394 """ | |
| 395 if digest is None: | |
| 396 digester = None | |
| 397 logger.debug('No digest specified') | |
| 398 else: | |
| 399 if isinstance(digest, (list, tuple)): | |
| 400 hasher, digest = digest | |
| 401 else: | |
| 402 hasher = 'md5' | |
| 403 digester = getattr(hashlib, hasher)() | |
| 404 logger.debug('Digest specified: %s' % digest) | |
| 405 # The following code is equivalent to urlretrieve. | |
| 406 # We need to do it this way so that we can compute the | |
| 407 # digest of the file as we go. | |
| 408 with open(destfile, 'wb') as dfp: | |
| 409 # addinfourl is not a context manager on 2.x | |
| 410 # so we have to use try/finally | |
| 411 sfp = self.send_request(Request(url)) | |
| 412 try: | |
| 413 headers = sfp.info() | |
| 414 blocksize = 8192 | |
| 415 size = -1 | |
| 416 read = 0 | |
| 417 blocknum = 0 | |
| 418 if "content-length" in headers: | |
| 419 size = int(headers["Content-Length"]) | |
| 420 if reporthook: | |
| 421 reporthook(blocknum, blocksize, size) | |
| 422 while True: | |
| 423 block = sfp.read(blocksize) | |
| 424 if not block: | |
| 425 break | |
| 426 read += len(block) | |
| 427 dfp.write(block) | |
| 428 if digester: | |
| 429 digester.update(block) | |
| 430 blocknum += 1 | |
| 431 if reporthook: | |
| 432 reporthook(blocknum, blocksize, size) | |
| 433 finally: | |
| 434 sfp.close() | |
| 435 | |
| 436 # check that we got the whole file, if we can | |
| 437 if size >= 0 and read < size: | |
| 438 raise DistlibException( | |
| 439 'retrieval incomplete: got only %d out of %d bytes' | |
| 440 % (read, size)) | |
| 441 # if we have a digest, it must match. | |
| 442 if digester: | |
| 443 actual = digester.hexdigest() | |
| 444 if digest != actual: | |
| 445 raise DistlibException('%s digest mismatch for %s: expected ' | |
| 446 '%s, got %s' % (hasher, destfile, | |
| 447 digest, actual)) | |
| 448 logger.debug('Digest verified: %s', digest) | |
| 449 | |
| 450 def send_request(self, req): | |
| 451 """ | |
| 452 Send a standard library :class:`Request` to PyPI and return its | |
| 453 response. | |
| 454 | |
| 455 :param req: The request to send. | |
| 456 :return: The HTTP response from PyPI (a standard library HTTPResponse). | |
| 457 """ | |
| 458 handlers = [] | |
| 459 if self.password_handler: | |
| 460 handlers.append(self.password_handler) | |
| 461 if self.ssl_verifier: | |
| 462 handlers.append(self.ssl_verifier) | |
| 463 opener = build_opener(*handlers) | |
| 464 return opener.open(req) | |
| 465 | |
| 466 def encode_request(self, fields, files): | |
| 467 """ | |
| 468 Encode fields and files for posting to an HTTP server. | |
| 469 | |
| 470 :param fields: The fields to send as a list of (fieldname, value) | |
| 471 tuples. | |
| 472 :param files: The files to send as a list of (fieldname, filename, | |
| 473 file_bytes) tuple. | |
| 474 """ | |
| 475 # Adapted from packaging, which in turn was adapted from | |
| 476 # http://code.activestate.com/recipes/146306 | |
| 477 | |
| 478 parts = [] | |
| 479 boundary = self.boundary | |
| 480 for k, values in fields: | |
| 481 if not isinstance(values, (list, tuple)): | |
| 482 values = [values] | |
| 483 | |
| 484 for v in values: | |
| 485 parts.extend(( | |
| 486 b'--' + boundary, | |
| 487 ('Content-Disposition: form-data; name="%s"' % | |
| 488 k).encode('utf-8'), | |
| 489 b'', | |
| 490 v.encode('utf-8'))) | |
| 491 for key, filename, value in files: | |
| 492 parts.extend(( | |
| 493 b'--' + boundary, | |
| 494 ('Content-Disposition: form-data; name="%s"; filename="%s"' % | |
| 495 (key, filename)).encode('utf-8'), | |
| 496 b'', | |
| 497 value)) | |
| 498 | |
| 499 parts.extend((b'--' + boundary + b'--', b'')) | |
| 500 | |
| 501 body = b'\r\n'.join(parts) | |
| 502 ct = b'multipart/form-data; boundary=' + boundary | |
| 503 headers = { | |
| 504 'Content-type': ct, | |
| 505 'Content-length': str(len(body)) | |
| 506 } | |
| 507 return Request(self.url, body, headers) | |
| 508 | |
| 509 def search(self, terms, operator=None): | |
| 510 if isinstance(terms, string_types): | |
| 511 terms = {'name': terms} | |
| 512 rpc_proxy = ServerProxy(self.url, timeout=3.0) | |
| 513 try: | |
| 514 return rpc_proxy.search(terms, operator or 'and') | |
| 515 finally: | |
| 516 rpc_proxy('close')() | 
