blob: 644c400aaa380de9911eca2ff056c2eca4654baf [file] [log] [blame]
Phillip J. Eby069159b2006-04-18 04:05:34 +00001"""distutils.command.upload
2
3Implements the Distutils 'upload' subcommand (upload package to PyPI)."""
4
5from distutils.errors import *
6from distutils.core import Command
7from distutils.spawn import spawn
8from distutils import log
9from md5 import md5
10import os
11import socket
12import platform
13import ConfigParser
14import httplib
15import base64
16import urlparse
17import cStringIO as StringIO
18
19class upload(Command):
20
21 description = "upload binary package to PyPI"
22
23 DEFAULT_REPOSITORY = 'http://www.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
83 content = open(filename,'rb').read()
84 basename = os.path.basename(filename)
85 comment = ''
86 if command=='bdist_egg' and self.distribution.has_ext_modules():
87 comment = "built on %s" % platform.platform(terse=1)
88 data = {
89 ':action':'file_upload',
90 'protcol_version':'1',
91 'name':self.distribution.get_name(),
92 'version':self.distribution.get_version(),
93 'content':(basename,content),
94 'filetype':command,
95 'pyversion':pyversion,
96 'md5_digest':md5(content).hexdigest(),
97 }
98 if command == 'bdist_rpm':
99 dist, version, id = platform.dist()
100 if dist:
101 comment = 'built for %s %s' % (dist, version)
102 elif command == 'bdist_dumb':
103 comment = 'built for %s' % platform.platform(terse=1)
104 data['comment'] = comment
105
106 if self.sign:
107 data['gpg_signature'] = (os.path.basename(filename) + ".asc",
108 open(filename+".asc").read())
109
110 # set up the authentication
111 auth = "Basic " + base64.encodestring(self.username + ":" + self.password).strip()
112
113 # Build up the MIME payload for the POST data
114 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
115 sep_boundary = '\n--' + boundary
116 end_boundary = sep_boundary + '--'
117 body = StringIO.StringIO()
118 for key, value in data.items():
119 # handle multiple entries for the same name
120 if type(value) != type([]):
121 value = [value]
122 for value in value:
123 if type(value) is tuple:
124 fn = ';filename="%s"' % value[0]
125 value = value[1]
126 else:
127 fn = ""
128 value = str(value)
129 body.write(sep_boundary)
130 body.write('\nContent-Disposition: form-data; name="%s"'%key)
131 body.write(fn)
132 body.write("\n\n")
133 body.write(value)
134 if value and value[-1] == '\r':
135 body.write('\n') # write an extra newline (lurve Macs)
136 body.write(end_boundary)
137 body.write("\n")
138 body = body.getvalue()
139
140 self.announce("Submitting %s to %s" % (filename, self.repository), log.INFO)
141
142 # build the Request
143 # We can't use urllib2 since we need to send the Basic
144 # auth right with the first request
145 schema, netloc, url, params, query, fragments = \
146 urlparse.urlparse(self.repository)
147 assert not params and not query and not fragments
148 if schema == 'http':
149 http = httplib.HTTPConnection(netloc)
150 elif schema == 'https':
151 http = httplib.HTTPSConnection(netloc)
152 else:
153 raise AssertionError, "unsupported schema "+schema
154
155 data = ''
156 loglevel = log.INFO
157 try:
158 http.connect()
159 http.putrequest("POST", url)
160 http.putheader('Content-type',
161 'multipart/form-data; boundary=%s'%boundary)
162 http.putheader('Content-length', str(len(body)))
163 http.putheader('Authorization', auth)
164 http.endheaders()
165 http.send(body)
166 except socket.error, e:
167 self.announce(e.msg, log.ERROR)
168 return
169
170 r = http.getresponse()
171 if r.status == 200:
172 self.announce('Server response (%s): %s' % (r.status, r.reason),
173 log.INFO)
174 else:
175 self.announce('Upload failed (%s): %s' % (r.status, r.reason),
176 log.ERROR)
177 if self.show_response:
178 print '-'*75, r.read(), '-'*75