├── LICENSE.txt ├── djng ├── __init__.py ├── errors.py ├── middleware.py ├── response.py ├── router.py ├── services │ ├── __init__.py │ ├── base.py │ ├── cache │ │ └── __init__.py │ ├── configure.py │ ├── mail │ │ └── __init__.py │ └── manager.py ├── template │ ├── __init__.py │ └── template_response.py └── wsgi.py ├── djng_old.py ├── example_forms.py ├── example_hello.py ├── example_middleware.py ├── example_rest_view.py ├── example_services_incomplete.py ├── example_template.py ├── example_templates └── example.html ├── example_urls.py ├── planned_services.txt ├── readme.txt ├── services_api_ideas.txt └── setup.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Simon Willison 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /djng/__init__.py: -------------------------------------------------------------------------------- 1 | # Some settings are just too much work to monkey-patch around 2 | from django.conf import settings 3 | settings.configure(USE_18N = False) 4 | del settings 5 | 6 | import middleware 7 | from django.conf.urls.defaults import url 8 | from router import Router 9 | from errors import ErrorWrapper 10 | from response import Response 11 | from wsgi import serve 12 | from django import forms 13 | from django.utils.html import escape 14 | from django.utils.safestring import mark_safe 15 | import template 16 | from template import TemplateResponse 17 | -------------------------------------------------------------------------------- /djng/errors.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from response import Response 3 | 4 | class ErrorWrapper(object): 5 | def __init__(self, app, custom_404 = None, custom_500 = None): 6 | self.app = app 7 | self.error_404 = custom_404 or self.default_error_404 8 | self.error_500 = custom_500 or self.default_error_404 9 | 10 | def __call__(self, request): 11 | try: 12 | response = self.app(request) 13 | except Http404, e: 14 | return self.error_404(request) 15 | except Exception, e: 16 | return self.error_500(request, e) 17 | return response 18 | 19 | def default_error_404(self, request): 20 | return Response('A 404 error occurred', status=404) 21 | 22 | def default_error_500(self, request, e): 23 | return Response('A 500 error occurred: %r' % e, status=505) 24 | -------------------------------------------------------------------------------- /djng/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import decorator_from_middleware 2 | from django.middleware.gzip import GZipMiddleware 3 | 4 | GZip = decorator_from_middleware(GZipMiddleware) 5 | del GZipMiddleware -------------------------------------------------------------------------------- /djng/response.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse as HttpResponseOld 2 | from Cookie import SimpleCookie 3 | 4 | class Response(HttpResponseOld): 5 | _charset = 'utf8' 6 | def __init__(self, content='', status=None, content_type=None): 7 | if not content_type: 8 | content_type = 'text/html; charset=%s' % self._charset 9 | if not isinstance(content, basestring) and\ 10 | hasattr(content, '__iter__'): 11 | self._container = content 12 | self._is_string = False 13 | else: 14 | self._container = [content] 15 | self._is_string = True 16 | self.cookies = SimpleCookie() 17 | if status: 18 | self.status_code = status 19 | self._headers = {'content-type': ('Content-Type', content_type)} 20 | -------------------------------------------------------------------------------- /djng/router.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns 2 | from django.core import urlresolvers 3 | 4 | class Router(object): 5 | """ 6 | Convenient wrapper around Django's urlresolvers, allowing them to be used 7 | from normal application code. 8 | 9 | from django.http import HttpResponse 10 | from django_openid.request_factory import RequestFactory 11 | from django.conf.urls.defaults import url 12 | router = Router( 13 | url('^foo/$', lambda r: HttpResponse('foo'), name='foo'), 14 | url('^bar/$', lambda r: HttpResponse('bar'), name='bar') 15 | ) 16 | rf = RequestFactory() 17 | print router(rf.get('/bar/')) 18 | """ 19 | def __init__(self, *urlpairs): 20 | self.urlpatterns = patterns('', *urlpairs) 21 | # for 1.0 compatibility we pass in None for urlconf_name and then 22 | # modify the _urlconf_module to make self hack as if its the module. 23 | self.resolver = urlresolvers.RegexURLResolver(r'^/', None) 24 | self.resolver._urlconf_module = self 25 | 26 | def handle(self, request): 27 | path = request.path_info 28 | callback, callback_args, callback_kwargs = self.resolver.resolve(path) 29 | return callback(request, *callback_args, **callback_kwargs) 30 | 31 | def __call__(self, request): 32 | return self.handle(request) -------------------------------------------------------------------------------- /djng/services/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Services 3 | -------- 4 | 5 | Services are classes that can have their underlying implementation swapped out 6 | at runtime. 7 | 8 | """ 9 | 10 | class Service(object): 11 | implementation = None 12 | 13 | class TemplateService(Service): 14 | def render(self, template, context): 15 | return template % context 16 | -------------------------------------------------------------------------------- /djng/services/base.py: -------------------------------------------------------------------------------- 1 | from manager import ServiceManager 2 | 3 | class ServiceConfigurationError(Exception): 4 | pass 5 | 6 | 7 | def proxy(methodname, servicemanager): 8 | def method(self, *args, **kwargs): 9 | return getattr(servicemanager.current(), methodname)(*args, **kwargs) 10 | return method 11 | 12 | class ServiceMeta(type): 13 | def __new__(cls, name, bases, attrs): 14 | # First add service manager instance to attrs 15 | attrs['service'] = ServiceManager() 16 | # All attrs methods are converted in to proxies 17 | for key, value in attrs.items(): 18 | if callable(value): 19 | # TODO: inspect funcargs, copy them and the docstring so that 20 | # introspection tools will tell us correct arguments 21 | attrs[key] = proxy(key, attrs['service']) 22 | return super(ServiceMeta, cls).__new__(cls, name, bases, attrs) 23 | 24 | class Service(object): 25 | __metaclass__ = ServiceMeta 26 | -------------------------------------------------------------------------------- /djng/services/cache/__init__.py: -------------------------------------------------------------------------------- 1 | from djng.services.base import Service, ServiceConfigurationError 2 | 3 | class CacheConfigure(object): 4 | def __init__(self, next, impl=None, in_memory=False): 5 | self.next = next 6 | if impl and in_memory: 7 | raise ServiceConfigurationError, 'Only one of impl or in_memory' 8 | if not (impl or in_memory): 9 | raise ServiceConfigurationError, 'One of impl or in_memory reqd.' 10 | if in_memory: 11 | impl = DictCache() 12 | self.impl = impl 13 | 14 | def __call__(self, *args, **kwargs): 15 | cache.service.push(self.impl) 16 | try: 17 | return self.next(*args, **kwargs) 18 | finally: 19 | obj = cache.service.pop() 20 | assert obj == self.impl, 'Popped the wrong cache implementation!' 21 | 22 | class CacheService(Service): 23 | def get(self, key): 24 | pass 25 | 26 | def set(self, key, value): 27 | pass 28 | 29 | class DictCache(object): 30 | def __init__(self): 31 | self._d = {} 32 | 33 | def get(self, key): 34 | return self._d.get(key) 35 | 36 | def set(self, key, value): 37 | self._d[key] = value 38 | 39 | class UpperDictCache(DictCache): 40 | def get(self, key): 41 | return self._d.get(key, '').upper() 42 | 43 | cache = CacheService() -------------------------------------------------------------------------------- /djng/services/configure.py: -------------------------------------------------------------------------------- 1 | class Configure(object): 2 | def __init__(self, next, **kwargs): 3 | """ 4 | **kwargs should have keys that are names of services and value that 5 | are implementations of those services. 6 | """ 7 | for name, impl in kwargs.items(): 8 | self.get_service(name).push(impl) 9 | 10 | def get_service(self, name): 11 | # TODO: implement this 12 | pass 13 | 14 | -------------------------------------------------------------------------------- /djng/services/mail/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonw/djng/953eb33390972cbdd0ac0a52e3b23bfdd55e2cfe/djng/services/mail/__init__.py -------------------------------------------------------------------------------- /djng/services/manager.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | class ServiceNotConfigured(Exception): 4 | # TODO: This needs to indicate WHICH service is not configured 5 | pass 6 | 7 | class ServiceManager(threading.local): 8 | """ 9 | A ServiceManager keeps track of the available implementations for a 10 | given service, and which implementation is currently the default. It 11 | provides methods for registering new implementations and pushing and 12 | popping a stack representing the default implementation. 13 | """ 14 | def __init__(self, default_implementation=None): 15 | self.clear_stack() 16 | if default_implementation is not None: 17 | self.push(default_implementation) 18 | 19 | def clear_stack(self): 20 | self._stack = [] 21 | 22 | def push(self, impl): 23 | self._stack.insert(0, impl) 24 | 25 | def pop(self): 26 | return self._stack.pop(0) 27 | 28 | def current(self): 29 | if not self._stack: 30 | raise ServiceNotConfigured 31 | return self._stack[0] 32 | -------------------------------------------------------------------------------- /djng/template/__init__.py: -------------------------------------------------------------------------------- 1 | from template_response import TemplateResponse 2 | from django.conf import settings 3 | 4 | def configure(template_dirs): 5 | if isinstance(template_dirs, basestring): 6 | template_dirs = [template_dirs] 7 | settings.TEMPLATE_DIRS = template_dirs 8 | -------------------------------------------------------------------------------- /djng/template/template_response.py: -------------------------------------------------------------------------------- 1 | from djng.response import Response 2 | from django.template import loader, RequestContext 3 | 4 | class TemplateResponse(Response): 5 | def __init__(self, request, template, context = None): 6 | self.context = context or {} 7 | self.template = template 8 | self.request = request 9 | super(TemplateResponse, self).__init__() 10 | 11 | def get_container(self): 12 | return [ 13 | loader.get_template(self.template).render( 14 | RequestContext(self.request, self.context) 15 | ) 16 | ] 17 | 18 | def set_container(self, *args): 19 | pass # ignore 20 | 21 | _container = property(get_container, set_container) 22 | -------------------------------------------------------------------------------- /djng/wsgi.py: -------------------------------------------------------------------------------- 1 | # First we have to monkey-patch django.core.handlers.base because 2 | # get_script_name in that module has a dependency on settings which bubbles 3 | # up to affect WSGIRequest and WSGIHandler 4 | from django.utils.encoding import force_unicode 5 | def get_script_name(environ): 6 | script_url = environ.get('SCRIPT_URL', u'') 7 | if not script_url: 8 | script_url = environ.get('REDIRECT_URL', u'') 9 | if script_url: 10 | return force_unicode(script_url[:-len(environ.get('PATH_INFO', ''))]) 11 | return force_unicode(environ.get('SCRIPT_NAME', u'')) 12 | from django.core.handlers import base 13 | base.get_script_name = get_script_name 14 | 15 | # Now on with the real code... 16 | from django import http 17 | from django.core.handlers.wsgi import STATUS_CODE_TEXT 18 | from django.core.handlers.wsgi import WSGIRequest as WSGIRequestOld 19 | import sys 20 | 21 | class WSGIRequest(WSGIRequestOld): 22 | def __init__(self, environ): 23 | super(WSGIRequest, self).__init__(environ) 24 | # Setting self._encoding prevents fallback to django.conf.settings 25 | self._encoding = 'utf8' 26 | 27 | class WSGIWrapper(object): 28 | # Changes that are always applied to a response (in this order). 29 | response_fixes = [ 30 | http.fix_location_header, 31 | http.conditional_content_removal, 32 | http.fix_IE_for_attach, 33 | http.fix_IE_for_vary, 34 | ] 35 | def __init__(self, view): 36 | self.view = view 37 | 38 | def __call__(self, environ, start_response): 39 | request = WSGIRequest(environ) 40 | response = self.view(request) 41 | response = self.apply_response_fixes(request, response) 42 | try: 43 | status_text = STATUS_CODE_TEXT[response.status_code] 44 | except KeyError: 45 | status_text = 'UNKNOWN STATUS CODE' 46 | status = '%s %s' % (response.status_code, status_text) 47 | response_headers = [(str(k), str(v)) for k, v in response.items()] 48 | for c in response.cookies.values(): 49 | response_headers.append(('Set-Cookie', str(c.output(header='')))) 50 | start_response(status, response_headers) 51 | return response 52 | 53 | def apply_response_fixes(self, request, response): 54 | """ 55 | Applies each of the functions in self.response_fixes to the request 56 | and response, modifying the response in the process. Returns the new 57 | response. 58 | """ 59 | for func in self.response_fixes: 60 | response = func(request, response) 61 | return response 62 | 63 | from django.core.servers.basehttp import \ 64 | WSGIRequestHandler as WSGIRequestHandlerOld, \ 65 | BaseHTTPRequestHandler, WSGIServer 66 | 67 | class WSGIRequestHandler(WSGIRequestHandlerOld): 68 | # Just enough to get rid of settings.py dependencies 69 | def __init__(self, *args, **kwargs): 70 | self.path = '' 71 | BaseHTTPRequestHandler.__init__(self, *args, **kwargs) 72 | 73 | def log_message(self, format, *args): 74 | sys.stderr.write( 75 | "[%s] %s\n" % (self.log_date_time_string(), format % args) 76 | ) 77 | 78 | def serve(view, host='localhost', port=6789): 79 | httpd = WSGIServer((host, port), WSGIRequestHandler) 80 | httpd.set_app(WSGIWrapper(view)) 81 | httpd.serve_forever() 82 | -------------------------------------------------------------------------------- /djng_old.py: -------------------------------------------------------------------------------- 1 | """ 2 | Just some sketched out ideas at the moment, this code has never been executed. 3 | """ 4 | 5 | from django import http 6 | from django.core import signals 7 | from django.utils.encoding import force_unicode 8 | from django.utils.importlib import import_module 9 | 10 | from django.core.handlers.wsgi import STATUS_CODE_TEXT, WSGIRequest 11 | 12 | import sys 13 | 14 | class Handler(object): 15 | # Changes that are always applied to a response (in this order). 16 | response_fixes = [ 17 | http.fix_location_header, 18 | http.conditional_content_removal, 19 | http.fix_IE_for_attach, 20 | http.fix_IE_for_vary, 21 | ] 22 | request_middleware = [] 23 | response_middleware = [] 24 | exception_middleware = [] 25 | 26 | debug = False 27 | propagate_exceptions = False 28 | 29 | def __init__(self, router): 30 | self.router = router 31 | 32 | def __call__(self, environ, start_response): 33 | try: 34 | request = WSGIRequest(environ) 35 | except UnicodeDecodeError: 36 | response = http.HttpResponseBadRequest() 37 | else: 38 | response = self.get_response(request) 39 | 40 | # Apply response middleware 41 | for middleware_method in self.response_middleware: 42 | response = middleware_method(request, response) 43 | response = self.apply_response_fixes(request, response) 44 | 45 | try: 46 | status_text = STATUS_CODE_TEXT[response.status_code] 47 | except KeyError: 48 | status_text = 'UNKNOWN STATUS CODE' 49 | status = '%s %s' % (response.status_code, status_text) 50 | response_headers = [(str(k), str(v)) for k, v in response.items()] 51 | for c in response.cookies.values(): 52 | response_headers.append(('Set-Cookie', str(c.output(header='')))) 53 | start_response(status, response_headers) 54 | return response 55 | 56 | def get_response(self, request): 57 | "Returns an HttpResponse object for the given HttpRequest" 58 | from django.core import exceptions, urlresolvers 59 | 60 | # Apply request middleware 61 | for middleware_method in self.request_middleware: 62 | response = middleware_method(request) 63 | if response: 64 | return response 65 | 66 | # Resolve and execute the view, catching any errors 67 | try: 68 | response = self.router(request) 69 | except Exception, e: 70 | # If the view raised an exception, run it through exception 71 | # middleware, and if the exception middleware returns a 72 | # response, use that. Otherwise, reraise the exception. 73 | for middleware_method in self.exception_middleware: 74 | response = middleware_method(request, e) 75 | if response: 76 | return response 77 | raise 78 | except http.Http404, e: 79 | return self.handle_404(request, e) 80 | except exceptions.PermissionDenied: 81 | return self.handle_permission_denied(request) 82 | except SystemExit: 83 | # Allow sys.exit() to actually exit. See tickets #1023 and #4701 84 | raise 85 | except: # Handle everything else, including SuspiciousOperation, etc. 86 | # Get exc_info now, in case another exception is thrown later 87 | exc_info = sys.exc_info() 88 | receivers = signals.got_request_exception.send( 89 | sender=self.__class__, request=request 90 | ) 91 | return self.handle_uncaught_exception(request, exc_info) 92 | 93 | def handle_404(self, request, e): 94 | if self.debug: 95 | from django.views import debug 96 | return debug.technical_404_response(request, e) 97 | else: 98 | return http.HttpResponseNotFound('

404

') 99 | 100 | def handle_permission_denied(self, request): 101 | return http.HttpResponseForbidden('

Permission denied

') 102 | 103 | def handle_uncaught_exception(self, request, exc_info): 104 | """ 105 | Processing for any otherwise uncaught exceptions (those that will 106 | generate HTTP 500 responses). Can be overridden by subclasses who want 107 | customised 500 handling. 108 | 109 | Be *very* careful when overriding this because the error could be 110 | caused by anything, so assuming something like the database is always 111 | available would be an error. 112 | """ 113 | from django.core.mail import mail_admins 114 | 115 | if self.propagate_exceptions: 116 | raise 117 | 118 | if self.debug: 119 | from django.views import debug 120 | return debug.technical_500_response(request, *exc_info) 121 | 122 | # When DEBUG is False, send an error message to the admins. 123 | subject = 'Error: %s' % request.path 124 | try: 125 | request_repr = repr(request) 126 | except: 127 | request_repr = "Request repr() unavailable" 128 | message = "%s\n\n%s" % (self._get_traceback(exc_info), request_repr) 129 | mail_admins(subject, message, fail_silently=True) 130 | # Return an HttpResponse that displays a friendly error message. 131 | return self.handle_500(request, exc_info) 132 | 133 | def _get_traceback(self, exc_info=None): 134 | "Helper function to return the traceback as a string" 135 | import traceback 136 | return '\n'.join( 137 | traceback.format_exception(*(exc_info or sys.exc_info())) 138 | ) 139 | 140 | def apply_response_fixes(self, request, response): 141 | """ 142 | Applies each of the functions in self.response_fixes to the request 143 | and response, modifying the response in the process. Returns the new 144 | response. 145 | """ 146 | for func in self.response_fixes: 147 | response = func(request, response) 148 | return response 149 | 150 | def serve(handler, host='localhost', port=6789): 151 | from django.core.servers.basehttp import run 152 | run(host, int(port), handler) 153 | -------------------------------------------------------------------------------- /example_forms.py: -------------------------------------------------------------------------------- 1 | import djng 2 | 3 | def index(request): 4 | return djng.Response(""" 5 |

Forms demo

6 |
7 |

8 | 9 | 10 |

11 |
12 |
13 |

14 |

15 |
16 | Form validation demo 17 | """) 18 | 19 | def search(request): 20 | return djng.Response( 21 | "This page would search for %s" % djng.escape( 22 | request.GET.get('q', 'no-search-term') 23 | ) 24 | ) 25 | 26 | def submit(request): 27 | text = request.POST.get('text', 'no-text') 28 | return djng.Response(djng.escape(text.upper())) 29 | 30 | class DemoForm(djng.forms.Form): 31 | name = djng.forms.CharField(max_length = 100) 32 | email = djng.forms.EmailField() 33 | optional_text = djng.forms.CharField(required = False) 34 | 35 | def validate(request): 36 | if request.method == 'POST': 37 | form = DemoForm(request.POST) 38 | if form.is_valid(): 39 | return djng.Response('Form was valid: %s' % djng.escape( 40 | repr(form.cleaned_data) 41 | )) 42 | else: 43 | form = DemoForm() 44 | return djng.Response(""" 45 |
46 | %s 47 |

48 |

49 | """ % form.as_p()) 50 | 51 | app = djng.Router( 52 | (r'^$', index), 53 | (r'^search/$', search), 54 | (r'^submit/$', submit), 55 | (r'^validate/$', validate), 56 | ) 57 | 58 | if __name__ == '__main__': 59 | djng.serve(app, '0.0.0.0', 8888) 60 | -------------------------------------------------------------------------------- /example_hello.py: -------------------------------------------------------------------------------- 1 | import djng 2 | 3 | def index(request): 4 | return djng.Response('Hello, world') 5 | 6 | if __name__ == '__main__': 7 | djng.serve(index, '0.0.0.0', 8888) 8 | -------------------------------------------------------------------------------- /example_middleware.py: -------------------------------------------------------------------------------- 1 | import djng 2 | 3 | def hello(request): 4 | return djng.Response('Hello, world ' * 100) 5 | 6 | def goodbye(request): 7 | return djng.Response('Goodbye, world ' * 100) 8 | 9 | app = djng.Router( 10 | (r'^hello$', hello), 11 | (r'^goodbye$', djng.middleware.GZip(goodbye)), 12 | ) 13 | 14 | if __name__ == '__main__': 15 | djng.serve(app, '0.0.0.0', 8888) 16 | -------------------------------------------------------------------------------- /example_rest_view.py: -------------------------------------------------------------------------------- 1 | import djng 2 | 3 | class RestView(object): 4 | def __call__(self, request, *args, **kwargs): 5 | method = request.method.upper() 6 | if hasattr(self, method): 7 | return getattr(self, method)(request, *args, **kwargs) 8 | return self.method_not_supported(request) 9 | 10 | @staticmethod 11 | def method_not_supported(request): 12 | return djng.Response('Method not supported') 13 | 14 | 15 | class MyView(RestView): 16 | @staticmethod 17 | def GET(request): 18 | return djng.Response('This is a GET') 19 | 20 | @staticmethod 21 | def POST(request): 22 | return djng.Response('This is a POST') 23 | 24 | if __name__ == '__main__': 25 | djng.serve(MyView(), '0.0.0.0', 8888) 26 | -------------------------------------------------------------------------------- /example_services_incomplete.py: -------------------------------------------------------------------------------- 1 | from djng import services 2 | from djng.services.cache import CacheConfigure 3 | 4 | # Default service configuration 5 | services.configure('cache', CacheConfigure( 6 | in_memory = True, 7 | )) 8 | # Or maybe this: 9 | # services.cache.configure(CacheConfigure(in_memory = True)) 10 | # Or even: 11 | # services.cache.configure(in_memory = True) 12 | # Or... 13 | # services.default('cache', InMemoryCache()) 14 | # Or... 15 | # services.configure('cache', InMemoryCache()) 16 | 17 | def app(request): 18 | from djng.services.cache import cache 19 | counter = cache.get('counter') 20 | if not counter: 21 | counter = 1 22 | else: 23 | counter += 1 24 | cache.set('counter', counter) 25 | print counter 26 | 27 | app(None) 28 | app(None) 29 | 30 | # Middleware that reconfigures service for the duration of the request 31 | app = services.wrap(app, 'cache', InMemoryCache()) 32 | 33 | # Or... 34 | app = services.wrap(app, 35 | cache = InMemoryCache(), 36 | ) 37 | 38 | 39 | app(None) 40 | app(None) 41 | app(None) 42 | -------------------------------------------------------------------------------- /example_template.py: -------------------------------------------------------------------------------- 1 | import djng, os, datetime 2 | 3 | djng.template.configure( 4 | os.path.join(os.path.dirname(__file__), 'example_templates') 5 | ) 6 | 7 | def index(request): 8 | return djng.TemplateResponse(request, 'example.html', { 9 | 'time': str(datetime.datetime.now()), 10 | }) 11 | 12 | if __name__ == '__main__': 13 | djng.serve(index, '0.0.0.0', 8888) 14 | -------------------------------------------------------------------------------- /example_templates/example.html: -------------------------------------------------------------------------------- 1 | Hello from djng! {{ time }} 2 | -------------------------------------------------------------------------------- /example_urls.py: -------------------------------------------------------------------------------- 1 | import djng 2 | 3 | app = djng.ErrorWrapper( 4 | djng.Router( 5 | (r'^hello$', lambda request: djng.Response('Hello, world')), 6 | (r'^goodbye$', lambda request: djng.Response('Goodbye, world')), 7 | ), 8 | custom_404 = lambda request: djng.Response('404 error', status=404), 9 | custom_500 = lambda request: djng.Response('500 error', status=500) 10 | ) 11 | 12 | if __name__ == '__main__': 13 | djng.serve(app, '0.0.0.0', 8888) 14 | -------------------------------------------------------------------------------- /planned_services.txt: -------------------------------------------------------------------------------- 1 | What should be a service? 2 | ========================= 3 | 4 | As a general rule, services should be things that get configured in some way. 5 | Not everything in Django should be converted in to a service. If a component 6 | doesn't require any per-site configuration (e.g. forms, syndication feeds) it 7 | should probably be left as a regular library. 8 | 9 | Stuff in Django that should be a service 10 | ---------------------------------------- 11 | * Caching 12 | * Templating 13 | * Sending e-mail 14 | * "Current user" authentication 15 | * Sessions 16 | * Database connection - django.db.connection 17 | * Higher level ORM 18 | * File storage 19 | * Sites - or at least the concept of the "current site" 20 | * i18n / translation 21 | * URL reversing 22 | 23 | Stuff that isn't yet in Django but should be a service 24 | ------------------------------------------------------ 25 | * HTTP client 26 | * Logging 27 | * Message queue 28 | * Cryptography (in particular signing things) 29 | 30 | Stuff that shouldn't be a service 31 | --------------------------------- 32 | * The admin: it's just an application 33 | * Signals: they are part of core Django itself 34 | * Forms: they are a library, they don't need configuring at all 35 | * Testing: core Django, lives above services as has tools for mocking them 36 | * Sitemaps / syndication: again, these are libraries 37 | 38 | Stuff I'm not sure about 39 | ------------------------ 40 | * Content Types / generic relations -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | djng 2 | ==== 3 | (pronounced "djing", with a mostly-silent "d") 4 | 5 | Blog entry: http://simonwillison.net/2009/May/19/djng/ 6 | Mailing list: http://groups.google.com/group/djng 7 | 8 | djng is a micro-framework that depends on a macro-framework (Django). 9 | 10 | My definition of a micro-framework: something that lets you create an entire 11 | Python web application in a single module: 12 | 13 | import djng 14 | 15 | def index(request): 16 | return djng.Response('Hello, world') 17 | 18 | if __name__ == '__main__': 19 | djng.serve(index, '0.0.0.0', 8888) 20 | 21 | Or if you want hello and goodbye URLs, and a custom 404 page: 22 | 23 | import djng 24 | 25 | app = djng.ErrorWrapper( 26 | djng.Router( 27 | (r'^hello$', lambda request: djng.Response('Hello, world')), 28 | (r'^goodbye$', lambda request: djng.Response('Goodbye, world')), 29 | ), 30 | custom_404 = lambda request: djng.Response('404 error', status=404), 31 | custom_500 = lambda request: djng.Response('500 error', status=500) 32 | ) 33 | 34 | if __name__ == '__main__': 35 | djng.serve(app, '0.0.0.0', 8888) 36 | 37 | Under the hood, djng will re-use large amounts of functionality from Django, 38 | while re-imagining various aspects of the framework. A djng request object is 39 | a Django HttpRequest object; a djng response object is a Django HttpResponse. 40 | Django's template language and ORM will be available. Ideally, Django code 41 | will run almost entirely unmodified under djng, and vice versa. 42 | 43 | Services, not Settings 44 | ====================== 45 | 46 | I dislike Django's settings.py file - I often find I want to reconfigure 47 | settings at run-time, and I'm not comfortable with having arbitrary settings 48 | for so many different aspects of the framework. 49 | 50 | djng experiments with /services/ in place of settings. Services are bits of 51 | shared functionality that djng makes available to applications - for example, 52 | caching, templating, ORM-ing and mail-sending. 53 | 54 | Most of the stuff that Django sets up in settings.py will in djng be set up by 55 | configuring services. These services will be designed to be reconfigured at 56 | run-time, using a mechanism similar to Django middleware. 57 | 58 | Some things that live in settings.py that really don't belong there - 59 | middleware for example. These will generally be constructed by composing 60 | together a djng application in code. 61 | 62 | I'm still figuring out how the syntax for services should work. 63 | -------------------------------------------------------------------------------- /services_api_ideas.txt: -------------------------------------------------------------------------------- 1 | Services API 2 | ============ 3 | 4 | Requirements: 5 | 6 | - Maintain a stack of implementations for each service 7 | - Only one implementation of a service is "active" at a time 8 | - The default API for a service uses the current active implementation 9 | - Implementations can be used without participating in the stack at all 10 | - Middleware can temporarily push a new service on to the stack, for the 11 | duration of the current request 12 | - Services temporarily pushed on to the stack are reliably popped off again 13 | at the end of the current request, even if an exception is raised 14 | 15 | It would be nice if the solution meant that the current Django APIs for things 16 | like accessing the cache or loading a template could remain backwards 17 | compatible. 18 | 19 | Some ideas 20 | ---------- 21 | 22 | # Configure the default service (at the bottom of the stack) 23 | djng.template.configure(template_dirs = ('templates/default',)) 24 | 25 | # Reconfigure for part of the URL space 26 | app = djng.Router( 27 | (r'^foo', djng.reconfigure( 28 | foo_view, djng.template, 29 | template_dirs = ('templates/foo', 'templates/default') 30 | )), 31 | (r'^bar', bar_view), 32 | ) 33 | 34 | djng.reconfigure is middleware which wrapes foo_view, then duplicates the 35 | current djng.template service and applies a new template_dirs property to it , 36 | based on keyword argument. 37 | 38 | Or... use a decorator: 39 | 40 | app = djng.Router( 41 | (r'^foo', djng.reconfigure( 42 | djng.template, template_dirs = ('templates/foo', 'templates/default') 43 | )(foo_view)), 44 | (r'^bar', bar_view), 45 | ) 46 | 47 | Which could also be written: 48 | 49 | @djng.reconfigure( 50 | djng.template, template_dirs = ('templates/foo', 'templates/default') 51 | ) 52 | def foo_view(request): 53 | # ... 54 | 55 | Reconfigure is a bit strange though, because the majority of services will 56 | probably want a completely new implementation rather than a tweak to the 57 | existing one. Caching is a good example: 58 | 59 | # Set the default cache to be an InMemoryCache 60 | djng.cache.configure(djng.cache.InMemoryCache()) 61 | 62 | # One URL path gets to use memcache instead 63 | app = djng.Router( 64 | (r'^foo', djng.reconfigure( 65 | foo_view, djng.cache, djng.cache.Memcache('127.0.0.1:11221') 66 | )), 67 | (r'^bar', bar_view), 68 | ) 69 | 70 | Or as a decorator: 71 | 72 | app = djng.Router( 73 | (r'^foo', djng.reconfigure( 74 | djng.cache, djng.cache.Memcache('127.0.0.1:11221') 75 | )(foo_view)), 76 | (r'^bar', bar_view), 77 | ) 78 | 79 | The signature of djng.reconfigure feels a bit strange though: 80 | 81 | def reconfigure( 82 | service_to_reconfigure, 83 | [optional new_service_instance], 84 | **reconfigure_kwargs 85 | ): 86 | # ... 87 | 88 | Internally, reconfigure uses a try/finally block to ensure that the altered 89 | service implementation pushed on to the stack is popped off by the end of the 90 | request. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from distutils.core import setup 4 | 5 | 6 | def fullsplit(path, result=None): 7 | """ 8 | Split a pathname into components (the opposite of os.path.join) in a 9 | platform-neutral way. 10 | """ 11 | if result is None: 12 | result = [] 13 | head, tail = os.path.split(path) 14 | if head == '': 15 | return [tail] + result 16 | if head == path: 17 | return result 18 | return fullsplit(head, [tail] + result) 19 | 20 | 21 | data_files, packages = [], [] 22 | djng_dir = "djng" 23 | 24 | for dirpath, dirnames, filenames in os.walk(djng_dir): 25 | # Ignore dirnames that start with "." 26 | for i, dirname in enumerate(dirnames): 27 | if dirname.startswith("."): 28 | del dirnames[i] 29 | if "__init__.py" in filenames: 30 | packages.append(".".join(fullsplit(dirpath))) 31 | elif filenames: 32 | data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) 33 | 34 | setup( 35 | name = "djng", 36 | version = "0.1", 37 | packages = packages, 38 | data_files = data_files, 39 | ) 40 | --------------------------------------------------------------------------------