• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""distutils.command.upload
2
3Implements the Distutils 'upload' subcommand (upload package to PyPI)."""
4import os
5import socket
6import platform
7from urllib2 import urlopen, Request, HTTPError
8from base64 import standard_b64encode
9import urlparse
10import cStringIO as StringIO
11from hashlib import md5
12
13from distutils.errors import DistutilsError, DistutilsOptionError
14from distutils.core import PyPIRCCommand
15from distutils.spawn import spawn
16from distutils import log
17
18class 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            msg = ("Must create and upload files in one command "
59                   "(e.g. setup.py sdist upload)")
60            raise DistutilsOptionError(msg)
61        for command, pyversion, filename in self.distribution.dist_files:
62            self.upload_file(command, pyversion, filename)
63
64    def upload_file(self, command, pyversion, filename):
65        # Makes sure the repository URL is compliant
66        schema, netloc, url, params, query, fragments = \
67            urlparse.urlparse(self.repository)
68        if params or query or fragments:
69            raise AssertionError("Incompatible url %s" % self.repository)
70
71        if schema not in ('http', 'https'):
72            raise AssertionError("unsupported schema " + schema)
73
74        # Sign if requested
75        if self.sign:
76            gpg_args = ["gpg", "--detach-sign", "-a", filename]
77            if self.identity:
78                gpg_args[2:2] = ["--local-user", self.identity]
79            spawn(gpg_args,
80                  dry_run=self.dry_run)
81
82        # Fill in the data - send all the meta-data in case we need to
83        # register a new release
84        f = open(filename,'rb')
85        try:
86            content = f.read()
87        finally:
88            f.close()
89        meta = self.distribution.metadata
90        data = {
91            # action
92            ':action': 'file_upload',
93            'protcol_version': '1',
94
95            # identify release
96            'name': meta.get_name(),
97            'version': meta.get_version(),
98
99            # file content
100            'content': (os.path.basename(filename),content),
101            'filetype': command,
102            'pyversion': pyversion,
103            'md5_digest': md5(content).hexdigest(),
104
105            # additional meta-data
106            'metadata_version' : '1.0',
107            'summary': meta.get_description(),
108            'home_page': meta.get_url(),
109            'author': meta.get_contact(),
110            'author_email': meta.get_contact_email(),
111            'license': meta.get_licence(),
112            'description': meta.get_long_description(),
113            'keywords': meta.get_keywords(),
114            'platform': meta.get_platforms(),
115            'classifiers': meta.get_classifiers(),
116            'download_url': meta.get_download_url(),
117            # PEP 314
118            'provides': meta.get_provides(),
119            'requires': meta.get_requires(),
120            'obsoletes': meta.get_obsoletes(),
121            }
122        comment = ''
123        if command == 'bdist_rpm':
124            dist, version, id = platform.dist()
125            if dist:
126                comment = 'built for %s %s' % (dist, version)
127        elif command == 'bdist_dumb':
128            comment = 'built for %s' % platform.platform(terse=1)
129        data['comment'] = comment
130
131        if self.sign:
132            data['gpg_signature'] = (os.path.basename(filename) + ".asc",
133                                     open(filename+".asc").read())
134
135        # set up the authentication
136        auth = "Basic " + standard_b64encode(self.username + ":" +
137                                             self.password)
138
139        # Build up the MIME payload for the POST data
140        boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
141        sep_boundary = '\r\n--' + boundary
142        end_boundary = sep_boundary + '--\r\n'
143        body = StringIO.StringIO()
144        for key, value in data.items():
145            # handle multiple entries for the same name
146            if not isinstance(value, list):
147                value = [value]
148            for value in value:
149                if isinstance(value, tuple):
150                    fn = ';filename="%s"' % value[0]
151                    value = value[1]
152                else:
153                    fn = ""
154
155                body.write(sep_boundary)
156                body.write('\r\nContent-Disposition: form-data; name="%s"' % key)
157                body.write(fn)
158                body.write("\r\n\r\n")
159                body.write(value)
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