mirror of
				https://github.com/graphql-python/graphene.git
				synced 2025-10-30 23:47:55 +03:00 
			
		
		
		
	Initial version debug schema in django
This commit is contained in:
		
							parent
							
								
									0fa7f5c9aa
								
							
						
					
					
						commit
						14439155ee
					
				
							
								
								
									
										4
									
								
								graphene/contrib/django/debug/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								graphene/contrib/django/debug/__init__.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| from .schema import DebugSchema | ||||
| from .types import DjangoDebug | ||||
| 
 | ||||
| __all__ = ['DebugSchema', 'DjangoDebug'] | ||||
							
								
								
									
										68
									
								
								graphene/contrib/django/debug/schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								graphene/contrib/django/debug/schema.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,68 @@ | |||
| from django.db import connections | ||||
| 
 | ||||
| from ....core.schema import Schema | ||||
| from ....core.types import Field | ||||
| from .sql.tracking import unwrap_cursor, wrap_cursor | ||||
| from .sql.types import DjangoDebugSQL | ||||
| from .types import DjangoDebug | ||||
| 
 | ||||
| 
 | ||||
| class WrappedRoot(object): | ||||
| 
 | ||||
|     def __init__(self, root): | ||||
|         self._recorded = [] | ||||
|         self._root = root | ||||
| 
 | ||||
|     def record(self, **log): | ||||
|         self._recorded.append(DjangoDebugSQL(**log)) | ||||
| 
 | ||||
|     def debug(self): | ||||
|         return DjangoDebug(sql=self._recorded) | ||||
| 
 | ||||
| 
 | ||||
| class WrapRoot(object): | ||||
| 
 | ||||
|     @property | ||||
|     def _root(self): | ||||
|         return self._wrapped_root.root | ||||
| 
 | ||||
|     @_root.setter | ||||
|     def _root(self, value): | ||||
|         self._wrapped_root = value | ||||
| 
 | ||||
|     def resolve_debug(self, args, info): | ||||
|         return self._wrapped_root.debug() | ||||
| 
 | ||||
| 
 | ||||
| def debug_objecttype(objecttype): | ||||
|     return type('Debug{}'.format(objecttype._meta.type_name), (WrapRoot, objecttype), {'debug': Field(DjangoDebug, name='__debug')}) | ||||
| 
 | ||||
| 
 | ||||
| class DebugSchema(Schema): | ||||
| 
 | ||||
|     @property | ||||
|     def query(self): | ||||
|         if not self._query: | ||||
|             return | ||||
|         return debug_objecttype(self._query) | ||||
| 
 | ||||
|     @query.setter | ||||
|     def query(self, value): | ||||
|         self._query = value | ||||
| 
 | ||||
|     def enable_instrumentation(self, wrapped_root): | ||||
|         # This is thread-safe because database connections are thread-local. | ||||
|         for connection in connections.all(): | ||||
|             wrap_cursor(connection, wrapped_root) | ||||
| 
 | ||||
|     def disable_instrumentation(self): | ||||
|         for connection in connections.all(): | ||||
|             unwrap_cursor(connection) | ||||
| 
 | ||||
|     def execute(self, *args, **kwargs): | ||||
|         root = kwargs.pop('root', object()) | ||||
|         wrapped_root = WrappedRoot(root=root) | ||||
|         self.enable_instrumentation(wrapped_root) | ||||
|         result = super(DebugSchema, self).execute(root=wrapped_root, *args, **kwargs) | ||||
|         self.disable_instrumentation() | ||||
|         return result | ||||
							
								
								
									
										0
									
								
								graphene/contrib/django/debug/sql/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								graphene/contrib/django/debug/sql/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										165
									
								
								graphene/contrib/django/debug/sql/tracking.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								graphene/contrib/django/debug/sql/tracking.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,165 @@ | |||
| # Code obtained from django-debug-toolbar sql panel tracking | ||||
| from __future__ import absolute_import, unicode_literals | ||||
| 
 | ||||
| import json | ||||
| from threading import local | ||||
| from time import time | ||||
| 
 | ||||
| from django.utils import six | ||||
| from django.utils.encoding import force_text | ||||
| 
 | ||||
| 
 | ||||
| class SQLQueryTriggered(Exception): | ||||
|     """Thrown when template panel triggers a query""" | ||||
| 
 | ||||
| 
 | ||||
| class ThreadLocalState(local): | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self.enabled = True | ||||
| 
 | ||||
|     @property | ||||
|     def Wrapper(self): | ||||
|         if self.enabled: | ||||
|             return NormalCursorWrapper | ||||
|         return ExceptionCursorWrapper | ||||
| 
 | ||||
|     def recording(self, v): | ||||
|         self.enabled = v | ||||
| 
 | ||||
| 
 | ||||
| state = ThreadLocalState() | ||||
| recording = state.recording  # export function | ||||
| 
 | ||||
| 
 | ||||
| def wrap_cursor(connection, panel): | ||||
|     if not hasattr(connection, '_djdt_cursor'): | ||||
|         connection._djdt_cursor = connection.cursor | ||||
| 
 | ||||
|         def cursor(): | ||||
|             return state.Wrapper(connection._djdt_cursor(), connection, panel) | ||||
| 
 | ||||
|         connection.cursor = cursor | ||||
|         return cursor | ||||
| 
 | ||||
| 
 | ||||
| def unwrap_cursor(connection): | ||||
|     if hasattr(connection, '_djdt_cursor'): | ||||
|         del connection._djdt_cursor | ||||
|         del connection.cursor | ||||
| 
 | ||||
| 
 | ||||
| class ExceptionCursorWrapper(object): | ||||
|     """ | ||||
|     Wraps a cursor and raises an exception on any operation. | ||||
|     Used in Templates panel. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, cursor, db, logger): | ||||
|         pass | ||||
| 
 | ||||
|     def __getattr__(self, attr): | ||||
|         raise SQLQueryTriggered() | ||||
| 
 | ||||
| 
 | ||||
| class NormalCursorWrapper(object): | ||||
|     """ | ||||
|     Wraps a cursor and logs queries. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, cursor, db, logger): | ||||
|         self.cursor = cursor | ||||
|         # Instance of a BaseDatabaseWrapper subclass | ||||
|         self.db = db | ||||
|         # logger must implement a ``record`` method | ||||
|         self.logger = logger | ||||
| 
 | ||||
|     def _quote_expr(self, element): | ||||
|         if isinstance(element, six.string_types): | ||||
|             return "'%s'" % force_text(element).replace("'", "''") | ||||
|         else: | ||||
|             return repr(element) | ||||
| 
 | ||||
|     def _quote_params(self, params): | ||||
|         if not params: | ||||
|             return params | ||||
|         if isinstance(params, dict): | ||||
|             return dict((key, self._quote_expr(value)) | ||||
|                         for key, value in params.items()) | ||||
|         return list(map(self._quote_expr, params)) | ||||
| 
 | ||||
|     def _decode(self, param): | ||||
|         try: | ||||
|             return force_text(param, strings_only=True) | ||||
|         except UnicodeDecodeError: | ||||
|             return '(encoded string)' | ||||
| 
 | ||||
|     def _record(self, method, sql, params): | ||||
|         start_time = time() | ||||
|         try: | ||||
|             return method(sql, params) | ||||
|         finally: | ||||
|             stop_time = time() | ||||
|             duration = (stop_time - start_time) * 1000 | ||||
|             _params = '' | ||||
|             try: | ||||
|                 _params = json.dumps(list(map(self._decode, params))) | ||||
|             except Exception: | ||||
|                 pass  # object not JSON serializable | ||||
| 
 | ||||
|             alias = getattr(self.db, 'alias', 'default') | ||||
|             conn = self.db.connection | ||||
|             vendor = getattr(conn, 'vendor', 'unknown') | ||||
| 
 | ||||
|             params = { | ||||
|                 'vendor': vendor, | ||||
|                 'alias': alias, | ||||
|                 'sql': self.db.ops.last_executed_query( | ||||
|                     self.cursor, sql, self._quote_params(params)), | ||||
|                 'duration': duration, | ||||
|                 'raw_sql': sql, | ||||
|                 'params': _params, | ||||
|                 'start_time': start_time, | ||||
|                 'stop_time': stop_time, | ||||
|                 'is_slow': duration > 10, | ||||
|                 'is_select': sql.lower().strip().startswith('select'), | ||||
|             } | ||||
| 
 | ||||
|             if vendor == 'postgresql': | ||||
|                 # If an erroneous query was ran on the connection, it might | ||||
|                 # be in a state where checking isolation_level raises an | ||||
|                 # exception. | ||||
|                 try: | ||||
|                     iso_level = conn.isolation_level | ||||
|                 except conn.InternalError: | ||||
|                     iso_level = 'unknown' | ||||
|                 params.update({ | ||||
|                     'trans_id': self.logger.get_transaction_id(alias), | ||||
|                     'trans_status': conn.get_transaction_status(), | ||||
|                     'iso_level': iso_level, | ||||
|                     'encoding': conn.encoding, | ||||
|                 }) | ||||
| 
 | ||||
|             # We keep `sql` to maintain backwards compatibility | ||||
|             self.logger.record(**params) | ||||
| 
 | ||||
|     def callproc(self, procname, params=()): | ||||
|         return self._record(self.cursor.callproc, procname, params) | ||||
| 
 | ||||
|     def execute(self, sql, params=()): | ||||
|         return self._record(self.cursor.execute, sql, params) | ||||
| 
 | ||||
|     def executemany(self, sql, param_list): | ||||
|         return self._record(self.cursor.executemany, sql, param_list) | ||||
| 
 | ||||
|     def __getattr__(self, attr): | ||||
|         return getattr(self.cursor, attr) | ||||
| 
 | ||||
|     def __iter__(self): | ||||
|         return iter(self.cursor) | ||||
| 
 | ||||
|     def __enter__(self): | ||||
|         return self | ||||
| 
 | ||||
|     def __exit__(self, type, value, traceback): | ||||
|         self.close() | ||||
							
								
								
									
										19
									
								
								graphene/contrib/django/debug/sql/types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								graphene/contrib/django/debug/sql/types.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| from .....core import Float, ObjectType, String | ||||
| 
 | ||||
| 
 | ||||
| class DjangoDebugSQL(ObjectType): | ||||
|     vendor = String() | ||||
|     alias = String() | ||||
|     sql = String() | ||||
|     duration = Float() | ||||
|     raw_sql = String() | ||||
|     params = String() | ||||
|     start_time = Float() | ||||
|     stop_time = Float() | ||||
|     is_slow = String() | ||||
|     is_select = String() | ||||
| 
 | ||||
|     trans_id = String() | ||||
|     trans_status = String() | ||||
|     iso_level = String() | ||||
|     encoding = String() | ||||
							
								
								
									
										0
									
								
								graphene/contrib/django/debug/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								graphene/contrib/django/debug/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										70
									
								
								graphene/contrib/django/debug/tests/test_query.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								graphene/contrib/django/debug/tests/test_query.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | |||
| import pytest | ||||
| 
 | ||||
| import graphene | ||||
| from graphene.contrib.django import DjangoObjectType | ||||
| 
 | ||||
| from ...tests.models import Reporter | ||||
| from ..schema import DebugSchema | ||||
| 
 | ||||
| # from examples.starwars_django.models import Character | ||||
| 
 | ||||
| pytestmark = pytest.mark.django_db | ||||
| 
 | ||||
| 
 | ||||
| def test_should_query_well(): | ||||
|     r1 = Reporter(last_name='ABA') | ||||
|     r1.save() | ||||
|     r2 = Reporter(last_name='Griffin') | ||||
|     r2.save() | ||||
| 
 | ||||
|     class ReporterType(DjangoObjectType): | ||||
| 
 | ||||
|         class Meta: | ||||
|             model = Reporter | ||||
| 
 | ||||
|     class Query(graphene.ObjectType): | ||||
|         reporter = graphene.Field(ReporterType) | ||||
|         all_reporters = ReporterType.List | ||||
| 
 | ||||
|         def resolve_all_reporters(self, *args, **kwargs): | ||||
|             return Reporter.objects.all() | ||||
| 
 | ||||
|         def resolve_reporter(self, *args, **kwargs): | ||||
|             return Reporter.objects.first() | ||||
| 
 | ||||
|     query = ''' | ||||
|         query ReporterQuery { | ||||
|           reporter { | ||||
|             lastName | ||||
|           } | ||||
|           allReporters { | ||||
|             lastName | ||||
|           } | ||||
|           __debug { | ||||
|             sql { | ||||
|               rawSql | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|     ''' | ||||
|     expected = { | ||||
|         'reporter': { | ||||
|             'lastName': 'ABA', | ||||
|         }, | ||||
|         'allReporters': [{ | ||||
|             'lastName': 'ABA', | ||||
|         }, { | ||||
|             'lastName': 'Griffin', | ||||
|         }], | ||||
|         '__debug': { | ||||
|             'sql': [{ | ||||
|                 'rawSql': str(Reporter.objects.order_by('pk')[:1].query) | ||||
|             }, { | ||||
|                 'rawSql': str(Reporter.objects.all().query) | ||||
|             }] | ||||
|         } | ||||
|     } | ||||
|     schema = DebugSchema(query=Query) | ||||
|     result = schema.execute(query) | ||||
|     assert not result.errors | ||||
|     assert result.data == expected | ||||
							
								
								
									
										7
									
								
								graphene/contrib/django/debug/types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								graphene/contrib/django/debug/types.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| from ....core.classtypes.objecttype import ObjectType | ||||
| from ....core.types import Field | ||||
| from .sql.types import DjangoDebugSQL | ||||
| 
 | ||||
| 
 | ||||
| class DjangoDebug(ObjectType): | ||||
|     sql = Field(DjangoDebugSQL.List) | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user