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 DistutilsError, 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 = '\r\n--' + boundary
    140         end_boundary = sep_boundary + '--\r\n'
    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('\r\nContent-Disposition: form-data; name="%s"' % key)
    155                 body.write(fn)
    156                 body.write("\r\n\r\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 = body.getvalue()
    162 
    163         self.announce("Submitting %s to %s" % (filename, self.repository), log.INFO)
    164 
    165         # build the Request
    166         headers = {'Content-type':
    167                         'multipart/form-data; boundary=%s' % boundary,
    168                    'Content-length': str(len(body)),
    169                    'Authorization': auth}
    170 
    171         request = Request(self.repository, data=body,
    172                           headers=headers)
    173         # send the data
    174         try:
    175             result = urlopen(request)
    176             status = result.getcode()
    177             reason = result.msg
    178             if self.show_response:
    179                 msg = '\n'.join(('-' * 75, result.read(), '-' * 75))
    180                 self.announce(msg, log.INFO)
    181         except socket.error, e:
    182             self.announce(str(e), log.ERROR)
    183             raise
    184         except HTTPError, e:
    185             status = e.code
    186             reason = e.msg
    187 
    188         if status == 200:
    189             self.announce('Server response (%s): %s' % (status, reason),
    190                           log.INFO)
    191         else:
    192             msg = 'Upload failed (%s): %s' % (status, reason)
    193             self.announce(msg, log.ERROR)
    194             raise DistutilsError(msg)
    195