beaconhq 0.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,14 @@
1
+ # Python build / cache artifacts — never commit
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+
12
+ # Virtualenvs
13
+ .venv/
14
+ venv/
beaconhq-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Skyware LLC
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,269 @@
1
+ Metadata-Version: 2.4
2
+ Name: beaconhq
3
+ Version: 0.1.0
4
+ Summary: Beacon Python client SDK — auto-capture HTTP request telemetry and ship it to the Beacon ingest API. FastAPI/Starlette, Flask, and Django adapters.
5
+ Project-URL: Homepage, https://beacon.skyware.dev
6
+ Project-URL: Documentation, https://beacon.skyware.dev/docs
7
+ Project-URL: Repository, https://github.com/kb-gardner/HighHrothgar
8
+ Project-URL: Issues, https://github.com/kb-gardner/HighHrothgar/issues
9
+ Author: Skyware LLC
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: api-monitoring,apm,beacon,django,fastapi,flask,monitoring,observability,starlette,telemetry
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: System :: Monitoring
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Provides-Extra: dev
28
+ Requires-Dist: django>=3.2; extra == 'dev'
29
+ Requires-Dist: flask>=2.0; extra == 'dev'
30
+ Requires-Dist: httpx>=0.24; extra == 'dev'
31
+ Requires-Dist: pytest>=7.0; extra == 'dev'
32
+ Requires-Dist: starlette>=0.27; extra == 'dev'
33
+ Provides-Extra: django
34
+ Requires-Dist: django>=3.2; extra == 'django'
35
+ Provides-Extra: fastapi
36
+ Requires-Dist: starlette>=0.27; extra == 'fastapi'
37
+ Provides-Extra: flask
38
+ Requires-Dist: flask>=2.0; extra == 'flask'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # beaconhq — Beacon Python SDK
42
+
43
+ Auto-capture HTTP request telemetry from your **FastAPI / Starlette**, **Flask**,
44
+ or **Django** app and ship it to [Beacon](https://beacon.skyware.dev). This is the
45
+ Python port of `@beaconhq/sdk` — same product, same ingest contract, idiomatic
46
+ Python.
47
+
48
+ > Full docs: <https://beacon.skyware.dev/docs>
49
+
50
+ - **Distribution name:** `beaconhq` (`pip install beaconhq`)
51
+ - **Import package:** `beaconhq` (`import beaconhq`)
52
+ - **Zero required dependencies** for the core — it ships telemetry using only the
53
+ Python standard library (`urllib`, `threading`, `json`, `atexit`). Framework
54
+ support is opt-in via extras and imported lazily, so `import beaconhq` works with
55
+ no web framework installed.
56
+ - **Python 3.9+.**
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install beaconhq # core only
62
+ pip install "beaconhq[fastapi]" # + Starlette/FastAPI adapter
63
+ pip install "beaconhq[flask]" # + Flask adapter
64
+ pip install "beaconhq[django]" # + Django adapter
65
+ ```
66
+
67
+ ## Quickstart
68
+
69
+ You only need a project ingest key — the client defaults to Beacon's hosted ingest
70
+ endpoint (`https://ingest.beacon.skyware.dev/v1/ingest`).
71
+
72
+ ```python
73
+ from beaconhq import BeaconClient
74
+
75
+ beacon = BeaconClient(ingest_key="your-per-project-ingest-key")
76
+ ```
77
+
78
+ Or configure from the environment (`BEACON_INGEST_KEY`, optional `BEACON_INGEST_URL`,
79
+ optional `BEACON_ENABLED`):
80
+
81
+ ```python
82
+ from beaconhq import BeaconClient
83
+ beacon = BeaconClient.from_env()
84
+ ```
85
+
86
+ ### FastAPI / Starlette
87
+
88
+ ```python
89
+ from fastapi import FastAPI
90
+ from beaconhq import BeaconClient
91
+ from beaconhq.integrations.asgi import BeaconASGIMiddleware
92
+
93
+ beacon = BeaconClient(ingest_key=KEY)
94
+
95
+ app = FastAPI()
96
+ app.add_middleware(BeaconASGIMiddleware, client=beacon, consumer_header="x-api-key")
97
+
98
+ @app.get("/users/{user_id}")
99
+ def get_user(user_id: int):
100
+ return {"id": user_id}
101
+ # Captured route template: "/users/{user_id}" (not the concrete "/users/123")
102
+ ```
103
+
104
+ ### Flask
105
+
106
+ ```python
107
+ from flask import Flask
108
+ from beaconhq import BeaconClient
109
+ from beaconhq.integrations.flask import init_flask
110
+
111
+ beacon = BeaconClient(ingest_key=KEY)
112
+
113
+ app = Flask(__name__)
114
+ init_flask(app, beacon, consumer_header="X-API-Key")
115
+
116
+ @app.route("/users/<int:user_id>")
117
+ def get_user(user_id):
118
+ return {"id": user_id}
119
+ # Captured route template: "/users/<int:user_id>"
120
+ ```
121
+
122
+ ### Django
123
+
124
+ ```python
125
+ # settings.py
126
+ from beaconhq import BeaconClient
127
+
128
+ BEACON_CLIENT = BeaconClient(ingest_key="your-per-project-ingest-key")
129
+ BEACON_CONSUMER_HEADER = "X-API-Key" # optional
130
+
131
+ MIDDLEWARE = [
132
+ "beaconhq.integrations.django.BeaconDjangoMiddleware", # put it near the top
133
+ # ... your other middleware ...
134
+ ]
135
+ # Captured route template: "/users/<int:pk>" (from request.resolver_match.route)
136
+ ```
137
+
138
+ If `BEACON_CLIENT` is missing the middleware disables itself cleanly (Django
139
+ `MiddlewareNotUsed`) rather than erroring.
140
+
141
+ ## Self-hosting / overriding the endpoint
142
+
143
+ Point at your own Beacon ingest by passing `ingest_url` (a full endpoint, or a base
144
+ URL to which `/v1/ingest` is appended):
145
+
146
+ ```python
147
+ beacon = BeaconClient(
148
+ ingest_key=KEY,
149
+ ingest_url="https://beacon.internal.example.com", # /v1/ingest appended if omitted
150
+ )
151
+ ```
152
+
153
+ ## How it works
154
+
155
+ `capture()` only appends to an in-memory buffer — it is **non-blocking** and adds
156
+ negligible latency to your request path. A background daemon thread flushes the
157
+ buffer to the ingest API every `flush_interval` seconds, or eagerly when it reaches
158
+ `batch_size`. The client **never raises into your app**: serialization and network
159
+ failures are caught, optionally reported via `on_error`, and the batch is re-queued
160
+ (network error / 5xx) or dropped (4xx). Remaining events are flushed on interpreter
161
+ exit via `atexit`.
162
+
163
+ ## Identifying the consumer (the "who called me" hook)
164
+
165
+ `consumer` is the identity of the caller (an API-key id, user id, tenant…). The
166
+ simplest option is to read a header:
167
+
168
+ ```python
169
+ app.add_middleware(BeaconASGIMiddleware, client=beacon, consumer_header="x-api-key")
170
+ ```
171
+
172
+ For anything richer, pass a `consumer_resolver` — a callable that receives the
173
+ framework's request object and returns a string or `None`:
174
+
175
+ ```python
176
+ def resolve_consumer(request) -> str | None:
177
+ # FastAPI/Starlette: `request` is the raw ASGI `scope` dict
178
+ # Flask: `request` is `flask.request`
179
+ # Django: `request` is the `HttpRequest`
180
+ return getattr(getattr(request, "state", None), "api_key_id", None)
181
+
182
+ app.add_middleware(
183
+ BeaconASGIMiddleware, client=beacon, consumer_resolver=resolve_consumer
184
+ )
185
+ ```
186
+
187
+ You can also set a default resolver on the client itself
188
+ (`BeaconClient(..., consumer_resolver=...)`) so every adapter uses it. Resolution
189
+ precedence: adapter `consumer_resolver` → client `consumer_resolver` → header value.
190
+ A resolver that raises is treated as "no consumer" — it never breaks the request.
191
+
192
+ ## Configuration reference
193
+
194
+ `BeaconClient(...)` — option names are the snake_case translation of the JS
195
+ `BeaconClientOptions`; defaults match `@beaconhq/sdk`.
196
+
197
+ | Option | Type | Default | Description |
198
+ |---|---|---|---|
199
+ | `ingest_key` | `str` | — (required) | Per-project ingest key, sent as `Authorization: Bearer`. The only required argument. |
200
+ | `ingest_url` | `str` | `https://ingest.beacon.skyware.dev/v1/ingest` | Full endpoint or base URL (`/v1/ingest` appended). Override for self-hosted Beacon. |
201
+ | `batch_size` | `int` | `100` | Buffered events that trigger an eager flush. |
202
+ | `flush_interval` | `float` | `5.0` | Background flush cadence, **seconds** (JS uses 5000 ms). |
203
+ | `max_buffer_size` | `int` | `10000` | Memory cap; oldest events dropped past this. |
204
+ | `timeout` | `float` | `5.0` | Per-request HTTP timeout, seconds. |
205
+ | `enabled` | `bool` | `True` | When `False`, `capture()` is a no-op. |
206
+ | `dry_run` | `bool` | `False` | Buffer + "flush" to an in-memory sink; nothing leaves the process. |
207
+ | `debug` | `bool` | `False` | Attach a default `on_error` that logs at WARNING. |
208
+ | `on_error` | `Callable[[BaseException], None]` | no-op | Observability hook; never required, never re-raised. |
209
+ | `consumer_resolver` | `Callable[[req], str \| None]` | `None` | Default consumer hook for all adapters. |
210
+ | `transport` | `Transport` | HTTP | Inject a custom transport (tests). |
211
+ | `register_atexit` | `bool` | `True` | Flush remaining events on interpreter exit. |
212
+
213
+ Adapter options (`BeaconASGIMiddleware`, `init_flask`, and the Django settings
214
+ `BEACON_CONSUMER_HEADER` / `BEACON_CONSUMER_RESOLVER`): `consumer_header` and
215
+ `consumer_resolver`, as above.
216
+
217
+ Environment variables (read by `BeaconClient.from_env()`): `BEACON_INGEST_KEY`
218
+ (required), `BEACON_INGEST_URL` (optional — defaults to the hosted endpoint),
219
+ `BEACON_ENABLED` (`0`/`false`/`no` disables).
220
+
221
+ ## Event payload
222
+
223
+ Each captured request becomes one event in the batched `POST /v1/ingest` body
224
+ (`{"events": [ ... ]}`). Field names and types match the ingest contract and the
225
+ server's validation exactly:
226
+
227
+ ```json
228
+ {
229
+ "ts": "2026-06-03T12:00:00.000+00:00",
230
+ "method": "GET",
231
+ "route": "/users/{id}",
232
+ "path": "/users/123",
233
+ "status": 200,
234
+ "duration_ms": 42,
235
+ "consumer": "acme",
236
+ "error": null
237
+ }
238
+ ```
239
+
240
+ `route` is the low-cardinality **template** (resolved from the framework's
241
+ routing), which powers per-endpoint aggregation; `path` is the concrete request
242
+ path. `consumer` and `error` are always sent (as `null` when absent), matching the
243
+ JS SDK.
244
+
245
+ ## Graceful shutdown
246
+
247
+ Buffered events are flushed automatically on interpreter exit. To flush explicitly
248
+ (e.g. in a worker's shutdown hook):
249
+
250
+ ```python
251
+ beacon.shutdown() # stops the timer and flushes remaining events; idempotent
252
+ ```
253
+
254
+ ## Development & tests
255
+
256
+ ```bash
257
+ python -m venv .venv && source .venv/bin/activate
258
+ pip install -e ".[dev]"
259
+ pytest
260
+ ```
261
+
262
+ Core tests run against an in-memory mock transport (no network) and assert the
263
+ exact payload shape, batching, interval/shutdown flush, and the
264
+ never-raise/requeue behavior. Adapter smoke tests drive a real request through
265
+ each framework and skip if the framework isn't installed.
266
+
267
+ ## License
268
+
269
+ MIT © Skyware LLC
@@ -0,0 +1,229 @@
1
+ # beaconhq — Beacon Python SDK
2
+
3
+ Auto-capture HTTP request telemetry from your **FastAPI / Starlette**, **Flask**,
4
+ or **Django** app and ship it to [Beacon](https://beacon.skyware.dev). This is the
5
+ Python port of `@beaconhq/sdk` — same product, same ingest contract, idiomatic
6
+ Python.
7
+
8
+ > Full docs: <https://beacon.skyware.dev/docs>
9
+
10
+ - **Distribution name:** `beaconhq` (`pip install beaconhq`)
11
+ - **Import package:** `beaconhq` (`import beaconhq`)
12
+ - **Zero required dependencies** for the core — it ships telemetry using only the
13
+ Python standard library (`urllib`, `threading`, `json`, `atexit`). Framework
14
+ support is opt-in via extras and imported lazily, so `import beaconhq` works with
15
+ no web framework installed.
16
+ - **Python 3.9+.**
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install beaconhq # core only
22
+ pip install "beaconhq[fastapi]" # + Starlette/FastAPI adapter
23
+ pip install "beaconhq[flask]" # + Flask adapter
24
+ pip install "beaconhq[django]" # + Django adapter
25
+ ```
26
+
27
+ ## Quickstart
28
+
29
+ You only need a project ingest key — the client defaults to Beacon's hosted ingest
30
+ endpoint (`https://ingest.beacon.skyware.dev/v1/ingest`).
31
+
32
+ ```python
33
+ from beaconhq import BeaconClient
34
+
35
+ beacon = BeaconClient(ingest_key="your-per-project-ingest-key")
36
+ ```
37
+
38
+ Or configure from the environment (`BEACON_INGEST_KEY`, optional `BEACON_INGEST_URL`,
39
+ optional `BEACON_ENABLED`):
40
+
41
+ ```python
42
+ from beaconhq import BeaconClient
43
+ beacon = BeaconClient.from_env()
44
+ ```
45
+
46
+ ### FastAPI / Starlette
47
+
48
+ ```python
49
+ from fastapi import FastAPI
50
+ from beaconhq import BeaconClient
51
+ from beaconhq.integrations.asgi import BeaconASGIMiddleware
52
+
53
+ beacon = BeaconClient(ingest_key=KEY)
54
+
55
+ app = FastAPI()
56
+ app.add_middleware(BeaconASGIMiddleware, client=beacon, consumer_header="x-api-key")
57
+
58
+ @app.get("/users/{user_id}")
59
+ def get_user(user_id: int):
60
+ return {"id": user_id}
61
+ # Captured route template: "/users/{user_id}" (not the concrete "/users/123")
62
+ ```
63
+
64
+ ### Flask
65
+
66
+ ```python
67
+ from flask import Flask
68
+ from beaconhq import BeaconClient
69
+ from beaconhq.integrations.flask import init_flask
70
+
71
+ beacon = BeaconClient(ingest_key=KEY)
72
+
73
+ app = Flask(__name__)
74
+ init_flask(app, beacon, consumer_header="X-API-Key")
75
+
76
+ @app.route("/users/<int:user_id>")
77
+ def get_user(user_id):
78
+ return {"id": user_id}
79
+ # Captured route template: "/users/<int:user_id>"
80
+ ```
81
+
82
+ ### Django
83
+
84
+ ```python
85
+ # settings.py
86
+ from beaconhq import BeaconClient
87
+
88
+ BEACON_CLIENT = BeaconClient(ingest_key="your-per-project-ingest-key")
89
+ BEACON_CONSUMER_HEADER = "X-API-Key" # optional
90
+
91
+ MIDDLEWARE = [
92
+ "beaconhq.integrations.django.BeaconDjangoMiddleware", # put it near the top
93
+ # ... your other middleware ...
94
+ ]
95
+ # Captured route template: "/users/<int:pk>" (from request.resolver_match.route)
96
+ ```
97
+
98
+ If `BEACON_CLIENT` is missing the middleware disables itself cleanly (Django
99
+ `MiddlewareNotUsed`) rather than erroring.
100
+
101
+ ## Self-hosting / overriding the endpoint
102
+
103
+ Point at your own Beacon ingest by passing `ingest_url` (a full endpoint, or a base
104
+ URL to which `/v1/ingest` is appended):
105
+
106
+ ```python
107
+ beacon = BeaconClient(
108
+ ingest_key=KEY,
109
+ ingest_url="https://beacon.internal.example.com", # /v1/ingest appended if omitted
110
+ )
111
+ ```
112
+
113
+ ## How it works
114
+
115
+ `capture()` only appends to an in-memory buffer — it is **non-blocking** and adds
116
+ negligible latency to your request path. A background daemon thread flushes the
117
+ buffer to the ingest API every `flush_interval` seconds, or eagerly when it reaches
118
+ `batch_size`. The client **never raises into your app**: serialization and network
119
+ failures are caught, optionally reported via `on_error`, and the batch is re-queued
120
+ (network error / 5xx) or dropped (4xx). Remaining events are flushed on interpreter
121
+ exit via `atexit`.
122
+
123
+ ## Identifying the consumer (the "who called me" hook)
124
+
125
+ `consumer` is the identity of the caller (an API-key id, user id, tenant…). The
126
+ simplest option is to read a header:
127
+
128
+ ```python
129
+ app.add_middleware(BeaconASGIMiddleware, client=beacon, consumer_header="x-api-key")
130
+ ```
131
+
132
+ For anything richer, pass a `consumer_resolver` — a callable that receives the
133
+ framework's request object and returns a string or `None`:
134
+
135
+ ```python
136
+ def resolve_consumer(request) -> str | None:
137
+ # FastAPI/Starlette: `request` is the raw ASGI `scope` dict
138
+ # Flask: `request` is `flask.request`
139
+ # Django: `request` is the `HttpRequest`
140
+ return getattr(getattr(request, "state", None), "api_key_id", None)
141
+
142
+ app.add_middleware(
143
+ BeaconASGIMiddleware, client=beacon, consumer_resolver=resolve_consumer
144
+ )
145
+ ```
146
+
147
+ You can also set a default resolver on the client itself
148
+ (`BeaconClient(..., consumer_resolver=...)`) so every adapter uses it. Resolution
149
+ precedence: adapter `consumer_resolver` → client `consumer_resolver` → header value.
150
+ A resolver that raises is treated as "no consumer" — it never breaks the request.
151
+
152
+ ## Configuration reference
153
+
154
+ `BeaconClient(...)` — option names are the snake_case translation of the JS
155
+ `BeaconClientOptions`; defaults match `@beaconhq/sdk`.
156
+
157
+ | Option | Type | Default | Description |
158
+ |---|---|---|---|
159
+ | `ingest_key` | `str` | — (required) | Per-project ingest key, sent as `Authorization: Bearer`. The only required argument. |
160
+ | `ingest_url` | `str` | `https://ingest.beacon.skyware.dev/v1/ingest` | Full endpoint or base URL (`/v1/ingest` appended). Override for self-hosted Beacon. |
161
+ | `batch_size` | `int` | `100` | Buffered events that trigger an eager flush. |
162
+ | `flush_interval` | `float` | `5.0` | Background flush cadence, **seconds** (JS uses 5000 ms). |
163
+ | `max_buffer_size` | `int` | `10000` | Memory cap; oldest events dropped past this. |
164
+ | `timeout` | `float` | `5.0` | Per-request HTTP timeout, seconds. |
165
+ | `enabled` | `bool` | `True` | When `False`, `capture()` is a no-op. |
166
+ | `dry_run` | `bool` | `False` | Buffer + "flush" to an in-memory sink; nothing leaves the process. |
167
+ | `debug` | `bool` | `False` | Attach a default `on_error` that logs at WARNING. |
168
+ | `on_error` | `Callable[[BaseException], None]` | no-op | Observability hook; never required, never re-raised. |
169
+ | `consumer_resolver` | `Callable[[req], str \| None]` | `None` | Default consumer hook for all adapters. |
170
+ | `transport` | `Transport` | HTTP | Inject a custom transport (tests). |
171
+ | `register_atexit` | `bool` | `True` | Flush remaining events on interpreter exit. |
172
+
173
+ Adapter options (`BeaconASGIMiddleware`, `init_flask`, and the Django settings
174
+ `BEACON_CONSUMER_HEADER` / `BEACON_CONSUMER_RESOLVER`): `consumer_header` and
175
+ `consumer_resolver`, as above.
176
+
177
+ Environment variables (read by `BeaconClient.from_env()`): `BEACON_INGEST_KEY`
178
+ (required), `BEACON_INGEST_URL` (optional — defaults to the hosted endpoint),
179
+ `BEACON_ENABLED` (`0`/`false`/`no` disables).
180
+
181
+ ## Event payload
182
+
183
+ Each captured request becomes one event in the batched `POST /v1/ingest` body
184
+ (`{"events": [ ... ]}`). Field names and types match the ingest contract and the
185
+ server's validation exactly:
186
+
187
+ ```json
188
+ {
189
+ "ts": "2026-06-03T12:00:00.000+00:00",
190
+ "method": "GET",
191
+ "route": "/users/{id}",
192
+ "path": "/users/123",
193
+ "status": 200,
194
+ "duration_ms": 42,
195
+ "consumer": "acme",
196
+ "error": null
197
+ }
198
+ ```
199
+
200
+ `route` is the low-cardinality **template** (resolved from the framework's
201
+ routing), which powers per-endpoint aggregation; `path` is the concrete request
202
+ path. `consumer` and `error` are always sent (as `null` when absent), matching the
203
+ JS SDK.
204
+
205
+ ## Graceful shutdown
206
+
207
+ Buffered events are flushed automatically on interpreter exit. To flush explicitly
208
+ (e.g. in a worker's shutdown hook):
209
+
210
+ ```python
211
+ beacon.shutdown() # stops the timer and flushes remaining events; idempotent
212
+ ```
213
+
214
+ ## Development & tests
215
+
216
+ ```bash
217
+ python -m venv .venv && source .venv/bin/activate
218
+ pip install -e ".[dev]"
219
+ pytest
220
+ ```
221
+
222
+ Core tests run against an in-memory mock transport (no network) and assert the
223
+ exact payload shape, batching, interval/shutdown flush, and the
224
+ never-raise/requeue behavior. Adapter smoke tests drive a real request through
225
+ each framework and skip if the framework isn't installed.
226
+
227
+ ## License
228
+
229
+ MIT © Skyware LLC
@@ -0,0 +1,80 @@
1
+ # PEP 621 packaging for the Beacon Python SDK.
2
+ #
3
+ # Distribution name: beaconhq (pip install beaconhq)
4
+ # Import package: beaconhq (import beaconhq)
5
+ #
6
+ # The core has ZERO required third-party dependencies — it ships telemetry to the
7
+ # Beacon ingest API using only the standard library (urllib, threading, json,
8
+ # atexit). Framework support is provided via optional extras that pull in the
9
+ # matching web framework and are imported lazily, so `import beaconhq` works with
10
+ # none of them installed.
11
+ [build-system]
12
+ requires = ["hatchling"]
13
+ build-backend = "hatchling.build"
14
+
15
+ [project]
16
+ name = "beaconhq"
17
+ version = "0.1.0"
18
+ description = "Beacon Python client SDK — auto-capture HTTP request telemetry and ship it to the Beacon ingest API. FastAPI/Starlette, Flask, and Django adapters."
19
+ readme = "README.md"
20
+ requires-python = ">=3.9"
21
+ license = { text = "MIT" }
22
+ authors = [{ name = "Skyware LLC" }]
23
+ keywords = [
24
+ "beacon",
25
+ "observability",
26
+ "monitoring",
27
+ "telemetry",
28
+ "apm",
29
+ "api-monitoring",
30
+ "fastapi",
31
+ "starlette",
32
+ "flask",
33
+ "django",
34
+ ]
35
+ classifiers = [
36
+ "Development Status :: 4 - Beta",
37
+ "Intended Audience :: Developers",
38
+ "License :: OSI Approved :: MIT License",
39
+ "Operating System :: OS Independent",
40
+ "Programming Language :: Python :: 3",
41
+ "Programming Language :: Python :: 3.9",
42
+ "Programming Language :: Python :: 3.10",
43
+ "Programming Language :: Python :: 3.11",
44
+ "Programming Language :: Python :: 3.12",
45
+ "Programming Language :: Python :: 3.13",
46
+ "Topic :: System :: Monitoring",
47
+ "Topic :: Software Development :: Libraries :: Python Modules",
48
+ "Typing :: Typed",
49
+ ]
50
+ dependencies = []
51
+
52
+ [project.urls]
53
+ Homepage = "https://beacon.skyware.dev"
54
+ Documentation = "https://beacon.skyware.dev/docs"
55
+ Repository = "https://github.com/kb-gardner/HighHrothgar"
56
+ Issues = "https://github.com/kb-gardner/HighHrothgar/issues"
57
+
58
+ [project.optional-dependencies]
59
+ # Framework adapters. Each imports its framework lazily; install only what you use.
60
+ fastapi = ["starlette>=0.27"]
61
+ flask = ["flask>=2.0"]
62
+ django = ["django>=3.2"]
63
+ # Dev/test toolchain.
64
+ dev = [
65
+ "pytest>=7.0",
66
+ "starlette>=0.27",
67
+ "httpx>=0.24",
68
+ "flask>=2.0",
69
+ "django>=3.2",
70
+ ]
71
+
72
+ [tool.hatch.build.targets.wheel]
73
+ packages = ["src/beaconhq"]
74
+
75
+ [tool.hatch.build.targets.sdist]
76
+ include = ["src/beaconhq", "README.md", "pyproject.toml", "LICENSE"]
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+ addopts = "-q"
@@ -0,0 +1,61 @@
1
+ """beaconhq — the Beacon Python client SDK.
2
+
3
+ The core (:class:`BeaconClient`, :class:`BeaconEvent`) buffers and ships request
4
+ events to the Beacon ingest API and depends only on the standard library. The
5
+ framework adapters auto-capture method / route-template / status / latency from
6
+ FastAPI/Starlette, Flask, and Django, and live in :mod:`beaconhq.integrations`.
7
+
8
+ The raw HTTP contract in ``docs/ingest-contract.md`` is the language-agnostic
9
+ source of truth; this SDK is the convenience wrapper for Python services and is a
10
+ direct port of ``@beaconhq/sdk``.
11
+
12
+ Importing ``beaconhq`` never imports a web framework. The adapters are exposed
13
+ lazily here — accessing :data:`BeaconASGIMiddleware`, :data:`init_flask`, or
14
+ :data:`BeaconDjangoMiddleware` imports the relevant ``beaconhq.integrations.*``
15
+ module (which imports its framework) only on first use, so ``import beaconhq``
16
+ works with zero framework dependencies installed.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ from .client import DEFAULT_INGEST_URL, BeaconClient, utc_now_iso
24
+ from .event import BeaconEvent
25
+ from .transport import HttpTransport, MockTransport, SendResult, Transport
26
+
27
+ __all__ = [
28
+ "BeaconClient",
29
+ "BeaconEvent",
30
+ "Transport",
31
+ "HttpTransport",
32
+ "MockTransport",
33
+ "SendResult",
34
+ "utc_now_iso",
35
+ "DEFAULT_INGEST_URL",
36
+ # Lazily resolved framework adapters (see __getattr__).
37
+ "BeaconASGIMiddleware",
38
+ "init_flask",
39
+ "BeaconDjangoMiddleware",
40
+ ]
41
+
42
+ __version__ = "0.1.0"
43
+
44
+ # Map of lazily-exposed adapter names -> (submodule, attribute). Resolved on
45
+ # first attribute access so the core import stays framework-free.
46
+ _LAZY_ATTRS = {
47
+ "BeaconASGIMiddleware": (".integrations.asgi", "BeaconASGIMiddleware"),
48
+ "init_flask": (".integrations.flask", "init_flask"),
49
+ "BeaconDjangoMiddleware": (".integrations.django", "BeaconDjangoMiddleware"),
50
+ }
51
+
52
+
53
+ def __getattr__(name: str) -> Any:
54
+ """PEP 562 module-level lazy attribute access for the framework adapters."""
55
+ target = _LAZY_ATTRS.get(name)
56
+ if target is None:
57
+ raise AttributeError(f"module 'beaconhq' has no attribute {name!r}")
58
+ from importlib import import_module
59
+
60
+ module = import_module(target[0], __name__)
61
+ return getattr(module, target[1])