codekingpro/portable-devtools
114k
1"""distutils.command.register2 3Implements the Distutils 'register' command (register with the repository).4"""5 6# created 2002/10/21, Richard Jones7 8import getpass9import io10import logging11import urllib.parse12import urllib.request13from distutils._log import log14from warnings import warn15 16from .._itertools import always_iterable17from ..core import PyPIRCCommand18 19 20class register(PyPIRCCommand):21 description = "register the distribution with the Python package index"22 user_options = PyPIRCCommand.user_options + [23 ('list-classifiers', None, 'list the valid Trove classifiers'),24 (25 'strict',26 None,27 'Will stop the registering if the meta-data are not fully compliant',28 ),29 ]30 boolean_options = PyPIRCCommand.boolean_options + [31 'verify',32 'list-classifiers',33 'strict',34 ]35 36 sub_commands = [('check', lambda self: True)]37 38 def initialize_options(self):39 PyPIRCCommand.initialize_options(self)40 self.list_classifiers = 041 self.strict = 042 43 def finalize_options(self):44 PyPIRCCommand.finalize_options(self)45 # setting options for the `check` subcommand46 check_options = {47 'strict': ('register', self.strict),48 'restructuredtext': ('register', 1),49 }50 self.distribution.command_options['check'] = check_options51 52 def run(self):53 self.finalize_options()54 self._set_config()55 56 # Run sub commands57 for cmd_name in self.get_sub_commands():58 self.run_command(cmd_name)59 60 if self.dry_run:61 self.verify_metadata()62 elif self.list_classifiers:63 self.classifiers()64 else:65 self.send_metadata()66 67 def check_metadata(self):68 """Deprecated API."""69 warn(70 "distutils.command.register.check_metadata is deprecated; "71 "use the check command instead",72 DeprecationWarning,73 )74 check = self.distribution.get_command_obj('check')75 check.ensure_finalized()76 check.strict = self.strict77 check.restructuredtext = 178 check.run()79 80 def _set_config(self):81 """Reads the configuration file and set attributes."""82 config = self._read_pypirc()83 if config != {}:84 self.username = config['username']85 self.password = config['password']86 self.repository = config['repository']87 self.realm = config['realm']88 self.has_config = True89 else:90 if self.repository not in ('pypi', self.DEFAULT_REPOSITORY):91 raise ValueError('%s not found in .pypirc' % self.repository)92 if self.repository == 'pypi':93 self.repository = self.DEFAULT_REPOSITORY94 self.has_config = False95 96 def classifiers(self):97 """Fetch the list of classifiers from the server."""98 url = self.repository + '?:action=list_classifiers'99 response = urllib.request.urlopen(url)100 log.info(self._read_pypi_response(response))101 102 def verify_metadata(self):103 """Send the metadata to the package index server to be checked."""104 # send the info to the server and report the result105 (code, result) = self.post_to_server(self.build_post_data('verify'))106 log.info('Server response (%s): %s', code, result)107 108 def send_metadata(self): # noqa: C901109 """Send the metadata to the package index server.110 111 Well, do the following:112 1. figure who the user is, and then113 2. send the data as a Basic auth'ed POST.114 115 First we try to read the username/password from $HOME/.pypirc,116 which is a ConfigParser-formatted file with a section117 [distutils] containing username and password entries (both118 in clear text). Eg:119 120 [distutils]121 index-servers =122 pypi123 124 [pypi]125 username: fred126 password: sekrit127 128 Otherwise, to figure who the user is, we offer the user three129 choices:130 131 1. use existing login,132 2. register as a new user, or133 3. set the password to a random string and email the user.134 135 """136 # see if we can short-cut and get the username/password from the137 # config138 if self.has_config:139 choice = '1'140 username = self.username141 password = self.password142 else:143 choice = 'x'144 username = password = ''145 146 # get the user's login info147 choices = '1 2 3 4'.split()148 while choice not in choices:149 self.announce(150 """\151We need to know who you are, so please choose either:152 1. use your existing login,153 2. register as a new user,154 3. have the server generate a new password for you (and email it to you), or155 4. quit156Your selection [default 1]: """,157 logging.INFO,158 )159 choice = input()160 if not choice:161 choice = '1'162 elif choice not in choices:163 print('Please choose one of the four options!')164 165 if choice == '1':166 # get the username and password167 while not username:168 username = input('Username: ')169 while not password:170 password = getpass.getpass('Password: ')171 172 # set up the authentication173 auth = urllib.request.HTTPPasswordMgr()174 host = urllib.parse.urlparse(self.repository)[1]175 auth.add_password(self.realm, host, username, password)176 # send the info to the server and report the result177 code, result = self.post_to_server(self.build_post_data('submit'), auth)178 self.announce(f'Server response ({code}): {result}', logging.INFO)179 180 # possibly save the login181 if code == 200:182 if self.has_config:183 # sharing the password in the distribution instance184 # so the upload command can reuse it185 self.distribution.password = password186 else:187 self.announce(188 (189 'I can store your PyPI login so future '190 'submissions will be faster.'191 ),192 logging.INFO,193 )194 self.announce(195 '(the login will be stored in %s)' % self._get_rc_file(),196 logging.INFO,197 )198 choice = 'X'199 while choice.lower() not in 'yn':200 choice = input('Save your login (y/N)?')201 if not choice:202 choice = 'n'203 if choice.lower() == 'y':204 self._store_pypirc(username, password)205 206 elif choice == '2':207 data = {':action': 'user'}208 data['name'] = data['password'] = data['email'] = ''209 data['confirm'] = None210 while not data['name']:211 data['name'] = input('Username: ')212 while data['password'] != data['confirm']:213 while not data['password']:214 data['password'] = getpass.getpass('Password: ')215 while not data['confirm']:216 data['confirm'] = getpass.getpass(' Confirm: ')217 if data['password'] != data['confirm']:218 data['password'] = ''219 data['confirm'] = None220 print("Password and confirm don't match!")221 while not data['email']:222 data['email'] = input(' EMail: ')223 code, result = self.post_to_server(data)224 if code != 200:225 log.info('Server response (%s): %s', code, result)226 else:227 log.info('You will receive an email shortly.')228 log.info('Follow the instructions in it to ' 'complete registration.')229 elif choice == '3':230 data = {':action': 'password_reset'}231 data['email'] = ''232 while not data['email']:233 data['email'] = input('Your email address: ')234 code, result = self.post_to_server(data)235 log.info('Server response (%s): %s', code, result)236 237 def build_post_data(self, action):238 # figure the data to send - the metadata plus some additional239 # information used by the package server240 meta = self.distribution.metadata241 data = {242 ':action': action,243 'metadata_version': '1.0',244 'name': meta.get_name(),245 'version': meta.get_version(),246 'summary': meta.get_description(),247 'home_page': meta.get_url(),248 'author': meta.get_contact(),249 'author_email': meta.get_contact_email(),250 'license': meta.get_licence(),251 'description': meta.get_long_description(),252 'keywords': meta.get_keywords(),253 'platform': meta.get_platforms(),254 'classifiers': meta.get_classifiers(),255 'download_url': meta.get_download_url(),256 # PEP 314257 'provides': meta.get_provides(),258 'requires': meta.get_requires(),259 'obsoletes': meta.get_obsoletes(),260 }261 if data['provides'] or data['requires'] or data['obsoletes']:262 data['metadata_version'] = '1.1'263 return data264 265 def post_to_server(self, data, auth=None): # noqa: C901266 """Post a query to the server, and return a string response."""267 if 'name' in data:268 self.announce(269 'Registering {} to {}'.format(data['name'], self.repository),270 logging.INFO,271 )272 # Build up the MIME payload for the urllib2 POST data273 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'274 sep_boundary = '\n--' + boundary275 end_boundary = sep_boundary + '--'276 body = io.StringIO()277 for key, values in data.items():278 for value in map(str, make_iterable(values)):279 body.write(sep_boundary)280 body.write('\nContent-Disposition: form-data; name="%s"' % key)281 body.write("\n\n")282 body.write(value)283 if value and value[-1] == '\r':284 body.write('\n') # write an extra newline (lurve Macs)285 body.write(end_boundary)286 body.write("\n")287 body = body.getvalue().encode("utf-8")288 289 # build the Request290 headers = {291 'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'292 % boundary,293 'Content-length': str(len(body)),294 }295 req = urllib.request.Request(self.repository, body, headers)296 297 # handle HTTP and include the Basic Auth handler298 opener = urllib.request.build_opener(299 urllib.request.HTTPBasicAuthHandler(password_mgr=auth)300 )301 data = ''302 try:303 result = opener.open(req)304 except urllib.error.HTTPError as e:305 if self.show_response:306 data = e.fp.read()307 result = e.code, e.msg308 except urllib.error.URLError as e:309 result = 500, str(e)310 else:311 if self.show_response:312 data = self._read_pypi_response(result)313 result = 200, 'OK'314 if self.show_response:315 msg = '\n'.join(('-' * 75, data, '-' * 75))316 self.announce(msg, logging.INFO)317 return result318 319 320def make_iterable(values):321 if values is None:322 return [None]323 return always_iterable(values)324 