|
| 1 | +"""distutils.command.register |
| 2 | +
|
| 3 | +Implements the Distutils 'register' command (register with the repository). |
| 4 | +""" |
| 5 | + |
| 6 | +# created 2002/10/21, Richard Jones |
| 7 | + |
| 8 | +__revision__ = "$Id$" |
| 9 | + |
| 10 | +import sys, os, string, urllib2, getpass, urlparse |
| 11 | +import StringIO, ConfigParser |
| 12 | + |
| 13 | +from distutils.core import Command |
| 14 | +from distutils.errors import * |
| 15 | + |
| 16 | +class register(Command): |
| 17 | + |
| 18 | + description = "register the distribution with the repository" |
| 19 | + |
| 20 | + # XXX must update this to python.org before 2.3final! |
| 21 | + DEFAULT_REPOSITORY = 'http://www.amk.ca/cgi-bin/pypi.cgi' |
| 22 | + |
| 23 | + user_options = [ |
| 24 | + ('repository=', 'r', |
| 25 | + "url of repository [default: %s]"%DEFAULT_REPOSITORY), |
| 26 | + ('verify', None, |
| 27 | + 'verify the package metadata for correctness'), |
| 28 | + ('list-classifiers', None, |
| 29 | + 'list the valid Trove classifiers'), |
| 30 | + ('verbose', None, |
| 31 | + 'display full response from server'), |
| 32 | + ] |
| 33 | + boolean_options = ['verify', 'verbose', 'list-classifiers'] |
| 34 | + |
| 35 | + def initialize_options(self): |
| 36 | + self.repository = None |
| 37 | + self.verify = 0 |
| 38 | + self.verbose = 0 |
| 39 | + self.list_classifiers = 0 |
| 40 | + |
| 41 | + def finalize_options(self): |
| 42 | + if self.repository is None: |
| 43 | + self.repository = self.DEFAULT_REPOSITORY |
| 44 | + |
| 45 | + def run(self): |
| 46 | + self.check_metadata() |
| 47 | + if self.verify: |
| 48 | + self.verify_metadata() |
| 49 | + elif self.list_classifiers: |
| 50 | + self.classifiers() |
| 51 | + else: |
| 52 | + self.send_metadata() |
| 53 | + |
| 54 | + def check_metadata(self): |
| 55 | + """Ensure that all required elements of meta-data (name, version, |
| 56 | + URL, (author and author_email) or (maintainer and |
| 57 | + maintainer_email)) are supplied by the Distribution object; warn if |
| 58 | + any are missing. |
| 59 | + """ |
| 60 | + metadata = self.distribution.metadata |
| 61 | + |
| 62 | + missing = [] |
| 63 | + for attr in ('name', 'version', 'url'): |
| 64 | + if not (hasattr(metadata, attr) and getattr(metadata, attr)): |
| 65 | + missing.append(attr) |
| 66 | + |
| 67 | + if missing: |
| 68 | + self.warn("missing required meta-data: " + |
| 69 | + string.join(missing, ", ")) |
| 70 | + |
| 71 | + if metadata.author: |
| 72 | + if not metadata.author_email: |
| 73 | + self.warn("missing meta-data: if 'author' supplied, " + |
| 74 | + "'author_email' must be supplied too") |
| 75 | + elif metadata.maintainer: |
| 76 | + if not metadata.maintainer_email: |
| 77 | + self.warn("missing meta-data: if 'maintainer' supplied, " + |
| 78 | + "'maintainer_email' must be supplied too") |
| 79 | + else: |
| 80 | + self.warn("missing meta-data: either (author and author_email) " + |
| 81 | + "or (maintainer and maintainer_email) " + |
| 82 | + "must be supplied") |
| 83 | + |
| 84 | + def classifiers(self): |
| 85 | + ''' Fetch the list of classifiers from the server. |
| 86 | + ''' |
| 87 | + response = urllib2.urlopen(self.repository+'?:action=list_classifiers') |
| 88 | + print response.read() |
| 89 | + |
| 90 | + def verify_metadata(self): |
| 91 | + ''' Send the metadata to the package index server to be checked. |
| 92 | + ''' |
| 93 | + # send the info to the server and report the result |
| 94 | + (code, result) = self.post_to_server(self.build_post_data('verify')) |
| 95 | + print 'Server response (%s): %s'%(code, result) |
| 96 | + |
| 97 | + def send_metadata(self): |
| 98 | + ''' Send the metadata to the package index server. |
| 99 | +
|
| 100 | + Well, do the following: |
| 101 | + 1. figure who the user is, and then |
| 102 | + 2. send the data as a Basic auth'ed POST. |
| 103 | +
|
| 104 | + First we try to read the username/password from $HOME/.pypirc, |
| 105 | + which is a ConfigParser-formatted file with a section |
| 106 | + [server-login] containing username and password entries (both |
| 107 | + in clear text). Eg: |
| 108 | +
|
| 109 | + [server-login] |
| 110 | + username: fred |
| 111 | + password: sekrit |
| 112 | +
|
| 113 | + Otherwise, to figure who the user is, we offer the user three |
| 114 | + choices: |
| 115 | +
|
| 116 | + 1. use existing login, |
| 117 | + 2. register as a new user, or |
| 118 | + 3. set the password to a random string and email the user. |
| 119 | +
|
| 120 | + ''' |
| 121 | + choice = 'x' |
| 122 | + username = password = '' |
| 123 | + |
| 124 | + # see if we can short-cut and get the username/password from the |
| 125 | + # config |
| 126 | + config = None |
| 127 | + if os.environ.has_key('HOME'): |
| 128 | + rc = os.path.join(os.environ['HOME'], '.pypirc') |
| 129 | + if os.path.exists(rc): |
| 130 | + print 'Using PyPI login from %s'%rc |
| 131 | + config = ConfigParser.ConfigParser() |
| 132 | + config.read(rc) |
| 133 | + username = config.get('server-login', 'username') |
| 134 | + password = config.get('server-login', 'password') |
| 135 | + choice = '1' |
| 136 | + |
| 137 | + # get the user's login info |
| 138 | + choices = '1 2 3 4'.split() |
| 139 | + while choice not in choices: |
| 140 | + print '''We need to know who you are, so please choose either: |
| 141 | + 1. use your existing login, |
| 142 | + 2. register as a new user, |
| 143 | + 3. have the server generate a new password for you (and email it to you), or |
| 144 | + 4. quit |
| 145 | +Your selection [default 1]: ''', |
| 146 | + choice = raw_input() |
| 147 | + if not choice: |
| 148 | + choice = '1' |
| 149 | + elif choice not in choices: |
| 150 | + print 'Please choose one of the four options!' |
| 151 | + |
| 152 | + if choice == '1': |
| 153 | + # get the username and password |
| 154 | + while not username: |
| 155 | + username = raw_input('Username: ') |
| 156 | + while not password: |
| 157 | + password = getpass.getpass('Password: ') |
| 158 | + |
| 159 | + # set up the authentication |
| 160 | + auth = urllib2.HTTPPasswordMgr() |
| 161 | + host = urlparse.urlparse(self.repository)[1] |
| 162 | + auth.add_password('pypi', host, username, password) |
| 163 | + |
| 164 | + # send the info to the server and report the result |
| 165 | + code, result = self.post_to_server(self.build_post_data('submit'), |
| 166 | + auth) |
| 167 | + print 'Server response (%s): %s'%(code, result) |
| 168 | + |
| 169 | + # possibly save the login |
| 170 | + if os.environ.has_key('HOME') and config is None and code == 200: |
| 171 | + rc = os.path.join(os.environ['HOME'], '.pypirc') |
| 172 | + print 'I can store your PyPI login so future submissions will be faster.' |
| 173 | + print '(the login will be stored in %s)'%rc |
| 174 | + choice = 'X' |
| 175 | + while choice.lower() not in 'yn': |
| 176 | + choice = raw_input('Save your login (y/N)?') |
| 177 | + if not choice: |
| 178 | + choice = 'n' |
| 179 | + if choice.lower() == 'y': |
| 180 | + f = open(rc, 'w') |
| 181 | + f.write('[server-login]\nusername:%s\npassword:%s\n'%( |
| 182 | + username, password)) |
| 183 | + f.close() |
| 184 | + try: |
| 185 | + os.chmod(rc, 0600) |
| 186 | + except: |
| 187 | + pass |
| 188 | + elif choice == '2': |
| 189 | + data = {':action': 'user'} |
| 190 | + data['name'] = data['password'] = data['email'] = '' |
| 191 | + data['confirm'] = None |
| 192 | + while not data['name']: |
| 193 | + data['name'] = raw_input('Username: ') |
| 194 | + while data['password'] != data['confirm']: |
| 195 | + while not data['password']: |
| 196 | + data['password'] = getpass.getpass('Password: ') |
| 197 | + while not data['confirm']: |
| 198 | + data['confirm'] = getpass.getpass(' Confirm: ') |
| 199 | + if data['password'] != data['confirm']: |
| 200 | + data['password'] = '' |
| 201 | + data['confirm'] = None |
| 202 | + print "Password and confirm don't match!" |
| 203 | + while not data['email']: |
| 204 | + data['email'] = raw_input(' EMail: ') |
| 205 | + code, result = self.post_to_server(data) |
| 206 | + if code != 200: |
| 207 | + print 'Server response (%s): %s'%(code, result) |
| 208 | + else: |
| 209 | + print 'You will receive an email shortly.' |
| 210 | + print 'Follow the instructions in it to complete registration.' |
| 211 | + elif choice == '3': |
| 212 | + data = {':action': 'password_reset'} |
| 213 | + data['email'] = '' |
| 214 | + while not data['email']: |
| 215 | + data['email'] = raw_input('Your email address: ') |
| 216 | + code, result = self.post_to_server(data) |
| 217 | + print 'Server response (%s): %s'%(code, result) |
| 218 | + |
| 219 | + def build_post_data(self, action): |
| 220 | + # figure the data to send - the metadata plus some additional |
| 221 | + # information used by the package server |
| 222 | + meta = self.distribution.metadata |
| 223 | + data = { |
| 224 | + ':action': action, |
| 225 | + 'metadata_version' : '1.0', |
| 226 | + 'name': meta.get_name(), |
| 227 | + 'version': meta.get_version(), |
| 228 | + 'summary': meta.get_description(), |
| 229 | + 'home_page': meta.get_url(), |
| 230 | + 'author': meta.get_contact(), |
| 231 | + 'author_email': meta.get_contact_email(), |
| 232 | + 'license': meta.get_licence(), |
| 233 | + 'description': meta.get_long_description(), |
| 234 | + 'keywords': meta.get_keywords(), |
| 235 | + 'platform': meta.get_platforms(), |
| 236 | + } |
| 237 | + if hasattr(meta, 'classifiers'): |
| 238 | + data['classifiers'] = meta.get_classifiers() |
| 239 | + return data |
| 240 | + |
| 241 | + def post_to_server(self, data, auth=None): |
| 242 | + ''' Post a query to the server, and return a string response. |
| 243 | + ''' |
| 244 | + |
| 245 | + # Build up the MIME payload for the urllib2 POST data |
| 246 | + boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' |
| 247 | + sep_boundary = '\n--' + boundary |
| 248 | + end_boundary = sep_boundary + '--' |
| 249 | + body = StringIO.StringIO() |
| 250 | + for key, value in data.items(): |
| 251 | + # handle multiple entries for the same name |
| 252 | + if type(value) != type([]): |
| 253 | + value = [value] |
| 254 | + for value in value: |
| 255 | + value = str(value) |
| 256 | + body.write(sep_boundary) |
| 257 | + body.write('\nContent-Disposition: form-data; name="%s"'%key) |
| 258 | + body.write("\n\n") |
| 259 | + body.write(value) |
| 260 | + if value and value[-1] == '\r': |
| 261 | + body.write('\n') # write an extra newline (lurve Macs) |
| 262 | + body.write(end_boundary) |
| 263 | + body.write("\n") |
| 264 | + body = body.getvalue() |
| 265 | + |
| 266 | + # build the Request |
| 267 | + headers = { |
| 268 | + 'Content-type': 'multipart/form-data; boundary=%s'%boundary, |
| 269 | + 'Content-length': str(len(body)) |
| 270 | + } |
| 271 | + req = urllib2.Request(self.repository, body, headers) |
| 272 | + |
| 273 | + # handle HTTP and include the Basic Auth handler |
| 274 | + opener = urllib2.build_opener( |
| 275 | + urllib2.HTTPBasicAuthHandler(password_mgr=auth) |
| 276 | + ) |
| 277 | + data = '' |
| 278 | + try: |
| 279 | + result = opener.open(req) |
| 280 | + except urllib2.HTTPError, e: |
| 281 | + if self.verbose: |
| 282 | + data = e.fp.read() |
| 283 | + result = e.code, e.msg |
| 284 | + except urllib2.URLError, e: |
| 285 | + result = 500, str(e) |
| 286 | + else: |
| 287 | + if self.verbose: |
| 288 | + data = result.read() |
| 289 | + result = 200, 'OK' |
| 290 | + if self.verbose: |
| 291 | + print '-'*75, data, '-'*75 |
| 292 | + return result |
| 293 | + |
0 commit comments