1 | """distutils.command.upload
|
---|
2 |
|
---|
3 | Implements the Distutils 'upload' subcommand (upload package to PyPI)."""
|
---|
4 | import os
|
---|
5 | import socket
|
---|
6 | import platform
|
---|
7 | from urllib2 import urlopen, Request, HTTPError
|
---|
8 | from base64 import standard_b64encode
|
---|
9 | import urlparse
|
---|
10 | import cStringIO as StringIO
|
---|
11 | from hashlib import md5
|
---|
12 |
|
---|
13 | from distutils.errors import DistutilsOptionError
|
---|
14 | from distutils.core import PyPIRCCommand
|
---|
15 | from distutils.spawn import spawn
|
---|
16 | from distutils import log
|
---|
17 |
|
---|
18 | class 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)
|
---|