diff --git a/hyde/ext/publishers/pypi.py b/hyde/ext/publishers/pypi.py new file mode 100644 index 0000000..5292da1 --- /dev/null +++ b/hyde/ext/publishers/pypi.py @@ -0,0 +1,140 @@ +""" +Contains classes and utilities that help publishing a hyde website to +the documentation hosting on http://packages.python.org/. + +""" + +import os +import getpass +import zipfile +import tempfile +import httplib +import urlparse +from base64 import standard_b64encode +import ConfigParser + +from hyde.fs import File, Folder +from hyde.publisher import Publisher + +from hyde.util import getLoggerWithNullHandler +logger = getLoggerWithNullHandler('hyde.ext.publishers.pypi') + + + + +class PyPI(Publisher): + + def initialize(self, settings): + self.settings = settings + self.project = settings.project + self.url = getattr(settings,"url","https://pypi.python.org/pypi/") + self.username = getattr(settings,"username",None) + self.password = getattr(settings,"password",None) + self.prompt_for_credentials() + + def prompt_for_credentials(self): + pypirc_file = os.path.expanduser("~/.pypirc") + if not os.path.isfile(pypirc_file): + pypirc = None + else: + pypirc = ConfigParser.RawConfigParser() + pypirc.read([pypirc_file]) + missing_errs = (ConfigParser.NoSectionError,ConfigParser.NoOptionError) + # Try to find username in .pypirc + if self.username is None: + if pypirc is not None: + try: + self.username = pypirc.get("server-login","username") + except missing_errs: + pass + # Prompt for username on command-line + if self.username is None: + print "Username: ", + self.username = raw_input().strip() + # Try to find password in .pypirc + if self.password is None: + if pypirc is not None: + try: + self.password = pypirc.get("server-login","password") + except missing_errs: + pass + # Prompt for username on command-line + if self.password is None: + self.password = getpass.getpass("Password: ") + # Validate the values. + if not self.username: + raise ValueError("PyPI requires a username") + if not self.password: + raise ValueError("PyPI requires a password") + + def publish(self): + super(PyPI, self).publish() + tf = tempfile.TemporaryFile() + try: + # Bundle it up into a zipfile + logger.info("building the zipfile") + root = self.site.config.deploy_root_path + zf = zipfile.ZipFile(tf,"w",zipfile.ZIP_DEFLATED) + try: + for item in root.walker.walk_files(): + logger.info(" adding file: %s",item.path) + zf.write(item.path,item.get_relative_path(root)) + finally: + zf.close() + # Formulate the necessary bits for the HTTP POST. + # Multipart/form-data encoding. Yuck. + authz = self.username + ":" + self.password + authz = "Basic " + standard_b64encode(authz) + boundary = "-----------" + os.urandom(20).encode("hex") + sep_boundary = "\r\n--" + boundary + end_boundary = "\r\n--" + boundary + "--\r\n" + content_type = "multipart/form-data; boundary=%s" % (boundary,) + items = ((":action","doc_upload"),("name",self.project)) + body_prefix = "" + for (name,value) in items: + body_prefix += "--" + boundary + "\r\n" + body_prefix += "Content-Disposition: form-data; name=\"" + body_prefix += name + "\"\r\n\r\n" + body_prefix += value + "\r\n" + body_prefix += "--" + boundary + "\r\n" + body_prefix += "Content-Disposition: form-data; name=\"content\"" + body_prefix += "; filename=\"website.zip\"\r\n\r\n" + body_suffix = "\r\n--" + boundary + "--\r\n" + content_length = len(body_prefix) + tf.tell() + len(body_suffix) + # POST it up to PyPI + logger.info("uploading to PyPI") + url = urlparse.urlparse(self.url) + if url.scheme == "https": + con = httplib.HTTPSConnection(url.netloc) + else: + con = httplib.HTTPConnection(url.netloc) + con.connect() + try: + con.putrequest("POST", self.url) + con.putheader("Content-Type",content_type) + con.putheader("Content-Length",str(content_length)) + con.putheader("Authorization",authz) + con.endheaders() + con.send(body_prefix) + tf.seek(0) + data = tf.read(1024*32) + while data: + con.send(data) + data = tf.read(1024*32) + con.send(body_suffix) + r = con.getresponse() + try: + # PyPI tries to redirect to the page on success. + if r.status in (200,301,): + logger.info("success!") + else: + msg = "Upload failed: %s %s" % (r.status,r.reason,) + raise Exception(msg) + finally: + r.close() + finally: + con.close() + finally: + tf.close() + +