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
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
|
+
[](https://github.com/petry-projects/broodminder-data/actions/workflows/ci.yml)
|
|
41
|
+
[](https://sonarcloud.io/summary/new_code?id=petry-projects_broodminder-export2)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](https://www.python.org/)
|
|
44
|
+
[](https://pypi.org/project/broodminder-data/)
|
|
45
|
+
[](openapi.yaml)
|
|
46
|
+
[](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,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
|