• 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
125        data['comment'] = ''
126
127        if self.sign:
128            with open(filename + ".asc", "rb") as f:
129                data['gpg_signature'] = (os.path.basename(filename) + ".asc",
130                                         f.read())
131
132        # set up the authentication
133        user_pass = (self.username + ":" + self.password).encode('ascii')
134        # The exact encoding of the authentication string is debated.
135        # Anyway PyPI only accepts ascii for both username or password.
136        auth = "Basic " + standard_b64encode(user_pass).decode('ascii')
137
138        # Build up the MIME payload for the POST data
139        boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
140        sep_boundary = b'\r\n--' + boundary.encode('ascii')
141        end_boundary = sep_boundary + b'--\r\n'
142        body = io.BytesIO()
143        for key, value in data.items():
144            title = '\r\nContent-Disposition: form-data; name="%s"' % key
145            # handle multiple entries for the same name
146            if not isinstance(value, list):
147                value = [value]
148            for value in value:
149                if type(value) is tuple:
150                    title += '; filename="%s"' % value[0]
151                    value = value[1]
152                else:
153                    value = str(value).encode('utf-8')
154                body.write(sep_boundary)
155                body.write(title.encode('utf-8'))
156                body.write(b"\r\n\r\n")
157                body.write(value)
158        body.write(end_boundary)
159        body = body.getvalue()
160
161        msg = "Submitting %s to %s" % (filename, self.repository)
162        self.announce(msg, log.INFO)
163
164        # build the Request
165        headers = {
166            'Content-type': 'multipart/form-data; boundary=%s' % boundary,
167            'Content-length': str(len(body)),
168            'Authorization': auth,
169        }
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        except HTTPError as e:
179            status = e.code
180            reason = e.msg
181        except OSError as e:
182            self.announce(str(e), log.ERROR)
183            raise
184
185        if status == 200:
186            self.announce('Server response (%s): %s' % (status, reason),
187                          log.INFO)
188            if self.show_response:
189                text = self._read_pypi_response(result)
190                msg = '\n'.join(('-' * 75, text, '-' * 75))
191                self.announce(msg, log.INFO)
192        else:
193            msg = 'Upload failed (%s): %s' % (status, reason)
194            self.announce(msg, log.ERROR)
195            raise DistutilsError(msg)
196