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 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 = '\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