blob: 782446660583d3249c973f0c7ec1f9b915045b24 [file] [log] [blame]
Jason R. Coombs7ae0fde2014-05-10 13:20:28 -04001"""
2distutils.command.upload
Martin v. Löwis55f1bb82005-03-21 20:56:35 +00003
Jason R. Coombs7ae0fde2014-05-10 13:20:28 -04004Implements the Distutils 'upload' subcommand (upload package to a package
5index).
6"""
Martin v. Löwis98858c92005-03-21 21:00:59 +00007
Tarek Ziadé36797272010-07-22 12:50:05 +00008import sys
Jason R. Coombs7ae0fde2014-05-10 13:20:28 -04009import os
10import io
Tarek Ziadé36797272010-07-22 12:50:05 +000011import platform
Tarek Ziadé36797272010-07-22 12:50:05 +000012from base64 import standard_b64encode
Jason R. Coombsa2ebfd02013-11-10 18:50:10 -050013from urllib.request import urlopen, Request, HTTPError
14from urllib.parse import urlparse
Jason R. Coombs6f717262014-05-10 13:21:02 -040015from distutils.errors import DistutilsOptionError
Jason R. Coombs7ae0fde2014-05-10 13:20:28 -040016from distutils.core import PyPIRCCommand
17from distutils.spawn import spawn
18from distutils import log
Tarek Ziadé36797272010-07-22 12:50:05 +000019
20# this keeps compatibility for 2.3 and 2.4
21if sys.version < "2.5":
22 from md5 import md5
23else:
24 from hashlib import md5
Tarek Ziadé38e3d512009-02-27 12:58:56 +000025
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000026class upload(PyPIRCCommand):
Martin v. Löwis98858c92005-03-21 21:00:59 +000027
28 description = "upload binary package to PyPI"
29
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000030 user_options = PyPIRCCommand.user_options + [
Martin v. Löwisf74b9232005-03-22 15:51:14 +000031 ('sign', 's',
32 'sign files to upload using gpg'),
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000033 ('identity=', 'i', 'GPG identity used to sign files'),
Martin v. Löwis98858c92005-03-21 21:00:59 +000034 ]
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000035
36 boolean_options = PyPIRCCommand.boolean_options + ['sign']
Martin v. Löwis98858c92005-03-21 21:00:59 +000037
38 def initialize_options(self):
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000039 PyPIRCCommand.initialize_options(self)
Martin v. Löwis98858c92005-03-21 21:00:59 +000040 self.username = ''
41 self.password = ''
Martin v. Löwis98858c92005-03-21 21:00:59 +000042 self.show_response = 0
Martin v. Löwisf74b9232005-03-22 15:51:14 +000043 self.sign = False
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000044 self.identity = None
Martin v. Löwis98858c92005-03-21 21:00:59 +000045
46 def finalize_options(self):
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000047 PyPIRCCommand.finalize_options(self)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000048 if self.identity and not self.sign:
49 raise DistutilsOptionError(
50 "Must use --sign for --identity to have meaning"
51 )
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000052 config = self._read_pypirc()
53 if config != {}:
54 self.username = config['username']
55 self.password = config['password']
56 self.repository = config['repository']
57 self.realm = config['realm']
Martin v. Löwis98858c92005-03-21 21:00:59 +000058
Tarek Ziadé13f7c3b2009-01-09 00:15:45 +000059 # getting the password from the distribution
60 # if previously set by the register command
61 if not self.password and self.distribution.password:
62 self.password = self.distribution.password
63
Martin v. Löwis98858c92005-03-21 21:00:59 +000064 def run(self):
65 if not self.distribution.dist_files:
66 raise DistutilsOptionError("No dist file created in earlier command")
Martin v. Löwis98da5622005-03-23 18:54:36 +000067 for command, pyversion, filename in self.distribution.dist_files:
68 self.upload_file(command, pyversion, filename)
Martin v. Löwis98858c92005-03-21 21:00:59 +000069
Martin v. Löwis98da5622005-03-23 18:54:36 +000070 def upload_file(self, command, pyversion, filename):
Jason R. Coombsa2ebfd02013-11-10 18:50:10 -050071 # Makes sure the repository URL is compliant
72 schema, netloc, url, params, query, fragments = \
73 urlparse(self.repository)
74 if params or query or fragments:
75 raise AssertionError("Incompatible url %s" % self.repository)
76
77 if schema not in ('http', 'https'):
78 raise AssertionError("unsupported schema " + schema)
79
Martin v. Löwisf74b9232005-03-22 15:51:14 +000080 # Sign if requested
81 if self.sign:
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000082 gpg_args = ["gpg", "--detach-sign", "-a", filename]
83 if self.identity:
84 gpg_args[2:2] = ["--local-user", self.identity]
85 spawn(gpg_args,
Martin v. Löwisf74b9232005-03-22 15:51:14 +000086 dry_run=self.dry_run)
Martin v. Löwis98858c92005-03-21 21:00:59 +000087
Martin v. Löwis6d0c85a2006-01-08 10:48:54 +000088 # Fill in the data - send all the meta-data in case we need to
89 # register a new release
Éric Araujobee5cef2010-11-05 23:51:56 +000090 f = open(filename,'rb')
91 try:
92 content = f.read()
93 finally:
94 f.close()
Martin v. Löwis6d0c85a2006-01-08 10:48:54 +000095 meta = self.distribution.metadata
Martin v. Löwis98858c92005-03-21 21:00:59 +000096 data = {
Martin v. Löwis6d0c85a2006-01-08 10:48:54 +000097 # action
98 ':action': 'file_upload',
99 'protcol_version': '1',
100
101 # identify release
102 'name': meta.get_name(),
103 'version': meta.get_version(),
104
105 # file content
106 'content': (os.path.basename(filename),content),
107 'filetype': command,
108 'pyversion': pyversion,
109 'md5_digest': md5(content).hexdigest(),
110
111 # additional meta-data
Jason R. Coombs7ae0fde2014-05-10 13:20:28 -0400112 'metadata_version': '1.0',
Martin v. Löwis6d0c85a2006-01-08 10:48:54 +0000113 'summary': meta.get_description(),
114 'home_page': meta.get_url(),
115 'author': meta.get_contact(),
116 'author_email': meta.get_contact_email(),
117 'license': meta.get_licence(),
118 'description': meta.get_long_description(),
119 'keywords': meta.get_keywords(),
120 'platform': meta.get_platforms(),
121 'classifiers': meta.get_classifiers(),
122 'download_url': meta.get_download_url(),
123 # PEP 314
124 'provides': meta.get_provides(),
125 'requires': meta.get_requires(),
126 'obsoletes': meta.get_obsoletes(),
Martin v. Löwis98858c92005-03-21 21:00:59 +0000127 }
128 comment = ''
129 if command == 'bdist_rpm':
130 dist, version, id = platform.dist()
131 if dist:
132 comment = 'built for %s %s' % (dist, version)
133 elif command == 'bdist_dumb':
134 comment = 'built for %s' % platform.platform(terse=1)
135 data['comment'] = comment
136
Martin v. Löwisf74b9232005-03-22 15:51:14 +0000137 if self.sign:
138 data['gpg_signature'] = (os.path.basename(filename) + ".asc",
Antoine Pitrou24319ac2012-06-29 01:05:26 +0200139 open(filename+".asc", "rb").read())
Martin v. Löwisf74b9232005-03-22 15:51:14 +0000140
Martin v. Löwis98858c92005-03-21 21:00:59 +0000141 # set up the authentication
Amaury Forgeot d'Arc836b6702008-11-20 23:53:46 +0000142 user_pass = (self.username + ":" + self.password).encode('ascii')
143 # The exact encoding of the authentication string is debated.
144 # Anyway PyPI only accepts ascii for both username or password.
Tarek Ziadé8b9361a2009-12-21 00:02:20 +0000145 auth = "Basic " + standard_b64encode(user_pass).decode('ascii')
Martin v. Löwis98858c92005-03-21 21:00:59 +0000146
147 # Build up the MIME payload for the POST data
148 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
Amaury Forgeot d'Arc836b6702008-11-20 23:53:46 +0000149 sep_boundary = b'\n--' + boundary.encode('ascii')
150 end_boundary = sep_boundary + b'--'
151 body = io.BytesIO()
Martin v. Löwis98858c92005-03-21 21:00:59 +0000152 for key, value in data.items():
Amaury Forgeot d'Arc836b6702008-11-20 23:53:46 +0000153 title = '\nContent-Disposition: form-data; name="%s"' % key
Martin v. Löwis98858c92005-03-21 21:00:59 +0000154 # handle multiple entries for the same name
Tarek Ziadé36797272010-07-22 12:50:05 +0000155 if type(value) != type([]):
Martin v. Löwis98858c92005-03-21 21:00:59 +0000156 value = [value]
157 for value in value:
Tarek Ziadé36797272010-07-22 12:50:05 +0000158 if type(value) is tuple:
Amaury Forgeot d'Arc836b6702008-11-20 23:53:46 +0000159 title += '; filename="%s"' % value[0]
Martin v. Löwis98858c92005-03-21 21:00:59 +0000160 value = value[1]
161 else:
Amaury Forgeot d'Arc836b6702008-11-20 23:53:46 +0000162 value = str(value).encode('utf-8')
Martin v. Löwis98858c92005-03-21 21:00:59 +0000163 body.write(sep_boundary)
Amaury Forgeot d'Arc836b6702008-11-20 23:53:46 +0000164 body.write(title.encode('utf-8'))
165 body.write(b"\n\n")
Martin v. Löwis98858c92005-03-21 21:00:59 +0000166 body.write(value)
Amaury Forgeot d'Arc836b6702008-11-20 23:53:46 +0000167 if value and value[-1:] == b'\r':
168 body.write(b'\n') # write an extra newline (lurve Macs)
Martin v. Löwis98858c92005-03-21 21:00:59 +0000169 body.write(end_boundary)
Amaury Forgeot d'Arc836b6702008-11-20 23:53:46 +0000170 body.write(b"\n")
Martin v. Löwis98858c92005-03-21 21:00:59 +0000171 body = body.getvalue()
172
Jason R. Coombs7ae0fde2014-05-10 13:20:28 -0400173 msg = "Submitting %s to %s" % (filename, self.repository)
174 self.announce(msg, log.INFO)
Martin v. Löwis98858c92005-03-21 21:00:59 +0000175
176 # build the Request
Jason R. Coombs7ae0fde2014-05-10 13:20:28 -0400177 headers = {
178 'Content-type': 'multipart/form-data; boundary=%s' % boundary,
179 'Content-length': str(len(body)),
180 'Authorization': auth,
181 }
Martin v. Löwis98858c92005-03-21 21:00:59 +0000182
Jason R. Coombsa2ebfd02013-11-10 18:50:10 -0500183 request = Request(self.repository, data=body,
184 headers=headers)
185 # send the data
Martin v. Löwis98858c92005-03-21 21:00:59 +0000186 try:
Jason R. Coombsa2ebfd02013-11-10 18:50:10 -0500187 result = urlopen(request)
188 status = result.getcode()
189 reason = result.msg
Andrew Svetlov0832af62012-12-18 23:10:48 +0200190 except OSError as e:
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000191 self.announce(str(e), log.ERROR)
Martin v. Löwis98858c92005-03-21 21:00:59 +0000192 return
Jason R. Coombsa2ebfd02013-11-10 18:50:10 -0500193 except HTTPError as e:
194 status = e.code
195 reason = e.msg
Martin v. Löwis98858c92005-03-21 21:00:59 +0000196
Jason R. Coombsa2ebfd02013-11-10 18:50:10 -0500197 if status == 200:
198 self.announce('Server response (%s): %s' % (status, reason),
Martin v. Löwis98858c92005-03-21 21:00:59 +0000199 log.INFO)
200 else:
Jason R. Coombsa2ebfd02013-11-10 18:50:10 -0500201 self.announce('Upload failed (%s): %s' % (status, reason),
Martin v. Löwisf74b9232005-03-22 15:51:14 +0000202 log.ERROR)
Martin v. Löwis98858c92005-03-21 21:00:59 +0000203 if self.show_response:
Antoine Pitrou335a5122013-12-22 18:13:51 +0100204 text = self._read_pypi_response(result)
205 msg = '\n'.join(('-' * 75, text, '-' * 75))
Éric Araujo480504b2010-09-07 23:08:57 +0000206 self.announce(msg, log.INFO)