|
1 """distutils.command.upload |
|
2 |
|
3 Implements the Distutils 'upload' subcommand (upload package to PyPI).""" |
|
4 |
|
5 from distutils.errors import * |
|
6 from distutils.core import Command |
|
7 from distutils.spawn import spawn |
|
8 from distutils import log |
|
9 from hashlib import md5 |
|
10 import os |
|
11 import socket |
|
12 import platform |
|
13 import ConfigParser |
|
14 import httplib |
|
15 import base64 |
|
16 import urlparse |
|
17 import cStringIO as StringIO |
|
18 |
|
19 class upload(Command): |
|
20 |
|
21 description = "upload binary package to PyPI" |
|
22 |
|
23 DEFAULT_REPOSITORY = 'http://pypi.python.org/pypi' |
|
24 |
|
25 user_options = [ |
|
26 ('repository=', 'r', |
|
27 "url of repository [default: %s]" % DEFAULT_REPOSITORY), |
|
28 ('show-response', None, |
|
29 'display full response text from server'), |
|
30 ('sign', 's', |
|
31 'sign files to upload using gpg'), |
|
32 ('identity=', 'i', 'GPG identity used to sign files'), |
|
33 ] |
|
34 boolean_options = ['show-response', 'sign'] |
|
35 |
|
36 def initialize_options(self): |
|
37 self.username = '' |
|
38 self.password = '' |
|
39 self.repository = '' |
|
40 self.show_response = 0 |
|
41 self.sign = False |
|
42 self.identity = None |
|
43 |
|
44 def finalize_options(self): |
|
45 if self.identity and not self.sign: |
|
46 raise DistutilsOptionError( |
|
47 "Must use --sign for --identity to have meaning" |
|
48 ) |
|
49 if os.environ.has_key('HOME'): |
|
50 rc = os.path.join(os.environ['HOME'], '.pypirc') |
|
51 if os.path.exists(rc): |
|
52 self.announce('Using PyPI login from %s' % rc) |
|
53 config = ConfigParser.ConfigParser({ |
|
54 'username':'', |
|
55 'password':'', |
|
56 'repository':''}) |
|
57 config.read(rc) |
|
58 if not self.repository: |
|
59 self.repository = config.get('server-login', 'repository') |
|
60 if not self.username: |
|
61 self.username = config.get('server-login', 'username') |
|
62 if not self.password: |
|
63 self.password = config.get('server-login', 'password') |
|
64 if not self.repository: |
|
65 self.repository = self.DEFAULT_REPOSITORY |
|
66 |
|
67 def run(self): |
|
68 if not self.distribution.dist_files: |
|
69 raise DistutilsOptionError("No dist file created in earlier command") |
|
70 for command, pyversion, filename in self.distribution.dist_files: |
|
71 self.upload_file(command, pyversion, filename) |
|
72 |
|
73 def upload_file(self, command, pyversion, filename): |
|
74 # Sign if requested |
|
75 if self.sign: |
|
76 gpg_args = ["gpg", "--detach-sign", "-a", filename] |
|
77 if self.identity: |
|
78 gpg_args[2:2] = ["--local-user", self.identity] |
|
79 spawn(gpg_args, |
|
80 dry_run=self.dry_run) |
|
81 |
|
82 # Fill in the data - send all the meta-data in case we need to |
|
83 # register a new release |
|
84 content = open(filename,'rb').read() |
|
85 meta = self.distribution.metadata |
|
86 data = { |
|
87 # action |
|
88 ':action': 'file_upload', |
|
89 'protcol_version': '1', |
|
90 |
|
91 # identify release |
|
92 'name': meta.get_name(), |
|
93 'version': meta.get_version(), |
|
94 |
|
95 # file content |
|
96 'content': (os.path.basename(filename),content), |
|
97 'filetype': command, |
|
98 'pyversion': pyversion, |
|
99 'md5_digest': md5(content).hexdigest(), |
|
100 |
|
101 # additional meta-data |
|
102 'metadata_version' : '1.0', |
|
103 'summary': meta.get_description(), |
|
104 'home_page': meta.get_url(), |
|
105 'author': meta.get_contact(), |
|
106 'author_email': meta.get_contact_email(), |
|
107 'license': meta.get_licence(), |
|
108 'description': meta.get_long_description(), |
|
109 'keywords': meta.get_keywords(), |
|
110 'platform': meta.get_platforms(), |
|
111 'classifiers': meta.get_classifiers(), |
|
112 'download_url': meta.get_download_url(), |
|
113 # PEP 314 |
|
114 'provides': meta.get_provides(), |
|
115 'requires': meta.get_requires(), |
|
116 'obsoletes': meta.get_obsoletes(), |
|
117 } |
|
118 comment = '' |
|
119 if command == 'bdist_rpm': |
|
120 dist, version, id = platform.dist() |
|
121 if dist: |
|
122 comment = 'built for %s %s' % (dist, version) |
|
123 elif command == 'bdist_dumb': |
|
124 comment = 'built for %s' % platform.platform(terse=1) |
|
125 data['comment'] = comment |
|
126 |
|
127 if self.sign: |
|
128 data['gpg_signature'] = (os.path.basename(filename) + ".asc", |
|
129 open(filename+".asc").read()) |
|
130 |
|
131 # set up the authentication |
|
132 auth = "Basic " + base64.encodestring(self.username + ":" + self.password).strip() |
|
133 |
|
134 # Build up the MIME payload for the POST data |
|
135 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' |
|
136 sep_boundary = '\n--' + boundary |
|
137 end_boundary = sep_boundary + '--' |
|
138 body = StringIO.StringIO() |
|
139 for key, value in data.items(): |
|
140 # handle multiple entries for the same name |
|
141 if type(value) != type([]): |
|
142 value = [value] |
|
143 for value in value: |
|
144 if type(value) is tuple: |
|
145 fn = ';filename="%s"' % value[0] |
|
146 value = value[1] |
|
147 else: |
|
148 fn = "" |
|
149 value = str(value) |
|
150 body.write(sep_boundary) |
|
151 body.write('\nContent-Disposition: form-data; name="%s"'%key) |
|
152 body.write(fn) |
|
153 body.write("\n\n") |
|
154 body.write(value) |
|
155 if value and value[-1] == '\r': |
|
156 body.write('\n') # write an extra newline (lurve Macs) |
|
157 body.write(end_boundary) |
|
158 body.write("\n") |
|
159 body = body.getvalue() |
|
160 |
|
161 self.announce("Submitting %s to %s" % (filename, self.repository), log.INFO) |
|
162 |
|
163 # build the Request |
|
164 # We can't use urllib2 since we need to send the Basic |
|
165 # auth right with the first request |
|
166 schema, netloc, url, params, query, fragments = \ |
|
167 urlparse.urlparse(self.repository) |
|
168 assert not params and not query and not fragments |
|
169 if schema == 'http': |
|
170 http = httplib.HTTPConnection(netloc) |
|
171 elif schema == 'https': |
|
172 http = httplib.HTTPSConnection(netloc) |
|
173 else: |
|
174 raise AssertionError, "unsupported schema "+schema |
|
175 |
|
176 data = '' |
|
177 loglevel = log.INFO |
|
178 try: |
|
179 http.connect() |
|
180 http.putrequest("POST", url) |
|
181 http.putheader('Content-type', |
|
182 'multipart/form-data; boundary=%s'%boundary) |
|
183 http.putheader('Content-length', str(len(body))) |
|
184 http.putheader('Authorization', auth) |
|
185 http.endheaders() |
|
186 http.send(body) |
|
187 except socket.error, e: |
|
188 self.announce(str(e), log.ERROR) |
|
189 return |
|
190 |
|
191 r = http.getresponse() |
|
192 if r.status == 200: |
|
193 self.announce('Server response (%s): %s' % (r.status, r.reason), |
|
194 log.INFO) |
|
195 else: |
|
196 self.announce('Upload failed (%s): %s' % (r.status, r.reason), |
|
197 log.ERROR) |
|
198 if self.show_response: |
|
199 print '-'*75, r.read(), '-'*75 |