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, urllib.request
  8. from warnings import warn
  9. from distutils.core import PyPIRCCommand
  10. from distutils.errors import *
  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. PendingDeprecationWarning,
  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):
  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 (%s): %s' % (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):
  233. '''Post a query to the server, and return a string response.'''
  234. if 'name' in data:
  235. self.announce(
  236. 'Registering %s to %s' % (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