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