wrapture-instrumentation 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,26 @@
1
+ """
2
+ Instrumentation for common Python packages, applied through wrapture.
3
+
4
+ Each target package has its own subpackage here, named for its
5
+ category and the target (framework_flask, external_requests), holding
6
+ one wrapture.Instrumentation subclass registered in the
7
+ wrapture.instrumentation entry point group under the bare target name.
8
+ This module carries only the version: importing it loads no
9
+ instrumentation and no target.
10
+ """
11
+
12
+
13
+ def _format_version(parts: tuple[str, ...]) -> str:
14
+ base = ".".join(parts[:3])
15
+
16
+ if len(parts) == 3:
17
+ return base
18
+
19
+ suffix = parts[3]
20
+ return (
21
+ f"{base}.{suffix}" if suffix.startswith(("dev", "post")) else f"{base}{suffix}"
22
+ )
23
+
24
+
25
+ __version_info__ = ("1", "0", "0", "dev1")
26
+ __version__ = _format_version(__version_info__)
@@ -0,0 +1,36 @@
1
+ """Instrumentation for Flask: request and view tracing for every
2
+ application the process creates.
3
+
4
+ This module imports only wrapture. Everything that touches Flask lives
5
+ in the sibling hooks module, which itself imports only wrapture at top
6
+ level, so loading this class when a config loads never imports Flask
7
+ ahead of the hook meant to fire on its import.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ import wrapture
15
+
16
+ from . import hooks
17
+
18
+
19
+ class FlaskInstrumentation(wrapture.Instrumentation):
20
+ """Request and view tracing for Flask applications."""
21
+
22
+ # The description defaults to the distribution's summary, which
23
+ # describes the whole collection; each class in a multi-target
24
+ # package says what it alone does.
25
+
26
+ description = "Request and view tracing for Flask applications."
27
+
28
+ target = "flask"
29
+ supports = ">=3.0,<4"
30
+ removable = True
31
+
32
+ @wrapture.instrumentation_hook("flask.app")
33
+ def flask_app(self, name: str, module: Any) -> None:
34
+ """Patch the Flask class's choke points once flask.app exists."""
35
+
36
+ hooks.instrument(module, self)
@@ -0,0 +1,115 @@
1
+ """The Flask patches, at Flask's own choke points.
2
+
3
+ Three bindings, all created with when=False so they are behaviour-only:
4
+ the plumbing is not the trace, so they act without ever recording
5
+ their own calls.
6
+
7
+ - Flask.__init__ installs the recording WSGI middleware on each new
8
+ instance's wsgi_app attribute, the documented place Flask middleware
9
+ goes, so every request records as one "request" event however the
10
+ instance was made: module level, an application factory, several
11
+ applications in one process.
12
+ - Flask.add_url_rule substitutes wrapture.observed(view_func) as
13
+ routes register, so every view handler records as a "call" event
14
+ beneath its request, wherever the view came from: module functions,
15
+ closures, blueprints from other modules, MethodView classes (which
16
+ register a generated view function). Flask captures view functions
17
+ into its dispatch table the moment a route registers, before any
18
+ observation could exist, which is why registration itself is the
19
+ point to intercept. observed() is idempotent, so a view registered
20
+ twice is not wrapped twice.
21
+ - Flask.handle_exception is the one place a view's exception can be
22
+ seen after Flask catches it: wsgi_app catches around the dispatch
23
+ and hands the exception here, which returns the 500 response, so
24
+ the request itself completes normally. The binding notes the
25
+ exception against the enclosing request event with note_exception(),
26
+ so the request shows the failure beside its status.
27
+
28
+ The three are applied as one group whose remove() is registered as a
29
+ cleanup callback with on_cleanup(), which is what lets remove(), and
30
+ with it AppliedConfig.revert() and the instrumentation() context
31
+ manager, take the whole thing down again.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import Any
37
+
38
+ import wrapture
39
+
40
+
41
+ def wrap_app(
42
+ wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
43
+ ) -> Any:
44
+ """Run Flask.__init__, then install the recording middleware on the
45
+ new instance's wsgi_app."""
46
+
47
+ outcome = wrapped(*args, **kwargs)
48
+
49
+ # Label with the app's own import name, so requests read as
50
+ # "myapp.wsgi_app" and two apps in one process stay distinct.
51
+
52
+ instance.wsgi_app = wrapture.WSGIMiddleware(
53
+ instance.wsgi_app, label=f"{instance.name}.wsgi_app"
54
+ )
55
+
56
+ return outcome
57
+
58
+
59
+ def wrap_view(
60
+ args: tuple[Any, ...], kwargs: dict[str, Any]
61
+ ) -> tuple[tuple[Any, ...], dict[str, Any]]:
62
+ """Substitute an observed view function into an add_url_rule call.
63
+
64
+ The signature is add_url_rule(rule, endpoint=None, view_func=None,
65
+ ...), so the view is either the view_func keyword or the third
66
+ positional argument; a registration without a view (an endpoint
67
+ name alone) passes through untouched.
68
+ """
69
+
70
+ if kwargs.get("view_func") is not None:
71
+ kwargs = dict(kwargs, view_func=wrapture.observed(kwargs["view_func"]))
72
+ elif len(args) >= 3 and args[2] is not None:
73
+ args = (*args[:2], wrapture.observed(args[2]), *args[3:])
74
+
75
+ return args, kwargs
76
+
77
+
78
+ def note_failure(
79
+ wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
80
+ ) -> Any:
81
+ """Note the exception handle_exception received against the
82
+ enclosing request event, then let the handler run."""
83
+
84
+ # The exception is the handler's one positional argument. Aim the
85
+ # note at the request the middleware recorded, not at this call,
86
+ # which is not recorded anyway; outside a request (a handler
87
+ # invoked with nothing recording) this is a no-op.
88
+
89
+ exception = args[0] if args else kwargs.get("e")
90
+ if exception is not None:
91
+ wrapture.note_exception(exception, event=wrapture.current_event(kind="request"))
92
+
93
+ return wrapped(*args, **kwargs)
94
+
95
+
96
+ def instrument(module: Any, instrumentation: wrapture.Instrumentation) -> None:
97
+ """Bind the three choke points on the Flask class found in the
98
+ flask.app module, apply them as one group, and register the
99
+ group's removal as the instrumentation's cleanup."""
100
+
101
+ constructor = wrapture.binding(module.Flask, "__init__", when=False)
102
+ constructor.on_call.decorates(wrap_app)
103
+
104
+ registrar = wrapture.binding(module.Flask, "add_url_rule", when=False)
105
+ registrar.on_call.transforms_args(wrap_view)
106
+
107
+ handler = wrapture.binding(module.Flask, "handle_exception", when=False)
108
+ handler.on_call.decorates(note_failure)
109
+
110
+ group = wrapture.bindings(
111
+ constructor=constructor, registrar=registrar, handler=handler
112
+ )
113
+ group.apply()
114
+
115
+ instrumentation.on_cleanup(group.remove)
File without changes
@@ -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,10 @@
1
+ wrapture_instrumentation/__init__.py,sha256=nwGba4lOYVvBYcR7FYxR5cqYiVUmCGW2_ao66slVMYg,783
2
+ wrapture_instrumentation/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ wrapture_instrumentation/framework_flask/__init__.py,sha256=DtfT6EALymqOhKboyn5XToJQKch_C781kMSDz0IbZqs,1108
4
+ wrapture_instrumentation/framework_flask/hooks.py,sha256=EtS_Rw_IIiQp_sHICupAC08NgGTqzEo0k8uZn7Sa2pw,4534
5
+ wrapture_instrumentation-1.0.0.dev1.dist-info/licenses/LICENSE,sha256=RPMyRnXwgrw9co5o6wY9G3DKlb-oBZ44tpiRnuYeec8,1299
6
+ wrapture_instrumentation-1.0.0.dev1.dist-info/METADATA,sha256=xqT_KxyHckRsVLxPS2SRaoozBoPCnXVMunRV1L3L5ns,5353
7
+ wrapture_instrumentation-1.0.0.dev1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ wrapture_instrumentation-1.0.0.dev1.dist-info/entry_points.txt,sha256=UZXOmg--ham7LOqtX-KHrGPo2TZxz7ttUI6UTbcFCMo,97
9
+ wrapture_instrumentation-1.0.0.dev1.dist-info/top_level.txt,sha256=IIkfVp5ZVFBXZhNnU-lt7OnulEuRBy6EGW9Ci2q3B-o,25
10
+ wrapture_instrumentation-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,2 @@
1
+ [wrapture.instrumentation]
2
+ flask = wrapture_instrumentation.framework_flask:FlaskInstrumentation
@@ -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