register.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. """distutils.command.register
  2. Implements the Distutils 'register' command (register with the repository).
  3. """
  4. # created 2002/10/21, Richard Jones
  5. import getpass
  6. import io
  7. import logging
  8. import urllib.parse
  9. import urllib.request
  10. from warnings import warn
  11. from ..core import PyPIRCCommand
  12. from distutils._log import log
  13. class register(PyPIRCCommand):
  14. description = "register the distribution with the Python package index"
  15. user_options = PyPIRCCommand.user_options + [
  16. ('list-classifiers', None, 'list the valid Trove classifiers'),
  17. (
  18. 'strict',
  19. None,
  20. 'Will stop the registering if the meta-data are not fully compliant',
  21. ),
  22. ]
  23. boolean_options = PyPIRCCommand.boolean_options + [
  24. 'verify',
  25. 'list-classifiers',
  26. 'strict',
  27. ]
  28. sub_commands = [('check', lambda self: True)]
  29. def initialize_options(self):
  30. PyPIRCCommand.initialize_options(self)
  31. self.list_classifiers = 0
  32. self.strict = 0
  33. def finalize_options(self):
  34. PyPIRCCommand.finalize_options(self)
  35. # setting options for the `check` subcommand
  36. check_options = {
  37. 'strict': ('register', self.strict),
  38. 'restructuredtext': ('register', 1),
  39. }
  40. self.distribution.command_options['check'] = check_options
  41. def run(self):
  42. self.finalize_options()
  43. self._set_config()
  44. # Run sub commands
  45. for cmd_name in self.get_sub_commands():
  46. self.run_command(cmd_name)
  47. if self.dry_run:
  48. self.verify_metadata()
  49. elif self.list_classifiers:
  50. self.classifiers()
  51. else:
  52. self.send_metadata()
  53. def check_metadata(self):
  54. """Deprecated API."""
  55. warn(
  56. "distutils.command.register.check_metadata is deprecated; "
  57. "use the check command instead",
  58. DeprecationWarning,
  59. )
  60. check = self.distribution.get_command_obj('check')
  61. check.ensure_finalized()
  62. check.strict = self.strict
  63. check.restructuredtext = 1
  64. check.run()
  65. def _set_config(self):
  66. '''Reads the configuration file and set attributes.'''
  67. config = self._read_pypirc()
  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', self.DEFAULT_REPOSITORY):
  76. raise ValueError('%s not found in .pypirc' % self.repository)
  77. if self.repository == 'pypi':
  78. self.repository = self.DEFAULT_REPOSITORY
  79. self.has_config = False
  80. def classifiers(self):
  81. '''Fetch the list of classifiers from the server.'''
  82. url = self.repository + '?:action=list_classifiers'
  83. response = urllib.request.urlopen(url)
  84. log.info(self._read_pypi_response(response))
  85. def verify_metadata(self):
  86. '''Send the metadata to the package index server to be checked.'''
  87. # send the info to the server and report the result
  88. (code, result) = self.post_to_server(self.build_post_data('verify'))
  89. log.info('Server response (%s): %s', code, result)
  90. def send_metadata(self): # noqa: C901
  91. '''Send the metadata to the package index server.
  92. Well, do the following:
  93. 1. figure who the user is, and then
  94. 2. send the data as a Basic auth'ed POST.
  95. First we try to read the username/password from $HOME/.pypirc,
  96. which is a ConfigParser-formatted file with a section
  97. [distutils] containing username and password entries (both
  98. in clear text). Eg:
  99. [distutils]
  100. index-servers =
  101. pypi
  102. [pypi]
  103. username: fred
  104. password: sekrit
  105. Otherwise, to figure who the user is, we offer the user three
  106. choices:
  107. 1. use existing login,
  108. 2. register as a new user, or
  109. 3. set the password to a random string and email the user.
  110. '''
  111. # see if we can short-cut and get the username/password from the
  112. # config
  113. if self.has_config:
  114. choice = '1'
  115. username = self.username
  116. password = self.password
  117. else:
  118. choice = 'x'
  119. username = password = ''
  120. # get the user's login info
  121. choices = '1 2 3 4'.split()
  122. while choice not in choices:
  123. self.announce(
  124. '''\
  125. We need to know who you are, so please choose either:
  126. 1. use your existing login,
  127. 2. register as a new user,
  128. 3. have the server generate a new password for you (and email it to you), or
  129. 4. quit
  130. Your selection [default 1]: ''',
  131. logging.INFO,
  132. )
  133. choice = input()
  134. if not choice:
  135. choice = '1'
  136. elif choice not in choices:
  137. print('Please choose one of the four options!')
  138. if choice == '1':
  139. # get the username and password
  140. while not username:
  141. username = input('Username: ')
  142. while not password:
  143. password = getpass.getpass('Password: ')
  144. # set up the authentication
  145. auth = urllib.request.HTTPPasswordMgr()
  146. host = urllib.parse.urlparse(self.repository)[1]
  147. auth.add_password(self.realm, host, username, password)
  148. # send the info to the server and report the result
  149. code, result = self.post_to_server(self.build_post_data('submit'), auth)
  150. self.announce('Server response ({}): {}'.format(code, result), logging.INFO)
  151. # possibly save the login
  152. if code == 200:
  153. if self.has_config:
  154. # sharing the password in the distribution instance
  155. # so the upload command can reuse it
  156. self.distribution.password = password
  157. else:
  158. self.announce(
  159. (
  160. 'I can store your PyPI login so future '
  161. 'submissions will be faster.'
  162. ),
  163. logging.INFO,
  164. )
  165. self.announce(
  166. '(the login will be stored in %s)' % self._get_rc_file(),
  167. logging.INFO,
  168. )
  169. choice = 'X'
  170. while choice.lower() not in 'yn':
  171. choice = input('Save your login (y/N)?')
  172. if not choice:
  173. choice = 'n'
  174. if choice.lower() == 'y':
  175. self._store_pypirc(username, password)
  176. elif choice == '2':
  177. data = {':action': 'user'}
  178. data['name'] = data['password'] = data['email'] = ''
  179. data['confirm'] = None
  180. while not data['name']:
  181. data['name'] = input('Username: ')
  182. while data['password'] != data['confirm']:
  183. while not data['password']:
  184. data['password'] = getpass.getpass('Password: ')
  185. while not data['confirm']:
  186. data['confirm'] = getpass.getpass(' Confirm: ')
  187. if data['password'] != data['confirm']:
  188. data['password'] = ''
  189. data['confirm'] = None
  190. print("Password and confirm don't match!")
  191. while not data['email']:
  192. data['email'] = input(' EMail: ')
  193. code, result = self.post_to_server(data)
  194. if code != 200:
  195. log.info('Server response (%s): %s', code, result)
  196. else:
  197. log.info('You will receive an email shortly.')
  198. log.info('Follow the instructions in it to ' 'complete registration.')
  199. elif choice == '3':
  200. data = {':action': 'password_reset'}
  201. data['email'] = ''
  202. while not data['email']:
  203. data['email'] = input('Your email address: ')
  204. code, result = self.post_to_server(data)
  205. log.info('Server response (%s): %s', code, result)
  206. def build_post_data(self, action):
  207. # figure the data to send - the metadata plus some additional
  208. # information used by the package server
  209. meta = self.distribution.metadata
  210. data = {
  211. ':action': action,
  212. 'metadata_version': '1.0',
  213. 'name': meta.get_name(),
  214. 'version': meta.get_version(),
  215. 'summary': meta.get_description(),
  216. 'home_page': meta.get_url(),
  217. 'author': meta.get_contact(),
  218. 'author_email': meta.get_contact_email(),
  219. 'license': meta.get_licence(),
  220. 'description': meta.get_long_description(),
  221. 'keywords': meta.get_keywords(),
  222. 'platform': meta.get_platforms(),
  223. 'classifiers': meta.get_classifiers(),
  224. 'download_url': meta.get_download_url(),
  225. # PEP 314
  226. 'provides': meta.get_provides(),
  227. 'requires': meta.get_requires(),
  228. 'obsoletes': meta.get_obsoletes(),
  229. }
  230. if data['provides'] or data['requires'] or data['obsoletes']:
  231. data['metadata_version'] = '1.1'
  232. return data
  233. def post_to_server(self, data, auth=None): # noqa: C901
  234. '''Post a query to the server, and return a string response.'''
  235. if 'name' in data:
  236. self.announce(
  237. 'Registering {} to {}'.format(data['name'], self.repository),
  238. logging.INFO,
  239. )
  240. # Build up the MIME payload for the urllib2 POST data
  241. boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
  242. sep_boundary = '\n--' + boundary
  243. end_boundary = sep_boundary + '--'
  244. body = io.StringIO()
  245. for key, value in data.items():
  246. # handle multiple entries for the same name
  247. if type(value) not in (type([]), type(())):
  248. value = [value]
  249. for value in value:
  250. value = str(value)
  251. body.write(sep_boundary)
  252. body.write('\nContent-Disposition: form-data; name="%s"' % key)
  253. body.write("\n\n")
  254. body.write(value)
  255. if value and value[-1] == '\r':
  256. body.write('\n') # write an extra newline (lurve Macs)
  257. body.write(end_boundary)
  258. body.write("\n")
  259. body = body.getvalue().encode("utf-8")
  260. # build the Request
  261. headers = {
  262. 'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'
  263. % boundary,
  264. 'Content-length': str(len(body)),
  265. }
  266. req = urllib.request.Request(self.repository, body, headers)
  267. # handle HTTP and include the Basic Auth handler
  268. opener = urllib.request.build_opener(
  269. urllib.request.HTTPBasicAuthHandler(password_mgr=auth)
  270. )
  271. data = ''
  272. try:
  273. result = opener.open(req)
  274. except urllib.error.HTTPError as e:
  275. if self.show_response:
  276. data = e.fp.read()
  277. result = e.code, e.msg
  278. except urllib.error.URLError as e:
  279. result = 500, str(e)
  280. else:
  281. if self.show_response:
  282. data = self._read_pypi_response(result)
  283. result = 200, 'OK'
  284. if self.show_response:
  285. msg = '\n'.join(('-' * 75, data, '-' * 75))
  286. self.announce(msg, logging.INFO)
  287. return result