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