insider-python 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.
insider/transport.py ADDED
@@ -0,0 +1,213 @@
1
+ """
2
+ Background HTTP transport.
3
+
4
+ The capture path is on the customer's request thread. Sending the beacon
5
+ on that thread would add a network round-trip to every error response —
6
+ that is exactly what we will not do. Instead:
7
+
8
+ 1. `submit(beacon)` puts the beacon on a bounded in-memory queue
9
+ (`queue.Queue.put_nowait`).
10
+ 2. If the queue is full we drop the beacon and increment a counter.
11
+ The customer's thread never blocks waiting on us.
12
+ 3. A daemon worker thread loops on `queue.get()` and POSTs each beacon
13
+ to the beam endpoint. Any error during the POST is caught, logged,
14
+ and the beacon discarded. No retries in v1 (see docs/python-sdk-plan
15
+ -> Non-goals).
16
+ 4. `atexit` registers `close()` so short-lived scripts and management
17
+ commands drain before exit (bounded by `flush_timeout`).
18
+
19
+ The transport is the *only* place inside the SDK that talks to the
20
+ network. Everything else is in-process work.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import queue
27
+ import threading
28
+ import time
29
+ from typing import Any, Dict, Optional
30
+
31
+ import urllib3
32
+
33
+ from ._version import __version__
34
+ from .dsn import DSN
35
+ from .safety import debug
36
+
37
+
38
+ _SENTINEL = object()
39
+
40
+
41
+ class BackgroundTransport:
42
+ """
43
+ Bounded queue + daemon worker thread + urllib3 connection pool.
44
+
45
+ Public methods are thread-safe. `submit` is the only hot-path call.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ dsn: DSN,
51
+ *,
52
+ queue_size: int = 1000,
53
+ flush_timeout: float = 2.0,
54
+ connect_timeout: float = 2.0,
55
+ read_timeout: float = 5.0,
56
+ ) -> None:
57
+ self._dsn = dsn
58
+ self._flush_timeout = flush_timeout
59
+ self._queue: "queue.Queue[Any]" = queue.Queue(maxsize=queue_size)
60
+ self._pool: Optional[urllib3.PoolManager] = urllib3.PoolManager(
61
+ timeout=urllib3.Timeout(connect=connect_timeout, read=read_timeout),
62
+ retries=False,
63
+ maxsize=4,
64
+ block=False,
65
+ )
66
+ self._closed = threading.Event()
67
+
68
+ # Bookkeeping the customer can read for telemetry-on-telemetry.
69
+ self.submitted_total: int = 0
70
+ self.sent_total: int = 0
71
+ self.dropped_full: int = 0
72
+ self.dropped_error: int = 0
73
+
74
+ self._worker = threading.Thread(
75
+ target=self._run,
76
+ name="insider-transport",
77
+ daemon=True,
78
+ )
79
+ self._worker.start()
80
+
81
+ # ------------------------------------------------------------------
82
+ # Public API
83
+ # ------------------------------------------------------------------
84
+
85
+ def submit(self, beacon: Dict[str, Any]) -> bool:
86
+ """
87
+ Enqueue a beacon. Returns True if accepted, False if dropped.
88
+ Never blocks the caller. Never raises.
89
+ """
90
+ if self._closed.is_set():
91
+ self.dropped_full += 1
92
+ return False
93
+ try:
94
+ self._queue.put_nowait(beacon)
95
+ except queue.Full:
96
+ self.dropped_full += 1
97
+ debug("queue full; dropping beacon")
98
+ return False
99
+ self.submitted_total += 1
100
+ return True
101
+
102
+ def flush(self, timeout: Optional[float] = None) -> bool:
103
+ """
104
+ Block until the queue drains or `timeout` elapses. Returns True
105
+ if drained, False otherwise. `timeout=None` uses `flush_timeout`.
106
+ """
107
+ deadline = time.monotonic() + (
108
+ timeout if timeout is not None else self._flush_timeout
109
+ )
110
+ while time.monotonic() < deadline:
111
+ if self._queue.unfinished_tasks == 0:
112
+ return True
113
+ time.sleep(0.01)
114
+ return self._queue.unfinished_tasks == 0
115
+
116
+ def close(self, timeout: Optional[float] = None) -> None:
117
+ """
118
+ Stop accepting new beacons, drain the queue, join the worker.
119
+ Idempotent. Bounded by `timeout` (defaults to `flush_timeout`).
120
+ """
121
+ if self._closed.is_set():
122
+ return
123
+ self._closed.set()
124
+ # Push sentinel even if queue is full — use put with timeout so we
125
+ # don't hang shutdown forever if a runaway producer is filling it.
126
+ try:
127
+ self._queue.put(_SENTINEL, timeout=1.0)
128
+ except queue.Full:
129
+ debug("close: queue full, sentinel may be late")
130
+ wait = timeout if timeout is not None else self._flush_timeout
131
+ self._worker.join(timeout=wait)
132
+ if self._pool is not None:
133
+ try:
134
+ self._pool.clear()
135
+ except Exception as exc:
136
+ debug(f"pool clear failed: {exc}")
137
+ self._pool = None
138
+
139
+ # ------------------------------------------------------------------
140
+ # Worker loop
141
+ # ------------------------------------------------------------------
142
+
143
+ def _run(self) -> None:
144
+ # Outer try guarantees the worker thread never dies on an unexpected
145
+ # exception. Losing the worker would silently leak beacons forever.
146
+ while True:
147
+ try:
148
+ item = self._queue.get()
149
+ except Exception as exc:
150
+ debug(f"queue.get failed: {exc}")
151
+ continue
152
+
153
+ try:
154
+ if item is _SENTINEL:
155
+ return
156
+ self._send_one(item)
157
+ except Exception as exc:
158
+ self.dropped_error += 1
159
+ debug(f"send loop swallowed {type(exc).__name__}: {exc}")
160
+ finally:
161
+ try:
162
+ self._queue.task_done()
163
+ except Exception:
164
+ pass
165
+
166
+ def _send_one(self, beacon: Dict[str, Any]) -> None:
167
+ if self._pool is None:
168
+ self.dropped_error += 1
169
+ return
170
+ try:
171
+ body = json.dumps(beacon, default=str, ensure_ascii=False).encode("utf-8")
172
+ except Exception as exc:
173
+ # If we can't even serialize, ship a minimal fallback so the
174
+ # event isn't lost entirely.
175
+ self.dropped_error += 1
176
+ debug(f"json encode failed: {exc}; shipping minimal envelope")
177
+ body = json.dumps(
178
+ {
179
+ "kind": "error",
180
+ "level": "error",
181
+ "occurred_at": beacon.get("occurred_at"),
182
+ "message": "<beacon could not be serialized>",
183
+ "payload": {"truncated": True, "encode_error": str(exc)},
184
+ },
185
+ default=str,
186
+ ).encode("utf-8")
187
+
188
+ headers = {
189
+ "Content-Type": "application/json",
190
+ "Authorization": f"Bearer {self._dsn.token}",
191
+ "User-Agent": f"insider-python/{__version__}",
192
+ "X-Insider-SDK": f"python/{__version__}",
193
+ }
194
+
195
+ try:
196
+ resp = self._pool.urlopen(
197
+ "POST",
198
+ self._dsn.beam_url,
199
+ body=body,
200
+ headers=headers,
201
+ retries=False,
202
+ preload_content=True,
203
+ )
204
+ except Exception as exc:
205
+ self.dropped_error += 1
206
+ debug(f"POST failed: {type(exc).__name__}: {exc}")
207
+ return
208
+
209
+ if resp.status == 202:
210
+ self.sent_total += 1
211
+ else:
212
+ self.dropped_error += 1
213
+ debug(f"server returned {resp.status}; dropping beacon")
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: insider-python
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Insider — ship Beacons to your Insider server.
5
+ Author: Insider
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Insider-Inc/insider-python
8
+ Keywords: insider,telemetry,errors,monitoring,sdk
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: urllib3>=1.26
20
+ Provides-Extra: django
21
+ Requires-Dist: django>=4.2; extra == "django"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8; extra == "dev"
24
+ Requires-Dist: pytest-django>=4.8; extra == "dev"
25
+ Requires-Dist: django>=4.2; extra == "dev"
26
+
27
+ # insider-python
28
+
29
+ The Python SDK for [Insider](https://insider.moraks.cloud/).
30
+
31
+ Beam Beacons from your Python service to your Insider server with a
32
+ one-line setup. No runtime overhead on your request path. Never raises
33
+ into your code, no matter what.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install insider-python
39
+ ```
40
+
41
+ For the Django integration:
42
+
43
+ ```bash
44
+ pip install "insider-python[django]"
45
+ ```
46
+
47
+ ## Quick start
48
+
49
+ ### Plain Python
50
+
51
+ ```python
52
+ import insider
53
+
54
+ insider.init(
55
+ dsn="https://<beacon_token>@insider.example.com/<project_uuid>",
56
+ environment="production",
57
+ release="1.2.3",
58
+ )
59
+
60
+ try:
61
+ risky()
62
+ except Exception as exc:
63
+ insider.capture_exception(exc)
64
+
65
+ insider.capture_message("cache miss spiked", level="warning")
66
+ ```
67
+
68
+ ### Django
69
+
70
+ Add the integration to `INSTALLED_APPS` and configure via settings:
71
+
72
+ ```python
73
+ INSTALLED_APPS = [
74
+ # ...
75
+ "insider.contrib.django",
76
+ ]
77
+
78
+ MIDDLEWARE = [
79
+ # ...
80
+ "insider.contrib.django.middleware.InsiderMiddleware",
81
+ ]
82
+
83
+ INSIDER_DSN = "https://<beacon_token>@insider.example.com/<project_uuid>"
84
+ INSIDER_ENVIRONMENT = "production"
85
+ INSIDER_RELEASE = "1.2.3"
86
+ ```
87
+
88
+ That's the whole setup. Every unhandled exception in a view is now a
89
+ Beacon in your dashboard.
90
+
91
+ ## Configuration
92
+
93
+ Order of precedence (first wins):
94
+
95
+ 1. Keyword args to `insider.init(...)`.
96
+ 2. `INSIDER_*` environment variables.
97
+ 3. Django settings (when the Django integration is active).
98
+ 4. Hard-coded defaults.
99
+
100
+ If no DSN is found anywhere, the SDK enters **disabled mode**: every
101
+ public call is a no-op, no thread starts, no socket opens.
102
+
103
+ | Option | Default | Notes |
104
+ |--------|---------|-------|
105
+ | `dsn` | env `INSIDER_DSN` | If absent, SDK is disabled |
106
+ | `environment` | `"production"` | Top-level Beacon field |
107
+ | `release` | `None` | Top-level Beacon field |
108
+ | `send_default_pii` | `False` | Required to capture `user.id`, request bodies |
109
+ | `before_send` | `None` | `(beacon) -> beacon | None` hook |
110
+ | `scrub_keys` | `None` | Extra keys to filter (added to the default deny-list) |
111
+ | `in_app_include` | `None` | Filename prefixes considered "your code" |
112
+ | `transport_queue_size` | `1000` | Bounded; drops on overflow |
113
+ | `transport_flush_timeout` | `2.0` | Seconds. Used by `close()` / `flush()` |
114
+ | `debug` | `False` | Print SDK's own warnings to stderr |
115
+
116
+ ## Promise
117
+
118
+ The SDK never raises into your code. Every public function catches
119
+ `Exception` at its boundary; if something goes wrong inside the SDK,
120
+ you get nothing back and a debug log (if enabled). Your app keeps
121
+ running.
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,19 @@
1
+ insider/__init__.py,sha256=DgDevX2wcy9lonZiHaq5dJLieD3_fp3p6l3lotuTpGE,781
2
+ insider/_envelope.py,sha256=nxMumrnB_KXF51aOmSoPVlvJ_On7cQx2GS96YM64CB4,6583
3
+ insider/_version.py,sha256=oPP6-Q8wh-D2EeEaYkiVm6AZMC4ipVMVwcqq2RJp7u8,246
4
+ insider/client.py,sha256=NacSGY0FycPogWa9m6sZYMNWhn9BWAdr_sXjbhpmIRc,10797
5
+ insider/dsn.py,sha256=S8BbRhmKKKU1N6W8cpMSQoyDO_EuUJ1PkcjhGrrf5hw,3009
6
+ insider/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ insider/safety.py,sha256=mtzzWLujuvmd24W2VtVcMGO_81GPBbB2tYuVqU2KsuI,1910
8
+ insider/scope.py,sha256=F1eMWt6uu14uBdncc5DC6Lh735EPcUv1yF4prBi--pc,1708
9
+ insider/scrubbing.py,sha256=U8N3MS5VtDUNpIWN4PZ8bvrPqmjRRXq5hWGXxzoF5Gw,2853
10
+ insider/stacktrace.py,sha256=i_Okdtu6ZLRicl7YBFzxe3B1likDIS4tK7GJG2iWKcw,5048
11
+ insider/transport.py,sha256=8ncFb3C0LgF2hMRoj_fhxj49NIUD6hsVAHfPZKtNcn4,7292
12
+ insider/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ insider/contrib/django/__init__.py,sha256=UMGrJgVO-FqszZo9Cviw7oWS8PLdXVHt3hwXZ_YMj8A,680
14
+ insider/contrib/django/apps.py,sha256=WOKn-fNp_SSPNyt8xXz_fXYVhkyepafAi1uxpWC9_Bo,1947
15
+ insider/contrib/django/middleware.py,sha256=JWnP_p4JBGyKQMswevjWxoUBSE5o4xgLKYOm1Zrb30g,5365
16
+ insider_python-0.1.0.dist-info/METADATA,sha256=xNxX_dGung9se-WnsdnbUJprcwYHZFXdB0lqB9YEKWI,3531
17
+ insider_python-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
+ insider_python-0.1.0.dist-info/top_level.txt,sha256=g_YKp2jCaaefmasZ2nOa9capm0X8q2sAWI_eEClKIos,8
19
+ insider_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ insider