• 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            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