[Checkins] SVN: megrok.responseheaders/ Adding the new experimental
extension megrok.responseheaders
Peter Bengtsson
zope at peterbe.com
Tue May 6 06:40:25 EDT 2008
Log message for revision 86499:
Adding the new experimental extension megrok.responseheaders
Changed:
A megrok.responseheaders/
A megrok.responseheaders/buildout.cfg
A megrok.responseheaders/megrok/
A megrok.responseheaders/megrok/__init__.py
A megrok.responseheaders/megrok/responseheaders/
A megrok.responseheaders/megrok/responseheaders/__init__.py
A megrok.responseheaders/megrok/responseheaders/configure.zcml
A megrok.responseheaders/megrok/responseheaders/ftesting.zcml
A megrok.responseheaders/megrok/responseheaders/ftests/
A megrok.responseheaders/megrok/responseheaders/ftests/__init__.py
A megrok.responseheaders/megrok/responseheaders/ftests/functional.txt
A megrok.responseheaders/megrok/responseheaders/ftests/sample.py
A megrok.responseheaders/megrok/responseheaders/ftests/sample_templates/
A megrok.responseheaders/megrok/responseheaders/ftests/sample_templates/template1.pt
A megrok.responseheaders/megrok/responseheaders/ftests/sample_templates/template2.pt
A megrok.responseheaders/megrok/responseheaders/headers.py
A megrok.responseheaders/megrok/responseheaders/testing.py
A megrok.responseheaders/megrok/responseheaders/tests.py
A megrok.responseheaders/setup.py
-=-
Added: megrok.responseheaders/buildout.cfg
===================================================================
--- megrok.responseheaders/buildout.cfg (rev 0)
+++ megrok.responseheaders/buildout.cfg 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,16 @@
+[buildout]
+develop = . grok
+parts = interpreter test
+find-links = http://download.zope.org/distribution/
+extends = grok/versions.cfg
+versions = versions
+
+[interpreter]
+recipe = zc.recipe.egg
+eggs = megrok.responseheaders
+interpreter = python
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = megrok.responseheaders
+defaults = ['--tests-pattern', '^f?tests$', '-v']
\ No newline at end of file
Added: megrok.responseheaders/megrok/__init__.py
===================================================================
--- megrok.responseheaders/megrok/__init__.py (rev 0)
+++ megrok.responseheaders/megrok/__init__.py 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
Added: megrok.responseheaders/megrok/responseheaders/__init__.py
===================================================================
--- megrok.responseheaders/megrok/responseheaders/__init__.py (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/__init__.py 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1 @@
+from headers import http_cache_control, http_content_type
Added: megrok.responseheaders/megrok/responseheaders/configure.zcml
===================================================================
--- megrok.responseheaders/megrok/responseheaders/configure.zcml (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/configure.zcml 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,6 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+ xmlns:grok="http://namespaces.zope.org/grok">
+ <include package="grok" />
+ <includeDependencies package="." />
+ <grok:grok package="." />
+</configure>
Added: megrok.responseheaders/megrok/responseheaders/ftesting.zcml
===================================================================
--- megrok.responseheaders/megrok/responseheaders/ftesting.zcml (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/ftesting.zcml 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,35 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ i18n_domain="megrok.responseheaders"
+ package="megrok.responseheaders"
+ >
+
+ <include package="grok" />
+ <include package="megrok.responseheaders" />
+
+ <!-- Typical functional testing security setup -->
+ <securityPolicy
+ component="zope.securitypolicy.zopepolicy.ZopeSecurityPolicy"
+ />
+
+ <unauthenticatedPrincipal
+ id="zope.anybody"
+ title="Unauthenticated User"
+ />
+ <grant
+ permission="zope.View"
+ principal="zope.anybody"
+ />
+
+ <principal
+ id="zope.mgr"
+ title="Manager"
+ login="mgr"
+ password="mgrpw"
+ />
+
+ <role id="zope.Manager" title="Site Manager" />
+ <grantAll role="zope.Manager" />
+ <grant role="zope.Manager" principal="zope.mgr" />
+
+</configure>
Added: megrok.responseheaders/megrok/responseheaders/ftests/__init__.py
===================================================================
--- megrok.responseheaders/megrok/responseheaders/ftests/__init__.py (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/ftests/__init__.py 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1 @@
+#
\ No newline at end of file
Added: megrok.responseheaders/megrok/responseheaders/ftests/functional.txt
===================================================================
--- megrok.responseheaders/megrok/responseheaders/ftests/functional.txt (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/ftests/functional.txt 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,103 @@
+Functional test of megrok.responseheaders
+********************************
+
+:Test-Layer: functional
+
+Create an instance in Zope of the sample application code::
+
+ >>> from grok import grok
+ >>> from megrok.responseheaders.ftests import sample
+ >>> grok('megrok.responseheaders.ftests.sample') # sets up views
+
+ >>> root = getRootFolder()
+ >>> root['sample'] = sample.Sample()
+ >>> root['sample'].__name__
+ u'sample'
+
+Create a request and get to the view:
+
+ >>> from zope.testbrowser.testing import Browser
+ >>> browser = Browser()
+ >>> browser.handleErrors = False
+ >>> browser.open("http://localhost/sample")
+ >>> browser.contents
+ 'Index view'
+ >>> browser.headers.get('Cache-Control','not set')
+ 'not set'
+
+The index view doesn't set any headers with megrok.responseheaders but
+the sample application defines two other views. One called 'do'
+another called 'dont':
+
+ >>> # 'dont' actually sets the expires headers 24 hours back
+ >>> browser.open('http://localhost/sample/dont')
+ >>> browser.headers.get('Cache-Control')
+ 'private'
+
+ >>> browser.open('http://localhost/sample/do')
+ >>> cc = browser.headers.get('Cache-control')
+ >>> expect_seconds = 60*60*24 * 10 # directive sets days=10
+ >>> expect_cc = 'public,max-age=%s' % expect_seconds
+ >>> cc
+ 'public,max-age=864000'
+ >>> expect_cc
+ 'public,max-age=864000'
+ >>> expires = browser.headers.get('Expires')
+ >>> from zope.datetime import rfc1123_date
+ >>> from time import time
+ >>> expect_expires = rfc1123_date(time() + 10 * 24 * 3600)
+ >>> # To avoid differences in the second don't compare them directly
+ >>> assert expect_expires[:23] == expires[:23], "%r != %r" % (expect_expires[:23], expires[:23])
+ >>> expect_expires[:23] == expires[:23]
+ True
+
+
+We have another cached view that uses the http_cache_control directive
+like like: 'http_cache_control(seconds=1, minutes=1, hours=1, days=1,
+years=1)'
+
+ >>> browser.open('http://localhost/sample/do2')
+ >>> cc = browser.headers.get('Cache-control')
+ >>> expect_seconds = 1 + 60 + 60*60 + 60*60*24 + 60*60*24*365
+ >>> expect_cc = 'public,max-age=%s' % expect_seconds
+ >>> cc
+ 'public,max-age=31626061'
+ >>> expect_cc
+ 'public,max-age=31626061'
+ >>> expires = browser.headers.get('Expires')
+ >>> expect_expires = rfc1123_date(time() + expect_seconds)
+ >>> # To avoid differences in the second don't compare them directly
+ >>> expect_expires[:23] == expires[:23]
+ True
+
+megrok.responseheaders also supports the ability to set the Content-Type header
+quite conveniently.
+
+The default content type of a view when you don't use page templates is
+to return 'text/plain'
+
+ >>> browser.open('http://localhost/sample/index')
+ >>> browser.headers.get('Content-Type')
+ 'text/plain'
+
+When you use a page template on a view, it defaults to text/html with
+charset utf-8:
+
+ >>> browser.open('http://localhost/sample/template1')
+ >>> browser.headers.get('Content-Type')
+ 'text/html;charset=utf-8'
+
+Template2 contains a <meta> tag that sets a content type different
+from 'text/html;charset=utf-8' but the actual header that is sent from
+the server is 'text/html;charset=utf-8':
+
+ >>> browser.open('http://localhost/sample/template2')
+ >>> browser.headers.get('Content-Type')
+ 'text/html;charset=utf-8'
+
+In template3 we use megrok.responseheaders to override the content type and not
+let Grok guess it from the fact that the template is html.
+
+ >>> browser.open('http://localhost/sample/template3')
+ >>> browser.headers.get('Content-Type')
+ 'application/xhtml+xml;charset=utf-8'
Added: megrok.responseheaders/megrok/responseheaders/ftests/sample.py
===================================================================
--- megrok.responseheaders/megrok/responseheaders/ftests/sample.py (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/ftests/sample.py 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,70 @@
+import grok
+
+from megrok.responseheaders import http_cache_control, http_content_type
+
+class Sample(grok.Application, grok.Container):
+ pass
+
+class Index(grok.View):
+ def render(self):
+ return "Index view"
+
+class DoCache(grok.View):
+ grok.name('do')
+
+ http_cache_control(days=10)
+
+ def render(self):
+ return "Cached"
+
+
+class DoCache2(grok.View):
+ grok.name('do2')
+
+ http_cache_control(seconds=1, minutes=1, hours=1, days=1, years=1)
+
+ def render(self):
+ return "Cached"
+
+class DontCache(grok.View):
+ grok.name('dont')
+
+ http_cache_control(private=True)
+
+ def render(self):
+ return "Not cached"
+
+class Template1(grok.View):
+ grok.template('template1')
+
+class Template2(grok.View):
+ grok.template('template2')
+
+
+
+# This is a rather advanced case since we're setting a non-text
+# content type on something where we don't manually control the
+# rendering method.
+# Normally, if you do something like rendering the bulk of a CSV
+# file for example as the output of the view you'll write your
+# own render() method and make sure you render it correctly.
+# Zope will, on HTML content try to be clever and check the
+# content type and if you try to mess with that the publisher will
+# raise an error basically saying that if you want to use a
+# special content type you're on your own.
+
+from zope.publisher.interfaces.http import IResult, IHTTPRequest
+ at grok.adapter(unicode, IHTTPRequest)
+ at grok.implementer(IResult)
+def myresultadapter(string, request):
+ if request.response.getHeader('content-type',''
+ ).startswith('application/xhtml+xml'):
+ return string.encode('utf-8')
+
+class Template3(grok.View):
+
+ http_content_type('application/xhtml+xml')
+
+ grok.template('template2')
+
+
Added: megrok.responseheaders/megrok/responseheaders/ftests/sample_templates/template1.pt
===================================================================
--- megrok.responseheaders/megrok/responseheaders/ftests/sample_templates/template1.pt (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/ftests/sample_templates/template1.pt 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Template1</title>
+ </head>
+ <body>
+ Template1
+ </body>
+</html>
\ No newline at end of file
Added: megrok.responseheaders/megrok/responseheaders/ftests/sample_templates/template2.pt
===================================================================
--- megrok.responseheaders/megrok/responseheaders/ftests/sample_templates/template2.pt (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/ftests/sample_templates/template2.pt 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,13 @@
+<html>
+ <head>
+ <title>Template2</title>
+
+ <!-- stupid but good for the test. -->
+ <!-- In fact, Grok ignores this tag -->
+ <meta type="http-equiv" content="text/plain; charset=latin1" />
+
+ </head>
+ <body>
+ Template2
+ </body>
+</html>
\ No newline at end of file
Added: megrok.responseheaders/megrok/responseheaders/headers.py
===================================================================
--- megrok.responseheaders/megrok/responseheaders/headers.py (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/headers.py 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,113 @@
+from time import time
+
+import grok
+from zope.app.publication.interfaces import IBeforeTraverseEvent
+from zope.datetime import rfc1123_date
+
+import martian
+class http_content_type(martian.Directive):
+ scope = martian.CLASS
+ store = martian.ONCE
+ default = None
+
+ def validate(self, value):
+ # basic text test
+ martian.validateText(self, value)
+
+ # check the mime type
+ mime_type = value.split(';')[0]
+ if not mime_type.find('/') > 0:
+ raise ValueError("Unexpected mimetype %r" % mime_type)
+
+ # check the charset
+ if value.find('charset=') > -1:
+ charset = value[value.find('charset=')+8:]
+ try:
+ unicode('s', charset)
+ except LookupError:
+ raise ValueError("Invalid charset value %r" % charset)
+
+
+class http_cache_control(martian.Directive):
+ scope = martian.CLASS
+ store = martian.ONCE
+
+ # an alternative to this `default=None` you can make a method
+ # called get_default(...)
+ default = None
+
+ def factory(self, seconds=0, minutes=0, hours=0,
+ days=0, years=0, private=False):
+ """ return a dictionary containing all the valid parameters """
+ parameters = locals()
+ parameters.pop('self')
+
+ return parameters
+
+
+ at grok.subscribe(grok.View, IBeforeTraverseEvent)
+def handle(app, event):
+ cache_control = http_cache_control.get(app) # None or {'days'...}
+ if cache_control:
+ assert isinstance(cache_control, dict)
+ for key, value in get_cache_headers(cache_control).items():
+ event.request.response.setHeader(key, value)
+
+ content_type = http_content_type.get(app)
+ charset = getattr(app, 'http_content_type_charset', 'utf-8')
+ if content_type:
+ if charset:
+ event.request.response.setHeader('Content-Type',
+ '%s;charset=%s' % \
+ (content_type, charset))
+ else:
+ event.request.response.setHeader('Content-Type', content_type)
+
+
+def get_cache_headers(parameters):
+ """ return a dict of suitable HTTP headers. The parameters can contain
+ the following keys:
+
+ * seconds (int)
+ * minutes (int)
+ * hours (int)
+ * days (int)
+ * years (int)
+ * private (bool)
+
+ For whatever you pass, it will return both 'Cache-Control' and 'Expires'.
+ """
+ # First, convert whatever the parameters were into one variable 'hours'
+ if parameters.get('private'):
+ hours = -24
+ else:
+ hours = 0
+ hours += parameters.get('seconds', 0) / 60.0 / 60
+ hours += parameters.get('minutes', 0) / 60.0
+ hours += parameters.get('hours', 0)
+ hours += parameters.get('days', 0) * 24
+ hours += parameters.get('years', 0) * 24 * 365
+
+ # TODO: Verify that these are correct again.
+ # TODO: Consider making the "public" optional
+ if hours < 0:
+ # All major sites lite google, yahoo and micrsoft use this
+ return {'Cache-Control': 'private'}
+
+ # Plone.org sets Expires to 1998 and Cache-Control to
+ # 'max-age=0, s-maxage=3600, must-revalidate'
+
+ # This was the old way to do it.
+ #return {'Expires': rfc1123_date(time() + 3600 * int(hours)),
+ # 'Cache-Control': 'private,max-age=0',
+ # 'Pragma': 'no-cache'
+ # }
+
+ else:
+ seconds = int(round(3600 * hours))
+ return {'Expires': rfc1123_date(time() + seconds),
+ 'Cache-Control':
+ 'public,max-age=%d' % seconds
+ }
+
+
Added: megrok.responseheaders/megrok/responseheaders/testing.py
===================================================================
--- megrok.responseheaders/megrok/responseheaders/testing.py (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/testing.py 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,7 @@
+import os.path
+import megrok.responseheaders
+from zope.app.testing.functional import ZCMLLayer
+
+ftesting_zcml = os.path.join(
+ os.path.dirname(megrok.responseheaders.__file__), 'ftesting.zcml')
+FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer')
Added: megrok.responseheaders/megrok/responseheaders/tests.py
===================================================================
--- megrok.responseheaders/megrok/responseheaders/tests.py (rev 0)
+++ megrok.responseheaders/megrok/responseheaders/tests.py 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,4 @@
+import z3c.testsetup
+test_suite = z3c.testsetup.register_all_tests('megrok.responseheaders')
+
+
Added: megrok.responseheaders/setup.py
===================================================================
--- megrok.responseheaders/setup.py (rev 0)
+++ megrok.responseheaders/setup.py 2008-05-06 10:40:24 UTC (rev 86499)
@@ -0,0 +1,33 @@
+from setuptools import setup, find_packages
+import os
+
+version = '0.1'
+
+setup(name='megrok.responseheaders',
+ version=version,
+ description="Grok extension to manage response headers",
+ long_description=open("README.txt").read() + "\n" +
+ open(os.path.join("docs", "HISTORY.txt")).read(),
+ # Get more strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+ classifiers=[
+ "Programming Language :: Python",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ keywords='',
+ author='Peter Bengtsson',
+ author_email='mail at peterbe.com',
+ url='http://svn.zope.org/megrok.responseheaders',
+ license='ZPL',
+ packages=find_packages(exclude=['ez_setup']),
+ namespace_packages=['megrok'],
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ 'setuptools',
+ 'grok',
+ 'z3c.testsetup',
+ ],
+ entry_points="""
+ # -*- Entry points: -*-
+ """,
+ )
More information about the Checkins
mailing list