lens-debug 1.1.0__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.
@@ -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,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,73 @@
1
+ # Lens for Python
2
+
3
+ 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.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install lens-debug
9
+ ```
10
+
11
+ ## Use
12
+
13
+ ```python
14
+ from lens_debug import lens
15
+
16
+ lens("hello", user) # send any values
17
+ lens([1, 2, 3]).label("my array") # add a label
18
+ lens("careful").red() # colour the entry
19
+ lens.clear() # clear the Lens window
20
+ ```
21
+
22
+ Colours: `red`, `green`, `blue`, `orange`, `purple`, `gray`.
23
+
24
+ ## Lens Cloud (optional)
25
+
26
+ To send events straight to [Lens Cloud](https://app.lensapp.eu), no desktop app required, set both
27
+ in your environment:
28
+
29
+ ```bash
30
+ LENS_PROJECT_KEY=your-project-key-from-lens-cloud
31
+ LENS_CLOUD_URL=https://app.lensapp.eu
32
+ ```
33
+
34
+ The project key links events to the right project; the cloud URL is where they are sent. With both
35
+ set, every event goes to Lens Cloud (and to the desktop app too, if it is running). You can also
36
+ configure it in code:
37
+
38
+ ```python
39
+ lens.configure(cloud_url="https://app.lensapp.eu", key="your-project-key")
40
+ ```
41
+
42
+ Each event also carries context (Python version, OS, hostname and detected framework) which shows
43
+ up as tags in Lens Cloud.
44
+
45
+ ### Caught exceptions
46
+
47
+ ```python
48
+ try:
49
+ risky()
50
+ except Exception as err:
51
+ lens.exception(err)
52
+ ```
53
+
54
+ The exception (with its stack trace) shows up in Lens and can be picked up by the built-in "Summarize errors" AI button.
55
+
56
+ ## Configuration
57
+
58
+ Lens listens on `127.0.0.1:23600` by default. Override it in code or via environment variables:
59
+
60
+ ```python
61
+ lens.configure(host="127.0.0.1", port=23600)
62
+ ```
63
+
64
+ ```bash
65
+ export LENS_HOST=127.0.0.1
66
+ export LENS_PORT=23600
67
+ ```
68
+
69
+ ## Safety
70
+
71
+ 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.
72
+
73
+ MIT licensed.
@@ -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,8 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ lens_debug/__init__.py
5
+ lens_debug.egg-info/PKG-INFO
6
+ lens_debug.egg-info/SOURCES.txt
7
+ lens_debug.egg-info/dependency_links.txt
8
+ lens_debug.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ lens_debug
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lens-debug"
7
+ version = "1.1.0"
8
+ description = "Lens - send debug payloads to the Lens desktop app and Lens Cloud"
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "LensApp", email = "hello@lensapp.eu" }]
13
+ keywords = ["debug", "debugging", "dump", "developer-tools", "lens"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Debuggers",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://lens.lensapp.eu"
25
+ Source = "https://github.com/lensapp-eu/lens-python"
26
+
27
+ [tool.setuptools]
28
+ packages = ["lens_debug"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+