Home | History | Annotate | Download | only in command
      1 """distutils.command.upload
      2 
      3 Implements the Distutils 'upload' subcommand (upload package to PyPI)."""
      4 import os
      5 import socket
      6 import platform
      7 from urllib2 import urlopen, Request, HTTPError
      8 from base64 import standard_b64encode
      9 import urlparse
     10 import cStringIO as StringIO
     11 from hashlib import md5
     12 
     13 from distutils.errors import DistutilsOptionError
     14 from distutils.core import PyPIRCCommand
     15 from distutils.spawn import spawn
     16 from distutils import log
     17 
     18 class upload(PyPIRCCommand):
     19 
     20     description = "upload binary package to PyPI"
     21 
     22     user_options = PyPIRCCommand.user_options + [
     23         ('sign', 's',
     24          'sign files to upload using gpg'),
     25         ('identity=', 'i', 'GPG identity used to sign files'),
     26         ]
     27 
     28     boolean_options = PyPIRCCommand.boolean_options + ['sign']
     29 
     30     def initialize_options(self):
     31         PyPIRCCommand.initialize_options(self)
     32         self.username = ''
     33         self.password = ''
     34         self.show_response = 0
     35         self.sign = False
     36         self.identity = None
     37 
     38     def finalize_options(self):
     39         PyPIRCCommand.finalize_options(self)
     40         if self.identity and not self.sign:
     41             raise DistutilsOptionError(
     42                 "Must use --sign for --identity to have meaning"
     43             )
     44         config = self._read_pypirc()
     45         if config != {}:
     46             self.username = config['username']
     47             self.password = config['password']
     48             self.repository = config['repository']
     49             self.realm = config['realm']
     50 
     51         # getting the password from the distribution
     52         # if previously set by the register command
     53         if not self.password and self.distribution.password:
     54             self.password = self.distribution.password
     55 
     56     def run(self):
     57         if not self.distribution.dist_files:
     58             raise DistutilsOptionError("No dist file created in earlier command")
     59         for command, pyversion, filename in self.distribution.dist_files:
     60             self.upload_file(command, pyversion, filename)
     61 
     62     def upload_file(self, command, pyversion, filename):
     63         # Makes sure the repository URL is compliant
     64         schema, netloc, url, params, query, fragments = \
     65             urlparse.urlparse(self.repository)
     66         if params or query or fragments:
     67             raise AssertionError("Incompatible url %s" % self.repository)
     68 
     69         if schema not in ('http', 'https'):
     70             raise AssertionError("unsupported schema " + schema)
     71 
     72         # Sign if requested
     73         if self.sign:
     74             gpg_args = ["gpg", "--detach-sign", "-a", filename]
     75             if self.identity:
     76                 gpg_args[2:2] = ["--local-user", self.identity]
     77             spawn(gpg_args,
     78                   dry_run=self.dry_run)
     79 
     80         # Fill in the data - send all the meta-data in case we need to
     81         # register a new release
     82         f = open(filename,'rb')
     83         try:
     84             content = f.read()
     85         finally:
     86             f.close()
     87         meta = self.distribution.metadata
     88         data = {
     89             # action
     90             ':action': 'file_upload',
     91             'protcol_version': '1',
     92 
     93             # identify release
     94             'name': meta.get_name(),
     95             'version': meta.get_version(),
     96 
     97             # file content
     98             'content': (os.path.basename(filename),content),
     99             'filetype': command,
    100             'pyversion': pyversion,
    101             'md5_digest': md5(content).hexdigest(),
    102 
    103             # additional meta-data
    104             'metadata_version' : '1.0',
    105             'summary': meta.get_description(),
    106             'home_page': meta.get_url(),
    107             'author': meta.get_contact(),
    108             'author_email': meta.get_contact_email(),
    109             'license': meta.get_licence(),
    110             'description': meta.get_long_description(),
    111             'keywords': meta.get_keywords(),
    112             'platform': meta.get_platforms(),
    113             'classifiers': meta.get_classifiers(),
    114             'download_url': meta.get_download_url(),
    115             # PEP 314
    116             'provides': meta.get_provides(),
    117             'requires': meta.get_requires(),
    118             'obsoletes': meta.get_obsoletes(),
    119             }
    120         comment = ''
    121         if command == 'bdist_rpm':
    122             dist, version, id = platform.dist()
    123             if dist:
    124                 comment = 'built for %s %s' % (dist, version)
    125         elif command == 'bdist_dumb':
    126             comment = 'built for %s' % platform.platform(terse=1)
    127         data['comment'] = comment
    128 
    129         if self.sign:
    130             data['gpg_signature'] = (os.path.basename(filename) + ".asc",
    131                                      open(filename+".asc").read())
    132 
    133         # set up the authentication
    134         auth = "Basic " + standard_b64encode(self.username + ":" +
    135                                              self.password)
    136 
    137         # Build up the MIME payload for the POST data
    138         boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
    139         sep_boundary = '\n--' + boundary
    140         end_boundary = sep_boundary + '--'
    141         body = StringIO.StringIO()
    142         for key, value in data.items():
    143             # handle multiple entries for the same name
    144             if not isinstance(value, list):
    145                 value = [value]
    146             for value in value:
    147                 if isinstance(value, tuple):
    148                     fn = ';filename="%s"' % value[0]
    149                     value = value[1]
    150                 else:
    151                     fn = ""
    152 
    153                 body.write(sep_boundary)
    154                 body.write('\nContent-Disposition: form-data; name="%s"'%key)
    155                 body.write(fn)
    156                 body.write("\n\n")
    157                 body.write(value)
    158                 if value and value[-1] == '\r':
    159                     body.write('\n')  # write an extra newline (lurve Macs)
    160         body.write(end_boundary)
    161         body.write("\n")
    162         body = body.getvalue()
    163 
    164         self.announce("Submitting %s to %s" % (filename, self.repository), log.INFO)
    165 
    166         # build the Request
    167         headers = {'Content-type':
    168                         'multipart/form-data; boundary=%s' % boundary,
    169                    'Content-length': str(len(body)),
    170                    'Authorization': auth}
    171 
    172         request = Request(self.repository, data=body,
    173                           headers=headers)
    174         # send the data
    175         try:
    176             result = urlopen(request)
    177             status = result.getcode()
    178             reason = result.msg
    179             if self.show_response:
    180                 msg = '\n'.join(('-' * 75, r.read(), '-' * 75))
    181                 self.announce(msg, log.INFO)
    182         except socket.error, e:
    183             self.announce(str(e), log.ERROR)
    184             return
    185         except HTTPError, e:
    186             status = e.code
    187             reason = e.msg
    188 
    189         if status == 200:
    190             self.announce('Server response (%s): %s' % (status, reason),
    191                           log.INFO)
    192         else:
    193             self.announce('Upload failed (%s): %s' % (status, reason),
    194                           log.ERROR)
    195