/hyde/ext/publishers/pypi.py

http://github.com/hyde/hyde · Python · 137 lines · 110 code · 8 blank · 19 comment · 29 complexity · b602fab342fcdffddae122e93f637a7a MD5 · raw file

  1. """
  2. Contains classes and utilities that help publishing a hyde website to
  3. the documentation hosting on http://packages.python.org/.
  4. """
  5. import os
  6. import getpass
  7. import zipfile
  8. import tempfile
  9. from base64 import standard_b64encode
  10. from hyde._compat import (configparser, input, HTTPConnection, HTTPSConnection,
  11. parse)
  12. from hyde.publisher import Publisher
  13. from commando.util import getLoggerWithNullHandler
  14. logger = getLoggerWithNullHandler('hyde.ext.publishers.pypi')
  15. class PyPI(Publisher):
  16. def initialize(self, settings):
  17. self.settings = settings
  18. self.project = settings.project
  19. self.url = getattr(settings, "url", "https://pypi.python.org/pypi/")
  20. self.username = getattr(settings, "username", None)
  21. self.password = getattr(settings, "password", None)
  22. self.prompt_for_credentials()
  23. def prompt_for_credentials(self):
  24. pypirc_file = os.path.expanduser("~/.pypirc")
  25. if not os.path.isfile(pypirc_file):
  26. pypirc = None
  27. else:
  28. pypirc = configparser.Rawconfigparser()
  29. pypirc.read([pypirc_file])
  30. missing_errs = (
  31. configparser.NoSectionError, configparser.NoOptionError)
  32. # Try to find username in .pypirc
  33. if self.username is None:
  34. if pypirc is not None:
  35. try:
  36. self.username = pypirc.get("server-login", "username")
  37. except missing_errs:
  38. pass
  39. # Prompt for username on command-line
  40. if self.username is None:
  41. print("Username: ",)
  42. self.username = input().strip()
  43. # Try to find password in .pypirc
  44. if self.password is None:
  45. if pypirc is not None:
  46. try:
  47. self.password = pypirc.get("server-login", "password")
  48. except missing_errs:
  49. pass
  50. # Prompt for username on command-line
  51. if self.password is None:
  52. self.password = getpass.getpass("Password: ")
  53. # Validate the values.
  54. if not self.username:
  55. raise ValueError("PyPI requires a username")
  56. if not self.password:
  57. raise ValueError("PyPI requires a password")
  58. def publish(self):
  59. super(PyPI, self).publish()
  60. tf = tempfile.TemporaryFile()
  61. try:
  62. # Bundle it up into a zipfile
  63. logger.info("building the zipfile")
  64. root = self.site.config.deploy_root_path
  65. zf = zipfile.ZipFile(tf, "w", zipfile.ZIP_DEFLATED)
  66. try:
  67. for item in root.walker.walk_files():
  68. logger.info(" adding file: %s", item.path)
  69. zf.write(item.path, item.get_relative_path(root))
  70. finally:
  71. zf.close()
  72. # Formulate the necessary bits for the HTTP POST.
  73. # Multipart/form-data encoding. Yuck.
  74. authz = self.username + ":" + self.password
  75. authz = "Basic " + standard_b64encode(authz)
  76. boundary = "-----------" + os.urandom(20).encode("hex")
  77. # *F841 local variable 'sep_boundary' is assigned to but never used
  78. # sep_boundary = "\r\n--" + boundary
  79. # *F841 local variable 'end_boundary' is assigned to but never used
  80. # end_boundary = "\r\n--" + boundary + "--\r\n"
  81. content_type = "multipart/form-data; boundary=%s" % (boundary,)
  82. items = ((":action", "doc_upload"), ("name", self.project))
  83. body_prefix = ""
  84. for (name, value) in items:
  85. body_prefix += "--" + boundary + "\r\n"
  86. body_prefix += "Content-Disposition: form-data; name=\""
  87. body_prefix += name + "\"\r\n\r\n"
  88. body_prefix += value + "\r\n"
  89. body_prefix += "--" + boundary + "\r\n"
  90. body_prefix += "Content-Disposition: form-data; name=\"content\""
  91. body_prefix += "; filename=\"website.zip\"\r\n\r\n"
  92. body_suffix = "\r\n--" + boundary + "--\r\n"
  93. content_length = len(body_prefix) + tf.tell() + len(body_suffix)
  94. # POST it up to PyPI
  95. logger.info("uploading to PyPI")
  96. url = parse.urlparse(self.url)
  97. if url.scheme == "https":
  98. con = HTTPSConnection(url.netloc)
  99. else:
  100. con = HTTPConnection(url.netloc)
  101. con.connect()
  102. try:
  103. con.putrequest("POST", self.url)
  104. con.putheader("Content-Type", content_type)
  105. con.putheader("Content-Length", str(content_length))
  106. con.putheader("Authorization", authz)
  107. con.endheaders()
  108. con.send(body_prefix)
  109. tf.seek(0)
  110. data = tf.read(1024 * 32)
  111. while data:
  112. con.send(data)
  113. data = tf.read(1024 * 32)
  114. con.send(body_suffix)
  115. r = con.getresponse()
  116. try:
  117. # PyPI tries to redirect to the page on success.
  118. if r.status in (200, 301,):
  119. logger.info("success!")
  120. else:
  121. msg = "Upload failed: %s %s" % (r.status, r.reason,)
  122. raise Exception(msg)
  123. finally:
  124. r.close()
  125. finally:
  126. con.close()
  127. finally:
  128. tf.close()