[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