• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2distutils.command.upload
3
4Implements the Distutils 'upload' subcommand (upload package to a package
5index).
6"""
7
8import os
9import io
10import platform
11import hashlib
12from base64 import standard_b64encode
13from urllib.request import urlopen, Request, HTTPError
14from urllib.parse import urlparse
15from distutils.errors import DistutilsError, DistutilsOptionError
16from distutils.core import PyPIRCCommand
17from distutils.spawn import spawn
18from distutils import log
19
20class upload(PyPIRCCommand):
21
22    description = "upload binary package to PyPI"
23
24    user_options = PyPIRCCommand.user_options + [
25        ('sign', 's',
26         'sign files to upload using gpg'),
27        ('identity=', 'i', 'GPG identity used to sign files'),
28        ]
29
30    boolean_options = PyPIRCCommand.boolean_options + ['sign']
31
32    def initialize_options(self):
33        PyPIRCCommand.initialize_options(self)
34        self.username = ''
35        self.password = ''
36        self.show_response = 0
37        self.sign = False
38        self.identity = None
39
40    def finalize_options(self):
41        PyPIRCCommand.finalize_options(self)
42        if self.identity and not self.sign:
43            raise DistutilsOptionError(
44                "Must use --sign for --identity to have meaning"
45            )
46        config = self._read_pypirc()
47        if config != {}:
48            self.username = config['username']
49            self.password = config['password']
50            self.repository = config['repository']
51            self.realm = config['realm']
52
53        # getting the password from the distribution
54        # if previously set by the register command
55        if not self.password and self.distribution.password:
56            self.password = self.distribution.password
57
58    def run(self):
59        if not self.distribution.dist_files:
60            msg = ("Must create and upload files in one command "
61                   "(e.g. setup.py sdist upload)")
62            raise DistutilsOptionError(msg)
63        for command, pyversion, filename in self.distribution.dist_files:
64            self.upload_file(command, pyversion, filename)
65
66    def upload_file(self, command, pyversion, filename):
67        # Makes sure the repository URL is compliant
68        schema, netloc, url, params, query, fragments = \
69            urlparse(self.repository)
70        if params or query or fragments:
71            raise AssertionError("Incompatible url %s" % self.repository)
72
73        if schema not in ('http', 'https'):
74            raise AssertionError("unsupported schema " + schema)
75
76        # Sign if requested
77        if self.sign:
78            gpg_args = ["gpg", "--detach-sign", "-a", filename]
79            if self.identity:
80                gpg_args[2:2] = ["--local-user", self.identity]
81            spawn(gpg_args,
82                  dry_run=self.dry_run)
83
84        # Fill in the data - send all the meta-data in case we need to
85        # register a new release
86        f = open(filename,'rb')
87        try:
88            content = f.read()
89        finally:
90            f.close()
91        meta = self.distribution.metadata
92        data = {
93            # action
94            ':action': 'file_upload',
95            'protocol_version': '1',
96
97            # identify release
98            'name': meta.get_name(),
99            'version': meta.get_version(),
100
101            # file content
102            'content': (os.path.basename(filename),content),
103            'filetype': command,
104            'pyversion': pyversion,
105            'md5_digest': hashlib.md5(content).hexdigest(),
106
107            # additional meta-data
108            'metadata_version': '1.0',
109            'summary': meta.get_description(),
110            'home_page': meta.get_url(),
111            'author': meta.get_contact(),
112            'author_email': meta.get_contact_email(),
113            'license': meta.get_licence(),
114            'description': meta.get_long_description(),
115            'keywords': meta.get_keywords(),
116            'platform': meta.get_platforms(),
117            'classifiers': meta.get_classifiers(),
118            'download_url': meta.get_download_url(),
119            # PEP 314
120            'provides': meta.get_provides(),
121            'requires': meta.get_requires(),
122            'obsoletes': meta.get_obsoletes(),
123            }
124        comment = ''
125        if command == 'bdist_rpm':
126            dist, version, id = platform.dist()
127            if dist:
128                comment = 'built for %s %s' % (dist, version)
129        elif command == 'bdist_dumb':
130            comment = 'built for %s' % platform.platform(terse=1)
131        data['comment'] = comment
132
133        if self.sign:
134            data['gpg_signature'] = (os.path.basename(filename) + ".asc",
135                                     open(filename+".asc", "rb").read())
136
137        # set up the authentication
138        user_pass = (self.username + ":" + self.password).encode('ascii')
139        # The exact encoding of the authentication string is debated.
140        # Anyway PyPI only accepts ascii for both username or password.
141        auth = "Basic " + standard_b64encode(user_pass).decode('ascii')
142
143        # Build up the MIME payload for the POST data
144        boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
145        sep_boundary = b'\r\n--' + boundary.encode('ascii')
146        end_boundary = sep_boundary + b'--\r\n'
147        body = io.BytesIO()
148        for key, value in data.items():
149            title = '\r\nContent-Disposition: form-data; name="%s"' % key
150            # handle multiple entries for the same name
151            if not isinstance(value, list):
152                value = [value]
153            for value in value:
154                if type(value) is tuple:
155                    title += '; filename="%s"' % value[0]
156                    value = value[1]
157                else:
158                    value = str(value).encode('utf-8')
159                body.write(sep_boundary)
160                body.write(title.encode('utf-8'))
161                body.write(b"\r\n\r\n")
162                body.write(value)
163        body.write(end_boundary)
164        body = body.getvalue()
165
166        msg = "Submitting %s to %s" % (filename, self.repository)
167        self.announce(msg, log.INFO)
168
169        # build the Request
170        headers = {
171            'Content-type': 'multipart/form-data; boundary=%s' % boundary,
172            'Content-length': str(len(body)),
173            'Authorization': auth,
174        }
175
176        request = Request(self.repository, data=body,
177                          headers=headers)
178        # send the data
179        try:
180            result = urlopen(request)
181            status = result.getcode()
182            reason = result.msg
183        except HTTPError as e:
184            status = e.code
185            reason = e.msg
186        except OSError as e:
187            self.announce(str(e), log.ERROR)
188            raise
189
190        if status == 200:
191            self.announce('Server response (%s): %s' % (status, reason),
192                          log.INFO)
193            if self.show_response:
194                text = self._read_pypi_response(result)
195                msg = '\n'.join(('-' * 75, text, '-' * 75))
196                self.announce(msg, log.INFO)
197        else:
198            msg = 'Upload failed (%s): %s' % (status, reason)
199            self.announce(msg, log.ERROR)
200            raise DistutilsError(msg)
201