├── src └── djexceptional │ ├── models.py │ ├── tests │ ├── __init__.py │ └── memoize.py │ ├── utils.py │ └── __init__.py ├── test └── example │ ├── __init__.py │ ├── templates │ └── 500.html │ ├── README.md │ ├── manage.py │ ├── urls.py │ └── settings.py ├── MANIFEST.in ├── .gitignore ├── setup.py ├── UNLICENSE ├── README.md └── distribute_setup.py /src/djexceptional/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include distribute_setup.py -------------------------------------------------------------------------------- /test/example/templates/500.html: -------------------------------------------------------------------------------- 1 | Error! 2 | -------------------------------------------------------------------------------- /src/djexceptional/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from djexceptional.tests.memoize import MemoizeTest 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | *.pyo 4 | .DS_Store 5 | build 6 | dist 7 | distribute-* 8 | MANIFEST 9 | test/example/*.sqlite3 10 | test/example/.virtualenv 11 | test/example/exceptional.key 12 | -------------------------------------------------------------------------------- /test/example/README.md: -------------------------------------------------------------------------------- 1 | # Testing/Example App 2 | 3 | To run the unit tests, use `./manage.py test djexceptional`. 4 | 5 | To run the example server, first write your API key to `./exceptional.key` 6 | (which has conveniently been added to the `.gitignore` file) and then just use 7 | `./manage.py runserver`. Visit and then wait for the 8 | exception to show up on . 9 | -------------------------------------------------------------------------------- /test/example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import re 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | from setuptools import setup 10 | 11 | 12 | rel_file = lambda *args: os.path.join(os.path.dirname(os.path.abspath(__file__)), *args) 13 | 14 | def read_from(filename): 15 | fp = open(filename) 16 | try: 17 | return fp.read() 18 | finally: 19 | fp.close() 20 | 21 | def get_version(): 22 | data = read_from(rel_file('src', 'djexceptional', '__init__.py')) 23 | return re.search(r"__version__ = '([^']+)'", data).group(1) 24 | 25 | 26 | setup( 27 | name = 'django-exceptional', 28 | version = get_version(), 29 | author = "Zachary Voase", 30 | author_email = "z@zacharyvoase.com", 31 | url = 'http://github.com/zacharyvoase/django-exceptional', 32 | description = "A Django client for Exceptional (getexceptional.com).", 33 | packages = ['djexceptional', 'djexceptional.tests'], 34 | package_dir = {'': 'src'}, 35 | ) 36 | -------------------------------------------------------------------------------- /test/example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | 8 | def just_raise(request): 9 | def f(): 10 | def g(): 11 | raise ValueError("We've run out of Venezuelan Beaver Cheese.") 12 | g() 13 | return f() 14 | 15 | class ClassBasedView(object): 16 | 17 | def method(self, request): 18 | raise TypeError("Something happened inside a method.") 19 | 20 | def __call__(self, request): 21 | raise TypeError("Something happened inside a class.") 22 | 23 | 24 | urlpatterns = patterns('', 25 | # Example: 26 | # (r'^example/', include('example.foo.urls')), 27 | 28 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 29 | # to INSTALLED_APPS to enable admin documentation: 30 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 31 | 32 | # Uncomment the next line to enable the admin: 33 | # (r'^admin/', include(admin.site.urls)), 34 | (r'^$', just_raise), 35 | (r'^class/$', ClassBasedView()), 36 | (r'^method/$', ClassBasedView().method), 37 | ) 38 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `django-exceptional` 2 | 3 | `django-exceptional` is a Django client for [Exceptional][], a service which 4 | tracks errors in your web apps. 5 | 6 | [exceptional]: http://getexceptional.com/ 7 | 8 | 9 | ## Usage 10 | 11 | Install the app: 12 | 13 | pip install django-exceptional 14 | 15 | Add the middleware to your MIDDLEWARE_CLASSES: 16 | 17 | MIDDLEWARE_CLASSES = ( 18 | ... 19 | 'djexceptional.ExceptionalMiddleware', 20 | ... 21 | ) 22 | 23 | Set up your Exceptional API keys in `settings.py`: 24 | 25 | EXCEPTIONAL_API_KEY = '...' 26 | 27 | Done! Remember, the middleware will only log exceptions when `DEBUG` is off. 28 | 29 | 30 | ## (Un)license 31 | 32 | This is free and unencumbered software released into the public domain. 33 | 34 | Anyone is free to copy, modify, publish, use, compile, sell, or 35 | distribute this software, either in source code form or as a compiled 36 | binary, for any purpose, commercial or non-commercial, and by any 37 | means. 38 | 39 | In jurisdictions that recognize copyright laws, the author or authors 40 | of this software dedicate any and all copyright interest in the 41 | software to the public domain. We make this dedication for the benefit 42 | of the public at large and to the detriment of our heirs and 43 | successors. We intend this dedication to be an overt act of 44 | relinquishment in perpetuity of all present and future rights to this 45 | software under copyright law. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 48 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 49 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 50 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 51 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 52 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 53 | OTHER DEALINGS IN THE SOFTWARE. 54 | 55 | For more information, please refer to 56 | -------------------------------------------------------------------------------- /src/djexceptional/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import re 4 | 5 | from django.utils import datetime_safe 6 | from django.utils import simplejson 7 | 8 | 9 | class ResilientJSONEncoder(simplejson.JSONEncoder): 10 | """A JSON encoder (with support for dates/times) that should never fail.""" 11 | 12 | DATE_FORMAT = "%Y-%m-%d" 13 | TIME_FORMAT = "%H:%M:%S" 14 | 15 | def default(self, o): 16 | if isinstance(o, datetime.datetime): 17 | d = datetime_safe.new_datetime(o) 18 | return d.strftime("%s %s" % (self.DATE_FORMAT, self.TIME_FORMAT)) 19 | elif isinstance(o, datetime.date): 20 | d = datetime_safe.new_date(o) 21 | return d.strftime(self.DATE_FORMAT) 22 | elif isinstance(o, datetime.time): 23 | return o.strftime(self.TIME_FORMAT) 24 | elif isinstance(o, decimal.Decimal): 25 | return str(o) 26 | else: 27 | try: 28 | return super(ResilientJSONEncoder, self).default(o) 29 | except Exception: 30 | return repr(o) 31 | 32 | 33 | def json_dumps(obj): 34 | """Dump an object to a JSON string, using the resilient JSON encoder.""" 35 | 36 | return simplejson.dumps(obj, cls=ResilientJSONEncoder) 37 | 38 | 39 | def meta_to_http(meta): 40 | """Convert a request.META into a dictionary of HTTP headers.""" 41 | 42 | headers = {} 43 | for key in meta: 44 | if key.startswith("HTTP_"): 45 | # A heuristic; HTTP_X_FORWARDED_FOR => X-Forwarded-For 46 | header = re.sub(r'^HTTP_', '', key) 47 | header = key.replace("_", " ").title().replace(" ", "-") 48 | elif key in ["CONTENT_LENGTH", "CONTENT_TYPE"]: 49 | header = key.replace("_", " ").title().replace(" ", "-") 50 | else: 51 | continue 52 | headers[header] = meta[key] 53 | return headers 54 | 55 | 56 | def memoize(func): 57 | """A simple memoize decorator (with no support for keyword arguments).""" 58 | 59 | cache = {} 60 | def wrapper(*args): 61 | if args in cache: 62 | return cache[args] 63 | cache[args] = value = func(*args) 64 | return value 65 | 66 | wrapper.__name__ = func.__name__ 67 | wrapper.__doc__ = func.__doc__ 68 | if hasattr(func, '__module__'): 69 | wrapper.__module__ = func.__module__ 70 | wrapper.clear = cache.clear 71 | 72 | return wrapper 73 | -------------------------------------------------------------------------------- /test/example/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | 3 | import os 4 | import sys 5 | 6 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 7 | sys.path.append(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'src')) 8 | 9 | 10 | DEBUG = False 11 | TEMPLATE_DEBUG = DEBUG 12 | 13 | ADMINS = ( 14 | ('Zachary Voase', 'z@zacharyvoase.com'), 15 | ) 16 | 17 | MANAGERS = ADMINS 18 | 19 | DATABASE_ENGINE = 'sqlite3' 20 | DATABASE_NAME = 'dev.sqlite3' 21 | DATABASE_USER = '' 22 | DATABASE_PASSWORD = '' 23 | DATABASE_HOST = '' 24 | DATABASE_PORT = '' 25 | 26 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 27 | CACHE_BACKEND = "dummy://" 28 | 29 | # Local time zone for this installation. Choices can be found here: 30 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 31 | # although not all choices may be available on all operating systems. 32 | # If running in a Windows environment this must be set to the same as your 33 | # system time zone. 34 | TIME_ZONE = 'Europe/London' 35 | 36 | # Language code for this installation. All choices can be found here: 37 | # http://www.i18nguy.com/unicode/language-identifiers.html 38 | LANGUAGE_CODE = 'en-gb' 39 | 40 | SITE_ID = 1 41 | 42 | # If you set this to False, Django will make some optimizations so as not 43 | # to load the internationalization machinery. 44 | USE_I18N = True 45 | 46 | # Absolute path to the directory that holds media. 47 | # Example: "/home/media/media.lawrence.com/" 48 | MEDIA_ROOT = '' 49 | 50 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 51 | # trailing slash if there is a path component (optional in other cases). 52 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 53 | MEDIA_URL = '' 54 | 55 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 56 | # trailing slash. 57 | # Examples: "http://foo.com/media/", "/media/". 58 | ADMIN_MEDIA_PREFIX = '/media/' 59 | 60 | # Make this unique, and don't share it with anybody. 61 | SECRET_KEY = '8@+k3lm3=s+ml6_*(cnpbg1w=6k9xpk5f=irs+&j4_6i=62fy^' 62 | 63 | EXCEPTIONAL_API_KEY = open("exceptional.key").read().strip() 64 | 65 | # List of callables that know how to import templates from various sources. 66 | TEMPLATE_LOADERS = ( 67 | 'django.template.loaders.filesystem.load_template_source', 68 | 'django.template.loaders.app_directories.load_template_source', 69 | # 'django.template.loaders.eggs.load_template_source', 70 | ) 71 | 72 | MIDDLEWARE_CLASSES = ( 73 | 'django.middleware.common.CommonMiddleware', 74 | 'django.contrib.sessions.middleware.SessionMiddleware', 75 | 'djexceptional.ExceptionalMiddleware', 76 | ) 77 | 78 | ROOT_URLCONF = 'example.urls' 79 | 80 | TEMPLATE_DIRS = ( 81 | os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates'), 82 | ) 83 | 84 | INSTALLED_APPS = ( 85 | 'django.contrib.sessions', 86 | 'djexceptional', # So we can run the app tests. 87 | ) 88 | -------------------------------------------------------------------------------- /src/djexceptional/tests/memoize.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from djexceptional.utils import memoize 4 | 5 | 6 | class MemoizeTest(TestCase): 7 | 8 | def test_no_args(self): 9 | """Test @memoize on functions without any arguments.""" 10 | 11 | counter = [] 12 | def increment_counter(): 13 | counter.append(None) 14 | return len(counter) 15 | 16 | self.assertEqual(len(counter), 0) 17 | self.assertEqual(increment_counter(), 1) 18 | self.assertEqual(len(counter), 1) 19 | self.assertEqual(increment_counter(), 2) 20 | self.assertEqual(len(counter), 2) 21 | 22 | increment_counter = memoize(increment_counter) 23 | 24 | self.assertEqual(increment_counter(), 3) 25 | self.assertEqual(len(counter), 3) 26 | self.assertEqual(increment_counter(), 3) 27 | self.assertEqual(len(counter), 3) 28 | 29 | def test_args(self): 30 | """Test @memoize on functions with arguments.""" 31 | 32 | counter = [] 33 | def sum_squared(x, y): 34 | counter.append(None) 35 | return x ** 2 + y ** 2 36 | 37 | self.assertEqual(len(counter), 0) 38 | self.assertEqual(sum_squared(3, 4), 25) 39 | self.assertEqual(len(counter), 1) 40 | self.assertEqual(sum_squared(4, 5), 41) 41 | self.assertEqual(len(counter), 2) 42 | 43 | sum_squared = memoize(sum_squared) 44 | 45 | self.assertEqual(sum_squared(3, 4), 25) 46 | self.assertEqual(len(counter), 3) 47 | self.assertEqual(sum_squared(3, 4), 25) 48 | self.assertEqual(sum_squared(3, 4), 25) 49 | self.assertEqual(sum_squared(3, 4), 25) 50 | self.assertEqual(len(counter), 3) 51 | 52 | self.assertEqual(sum_squared(4, 5), 41) 53 | self.assertEqual(len(counter), 4) 54 | self.assertEqual(sum_squared(4, 5), 41) 55 | self.assertEqual(sum_squared(4, 5), 41) 56 | self.assertEqual(sum_squared(4, 5), 41) 57 | self.assertEqual(len(counter), 4) 58 | 59 | def test_clear(self): 60 | """Test the `clear()` method added to wrapped functions.""" 61 | 62 | counter = [] 63 | def increment_counter(): 64 | counter.append(None) 65 | return len(counter) 66 | increment_counter = memoize(increment_counter) 67 | 68 | self.assertEqual(len(counter), 0) 69 | self.assertEqual(increment_counter(), 1) 70 | self.assertEqual(len(counter), 1) 71 | self.assertEqual(increment_counter(), 1) 72 | self.assertEqual(increment_counter(), 1) 73 | self.assertEqual(increment_counter(), 1) 74 | self.assertEqual(len(counter), 1) 75 | 76 | increment_counter.clear() 77 | 78 | self.assertEqual(increment_counter(), 2) 79 | self.assertEqual(len(counter), 2) 80 | self.assertEqual(increment_counter(), 2) 81 | self.assertEqual(increment_counter(), 2) 82 | self.assertEqual(increment_counter(), 2) 83 | self.assertEqual(len(counter), 2) 84 | -------------------------------------------------------------------------------- /src/djexceptional/__init__.py: -------------------------------------------------------------------------------- 1 | from cStringIO import StringIO 2 | 3 | import datetime 4 | import gzip 5 | import inspect 6 | import logging 7 | import os 8 | import sys 9 | import traceback 10 | import urllib 11 | import urllib2 12 | 13 | from django.conf import settings 14 | from django.core.exceptions import MiddlewareNotUsed, ImproperlyConfigured 15 | from django.core.urlresolvers import resolve 16 | 17 | from djexceptional.utils import memoize, json_dumps, meta_to_http 18 | 19 | 20 | __version__ = '0.1.5' 21 | 22 | EXCEPTIONAL_PROTOCOL_VERSION = 6 23 | EXCEPTIONAL_API_ENDPOINT = getattr(settings, 'EXCEPTIONAL_API_ENDPOINT', 24 | "http://api.getexceptional.com/api/errors") 25 | 26 | LOG = logging.getLogger('djexceptional') 27 | 28 | 29 | class ExceptionalMiddleware(object): 30 | 31 | """ 32 | Middleware to interface with the Exceptional service. 33 | 34 | Requires very little intervention on behalf of the user; you just need to 35 | add `EXCEPTIONAL_API_KEY` to your Django settings. You can also optionally 36 | set `EXCEPTIONAL_API_ENDPOINT` to change the API endpoint which will be 37 | used; the default is `'http://api.getexceptional.com/api/errors'`. 38 | """ 39 | 40 | def __init__(self): 41 | if settings.DEBUG: 42 | raise MiddlewareNotUsed 43 | 44 | try: 45 | self.api_key = settings.EXCEPTIONAL_API_KEY 46 | except AttributeError: 47 | raise ImproperlyConfigured("You need to add an EXCEPTIONAL_API_KEY setting.") 48 | 49 | self.api_endpoint = EXCEPTIONAL_API_ENDPOINT + "?" + urllib.urlencode({ 50 | "api_key": self.api_key, 51 | "protocol_version": EXCEPTIONAL_PROTOCOL_VERSION 52 | }) 53 | 54 | def process_exception(self, request, exc): 55 | info = {} 56 | info.update(self.environment_info()) 57 | info.update(self.request_info(request)) 58 | info.update(self.exception_info(exc, sys.exc_info()[2])) 59 | 60 | payload = self.compress(json_dumps(info)) 61 | req = urllib2.Request(self.api_endpoint, data=payload) 62 | req.headers['Content-Encoding'] = 'gzip' 63 | req.headers['Content-Type'] = 'application/json' 64 | 65 | try: 66 | conn = urllib2.urlopen(req) 67 | try: 68 | conn.read() 69 | finally: 70 | conn.close() 71 | except Exception, exc: 72 | LOG.exception("Error communicating with the Exceptional service: %r", exc) 73 | 74 | @staticmethod 75 | def compress(bytes): 76 | """Compress a bytestring using gzip.""" 77 | 78 | stream = StringIO() 79 | # Use `compresslevel=1`; it's the least compressive but it's fast. 80 | gzstream = gzip.GzipFile(fileobj=stream, compresslevel=1, mode='wb') 81 | try: 82 | try: 83 | gzstream.write(bytes) 84 | finally: 85 | gzstream.close() 86 | return stream.getvalue() 87 | finally: 88 | stream.close() 89 | 90 | @memoize 91 | def environment_info(self): 92 | 93 | """ 94 | Return a dictionary representing the server environment. 95 | 96 | The idea is that the result of this function will rarely (if ever) 97 | change for a given app instance. Ergo, the result can be cached between 98 | requests. 99 | """ 100 | 101 | return { 102 | "application_environment": { 103 | "framework": "django", 104 | "env": dict(os.environ), 105 | "language": "python", 106 | "language_version": sys.version.replace('\n', ''), 107 | "application_root_directory": self.project_root() 108 | }, 109 | "client": { 110 | "name": "django-exceptional", 111 | "version": __version__, 112 | "protocol_version": EXCEPTIONAL_PROTOCOL_VERSION 113 | } 114 | } 115 | 116 | def request_info(self, request): 117 | 118 | """ 119 | Return a dictionary of information for a given request. 120 | 121 | This will be run once for every request. 122 | """ 123 | 124 | # We have to re-resolve the request path here, because the information 125 | # is not stored on the request. 126 | view, args, kwargs = resolve(request.path) 127 | for i, arg in enumerate(args): 128 | kwargs[i] = arg 129 | 130 | view_name = self.get_view_name(view) 131 | 132 | parameters = {} 133 | parameters.update(kwargs) 134 | parameters.update(request.POST.items()) 135 | parameters = self.filter_params(parameters) 136 | 137 | return { 138 | "request": { 139 | "session": dict(request.session), 140 | "remote_ip": request.META["REMOTE_ADDR"], 141 | "parameters": parameters, 142 | "controller": view_name[0], 143 | "action": view_name[1], 144 | "url": request.build_absolute_uri(), 145 | "request_method": request.method, 146 | "headers": meta_to_http(request.META) 147 | } 148 | } 149 | 150 | def exception_info(self, exception, tb, timestamp=None): 151 | backtrace = [] 152 | for tb_part in traceback.format_tb(tb): 153 | backtrace.extend(tb_part.rstrip().splitlines()) 154 | 155 | if timestamp is None: 156 | timestamp = datetime.datetime.utcnow() 157 | 158 | return { 159 | "exception": { 160 | # Naively assume all times are in UTC. 161 | "occurred_at": timestamp.isoformat() + 'Z', 162 | "message": str(exception), 163 | "backtrace": backtrace, 164 | "exception_class": self.exception_class(exception) 165 | } 166 | } 167 | 168 | def exception_class(self, exception): 169 | """Return a name representing the class of an exception.""" 170 | 171 | cls = type(exception) 172 | if cls.__module__ == 'exceptions': # Built-in exception. 173 | return cls.__name__ 174 | return "%s.%s" % (cls.__module__, cls.__name__) 175 | 176 | @memoize 177 | def project_root(self): 178 | 179 | """ 180 | Return the root of the current Django project on the filesystem. 181 | 182 | Looks for `settings.PROJECT_ROOT`; failing that, uses the directory 183 | containing the current settings file. 184 | """ 185 | 186 | if hasattr(settings, 'PROJECT_ROOT'): 187 | return settings.PROJECT_ROOT 188 | 189 | settings_file = sys.modules[settings.SETTINGS_MODULE].__file__ 190 | if settings_file.endswith(".pyc"): 191 | return settings_file[:-1] 192 | return settings_file 193 | 194 | @staticmethod 195 | def get_view_name(view): 196 | """Resolve a Django view object into a controller/action name pair.""" 197 | 198 | if inspect.isfunction(view): 199 | # function_module, function_name 200 | return view.__module__, view.__name__ 201 | elif inspect.ismethod(view): 202 | # class_module.ClassName, method_name 203 | return (view.im_class.__module__ + '.' + view.im_class.__name__, 204 | view.__name__) 205 | # class_module, ClassName 206 | return view.__class__.__module__, view.__class__.__name__ 207 | 208 | @staticmethod 209 | def filter_params(params): 210 | """Filter sensitive information out of parameter dictionaries.""" 211 | 212 | for key in params.keys(): 213 | if "password" in key: 214 | del params[key] 215 | return params 216 | -------------------------------------------------------------------------------- /distribute_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap distribute installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import sys 18 | import time 19 | import fnmatch 20 | import tempfile 21 | import tarfile 22 | from distutils import log 23 | 24 | try: 25 | from site import USER_SITE 26 | except ImportError: 27 | USER_SITE = None 28 | 29 | try: 30 | import subprocess 31 | 32 | def _python_cmd(*args): 33 | args = (sys.executable,) + args 34 | return subprocess.call(args) == 0 35 | 36 | except ImportError: 37 | # will be used for python 2.3 38 | def _python_cmd(*args): 39 | args = (sys.executable,) + args 40 | # quoting arguments if windows 41 | if sys.platform == 'win32': 42 | def quote(arg): 43 | if ' ' in arg: 44 | return '"%s"' % arg 45 | return arg 46 | args = [quote(arg) for arg in args] 47 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 48 | 49 | DEFAULT_VERSION = "0.6.21" 50 | DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" 51 | SETUPTOOLS_FAKED_VERSION = "0.6c11" 52 | 53 | SETUPTOOLS_PKG_INFO = """\ 54 | Metadata-Version: 1.0 55 | Name: setuptools 56 | Version: %s 57 | Summary: xxxx 58 | Home-page: xxx 59 | Author: xxx 60 | Author-email: xxx 61 | License: xxx 62 | Description: xxx 63 | """ % SETUPTOOLS_FAKED_VERSION 64 | 65 | 66 | def _install(tarball): 67 | # extracting the tarball 68 | tmpdir = tempfile.mkdtemp() 69 | log.warn('Extracting in %s', tmpdir) 70 | old_wd = os.getcwd() 71 | try: 72 | os.chdir(tmpdir) 73 | tar = tarfile.open(tarball) 74 | _extractall(tar) 75 | tar.close() 76 | 77 | # going in the directory 78 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 79 | os.chdir(subdir) 80 | log.warn('Now working in %s', subdir) 81 | 82 | # installing 83 | log.warn('Installing Distribute') 84 | if not _python_cmd('setup.py', 'install'): 85 | log.warn('Something went wrong during the installation.') 86 | log.warn('See the error message above.') 87 | finally: 88 | os.chdir(old_wd) 89 | 90 | 91 | def _build_egg(egg, tarball, to_dir): 92 | # extracting the tarball 93 | tmpdir = tempfile.mkdtemp() 94 | log.warn('Extracting in %s', tmpdir) 95 | old_wd = os.getcwd() 96 | try: 97 | os.chdir(tmpdir) 98 | tar = tarfile.open(tarball) 99 | _extractall(tar) 100 | tar.close() 101 | 102 | # going in the directory 103 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 104 | os.chdir(subdir) 105 | log.warn('Now working in %s', subdir) 106 | 107 | # building an egg 108 | log.warn('Building a Distribute egg in %s', to_dir) 109 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 110 | 111 | finally: 112 | os.chdir(old_wd) 113 | # returning the result 114 | log.warn(egg) 115 | if not os.path.exists(egg): 116 | raise IOError('Could not build the egg.') 117 | 118 | 119 | def _do_download(version, download_base, to_dir, download_delay): 120 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' 121 | % (version, sys.version_info[0], sys.version_info[1])) 122 | if not os.path.exists(egg): 123 | tarball = download_setuptools(version, download_base, 124 | to_dir, download_delay) 125 | _build_egg(egg, tarball, to_dir) 126 | sys.path.insert(0, egg) 127 | import setuptools 128 | setuptools.bootstrap_install_from = egg 129 | 130 | 131 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 132 | to_dir=os.curdir, download_delay=15, no_fake=True): 133 | # making sure we use the absolute path 134 | to_dir = os.path.abspath(to_dir) 135 | was_imported = 'pkg_resources' in sys.modules or \ 136 | 'setuptools' in sys.modules 137 | try: 138 | try: 139 | import pkg_resources 140 | if not hasattr(pkg_resources, '_distribute'): 141 | if not no_fake: 142 | _fake_setuptools() 143 | raise ImportError 144 | except ImportError: 145 | return _do_download(version, download_base, to_dir, download_delay) 146 | try: 147 | pkg_resources.require("distribute>="+version) 148 | return 149 | except pkg_resources.VersionConflict: 150 | e = sys.exc_info()[1] 151 | if was_imported: 152 | sys.stderr.write( 153 | "The required version of distribute (>=%s) is not available,\n" 154 | "and can't be installed while this script is running. Please\n" 155 | "install a more recent version first, using\n" 156 | "'easy_install -U distribute'." 157 | "\n\n(Currently using %r)\n" % (version, e.args[0])) 158 | sys.exit(2) 159 | else: 160 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 161 | return _do_download(version, download_base, to_dir, 162 | download_delay) 163 | except pkg_resources.DistributionNotFound: 164 | return _do_download(version, download_base, to_dir, 165 | download_delay) 166 | finally: 167 | if not no_fake: 168 | _create_fake_setuptools_pkg_info(to_dir) 169 | 170 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 171 | to_dir=os.curdir, delay=15): 172 | """Download distribute from a specified location and return its filename 173 | 174 | `version` should be a valid distribute version number that is available 175 | as an egg for download under the `download_base` URL (which should end 176 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 177 | `delay` is the number of seconds to pause before an actual download 178 | attempt. 179 | """ 180 | # making sure we use the absolute path 181 | to_dir = os.path.abspath(to_dir) 182 | try: 183 | from urllib.request import urlopen 184 | except ImportError: 185 | from urllib2 import urlopen 186 | tgz_name = "distribute-%s.tar.gz" % version 187 | url = download_base + tgz_name 188 | saveto = os.path.join(to_dir, tgz_name) 189 | src = dst = None 190 | if not os.path.exists(saveto): # Avoid repeated downloads 191 | try: 192 | log.warn("Downloading %s", url) 193 | src = urlopen(url) 194 | # Read/write all in one block, so we don't create a corrupt file 195 | # if the download is interrupted. 196 | data = src.read() 197 | dst = open(saveto, "wb") 198 | dst.write(data) 199 | finally: 200 | if src: 201 | src.close() 202 | if dst: 203 | dst.close() 204 | return os.path.realpath(saveto) 205 | 206 | def _no_sandbox(function): 207 | def __no_sandbox(*args, **kw): 208 | try: 209 | from setuptools.sandbox import DirectorySandbox 210 | if not hasattr(DirectorySandbox, '_old'): 211 | def violation(*args): 212 | pass 213 | DirectorySandbox._old = DirectorySandbox._violation 214 | DirectorySandbox._violation = violation 215 | patched = True 216 | else: 217 | patched = False 218 | except ImportError: 219 | patched = False 220 | 221 | try: 222 | return function(*args, **kw) 223 | finally: 224 | if patched: 225 | DirectorySandbox._violation = DirectorySandbox._old 226 | del DirectorySandbox._old 227 | 228 | return __no_sandbox 229 | 230 | def _patch_file(path, content): 231 | """Will backup the file then patch it""" 232 | existing_content = open(path).read() 233 | if existing_content == content: 234 | # already patched 235 | log.warn('Already patched.') 236 | return False 237 | log.warn('Patching...') 238 | _rename_path(path) 239 | f = open(path, 'w') 240 | try: 241 | f.write(content) 242 | finally: 243 | f.close() 244 | return True 245 | 246 | _patch_file = _no_sandbox(_patch_file) 247 | 248 | def _same_content(path, content): 249 | return open(path).read() == content 250 | 251 | def _rename_path(path): 252 | new_name = path + '.OLD.%s' % time.time() 253 | log.warn('Renaming %s into %s', path, new_name) 254 | os.rename(path, new_name) 255 | return new_name 256 | 257 | def _remove_flat_installation(placeholder): 258 | if not os.path.isdir(placeholder): 259 | log.warn('Unkown installation at %s', placeholder) 260 | return False 261 | found = False 262 | for file in os.listdir(placeholder): 263 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'): 264 | found = True 265 | break 266 | if not found: 267 | log.warn('Could not locate setuptools*.egg-info') 268 | return 269 | 270 | log.warn('Removing elements out of the way...') 271 | pkg_info = os.path.join(placeholder, file) 272 | if os.path.isdir(pkg_info): 273 | patched = _patch_egg_dir(pkg_info) 274 | else: 275 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) 276 | 277 | if not patched: 278 | log.warn('%s already patched.', pkg_info) 279 | return False 280 | # now let's move the files out of the way 281 | for element in ('setuptools', 'pkg_resources.py', 'site.py'): 282 | element = os.path.join(placeholder, element) 283 | if os.path.exists(element): 284 | _rename_path(element) 285 | else: 286 | log.warn('Could not find the %s element of the ' 287 | 'Setuptools distribution', element) 288 | return True 289 | 290 | _remove_flat_installation = _no_sandbox(_remove_flat_installation) 291 | 292 | def _after_install(dist): 293 | log.warn('After install bootstrap.') 294 | placeholder = dist.get_command_obj('install').install_purelib 295 | _create_fake_setuptools_pkg_info(placeholder) 296 | 297 | def _create_fake_setuptools_pkg_info(placeholder): 298 | if not placeholder or not os.path.exists(placeholder): 299 | log.warn('Could not find the install location') 300 | return 301 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 302 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \ 303 | (SETUPTOOLS_FAKED_VERSION, pyver) 304 | pkg_info = os.path.join(placeholder, setuptools_file) 305 | if os.path.exists(pkg_info): 306 | log.warn('%s already exists', pkg_info) 307 | return 308 | 309 | log.warn('Creating %s', pkg_info) 310 | f = open(pkg_info, 'w') 311 | try: 312 | f.write(SETUPTOOLS_PKG_INFO) 313 | finally: 314 | f.close() 315 | 316 | pth_file = os.path.join(placeholder, 'setuptools.pth') 317 | log.warn('Creating %s', pth_file) 318 | f = open(pth_file, 'w') 319 | try: 320 | f.write(os.path.join(os.curdir, setuptools_file)) 321 | finally: 322 | f.close() 323 | 324 | _create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) 325 | 326 | def _patch_egg_dir(path): 327 | # let's check if it's already patched 328 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 329 | if os.path.exists(pkg_info): 330 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): 331 | log.warn('%s already patched.', pkg_info) 332 | return False 333 | _rename_path(path) 334 | os.mkdir(path) 335 | os.mkdir(os.path.join(path, 'EGG-INFO')) 336 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 337 | f = open(pkg_info, 'w') 338 | try: 339 | f.write(SETUPTOOLS_PKG_INFO) 340 | finally: 341 | f.close() 342 | return True 343 | 344 | _patch_egg_dir = _no_sandbox(_patch_egg_dir) 345 | 346 | def _before_install(): 347 | log.warn('Before install bootstrap.') 348 | _fake_setuptools() 349 | 350 | 351 | def _under_prefix(location): 352 | if 'install' not in sys.argv: 353 | return True 354 | args = sys.argv[sys.argv.index('install')+1:] 355 | for index, arg in enumerate(args): 356 | for option in ('--root', '--prefix'): 357 | if arg.startswith('%s=' % option): 358 | top_dir = arg.split('root=')[-1] 359 | return location.startswith(top_dir) 360 | elif arg == option: 361 | if len(args) > index: 362 | top_dir = args[index+1] 363 | return location.startswith(top_dir) 364 | if arg == '--user' and USER_SITE is not None: 365 | return location.startswith(USER_SITE) 366 | return True 367 | 368 | 369 | def _fake_setuptools(): 370 | log.warn('Scanning installed packages') 371 | try: 372 | import pkg_resources 373 | except ImportError: 374 | # we're cool 375 | log.warn('Setuptools or Distribute does not seem to be installed.') 376 | return 377 | ws = pkg_resources.working_set 378 | try: 379 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', 380 | replacement=False)) 381 | except TypeError: 382 | # old distribute API 383 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) 384 | 385 | if setuptools_dist is None: 386 | log.warn('No setuptools distribution found') 387 | return 388 | # detecting if it was already faked 389 | setuptools_location = setuptools_dist.location 390 | log.warn('Setuptools installation detected at %s', setuptools_location) 391 | 392 | # if --root or --preix was provided, and if 393 | # setuptools is not located in them, we don't patch it 394 | if not _under_prefix(setuptools_location): 395 | log.warn('Not patching, --root or --prefix is installing Distribute' 396 | ' in another location') 397 | return 398 | 399 | # let's see if its an egg 400 | if not setuptools_location.endswith('.egg'): 401 | log.warn('Non-egg installation') 402 | res = _remove_flat_installation(setuptools_location) 403 | if not res: 404 | return 405 | else: 406 | log.warn('Egg installation') 407 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') 408 | if (os.path.exists(pkg_info) and 409 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): 410 | log.warn('Already patched.') 411 | return 412 | log.warn('Patching...') 413 | # let's create a fake egg replacing setuptools one 414 | res = _patch_egg_dir(setuptools_location) 415 | if not res: 416 | return 417 | log.warn('Patched done.') 418 | _relaunch() 419 | 420 | 421 | def _relaunch(): 422 | log.warn('Relaunching...') 423 | # we have to relaunch the process 424 | # pip marker to avoid a relaunch bug 425 | if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: 426 | sys.argv[0] = 'setup.py' 427 | args = [sys.executable] + sys.argv 428 | sys.exit(subprocess.call(args)) 429 | 430 | 431 | def _extractall(self, path=".", members=None): 432 | """Extract all members from the archive to the current working 433 | directory and set owner, modification time and permissions on 434 | directories afterwards. `path' specifies a different directory 435 | to extract to. `members' is optional and must be a subset of the 436 | list returned by getmembers(). 437 | """ 438 | import copy 439 | import operator 440 | from tarfile import ExtractError 441 | directories = [] 442 | 443 | if members is None: 444 | members = self 445 | 446 | for tarinfo in members: 447 | if tarinfo.isdir(): 448 | # Extract directories with a safe mode. 449 | directories.append(tarinfo) 450 | tarinfo = copy.copy(tarinfo) 451 | tarinfo.mode = 448 # decimal for oct 0700 452 | self.extract(tarinfo, path) 453 | 454 | # Reverse sort directories. 455 | if sys.version_info < (2, 4): 456 | def sorter(dir1, dir2): 457 | return cmp(dir1.name, dir2.name) 458 | directories.sort(sorter) 459 | directories.reverse() 460 | else: 461 | directories.sort(key=operator.attrgetter('name'), reverse=True) 462 | 463 | # Set correct owner, mtime and filemode on directories. 464 | for tarinfo in directories: 465 | dirpath = os.path.join(path, tarinfo.name) 466 | try: 467 | self.chown(tarinfo, dirpath) 468 | self.utime(tarinfo, dirpath) 469 | self.chmod(tarinfo, dirpath) 470 | except ExtractError: 471 | e = sys.exc_info()[1] 472 | if self.errorlevel > 1: 473 | raise 474 | else: 475 | self._dbg(1, "tarfile: %s" % e) 476 | 477 | 478 | def main(argv, version=DEFAULT_VERSION): 479 | """Install or upgrade setuptools and EasyInstall""" 480 | tarball = download_setuptools() 481 | _install(tarball) 482 | 483 | 484 | if __name__ == '__main__': 485 | main(sys.argv[1:]) 486 | --------------------------------------------------------------------------------