Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1 | """Register a release with a project index.""" |
| 2 | |
| 3 | # Contributed by Richard Jones |
| 4 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 5 | import getpass |
| 6 | import urllib.error |
| 7 | import urllib.parse |
| 8 | import urllib.request |
| 9 | |
| 10 | from packaging import logger |
| 11 | from packaging.util import (read_pypirc, generate_pypirc, DEFAULT_REPOSITORY, |
Éric Araujo | ce5fe83 | 2011-07-08 16:27:12 +0200 | [diff] [blame] | 12 | DEFAULT_REALM, get_pypirc_path, encode_multipart) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 13 | from packaging.command.cmd import Command |
| 14 | |
| 15 | class register(Command): |
| 16 | |
| 17 | description = "register a release with PyPI" |
| 18 | user_options = [ |
| 19 | ('repository=', 'r', |
| 20 | "repository URL [default: %s]" % DEFAULT_REPOSITORY), |
| 21 | ('show-response', None, |
| 22 | "display full response text from server"), |
| 23 | ('list-classifiers', None, |
| 24 | "list valid Trove classifiers"), |
| 25 | ('strict', None , |
| 26 | "stop the registration if the metadata is not fully compliant") |
| 27 | ] |
| 28 | |
| 29 | boolean_options = ['show-response', 'list-classifiers', 'strict'] |
| 30 | |
| 31 | def initialize_options(self): |
| 32 | self.repository = None |
| 33 | self.realm = None |
| 34 | self.show_response = False |
| 35 | self.list_classifiers = False |
| 36 | self.strict = False |
| 37 | |
| 38 | def finalize_options(self): |
| 39 | if self.repository is None: |
| 40 | self.repository = DEFAULT_REPOSITORY |
| 41 | if self.realm is None: |
| 42 | self.realm = DEFAULT_REALM |
| 43 | |
| 44 | def run(self): |
| 45 | self._set_config() |
| 46 | |
| 47 | # Check the package metadata |
| 48 | check = self.distribution.get_command_obj('check') |
| 49 | if check.strict != self.strict and not check.all: |
| 50 | # If check was already run but with different options, |
| 51 | # re-run it |
| 52 | check.strict = self.strict |
| 53 | check.all = True |
| 54 | self.distribution.have_run.pop('check', None) |
| 55 | self.run_command('check') |
| 56 | |
| 57 | if self.dry_run: |
| 58 | self.verify_metadata() |
| 59 | elif self.list_classifiers: |
| 60 | self.classifiers() |
| 61 | else: |
| 62 | self.send_metadata() |
| 63 | |
| 64 | def _set_config(self): |
| 65 | ''' Reads the configuration file and set attributes. |
| 66 | ''' |
| 67 | config = read_pypirc(self.repository, self.realm) |
| 68 | if config != {}: |
| 69 | self.username = config['username'] |
| 70 | self.password = config['password'] |
| 71 | self.repository = config['repository'] |
| 72 | self.realm = config['realm'] |
| 73 | self.has_config = True |
| 74 | else: |
| 75 | if self.repository not in ('pypi', DEFAULT_REPOSITORY): |
| 76 | raise ValueError('%s not found in .pypirc' % self.repository) |
| 77 | if self.repository == 'pypi': |
| 78 | self.repository = DEFAULT_REPOSITORY |
| 79 | self.has_config = False |
| 80 | |
| 81 | def classifiers(self): |
| 82 | ''' Fetch the list of classifiers from the server. |
| 83 | ''' |
| 84 | response = urllib.request.urlopen(self.repository+'?:action=list_classifiers') |
| 85 | logger.info(response.read()) |
| 86 | |
| 87 | def verify_metadata(self): |
| 88 | ''' Send the metadata to the package index server to be checked. |
| 89 | ''' |
| 90 | # send the info to the server and report the result |
| 91 | code, result = self.post_to_server(self.build_post_data('verify')) |
| 92 | logger.info('server response (%s): %s', code, result) |
| 93 | |
| 94 | |
| 95 | def send_metadata(self): |
| 96 | ''' Send the metadata to the package index server. |
| 97 | |
| 98 | Well, do the following: |
| 99 | 1. figure who the user is, and then |
| 100 | 2. send the data as a Basic auth'ed POST. |
| 101 | |
| 102 | First we try to read the username/password from $HOME/.pypirc, |
| 103 | which is a ConfigParser-formatted file with a section |
| 104 | [distutils] containing username and password entries (both |
| 105 | in clear text). Eg: |
| 106 | |
| 107 | [distutils] |
| 108 | index-servers = |
| 109 | pypi |
| 110 | |
| 111 | [pypi] |
| 112 | username: fred |
| 113 | password: sekrit |
| 114 | |
| 115 | Otherwise, to figure who the user is, we offer the user three |
| 116 | choices: |
| 117 | |
| 118 | 1. use existing login, |
| 119 | 2. register as a new user, or |
| 120 | 3. set the password to a random string and email the user. |
| 121 | |
| 122 | ''' |
| 123 | # TODO factor registration out into another method |
| 124 | # TODO use print to print, not logging |
| 125 | |
| 126 | # see if we can short-cut and get the username/password from the |
| 127 | # config |
| 128 | if self.has_config: |
| 129 | choice = '1' |
| 130 | username = self.username |
| 131 | password = self.password |
| 132 | else: |
| 133 | choice = 'x' |
| 134 | username = password = '' |
| 135 | |
| 136 | # get the user's login info |
| 137 | choices = '1 2 3 4'.split() |
| 138 | while choice not in choices: |
| 139 | logger.info('''\ |
| 140 | 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 | |
| 147 | choice = input() |
| 148 | if not choice: |
| 149 | choice = '1' |
| 150 | elif choice not in choices: |
| 151 | print('Please choose one of the four options!') |
| 152 | |
| 153 | if choice == '1': |
| 154 | # get the username and password |
| 155 | while not username: |
| 156 | username = input('Username: ') |
| 157 | while not password: |
| 158 | password = getpass.getpass('Password: ') |
| 159 | |
| 160 | # set up the authentication |
| 161 | auth = urllib.request.HTTPPasswordMgr() |
| 162 | host = urllib.parse.urlparse(self.repository)[1] |
| 163 | auth.add_password(self.realm, host, username, password) |
| 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 | logger.info('Server response (%s): %s', code, result) |
| 168 | |
| 169 | # possibly save the login |
| 170 | if code == 200: |
| 171 | if self.has_config: |
| 172 | # sharing the password in the distribution instance |
| 173 | # so the upload command can reuse it |
| 174 | self.distribution.password = password |
| 175 | else: |
| 176 | logger.info( |
| 177 | 'I can store your PyPI login so future submissions ' |
| 178 | 'will be faster.\n(the login will be stored in %s)', |
| 179 | get_pypirc_path()) |
| 180 | choice = 'X' |
| 181 | while choice.lower() not in 'yn': |
| 182 | choice = input('Save your login (y/N)?') |
| 183 | if not choice: |
| 184 | choice = 'n' |
| 185 | if choice.lower() == 'y': |
| 186 | generate_pypirc(username, password) |
| 187 | |
| 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'] = 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'] = input(' EMail: ') |
| 205 | code, result = self.post_to_server(data) |
| 206 | if code != 200: |
| 207 | logger.info('server response (%s): %s', code, result) |
| 208 | else: |
| 209 | logger.info('you will receive an email shortly; follow the ' |
| 210 | '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'] = input('Your email address: ') |
| 216 | code, result = self.post_to_server(data) |
| 217 | logger.info('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 | data = self.distribution.metadata.todict() |
| 223 | data[':action'] = action |
| 224 | return data |
| 225 | |
| 226 | # XXX to be refactored with upload.upload_file |
| 227 | def post_to_server(self, data, auth=None): |
| 228 | ''' Post a query to the server, and return a string response. |
| 229 | ''' |
| 230 | if 'name' in data: |
| 231 | logger.info('Registering %s to %s', data['name'], self.repository) |
| 232 | # Build up the MIME payload for the urllib2 POST data |
Éric Araujo | ce5fe83 | 2011-07-08 16:27:12 +0200 | [diff] [blame] | 233 | content_type, body = encode_multipart(data.items(), []) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 234 | |
| 235 | # build the Request |
| 236 | headers = { |
Éric Araujo | ce5fe83 | 2011-07-08 16:27:12 +0200 | [diff] [blame] | 237 | 'Content-type': content_type, |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 238 | 'Content-length': str(len(body)) |
| 239 | } |
| 240 | req = urllib.request.Request(self.repository, body, headers) |
| 241 | |
| 242 | # handle HTTP and include the Basic Auth handler |
| 243 | opener = urllib.request.build_opener( |
| 244 | urllib.request.HTTPBasicAuthHandler(password_mgr=auth) |
| 245 | ) |
| 246 | data = '' |
| 247 | try: |
| 248 | result = opener.open(req) |
| 249 | except urllib.error.HTTPError as e: |
| 250 | if self.show_response: |
| 251 | data = e.fp.read() |
| 252 | result = e.code, e.msg |
| 253 | except urllib.error.URLError as e: |
| 254 | result = 500, str(e) |
| 255 | else: |
| 256 | if self.show_response: |
| 257 | data = result.read() |
| 258 | result = 200, 'OK' |
| 259 | if self.show_response: |
| 260 | dashes = '-' * 75 |
| 261 | logger.info('%s%s%s', dashes, data, dashes) |
| 262 | |
| 263 | return result |