Browse Source

Merge pull request #49 from rfk/hyde

---

This is a simple publisher that uploads a zipfile to PyPI.  I am starting to do some basic websites for my python modules using Hyde and hosting them on packages.python.org.
main
Lakshmi Vyasarajan 13 years ago
parent
commit
29cf0ab32a
5 changed files with 160 additions and 4 deletions
  1. +1
    -1
      AUTHORS.rst
  2. +1
    -1
      CHANGELOG.rst
  3. +140
    -0
      hyde/ext/publishers/pypi.py
  4. +14
    -0
      hyde/ext/templates/jinja.py
  5. +4
    -2
      hyde/site.py

+ 1
- 1
AUTHORS.rst View File

@@ -31,7 +31,7 @@ Contributors


* Bug fix: LessCSSPlugin: return original text if not a .less file * Bug fix: LessCSSPlugin: return original text if not a .less file
* Added 'use_figure' configuration option for syntax tag * Added 'use_figure' configuration option for syntax tag
* Pyfs publisher with `mimetype` and `etags` support
* PyFS publisher with `mtime` and `etags` support


- |tinnet|_ - |tinnet|_




+ 1
- 1
CHANGELOG.rst View File

@@ -37,7 +37,7 @@ Thanks to @rfk.


* Updated to use nose 1.0 * Updated to use nose 1.0
* Bug fix: LessCSSPlugin: return original text if not a .less file * 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 Version 0.8
============== ==============


+ 140
- 0
hyde/ext/publishers/pypi.py View File

@@ -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()


+ 14
- 0
hyde/ext/templates/jinja.py View File

@@ -48,6 +48,13 @@ def content_url(context, path):
""" """
return context['site'].content_url(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 @contextfilter
def date_format(ctx, dt, fmt=None): def date_format(ctx, dt, fmt=None):
@@ -561,6 +568,7 @@ class Jinja2Template(Template):
extensions=settings['extensions']) extensions=settings['extensions'])
self.env.globals['media_url'] = media_url self.env.globals['media_url'] = media_url
self.env.globals['content_url'] = content_url self.env.globals['content_url'] = content_url
self.env.globals['full_url'] = full_url
self.env.globals['engine'] = engine self.env.globals['engine'] = engine
self.env.globals['deps'] = {} self.env.globals['deps'] = {}
self.env.filters['markdown'] = markdown self.env.filters['markdown'] = markdown
@@ -669,6 +677,12 @@ class Jinja2Template(Template):
""" """
return '{{ media_url(\'%s\') }}' % url 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): def render_resource(self, resource, context):
""" """
Renders the given resource using the context Renders the given resource using the context


+ 4
- 2
hyde/site.py View File

@@ -4,6 +4,7 @@ Parses & holds information about the site to be generated.
""" """
import os import os
import fnmatch import fnmatch
import urlparse
from hyde.exceptions import HydeException from hyde.exceptions import HydeException
from hyde.fs import FS, File, Folder from hyde.fs import FS, File, Folder
from hyde.model import Config from hyde.model import Config
@@ -398,7 +399,8 @@ class Site(object):
Determines if the given path is media or content based on the Determines if the given path is media or content based on the
configuration and returns the appropriate url. configuration and returns the appropriate url.
""" """

if urlparse.urlparse(path)[:2] != ("",""):
return path
if self.is_media(path): if self.is_media(path):
return self.media_url( return self.media_url(
FS(path).get_relative_path( FS(path).get_relative_path(
@@ -411,4 +413,4 @@ class Site(object):
Given the relative path, determines if it is content or media. Given the relative path, determines if it is content or media.
""" """
folder = self.content.source.child_folder(path) folder = self.content.source.child_folder(path)
return folder.is_descendant_of(self.config.media_root_path)
return folder.is_descendant_of(self.config.media_root_path)

Loading…
Cancel
Save