├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── examples └── soliddjango │ ├── manage.py │ ├── requirements.txt │ └── soliddjango │ ├── __init__.py │ ├── apps │ ├── __init__.py │ └── core │ │ ├── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── setup.py ├── solidpy ├── __init__.py ├── exceptions.py ├── handlers │ ├── __init__.py │ ├── base.py │ └── django.py └── utils │ ├── __init__.py │ ├── stack.py │ └── wsgi.py └── tests ├── __init__.py └── utils ├── __init__.py └── stack_tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | _build/ 3 | build/ 4 | dist/ 5 | amonpy.egg-info/ 6 | bench.log 7 | .sass-cache 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Martin Rusev 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 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE.txt 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Solidpy 3 | ================== 4 | 5 | Solidpy is the python Client for Solid 6 | 7 | =============== 8 | Installation 9 | =============== 10 | 11 | 12 | 1. Install the package with ``pip install solidpy`` or alternatively you can 13 | download the tarball and run ``python setup.py solidpy`` 14 | 15 | 16 | ================ 17 | Django 18 | ================ 19 | To capture and log exceptions in settings.py add the following: 20 | 21 | 22 | :: 23 | 24 | SOLID_CONFIG = { 25 | 'url': 'http://solid_instance:port', 26 | 'secret': 'secret_key' 27 | } 28 | 29 | 30 | :: 31 | 32 | MIDDLEWARE_CLASSES = ( 33 | ..... 34 | 'solidpy.handlers.django.DjangoExceptionMiddleware' 35 | ) 36 | 37 | =============== 38 | Requirements 39 | =============== 40 | 41 | 42 | Python 2.6+ 43 | 44 | requests 45 | 46 | -------------------------------------------------------------------------------- /examples/soliddjango/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "soliddjango.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /examples/soliddjango/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.5.1 2 | requests==2.20.0 -------------------------------------------------------------------------------- /examples/soliddjango/soliddjango/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinrusev/solid-python/930f944ce616f63af24afe7750a429b6123bb239/examples/soliddjango/soliddjango/__init__.py -------------------------------------------------------------------------------- /examples/soliddjango/soliddjango/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinrusev/solid-python/930f944ce616f63af24afe7750a429b6123bb239/examples/soliddjango/soliddjango/apps/__init__.py -------------------------------------------------------------------------------- /examples/soliddjango/soliddjango/apps/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinrusev/solid-python/930f944ce616f63af24afe7750a429b6123bb239/examples/soliddjango/soliddjango/apps/core/__init__.py -------------------------------------------------------------------------------- /examples/soliddjango/soliddjango/apps/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /examples/soliddjango/soliddjango/apps/core/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /examples/soliddjango/soliddjango/apps/core/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | def home(request): 4 | 5 | raise 6 | 7 | return HttpResponse('test') 8 | -------------------------------------------------------------------------------- /examples/soliddjango/soliddjango/settings.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, '/home/martin/solid-python') 3 | 4 | DEBUG = True 5 | TEMPLATE_DEBUG = DEBUG 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 10 | 'NAME': '', # Or path to database file if using sqlite3. 11 | # The following settings are not used with sqlite3: 12 | 'USER': '', 13 | 'PASSWORD': '', 14 | 'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. 15 | 'PORT': '', # Set to empty string for default. 16 | } 17 | } 18 | 19 | ALLOWED_HOSTS = [] 20 | 21 | TIME_ZONE = 'Europe/Sofia' 22 | 23 | LANGUAGE_CODE = 'en-us' 24 | 25 | SITE_ID = 1 26 | 27 | 28 | USE_I18N = False 29 | USE_L10N = False 30 | 31 | USE_TZ = True 32 | 33 | MEDIA_ROOT = '' 34 | MEDIA_URL = '' 35 | 36 | STATIC_ROOT = '' 37 | STATIC_URL = '/static/' 38 | 39 | STATICFILES_DIRS = ( 40 | 41 | ) 42 | 43 | STATICFILES_FINDERS = ( 44 | 'django.contrib.staticfiles.finders.FileSystemFinder', 45 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 46 | ) 47 | 48 | SECRET_KEY = '2%zbj$wlu7ju@xl()#1dtu1%7p-tvvrgomv#hfw^*xoy4+=g^h' 49 | 50 | TEMPLATE_LOADERS = ( 51 | 'django.template.loaders.filesystem.Loader', 52 | 'django.template.loaders.app_directories.Loader', 53 | ) 54 | 55 | MIDDLEWARE_CLASSES = ( 56 | 'django.middleware.common.CommonMiddleware', 57 | 'django.contrib.sessions.middleware.SessionMiddleware', 58 | 'django.middleware.csrf.CsrfViewMiddleware', 59 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 60 | 'django.contrib.messages.middleware.MessageMiddleware', 61 | 'solidpy.handlers.django.SolidDjangoMiddleware' 62 | ) 63 | 64 | ROOT_URLCONF = 'soliddjango.urls' 65 | 66 | # Python dotted path to the WSGI application used by Django's runserver. 67 | WSGI_APPLICATION = 'soliddjango.wsgi.application' 68 | 69 | TEMPLATE_DIRS = ( 70 | 71 | ) 72 | 73 | INSTALLED_APPS = ( 74 | 'django.contrib.auth', 75 | 'django.contrib.contenttypes', 76 | 'django.contrib.sessions', 77 | 'django.contrib.sites', 78 | 'django.contrib.messages', 79 | 'django.contrib.staticfiles', 80 | 'soliddjango.apps.core', 81 | 82 | ) 83 | 84 | SOLID_CONFIG = { 85 | 'url': 'http://127.0.0.1:6464', 86 | 'secret': "p86f9cfgEuvl4HFGthR1TBAUE7Sfiz8NoDGTeNHh7Sw" 87 | } 88 | 89 | LOGGING = { 90 | 'version': 1, 91 | 'disable_existing_loggers': False, 92 | 'filters': { 93 | 'require_debug_false': { 94 | '()': 'django.utils.log.RequireDebugFalse' 95 | } 96 | }, 97 | 'handlers': { 98 | 'mail_admins': { 99 | 'level': 'ERROR', 100 | 'filters': ['require_debug_false'], 101 | 'class': 'django.utils.log.AdminEmailHandler' 102 | } 103 | }, 104 | 'loggers': { 105 | 'django.request': { 106 | 'handlers': ['mail_admins'], 107 | 'level': 'ERROR', 108 | 'propagate': True, 109 | }, 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /examples/soliddjango/soliddjango/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Examples: 9 | url(r'^$', 'soliddjango.apps.core.views.home', name='home'), 10 | # url(r'^soliddjango/', include('soliddjango.foo.urls')), 11 | 12 | # Uncomment the admin/doc line below to enable admin documentation: 13 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 14 | 15 | # Uncomment the next line to enable the admin: 16 | # url(r'^admin/', include(admin.site.urls)), 17 | ) 18 | -------------------------------------------------------------------------------- /examples/soliddjango/soliddjango/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for soliddjango project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "soliddjango.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "soliddjango.settings") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | application = get_wsgi_application() 29 | 30 | # Apply WSGI middleware here. 31 | # from helloworld.wsgi import HelloWorldApplication 32 | # application = HelloWorldApplication(application) 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | version = '1.0' 4 | 5 | setup( 6 | name='solidpy', 7 | version=version, 8 | description="Python client for the Solid API", 9 | long_description= open('README.rst').read(), 10 | keywords='error tracking, exception logging', 11 | author='Martin Rusev', 12 | author_email='martin@solidapp.io', 13 | url='https://github.com/martinrusev/solid-python', 14 | license='MIT', 15 | packages=['solidpy','solidpy.handlers'], 16 | package_dir={'solidpy':'solidpy'}, 17 | zip_safe=False, 18 | install_requires=['requests',], 19 | classifiers=[ 20 | 'Intended Audience :: Developers', 21 | 'Programming Language :: Python :: 2.6', 22 | 'Programming Language :: Python :: 2.7', 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /solidpy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinrusev/solid-python/930f944ce616f63af24afe7750a429b6123bb239/solidpy/__init__.py -------------------------------------------------------------------------------- /solidpy/exceptions.py: -------------------------------------------------------------------------------- 1 | class ConnectionException(Exception): 2 | " Raised when the Solid Web interface does not respond" 3 | -------------------------------------------------------------------------------- /solidpy/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinrusev/solid-python/930f944ce616f63af24afe7750a429b6123bb239/solidpy/handlers/__init__.py -------------------------------------------------------------------------------- /solidpy/handlers/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | from solidpy.exceptions import ConnectionException 5 | 6 | 7 | class SolidBaseHandler(object): 8 | 9 | def send(self, data, config_dict): 10 | headers = {"Content-type": "application/json"} 11 | 12 | errors = {'connection': 'Could not establish connection to '} 13 | 14 | 15 | url = config_dict.get('url', 'http://127.0.0.1:6464') 16 | secret_key = config_dict.get('secret', None) 17 | 18 | post_url = "{0}/api/exception/{1}".format(url, secret_key) 19 | 20 | data = json.dumps(data) 21 | 22 | r = requests.post(post_url, data, headers=headers, timeout=5) 23 | 24 | if r.status_code != 200: 25 | error = "{0}-{1}".format(errors['connection'], url) 26 | 27 | raise ConnectionException(error) 28 | 29 | -------------------------------------------------------------------------------- /solidpy/handlers/django.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import traceback 4 | import inspect 5 | import sys 6 | import os 7 | 8 | from django.core.urlresolvers import resolve 9 | from django.conf import settings 10 | 11 | from solidpy.handlers.base import SolidBaseHandler 12 | from solidpy.utils.wsgi import get_headers, get_environ 13 | from solidpy.utils.stack import get_lines_from_file 14 | 15 | class SolidDjangoMiddleware(SolidBaseHandler): 16 | 17 | 18 | def process_exception(self, request, exc): 19 | exception_dict = {} 20 | 21 | exception_dict.update(self.exception_info(exc, sys.exc_info()[2])) 22 | exception_dict['request'] = self.request_info(request) 23 | 24 | self.send(exception_dict, settings.SOLID_CONFIG) 25 | 26 | 27 | def exception_class(self, exception): 28 | """Return a name representing the class of an exception.""" 29 | 30 | cls = type(exception) 31 | if cls.__module__ == 'exceptions': # Built-in exception. 32 | return cls.__name__ 33 | return "%s.%s" % (cls.__module__, cls.__name__) 34 | 35 | def request_info(self, request): 36 | 37 | """ 38 | Return a dictionary of information for a given request. 39 | 40 | This will be run once for every request. 41 | """ 42 | 43 | # We have to re-resolve the request path here, because the information 44 | # is not stored on the request. 45 | view, args, kwargs = resolve(request.path) 46 | for i, arg in enumerate(args): 47 | kwargs[i] = arg 48 | 49 | parameters = {} 50 | parameters.update(kwargs) 51 | parameters.update(request.POST.items()) 52 | 53 | environ = request.META 54 | 55 | return { 56 | "session": dict(request.session), 57 | 'cookies': dict(request.COOKIES), 58 | 'headers': dict(get_headers(environ)), 59 | 'env': dict(get_environ(environ)), 60 | "remote_ip": request.META["REMOTE_ADDR"], 61 | "parameters": parameters, 62 | "action": view.__name__, 63 | "application": view.__module__, 64 | "method": request.method, 65 | "url": request.build_absolute_uri() 66 | } 67 | 68 | def exception_info(self, exception, tb): 69 | 70 | culprit_filepath, lineno, method, error = traceback.extract_tb(tb)[-1] 71 | 72 | context = get_lines_from_file(filepath=culprit_filepath, culprit_lineno=lineno) 73 | 74 | backtrace = [] 75 | for tb_part in traceback.format_tb(tb): 76 | backtrace.extend(tb_part.rstrip().splitlines()) 77 | 78 | return { 79 | "message": str(exception), 80 | "backtrace": backtrace, 81 | "context": context, 82 | "exception_class": self.exception_class(exception) 83 | } 84 | 85 | -------------------------------------------------------------------------------- /solidpy/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinrusev/solid-python/930f944ce616f63af24afe7750a429b6123bb239/solidpy/utils/__init__.py -------------------------------------------------------------------------------- /solidpy/utils/stack.py: -------------------------------------------------------------------------------- 1 | def get_lines_from_file(filepath=None, culprit_lineno=0): 2 | 3 | pre_context, post_context, context_line = [], [], [] 4 | file_contents = open(filepath).readlines() 5 | 6 | # The index starts from 0, so the actual lineno is -1 7 | actual_culprit_lineno = culprit_lineno-1 8 | actual_culprit_lineno = 0 if actual_culprit_lineno < 0 else actual_culprit_lineno 9 | 10 | 11 | pre_culprit_lines = actual_culprit_lineno-5 12 | pre_culprit_lines = 0 if pre_culprit_lines < 0 else pre_culprit_lines 13 | 14 | for i in range(pre_culprit_lines, actual_culprit_lineno): 15 | line = file_contents[i] if len(file_contents) > i else False 16 | if line is not False: 17 | pre_context.append((i+1, line)) 18 | 19 | post_culprit_lines = actual_culprit_lineno+5 20 | post_culprit_index_start = actual_culprit_lineno+1 21 | 22 | for i in range(post_culprit_index_start, post_culprit_lines): 23 | line = file_contents[i] if len(file_contents) > i else False 24 | if line is not False: 25 | post_context.append((i+1, line)) 26 | 27 | context_line_contents = file_contents[actual_culprit_lineno] if len(file_contents) > actual_culprit_lineno else False 28 | if context_line_contents is not False: 29 | context_line = (culprit_lineno, context_line_contents) 30 | 31 | context_dict = { 32 | 'culprit_lineno': culprit_lineno, 33 | 'filepath': filepath, 34 | 'pre_context': pre_context, 35 | 'context_line': context_line, 36 | 'post_context': post_context, 37 | } 38 | 39 | 40 | return context_dict 41 | -------------------------------------------------------------------------------- /solidpy/utils/wsgi.py: -------------------------------------------------------------------------------- 1 | # `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` 2 | def get_headers(environ): 3 | """ 4 | Returns only proper HTTP headers. 5 | """ 6 | for key, value in environ.iteritems(): 7 | key = str(key) 8 | if key.startswith('HTTP_') and key not in \ 9 | ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'): 10 | yield key[5:].replace('_', '-').title(), value 11 | elif key in ('CONTENT_TYPE', 'CONTENT_LENGTH'): 12 | yield key.replace('_', '-').title(), value 13 | 14 | 15 | def get_environ(environ): 16 | """ 17 | Returns our whitelisted environment variables. 18 | """ 19 | for key in ('REMOTE_ADDR', 'SERVER_NAME', 'SERVER_PORT'): 20 | if key in environ: 21 | yield key, environ[key] 22 | 23 | 24 | # `get_host` comes from `werkzeug.wsgi` 25 | def get_host(environ): 26 | """Return the real host for the given WSGI environment. This takes care 27 | of the `X-Forwarded-Host` header. 28 | 29 | :param environ: the WSGI environment to get the host of. 30 | """ 31 | scheme = environ.get('wsgi.url_scheme') 32 | if 'HTTP_X_FORWARDED_HOST' in environ: 33 | result = environ['HTTP_X_FORWARDED_HOST'] 34 | elif 'HTTP_HOST' in environ: 35 | result = environ['HTTP_HOST'] 36 | else: 37 | result = environ['SERVER_NAME'] 38 | if (scheme, str(environ['SERVER_PORT'])) not \ 39 | in (('https', '443'), ('http', '80')): 40 | result += ':' + environ['SERVER_PORT'] 41 | if result.endswith(':80') and scheme == 'http': 42 | result = result[:-3] 43 | elif result.endswith(':443') and scheme == 'https': 44 | result = result[:-4] 45 | return result 46 | 47 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinrusev/solid-python/930f944ce616f63af24afe7750a429b6123bb239/tests/__init__.py -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinrusev/solid-python/930f944ce616f63af24afe7750a429b6123bb239/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/stack_tests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | import tempfile 4 | 5 | from nose.tools import eq_ 6 | from solidpy.utils.stack import get_lines_from_file 7 | 8 | 9 | class TestStack(unittest.TestCase): 10 | 11 | def test_no_exceptions_raised(self): 12 | temp = tempfile.NamedTemporaryFile(mode='w+t', delete=False) 13 | temp.writelines(["oh hello there\n", ]) 14 | temp.seek(0) 15 | 16 | self.assertTrue(get_lines_from_file(temp.name, culprit_lineno=10)) 17 | temp.close() 18 | 19 | 20 | def test_context(self): 21 | temp = tempfile.NamedTemporaryFile(mode='w+t', delete=False) 22 | temp.writelines(["line 1\n", "line 2\n", "line 3\n", "line 4\n" ,"line 5\n" ]) 23 | temp.seek(0) 24 | 25 | result = get_lines_from_file(temp.name, culprit_lineno=4) 26 | 27 | eq_(result['context_line'], (4 ,"line 4\n")) 28 | eq_(result['pre_context'], [(1, 'line 1\n'), (2, 'line 2\n'), (3, 'line 3\n')]) 29 | eq_(result['post_context'], [(5, 'line 5\n')]) 30 | 31 | 32 | temp.close() 33 | 34 | 35 | --------------------------------------------------------------------------------