broodminder-data 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.
bm/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """BroodMinder External User API client (spike)."""
2
+
3
+ from .client import BroodMinderClient, BroodMinderError, RateLimited
4
+
5
+ __all__ = ["BroodMinderClient", "BroodMinderError", "RateLimited"]
bm/client.py ADDED
@@ -0,0 +1,166 @@
1
+ """Thin, reusable client for the BroodMinder External User API.
2
+
3
+ Deliberately dependency-light and transport-clean so it can later be lifted
4
+ into an MCP server, a broodly ingestion job, or a Go port. The only stateful
5
+ concession to the alpha API is a call counter + 429 backoff, because the key is
6
+ capped at 1000 calls/day.
7
+
8
+ API surface (per https://external-api.mybroodminder.com/user/docs):
9
+ GET /user/metadata/apiaries
10
+ GET /user/hive/{id}/readings?start=&end=
11
+ GET /user/hive/{id}/notes?start=&end=
12
+ GET /user/device/{id}/readings?start=&end=
13
+
14
+ Auth: header `X-Api-Key: <key>`. Time params are unix epoch seconds; the
15
+ readings/notes endpoints are limited to a 6-month span per request.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import time
22
+ from dataclasses import dataclass, field
23
+ from datetime import datetime, timezone
24
+ from typing import Any, Iterator
25
+
26
+ import httpx
27
+
28
+ try:
29
+ from dotenv import load_dotenv
30
+
31
+ load_dotenv()
32
+ except Exception: # python-dotenv optional at runtime
33
+ pass
34
+
35
+ # The docs say "limited to 6 months per request". Use a conservative window
36
+ # (slightly under 6 * 30 days) so we never trip a strict boundary check.
37
+ MAX_WINDOW_SECONDS = 180 * 24 * 60 * 60 # ~180 days
38
+
39
+
40
+ class BroodMinderError(RuntimeError):
41
+ """Any non-2xx response that isn't a rate limit."""
42
+
43
+ def __init__(self, status: int, method: str, url: str, body: str):
44
+ self.status = status
45
+ self.method = method
46
+ self.url = url
47
+ self.body = body
48
+ super().__init__(f"{method} {url} -> {status}: {body[:300]}")
49
+
50
+
51
+ class RateLimited(BroodMinderError):
52
+ """429 Too Many Requests."""
53
+
54
+
55
+ @dataclass
56
+ class BroodMinderClient:
57
+ api_key: str | None = None
58
+ base_url: str | None = None
59
+ timeout: float = 30.0
60
+ max_retries: int = 3
61
+ # Observability: track usage against the 1000/day budget.
62
+ call_count: int = field(default=0, init=False)
63
+
64
+ def __post_init__(self) -> None:
65
+ self.api_key = self.api_key or os.environ.get("BROODMINDER_API_KEY")
66
+ self.base_url = (
67
+ self.base_url
68
+ or os.environ.get("BROODMINDER_BASE_URL")
69
+ or "https://external-api.mybroodminder.com"
70
+ ).rstrip("/")
71
+ if not self.api_key:
72
+ raise BroodMinderError(0, "INIT", self.base_url, "BROODMINDER_API_KEY not set")
73
+ self._client = httpx.Client(
74
+ base_url=self.base_url,
75
+ timeout=self.timeout,
76
+ headers={"X-Api-Key": self.api_key, "Accept": "application/json"},
77
+ )
78
+
79
+ # -- context manager -------------------------------------------------
80
+ def __enter__(self) -> "BroodMinderClient":
81
+ return self
82
+
83
+ def __exit__(self, *exc: Any) -> None:
84
+ self.close()
85
+
86
+ def close(self) -> None:
87
+ self._client.close()
88
+
89
+ # -- low-level -------------------------------------------------------
90
+ def _get(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
91
+ """GET with 429-aware retry. Returns the raw response (callers decide
92
+ whether to .json()), so contract tests can assert on status/headers."""
93
+ last_exc: Exception | None = None
94
+ for attempt in range(self.max_retries + 1):
95
+ self.call_count += 1
96
+ resp = self._client.get(path, params=params)
97
+ if resp.status_code == 429:
98
+ retry_after = float(resp.headers.get("Retry-After", 2 ** attempt))
99
+ last_exc = RateLimited(429, "GET", str(resp.url), resp.text)
100
+ if attempt < self.max_retries:
101
+ time.sleep(min(retry_after, 30))
102
+ continue
103
+ raise last_exc
104
+ return resp
105
+ raise last_exc # pragma: no cover
106
+
107
+ def get_json(self, path: str, params: dict[str, Any] | None = None) -> Any:
108
+ """GET and return parsed JSON, raising BroodMinderError on non-2xx."""
109
+ resp = self._get(path, params)
110
+ if not resp.is_success:
111
+ raise BroodMinderError(resp.status_code, "GET", str(resp.url), resp.text)
112
+ return resp.json()
113
+
114
+ # -- endpoints -------------------------------------------------------
115
+ def apiaries(self) -> Any:
116
+ """GET /user/metadata/apiaries — all active apiaries + nested hives."""
117
+ return self.get_json("/user/metadata/apiaries")
118
+
119
+ def hive_readings(self, hive_id: str | int, start: int, end: int) -> Any:
120
+ """GET /user/hive/{id}/readings — sensor readings grouped by position."""
121
+ return self.get_json(
122
+ f"/user/hive/{hive_id}/readings", {"start": int(start), "end": int(end)}
123
+ )
124
+
125
+ def hive_notes(self, hive_id: str | int, start: int, end: int) -> Any:
126
+ """GET /user/hive/{id}/notes — hive notes in window."""
127
+ return self.get_json(
128
+ f"/user/hive/{hive_id}/notes", {"start": int(start), "end": int(end)}
129
+ )
130
+
131
+ def device_readings(self, device_id: str | int, start: int, end: int) -> Any:
132
+ """GET /user/device/{id}/readings — single-device sensor readings."""
133
+ return self.get_json(
134
+ f"/user/device/{device_id}/readings", {"start": int(start), "end": int(end)}
135
+ )
136
+
137
+ # -- raw variants (for contract tests that need status/headers) ------
138
+ def raw(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
139
+ return self._get(path, params)
140
+
141
+
142
+ # -- time helpers --------------------------------------------------------
143
+ def now_epoch() -> int:
144
+ return int(datetime.now(tz=timezone.utc).timestamp())
145
+
146
+
147
+ def to_epoch(dt: datetime) -> int:
148
+ if dt.tzinfo is None:
149
+ dt = dt.replace(tzinfo=timezone.utc)
150
+ return int(dt.timestamp())
151
+
152
+
153
+ def iter_windows(start: int, end: int, window: int = MAX_WINDOW_SECONDS) -> Iterator[tuple[int, int]]:
154
+ """Yield [s, e) chunks no larger than `window`, covering [start, end].
155
+
156
+ Used to walk an arbitrarily long history past the API's 6-month per-request
157
+ cap. Windows are half-open back-to-back so readings are neither dropped nor
158
+ double-counted across the seam.
159
+ """
160
+ if end <= start:
161
+ return
162
+ cur = start
163
+ while cur < end:
164
+ nxt = min(cur + window, end)
165
+ yield cur, nxt
166
+ cur = nxt
@@ -0,0 +1,449 @@
1
+ Metadata-Version: 2.4
2
+ Name: broodminder-data
3
+ Version: 0.1.0
4
+ Summary: Unified Python client, CLI, bulk export, periodic delta sync, OpenAPI 3.1 spec, and MCP server for BroodMinder beehive data.
5
+ Author: Don Petry
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/petry-projects/broodminder-data
8
+ Project-URL: Documentation, https://github.com/petry-projects/broodminder-data#readme
9
+ Project-URL: Issues, https://github.com/petry-projects/broodminder-data/issues
10
+ Project-URL: Discussions, https://github.com/petry-projects/broodminder-data/discussions
11
+ Project-URL: Repository, https://github.com/petry-projects/broodminder-data.git
12
+ Keywords: broodminder,beekeeping,iot,data-export,api,mcp,openapi,cli
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: python-dotenv>=1.0
27
+ Provides-Extra: mcp
28
+ Requires-Dist: mcp>=1.2.0; python_version >= "3.10" and extra == "mcp"
29
+ Provides-Extra: build
30
+ Requires-Dist: build>=1.0; extra == "build"
31
+ Requires-Dist: twine>=5.0; extra == "build"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=8.0; extra == "dev"
34
+ Requires-Dist: build>=1.0; extra == "dev"
35
+ Requires-Dist: twine>=5.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # 🐝 broodminder-data
39
+
40
+ [![CI](https://github.com/petry-projects/broodminder-data/actions/workflows/ci.yml/badge.svg)](https://github.com/petry-projects/broodminder-data/actions/workflows/ci.yml)
41
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=petry-projects_broodminder-export2&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=petry-projects_broodminder-export2)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
43
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
44
+ [![PyPI version](https://img.shields.io/pypi/v/broodminder-data.svg)](https://pypi.org/project/broodminder-data/)
45
+ [![OpenAPI 3.1](https://img.shields.io/badge/OpenAPI-3.1-brightgreen.svg)](openapi.yaml)
46
+ [![MCP Ready](https://img.shields.io/badge/MCP-Ready-purple.svg)](https://modelcontextprotocol.io/)
47
+
48
+ **Make [BroodMinder](https://broodminder.com) beehive data easily accessible across diverse consumption use cases — bulk export, periodic delta sync, OpenAPI 3.1, and AI agents via MCP.**
49
+
50
+ `broodminder-data` (formerly `broodminder-export`) provides a unified developer platform for BroodMinder beehive sensor telemetry (internal/ambient temperature, relative humidity, scale weight, swarm indicators, acoustics, radar, and inspection notes). Similar to [`empower-personal-dashboard`](https://github.com/petry-projects/empower-personal-dashboard), this project provides a **published OpenAPI 3.1 specification**, a **Python CLI and client SDK**, and a **Model Context Protocol (MCP) server** for AI agents.
51
+
52
+ > [!IMPORTANT]
53
+ > **Unofficial.** Not affiliated with or endorsed by BroodMinder. It uses the
54
+ > public External User API with *your own* API key. The bundled
55
+ > [OpenAPI spec](openapi/broodminder-openapi.yaml) (symlinked to [`openapi.yaml`](openapi.yaml))
56
+ > is reverse-engineered from observed live behavior — corrections via PR are welcome.
57
+
58
+ ---
59
+
60
+ ## 5 Core Consumption Modalities
61
+
62
+ ```
63
+ ┌──────────────────────────────────────────────┐
64
+ │ BroodMinder Cloud API │
65
+ └──────────────────────┬───────────────────────┘
66
+ │
67
+ ┌───────────▼───────────┐
68
+ │ broodminder-data │
69
+ └───────────┬───────────┘
70
+ │
71
+ ┌──────────────────┬───────────────┼───────────────┬──────────────────┐
72
+ ▼ ▼ ▼ ▼ ▼
73
+ 📦 Bulk Export 🔄 Delta Sync 📜 OpenAPI 3.1 🤖 MCP Server 🐍 Python SDK / CLI
74
+ Full history Incremental Published spec Claude, Cursor, Typed models &
75
+ JSON / CSV / NDJSON catch-up cron & Redocly docs Antigravity CLI scriptable client
76
+ ```
77
+
78
+ 1. **📦 Bulk Historical Export**: Walks your entire apiary → hive → device hierarchy with rate-limit-aware, resumable 180-day windowing to extract complete multi-year histories into compressed JSON, NDJSON, and CSV.
79
+ 2. **🔄 Periodic Delta Sync**: Lightweight, incremental polling engine designed for regular cron or background services, fetching only new readings since the last checkpoint while buffering for late-arriving BLE uploads.
80
+ 3. **📜 Published OpenAPI 3.1 Specification**: Formal, validated OpenAPI 3.1 contract covering all observed endpoints, query parameters, error responses (including HTTP 412 auth responses), and canonical telemetry schemas.
81
+ 4. **🤖 Model Context Protocol (MCP) Server**: Native MCP integration (`broodminder-mcp`) connecting AI agents (**Claude Desktop, Antigravity CLI, Cursor, Windsurf, Claude Code**) directly to hive metrics, temperature trends, weight deltas, and notes.
82
+ 5. **🐍 Unified Python Client Library & CLI**: Strongly-typed domain models, offline sandbox mode, and a standalone `broodminder` CLI.
83
+
84
+ ---
85
+
86
+ ## Architecture Discussions & Roadmap
87
+
88
+ We are tracking each expanded capability in GitHub Discussions. Join the conversation:
89
+
90
+ - 💬 [**Discussion #148: Periodic Delta Sync Engine for Incremental Telemetry & Continuous Ingestion**](https://github.com/petry-projects/broodminder-data/discussions/148)
91
+ - 💬 [**Discussion #149: Published OpenAPI 3.1 Specification & Interactive Documentation (Redocly/Swagger)**](https://github.com/petry-projects/broodminder-data/discussions/149)
92
+ - 💬 [**Discussion #150: Model Context Protocol (MCP) Server for Hive Monitoring & AI Agent Integration**](https://github.com/petry-projects/broodminder-data/discussions/150)
93
+ - 💬 [**Discussion #151: Unified Python Client SDK and Standalone CLI Library**](https://github.com/petry-projects/broodminder-data/discussions/151)
94
+
95
+ ---
96
+
97
+ ## Table of Contents
98
+
99
+ - [Features](#features)
100
+ - [What You Get](#what-you-get)
101
+ - [Get an API Key](#get-an-api-key)
102
+ - [Installation](#installation)
103
+ - [Configuration](#configuration)
104
+ - [Quickstart Usage](#quickstart-usage)
105
+ - [1. Discover Account Topology](#1-discover-account-topology)
106
+ - [2. Bulk History Export](#2-bulk-history-export)
107
+ - [3. Incremental Catch-up Sync](#3-incremental-catch-up-sync)
108
+ - [4. Build Analysis-Ready Datasets](#4-build-analysis-ready-datasets)
109
+ - [OpenAPI 3.1 Specification & Interactive Docs](#openapi-31-specification--interactive-docs)
110
+ - [Model Context Protocol (MCP) Server](#model-context-protocol-mcp-server)
111
+ - [Python SDK Usage](#python-sdk-usage)
112
+ - [PyPI Packaging & Automated Publishing](#pypi-packaging--automated-publishing)
113
+ - [Output Files & Schema](#output-files--schema)
114
+ - [API Behavior & Rate Limits](#api-behavior--rate-limits)
115
+ - [Testing & Quality Gates](#testing--quality-gates)
116
+ - [Project Structure](#project-structure)
117
+ - [Privacy & Security](#privacy--security)
118
+ - [Contributing](#contributing)
119
+ - [License](#license)
120
+
121
+ ---
122
+
123
+ ## Features
124
+
125
+ - 📦 **Complete export** — walks every apiary → hive → device and pulls all readings and notes across your entire history.
126
+ - 🔁 **Resumable** — checkpoints each time window; stop and re-run anytime and it skips what's already fetched.
127
+ - 🚦 **Rate-limit-aware** — respects the ~1000 calls/day cap, self-throttles, and resumes cleanly after a `429`.
128
+ - 🧹 **Idempotent outputs** — de-duplicates overlapping windows, so re-runs never double-count.
129
+ - 🗜️ **Compact** — raw and flattened outputs are gzipped (a multi-year, 90-hive account is tens of MB).
130
+ - 📜 **OpenAPI 3.1 spec** — formal machine-readable API definition with Redocly validation.
131
+ - 🤖 **Agent-ready** — MCP server architecture for conversational hive analysis and automated inspections.
132
+ - 🧪 **Contract-tested** — live contract test suite pins the API's real behavior and acts as a canary when upstream changes.
133
+ - 🔌 **Reusable client** — `bm/client.py` is transport-clean and easy to lift into a notebook, script, or MCP server.
134
+
135
+ ---
136
+
137
+ ## What You Get
138
+
139
+ A flattened, analysis-ready row per reading:
140
+
141
+ | field | description |
142
+ |---|---|
143
+ | `apiaryId`, `apiaryName` | apiary the hive belongs to |
144
+ | `hiveId`, `hiveName` | hive identity |
145
+ | `positionID`, `deviceId` | sensor position + device (`deviceId` is the unique series key) |
146
+ | `timestamp`, `datetime` | Unix epoch seconds (UTC) + ISO-8601 string |
147
+ | `batteryLevel`, `chargeRemaining` | device power (nullable; the two alternate) |
148
+ | `m_temperature` | temperature (all devices) |
149
+ | `m_humidity` | relative humidity (humidity-capable devices) |
150
+ | `m_weight` | scale weight (hives with a scale) |
151
+ | `m_swarmState` | BroodMinder swarm indicator |
152
+ | `m_audio` | acoustic reading (audio-capable devices; unit/scale unconfirmed) |
153
+ | `m_radar` | movement/activity indicator (radar-equipped devices) |
154
+
155
+ > Metric presence varies by device type — temperature is near-universal; weight
156
+ > appears only on hives with a scale; audio and radar appear on specialized monitors.
157
+
158
+ ---
159
+
160
+ ## Get an API Key
161
+
162
+ The External User API is in alpha. Request a key from BroodMinder
163
+ ([support@broodminder.com](mailto:support@broodminder.com)). The key is tied to your account and only authorizes
164
+ access to your own data.
165
+
166
+ ---
167
+
168
+ ## Installation
169
+
170
+ ### From PyPI
171
+ ```bash
172
+ # Core package
173
+ pip install broodminder-data
174
+
175
+ # With Model Context Protocol (MCP) agent support:
176
+ pip install "broodminder-data[mcp]"
177
+ ```
178
+
179
+ ### From Source (Local Development)
180
+ ```bash
181
+ git clone https://github.com/petry-projects/broodminder-data.git
182
+ cd broodminder-data
183
+
184
+ python3 -m venv .venv
185
+ .venv/bin/pip install -r requirements.txt
186
+ .venv/bin/pip install -e ".[mcp,dev]"
187
+ ```
188
+
189
+ Requires **Python 3.10+**.
190
+
191
+ ---
192
+
193
+ ## Configuration
194
+
195
+ ```bash
196
+ cp .env.example .env
197
+ ```
198
+
199
+ Edit `.env` and paste your key:
200
+
201
+ ```dotenv
202
+ BROODMINDER_API_KEY=your-api-key-here
203
+ BROODMINDER_BASE_URL=https://external-api.mybroodminder.com
204
+ ```
205
+
206
+ `.env` is git-ignored and never leaves your machine.
207
+
208
+ ---
209
+
210
+ ## Quickstart Usage
211
+
212
+ ### 1. Discover Account Topology
213
+ Confirm auth and see your apiaries, hives, and a sensor data sample:
214
+ ```bash
215
+ .venv/bin/python scripts/discover.py
216
+ ```
217
+
218
+ ### 2. Bulk History Export
219
+ Pull historical telemetry (resumable; respects the daily quota):
220
+ ```bash
221
+ .venv/bin/python scripts/extract_all.py --start 2025-01-01
222
+ ```
223
+
224
+ Options:
225
+ | flag | default | purpose |
226
+ |---|---|---|
227
+ | `--start YYYY-MM-DD` | `2021-01-01` | history start |
228
+ | `--end YYYY-MM-DD` | today (UTC) | history end |
229
+ | `--catchup` | off | resume forward from each hive's latest completed window in `manifest.json` |
230
+ | `--window-days N` | `180` | request window size (API caps at ~6 months) |
231
+ | `--apiary NAME\|ID` | all | limit to one apiary (repeatable) |
232
+ | `--max-calls N` | `900` | stop before this many API calls (daily-cap guard) |
233
+ | `--reverse` | off | walk newest→oldest (for backfilling) |
234
+ | `--stop-after-empty N` | `0` | with `--reverse`, stop a hive after N empty windows |
235
+ | `--no-notes` | off | skip the notes endpoint |
236
+
237
+ ### 3. Incremental Catch-up Sync
238
+ Resume forward from each hive's latest extracted window:
239
+ ```bash
240
+ .venv/bin/python scripts/extract_all.py --catchup
241
+ ```
242
+
243
+ ### 4. Build Analysis-Ready Datasets
244
+ Convert raw windows into clean, de-duplicated NDJSON and CSV:
245
+ ```bash
246
+ .venv/bin/python scripts/flatten.py --merge
247
+ ```
248
+
249
+ ---
250
+
251
+ ## OpenAPI 3.1 Specification & Interactive Docs
252
+
253
+ `broodminder-data` maintains a formal [OpenAPI 3.1 specification](openapi/broodminder-openapi.yaml) (also accessible via the root symlink [`openapi.yaml`](openapi.yaml)).
254
+
255
+ ### Local Linting & Preview
256
+ Using [Redocly CLI](https://redocly.com/docs/cli/):
257
+
258
+ ```bash
259
+ # Lint specification against OpenAPI 3.1 rules
260
+ npx @redocly/cli lint openapi.yaml
261
+
262
+ # Launch interactive documentation preview server
263
+ npx @redocly/cli preview-docs openapi.yaml
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Model Context Protocol (MCP) Server
269
+
270
+ Connect your hive data directly to AI agents (**Claude Desktop, Antigravity CLI, Cursor, Windsurf, Claude Code**):
271
+
272
+ ### Agent Configuration (`claude_desktop_config.json` or `mcp.json`)
273
+
274
+ ```json
275
+ {
276
+ "mcpServers": {
277
+ "broodminder": {
278
+ "command": "python3",
279
+ "args": ["-m", "bm.mcp_server"],
280
+ "env": {
281
+ "BROODMINDER_API_KEY": "your-api-key-here"
282
+ }
283
+ }
284
+ }
285
+ }
286
+ ```
287
+
288
+ ### Core MCP Tools
289
+ - `get_apiary_summary`: High-level inventory of apiaries, hives, and device counts.
290
+ - `get_hive_status`: Latest sensor telemetry (brood temperature, ambient temperature, humidity, weight).
291
+ - `get_telemetry_trends`: Time-series rollups (min, max, mean, delta) over specified lookback windows.
292
+ - `get_hive_notes`: Recent inspection notes, treatments, and queen observations.
293
+ - `get_device_health`: Battery levels and sync freshness across sensors.
294
+
295
+ ---
296
+
297
+ ## Python SDK Usage
298
+
299
+ ```python
300
+ from bm.client import BroodMinderClient
301
+
302
+ # Automatically reads BROODMINDER_API_KEY from environment or .env
303
+ client = BroodMinderClient()
304
+
305
+ # List apiaries and hives
306
+ apiaries = client.get_apiaries()
307
+ for apiary in apiaries:
308
+ print(f"Apiary: {apiary['name']} (ID: {apiary['apiary_id']})")
309
+ hives = client.get_hives(apiary_id=apiary['apiary_id'])
310
+ for hive in hives:
311
+ print(f" - Hive: {hive['name']}")
312
+
313
+ # Fetch time-series readings for a device (epoch seconds)
314
+ readings = client.get_device_readings(
315
+ device_id="42:11:22:33:44:55",
316
+ start=1704067200, # 2024-01-01T00:00:00Z
317
+ end=1706745600, # 2024-02-01T00:00:00Z
318
+ )
319
+ print(f"Fetched {len(readings)} readings")
320
+ ```
321
+
322
+ ---
323
+
324
+ ## PyPI Packaging & Automated Publishing
325
+
326
+ `broodminder-data` adopts the automated, tokenless **PyPI Trusted Publishing (OIDC)** approach pioneered in [`don-petry/brand-ops`](https://github.com/don-petry/brand-ops).
327
+
328
+ ### 1. Tokenless Trusted Publishing Architecture
329
+
330
+ Releases publish directly from GitHub Actions without storing long-lived, sensitive API tokens:
331
+ - GitHub Actions exchanges its cryptographic OIDC ID token with PyPI for a short-lived upload token.
332
+ - PyPI validates the repository (`petry-projects/broodminder-data`), workflow (`publish.yml`), and environment (`pypi`).
333
+
334
+ ### 2. Onboarding Steps (First Release Setup)
335
+
336
+ Before publishing the first release, the account owner registers a **Pending Publisher** on PyPI:
337
+ 1. Log in to [pypi.org/manage/account/publishing/](https://pypi.org/manage/account/publishing/).
338
+ 2. Under **"Add a pending publisher"**, enter:
339
+ - **PyPI Project Name:** `broodminder-data`
340
+ - **Owner:** `petry-projects`
341
+ - **Repository name:** `broodminder-data`
342
+ - **Workflow name:** `publish.yml`
343
+ - **Environment name:** `pypi`
344
+ 3. Click **"Add publisher"**.
345
+
346
+ ### 3. Local Onboarding & Verification
347
+
348
+ Run the onboarding tool to inspect registry availability, build the sdist and wheel, and verify package metadata:
349
+
350
+ ```bash
351
+ # Probe PyPI status, build sdist/wheel, and run twine verification
352
+ python scripts/pypi_onboard.py
353
+ ```
354
+
355
+ ### 4. Automated Publishing Workflow
356
+
357
+ - **Automatic:** Creating a GitHub Release automatically builds and publishes packages to PyPI via `.github/workflows/publish.yml`.
358
+ - **Manual Trigger (with Dry Run):** You can also run the workflow manually via `workflow_dispatch` with `dry_run: true` (default) to test artifact generation without releasing.
359
+
360
+ ---
361
+
362
+ ## Output Files & Schema
363
+
364
+ Extracted data is saved under `data/extract/` (git-ignored):
365
+
366
+ | file | contents |
367
+ |---|---|
368
+ | `raw/<hiveId>/<start>-<end>.readings.json.gz` | lossless raw responses (replay source) |
369
+ | `raw/<hiveId>/<start>-<end>.notes.json.gz` | lossless raw notes |
370
+ | `manifest.json` | per-window progress + row counts (drives resume) |
371
+ | `readings.ndjson.gz` | one JSON object per reading (analysis-ready) |
372
+ | `readings.csv.gz` | same, columnar |
373
+ | `notes.ndjson` | one object per note |
374
+ | `coverage.json` | per-hive earliest/latest reading + counts |
375
+
376
+ ---
377
+
378
+ ## API Behavior & Rate Limits
379
+
380
+ The machine-readable description is in [`openapi.yaml`](openapi.yaml). Notable quirks handled automatically:
381
+
382
+ - **Authentication:** `X-Api-Key` header. Missing or invalid keys return **HTTP 412** (not 401/403).
383
+ - **Time Windows:** Maximum ~6 months per request — auto-chunked.
384
+ - **No Pagination:** Each window is a single JSON array payload.
385
+ - **Rate Limit:** ~1,000 calls per UTC day with no `Retry-After` header — the client tracks calls, self-throttles, and catches `429` responses cleanly.
386
+
387
+ ---
388
+
389
+ ## Testing & Quality Gates
390
+
391
+ ```bash
392
+ # Run unit & offline tests
393
+ .venv/bin/python -m pytest
394
+
395
+ # Byte-compile verification
396
+ python3 -m compileall bm scripts tests
397
+ ```
398
+
399
+ - **Offline tests (`tests/test_offline.py`, `tests/test_scripts_refactor.py`):** Run fast and hermetically without network access.
400
+ - **Live contract tests (`tests/test_contract.py`):** Automatically run when `BROODMINDER_API_KEY` is present to verify live API compatibility; skip gracefully otherwise.
401
+ - **OpenAPI validation:** `npx @redocly/cli lint openapi.yaml`.
402
+
403
+ ---
404
+
405
+ ## Project Structure
406
+
407
+ ```
408
+ broodminder-data/
409
+ ├── bm/
410
+ │ ├── __init__.py
411
+ │ └── client.py # Reusable BroodMinderClient (auth, retry, windowing)
412
+ ├── scripts/
413
+ │ ├── discover.py # Auth check + account topology/schema sample
414
+ │ ├── extract_all.py # Resumable, budget-aware extraction (--catchup)
415
+ │ ├── flatten.py # Raw → NDJSON/CSV/coverage (--merge)
416
+ │ ├── cron_sync.sh # Routine unattended forward catch-up sync
417
+ │ └── cron_backfill.sh # Initial unattended multi-day backfill
418
+ ├── tests/
419
+ │ ├── conftest.py
420
+ │ ├── test_offline.py # Fast deterministic unit tests
421
+ │ ├── test_scripts_refactor.py # Script unit test coverage
422
+ │ └── test_contract.py # Live contract tests (skip without key)
423
+ ├── openapi/
424
+ │ └── broodminder-openapi.yaml # OpenAPI 3.1 specification
425
+ ├── openapi.yaml -> openapi/broodminder-openapi.yaml # Root symlink
426
+ ├── redocly.yaml # Redocly linting & preview configuration
427
+ ├── requirements.txt # Runtime dependencies
428
+ └── pyproject.toml # Build configuration & metadata
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Privacy & Security
434
+
435
+ `.env` (your API key) and `data/` (your extracted hive data) are **git-ignored** and never leave your machine.
436
+ All test fixtures use synthetic or anonymized values. Please **never** paste an API key or raw hive telemetry into an issue, PR, or discussion.
437
+
438
+ ---
439
+
440
+ ## Contributing
441
+
442
+ See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and the [Code of Conduct](CODE_OF_CONDUCT.md).
443
+ Check out open [Discussions](https://github.com/petry-projects/broodminder-data/discussions) to weigh in on upcoming features and architectural decisions.
444
+
445
+ ---
446
+
447
+ ## License
448
+
449
+ [MIT](LICENSE) © Petry Projects.
@@ -0,0 +1,7 @@
1
+ bm/__init__.py,sha256=crOR6bf1V0uL_CNHzvjEBsnSZUqc9dT3qcyCAtr9kZE,190
2
+ bm/client.py,sha256=iudf6gzIoVw4GcnJJNo64111m9fGWiXpAu8pNuiudpM,6184
3
+ broodminder_data-0.1.0.dist-info/licenses/LICENSE,sha256=QcddDB-VvNVjeMV_D_2LR1FD3dfi3ZpIBUk2E82e3gQ,1065
4
+ broodminder_data-0.1.0.dist-info/METADATA,sha256=LDGbF6GepxsxM8FIWWZIxz3EHh2EfZPLOCLMUxwTsX8,19651
5
+ broodminder_data-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ broodminder_data-0.1.0.dist-info/top_level.txt,sha256=iiaL5oApc3BPAF4NCpcmWjqqgzZL1il3pMz2C9Wpkm8,3
7
+ broodminder_data-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) petry-projects
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
+ bm