wrapture-instrumentation-postgresql 1.0.0.dev1__py3-none-any.whl

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.
@@ -0,0 +1,402 @@
1
+ """The psycopg2 patches: the connect factory bound, and the
2
+ connections it returns made instances of recording subclasses, their
3
+ cursors likewise.
4
+
5
+ psycopg2's connection and cursor are C types, so there is no
6
+ attribute on them a binding could patch, and a proxy around them is
7
+ not an option either: psycopg2's own C entry points type-check the
8
+ objects handed back to them (`register_type`, and so `register_uuid`,
9
+ `register_hstore`, `register_json` and `register_composite`, plus
10
+ `quote_ident`, `Json(...).prepare()` and `lobject`), and a proxy
11
+ fails those checks. What psycopg2 does support is subclassing: the
12
+ `connection_factory=` argument to `connect()` and the `cursor_factory`
13
+ of a connection or a `cursor()` call are how its own extras
14
+ (`LoggingConnection`, `DictCursor`) work. The instrumentation uses
15
+ that mechanism: the one seam is `psycopg2.connect`, bound to record
16
+ the open and to substitute the requested connection factory with a
17
+ recording subclass of it, and that subclass hands out cursors that
18
+ are recording subclasses of whatever cursor class was asked for.
19
+
20
+ The recording subclasses are made per base class and cached: a mixin
21
+ of plain Python methods (`CursorMixin`, `ConnectionMixin`) placed
22
+ ahead of the base in the bases, each override calling the base's
23
+ method through super(), so the application's own factories
24
+ (`RealDictConnection`, `DictCursor`, `LoggingConnection`) keep
25
+ working and are simply recorded. The subclass takes the base's name
26
+ and module, so reprs read as before; isinstance checks and the C
27
+ type checks pass, since these are real subclasses. Those mixin
28
+ methods are what wrapture binds, each labelled with the psycopg2 name
29
+ it stands for (`psycopg2.extensions:cursor.execute`), the one thing
30
+ its recorded path cannot say.
31
+
32
+ Cursors are made recording in `ConnectionMixin.cursor`. A cursor class
33
+ named explicitly, in the call or on the connection's `cursor_factory`,
34
+ is substituted with its recording subclass before the base builds
35
+ the cursor. When nothing names one and the base would fall back to
36
+ psycopg2's own C cursor type, the recording subclass of that type is
37
+ passed explicitly, since an instance of the C type itself cannot be
38
+ reclassed afterwards. When a base class's own `cursor()` supplies a
39
+ default (the extras' connection classes do), it runs first and the
40
+ cursor it returns, an instance of a Python subclass, is reclassed to
41
+ the recording subclass of its class on the way out, which Python
42
+ permits for compatible heap types.
43
+
44
+ The recorded set is the acquisition, the execute family and the
45
+ transaction boundaries: `connect`; `execute`, `executemany` and
46
+ `callproc`; `copy_from`, `copy_to` and `copy_expert`; `commit` and
47
+ `rollback`; and the connection's commit-or-rollback context manager,
48
+ whose exit records which of the two it performed (psycopg2's does
49
+ not close the connection). Cursor creation and the fetch methods are
50
+ not recorded, and a named cursor's FETCHes not either, the model
51
+ every database target here follows. Every event carries the
52
+ database contract keys and the server reached, from the connection's
53
+ info; the SQL text as the application handed it over (psycopg2
54
+ interpolates the parameters client-side below this seam, so the
55
+ recorded text carries the placeholders) rides as `statement` only
56
+ when the setting is on, and bound parameters are never recorded.
57
+
58
+ Removing the instrumentation removes the bindings and restores
59
+ `connect`: connections already made keep their recording classes,
60
+ whose methods then delegate without recording.
61
+ """
62
+
63
+ from __future__ import annotations
64
+
65
+ from types import TracebackType
66
+ from typing import Any, cast
67
+
68
+ import wrapture
69
+
70
+ from ..common import SYSTEM, captured, query_data, server_of
71
+
72
+ _connections: dict[type, type] = {}
73
+ _cursors: dict[type, type] = {}
74
+
75
+
76
+ class CursorMixin:
77
+ """The recording overrides placed ahead of a psycopg2 cursor class:
78
+ each calls the base's method through super(), and each is bound
79
+ by wrapture below."""
80
+
81
+ def execute(self, query: Any, vars: Any = None) -> Any:
82
+ return cast(Any, super()).execute(query, vars)
83
+
84
+ def executemany(self, query: Any, vars_list: Any) -> Any:
85
+ return cast(Any, super()).executemany(query, vars_list)
86
+
87
+ def callproc(self, procname: Any, parameters: Any = None) -> Any:
88
+ return cast(Any, super()).callproc(procname, parameters)
89
+
90
+ def copy_from(
91
+ self,
92
+ file: Any,
93
+ table: Any,
94
+ sep: str = "\t",
95
+ null: str = "\\N",
96
+ size: int = 8192,
97
+ columns: Any = None,
98
+ ) -> Any:
99
+ return cast(Any, super()).copy_from(
100
+ file, table, sep=sep, null=null, size=size, columns=columns
101
+ )
102
+
103
+ def copy_to(
104
+ self,
105
+ file: Any,
106
+ table: Any,
107
+ sep: str = "\t",
108
+ null: str = "\\N",
109
+ columns: Any = None,
110
+ ) -> Any:
111
+ return cast(Any, super()).copy_to(
112
+ file, table, sep=sep, null=null, columns=columns
113
+ )
114
+
115
+ def copy_expert(self, sql: Any, file: Any, size: int = 8192) -> Any:
116
+ return cast(Any, super()).copy_expert(sql, file, size=size)
117
+
118
+
119
+ class ConnectionMixin:
120
+ """The recording overrides placed ahead of a psycopg2 connection
121
+ class: cursors come back as recording subclasses, and the
122
+ transaction boundaries call through to the base and are bound by
123
+ wrapture below."""
124
+
125
+ def cursor(self, *args: Any, **kwargs: Any) -> Any:
126
+ # A cursor class named in the call (by keyword or in the
127
+ # second positional slot) is substituted with its recording
128
+ # subclass before the base builds the cursor.
129
+
130
+ if "cursor_factory" in kwargs:
131
+ factory = kwargs["cursor_factory"]
132
+ elif len(args) > 1:
133
+ factory = args[1]
134
+ else:
135
+ factory = None
136
+
137
+ if factory is not None:
138
+ recording = recording_cursor(factory)
139
+ if "cursor_factory" in kwargs:
140
+ kwargs["cursor_factory"] = recording
141
+ else:
142
+ args = (args[0], recording, *args[2:])
143
+
144
+ # With none named and nothing set on the connection, a base
145
+ # that falls straight through to the C method would build an
146
+ # instance of the C cursor type, which cannot be reclassed
147
+ # afterwards, so its recording subclass is named explicitly.
148
+
149
+ elif getattr(self, "cursor_factory", None) is None and _falls_through(
150
+ type(self)
151
+ ):
152
+ kwargs["cursor_factory"] = recording_cursor(_c_cursor_type())
153
+
154
+ cursor = cast(Any, super()).cursor(*args, **kwargs)
155
+
156
+ # A base's own cursor() may have supplied its default cursor
157
+ # class instead; its instance is reclassed on the way out.
158
+
159
+ if not isinstance(cursor, CursorMixin):
160
+ try:
161
+ cursor.__class__ = recording_cursor(type(cursor))
162
+ except TypeError:
163
+ pass
164
+
165
+ return cursor
166
+
167
+ def commit(self) -> Any:
168
+ return cast(Any, super()).commit()
169
+
170
+ def rollback(self) -> Any:
171
+ return cast(Any, super()).rollback()
172
+
173
+ def __exit__(
174
+ self,
175
+ exc_type: type[BaseException] | None,
176
+ exc_value: BaseException | None,
177
+ traceback: TracebackType | None,
178
+ ) -> Any:
179
+ return cast(Any, super()).__exit__(exc_type, exc_value, traceback)
180
+
181
+
182
+ def _c_cursor_type() -> type:
183
+ import psycopg2.extensions
184
+
185
+ return cast(type, psycopg2.extensions.cursor)
186
+
187
+
188
+ def _falls_through(cls: type) -> bool:
189
+ """Whether cursor() on the given recording connection class, past
190
+ the mixin, is psycopg2's own C method rather than an override of
191
+ a base class supplying a cursor default of its own."""
192
+
193
+ import psycopg2.extensions
194
+
195
+ for base in cls.__mro__:
196
+ if base is ConnectionMixin:
197
+ continue
198
+ if "cursor" in vars(base):
199
+ return base is psycopg2.extensions.connection
200
+
201
+ return True
202
+
203
+
204
+ def _recording(cache: dict[type, type], mixin: type, base: type) -> type:
205
+ if isinstance(base, type) and issubclass(base, mixin):
206
+ return base
207
+
208
+ try:
209
+ return cache[base]
210
+ except KeyError:
211
+ pass
212
+
213
+ made = type(
214
+ base.__name__,
215
+ (mixin, base),
216
+ {"__module__": base.__module__, "__qualname__": base.__qualname__},
217
+ )
218
+ cache[base] = made
219
+
220
+ return made
221
+
222
+
223
+ def recording_cursor(base: type) -> type:
224
+ """The recording subclass of a psycopg2 cursor class, made once
225
+ per class; a class already recording is returned as is."""
226
+
227
+ return _recording(_cursors, CursorMixin, base)
228
+
229
+
230
+ def recording_connection(base: type) -> type:
231
+ """The recording subclass of a psycopg2 connection class, made
232
+ once per class; a class already recording is returned as is."""
233
+
234
+ return _recording(_connections, ConnectionMixin, base)
235
+
236
+
237
+ def instrument(module: Any, instrumentation: wrapture.Instrumentation) -> None:
238
+ """Bind the connect factory and the mixins' methods; register
239
+ their removal as this trigger's cleanup."""
240
+
241
+ settings = instrumentation.settings
242
+ record_statement = bool(settings["statement"])
243
+
244
+ def data_for(
245
+ cursor: Any, query: Any, operation: str | None = None
246
+ ) -> dict[str, Any]:
247
+ return query_data(
248
+ query, cursor, cursor.connection.info, record_statement, operation
249
+ )
250
+
251
+ def opens(
252
+ wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
253
+ ) -> Any:
254
+ wrapture.annotate(system=SYSTEM, operation="CONNECT")
255
+
256
+ # The requested connection class (keyword, or the second
257
+ # positional slot) is replaced with its recording subclass.
258
+
259
+ if "connection_factory" in kwargs:
260
+ factory = kwargs["connection_factory"]
261
+ elif len(args) > 1:
262
+ factory = args[1]
263
+ else:
264
+ factory = None
265
+
266
+ recording = recording_connection(factory or module.extensions.connection)
267
+ if "connection_factory" in kwargs or len(args) <= 1:
268
+ kwargs["connection_factory"] = recording
269
+ else:
270
+ args = (args[0], recording, *args[2:])
271
+
272
+ connection = wrapped(*args, **kwargs)
273
+ wrapture.annotate(**server_of(connection.info))
274
+
275
+ return connection
276
+
277
+ def queries(
278
+ wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
279
+ ) -> Any:
280
+ query = args[0] if args else kwargs.get("query")
281
+ wrapture.annotate(**data_for(instance, query))
282
+
283
+ return wrapped(*args, **kwargs)
284
+
285
+ def calls(
286
+ wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
287
+ ) -> Any:
288
+ procname = args[0] if args else kwargs.get("procname")
289
+ data = data_for(instance, None, "CALL")
290
+ if isinstance(procname, str):
291
+ data["procedure"] = procname
292
+ wrapture.annotate(**data)
293
+
294
+ return wrapped(*args, **kwargs)
295
+
296
+ def copies_table(
297
+ wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
298
+ ) -> Any:
299
+ table = args[1] if len(args) > 1 else kwargs.get("table")
300
+ data = data_for(instance, None, "COPY")
301
+ if isinstance(table, str):
302
+ data["collection"] = table
303
+ wrapture.annotate(**data)
304
+
305
+ return wrapped(*args, **kwargs)
306
+
307
+ def copies_statement(
308
+ wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
309
+ ) -> Any:
310
+ sql = args[0] if args else kwargs.get("sql")
311
+ wrapture.annotate(**data_for(instance, sql, "COPY"))
312
+
313
+ return wrapped(*args, **kwargs)
314
+
315
+ def performs(operation: str) -> Any:
316
+ def record(
317
+ wrapped: Any,
318
+ instance: Any,
319
+ args: tuple[Any, ...],
320
+ kwargs: dict[str, Any],
321
+ ) -> Any:
322
+ wrapture.annotate(
323
+ system=SYSTEM, operation=operation, **server_of(instance.info)
324
+ )
325
+
326
+ return wrapped(*args, **kwargs)
327
+
328
+ return record
329
+
330
+ def leaves(
331
+ wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
332
+ ) -> Any:
333
+ # The exit commits unless an exception is on its way through.
334
+
335
+ exc_type = args[0] if args else kwargs.get("exc_type")
336
+ operation = "COMMIT" if exc_type is None else "ROLLBACK"
337
+ wrapture.annotate(
338
+ system=SYSTEM, operation=operation, **server_of(instance.info)
339
+ )
340
+
341
+ return wrapped(*args, **kwargs)
342
+
343
+ def database_binding(
344
+ owner: Any, name: str, label: str | None = None, capture_args: Any = captured
345
+ ) -> wrapture.Binding:
346
+ return wrapture.binding(
347
+ owner,
348
+ name,
349
+ label=label,
350
+ category="database",
351
+ leaf=True,
352
+ capture_args=capture_args,
353
+ capture_result=captured,
354
+ )
355
+
356
+ named: dict[str, wrapture.Binding] = {}
357
+
358
+ # The factory: the module attribute's path says psycopg2:connect
359
+ # already, so it takes no label, and none of its arguments are
360
+ # captured (the dsn carries the password).
361
+
362
+ connect = database_binding(module, "connect", capture_args="none")
363
+ connect.on_call.decorates(opens)
364
+ named["connect"] = connect
365
+
366
+ # The cursor methods, labelled with the psycopg2 names they stand
367
+ # for.
368
+
369
+ for method, decorator in (
370
+ ("execute", queries),
371
+ ("executemany", queries),
372
+ ("callproc", calls),
373
+ ("copy_from", copies_table),
374
+ ("copy_to", copies_table),
375
+ ("copy_expert", copies_statement),
376
+ ):
377
+ bound = database_binding(
378
+ CursorMixin, method, label=f"psycopg2.extensions:cursor.{method}"
379
+ )
380
+ bound.on_call.decorates(decorator)
381
+ named[f"cursor_{method}"] = bound
382
+
383
+ # The transaction boundaries: the explicit calls, and the context
384
+ # manager exit that performs one of them.
385
+
386
+ for method, operation in (("commit", "COMMIT"), ("rollback", "ROLLBACK")):
387
+ bound = database_binding(
388
+ ConnectionMixin, method, label=f"psycopg2.extensions:connection.{method}"
389
+ )
390
+ bound.on_call.decorates(performs(operation))
391
+ named[f"connection_{method}"] = bound
392
+
393
+ closes = database_binding(
394
+ ConnectionMixin, "__exit__", label="psycopg2.extensions:connection.__exit__"
395
+ )
396
+ closes.on_call.decorates(leaves)
397
+ named["connection_exit"] = closes
398
+
399
+ group = wrapture.bindings(**named)
400
+ group.apply()
401
+
402
+ instrumentation.on_cleanup(group.remove)
File without changes
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: wrapture-instrumentation-postgresql
3
+ Version: 1.0.0.dev1
4
+ Summary: Instrumentation for the PostgreSQL client libraries (psycopg, psycopg2, asyncpg), applied through wrapture.
5
+ Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
6
+ License-Expression: BSD-2-Clause
7
+ Project-URL: Homepage, https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql
8
+ Project-URL: Documentation, https://wrapture.readthedocs.io
9
+ Project-URL: Bug Tracker, https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/issues/
10
+ Keywords: wrapture,instrumentation,postgresql,psycopg,psycopg2,asyncpg,tracing
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: 3.15
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: wrapture>=1.0.0a20
22
+ Dynamic: license-file
23
+
24
+ # wrapture-instrumentation-postgresql
25
+
26
+ Instrumentation for the PostgreSQL client libraries, applied through
27
+ [wrapture](https://github.com/GrahamDumpleton/wrapture).
28
+
29
+ wrapture attaches bindings to arbitrary Python call sites without
30
+ modifying the code being observed, and its config layer can switch on
31
+ packaged instrumentation for a third-party package by name. This is
32
+ the PostgreSQL package in that collection: one `wrapture.Instrumentation`
33
+ class per client library, so tracing every query, connection and
34
+ transaction your application sends to PostgreSQL is one config entry
35
+ and no code.
36
+
37
+ > **Status: alpha, ahead of 1.0.0.** Developed against wrapture's
38
+ > alpha series, with pre-releases published to
39
+ > [PyPI](https://pypi.org/project/wrapture-instrumentation-postgresql/),
40
+ > and until 1.0.0 is final a plain `pip install
41
+ > wrapture-instrumentation-postgresql` picks up the latest pre-release
42
+ > automatically, so there is no need to pin a specific version.
43
+
44
+ ## Why a separate package
45
+
46
+ The core
47
+ [wrapture-instrumentation](https://github.com/GrahamDumpleton/wrapture-instrumentation)
48
+ package deliberately covers only the standard library and third-party
49
+ packages that can be exercised in-process, with no separate backend
50
+ product or service needed to test against. A PostgreSQL driver is
51
+ exactly the kind of target the separate-package rule was drawn for:
52
+ its tests need a real server, so this package's suite runs one in a
53
+ docker container, and it carries the drivers as test dependencies
54
+ (some of them compiled wheels) and its own release cadence, so the
55
+ core package's test matrix stays light. One package covers every
56
+ client library for the one backend: psycopg, psycopg2 and asyncpg.
57
+
58
+ ## Installation
59
+
60
+ ```console
61
+ $ pip install wrapture-instrumentation-postgresql
62
+ ```
63
+
64
+ Installing it brings wrapture and nothing else. No driver is a
65
+ dependency: each instrumentation is inert until its driver is present,
66
+ and wrapture checks the installed version against the range the
67
+ instrumentation supports at apply time.
68
+
69
+ ## Using it
70
+
71
+ An `[[instrument]]` entry in `wrapture.toml` names the target:
72
+
73
+ ```toml
74
+ [[instrument]]
75
+ name = "psycopg"
76
+
77
+ [[sink]]
78
+ type = "printer"
79
+ ```
80
+
81
+ and the runner applies it before the application starts, so the patch
82
+ is in place before the driver is imported:
83
+
84
+ ```console
85
+ $ python -m wrapture -m myapp
86
+ ```
87
+
88
+ The same config works through
89
+ [autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
90
+ (`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`); through
91
+ [manual setup](https://wrapture.readthedocs.io/en/latest/manual-setup.html),
92
+ a few lines in the application's own startup where wrapping the launch
93
+ from outside is awkward; and, in a test, through
94
+ `wrapture.instrumentation("psycopg")` scoping the instrumentation to
95
+ a block. The
96
+ [ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
97
+ covers the config file itself.
98
+
99
+ To see what is installed, what it supports in the current
100
+ environment, and what settings it takes:
101
+
102
+ ```console
103
+ $ python -m wrapture.tools instrumentation --verbose
104
+ ```
105
+
106
+ ## Provided instrumentation
107
+
108
+ | Target | Supported versions | Records | Settings |
109
+ | ------ | ------------------ | ------- | -------- |
110
+ | [`psycopg`](https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/blob/develop/src/wrapture_instrumentation_postgresql/psycopg/README.md) | psycopg 3.1+ (3.x) | Every query as one `database` leaf, however it was issued (a cursor's `execute` or `executemany`, the connection's shortcut, a streamed query, a COPY, a server-side cursor's DECLARE), plus the connection being opened and each transaction boundary (`commit`, `rollback`, the connection's context manager, and a `transaction()` block's begin and end, savepoints included); sync and async classes alike, and connections from a pool. Each event carries the system, the operation, and the database, host and port it reached; a failing statement records the driver's exception. The SQL text is recorded only with the `statement` setting on, bound parameters never. | `statement` |
111
+ | [`psycopg2`](https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/blob/develop/src/wrapture_instrumentation_postgresql/psycopg2/README.md) | psycopg2 2.9+ (2.x), psycopg2-binary alike | Every query as one `database` leaf, however it was issued (`execute`, `executemany`, `callproc`, the extras' batch helpers, a named cursor's DECLARE), each COPY (`copy_from`, `copy_to`, `copy_expert`), the connection being opened and each transaction boundary (`commit`, `rollback`, the connection's context manager); through recording subclasses injected by psycopg2's own factory mechanism, so your `cursor_factory` and `connection_factory` classes keep working and are recorded too. Each event carries the system, the operation, and the database, host and port it reached; a failing statement records the driver's exception. The SQL text (the template with its placeholders) is recorded only with the `statement` setting on, bound parameters never. | `statement` |
112
+ | [`asyncpg`](https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/blob/develop/src/wrapture_instrumentation_postgresql/asyncpg/README.md) | asyncpg 0.29+ (0.x) | Every query as one `database` leaf recorded around its await, however it was issued (a connection's `execute`, `executemany`, `fetch`, `fetchrow`, `fetchval` or `fetchmany`, a prepared statement's own fetches, a server-side cursor's DECLARE and each FETCH), each COPY, the connection being opened, and each transaction boundary, which asyncpg issues through `execute`. Each event carries the system, the operation, and the database, host and port it reached; a failing statement records the driver's exception. The SQL text (with its `$n` placeholders) is recorded only with the `statement` setting on, query arguments never. A connection taken from a pool does not yet record its queries (a wrapture fix is pending). | `statement` |
113
+
114
+ The entry point name is the config's `name`""; the linked per-target
115
+ README is the full user documentation: what records, what the events
116
+ carry, the setting, and what is deliberately not traced.
117
+
118
+ ## What is not traced
119
+
120
+ By design, and where it goes:
121
+
122
+ - Fetching rows: a query event closes when its execute returns, so
123
+ time spent iterating rows afterwards is the application's, and a
124
+ server-side cursor's FETCHes are not recorded (its DECLARE is).
125
+
126
+ - Pool checkouts (`psycopg_pool`): a connection taken from a pool
127
+ records its queries like any other, but taking and returning it are
128
+ not database operations and are not recorded.
129
+
130
+ - A connection taken from an asyncpg pool does not yet record its
131
+ queries: the pool proxy calls the connection's methods through the
132
+ class, a calling convention wrapture's signature check does not
133
+ yet handle. The fix is on wrapture's side.
134
+
135
+ - LISTEN/NOTIFY, large objects and two-phase commit are out of scope.
136
+
137
+ ## Adding a target
138
+
139
+ Each client library is its own subpackage and entry point here. The
140
+ subpackage's `__init__.py` holds one `wrapture.Instrumentation`
141
+ subclass and imports only wrapture (and the package's own `common.py`,
142
+ which imports only wrapture too); everything that touches the driver
143
+ lives in sibling modules imported inside the hook. The class is
144
+ registered in `pyproject.toml` under
145
+ `[project.entry-points."wrapture.instrumentation"]`, and gets its own
146
+ test suite under `tests/<target>/` and a `README.md` linked from the
147
+ table above. The
148
+ [instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
149
+ page of the wrapture documentation is the full contract; TESTING.md
150
+ here covers the tests and the server they run against.
151
+
152
+ ## License
153
+
154
+ BSD 2-Clause. See
155
+ [LICENSE](https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/blob/develop/LICENSE).
@@ -0,0 +1,22 @@
1
+ wrapture_instrumentation_postgresql/__init__.py,sha256=WA0NLvebvLdxRWlvGnabFloQHU1Q8b4b4VWjpV3aZSo,755
2
+ wrapture_instrumentation_postgresql/common.py,sha256=bkRjg8p4nb1HMhzDZN55J9Dh-kGZ6Wr0gUt1I9nEdNU,5457
3
+ wrapture_instrumentation_postgresql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ wrapture_instrumentation_postgresql/asyncpg/README.md,sha256=PPxCwe1LLrvHpEg7X516O5sfL2MqR9hbAcFPBrOG-z0,6014
5
+ wrapture_instrumentation_postgresql/asyncpg/__init__.py,sha256=yb_axU7Ii-2LyqOjzXKgB7Z3lV5jcXsrDWyNOXJyTwY,2689
6
+ wrapture_instrumentation_postgresql/asyncpg/connection.py,sha256=HH5vinUeXs678wpV-puoQ0RwIbyQ4nJfAMTnKYlSmy8,7538
7
+ wrapture_instrumentation_postgresql/asyncpg/cursor.py,sha256=2_X9YIxRSDDhTLAKAaJULAAlQobwGr6b4asxf_IB5Yk,3169
8
+ wrapture_instrumentation_postgresql/asyncpg/prepared.py,sha256=mW71-YRdvnyEkMO444ATjsnioa-IAKH7kwCaQVp5PKk,2485
9
+ wrapture_instrumentation_postgresql/psycopg/README.md,sha256=WzCd-v6b2wrfkaOZ2oNJBpPtPgHiRWaPe359o7wPTos,6642
10
+ wrapture_instrumentation_postgresql/psycopg/__init__.py,sha256=36aNivArazNIE8Mj_yzYFY_uYnHjlcfWAwc4y4oo2Y4,2019
11
+ wrapture_instrumentation_postgresql/psycopg/connection.py,sha256=tcuE2v6FVaEO9Or2ut0980yhKphN8sUp5N06m49Qq4A,5696
12
+ wrapture_instrumentation_postgresql/psycopg/cursor.py,sha256=qhK4i1Qf8Ho36oyCBQ714MGy0CBJNiY7MUqNDuwmEwU,9063
13
+ wrapture_instrumentation_postgresql/psycopg/transaction.py,sha256=27FP_Iin2jZknZ8IsLJquuK6mkSmz9llKOtCWVsmxLs,4427
14
+ wrapture_instrumentation_postgresql/psycopg2/README.md,sha256=AWU5p9nCbx7wEGQILk4cBauo9y_KfqlkRxU_TBsYS2A,5897
15
+ wrapture_instrumentation_postgresql/psycopg2/__init__.py,sha256=CP_9F74TjeDGeGU3S0DPyVvNa02HuYeaQqd0EsN0pOg,1649
16
+ wrapture_instrumentation_postgresql/psycopg2/factories.py,sha256=cBcc5W1ywljUqIQSu8_7YV_M4t3Q4ktCYk-kkonr5Ds,14314
17
+ wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/licenses/LICENSE,sha256=RPMyRnXwgrw9co5o6wY9G3DKlb-oBZ44tpiRnuYeec8,1299
18
+ wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/METADATA,sha256=P76R8MC0fqKxN71T71AjYtRofVZyoxX3ay7_rIrCdZg,8933
19
+ wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
20
+ wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/entry_points.txt,sha256=pPB1skIzk-3RIv8jXObnlfM0rW7uiKTnCgyQ6hJ4lA0,261
21
+ wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/top_level.txt,sha256=xAb-vQUXGc51uRzpvxOF3M-gjE9VqNlTzdlLOFkDPHA,36
22
+ wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [wrapture.instrumentation]
2
+ asyncpg = wrapture_instrumentation_postgresql.asyncpg:AsyncpgInstrumentation
3
+ psycopg = wrapture_instrumentation_postgresql.psycopg:PsycopgInstrumentation
4
+ psycopg2 = wrapture_instrumentation_postgresql.psycopg2:Psycopg2Instrumentation
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2026, Graham Dumpleton
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 met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24
+ POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ wrapture_instrumentation_postgresql