shadow-debugger 0.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.
@@ -0,0 +1,64 @@
1
+ """Deduplicates identical errors so the LLM is not called repeatedly for the same bug."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import threading
7
+ import time
8
+ from collections import OrderedDict
9
+ from typing import Any, Callable, Dict, Tuple
10
+
11
+
12
+ class ErrorThrottler:
13
+ """Thread-safe, memory-bounded (LRU) cooldown tracker keyed by error fingerprint."""
14
+
15
+ def __init__(
16
+ self,
17
+ cooldown_seconds: float = 300,
18
+ max_entries: int = 1000,
19
+ clock: Callable[[], float] = time.monotonic,
20
+ ) -> None:
21
+ self.cooldown_seconds = cooldown_seconds
22
+ self.max_entries = max_entries
23
+ self._clock = clock
24
+ self._lock = threading.Lock()
25
+ # fingerprint -> [last_analyzed_timestamp, occurrence_count]
26
+ self._seen: "OrderedDict[str, list]" = OrderedDict()
27
+
28
+ @staticmethod
29
+ def compute_fingerprint(error_context: Dict[str, Any]) -> str:
30
+ if error_context.get("fingerprint"):
31
+ return str(error_context["fingerprint"])
32
+ exc_type = str(error_context.get("exception_type", ""))
33
+ filepath = str(error_context.get("file", ""))
34
+ lineno = str(error_context.get("line", ""))
35
+ raw_key = f"{exc_type}:{filepath}:{lineno}"
36
+ return hashlib.sha256(raw_key.encode("utf-8")).hexdigest()[:16]
37
+
38
+ def hit(self, fingerprint: str) -> Tuple[bool, int]:
39
+ """Record an occurrence. Returns ``(should_analyze, occurrence_count)``."""
40
+ now = self._clock()
41
+ with self._lock:
42
+ record = self._seen.get(fingerprint)
43
+ if record is None:
44
+ self._seen[fingerprint] = [now, 1]
45
+ if len(self._seen) > self.max_entries:
46
+ self._seen.popitem(last=False)
47
+ return True, 1
48
+
49
+ self._seen.move_to_end(fingerprint)
50
+ record[1] += 1
51
+ if now - record[0] >= self.cooldown_seconds:
52
+ record[0] = now
53
+ return True, record[1]
54
+ return False, record[1]
55
+
56
+ def should_analyze(self, error_context: Dict[str, Any]) -> Tuple[bool, str, int]:
57
+ """Check and update cooldown state for ``error_context``."""
58
+ fingerprint = self.compute_fingerprint(error_context)
59
+ allowed, count = self.hit(fingerprint)
60
+ return allowed, fingerprint, count
61
+
62
+ def __len__(self) -> int:
63
+ with self._lock:
64
+ return len(self._seen)
@@ -0,0 +1,292 @@
1
+ Metadata-Version: 2.4
2
+ Name: shadow-debugger
3
+ Version: 0.1.0
4
+ Summary: Non-blocking AI-powered background error diagnosis and root-cause analysis for Python applications and web APIs.
5
+ Author: GaspareDev
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/GaspareDev/shadow-debugger
8
+ Project-URL: Repository, https://github.com/GaspareDev/shadow-debugger
9
+ Project-URL: Issues, https://github.com/GaspareDev/shadow-debugger/issues
10
+ Project-URL: Changelog, https://github.com/GaspareDev/shadow-debugger/blob/main/CHANGELOG.md
11
+ Keywords: debugging,error-tracking,observability,llm,ai,gemini,ollama,openai,anthropic,fastapi,flask,django
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Bug Tracking
23
+ Classifier: Topic :: Software Development :: Debuggers
24
+ Classifier: Topic :: System :: Monitoring
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Provides-Extra: gemini
30
+ Requires-Dist: google-genai>=2.22.0; extra == "gemini"
31
+ Provides-Extra: openai
32
+ Requires-Dist: openai>=1.50.0; extra == "openai"
33
+ Provides-Extra: anthropic
34
+ Requires-Dist: anthropic>=0.40.0; extra == "anthropic"
35
+ Provides-Extra: fastapi
36
+ Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
37
+ Provides-Extra: flask
38
+ Requires-Dist: flask>=2.2.0; extra == "flask"
39
+ Requires-Dist: blinker>=1.6.0; extra == "flask"
40
+ Provides-Extra: django
41
+ Requires-Dist: django>=4.2; extra == "django"
42
+ Provides-Extra: all
43
+ Requires-Dist: google-genai>=2.22.0; extra == "all"
44
+ Requires-Dist: openai>=1.50.0; extra == "all"
45
+ Requires-Dist: anthropic>=0.40.0; extra == "all"
46
+ Requires-Dist: fastapi>=0.100.0; extra == "all"
47
+ Requires-Dist: flask>=2.2.0; extra == "all"
48
+ Requires-Dist: blinker>=1.6.0; extra == "all"
49
+ Provides-Extra: dev
50
+ Requires-Dist: pytest>=8.0; extra == "dev"
51
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
52
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
53
+ Requires-Dist: mypy>=1.11.0; extra == "dev"
54
+ Dynamic: license-file
55
+
56
+ # shadow-debugger
57
+
58
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
59
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
60
+
61
+ Background error diagnosis for Python apps and web APIs.
62
+
63
+ When an exception or HTTP 500 happens, `shadow-debugger` captures the stack frame from your code, strips secrets from local variables and request data, and sends the context to an LLM in a background worker thread. Your app returns its normal response right away; a few seconds later you get a Markdown postmortem in `./shadow_reports/`.
64
+
65
+ It works offline with **Ollama** (zero external dependencies) or with **Gemini**, **OpenAI** (including OpenAI-compatible endpoints like Groq, vLLM, LM Studio), and **Anthropic**.
66
+
67
+ ---
68
+
69
+ ## What a report looks like
70
+
71
+ Given a crashing endpoint:
72
+
73
+ ```python
74
+ @app.get("/checkout/{order_id}")
75
+ async def checkout(order_id: int, discount_items: int = 0):
76
+ unit_discount = 50 / discount_items
77
+ return {"order_id": order_id, "discount": unit_discount}
78
+ ```
79
+
80
+ `shadow-debugger` writes `shadow_reports/incident_20261018_103012_ZeroDivisionError_9f2a1c4b.md`:
81
+
82
+ ````markdown
83
+ # Shadow Debugger Incident Report
84
+
85
+ **Timestamp:** 2026-10-18T10:30:12.418290+00:00
86
+ **Exception:** `ZeroDivisionError`: division by zero
87
+ **Location:** `app/routes.py:42` in `checkout`
88
+ **Fingerprint:** `9f2a1c4b8e1032d1` (occurrences so far: 1)
89
+
90
+ ---
91
+
92
+ ### 1. Root Cause Summary
93
+ `checkout()` divides `50` by `discount_items` without checking for `0`. When the query parameter is omitted or set to `0`, Python raises `ZeroDivisionError`.
94
+
95
+ ### 2. Trigger Condition
96
+ `GET /checkout/1042?discount_items=0` with local state `{"order_id": 1042, "discount_items": 0}`.
97
+
98
+ ### 3. Proposed Fix
99
+ ```python
100
+ unit_discount = 50 / discount_items if discount_items > 0 else 0.0
101
+ ```
102
+ ```diff
103
+ - unit_discount = 50 / discount_items
104
+ + unit_discount = 50 / discount_items if discount_items > 0 else 0.0
105
+ ```
106
+
107
+ ### 4. Prevention Advice
108
+ Validate numeric query parameters with `Query(gt=0)` in FastAPI or guard denominators before dividing.
109
+ ````
110
+
111
+ ---
112
+
113
+ ## Installation
114
+
115
+ ```bash
116
+ # Core package (stdlib only — works with local Ollama out of the box)
117
+ pip install shadow-debugger
118
+
119
+ # Or install with the provider / framework you use:
120
+ pip install "shadow-debugger[gemini]"
121
+ pip install "shadow-debugger[openai]"
122
+ pip install "shadow-debugger[anthropic]"
123
+ pip install "shadow-debugger[fastapi]"
124
+ pip install "shadow-debugger[flask]"
125
+ pip install "shadow-debugger[django]"
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Quick start
131
+
132
+ ### Local with Ollama (no API key)
133
+
134
+ ```python
135
+ import shadow_debugger
136
+
137
+ shadow_debugger.init(
138
+ provider="ollama",
139
+ model="llama3.2",
140
+ print_to_console=True,
141
+ )
142
+ ```
143
+
144
+ ### Cloud providers
145
+
146
+ ```python
147
+ import shadow_debugger
148
+
149
+ # Uses GEMINI_API_KEY
150
+ shadow_debugger.init(provider="gemini", model="gemini-3.8-flash")
151
+
152
+ # Uses OPENAI_API_KEY (or pass base_url=... for Groq / vLLM / LM Studio)
153
+ shadow_debugger.init(provider="openai", model="gpt-5.4-nano")
154
+
155
+ # Uses ANTHROPIC_API_KEY
156
+ shadow_debugger.init(provider="anthropic", model="claude-haiku-5-5")
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Web frameworks
162
+
163
+ ### FastAPI / Starlette
164
+
165
+ ```python
166
+ from fastapi import FastAPI
167
+ import shadow_debugger
168
+
169
+ shadow_debugger.init(provider="ollama", model="llama3.2")
170
+
171
+ app = FastAPI()
172
+ app.add_middleware(shadow_debugger.FastAPIWatcherMiddleware)
173
+ ```
174
+
175
+ ### Flask
176
+
177
+ ```python
178
+ from flask import Flask
179
+ import shadow_debugger
180
+
181
+ app = Flask(__name__)
182
+ shadow_debugger.init(provider="ollama", model="llama3.2")
183
+ shadow_debugger.init_flask(app)
184
+ ```
185
+
186
+ `init_flask` hooks into `got_request_exception`, so normal 404/4xx responses are left untouched.
187
+
188
+ ### Django
189
+
190
+ In `settings.py`:
191
+
192
+ ```python
193
+ MIDDLEWARE = [
194
+ "shadow_debugger.integrations.DjangoWatcherMiddleware",
195
+ # ...
196
+ ]
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Decorator & manual capture
202
+
203
+ `@watch` works on both regular and `async def` functions:
204
+
205
+ ```python
206
+ import shadow_debugger
207
+
208
+ @shadow_debugger.watch(reraise=False)
209
+ async def process_order(payload: dict):
210
+ return payload["items"]["price"] / payload["items"]["quantity"]
211
+
212
+ # Or inside an existing try/except block:
213
+ try:
214
+ sync_inventory()
215
+ except Exception as exc:
216
+ shadow_debugger.capture_exception(exc, extra_metadata={"job": "inventory"})
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Privacy & what gets sent
222
+
223
+ If you use a cloud provider, the prompt includes the traceback, the 5 lines of source around the error, and the local variables of the failing function. To keep sensitive data out of prompts:
224
+
225
+ - **Variable & header names** matching common secret names (`password`, `secret`, `token`, `api_key`, `authorization`, `cookie`, etc.) are replaced with `[REDACTED]`.
226
+ - **String values** matching JWTs, API keys (`sk-...`, `AIza...`, `AKIA...`, `ghp_...`), `Bearer` tokens, PEM private keys, and URL query tokens (`?access_token=...`) are scrubbed across variables, tracebacks, and exception messages.
227
+ - **Lazy objects** (like Django `QuerySet`s) are inspected via `__dict__` without calling `repr()`/`str()`, so capturing an error won't trigger extra database queries.
228
+ - You can turn off parts of the payload with `send_locals=False` or `send_source_code=False`, add your own regexes via `redact_keys=[...]` and `redact_patterns=[...]`, or run everything locally with `provider="ollama"`.
229
+ - Proposed code patches are only checked with `ast.parse()` — generated code is never executed.
230
+
231
+ ---
232
+
233
+ ## Configuration
234
+
235
+ All options can be passed to `shadow_debugger.init(...)`:
236
+
237
+ | Option | Default | Description |
238
+ | :--- | :--- | :--- |
239
+ | `provider` | `"gemini"` | `"gemini"`, `"ollama"`, `"openai"`, `"anthropic"`, or a `Provider` instance. |
240
+ | `model` | `None` | Model name. Uses the provider default if omitted. |
241
+ | `api_key` | `None` | API key (falls back to `GEMINI_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`). |
242
+ | `base_url` | `None` | Custom URL for Ollama (`OLLAMA_HOST`) or OpenAI-compatible servers. |
243
+ | `output_dir` | `"./shadow_reports"` | Folder for Markdown reports (`None` disables file output). |
244
+ | `print_to_console` | `False` | Print diagnoses to `stderr` (handy in local dev). |
245
+ | `cooldown_seconds` | `300.0` | Minimum seconds before re-analyzing the same error at the same line. |
246
+ | `send_source_code` | `True` | Include source lines around the crash. |
247
+ | `send_locals` | `True` | Include redacted local variables. |
248
+ | `redact_keys` | `()` | Extra regexes for variable/key names to redact. |
249
+ | `redact_patterns` | `()` | Extra regexes for secret value patterns to scrub. |
250
+ | `request_timeout` | `60.0` | Timeout in seconds for LLM calls. |
251
+ | `shutdown_timeout` | `10.0` | Seconds to wait on process exit for pending reports to finish. |
252
+ | `max_queue_size` | `100` | Max queued incidents before dropping new ones. |
253
+ | `catch_uncaught` | `True` | Hook `sys.excepthook` and `threading.excepthook`. |
254
+
255
+ ---
256
+
257
+ ## Writing a custom provider
258
+
259
+ ```python
260
+ import shadow_debugger
261
+ from shadow_debugger import Provider, register_provider
262
+
263
+ @register_provider("my-gateway")
264
+ class MyGatewayProvider(Provider):
265
+ default_model = "my-model"
266
+ env_key = "MY_GATEWAY_KEY"
267
+
268
+ def complete(self, system: str, prompt: str) -> str:
269
+ self.check_configuration()
270
+ # Call your LLM endpoint and return Markdown
271
+ return "..."
272
+
273
+ shadow_debugger.init(provider="my-gateway")
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Roadmap & contributing
279
+
280
+ I'm actively looking for feedback and contributors. A few areas where PRs are very welcome:
281
+
282
+ - [ ] Webhook / notification sinks (Slack, Discord, Telegram, GitHub Issues)
283
+ - [ ] Additional providers (Mistral, LiteLLM, AWS Bedrock)
284
+ - [ ] Structured JSON output option alongside Markdown reports
285
+ - [ ] Celery / background worker task integration
286
+ - [ ] CLI command to browse and filter local reports in `./shadow_reports`
287
+
288
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup instructions.
289
+
290
+ ## License
291
+
292
+ [MIT](LICENSE)
@@ -0,0 +1,23 @@
1
+ shadow_debugger/__init__.py,sha256=VUITE75G6KBvXZ3RbIhnEMVnVNFF3Xj8582rwf0cDZ4,9366
2
+ shadow_debugger/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
3
+ shadow_debugger/analyzer.py,sha256=0g9IBoTUORJ2yb-Gr9BmAR9MJYAD2wg7RZXACC39Kg4,4049
4
+ shadow_debugger/config.py,sha256=Osh37V4SPZ5u88c9T1PeZ-4dx04fL4YEiPOMBEubgCg,1847
5
+ shadow_debugger/core.py,sha256=99J79AsAVsEoQRshBhzWiNvvchC8OyVQJH99jWM2TlE,9439
6
+ shadow_debugger/inspector.py,sha256=8ixEwvG2FtQiMMv6x-Ny6Du9AWvLFenGLHv0CyBiBIY,6294
7
+ shadow_debugger/patch_check.py,sha256=sy6mtJk6pNWrtdDkR64SrrGvvAP_o04uOgADwF3pIsM,1837
8
+ shadow_debugger/py.typed,sha256=ZSegze1ahS5tlaVlEVAF0HG-jTkkScOwTjUnglYQOS8,22
9
+ shadow_debugger/redaction.py,sha256=CwQQVO65EJl2IcwiGbb1M2eW5QHojxoFZ0kv558SRsI,6555
10
+ shadow_debugger/reporter.py,sha256=TawWb-Vh7jV_6kKPpthqD0vRPhD_44pT0Yax6bU1rAU,5485
11
+ shadow_debugger/throttler.py,sha256=EOPC_Z1ikbFGRBf0sjSc7TojuKgtqs3IL1h-JGyTrYc,2403
12
+ shadow_debugger/integrations/__init__.py,sha256=0tv-_AU5v7PEmhqZbUvMESL7TmllNvhKRu__6v1hkNM,7565
13
+ shadow_debugger/providers/__init__.py,sha256=R7MPArkRWxcu-NyhGcgbq77f3Lgg9ow-nPDrGOiD30c,607
14
+ shadow_debugger/providers/anthropic.py,sha256=R79gClpjwHz6j6w5ELzHfZtTUf2G0NzINYoRfP1mq_s,2031
15
+ shadow_debugger/providers/base.py,sha256=W66umF4drjd_MX9GAhzi3UPCNUWSFtFBUNlzLNlD67I,3071
16
+ shadow_debugger/providers/gemini.py,sha256=CUNJspEQsTZQ6rQ9SP4TmU3euABbXEBpchECKY7_9wk,2573
17
+ shadow_debugger/providers/ollama.py,sha256=Kn037q78PreDqUBYMIoFhxZkDQhxRXHGP-Zg_7-rvoQ,2378
18
+ shadow_debugger/providers/openai.py,sha256=Qqakyt9__nTmE1qG6oDr6ITa9c3xiBwegUDQkfxu6LA,2223
19
+ shadow_debugger-0.1.0.dist-info/licenses/LICENSE,sha256=DTl2sA7c-JVs06AF-1nxjdmE4Xycbe4GlcdpO-HP_o4,1067
20
+ shadow_debugger-0.1.0.dist-info/METADATA,sha256=wbD7bncP3VchW6a8WXXf9I8hsQ1Hp6UsZI2qcw3-tj8,10161
21
+ shadow_debugger-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
22
+ shadow_debugger-0.1.0.dist-info/top_level.txt,sha256=uydI_u61yAjtl6_x4rkzQ9Z-RV5QyH0Jn24bq5Rc680,16
23
+ shadow_debugger-0.1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GaspareDev
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
+ shadow_debugger