codetac 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -5
- package/readme.md +81 -14
- package/src/ai.mjs +4 -2
- package/src/boundaries.mjs +12 -9
- package/src/browser/bar.js +5 -3
- package/src/cli.mjs +251 -33
- package/src/detect-python.mjs +286 -0
- package/src/detect.mjs +21 -6
- package/src/diagnose.mjs +35 -5
- package/src/digest.mjs +18 -4
- package/src/page.mjs +19 -7
- package/src/panel.mjs +56 -14
- package/src/python/codetac_py/__init__.py +88 -0
- package/src/python/codetac_py/boundaries.py +435 -0
- package/src/python/codetac_py/capture.py +351 -0
- package/src/python/codetac_py/context.py +50 -0
- package/src/python/codetac_py/detail.py +333 -0
- package/src/python/codetac_py/files.py +135 -0
- package/src/python/codetac_py/frameworks.py +72 -0
- package/src/python/codetac_py/hooks.py +72 -0
- package/src/python/codetac_py/jinja_map.py +132 -0
- package/src/python/codetac_py/network.py +704 -0
- package/src/python/codetac_py/page.py +513 -0
- package/src/python/codetac_py/project.py +60 -0
- package/src/python/codetac_py/redact.py +104 -0
- package/src/python/codetac_py/servers.py +514 -0
- package/src/python/codetac_py/sitecustomize.py +62 -0
- package/src/python/codetac_py/writer.py +237 -0
- package/src/python/probe.py +99 -0
- package/src/recording.mjs +22 -4
- package/src/runtime.mjs +13 -1
- package/src/sentences.mjs +4 -0
- package/src/store.mjs +61 -9
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""Library boundaries: databases here (stage 6); the network (network.py) and
|
|
2
|
+
files (files.py) since stage 7. The classification and the fields are those
|
|
3
|
+
of src/boundaries.mjs:
|
|
4
|
+
|
|
5
|
+
boundary {id, parentId, requestId, kind: 'base-de-dados', library, operation, tables, sql}
|
|
6
|
+
boundary-end {id, requestId, durationNs, error, rows | affectedRows}
|
|
7
|
+
|
|
8
|
+
The SQL is recorded as a template (describe_sql, the port of describeSql):
|
|
9
|
+
string literals are replaced, and the parameters are never read.
|
|
10
|
+
|
|
11
|
+
Where each library is seen:
|
|
12
|
+
- SQLAlchemy / SQLModel, sync and async: the engine events
|
|
13
|
+
(before/after_cursor_execute, handle_error);
|
|
14
|
+
- sqlite3: connect() gives subclasses of Connection and Cursor (the C types
|
|
15
|
+
cannot be patched);
|
|
16
|
+
- psycopg (3): Cursor and AsyncCursor; psycopg2: connection and cursor
|
|
17
|
+
factories; PyMySQL and mysqlclient: their cursors; asyncpg: Connection;
|
|
18
|
+
- aiosqlite runs sqlite3 in its own thread: the calls it sends there carry
|
|
19
|
+
the caller's context.
|
|
20
|
+
|
|
21
|
+
A boundary inside another (SQLAlchemy over a driver, executemany over
|
|
22
|
+
execute) is recorded once: the outer one.
|
|
23
|
+
|
|
24
|
+
Keep it importable on old Pythons (3.8+): the minimal mode records boundaries.
|
|
25
|
+
"""
|
|
26
|
+
import contextvars
|
|
27
|
+
import functools
|
|
28
|
+
import itertools
|
|
29
|
+
import re
|
|
30
|
+
import time
|
|
31
|
+
|
|
32
|
+
from .context import current, request_of
|
|
33
|
+
from .hooks import register
|
|
34
|
+
|
|
35
|
+
WRITES = {'INSERT', 'UPDATE', 'DELETE', 'UPSERT', 'REPLACE', 'MERGE'}
|
|
36
|
+
_boundaries = itertools.count(1)
|
|
37
|
+
# Inside a database boundary: an inner one (the driver under SQLAlchemy) is not recorded.
|
|
38
|
+
_inside = contextvars.ContextVar('codetac_boundary', default=False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _codetac():
|
|
42
|
+
import codetac_py
|
|
43
|
+
return codetac_py
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# The SQL template (src/boundaries.mjs describeSql) --------------------------------
|
|
47
|
+
|
|
48
|
+
_literal = re.compile(r"'(?:[^'\\]|\\.|'')*'")
|
|
49
|
+
_space = re.compile(r'\s+')
|
|
50
|
+
_S = '[\\t\\n\\v\\f\\r \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]'
|
|
51
|
+
_first = re.compile('^' + _S + '*(\\w+)', re.ASCII)
|
|
52
|
+
_tables = re.compile(r'\b(?:from|into|update|join|table(?: if (?:not )?exists)?)\s+[`"\[]?([\w.]+)', re.ASCII | re.IGNORECASE)
|
|
53
|
+
_keywords = re.compile(r'^(select|if|not|exists)$', re.ASCII | re.IGNORECASE)
|
|
54
|
+
_js_trim = re.compile('^' + _S + '+|' + _S + '+$')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def describe_sql(sql):
|
|
58
|
+
if isinstance(sql, bytes):
|
|
59
|
+
sql = sql.decode('utf-8', 'replace')
|
|
60
|
+
if not isinstance(sql, str):
|
|
61
|
+
return {'operation': 'consulta'}
|
|
62
|
+
text = _js_trim.sub('', _space.sub(' ', _literal.sub("'?'", sql)))
|
|
63
|
+
first = _first.match(text)
|
|
64
|
+
operation = (first.group(1) if first else 'consulta').upper()
|
|
65
|
+
tables = []
|
|
66
|
+
for match in _tables.finditer(text):
|
|
67
|
+
name = match.group(1)
|
|
68
|
+
if not _keywords.match(name) and name not in tables:
|
|
69
|
+
tables.append(name)
|
|
70
|
+
return {'operation': operation, 'tables': tables[:10], 'sql': text[:2000]}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Boundaries ------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def start_boundary(details):
|
|
76
|
+
"""Records the start; returns end(extra), which records the end once."""
|
|
77
|
+
writer = _codetac().writer
|
|
78
|
+
state = current.get()
|
|
79
|
+
boundary_id = '%s:b%d' % (writer.process, next(_boundaries))
|
|
80
|
+
request, after = request_of(state[1])
|
|
81
|
+
event = {'type': 'boundary', 'id': boundary_id, 'parentId': None if state[0] is None else '%s:%d' % (writer.process, state[0]),
|
|
82
|
+
'requestId': request}
|
|
83
|
+
if after:
|
|
84
|
+
event['afterResponse'] = True
|
|
85
|
+
event.update(details)
|
|
86
|
+
writer.emit(event)
|
|
87
|
+
started = time.perf_counter_ns()
|
|
88
|
+
done = []
|
|
89
|
+
|
|
90
|
+
def end(extra=None):
|
|
91
|
+
if done:
|
|
92
|
+
return
|
|
93
|
+
done.append(True)
|
|
94
|
+
result = {'type': 'boundary-end', 'id': boundary_id, 'requestId': request,
|
|
95
|
+
'durationNs': time.perf_counter_ns() - started, 'error': False}
|
|
96
|
+
result.update(extra or {})
|
|
97
|
+
writer.emit(result)
|
|
98
|
+
|
|
99
|
+
return end
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _rows(operation, count):
|
|
103
|
+
"""rows for reads, affectedRows for writes; nothing when the driver does not know (-1)."""
|
|
104
|
+
if not isinstance(count, int) or isinstance(count, bool) or count < 0:
|
|
105
|
+
return {}
|
|
106
|
+
return {'affectedRows': count} if operation in WRITES else {'rows': count}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def database(library, sql, run, count):
|
|
110
|
+
"""Runs run() as a database boundary; count(result) gives the rows."""
|
|
111
|
+
if _inside.get():
|
|
112
|
+
return run()
|
|
113
|
+
try:
|
|
114
|
+
details = describe_sql(sql)
|
|
115
|
+
end = start_boundary(dict({'kind': 'base-de-dados', 'library': library}, **details))
|
|
116
|
+
except Exception:
|
|
117
|
+
return run()
|
|
118
|
+
token = _inside.set(True)
|
|
119
|
+
try:
|
|
120
|
+
result = run()
|
|
121
|
+
except BaseException:
|
|
122
|
+
end({'error': True})
|
|
123
|
+
raise
|
|
124
|
+
finally:
|
|
125
|
+
_inside.reset(token)
|
|
126
|
+
try:
|
|
127
|
+
end(_rows(details['operation'], count(result)))
|
|
128
|
+
except Exception:
|
|
129
|
+
end()
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def database_async(library, sql, run, count):
|
|
134
|
+
if _inside.get():
|
|
135
|
+
return await run()
|
|
136
|
+
try:
|
|
137
|
+
details = describe_sql(sql)
|
|
138
|
+
end = start_boundary(dict({'kind': 'base-de-dados', 'library': library}, **details))
|
|
139
|
+
except Exception:
|
|
140
|
+
return await run()
|
|
141
|
+
token = _inside.set(True)
|
|
142
|
+
try:
|
|
143
|
+
result = await run()
|
|
144
|
+
except BaseException:
|
|
145
|
+
end({'error': True})
|
|
146
|
+
raise
|
|
147
|
+
finally:
|
|
148
|
+
_inside.reset(token)
|
|
149
|
+
try:
|
|
150
|
+
end(_rows(details['operation'], count(result)))
|
|
151
|
+
except Exception:
|
|
152
|
+
end()
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _sql_of(args, kwargs, name='query'):
|
|
157
|
+
if args:
|
|
158
|
+
return args[0]
|
|
159
|
+
return kwargs.get(name, kwargs.get('sql', kwargs.get('operation')))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _wrap_cursor_method(owner, method, library, count=None):
|
|
163
|
+
"""cursor.execute(sql, ...) / executemany: rows from cursor.rowcount."""
|
|
164
|
+
original = getattr(owner, method, None)
|
|
165
|
+
if original is None or getattr(original, '__codetac__', False):
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
@functools.wraps(original)
|
|
169
|
+
def wrapper(self, *args, **kwargs):
|
|
170
|
+
return database(library, _sql_of(args, kwargs), lambda: original(self, *args, **kwargs),
|
|
171
|
+
count or (lambda result: getattr(self, 'rowcount', -1)))
|
|
172
|
+
|
|
173
|
+
wrapper.__codetac__ = True
|
|
174
|
+
setattr(owner, method, wrapper)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _wrap_async_cursor_method(owner, method, library):
|
|
178
|
+
original = getattr(owner, method, None)
|
|
179
|
+
if original is None or getattr(original, '__codetac__', False):
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
@functools.wraps(original)
|
|
183
|
+
async def wrapper(self, *args, **kwargs):
|
|
184
|
+
return await database_async(library, _sql_of(args, kwargs), lambda: original(self, *args, **kwargs),
|
|
185
|
+
lambda result: getattr(self, 'rowcount', -1))
|
|
186
|
+
|
|
187
|
+
wrapper.__codetac__ = True
|
|
188
|
+
setattr(owner, method, wrapper)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# sqlite3 ---------------------------------------------------------------------------
|
|
192
|
+
# Connection and Cursor are C types: connect() returns subclasses whose execute
|
|
193
|
+
# methods are boundaries. The app's own factory= is subclassed the same way.
|
|
194
|
+
|
|
195
|
+
_sqlite_classes = {}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _sqlite_cursor_class(base):
|
|
199
|
+
found = _sqlite_classes.get(('cursor', base))
|
|
200
|
+
if found is None:
|
|
201
|
+
found = type(base.__name__, (base,), {'__module__': base.__module__, '__qualname__': base.__qualname__})
|
|
202
|
+
for method in ('execute', 'executemany', 'executescript'):
|
|
203
|
+
_wrap_cursor_method(found, method, 'sqlite3')
|
|
204
|
+
_sqlite_classes[('cursor', base)] = found
|
|
205
|
+
return found
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _sqlite_connection_class(base, module):
|
|
209
|
+
found = _sqlite_classes.get(('connection', base))
|
|
210
|
+
if found is not None:
|
|
211
|
+
return found
|
|
212
|
+
|
|
213
|
+
def cursor(self, factory=None):
|
|
214
|
+
return base.cursor(self, _sqlite_cursor_class(factory or module.Cursor))
|
|
215
|
+
|
|
216
|
+
# Connection.execute* create a cursor in C: here they go through cursor().
|
|
217
|
+
def execute(self, sql, parameters=()):
|
|
218
|
+
return self.cursor().execute(sql, parameters)
|
|
219
|
+
|
|
220
|
+
def executemany(self, sql, parameters):
|
|
221
|
+
return self.cursor().executemany(sql, parameters)
|
|
222
|
+
|
|
223
|
+
def executescript(self, script):
|
|
224
|
+
return self.cursor().executescript(script)
|
|
225
|
+
|
|
226
|
+
found = type(base.__name__, (base,), {'__module__': base.__module__, '__qualname__': base.__qualname__,
|
|
227
|
+
'cursor': cursor, 'execute': execute, 'executemany': executemany,
|
|
228
|
+
'executescript': executescript})
|
|
229
|
+
_sqlite_classes[('connection', base)] = found
|
|
230
|
+
return found
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _patch_sqlite(module):
|
|
234
|
+
connect = module.connect
|
|
235
|
+
if getattr(connect, '__codetac__', False):
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
@functools.wraps(connect)
|
|
239
|
+
def codetac_connect(*args, **kwargs):
|
|
240
|
+
factory = kwargs.pop('factory', None) or module.Connection
|
|
241
|
+
try:
|
|
242
|
+
kwargs['factory'] = _sqlite_connection_class(factory, module)
|
|
243
|
+
except TypeError: # a factory that cannot be subclassed: unobserved
|
|
244
|
+
kwargs['factory'] = factory
|
|
245
|
+
return connect(*args, **kwargs)
|
|
246
|
+
|
|
247
|
+
codetac_connect.__codetac__ = True
|
|
248
|
+
module.connect = codetac_connect
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# aiosqlite: sqlite3 in its own thread; the calls carry the caller's context.
|
|
252
|
+
def _patch_aiosqlite(module):
|
|
253
|
+
connection = module.Connection
|
|
254
|
+
original = getattr(connection, '_execute', None)
|
|
255
|
+
if original is None or getattr(original, '__codetac__', False):
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
@functools.wraps(original)
|
|
259
|
+
async def _execute(self, fn, *args, **kwargs):
|
|
260
|
+
context = contextvars.copy_context()
|
|
261
|
+
return await original(self, context.run, fn, *args, **kwargs)
|
|
262
|
+
|
|
263
|
+
_execute.__codetac__ = True
|
|
264
|
+
connection._execute = _execute
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# SQLAlchemy / SQLModel ---------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
def _patch_sqlalchemy(module):
|
|
270
|
+
from sqlalchemy import event
|
|
271
|
+
from sqlalchemy.engine import Engine
|
|
272
|
+
if getattr(Engine, '__codetac__', False):
|
|
273
|
+
return
|
|
274
|
+
Engine.__codetac__ = True
|
|
275
|
+
|
|
276
|
+
def before(conn, cursor, statement, parameters, context, executemany):
|
|
277
|
+
if context is None or _inside.get():
|
|
278
|
+
return
|
|
279
|
+
try:
|
|
280
|
+
details = describe_sql(statement)
|
|
281
|
+
dialect = getattr(getattr(conn, 'dialect', None), 'name', None)
|
|
282
|
+
end = start_boundary(dict({'kind': 'base-de-dados', 'library': 'sqlalchemy' + ('/' + dialect if dialect else '')}, **details))
|
|
283
|
+
context._codetac = (end, details['operation'], _inside.set(True))
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
def finish(context, extra):
|
|
288
|
+
state = getattr(context, '_codetac', None)
|
|
289
|
+
if state is None:
|
|
290
|
+
return
|
|
291
|
+
context._codetac = None
|
|
292
|
+
end, operation, token = state
|
|
293
|
+
try:
|
|
294
|
+
_inside.reset(token)
|
|
295
|
+
except ValueError: # reset from another context (an async driver): let it go
|
|
296
|
+
_inside.set(False)
|
|
297
|
+
end(extra(operation))
|
|
298
|
+
|
|
299
|
+
def after(conn, cursor, statement, parameters, context, executemany):
|
|
300
|
+
finish(context, lambda operation: _rows(operation, getattr(cursor, 'rowcount', -1)))
|
|
301
|
+
|
|
302
|
+
def error(exception_context):
|
|
303
|
+
finish(exception_context.execution_context, lambda operation: {'error': True})
|
|
304
|
+
|
|
305
|
+
event.listen(Engine, 'before_cursor_execute', before)
|
|
306
|
+
event.listen(Engine, 'after_cursor_execute', after)
|
|
307
|
+
event.listen(Engine, 'handle_error', error)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# PostgreSQL and MySQL drivers ------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
def _patch_psycopg(module):
|
|
313
|
+
for name in ('execute', 'executemany'):
|
|
314
|
+
_wrap_cursor_method(module.Cursor, name, 'psycopg')
|
|
315
|
+
_wrap_async_cursor_method(module.AsyncCursor, name, 'psycopg')
|
|
316
|
+
# ServerCursor and AsyncServerCursor derive from these, and Connection.execute uses them.
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _patch_psycopg2(module):
|
|
320
|
+
extensions = module.extensions
|
|
321
|
+
classes = {}
|
|
322
|
+
|
|
323
|
+
def cursor_class(base):
|
|
324
|
+
found = classes.get(base)
|
|
325
|
+
if found is None:
|
|
326
|
+
found = type(base.__name__, (base,), {'__module__': base.__module__})
|
|
327
|
+
for name in ('execute', 'executemany'):
|
|
328
|
+
_wrap_cursor_method(found, name, 'psycopg2')
|
|
329
|
+
classes[base] = found
|
|
330
|
+
return found
|
|
331
|
+
|
|
332
|
+
def connection_class(base):
|
|
333
|
+
found = classes.get(('connection', base))
|
|
334
|
+
if found is None:
|
|
335
|
+
def cursor(self, *args, **kwargs):
|
|
336
|
+
factory = kwargs.get('cursor_factory') or self.cursor_factory or extensions.cursor
|
|
337
|
+
kwargs['cursor_factory'] = cursor_class(factory)
|
|
338
|
+
return base.cursor(self, *args, **kwargs)
|
|
339
|
+
found = type(base.__name__, (base,), {'__module__': base.__module__, 'cursor': cursor})
|
|
340
|
+
classes[('connection', base)] = found
|
|
341
|
+
return found
|
|
342
|
+
|
|
343
|
+
connect = module.connect
|
|
344
|
+
if getattr(connect, '__codetac__', False):
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
@functools.wraps(connect)
|
|
348
|
+
def codetac_connect(*args, **kwargs):
|
|
349
|
+
kwargs['connection_factory'] = connection_class(kwargs.get('connection_factory') or extensions.connection)
|
|
350
|
+
return connect(*args, **kwargs)
|
|
351
|
+
|
|
352
|
+
codetac_connect.__codetac__ = True
|
|
353
|
+
module.connect = codetac_connect
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _patch_pymysql_cursors(module):
|
|
357
|
+
for name in ('execute', 'executemany'):
|
|
358
|
+
_wrap_cursor_method(module.Cursor, name, 'pymysql')
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _patch_mysqldb_cursors(module):
|
|
362
|
+
for name in ('execute', 'executemany'):
|
|
363
|
+
_wrap_cursor_method(module.BaseCursor, name, 'mysqlclient')
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _asyncpg_count(method):
|
|
367
|
+
def count(result):
|
|
368
|
+
if method in ('fetch',):
|
|
369
|
+
return len(result)
|
|
370
|
+
if method == 'fetchrow':
|
|
371
|
+
return 0 if result is None else 1
|
|
372
|
+
if method == 'execute' and isinstance(result, str):
|
|
373
|
+
# The status: "INSERT 0 3", "UPDATE 2", "SELECT 5", "CREATE TABLE".
|
|
374
|
+
last = result.rsplit(' ', 1)[-1]
|
|
375
|
+
return int(last) if last.isdigit() else -1
|
|
376
|
+
return -1
|
|
377
|
+
return count
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _patch_asyncpg_connection(module):
|
|
381
|
+
connection = module.Connection
|
|
382
|
+
for method in ('execute', 'executemany', 'fetch', 'fetchrow', 'fetchval'):
|
|
383
|
+
original = getattr(connection, method, None)
|
|
384
|
+
if original is None or getattr(original, '__codetac__', False):
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
def make(original, method):
|
|
388
|
+
@functools.wraps(original)
|
|
389
|
+
async def wrapper(self, *args, **kwargs):
|
|
390
|
+
return await database_async('asyncpg', _sql_of(args, kwargs), lambda: original(self, *args, **kwargs), _asyncpg_count(method))
|
|
391
|
+
wrapper.__codetac__ = True
|
|
392
|
+
return wrapper
|
|
393
|
+
|
|
394
|
+
setattr(connection, method, make(original, method))
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _patch_asyncpg_transaction(module):
|
|
398
|
+
# Transaction control (BEGIN, COMMIT, ROLLBACK, SAVEPOINT) is not a read or
|
|
399
|
+
# a write: as with the other drivers, whose commit() is not a query, it is
|
|
400
|
+
# not a boundary (SQLAlchemy async issues it through this class).
|
|
401
|
+
transaction = module.Transaction
|
|
402
|
+
for method in ('start', 'commit', 'rollback'):
|
|
403
|
+
original = getattr(transaction, method, None)
|
|
404
|
+
if original is None or getattr(original, '__codetac__', False):
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
def make(original):
|
|
408
|
+
@functools.wraps(original)
|
|
409
|
+
async def wrapper(self, *args, **kwargs):
|
|
410
|
+
token = _inside.set(True)
|
|
411
|
+
try:
|
|
412
|
+
return await original(self, *args, **kwargs)
|
|
413
|
+
finally:
|
|
414
|
+
_inside.reset(token)
|
|
415
|
+
wrapper.__codetac__ = True
|
|
416
|
+
return wrapper
|
|
417
|
+
|
|
418
|
+
setattr(transaction, method, make(original))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def install(root=None, data=None):
|
|
422
|
+
from . import files, network
|
|
423
|
+
network.install()
|
|
424
|
+
if root is not None and data is not None:
|
|
425
|
+
files.install(root, data)
|
|
426
|
+
register('sqlite3.dbapi2', _patch_sqlite)
|
|
427
|
+
register('sqlite3', _patch_sqlite) # sqlite3 imported before the capture
|
|
428
|
+
register('aiosqlite.core', _patch_aiosqlite)
|
|
429
|
+
register('sqlalchemy', _patch_sqlalchemy)
|
|
430
|
+
register('psycopg', _patch_psycopg)
|
|
431
|
+
register('psycopg2', _patch_psycopg2)
|
|
432
|
+
register('pymysql.cursors', _patch_pymysql_cursors)
|
|
433
|
+
register('MySQLdb.cursors', _patch_mysqldb_cursors)
|
|
434
|
+
register('asyncpg.connection', _patch_asyncpg_connection)
|
|
435
|
+
register('asyncpg.transaction', _patch_asyncpg_transaction)
|