wrapture-instrumentation 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 (32) hide show
  1. wrapture_instrumentation-1.0.0.dev1/CHANGES.md +14 -0
  2. wrapture_instrumentation-1.0.0.dev1/LICENSE +24 -0
  3. wrapture_instrumentation-1.0.0.dev1/MANIFEST.in +7 -0
  4. wrapture_instrumentation-1.0.0.dev1/PKG-INFO +134 -0
  5. wrapture_instrumentation-1.0.0.dev1/README.md +112 -0
  6. wrapture_instrumentation-1.0.0.dev1/TESTING.md +183 -0
  7. wrapture_instrumentation-1.0.0.dev1/demo/__init__.py +17 -0
  8. wrapture_instrumentation-1.0.0.dev1/demo/framework_flask.py +117 -0
  9. wrapture_instrumentation-1.0.0.dev1/pyproject.toml +96 -0
  10. wrapture_instrumentation-1.0.0.dev1/setup.cfg +4 -0
  11. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation/__init__.py +26 -0
  12. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation/framework_flask/__init__.py +36 -0
  13. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation/framework_flask/hooks.py +115 -0
  14. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation/py.typed +0 -0
  15. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/PKG-INFO +134 -0
  16. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/SOURCES.txt +30 -0
  17. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/dependency_links.txt +1 -0
  18. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/entry_points.txt +2 -0
  19. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/not-zip-safe +1 -0
  20. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/requires.txt +1 -0
  21. wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/top_level.txt +1 -0
  22. wrapture_instrumentation-1.0.0.dev1/tests/__init__.py +0 -0
  23. wrapture_instrumentation-1.0.0.dev1/tests/conftest.py +88 -0
  24. wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/__init__.py +0 -0
  25. wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/shop.py +72 -0
  26. wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/test_apply_remove.py +169 -0
  27. wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/test_class.py +68 -0
  28. wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/test_registration.py +78 -0
  29. wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/test_tracing.py +148 -0
  30. wrapture_instrumentation-1.0.0.dev1/tests/test_package.py +149 -0
  31. wrapture_instrumentation-1.0.0.dev1/tests/test_wsgi.py +379 -0
  32. wrapture_instrumentation-1.0.0.dev1/tests/wsgi.py +258 -0
@@ -0,0 +1,14 @@
1
+ # Changes
2
+
3
+ ## Version 1.0.0
4
+
5
+ In development.
6
+
7
+ - Project skeleton: the `wrapture_instrumentation` package with its
8
+ version, the test-side WSGI driver, and the package-level tests
9
+ that keep every registered instrumentation import-light.
10
+
11
+ - Flask (`flask`, Flask 3.x): every application's `wsgi_app` wrapped in
12
+ the recording WSGI middleware at construction, every view function
13
+ observed as it registers, and the exception `Flask.handle_exception`
14
+ receives noted against the request event. No settings yet.
@@ -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,7 @@
1
+ # setuptools includes test_*.py files by default but not the helpers
2
+ # they import (conftest.py, wsgi.py, the package __init__.py), so the
3
+ # tests directory is taken whole, with the repository's other notes.
4
+ graft tests
5
+ graft demo
6
+ include CHANGES.md TESTING.md
7
+ global-exclude __pycache__ *.py[cod]
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: wrapture-instrumentation
3
+ Version: 1.0.0.dev1
4
+ Summary: Instrumentation for common Python packages, 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
8
+ Project-URL: Documentation, https://wrapture.readthedocs.io
9
+ Project-URL: Bug Tracker, https://github.com/GrahamDumpleton/wrapture-instrumentation/issues/
10
+ Keywords: wrapture,instrumentation,monkey patching,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 :: Implementation :: CPython
17
+ Requires-Python: >=3.12
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: wrapture>=1.0.0a4
21
+ Dynamic: license-file
22
+
23
+ # wrapture-instrumentation
24
+
25
+ Instrumentation for common Python packages, applied through
26
+ [wrapture](https://github.com/GrahamDumpleton/wrapture).
27
+
28
+ wrapture attaches bindings to arbitrary Python call sites without
29
+ modifying the code being observed, and its config layer can switch on
30
+ packaged instrumentation for a third-party package by name. This
31
+ project is the collection of that packaged instrumentation: one
32
+ `wrapture.Instrumentation` class per target package (Flask first,
33
+ more to follow), each registered under the bare target name, so that
34
+ tracing a framework is one config entry and no code.
35
+
36
+ > **Status: pre-alpha.** The package is being built target by target
37
+ > against wrapture's alpha series; nothing is published to PyPI yet.
38
+ > Flask is the first target and covers its basics; see the table below.
39
+
40
+ ## Installation
41
+
42
+ ```console
43
+ $ pip install wrapture-instrumentation
44
+ ```
45
+
46
+ Installing it brings wrapture and nothing else. No target package is
47
+ a dependency: the instrumentation for a package you do not have is
48
+ inert, and wrapture checks the installed version of each target
49
+ against the range the instrumentation supports at apply time.
50
+
51
+ ## Using it
52
+
53
+ An `[[instrument]]` entry in `wrapture.toml` names a target:
54
+
55
+ ```toml
56
+ [[instrument]]
57
+ name = "flask"
58
+
59
+ [[sink]]
60
+ type = "printer"
61
+ ```
62
+
63
+ and the runner applies it before the application starts, so the
64
+ patches are in place before the framework is imported:
65
+
66
+ ```console
67
+ $ python -m wrapture -m myapp
68
+ ```
69
+
70
+ The same config works through
71
+ [autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
72
+ (`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`) and, in a test,
73
+ through `wrapture.instrumentation("flask")` scoping the
74
+ instrumentation to a block. The
75
+ [ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
76
+ covers the config file itself.
77
+
78
+ To see what is installed, what each instrumentation supports in the
79
+ current environment, and what settings it takes:
80
+
81
+ ```console
82
+ $ python -m wrapture.tools instrumentation --verbose
83
+ ```
84
+
85
+ and to generate the `[[instrument]]` entries to paste into a config,
86
+ every one disabled and every setting commented out at its default:
87
+
88
+ ```console
89
+ $ python -m wrapture.tools instrumentation --toml
90
+ ```
91
+
92
+ ## Provided instrumentation
93
+
94
+ | Target | Supported versions | Records | Settings |
95
+ | ------ | ------------------ | ------- | -------- |
96
+ | `flask` | Flask 3.x | Every request as one tree (the recording WSGI middleware installed on each application at construction), every view function beneath its request (observed as routes register, blueprints and `MethodView`s included), and the exception Flask catches on its way to a 500 noted on the request. | none yet |
97
+
98
+ The entry point name is the config's `name`; the table says what each
99
+ instrumentation does in the current cut. Settings, further choke
100
+ points and wider version ranges are being added target by target.
101
+
102
+ ## Adding a target
103
+
104
+ Each target lives in its own subpackage under
105
+ `src/wrapture_instrumentation/`, named `<category>_<target>`:
106
+ `framework_flask`, `external_requests`, `database_sqlite3`. The
107
+ category says what kind of thing the target is and, with it, which
108
+ part of wrapture the instrumentation mostly uses:
109
+
110
+ - `framework_`: web frameworks, and their extensions as compound
111
+ names (`framework_flask_restful`).
112
+ - `external_`: outbound HTTP and RPC clients and service SDKs.
113
+ - `database_`: DB-API drivers and SQL toolkits.
114
+ - `datastore_`: other stores and caches.
115
+ - `task_`: task queues. `messaging_`: brokers and their clients.
116
+ - `server_`: WSGI and ASGI servers. `template_`: template engines.
117
+
118
+ A new category is added when a target fits none of these. The
119
+ directory name is internal; the entry point name, and so the name a
120
+ config uses, is always the bare target.
121
+
122
+ The subpackage's `__init__.py` holds one `wrapture.Instrumentation`
123
+ subclass and imports only wrapture; everything that touches the
124
+ target lives in `hooks.py`, reached by an import inside `apply()`.
125
+ The class is registered in `pyproject.toml` under
126
+ `[project.entry-points."wrapture.instrumentation"]`, and gets its own
127
+ test suite under `tests/<category>_<target>/`. The
128
+ [instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
129
+ page of the wrapture documentation is the full contract; TESTING.md
130
+ here covers the tests.
131
+
132
+ ## License
133
+
134
+ BSD-2-Clause, as wrapture.
@@ -0,0 +1,112 @@
1
+ # wrapture-instrumentation
2
+
3
+ Instrumentation for common Python packages, 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
9
+ project is the collection of that packaged instrumentation: one
10
+ `wrapture.Instrumentation` class per target package (Flask first,
11
+ more to follow), each registered under the bare target name, so that
12
+ tracing a framework is one config entry and no code.
13
+
14
+ > **Status: pre-alpha.** The package is being built target by target
15
+ > against wrapture's alpha series; nothing is published to PyPI yet.
16
+ > Flask is the first target and covers its basics; see the table below.
17
+
18
+ ## Installation
19
+
20
+ ```console
21
+ $ pip install wrapture-instrumentation
22
+ ```
23
+
24
+ Installing it brings wrapture and nothing else. No target package is
25
+ a dependency: the instrumentation for a package you do not have is
26
+ inert, and wrapture checks the installed version of each target
27
+ against the range the instrumentation supports at apply time.
28
+
29
+ ## Using it
30
+
31
+ An `[[instrument]]` entry in `wrapture.toml` names a target:
32
+
33
+ ```toml
34
+ [[instrument]]
35
+ name = "flask"
36
+
37
+ [[sink]]
38
+ type = "printer"
39
+ ```
40
+
41
+ and the runner applies it before the application starts, so the
42
+ patches are in place before the framework is imported:
43
+
44
+ ```console
45
+ $ python -m wrapture -m myapp
46
+ ```
47
+
48
+ The same config works through
49
+ [autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
50
+ (`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`) and, in a test,
51
+ through `wrapture.instrumentation("flask")` scoping the
52
+ instrumentation to a block. The
53
+ [ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
54
+ covers the config file itself.
55
+
56
+ To see what is installed, what each instrumentation supports in the
57
+ current environment, and what settings it takes:
58
+
59
+ ```console
60
+ $ python -m wrapture.tools instrumentation --verbose
61
+ ```
62
+
63
+ and to generate the `[[instrument]]` entries to paste into a config,
64
+ every one disabled and every setting commented out at its default:
65
+
66
+ ```console
67
+ $ python -m wrapture.tools instrumentation --toml
68
+ ```
69
+
70
+ ## Provided instrumentation
71
+
72
+ | Target | Supported versions | Records | Settings |
73
+ | ------ | ------------------ | ------- | -------- |
74
+ | `flask` | Flask 3.x | Every request as one tree (the recording WSGI middleware installed on each application at construction), every view function beneath its request (observed as routes register, blueprints and `MethodView`s included), and the exception Flask catches on its way to a 500 noted on the request. | none yet |
75
+
76
+ The entry point name is the config's `name`; the table says what each
77
+ instrumentation does in the current cut. Settings, further choke
78
+ points and wider version ranges are being added target by target.
79
+
80
+ ## Adding a target
81
+
82
+ Each target lives in its own subpackage under
83
+ `src/wrapture_instrumentation/`, named `<category>_<target>`:
84
+ `framework_flask`, `external_requests`, `database_sqlite3`. The
85
+ category says what kind of thing the target is and, with it, which
86
+ part of wrapture the instrumentation mostly uses:
87
+
88
+ - `framework_`: web frameworks, and their extensions as compound
89
+ names (`framework_flask_restful`).
90
+ - `external_`: outbound HTTP and RPC clients and service SDKs.
91
+ - `database_`: DB-API drivers and SQL toolkits.
92
+ - `datastore_`: other stores and caches.
93
+ - `task_`: task queues. `messaging_`: brokers and their clients.
94
+ - `server_`: WSGI and ASGI servers. `template_`: template engines.
95
+
96
+ A new category is added when a target fits none of these. The
97
+ directory name is internal; the entry point name, and so the name a
98
+ config uses, is always the bare target.
99
+
100
+ The subpackage's `__init__.py` holds one `wrapture.Instrumentation`
101
+ subclass and imports only wrapture; everything that touches the
102
+ target lives in `hooks.py`, reached by an import inside `apply()`.
103
+ The class is registered in `pyproject.toml` under
104
+ `[project.entry-points."wrapture.instrumentation"]`, and gets its own
105
+ test suite under `tests/<category>_<target>/`. The
106
+ [instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
107
+ page of the wrapture documentation is the full contract; TESTING.md
108
+ here covers the tests.
109
+
110
+ ## License
111
+
112
+ BSD-2-Clause, as wrapture.
@@ -0,0 +1,183 @@
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/. Test files are named `test_*.py` and
8
+ are discovered by pytest, which is 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, the listing tool reporting every entry cleanly,
16
+ and the test-side WSGI driver's own tests.
17
+ - One subdirectory per target, `tests/<category>_<target>/`
18
+ (`tests/framework_flask/`), holding that instrumentation's suite:
19
+ settings validation, applying and removing the class directly, the
20
+ whole path through `wrapture.instrumentation()` with a timeline
21
+ recording what the bindings observe, resolving the entry point by
22
+ name, and a check that the installed target satisfies the class's
23
+ `supports` range.
24
+
25
+ Shared helpers live in [tests/conftest.py](tests/conftest.py) and
26
+ [tests/wsgi.py](tests/wsgi.py).
27
+
28
+ ## The WSGI driver
29
+
30
+ WSGI applications are driven in process by `tests/wsgi.py`, which
31
+ plays the server's side of PEP 3333 exactly: it builds a complete
32
+ environ, supplies a `start_response` that honours the `exc_info`
33
+ re-invocation rule and returns a working `write` callable, iterates
34
+ the application's result, and always calls its `close()`, including
35
+ when iteration raises or is abandoned. It is used instead of a
36
+ framework's test client because a request event's closing line is
37
+ tied to the moment the response iterable is consumed and closed, and
38
+ the driver makes that moment explicit: `request(app, "GET", "/path")`
39
+ reads and closes for the common case, and `consume=False` hands back
40
+ the response with its body unconsumed so a test can check the
41
+ request is still open, then `read()` and `close()` it. The same
42
+ driver serves every WSGI target.
43
+
44
+ ## Running the tests
45
+
46
+ All tooling in this project goes through [uv](https://docs.astral.sh/uv/),
47
+ which manages the project environment and installs the package, its
48
+ development dependencies (including pytest) and the target packages
49
+ the tests need.
50
+
51
+ The simplest way to run the test suite is via the Justfile target:
52
+
53
+ ```console
54
+ just test
55
+ ```
56
+
57
+ Extra arguments are passed through to pytest, for example:
58
+
59
+ ```console
60
+ just test -v
61
+ just test tests/test_wsgi.py
62
+ just test -k version
63
+ ```
64
+
65
+ One target's suite alone:
66
+
67
+ ```console
68
+ just test-target framework_flask
69
+ ```
70
+
71
+ Equivalently, run pytest directly with uv:
72
+
73
+ ```console
74
+ uv run pytest
75
+ ```
76
+
77
+ ## Watching what the tests record
78
+
79
+ The suites assert on tapes rather than printing anything, but the
80
+ recorded events can be watched live for visual verification. Setting
81
+ WRAPTURE_PRINTER in the environment installs a process-wide
82
+ wrapture.Printer sink for the session, streaming one line to stderr
83
+ as each operation begins and a closing line with its outcome and
84
+ timing; pytest captures stderr, so add -s to see it:
85
+
86
+ ```console
87
+ WRAPTURE_PRINTER=1 just test tests/framework_flask -s
88
+ ```
89
+
90
+ The sink is consulted alongside the tests' own scoped tapes, so the
91
+ stream shows exactly what each tape hears without disturbing any
92
+ assertion.
93
+
94
+ For a purpose-built run rather than the tests' traffic, each target
95
+ also has a demo module under demo/ that applies its instrumentation,
96
+ drives the test application through the WSGI driver, and prints both
97
+ the live stream and the reconstructed tree with timings:
98
+
99
+ ```console
100
+ just demo-flask
101
+ ```
102
+
103
+ With --otel the same events also export as OpenTelemetry spans over
104
+ OTLP (to http://localhost:4318, or wherever
105
+ OTEL_EXPORTER_OTLP_ENDPOINT points), for verifying the spans in a
106
+ local backend such as Jaeger; the Justfile target overlays the
107
+ wrapture[otel] dependencies for the run:
108
+
109
+ ```console
110
+ just demo-flask --otel
111
+ ```
112
+
113
+ ## Testing across Python versions
114
+
115
+ The project supports multiple Python versions, including the free
116
+ threaded builds of 3.13, 3.14 and 3.15. The supported list is defined
117
+ at the top of the [Justfile](Justfile). The default version used by
118
+ plain `just test` is pinned in [.python-version](.python-version).
119
+
120
+ Run the test suite on every supported version:
121
+
122
+ ```console
123
+ just test-all
124
+ ```
125
+
126
+ Run the test suite on one nominated version:
127
+
128
+ ```console
129
+ just test-python 3.12
130
+ just test-python 3.14t
131
+ ```
132
+
133
+ Extra arguments are passed through to pytest for these targets too.
134
+ uv downloads any Python version it does not already have, and each
135
+ version gets its own environment (.venv-VERSION) so the default .venv
136
+ is left untouched.
137
+
138
+ ## Testing across target versions
139
+
140
+ The `test` dependency group installs each target at whatever version
141
+ the lock resolves. An instrumentation's `supports` range is kept
142
+ honest by running its suite against other versions of the target,
143
+ overlaid on the project environment for that run. Each target has a
144
+ recipe for it in the Justfile (`just test-flask 3.0.3`) and a list of
145
+ versions that `just test-flask-all` loops over; the CI workflow runs
146
+ the same matrix. A test in each target's suite asserts the installed
147
+ target satisfies `supports`, so a matrix entry outside the range
148
+ fails loudly rather than passing vacuously.
149
+
150
+ ## Testing against unreleased wrapture
151
+
152
+ The package depends on a released wrapture. To run the tests against
153
+ a checkout of wrapture in the sibling directory ../wrapture, without
154
+ editing pyproject.toml:
155
+
156
+ ```console
157
+ just test-dev
158
+ ```
159
+
160
+ which overlays that checkout as an editable install for the run.
161
+
162
+ ## Writing tests
163
+
164
+ - Put new test files in tests/ (package level) or
165
+ tests/<category>_<target>/ (for one instrumentation) and name them
166
+ `test_*.py`.
167
+ - Import the package under test as `wrapture_instrumentation`, and
168
+ the instrumentation classes from their subpackages. The project is
169
+ installed into the uv-managed environment, so no path manipulation
170
+ is needed.
171
+ - Validate behaviour with wrapture's own unit testing layer:
172
+ `wrapture.timeline()` to record what the instrumentation's bindings
173
+ observe and the tape's tree and queries to assert on it, bindings
174
+ with `when=` and behaviours where a test needs to see a target
175
+ internal being called or control what it does, and
176
+ `wrapture.instrumentation()` to scope an application of a class to
177
+ a block. Do not use `unittest.mock`. If something cannot be
178
+ expressed that way, write it plainly with a comment naming the gap,
179
+ and say so when summarising the work.
180
+ - Drive WSGI applications through `tests/wsgi.py`, not a framework's
181
+ test client.
182
+ - Tests should not depend on anything in the scratch/ directory,
183
+ which is not part of the repository.
@@ -0,0 +1,17 @@
1
+ """Demonstration scripts, one per instrumented target.
2
+
3
+ Each module drives a small application with its target's
4
+ instrumentation applied, streaming every recorded event live and
5
+ printing the reconstructed tree at the end, so the instrumentation
6
+ can be verified by eye rather than by assertion. Run from the
7
+ repository root as a module, so the tests package (whose application
8
+ and WSGI driver the demos reuse) imports cleanly:
9
+
10
+ uv run python -m demo.framework_flask
11
+
12
+ or through the Justfile target, which also carries the OpenTelemetry
13
+ dependencies for the --otel flag:
14
+
15
+ just demo-flask
16
+ just demo-flask --otel
17
+ """
@@ -0,0 +1,117 @@
1
+ """Drive the shop application with the Flask instrumentation applied.
2
+
3
+ The instrumentation is resolved by its entry point name, the way a
4
+ config file finds it, and the application is the tests' own shop,
5
+ built only after the instrumentation applies, the order the runner
6
+ guarantees in real use. A handful of requests then cover every shape
7
+ the instrumentation handles: plain views, a streaming response, a
8
+ class-based view, a blueprint, and a view that raises (Flask answers
9
+ 500 and the exception is noted against the request event).
10
+
11
+ Two views of the run always print: the live stream, one line as each
12
+ operation begins and a closing line with its outcome, and the tidy
13
+ tree reconstructed afterwards with timings. With --otel the same
14
+ events also export as OpenTelemetry spans to a local OTLP endpoint
15
+ (http://localhost:4318 unless OTEL_EXPORTER_OTLP_ENDPOINT says
16
+ otherwise), for verifying the spans in a backend such as Jaeger:
17
+
18
+ docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import os
25
+ import sys
26
+
27
+ import wrapture
28
+
29
+ # Every route the shop registers, the failing request last so the
30
+ # stream ends on the interesting case.
31
+
32
+ REQUESTS: tuple[tuple[str, str], ...] = (
33
+ ("GET", "/"),
34
+ ("GET", "/quote/widget"),
35
+ ("GET", "/export"),
36
+ ("GET", "/catalog"),
37
+ ("GET", "/reports/summary"),
38
+ ("GET", "/quote/missing"),
39
+ )
40
+
41
+
42
+ def add_otel_sink() -> None:
43
+ """Register the OpenTelemetry sink, exporting the run's events as
44
+ spans; exits with guidance when the optional dependencies are
45
+ missing."""
46
+
47
+ try:
48
+ import wrapture.otel
49
+ except ImportError as error:
50
+ raise SystemExit(
51
+ "the OpenTelemetry dependencies are not installed; run the"
52
+ " demo through `just demo-flask --otel`, which overlays"
53
+ " wrapture[otel] for the run"
54
+ ) from error
55
+
56
+ wrapture.add_sink(wrapture.otel.sink(service_name="wrapture-flask-demo"))
57
+
58
+
59
+ def main(arguments: list[str] | None = None) -> None:
60
+ """Run the demo: apply the instrumentation, drive the requests,
61
+ print the live stream and the tree, and flush any exporters."""
62
+
63
+ parser = argparse.ArgumentParser(
64
+ prog="demo.framework_flask",
65
+ description="Drive the shop application with the Flask"
66
+ " instrumentation applied, printing the live stream and the"
67
+ " tree.",
68
+ )
69
+ parser.add_argument(
70
+ "--otel",
71
+ action="store_true",
72
+ help="also export the events as OpenTelemetry spans over OTLP",
73
+ )
74
+ options = parser.parse_args(arguments)
75
+
76
+ # The exporting sink registers first, mirroring the config loader,
77
+ # which always puts the [otel] table's sink ahead of the [[sink]]
78
+ # list; the printer writes to stdout so the stream and the demo's
79
+ # own headings interleave in order.
80
+
81
+ if options.otel:
82
+ add_otel_sink()
83
+
84
+ wrapture.add_sink(wrapture.Printer(stream=sys.stdout))
85
+
86
+ print("== live stream ==")
87
+
88
+ with wrapture.instrumentation("flask"), wrapture.timeline() as tape:
89
+ from tests.framework_flask.shop import make_app
90
+ from tests.wsgi import request
91
+
92
+ app = make_app()
93
+
94
+ for method, path in REQUESTS:
95
+ request(app, method, path)
96
+
97
+ print()
98
+ print("== tree ==")
99
+ print(tape.tree(times=True))
100
+
101
+ # Deliver everything owed: batched spans push to the exporter here
102
+ # rather than at interpreter exit, so the closing hint below is
103
+ # true by the time it prints.
104
+
105
+ wrapture.shutdown()
106
+
107
+ if options.otel:
108
+ endpoint = os.environ.get(
109
+ "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"
110
+ )
111
+ print()
112
+ print("== otel ==")
113
+ print(f"spans flushed to {endpoint} as service wrapture-flask-demo")
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()