diff --git a/AUTHORS.rst b/AUTHORS.rst index 5322db0..964aed1 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -31,7 +31,7 @@ Contributors * Bug fix: LessCSSPlugin: return original text if not a .less file * Added 'use_figure' configuration option for syntax tag - * Pyfs publisher with `mimetype` and `etags` support + * PyFS publisher with `mtime` and `etags` support - |tinnet|_ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7d1ca58..44fd037 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,7 +37,7 @@ Thanks to @rfk. * Updated to use nose 1.0 * Bug fix: LessCSSPlugin: return original text if not a .less file -* PyFS publisher with mimetypes and etags support. +* PyFS publisher with mtime and etags support. Version 0.8 ============== 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() + + diff --git a/hyde/ext/templates/jinja.py b/hyde/ext/templates/jinja.py index 92ec1f2..f2be6a9 100644 --- a/hyde/ext/templates/jinja.py +++ b/hyde/ext/templates/jinja.py @@ -48,6 +48,13 @@ def content_url(context, path): """ return context['site'].content_url(path) +@contextfunction +def full_url(context, path): + """ + Returns the full url given a partial path. + """ + return context['site'].full_url(path) + @contextfilter def date_format(ctx, dt, fmt=None): @@ -561,6 +568,7 @@ class Jinja2Template(Template): extensions=settings['extensions']) self.env.globals['media_url'] = media_url self.env.globals['content_url'] = content_url + self.env.globals['full_url'] = full_url self.env.globals['engine'] = engine self.env.globals['deps'] = {} self.env.filters['markdown'] = markdown @@ -669,6 +677,12 @@ class Jinja2Template(Template): """ return '{{ media_url(\'%s\') }}' % url + def get_full_url_statement(self, url): + """ + Returns the full url statement. + """ + return '{{ full_url(\'%s\') }}' % url + def render_resource(self, resource, context): """ Renders the given resource using the context diff --git a/hyde/site.py b/hyde/site.py index 5a75f8d..e3a0fd3 100644 --- a/hyde/site.py +++ b/hyde/site.py @@ -4,6 +4,7 @@ Parses & holds information about the site to be generated. """ import os import fnmatch +import urlparse from hyde.exceptions import HydeException from hyde.fs import FS, File, Folder from hyde.model import Config @@ -398,7 +399,8 @@ class Site(object): Determines if the given path is media or content based on the configuration and returns the appropriate url. """ - + if urlparse.urlparse(path)[:2] != ("",""): + return path if self.is_media(path): return self.media_url( FS(path).get_relative_path( @@ -411,4 +413,4 @@ class Site(object): Given the relative path, determines if it is content or media. """ folder = self.content.source.child_folder(path) - return folder.is_descendant_of(self.config.media_root_path) \ No newline at end of file + return folder.is_descendant_of(self.config.media_root_path)