├── 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('