register.py 11 KB

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