wrapture-instrumentation-postgresql 1.0.0.dev1__tar.gz

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.
Files changed (62) hide show
  1. wrapture_instrumentation_postgresql-1.0.0.dev1/CHANGES.md +9 -0
  2. wrapture_instrumentation_postgresql-1.0.0.dev1/Dockerfile +15 -0
  3. wrapture_instrumentation_postgresql-1.0.0.dev1/LICENSE +24 -0
  4. wrapture_instrumentation_postgresql-1.0.0.dev1/MANIFEST.in +8 -0
  5. wrapture_instrumentation_postgresql-1.0.0.dev1/PKG-INFO +155 -0
  6. wrapture_instrumentation_postgresql-1.0.0.dev1/README.md +132 -0
  7. wrapture_instrumentation_postgresql-1.0.0.dev1/TESTING.md +264 -0
  8. wrapture_instrumentation_postgresql-1.0.0.dev1/compose.yml +56 -0
  9. wrapture_instrumentation_postgresql-1.0.0.dev1/demo/__init__.py +0 -0
  10. wrapture_instrumentation_postgresql-1.0.0.dev1/demo/asyncpg.py +150 -0
  11. wrapture_instrumentation_postgresql-1.0.0.dev1/demo/psycopg.py +138 -0
  12. wrapture_instrumentation_postgresql-1.0.0.dev1/demo/psycopg2.py +138 -0
  13. wrapture_instrumentation_postgresql-1.0.0.dev1/pyproject.toml +125 -0
  14. wrapture_instrumentation_postgresql-1.0.0.dev1/setup.cfg +4 -0
  15. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/__init__.py +26 -0
  16. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/asyncpg/README.md +136 -0
  17. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/asyncpg/__init__.py +75 -0
  18. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/asyncpg/connection.py +220 -0
  19. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/asyncpg/cursor.py +88 -0
  20. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/asyncpg/prepared.py +74 -0
  21. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/common.py +169 -0
  22. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/psycopg/README.md +144 -0
  23. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/psycopg/__init__.py +57 -0
  24. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/psycopg/connection.py +168 -0
  25. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/psycopg/cursor.py +258 -0
  26. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/psycopg/transaction.py +126 -0
  27. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/psycopg2/README.md +131 -0
  28. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/psycopg2/__init__.py +49 -0
  29. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/psycopg2/factories.py +402 -0
  30. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql/py.typed +0 -0
  31. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql.egg-info/PKG-INFO +155 -0
  32. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql.egg-info/SOURCES.txt +60 -0
  33. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql.egg-info/dependency_links.txt +1 -0
  34. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql.egg-info/entry_points.txt +4 -0
  35. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql.egg-info/not-zip-safe +1 -0
  36. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql.egg-info/requires.txt +1 -0
  37. wrapture_instrumentation_postgresql-1.0.0.dev1/src/wrapture_instrumentation_postgresql.egg-info/top_level.txt +1 -0
  38. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/__init__.py +0 -0
  39. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/asyncpg/__init__.py +0 -0
  40. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/asyncpg/conftest.py +22 -0
  41. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/asyncpg/test_apply_remove.py +89 -0
  42. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/asyncpg/test_class.py +67 -0
  43. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/asyncpg/test_composition.py +148 -0
  44. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/asyncpg/test_recording.py +430 -0
  45. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/asyncpg/test_registration.py +82 -0
  46. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/conftest.py +320 -0
  47. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg/__init__.py +0 -0
  48. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg/conftest.py +22 -0
  49. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg/test_apply_remove.py +94 -0
  50. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg/test_async.py +127 -0
  51. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg/test_class.py +70 -0
  52. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg/test_composition.py +182 -0
  53. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg/test_recording.py +478 -0
  54. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg/test_registration.py +79 -0
  55. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg2/__init__.py +0 -0
  56. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg2/conftest.py +23 -0
  57. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg2/test_apply_remove.py +95 -0
  58. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg2/test_class.py +85 -0
  59. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg2/test_composition.py +152 -0
  60. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg2/test_recording.py +526 -0
  61. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/psycopg2/test_registration.py +77 -0
  62. wrapture_instrumentation_postgresql-1.0.0.dev1/tests/test_package.py +179 -0
@@ -0,0 +1,9 @@
1
+ # Changes
2
+
3
+ ## Version 1.0.0
4
+
5
+ Initial release of wrapture-instrumentation-postgresql. Everything is
6
+ new in this version, so rather than listing changes, see the
7
+ [README](README.md) for what the package provides: the table of
8
+ covered targets there links to each target's own notes describing
9
+ what it records and its settings.
@@ -0,0 +1,15 @@
1
+ # The environment the test matrix runs in: Debian with uv, and the
2
+ # one thing psycopg's pure Python implementation needs from the
3
+ # machine, a libpq. Nothing else is baked in: uv fetches whichever
4
+ # interpreter the run asks for and installs the test dependencies into
5
+ # a per-version environment, both on named volumes so they persist
6
+ # across runs (see compose.yml). The repository is bind-mounted at
7
+ # run time, so the image never goes stale as the code changes.
8
+
9
+ FROM ghcr.io/astral-sh/uv:debian-slim
10
+
11
+ RUN apt-get update \
12
+ && apt-get install -y --no-install-recommends libpq5 \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ WORKDIR /work
@@ -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,8 @@
1
+ # setuptools includes test_*.py files by default but not the helpers
2
+ # they import (conftest.py, the package __init__.py), so the tests
3
+ # directory is taken whole, with the repository's other notes and the
4
+ # docker files the test matrix runs under.
5
+ graft tests
6
+ graft demo
7
+ include CHANGES.md TESTING.md compose.yml Dockerfile
8
+ global-exclude __pycache__ *.py[cod]
@@ -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,132 @@
1
+ # wrapture-instrumentation-postgresql
2
+
3
+ Instrumentation for the PostgreSQL client libraries, applied through
4
+ [wrapture](https://github.com/GrahamDumpleton/wrapture).
5
+
6
+ wrapture attaches bindings to arbitrary Python call sites without
7
+ modifying the code being observed, and its config layer can switch on
8
+ packaged instrumentation for a third-party package by name. This is
9
+ the PostgreSQL package in that collection: one `wrapture.Instrumentation`
10
+ class per client library, so tracing every query, connection and
11
+ transaction your application sends to PostgreSQL is one config entry
12
+ and no code.
13
+
14
+ > **Status: alpha, ahead of 1.0.0.** Developed against wrapture's
15
+ > alpha series, with pre-releases published to
16
+ > [PyPI](https://pypi.org/project/wrapture-instrumentation-postgresql/),
17
+ > and until 1.0.0 is final a plain `pip install
18
+ > wrapture-instrumentation-postgresql` picks up the latest pre-release
19
+ > automatically, so there is no need to pin a specific version.
20
+
21
+ ## Why a separate package
22
+
23
+ The core
24
+ [wrapture-instrumentation](https://github.com/GrahamDumpleton/wrapture-instrumentation)
25
+ package deliberately covers only the standard library and third-party
26
+ packages that can be exercised in-process, with no separate backend
27
+ product or service needed to test against. A PostgreSQL driver is
28
+ exactly the kind of target the separate-package rule was drawn for:
29
+ its tests need a real server, so this package's suite runs one in a
30
+ docker container, and it carries the drivers as test dependencies
31
+ (some of them compiled wheels) and its own release cadence, so the
32
+ core package's test matrix stays light. One package covers every
33
+ client library for the one backend: psycopg, psycopg2 and asyncpg.
34
+
35
+ ## Installation
36
+
37
+ ```console
38
+ $ pip install wrapture-instrumentation-postgresql
39
+ ```
40
+
41
+ Installing it brings wrapture and nothing else. No driver is a
42
+ dependency: each instrumentation is inert until its driver is present,
43
+ and wrapture checks the installed version against the range the
44
+ instrumentation supports at apply time.
45
+
46
+ ## Using it
47
+
48
+ An `[[instrument]]` entry in `wrapture.toml` names the target:
49
+
50
+ ```toml
51
+ [[instrument]]
52
+ name = "psycopg"
53
+
54
+ [[sink]]
55
+ type = "printer"
56
+ ```
57
+
58
+ and the runner applies it before the application starts, so the patch
59
+ is in place before the driver is imported:
60
+
61
+ ```console
62
+ $ python -m wrapture -m myapp
63
+ ```
64
+
65
+ The same config works through
66
+ [autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
67
+ (`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`); through
68
+ [manual setup](https://wrapture.readthedocs.io/en/latest/manual-setup.html),
69
+ a few lines in the application's own startup where wrapping the launch
70
+ from outside is awkward; and, in a test, through
71
+ `wrapture.instrumentation("psycopg")` scoping the instrumentation to
72
+ a block. The
73
+ [ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
74
+ covers the config file itself.
75
+
76
+ To see what is installed, what it supports in the current
77
+ environment, and what settings it takes:
78
+
79
+ ```console
80
+ $ python -m wrapture.tools instrumentation --verbose
81
+ ```
82
+
83
+ ## Provided instrumentation
84
+
85
+ | Target | Supported versions | Records | Settings |
86
+ | ------ | ------------------ | ------- | -------- |
87
+ | [`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` |
88
+ | [`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` |
89
+ | [`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` |
90
+
91
+ The entry point name is the config's `name`""; the linked per-target
92
+ README is the full user documentation: what records, what the events
93
+ carry, the setting, and what is deliberately not traced.
94
+
95
+ ## What is not traced
96
+
97
+ By design, and where it goes:
98
+
99
+ - Fetching rows: a query event closes when its execute returns, so
100
+ time spent iterating rows afterwards is the application's, and a
101
+ server-side cursor's FETCHes are not recorded (its DECLARE is).
102
+
103
+ - Pool checkouts (`psycopg_pool`): a connection taken from a pool
104
+ records its queries like any other, but taking and returning it are
105
+ not database operations and are not recorded.
106
+
107
+ - A connection taken from an asyncpg pool does not yet record its
108
+ queries: the pool proxy calls the connection's methods through the
109
+ class, a calling convention wrapture's signature check does not
110
+ yet handle. The fix is on wrapture's side.
111
+
112
+ - LISTEN/NOTIFY, large objects and two-phase commit are out of scope.
113
+
114
+ ## Adding a target
115
+
116
+ Each client library is its own subpackage and entry point here. The
117
+ subpackage's `__init__.py` holds one `wrapture.Instrumentation`
118
+ subclass and imports only wrapture (and the package's own `common.py`,
119
+ which imports only wrapture too); everything that touches the driver
120
+ lives in sibling modules imported inside the hook. The class is
121
+ registered in `pyproject.toml` under
122
+ `[project.entry-points."wrapture.instrumentation"]`, and gets its own
123
+ test suite under `tests/<target>/` and a `README.md` linked from the
124
+ table above. The
125
+ [instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
126
+ page of the wrapture documentation is the full contract; TESTING.md
127
+ here covers the tests and the server they run against.
128
+
129
+ ## License
130
+
131
+ BSD 2-Clause. See
132
+ [LICENSE](https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/blob/develop/LICENSE).
@@ -0,0 +1,264 @@
1
+ # Testing
2
+
3
+ ## Where the tests are
4
+
5
+ Tests live in the [tests/](tests/) directory at the top of the
6
+ repository, separate from the package code in
7
+ src/wrapture_instrumentation_postgresql/. Test files are named
8
+ `test_*.py` and are discovered by pytest, configured via the
9
+ `[tool.pytest.ini_options]` section of [pyproject.toml](pyproject.toml).
10
+
11
+ The directory has two levels:
12
+
13
+ - Package-level tests directly under tests/: the version, the rule
14
+ that importing the package or loading any registered class never
15
+ imports a target, and the listing tool reporting every entry
16
+ cleanly.
17
+
18
+ - One subdirectory per target, `tests/<target>/` (`tests/psycopg/`,
19
+ `tests/psycopg2/`, `tests/asyncpg/`),
20
+ holding that instrumentation's suite: settings validation, applying
21
+ and removing the class directly, the whole path through
22
+ `wrapture.instrumentation()` with a timeline recording what the
23
+ bindings observe (sync and async), resolving the entry point by
24
+ name, a check that the installed driver satisfies the class's
25
+ `supports` range, and the composition tests with the core package's
26
+ sqlalchemy target.
27
+
28
+ Shared helpers and the server fixture live in
29
+ [tests/conftest.py](tests/conftest.py).
30
+
31
+ ## The PostgreSQL server
32
+
33
+ The suites drive the real drivers against a real PostgreSQL server;
34
+ nothing is mocked. The session-scoped `postgresql` fixture supplies
35
+ it, in one of two ways:
36
+
37
+ - `WRAPTURE_POSTGRESQL_URL` in the environment names a server already
38
+ running (`postgresql://user:password@host:port/dbname`), and the
39
+ fixture uses it and starts nothing. This is how CI's service
40
+ container, the compose file's `postgres` service and a server of
41
+ your own are used.
42
+
43
+ - Otherwise the fixture runs a throwaway container from the official
44
+ `postgres:17` image on a random localhost port, waits until a real
45
+ connection from the host answers, and removes the container at the
46
+ end of the session. This needs the docker CLI and a running daemon
47
+ (Docker Desktop, or anything docker-compatible).
48
+
49
+ With neither, the session fails with a message saying so rather than
50
+ skipping, so a green run always means the suites ran. The fixture
51
+ yields a `Server` with the parts in both spellings the drivers take
52
+ (`server.url`, `server.kwargs`). Tests isolate themselves with
53
+ temporary tables (`CREATE TEMP TABLE`), private to their connection
54
+ and gone when it closes, so no cleanup fixtures are needed.
55
+
56
+ An interrupted run can leave its throwaway container behind;
57
+ `just postgresql-clean` removes any such container.
58
+
59
+ ## Running the tests
60
+
61
+ All tooling in this project goes through
62
+ [uv](https://docs.astral.sh/uv/), which manages the project
63
+ environment and installs the package, its development dependencies
64
+ (including pytest) and the drivers the tests need.
65
+
66
+ The simplest way to run the test suite is via the Justfile target,
67
+ which runs natively on the default Python version and starts a
68
+ throwaway server unless `WRAPTURE_POSTGRESQL_URL` is set:
69
+
70
+ ```console
71
+ just test
72
+ ```
73
+
74
+ Extra arguments are passed through to pytest, for example:
75
+
76
+ ```console
77
+ just test -v
78
+ just test tests/psycopg/test_recording.py
79
+ just test -k transaction
80
+ ```
81
+
82
+ One target's suite alone:
83
+
84
+ ```console
85
+ just test-target psycopg
86
+ ```
87
+
88
+ Equivalently, run pytest directly with uv:
89
+
90
+ ```console
91
+ uv run pytest
92
+ ```
93
+
94
+ To run against a longer-lived server instead of a throwaway one per
95
+ session, start the compose file's server and export the URL it
96
+ prints:
97
+
98
+ ```console
99
+ just postgresql-start
100
+ export WRAPTURE_POSTGRESQL_URL=postgresql://postgres:postgres@localhost:54329/postgres
101
+ just test
102
+ just postgresql-stop
103
+ ```
104
+
105
+ ## Watching what the tests record
106
+
107
+ The suites assert on tapes rather than printing anything, but the
108
+ recorded events can be watched live for visual verification. Setting
109
+ WRAPTURE_PRINTER in the environment installs a process-wide
110
+ wrapture.Printer sink for the session, streaming one line to stderr as
111
+ each operation begins and a closing line with its outcome and timing;
112
+ pytest captures stderr, so add -s to see it:
113
+
114
+ ```console
115
+ WRAPTURE_PRINTER=1 just test tests/psycopg -s
116
+ ```
117
+
118
+ The same events can go to an OpenTelemetry backend instead, for
119
+ checking the spans, their kinds and their attributes in something
120
+ like otel-desktop-viewer. Setting WRAPTURE_OTEL installs wrapture's
121
+ OpenTelemetry sink for the session (exporting over OTLP to
122
+ http://localhost:4318, or wherever OTEL_EXPORTER_OTLP_ENDPOINT
123
+ points) and flushes it when the session ends; the `test-otel` recipe
124
+ sets the variable and overlays the wrapture[otel] dependencies the
125
+ sink needs:
126
+
127
+ ```console
128
+ just test-otel tests/psycopg/test_recording.py -k transaction
129
+ ```
130
+
131
+ Each event the tests record arrives as its own single-span trace:
132
+ the tests drive the driver directly with nothing of their own around
133
+ the calls, and a `timeline()` roots what it records (an enclosing
134
+ block or observed function outside the timeline is not the parent of
135
+ what happens inside it), so a root span per test would have to be
136
+ opened inside each test's own timeline and would then appear on the
137
+ tape the assertions read.
138
+
139
+ For a purpose-built run rather than the tests' traffic, the demo
140
+ module under demo/ applies the instrumentation, drives the driver
141
+ against the server `WRAPTURE_POSTGRESQL_URL` names, and prints both
142
+ the live stream and the reconstructed tree with timings:
143
+
144
+ ```console
145
+ just postgresql-start
146
+ export WRAPTURE_POSTGRESQL_URL=postgresql://postgres:postgres@localhost:54329/postgres
147
+ just demo-psycopg
148
+ just demo-psycopg2
149
+ just demo-asyncpg
150
+ ```
151
+
152
+ With --otel the same events also export as OpenTelemetry spans over
153
+ OTLP (to http://localhost:4318, or wherever
154
+ OTEL_EXPORTER_OTLP_ENDPOINT points), for verifying the spans in a
155
+ local backend such as Jaeger; the Justfile target overlays the
156
+ wrapture[otel] dependencies for the run:
157
+
158
+ ```console
159
+ just demo-psycopg --otel
160
+ ```
161
+
162
+ ## Testing across Python versions
163
+
164
+ The project supports Python 3.12 through 3.15. Free threaded builds
165
+ are not in the matrix: the instrumentation is pure Python and behaves
166
+ the same on them, and a free threaded row would only be testing
167
+ whether the drivers' own C extensions build without the GIL, which is
168
+ their concern. The supported list is defined at the top of the
169
+ [Justfile](Justfile); the default version used by plain `just test`
170
+ is pinned in [.python-version](.python-version).
171
+
172
+ The full matrix runs inside docker, so the machine never needs
173
+ PostgreSQL client libraries: psycopg's binary wheels bundle a libpq up
174
+ to Python 3.14, but on 3.15 psycopg runs as pure Python and needs a
175
+ libpq from the machine, which the image in the [Dockerfile](Dockerfile)
176
+ supplies. The [compose.yml](compose.yml) file defines the server and
177
+ a `tests` container built from that image; uv fetches the requested
178
+ interpreter inside the container and installs the test dependencies
179
+ into a per-version environment, both kept on named volumes so reruns
180
+ pay for neither.
181
+
182
+ ```console
183
+ just test-all
184
+ just test-docker 3.15
185
+ ```
186
+
187
+ The first run per version downloads the interpreter and the wheels.
188
+ For the versions where every driver has a wheel, the suite can also
189
+ run natively in a per-version environment (.venv-VERSION) that leaves
190
+ the default .venv untouched:
191
+
192
+ ```console
193
+ just test-python 3.13
194
+ ```
195
+
196
+ ## Testing across driver versions
197
+
198
+ The `test` dependency group installs each driver at whatever version
199
+ the lock resolves. The instrumentation's `supports` range is kept
200
+ honest by running its suite against other lines of the driver. Each
201
+ line has a place in `psycopg_versions`, `psycopg2_versions` or
202
+ `asyncpg_versions` in the Justfile, run one at a time by
203
+ `just test-psycopg 3.1.20`, `just test-psycopg2 2.9.9` or
204
+ `just test-asyncpg 0.29.0`, or all by the `-all` forms, and the CI
205
+ workflow runs the same matrix. These runs use an environment of their
206
+ own on Python 3.12 with the driver's binary distribution at the
207
+ requested version (`psycopg[binary]`, `psycopg2-binary`, `asyncpg`): a driver's
208
+ binary wheel must match its version, and the older lines ship binary
209
+ wheels only up to the Pythons of their day, so 3.12 is the Python
210
+ every line has a wheel for, and no libpq is needed from the machine. A test in the suite
211
+ asserts the installed driver satisfies `supports`, so a matrix entry
212
+ outside the range fails loudly rather than passing vacuously.
213
+
214
+ ## Continuous integration
215
+
216
+ The workflow runs every test job on Linux with a PostgreSQL service
217
+ container (service containers are Linux-only, and the drivers do not
218
+ differ per OS at the instrumented layer), the URL in the environment,
219
+ across Python 3.12 to 3.15 and the driver version matrix.
220
+
221
+ ## Testing against unreleased wrapture
222
+
223
+ The package depends on a released wrapture. To run the tests against a
224
+ checkout of wrapture in the sibling directory ../wrapture, without
225
+ editing pyproject.toml:
226
+
227
+ ```console
228
+ just test-dev
229
+ ```
230
+
231
+ which overlays that checkout as an editable install for the run.
232
+
233
+ ## Writing tests
234
+
235
+ - Put new test files in tests/ (package level) or tests/<target>/
236
+ (for one instrumentation) and name them `test_*.py`.
237
+
238
+ - Import the package under test as
239
+ `wrapture_instrumentation_postgresql`, and the instrumentation
240
+ classes from their subpackages. The project is installed into the
241
+ uv-managed environment, so no path manipulation is needed.
242
+
243
+ - Guard the driver imports with `pytest.importorskip` at the top of
244
+ the module (`pytest.importorskip("psycopg")`), so a build without
245
+ that driver skips the suite rather than erroring.
246
+
247
+ - Take the `postgresql` fixture for the server and create temporary
248
+ tables for whatever the test needs; never leave ordinary tables
249
+ behind.
250
+
251
+ - Validate behaviour with wrapture's own unit testing layer:
252
+ `wrapture.timeline()` to record what the instrumentation's bindings
253
+ observe and the tape's tree and queries to assert on it, and
254
+ `wrapture.instrumentation()` to scope an application of a class to
255
+ a block, driving the real driver against the real server. Do not
256
+ mock a driver and do not use `unittest.mock`. If something cannot
257
+ be expressed that way, write it plainly with a comment naming the
258
+ gap, and say so when summarising the work.
259
+
260
+ - Async cases run their coroutine with `asyncio.run` inside the test;
261
+ no pytest-asyncio.
262
+
263
+ - Tests should not depend on anything in the scratch/ directory,
264
+ which is not part of the repository.
@@ -0,0 +1,56 @@
1
+ # The PostgreSQL server the tests need, and a container to run the
2
+ # tests in for the Python versions that need a libpq from the machine.
3
+ #
4
+ # just test-docker 3.15 one version, extra args to pytest
5
+ # just test-docker-all every supported version
6
+ # just postgresql-start the server alone, published on
7
+ # localhost:54329 for the demos and
8
+ # for running the tests natively
9
+ #
10
+ # The tests container gets the server's URL in the environment, so the
11
+ # suite's server fixture uses it and starts nothing itself, exactly as
12
+ # on CI with its service container. uv's cache and the managed
13
+ # interpreters live on one named volume and the per-version project
14
+ # environments on another, so a rerun pays for neither the download
15
+ # nor the install, and the container's environment never collides
16
+ # with the .venv the host uses in the same directory.
17
+
18
+ services:
19
+ postgres:
20
+ image: postgres:17
21
+ environment:
22
+ POSTGRES_PASSWORD: postgres
23
+ healthcheck:
24
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
25
+ interval: 2s
26
+ timeout: 5s
27
+ retries: 30
28
+ ports:
29
+ - "127.0.0.1:${WRAPTURE_POSTGRESQL_PORT:-54329}:5432"
30
+
31
+ tests:
32
+ build: .
33
+ depends_on:
34
+ postgres:
35
+ condition: service_healthy
36
+ environment:
37
+ WRAPTURE_POSTGRESQL_URL: postgresql://postgres:postgres@postgres:5432/postgres
38
+ UV_PYTHON: ${UV_PYTHON:-3.14}
39
+ UV_CACHE_DIR: /cache/uv
40
+ UV_PYTHON_INSTALL_DIR: /cache/python
41
+ UV_LINK_MODE: copy
42
+ volumes:
43
+ - .:/work
44
+ - uv-cache:/cache
45
+ - venvs:/venv
46
+ entrypoint:
47
+ - sh
48
+ - -c
49
+ - |
50
+ export UV_PROJECT_ENVIRONMENT="/venv/$$UV_PYTHON"
51
+ exec uv run --python "$$UV_PYTHON" --no-default-groups --group test pytest "$$@"
52
+ - tests
53
+
54
+ volumes:
55
+ uv-cache:
56
+ venvs: