lens-debug 1.1.0__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.
lens_debug/__init__.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Lens - send debug payloads to the Lens desktop app.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from lens_debug import lens
|
|
5
|
+
|
|
6
|
+
lens("hello", user) # log any values
|
|
7
|
+
lens([1, 2, 3]).label("my array") # add a label
|
|
8
|
+
lens("careful").red() # colour the entry
|
|
9
|
+
lens.exception(err) # send a caught exception
|
|
10
|
+
lens.clear() # clear the Lens window
|
|
11
|
+
|
|
12
|
+
Debugging must never break your program, so every transmission runs on a
|
|
13
|
+
daemon thread and swallows all errors silently.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import atexit
|
|
18
|
+
import queue
|
|
19
|
+
import threading
|
|
20
|
+
import traceback
|
|
21
|
+
import uuid
|
|
22
|
+
import json
|
|
23
|
+
import time as _time
|
|
24
|
+
from urllib import request as _request
|
|
25
|
+
|
|
26
|
+
__version__ = "1.1.0"
|
|
27
|
+
|
|
28
|
+
_CONFIG = {
|
|
29
|
+
"host": os.environ.get("LENS_HOST", "127.0.0.1"),
|
|
30
|
+
"port": int(os.environ.get("LENS_PORT", "23600")),
|
|
31
|
+
"cloud_url": os.environ.get("LENS_CLOUD_URL"),
|
|
32
|
+
"key": os.environ.get("LENS_PROJECT_KEY"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve_key():
|
|
37
|
+
return _CONFIG.get("key") or os.environ.get("LENS_PROJECT_KEY")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _resolve_cloud_url():
|
|
41
|
+
u = _CONFIG.get("cloud_url") or os.environ.get("LENS_CLOUD_URL")
|
|
42
|
+
return u.rstrip("/") if u else None
|
|
43
|
+
|
|
44
|
+
_THIS_FILE = os.path.abspath(__file__)
|
|
45
|
+
_COLORS = ("red", "green", "blue", "orange", "purple", "gray")
|
|
46
|
+
|
|
47
|
+
# A single worker drains this queue in order, so chained calls (.label(),
|
|
48
|
+
# .color()) always reach the app in the sequence they were made.
|
|
49
|
+
_queue = queue.Queue()
|
|
50
|
+
_worker_started = False
|
|
51
|
+
_worker_lock = threading.Lock()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _worker():
|
|
55
|
+
while True:
|
|
56
|
+
url, body, headers = _queue.get()
|
|
57
|
+
try:
|
|
58
|
+
req = _request.Request(url, data=body, headers=headers, method="POST")
|
|
59
|
+
_request.urlopen(req, timeout=3) # noqa: S310
|
|
60
|
+
except Exception:
|
|
61
|
+
pass # never let debugging crash the host program
|
|
62
|
+
finally:
|
|
63
|
+
_queue.task_done()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _ensure_worker():
|
|
67
|
+
global _worker_started
|
|
68
|
+
if _worker_started:
|
|
69
|
+
return
|
|
70
|
+
with _worker_lock:
|
|
71
|
+
if not _worker_started:
|
|
72
|
+
threading.Thread(target=_worker, daemon=True).start()
|
|
73
|
+
_worker_started = True
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _drain(timeout=3.0):
|
|
77
|
+
"""Best-effort flush on interpreter exit so short scripts don't drop the tail."""
|
|
78
|
+
end = _time.time() + timeout
|
|
79
|
+
while not _queue.empty() and _time.time() < end:
|
|
80
|
+
_time.sleep(0.02)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
atexit.register(_drain)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _detect_framework():
|
|
87
|
+
"""Best-effort framework detection; falls back to "Plain Python"."""
|
|
88
|
+
import sys
|
|
89
|
+
mods = sys.modules
|
|
90
|
+
try:
|
|
91
|
+
if "django" in mods:
|
|
92
|
+
return "Django " + mods["django"].get_version()
|
|
93
|
+
if "flask" in mods:
|
|
94
|
+
return "Flask " + getattr(mods["flask"], "__version__", "")
|
|
95
|
+
if "fastapi" in mods:
|
|
96
|
+
return "FastAPI " + getattr(mods["fastapi"], "__version__", "")
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
return "Plain Python"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _system_context():
|
|
103
|
+
"""Runtime info that helps debugging: Python version, OS, hostname, framework."""
|
|
104
|
+
import platform
|
|
105
|
+
import socket
|
|
106
|
+
ctx = {
|
|
107
|
+
"runtime": "Python " + platform.python_version(),
|
|
108
|
+
"os": (platform.system() + " " + platform.release()).strip(),
|
|
109
|
+
}
|
|
110
|
+
try:
|
|
111
|
+
ctx["hostname"] = socket.gethostname()
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
framework = _detect_framework()
|
|
115
|
+
if framework:
|
|
116
|
+
ctx["framework"] = framework
|
|
117
|
+
return {k: v for k, v in ctx.items() if v}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _transmit(payload):
|
|
121
|
+
payload.setdefault("time", int(_time.time() * 1000))
|
|
122
|
+
payload["meta"] = {"client": "python", "version": __version__}
|
|
123
|
+
key = _resolve_key()
|
|
124
|
+
if key:
|
|
125
|
+
payload["key"] = key
|
|
126
|
+
payload_type = payload.get("type", "log")
|
|
127
|
+
if payload_type not in ("clear", "pause"):
|
|
128
|
+
payload["context"] = _system_context()
|
|
129
|
+
body = json.dumps(payload, default=_safe).encode("utf-8")
|
|
130
|
+
|
|
131
|
+
_ensure_worker()
|
|
132
|
+
|
|
133
|
+
# Local Lens desktop app.
|
|
134
|
+
local_url = "http://%s:%d" % (_CONFIG["host"], _CONFIG["port"])
|
|
135
|
+
_queue.put((local_url, body, {"Content-Type": "application/json"}))
|
|
136
|
+
|
|
137
|
+
# Lens Cloud, straight from the client, no desktop required.
|
|
138
|
+
cloud_url = _resolve_cloud_url()
|
|
139
|
+
if cloud_url and key and payload_type not in ("clear", "pause"):
|
|
140
|
+
_queue.put((
|
|
141
|
+
cloud_url + "/api/ingest",
|
|
142
|
+
body,
|
|
143
|
+
{"Content-Type": "application/json", "x-lens-key": key},
|
|
144
|
+
))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _safe(obj):
|
|
148
|
+
"""Fallback serializer for values json cannot handle natively."""
|
|
149
|
+
try:
|
|
150
|
+
return vars(obj)
|
|
151
|
+
except Exception:
|
|
152
|
+
return repr(obj)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _resolve_origin():
|
|
156
|
+
"""Find the caller's file and line, skipping frames inside this package."""
|
|
157
|
+
for frame in reversed(traceback.extract_stack()):
|
|
158
|
+
if os.path.abspath(frame.filename) != _THIS_FILE:
|
|
159
|
+
return {"file": frame.filename, "line": frame.lineno}
|
|
160
|
+
return {"file": None, "line": None}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Lens:
|
|
164
|
+
def __init__(self, values):
|
|
165
|
+
self.id = str(uuid.uuid4())
|
|
166
|
+
self.values = list(values)
|
|
167
|
+
self._label = None
|
|
168
|
+
self._color = None
|
|
169
|
+
self.origin = _resolve_origin()
|
|
170
|
+
self._send()
|
|
171
|
+
|
|
172
|
+
def label(self, label):
|
|
173
|
+
self._label = label
|
|
174
|
+
return self._send()
|
|
175
|
+
|
|
176
|
+
def color(self, color):
|
|
177
|
+
self._color = color
|
|
178
|
+
return self._send()
|
|
179
|
+
|
|
180
|
+
def _send(self):
|
|
181
|
+
_transmit({
|
|
182
|
+
"id": self.id,
|
|
183
|
+
"type": "log",
|
|
184
|
+
"label": self._label,
|
|
185
|
+
"color": self._color,
|
|
186
|
+
"origin": self.origin,
|
|
187
|
+
"values": self.values,
|
|
188
|
+
})
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _make_color(name):
|
|
193
|
+
def _setter(self):
|
|
194
|
+
return self.color(name)
|
|
195
|
+
return _setter
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
for _c in _COLORS:
|
|
199
|
+
setattr(Lens, _c, _make_color(_c))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def lens(*values):
|
|
203
|
+
"""Send any values to Lens and return a chainable handle."""
|
|
204
|
+
return Lens(values)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _clear():
|
|
208
|
+
_transmit({"id": str(uuid.uuid4()), "type": "clear"})
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _exception(exc):
|
|
212
|
+
"""Send a caught exception so it shows up (and can be AI-summarized) in Lens."""
|
|
213
|
+
tb = traceback.extract_tb(exc.__traceback__)
|
|
214
|
+
last = tb[-1] if tb else None
|
|
215
|
+
_transmit({
|
|
216
|
+
"id": str(uuid.uuid4()),
|
|
217
|
+
"type": "exception",
|
|
218
|
+
"exception": {
|
|
219
|
+
"class": type(exc).__name__,
|
|
220
|
+
"message": str(exc),
|
|
221
|
+
"file": last.filename if last else None,
|
|
222
|
+
"line": last.lineno if last else None,
|
|
223
|
+
"frames": [
|
|
224
|
+
{"function": f.name, "file": f.filename, "line": f.lineno} for f in tb
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _configure(host=None, port=None, cloud_url=None, key=None):
|
|
231
|
+
if host is not None:
|
|
232
|
+
_CONFIG["host"] = host
|
|
233
|
+
if port is not None:
|
|
234
|
+
_CONFIG["port"] = int(port)
|
|
235
|
+
if cloud_url is not None:
|
|
236
|
+
_CONFIG["cloud_url"] = cloud_url.rstrip("/") if cloud_url else None
|
|
237
|
+
if key is not None:
|
|
238
|
+
_CONFIG["key"] = key or None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
lens.clear = _clear
|
|
242
|
+
lens.exception = _exception
|
|
243
|
+
lens.configure = _configure
|
|
244
|
+
|
|
245
|
+
__all__ = ["lens", "Lens"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lens-debug
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Lens - send debug payloads to the Lens desktop app and Lens Cloud
|
|
5
|
+
Author-email: LensApp <hello@lensapp.eu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://lens.lensapp.eu
|
|
8
|
+
Project-URL: Source, https://github.com/lensapp-eu/lens-python
|
|
9
|
+
Keywords: debug,debugging,dump,developer-tools,lens
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Lens for Python
|
|
21
|
+
|
|
22
|
+
Send debug payloads to the [Lens](https://lens.lensapp.eu) desktop app and [Lens Cloud](https://app.lensapp.eu) from any Python project (Django, Flask, FastAPI or plain scripts). No dependencies, just the standard library.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install lens-debug
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Use
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from lens_debug import lens
|
|
34
|
+
|
|
35
|
+
lens("hello", user) # send any values
|
|
36
|
+
lens([1, 2, 3]).label("my array") # add a label
|
|
37
|
+
lens("careful").red() # colour the entry
|
|
38
|
+
lens.clear() # clear the Lens window
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Colours: `red`, `green`, `blue`, `orange`, `purple`, `gray`.
|
|
42
|
+
|
|
43
|
+
## Lens Cloud (optional)
|
|
44
|
+
|
|
45
|
+
To send events straight to [Lens Cloud](https://app.lensapp.eu), no desktop app required, set both
|
|
46
|
+
in your environment:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
LENS_PROJECT_KEY=your-project-key-from-lens-cloud
|
|
50
|
+
LENS_CLOUD_URL=https://app.lensapp.eu
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The project key links events to the right project; the cloud URL is where they are sent. With both
|
|
54
|
+
set, every event goes to Lens Cloud (and to the desktop app too, if it is running). You can also
|
|
55
|
+
configure it in code:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
lens.configure(cloud_url="https://app.lensapp.eu", key="your-project-key")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each event also carries context (Python version, OS, hostname and detected framework) which shows
|
|
62
|
+
up as tags in Lens Cloud.
|
|
63
|
+
|
|
64
|
+
### Caught exceptions
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
try:
|
|
68
|
+
risky()
|
|
69
|
+
except Exception as err:
|
|
70
|
+
lens.exception(err)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The exception (with its stack trace) shows up in Lens and can be picked up by the built-in "Summarize errors" AI button.
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
Lens listens on `127.0.0.1:23600` by default. Override it in code or via environment variables:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
lens.configure(host="127.0.0.1", port=23600)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
export LENS_HOST=127.0.0.1
|
|
85
|
+
export LENS_PORT=23600
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Safety
|
|
89
|
+
|
|
90
|
+
Debugging never blocks or crashes your program: every payload is sent on a daemon thread and all transmission errors are swallowed silently. If the Lens app is not running, calls are simply no-ops.
|
|
91
|
+
|
|
92
|
+
MIT licensed.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
lens_debug/__init__.py,sha256=KV40ZgWsCiborKggF7PcQO7WbvRr2n1cz9E2Fw6RKO0,6876
|
|
2
|
+
lens_debug-1.1.0.dist-info/licenses/LICENSE,sha256=3RFisYDOTKjADGzdbZPZ8s7o_Agtctu8jK4JvaB1t1Q,1081
|
|
3
|
+
lens_debug-1.1.0.dist-info/METADATA,sha256=POBhs_QvcfznmRinFzYnf0e-4P1P0cCCQByvvLxqyvw,2701
|
|
4
|
+
lens_debug-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
lens_debug-1.1.0.dist-info/top_level.txt,sha256=kwaLChw_PfFQbkYLLww2yCiNRxBULilc66aygiZivos,11
|
|
6
|
+
lens_debug-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kevin Terpstra (LensApp)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lens_debug
|