Arrcus commited on
Commit
2db5c7d
·
1 Parent(s): a640d77

first test huggingface

Browse files
.gitignore ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Karim Hamdi
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.
__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Makes teleagriculture a Python package
api_call.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Daily data fetch for Teleagriculture kits.
2
+
3
+ Usage:
4
+ python api_call.py --kit-id 1001 --format csv
5
+
6
+ Env:
7
+ - KIT_API_KEY: optional Bearer token for the API
8
+ - KITS_API_BASE: override base URL (default https://kits.teleagriculture.org/api)
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import os
14
+ from datetime import datetime, timedelta
15
+ from pathlib import Path
16
+ from typing import List, Optional
17
+
18
+ import pandas as pd
19
+
20
+ # Import utility function and config
21
+ from utils import get_kit_measurements_df, BASE_URL
22
+
23
+
24
+ def get_last_day_data(kit_id: int) -> pd.DataFrame:
25
+ """Fetches all sensor data for a given kit from the last 24 hours."""
26
+ print(f"API base: {BASE_URL}")
27
+ print(f"Fetching last day's measurements for kit {kit_id}...\n")
28
+
29
+ # Fetch all data, sensors will be discovered automatically
30
+ df = get_kit_measurements_df(kit_id)
31
+
32
+ if df.empty or 'timestamp' not in df.columns:
33
+ print("No data or timestamp column found.")
34
+ return pd.DataFrame()
35
+
36
+ # Filter for the last 24 hours
37
+ # The timestamp column is already converted to timezone-aware datetimes in get_kit_measurements_df
38
+ one_day_ago = pd.Timestamp.utcnow() - timedelta(days=1)
39
+ last_day_df = df[df['timestamp'] >= one_day_ago].copy()
40
+
41
+ print(f"Fetched rows from the last day: {len(last_day_df)}")
42
+ if not last_day_df.empty:
43
+ try:
44
+ # Recalculate 'value' as numeric, coercing errors
45
+ last_day_df['value'] = pd.to_numeric(last_day_df['value'], errors='coerce')
46
+
47
+ print("Summary statistics for the last day:")
48
+ # Group by sensor and calculate statistics
49
+ summary = last_day_df.groupby('sensor')['value'].agg(['mean', 'min', 'max', 'count']).round(2)
50
+ print(summary)
51
+
52
+ except Exception as e:
53
+ print(f"Could not generate summary statistics: {e}")
54
+
55
+ return last_day_df
56
+
57
+
58
+ def parse_args() -> argparse.Namespace:
59
+ p = argparse.ArgumentParser(description="Fetch all measurements for a Teleagriculture kit and save to disk.")
60
+ p.add_argument("--kit-id", type=int, required=True, help="Numeric kit id to fetch (e.g., 1001)")
61
+ p.add_argument(
62
+ "--sensors",
63
+ type=str,
64
+ default=None,
65
+ help="Comma-separated sensor names to limit (default: discover all sensors on the kit)",
66
+ )
67
+ p.add_argument("--page-size", type=int, default=100, help="Page size for pagination (default: 100)")
68
+ p.add_argument(
69
+ "--format",
70
+ choices=["csv", "parquet"],
71
+ default="csv",
72
+ help="Output format (default: csv)",
73
+ )
74
+ p.add_argument(
75
+ "--out",
76
+ type=str,
77
+ default=None,
78
+ help="Output file path. If not provided, saves under teleagriculture/data/kit_<id>_<YYYY-MM-DD>.<ext>",
79
+ )
80
+ return p.parse_args()
81
+
82
+
83
+ def main() -> int:
84
+ args = parse_args()
85
+
86
+ sensors: Optional[List[str]] = None
87
+ if args.sensors:
88
+ sensors = [s.strip() for s in args.sensors.split(",") if s.strip()]
89
+
90
+ print(f"API base: {BASE_URL}")
91
+ print(f"Fetching kit {args.kit_id} measurements...\n")
92
+ df = get_kit_measurements_df(args.kit_id, sensors=sensors, page_size=args.page_size)
93
+
94
+ print(f"Fetched rows: {len(df)}")
95
+ if not df.empty:
96
+ try:
97
+ per_sensor = df.groupby("sensor").size().sort_values(ascending=False)
98
+ print("Rows per sensor:")
99
+ for s, n in per_sensor.items():
100
+ print(f" - {s}: {n}")
101
+ except Exception:
102
+ pass
103
+
104
+ # Determine output path
105
+ ext = args.format
106
+ if args.out:
107
+ out_path = Path(args.out)
108
+ else:
109
+ dt = datetime.utcnow().strftime("%Y-%m-%d")
110
+ out_dir = Path(__file__).parent / "data"
111
+ out_path = out_dir / f"kit_{args.kit_id}_{dt}.{ext}"
112
+
113
+ out_path.parent.mkdir(parents=True, exist_ok=True)
114
+
115
+ if args.format == "csv":
116
+ df.to_csv(out_path, index=False)
117
+ print(f"\nSaved CSV -> {out_path.resolve()}")
118
+ elif args.format == "parquet":
119
+ try:
120
+ df.to_parquet(out_path, index=False)
121
+ print(f"\nSaved Parquet -> {out_path.resolve()}")
122
+ except ImportError:
123
+ print("\nParquet write failed. Please install pyarrow or fastparquet.")
124
+ return 1
125
+ except Exception as e:
126
+ print(f"\nAn error occurred while saving the Parquet file: {e}")
127
+ return 1
128
+
129
+ return 0
130
+
131
+
132
+ if __name__ == "__main__":
133
+ # Example of using the new function.
134
+ # You can run this part by uncommenting it and running the script.
135
+ # try:
136
+ # kit_id_to_test = 1001 # Replace with a valid kit ID
137
+ # last_day_data = get_last_day_data(kit_id_to_test)
138
+ # if not last_day_data.empty:
139
+ # print("\n--- Last Day Dataframe ---")
140
+ # print(last_day_data.head())
141
+ # print("--------------------------")
142
+ # except Exception as e:
143
+ # print(f"An error occurred during the example run: {e}")
144
+
145
+ raise SystemExit(main())
api_tests.ipynb ADDED
@@ -0,0 +1,1097 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "8d8da681",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Teleagriculture API Tests\n",
9
+ "\n",
10
+ "This notebook tests API endpoints to find the board with the most data points."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": 25,
16
+ "id": "45dc5eca",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "# Import required libraries\n",
21
+ "import requests\n",
22
+ "import json\n",
23
+ "import pandas as pd\n",
24
+ "import matplotlib.pyplot as plt\n",
25
+ "from typing import List, Dict, Optional\n",
26
+ "from datetime import datetime"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "markdown",
31
+ "id": "f61e398c",
32
+ "metadata": {},
33
+ "source": [
34
+ "## API Configuration\n",
35
+ "\n",
36
+ "Based on the teleagriculture project documentation, these are IoT hardware boards that send data to cloud platforms. This notebook demonstrates how to query a data platform that collects data from multiple teleagriculture boards."
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "execution_count": 26,
42
+ "id": "0f5ac5fe",
43
+ "metadata": {},
44
+ "outputs": [
45
+ {
46
+ "name": "stdout",
47
+ "output_type": "stream",
48
+ "text": [
49
+ "API: https://kits.teleagriculture.org/api\n",
50
+ "Auth: none\n"
51
+ ]
52
+ }
53
+ ],
54
+ "source": [
55
+ "# API Configuration for Teleagriculture Kits API (minimal)\n",
56
+ "BASE_URL = \"https://kits.teleagriculture.org/api\" # official kits API base\n",
57
+ "\n",
58
+ "# Optional: put KIT_API_KEY in env to POST; GETs are public per docs (but docs also mention bearer header; we support both)\n",
59
+ "import os\n",
60
+ "KIT_API_KEY = os.getenv(\"KIT_API_KEY\")\n",
61
+ "\n",
62
+ "HEADERS = {\n",
63
+ " \"Accept\": \"application/json\",\n",
64
+ "}\n",
65
+ "if KIT_API_KEY:\n",
66
+ " HEADERS[\"Authorization\"] = f\"Bearer {KIT_API_KEY}\"\n",
67
+ "\n",
68
+ "print(\"API:\", BASE_URL)\n",
69
+ "print(\"Auth:\", \"Bearer set\" if \"Authorization\" in HEADERS else \"none\")"
70
+ ]
71
+ },
72
+ {
73
+ "cell_type": "code",
74
+ "execution_count": 27,
75
+ "id": "9e43c541",
76
+ "metadata": {},
77
+ "outputs": [],
78
+ "source": [
79
+ "# Minimal helpers per official docs\n",
80
+ "from typing import Tuple, Optional\n",
81
+ "\n",
82
+ "def get_kit_info(kit_id: int) -> Optional[dict]:\n",
83
+ " url = f\"{BASE_URL}/kits/{kit_id}\"\n",
84
+ " try:\n",
85
+ " r = requests.get(url, headers=HEADERS, timeout=30)\n",
86
+ " if r.status_code == 200:\n",
87
+ " return r.json().get(\"data\")\n",
88
+ " return None\n",
89
+ " except requests.RequestException:\n",
90
+ " return None\n",
91
+ "\n",
92
+ "\n",
93
+ "def count_sensor_measurements(kit_id: int, sensor_name: str, page_size: int = 50, max_pages: int = 200) -> int:\n",
94
+ " \"\"\"Count all measurements for a kit sensor using cursor pagination.\n",
95
+ " Limits pages to avoid unbounded runs.\n",
96
+ " \"\"\"\n",
97
+ " total = 0\n",
98
+ " cursor = None\n",
99
+ " pages = 0\n",
100
+ " while pages < max_pages:\n",
101
+ " params = {\"page[size]\": str(page_size)}\n",
102
+ " if cursor:\n",
103
+ " params[\"page[cursor]\"] = cursor\n",
104
+ " url = f\"{BASE_URL}/kits/{kit_id}/{sensor_name}/measurements\"\n",
105
+ " try:\n",
106
+ " r = requests.get(url, headers=HEADERS, params=params, timeout=30)\n",
107
+ " except requests.RequestException:\n",
108
+ " break\n",
109
+ " if r.status_code == 404:\n",
110
+ " break\n",
111
+ " if r.status_code != 200:\n",
112
+ " break\n",
113
+ " try:\n",
114
+ " body = r.json()\n",
115
+ " except Exception:\n",
116
+ " break\n",
117
+ " data = body.get(\"data\")\n",
118
+ " if isinstance(data, list):\n",
119
+ " total += len(data)\n",
120
+ " else:\n",
121
+ " break\n",
122
+ " meta = body.get(\"meta\", {})\n",
123
+ " cursor = meta.get(\"next_cursor\")\n",
124
+ " pages += 1\n",
125
+ " if not cursor:\n",
126
+ " break\n",
127
+ " return total"
128
+ ]
129
+ },
130
+ {
131
+ "cell_type": "markdown",
132
+ "id": "3b944747",
133
+ "metadata": {},
134
+ "source": [
135
+ "## Fetch Boards Function\n",
136
+ "\n",
137
+ "Function to retrieve all registered teleagriculture boards from the data platform API. Each \"board\" represents a deployed IoT device collecting agricultural data."
138
+ ]
139
+ },
140
+ {
141
+ "cell_type": "markdown",
142
+ "id": "43460b20",
143
+ "metadata": {},
144
+ "source": [
145
+ "## Fetch all sensors for a kit and count in parallel\n",
146
+ "\n",
147
+ "Minimal helpers to grab all sensors from one kit and count each sensor’s datapoints concurrently."
148
+ ]
149
+ },
150
+ {
151
+ "cell_type": "code",
152
+ "execution_count": 28,
153
+ "id": "bde9a436",
154
+ "metadata": {},
155
+ "outputs": [
156
+ {
157
+ "name": "stdout",
158
+ "output_type": "stream",
159
+ "text": [
160
+ "KIT 1001 BEST {'sensor': 'NH3', 'count': 1221}\n"
161
+ ]
162
+ }
163
+ ],
164
+ "source": [
165
+ "from concurrent.futures import ThreadPoolExecutor, as_completed\n",
166
+ "\n",
167
+ "\n",
168
+ "def get_kit_sensors(kit_id: int) -> list[dict]:\n",
169
+ " kit = get_kit_info(kit_id)\n",
170
+ " if not kit:\n",
171
+ " return []\n",
172
+ " sensors = kit.get(\"sensors\") or []\n",
173
+ " # normalize: keep only id and name if present\n",
174
+ " out = []\n",
175
+ " for s in sensors:\n",
176
+ " if isinstance(s, dict) and s.get(\"name\"):\n",
177
+ " out.append({\"id\": s.get(\"id\"), \"name\": s.get(\"name\")})\n",
178
+ " return out\n",
179
+ "\n",
180
+ "\n",
181
+ "def count_all_sensors_for_kit(kit_id: int, page_size: int = 50, max_workers: int = 8) -> dict:\n",
182
+ " sensors = get_kit_sensors(kit_id)\n",
183
+ " if not sensors:\n",
184
+ " return {\"kit_id\": kit_id, \"counts\": {}, \"best\": None}\n",
185
+ "\n",
186
+ " counts: dict[str, int] = {}\n",
187
+ " best = {\"sensor\": None, \"count\": -1}\n",
188
+ "\n",
189
+ " def _worker(sname: str) -> tuple[str, int]:\n",
190
+ " c = count_sensor_measurements(kit_id, sname, page_size=page_size)\n",
191
+ " return sname, c\n",
192
+ "\n",
193
+ " with ThreadPoolExecutor(max_workers=max_workers) as ex:\n",
194
+ " futures = {ex.submit(_worker, s[\"name\"]): s[\"name\"] for s in sensors}\n",
195
+ " for fut in as_completed(futures):\n",
196
+ " sname = futures[fut]\n",
197
+ " try:\n",
198
+ " sname, c = fut.result()\n",
199
+ " counts[sname] = c\n",
200
+ " if c > best[\"count\"]:\n",
201
+ " best = {\"sensor\": sname, \"count\": c}\n",
202
+ " except Exception:\n",
203
+ " counts[sname] = 0\n",
204
+ " return {\"kit_id\": kit_id, \"counts\": counts, \"best\": best}\n",
205
+ "\n",
206
+ "# minimal run example (change the kit id here)\n",
207
+ "one_kit_result = count_all_sensors_for_kit(1001, page_size=50)\n",
208
+ "print(\"KIT\", one_kit_result[\"kit_id\"], \"BEST\", one_kit_result[\"best\"])"
209
+ ]
210
+ },
211
+ {
212
+ "cell_type": "code",
213
+ "execution_count": 29,
214
+ "id": "76457d0a",
215
+ "metadata": {},
216
+ "outputs": [
217
+ {
218
+ "name": "stdout",
219
+ "output_type": "stream",
220
+ "text": [
221
+ "📡 Board fetching function defined successfully!\n",
222
+ "🌿 Ready to query teleagriculture board data from platform API.\n"
223
+ ]
224
+ }
225
+ ],
226
+ "source": [
227
+ "def fetch_all_boards() -> List[Dict]:\n",
228
+ " \"\"\"\n",
229
+ " Fetch all registered teleagriculture boards from the data platform.\n",
230
+ " \n",
231
+ " Returns:\n",
232
+ " List[Dict]: List of board objects with metadata, or empty list if error occurs\n",
233
+ " \"\"\"\n",
234
+ " try:\n",
235
+ " # Common API endpoints for IoT platforms that might host teleagriculture data\n",
236
+ " possible_endpoints = [\n",
237
+ " \"/devices\", # Common IoT platform endpoint\n",
238
+ " \"/boards\", # Board-specific endpoint\n",
239
+ " \"/nodes\", # LoRaWAN nodes\n",
240
+ " \"/sensors\", # Sensor networks\n",
241
+ " \"/stations\" # Weather/agri stations\n",
242
+ " ]\n",
243
+ " \n",
244
+ " for endpoint in possible_endpoints:\n",
245
+ " try:\n",
246
+ " url = f\"{BASE_URL}{endpoint}\"\n",
247
+ " response = requests.get(url, headers=HEADERS, timeout=30)\n",
248
+ " \n",
249
+ " if response.status_code == 200:\n",
250
+ " data = response.json()\n",
251
+ " \n",
252
+ " # Handle different response formats\n",
253
+ " if isinstance(data, list):\n",
254
+ " boards = data\n",
255
+ " elif isinstance(data, dict):\n",
256
+ " # Try common keys for device arrays\n",
257
+ " for key in ['devices', 'boards', 'nodes', 'sensors', 'stations', 'data', 'results']:\n",
258
+ " if key in data and isinstance(data[key], list):\n",
259
+ " boards = data[key]\n",
260
+ " break\n",
261
+ " else:\n",
262
+ " boards = []\n",
263
+ " else:\n",
264
+ " boards = []\n",
265
+ " \n",
266
+ " if boards:\n",
267
+ " print(f\"✅ Successfully fetched {len(boards)} boards from {endpoint}\")\n",
268
+ " return boards\n",
269
+ " \n",
270
+ " except Exception as e:\n",
271
+ " continue # Try next endpoint\n",
272
+ " \n",
273
+ " print(\"❌ Could not find boards at any common endpoint\")\n",
274
+ " return []\n",
275
+ " \n",
276
+ " except requests.exceptions.RequestException as e:\n",
277
+ " print(f\"❌ Network error: {e}\")\n",
278
+ " return []\n",
279
+ " except json.JSONDecodeError as e:\n",
280
+ " print(f\"❌ JSON decode error: {e}\")\n",
281
+ " return []\n",
282
+ " except Exception as e:\n",
283
+ " print(f\"❌ Unexpected error: {e}\")\n",
284
+ " return []\n",
285
+ "\n",
286
+ "# Test the function (will be used later)\n",
287
+ "print(\"📡 Board fetching function defined successfully!\")\n",
288
+ "print(\"🌿 Ready to query teleagriculture board data from platform API.\")"
289
+ ]
290
+ },
291
+ {
292
+ "cell_type": "markdown",
293
+ "id": "ec646dfe",
294
+ "metadata": {},
295
+ "source": [
296
+ "## Data Point Counting Function\n",
297
+ "\n",
298
+ "Function to count sensor data points collected by each teleagriculture board. This could include temperature readings, soil moisture, humidity, light levels, etc."
299
+ ]
300
+ },
301
+ {
302
+ "cell_type": "code",
303
+ "execution_count": 30,
304
+ "id": "875bf5fc",
305
+ "metadata": {},
306
+ "outputs": [
307
+ {
308
+ "name": "stdout",
309
+ "output_type": "stream",
310
+ "text": [
311
+ "📡 Sensor data counting functions defined successfully!\n",
312
+ "🌱 Ready to analyze agricultural sensor data from teleagriculture boards.\n"
313
+ ]
314
+ }
315
+ ],
316
+ "source": [
317
+ "def count_board_data_points(board_id: str) -> int:\n",
318
+ " \"\"\"\n",
319
+ " Count sensor data points collected by a specific teleagriculture board.\n",
320
+ " \n",
321
+ " Args:\n",
322
+ " board_id (str): The ID of the teleagriculture board\n",
323
+ " \n",
324
+ " Returns:\n",
325
+ " int: Number of data points (sensor readings) collected by the board\n",
326
+ " \"\"\"\n",
327
+ " try:\n",
328
+ " # Teleagriculture boards typically send sensor data to these types of endpoints\n",
329
+ " possible_endpoints = [\n",
330
+ " f\"/devices/{board_id}/data\", # Device data endpoint\n",
331
+ " f\"/devices/{board_id}/measurements\", # Measurement endpoint \n",
332
+ " f\"/devices/{board_id}/sensors\", # Sensor readings\n",
333
+ " f\"/boards/{board_id}/readings\", # Board readings\n",
334
+ " f\"/nodes/{board_id}/uplinks\", # LoRaWAN uplink messages\n",
335
+ " f\"/stations/{board_id}/observations\" # Weather station observations\n",
336
+ " ]\n",
337
+ " \n",
338
+ " for endpoint in possible_endpoints:\n",
339
+ " try:\n",
340
+ " url = f\"{BASE_URL}{endpoint}\"\n",
341
+ " response = requests.get(url, headers=HEADERS, timeout=30)\n",
342
+ " \n",
343
+ " if response.status_code == 200:\n",
344
+ " data = response.json()\n",
345
+ " \n",
346
+ " # Handle different data formats from IoT platforms\n",
347
+ " if isinstance(data, list):\n",
348
+ " count = len(data)\n",
349
+ " elif isinstance(data, dict):\n",
350
+ " # Try common keys for sensor data arrays\n",
351
+ " for key in ['measurements', 'readings', 'data', 'sensors', 'uplinks', 'observations', 'records']:\n",
352
+ " if key in data and isinstance(data[key], list):\n",
353
+ " count = len(data[key])\n",
354
+ " break\n",
355
+ " else:\n",
356
+ " # Count sensor types if structured differently\n",
357
+ " sensor_keys = ['temperature', 'humidity', 'pressure', 'soil_moisture', 'light', 'ph', 'nitrogen']\n",
358
+ " count = sum(1 for key in sensor_keys if key in data and data[key] is not None)\n",
359
+ " \n",
360
+ " if count == 0:\n",
361
+ " count = len(data) # Fallback to total keys\n",
362
+ " else:\n",
363
+ " count = 0\n",
364
+ " \n",
365
+ " print(f\"📊 Board {board_id}: {count} data points found via {endpoint}\")\n",
366
+ " return count\n",
367
+ " \n",
368
+ " except Exception as e:\n",
369
+ " continue # Try next endpoint\n",
370
+ " \n",
371
+ " print(f\"⚠️ Could not fetch sensor data for board {board_id}\")\n",
372
+ " return 0\n",
373
+ " \n",
374
+ " except Exception as e:\n",
375
+ " print(f\"❌ Error counting data points for board {board_id}: {e}\")\n",
376
+ " return 0\n",
377
+ "\n",
378
+ "def get_board_data_counts(boards: List[Dict]) -> Dict[str, Dict]:\n",
379
+ " \"\"\"\n",
380
+ " Get sensor data counts for all teleagriculture boards.\n",
381
+ " \n",
382
+ " Args:\n",
383
+ " boards (List[Dict]): List of board/device objects from the platform\n",
384
+ " \n",
385
+ " Returns:\n",
386
+ " Dict[str, Dict]: Dictionary with board info and data counts\n",
387
+ " \"\"\"\n",
388
+ " board_stats = {}\n",
389
+ " \n",
390
+ " for board in boards:\n",
391
+ " # Handle different IoT platform object structures\n",
392
+ " board_id = (board.get('id') or board.get('device_id') or board.get('node_id') or \n",
393
+ " board.get('sensor_id') or board.get('station_id') or board.get('_id'))\n",
394
+ " \n",
395
+ " board_name = (board.get('name') or board.get('device_name') or board.get('label') or \n",
396
+ " board.get('title') or board.get('station_name') or f\"Board {board_id}\")\n",
397
+ " \n",
398
+ " # Get location info if available (common in agricultural IoT)\n",
399
+ " location = board.get('location') or board.get('coordinates') or board.get('position')\n",
400
+ " \n",
401
+ " if board_id:\n",
402
+ " data_count = count_board_data_points(str(board_id))\n",
403
+ " board_stats[board_id] = {\n",
404
+ " 'name': board_name,\n",
405
+ " 'data_count': data_count,\n",
406
+ " 'location': location,\n",
407
+ " 'board_info': board\n",
408
+ " }\n",
409
+ " else:\n",
410
+ " print(f\"⚠️ Skipping board without ID: {board}\")\n",
411
+ " \n",
412
+ " return board_stats\n",
413
+ "\n",
414
+ "print(\"📡 Sensor data counting functions defined successfully!\")\n",
415
+ "print(\"🌱 Ready to analyze agricultural sensor data from teleagriculture boards.\")"
416
+ ]
417
+ },
418
+ {
419
+ "cell_type": "markdown",
420
+ "id": "e101dd72",
421
+ "metadata": {},
422
+ "source": [
423
+ "## Find Board with Most Data Points\n",
424
+ "\n",
425
+ "Main execution logic to analyze all boards and identify the one with the most data points."
426
+ ]
427
+ },
428
+ {
429
+ "cell_type": "code",
430
+ "execution_count": 31,
431
+ "id": "2d6d95de",
432
+ "metadata": {},
433
+ "outputs": [
434
+ {
435
+ "name": "stdout",
436
+ "output_type": "stream",
437
+ "text": [
438
+ "🚀 Starting board analysis...\n",
439
+ "==================================================\n",
440
+ "📋 Fetching all boards...\n",
441
+ "❌ Could not find boards at any common endpoint\n",
442
+ "❌ No boards found or error occurred. Check your API configuration.\n",
443
+ "❌ Could not find boards at any common endpoint\n",
444
+ "❌ No boards found or error occurred. Check your API configuration.\n"
445
+ ]
446
+ }
447
+ ],
448
+ "source": [
449
+ "def find_board_with_most_data():\n",
450
+ " \"\"\"\n",
451
+ " Main function to find the board with the most data points.\n",
452
+ " \"\"\"\n",
453
+ " print(\"🚀 Starting board analysis...\")\n",
454
+ " print(\"=\" * 50)\n",
455
+ " \n",
456
+ " # Step 1: Fetch all boards\n",
457
+ " print(\"📋 Fetching all boards...\")\n",
458
+ " boards = fetch_all_boards()\n",
459
+ " \n",
460
+ " if not boards:\n",
461
+ " print(\"❌ No boards found or error occurred. Check your API configuration.\")\n",
462
+ " return None\n",
463
+ " \n",
464
+ " print(f\"✅ Found {len(boards)} boards\")\n",
465
+ " print()\n",
466
+ " \n",
467
+ " # Step 2: Count data points for each board\n",
468
+ " print(\"📊 Counting data points for each board...\")\n",
469
+ " board_stats = get_board_data_counts(boards)\n",
470
+ " \n",
471
+ " if not board_stats:\n",
472
+ " print(\"❌ Could not get data counts for any boards.\")\n",
473
+ " return None\n",
474
+ " \n",
475
+ " # Step 3: Find the board with the most data points\n",
476
+ " max_board_id = max(board_stats.keys(), key=lambda k: board_stats[k]['data_count'])\n",
477
+ " max_board = board_stats[max_board_id]\n",
478
+ " \n",
479
+ " print()\n",
480
+ " print(\"🏆 RESULTS\")\n",
481
+ " print(\"=\" * 50)\n",
482
+ " print(f\"Board with most data points:\")\n",
483
+ " print(f\" 📋 Name: {max_board['name']}\")\n",
484
+ " print(f\" 🆔 ID: {max_board_id}\")\n",
485
+ " print(f\" 📊 Data Points: {max_board['data_count']}\")\n",
486
+ " print()\n",
487
+ " \n",
488
+ " # Summary of all boards\n",
489
+ " print(\"📋 All Boards Summary:\")\n",
490
+ " print(\"-\" * 30)\n",
491
+ " sorted_boards = sorted(board_stats.items(), key=lambda x: x[1]['data_count'], reverse=True)\n",
492
+ " \n",
493
+ " for i, (board_id, stats) in enumerate(sorted_boards, 1):\n",
494
+ " emoji = \"🥇\" if i == 1 else \"🥈\" if i == 2 else \"🥉\" if i == 3 else \"📋\"\n",
495
+ " print(f\"{emoji} {stats['name']}: {stats['data_count']} data points\")\n",
496
+ " \n",
497
+ " return {\n",
498
+ " 'winner': max_board,\n",
499
+ " 'winner_id': max_board_id,\n",
500
+ " 'all_stats': board_stats\n",
501
+ " }\n",
502
+ "\n",
503
+ "# Execute the analysis\n",
504
+ "result = find_board_with_most_data()"
505
+ ]
506
+ },
507
+ {
508
+ "cell_type": "markdown",
509
+ "id": "b191ccdf",
510
+ "metadata": {},
511
+ "source": [
512
+ "## Data Visualization\n",
513
+ "\n",
514
+ "Create charts and detailed analysis of the board data points."
515
+ ]
516
+ },
517
+ {
518
+ "cell_type": "markdown",
519
+ "id": "30486ada",
520
+ "metadata": {},
521
+ "source": [
522
+ "## Sensor Scan: IDs 1001–1060\n",
523
+ "\n",
524
+ "Iterate over sensor IDs 1001 to 1060, query the platform API, and find the sensor with the most datapoints."
525
+ ]
526
+ },
527
+ {
528
+ "cell_type": "code",
529
+ "execution_count": 32,
530
+ "id": "e860c05e",
531
+ "metadata": {},
532
+ "outputs": [
533
+ {
534
+ "name": "stdout",
535
+ "output_type": "stream",
536
+ "text": [
537
+ "kit 1001 sensor ftTemp: 1219\n",
538
+ "kit 1001 sensor gbHum: 1219\n",
539
+ "kit 1001 sensor gbHum: 1219\n",
540
+ "kit 1001 sensor gbTemp: 1219\n",
541
+ "kit 1001 sensor gbTemp: 1219\n",
542
+ "kit 1001 sensor Moist: 1219\n",
543
+ "kit 1001 sensor Moist: 1219\n",
544
+ "kit 1001 sensor CO: 1221\n",
545
+ "kit 1001 sensor CO: 1221\n",
546
+ "kit 1001 sensor NO2: 1221\n",
547
+ "kit 1001 sensor NO2: 1221\n",
548
+ "kit 1001 sensor NH3: 1221\n",
549
+ "kit 1001 sensor NH3: 1221\n",
550
+ "kit 1001 sensor C3H8: 1221\n",
551
+ "kit 1001 sensor C3H8: 1221\n",
552
+ "kit 1001 sensor C4H10: 1221\n",
553
+ "kit 1001 sensor C4H10: 1221\n",
554
+ "kit 1001 sensor CH4: 1221\n",
555
+ "kit 1001 sensor CH4: 1221\n",
556
+ "kit 1001 sensor H2: 1221\n",
557
+ "kit 1001 sensor H2: 1221\n",
558
+ "kit 1001 sensor C2H5OH: 1221\n",
559
+ "kit 1001 sensor pH: 1219\n",
560
+ "kit 1001 sensor NO3: 0\n",
561
+ "kit 1001 sensor NO2_aq: 0\n",
562
+ "kit 1001 sensor GH: 0\n",
563
+ "kit 1001 sensor KH: 0\n",
564
+ "kit 1001 sensor pH_strip: 0\n",
565
+ "kit 1001 sensor Cl2: 0\n",
566
+ "kit 1002 sensor ftTemp: 1218\n",
567
+ "kit 1002 sensor gbHum: 1218\n",
568
+ "kit 1002 sensor gbTemp: 1218\n",
569
+ "kit 1002 sensor Moist: 1218\n",
570
+ "kit 1002 sensor CO: 1218\n",
571
+ "kit 1002 sensor NO2: 1218\n",
572
+ "kit 1002 sensor NH3: 1218\n",
573
+ "kit 1002 sensor C3H8: 1218\n",
574
+ "kit 1002 sensor C4H10: 1218\n",
575
+ "kit 1002 sensor CH4: 1218\n",
576
+ "kit 1002 sensor H2: 1218\n",
577
+ "kit 1002 sensor C2H5OH: 1218\n",
578
+ "kit 1002 sensor pH: 1218\n",
579
+ "kit 1002 sensor NO3: 0\n",
580
+ "kit 1002 sensor NO2_aq: 0\n",
581
+ "kit 1002 sensor GH: 0\n",
582
+ "kit 1002 sensor KH: 0\n",
583
+ "kit 1002 sensor pH_strip: 0\n",
584
+ "kit 1002 sensor Cl2: 0\n",
585
+ "kit 1002 sensor Battery: 663\n",
586
+ "kit 1002 sensor temp: 1074\n",
587
+ "kit 1003 sensor pH_strip: 2\n",
588
+ "kit 1003 sensor temp2: 83\n",
589
+ "kit 1003 sensor hum: 6000\n",
590
+ "kit 1003 sensor temp: 4980\n",
591
+ "kit 1003 sensor mois: 30\n",
592
+ "kit 1003 sensor Battery: 210\n",
593
+ "kit 1004 sensor ftTemp: 60\n",
594
+ "kit 1004 sensor gbHum: 1548\n",
595
+ "kit 1004 sensor gbTemp: 1547\n",
596
+ "kit 1004 sensor Moist: 4658\n",
597
+ "kit 1004 sensor Soil Moisture: 1210\n",
598
+ "kit 1004 sensor NO2: 4658\n",
599
+ "kit 1004 sensor NH3: 4658\n",
600
+ "kit 1004 sensor C3H8: 4658\n",
601
+ "kit 1004 sensor C4H10: 4658\n",
602
+ "kit 1004 sensor CH4: 2880\n",
603
+ "kit 1004 sensor H2: 4658\n",
604
+ "kit 1004 sensor C2H5OH: 4658\n"
605
+ ]
606
+ },
607
+ {
608
+ "ename": "KeyboardInterrupt",
609
+ "evalue": "",
610
+ "output_type": "error",
611
+ "traceback": [
612
+ "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
613
+ "\u001b[31mKeyboardInterrupt\u001b[39m Traceback (most recent call last)",
614
+ "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[32]\u001b[39m\u001b[32m, line 24\u001b[39m\n\u001b[32m 21\u001b[39m best = {\u001b[33m\"\u001b[39m\u001b[33mkit_id\u001b[39m\u001b[33m\"\u001b[39m: kit_id, \u001b[33m\"\u001b[39m\u001b[33msensor\u001b[39m\u001b[33m\"\u001b[39m: name, \u001b[33m\"\u001b[39m\u001b[33mcount\u001b[39m\u001b[33m\"\u001b[39m: cnt}\n\u001b[32m 22\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m best\n\u001b[32m---> \u001b[39m\u001b[32m24\u001b[39m best = \u001b[43mfind_max_sensor_in_range\u001b[49m\u001b[43m(\u001b[49m\u001b[32;43m1001\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m1060\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpage_size\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m50\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 25\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33mRESULT\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 26\u001b[39m \u001b[38;5;28mprint\u001b[39m(best)\n",
615
+ "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[32]\u001b[39m\u001b[32m, line 18\u001b[39m, in \u001b[36mfind_max_sensor_in_range\u001b[39m\u001b[34m(start_kit, end_kit, page_size)\u001b[39m\n\u001b[32m 16\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m name:\n\u001b[32m 17\u001b[39m \u001b[38;5;28;01mcontinue\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m18\u001b[39m cnt = \u001b[43mcount_sensor_measurements\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkit_id\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpage_size\u001b[49m\u001b[43m=\u001b[49m\u001b[43mpage_size\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 19\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mkit \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mkit_id\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m sensor \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcnt\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 20\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m cnt > best[\u001b[33m\"\u001b[39m\u001b[33mcount\u001b[39m\u001b[33m\"\u001b[39m]:\n",
616
+ "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[27]\u001b[39m\u001b[32m, line 28\u001b[39m, in \u001b[36mcount_sensor_measurements\u001b[39m\u001b[34m(kit_id, sensor_name, page_size, max_pages)\u001b[39m\n\u001b[32m 26\u001b[39m url = \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mBASE_URL\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m/kits/\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mkit_id\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m/\u001b[39m\u001b[38;5;132;01m{\u001b[39;00msensor_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m/measurements\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 27\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m28\u001b[39m r = \u001b[43mrequests\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m=\u001b[49m\u001b[43mHEADERS\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mparams\u001b[49m\u001b[43m=\u001b[49m\u001b[43mparams\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m30\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 29\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m requests.RequestException:\n\u001b[32m 30\u001b[39m \u001b[38;5;28;01mbreak\u001b[39;00m\n",
617
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/site-packages/requests/api.py:73\u001b[39m, in \u001b[36mget\u001b[39m\u001b[34m(url, params, **kwargs)\u001b[39m\n\u001b[32m 62\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mget\u001b[39m(url, params=\u001b[38;5;28;01mNone\u001b[39;00m, **kwargs):\n\u001b[32m 63\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33mr\u001b[39m\u001b[33;03m\"\"\"Sends a GET request.\u001b[39;00m\n\u001b[32m 64\u001b[39m \n\u001b[32m 65\u001b[39m \u001b[33;03m :param url: URL for the new :class:`Request` object.\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 70\u001b[39m \u001b[33;03m :rtype: requests.Response\u001b[39;00m\n\u001b[32m 71\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m73\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mget\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mparams\u001b[49m\u001b[43m=\u001b[49m\u001b[43mparams\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
618
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/site-packages/requests/api.py:59\u001b[39m, in \u001b[36mrequest\u001b[39m\u001b[34m(method, url, **kwargs)\u001b[39m\n\u001b[32m 55\u001b[39m \u001b[38;5;66;03m# By using the 'with' statement we are sure the session is closed, thus we\u001b[39;00m\n\u001b[32m 56\u001b[39m \u001b[38;5;66;03m# avoid leaving sockets open which can trigger a ResourceWarning in some\u001b[39;00m\n\u001b[32m 57\u001b[39m \u001b[38;5;66;03m# cases, and look like a memory leak in others.\u001b[39;00m\n\u001b[32m 58\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m sessions.Session() \u001b[38;5;28;01mas\u001b[39;00m session:\n\u001b[32m---> \u001b[39m\u001b[32m59\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43msession\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[43m=\u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
619
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/site-packages/requests/sessions.py:589\u001b[39m, in \u001b[36mSession.request\u001b[39m\u001b[34m(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)\u001b[39m\n\u001b[32m 584\u001b[39m send_kwargs = {\n\u001b[32m 585\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mtimeout\u001b[39m\u001b[33m\"\u001b[39m: timeout,\n\u001b[32m 586\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mallow_redirects\u001b[39m\u001b[33m\"\u001b[39m: allow_redirects,\n\u001b[32m 587\u001b[39m }\n\u001b[32m 588\u001b[39m send_kwargs.update(settings)\n\u001b[32m--> \u001b[39m\u001b[32m589\u001b[39m resp = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msend\u001b[49m\u001b[43m(\u001b[49m\u001b[43mprep\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43msend_kwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 591\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m resp\n",
620
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/site-packages/requests/sessions.py:703\u001b[39m, in \u001b[36mSession.send\u001b[39m\u001b[34m(self, request, **kwargs)\u001b[39m\n\u001b[32m 700\u001b[39m start = preferred_clock()\n\u001b[32m 702\u001b[39m \u001b[38;5;66;03m# Send the request\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m703\u001b[39m r = \u001b[43madapter\u001b[49m\u001b[43m.\u001b[49m\u001b[43msend\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 705\u001b[39m \u001b[38;5;66;03m# Total elapsed time of the request (approximately)\u001b[39;00m\n\u001b[32m 706\u001b[39m elapsed = preferred_clock() - start\n",
621
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/site-packages/requests/adapters.py:667\u001b[39m, in \u001b[36mHTTPAdapter.send\u001b[39m\u001b[34m(self, request, stream, timeout, verify, cert, proxies)\u001b[39m\n\u001b[32m 664\u001b[39m timeout = TimeoutSauce(connect=timeout, read=timeout)\n\u001b[32m 666\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m667\u001b[39m resp = \u001b[43mconn\u001b[49m\u001b[43m.\u001b[49m\u001b[43murlopen\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 668\u001b[39m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 669\u001b[39m \u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[43m=\u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 670\u001b[39m \u001b[43m \u001b[49m\u001b[43mbody\u001b[49m\u001b[43m=\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbody\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 671\u001b[39m \u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m=\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m.\u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 672\u001b[39m \u001b[43m \u001b[49m\u001b[43mredirect\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 673\u001b[39m \u001b[43m \u001b[49m\u001b[43massert_same_host\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 674\u001b[39m \u001b[43m \u001b[49m\u001b[43mpreload_content\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 675\u001b[39m \u001b[43m \u001b[49m\u001b[43mdecode_content\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 676\u001b[39m \u001b[43m \u001b[49m\u001b[43mretries\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mmax_retries\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 677\u001b[39m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 678\u001b[39m \u001b[43m \u001b[49m\u001b[43mchunked\u001b[49m\u001b[43m=\u001b[49m\u001b[43mchunked\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 679\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 681\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m (ProtocolError, \u001b[38;5;167;01mOSError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m err:\n\u001b[32m 682\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mConnectionError\u001b[39;00m(err, request=request)\n",
622
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/site-packages/urllib3/connectionpool.py:787\u001b[39m, in \u001b[36mHTTPConnectionPool.urlopen\u001b[39m\u001b[34m(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)\u001b[39m\n\u001b[32m 784\u001b[39m response_conn = conn \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m release_conn \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m 786\u001b[39m \u001b[38;5;66;03m# Make the request on the HTTPConnection object\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m787\u001b[39m response = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_make_request\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 788\u001b[39m \u001b[43m \u001b[49m\u001b[43mconn\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 789\u001b[39m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 790\u001b[39m \u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 791\u001b[39m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtimeout_obj\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 792\u001b[39m \u001b[43m \u001b[49m\u001b[43mbody\u001b[49m\u001b[43m=\u001b[49m\u001b[43mbody\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 793\u001b[39m \u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m=\u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 794\u001b[39m \u001b[43m \u001b[49m\u001b[43mchunked\u001b[49m\u001b[43m=\u001b[49m\u001b[43mchunked\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 795\u001b[39m \u001b[43m \u001b[49m\u001b[43mretries\u001b[49m\u001b[43m=\u001b[49m\u001b[43mretries\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 796\u001b[39m \u001b[43m \u001b[49m\u001b[43mresponse_conn\u001b[49m\u001b[43m=\u001b[49m\u001b[43mresponse_conn\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 797\u001b[39m \u001b[43m \u001b[49m\u001b[43mpreload_content\u001b[49m\u001b[43m=\u001b[49m\u001b[43mpreload_content\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 798\u001b[39m \u001b[43m \u001b[49m\u001b[43mdecode_content\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdecode_content\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 799\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mresponse_kw\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 800\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 802\u001b[39m \u001b[38;5;66;03m# Everything went great!\u001b[39;00m\n\u001b[32m 803\u001b[39m clean_exit = \u001b[38;5;28;01mTrue\u001b[39;00m\n",
623
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/site-packages/urllib3/connectionpool.py:534\u001b[39m, in \u001b[36mHTTPConnectionPool._make_request\u001b[39m\u001b[34m(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)\u001b[39m\n\u001b[32m 532\u001b[39m \u001b[38;5;66;03m# Receive the response from the server\u001b[39;00m\n\u001b[32m 533\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m534\u001b[39m response = \u001b[43mconn\u001b[49m\u001b[43m.\u001b[49m\u001b[43mgetresponse\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 535\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m (BaseSSLError, \u001b[38;5;167;01mOSError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 536\u001b[39m \u001b[38;5;28mself\u001b[39m._raise_timeout(err=e, url=url, timeout_value=read_timeout)\n",
624
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/site-packages/urllib3/connection.py:516\u001b[39m, in \u001b[36mHTTPConnection.getresponse\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 513\u001b[39m _shutdown = \u001b[38;5;28mgetattr\u001b[39m(\u001b[38;5;28mself\u001b[39m.sock, \u001b[33m\"\u001b[39m\u001b[33mshutdown\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m 515\u001b[39m \u001b[38;5;66;03m# Get the response from http.client.HTTPConnection\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m516\u001b[39m httplib_response = \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mgetresponse\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 518\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 519\u001b[39m assert_header_parsing(httplib_response.msg)\n",
625
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/http/client.py:1430\u001b[39m, in \u001b[36mHTTPConnection.getresponse\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 1428\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 1429\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1430\u001b[39m \u001b[43mresponse\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbegin\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1431\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mConnectionError\u001b[39;00m:\n\u001b[32m 1432\u001b[39m \u001b[38;5;28mself\u001b[39m.close()\n",
626
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/http/client.py:331\u001b[39m, in \u001b[36mHTTPResponse.begin\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 329\u001b[39m \u001b[38;5;66;03m# read until we get a non-100 response\u001b[39;00m\n\u001b[32m 330\u001b[39m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m331\u001b[39m version, status, reason = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_read_status\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 332\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m status != CONTINUE:\n\u001b[32m 333\u001b[39m \u001b[38;5;28;01mbreak\u001b[39;00m\n",
627
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/http/client.py:292\u001b[39m, in \u001b[36mHTTPResponse._read_status\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 291\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_read_status\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[32m--> \u001b[39m\u001b[32m292\u001b[39m line = \u001b[38;5;28mstr\u001b[39m(\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mfp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mreadline\u001b[49m\u001b[43m(\u001b[49m\u001b[43m_MAXLINE\u001b[49m\u001b[43m \u001b[49m\u001b[43m+\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m1\u001b[39;49m\u001b[43m)\u001b[49m, \u001b[33m\"\u001b[39m\u001b[33miso-8859-1\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 293\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(line) > _MAXLINE:\n\u001b[32m 294\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m LineTooLong(\u001b[33m\"\u001b[39m\u001b[33mstatus line\u001b[39m\u001b[33m\"\u001b[39m)\n",
628
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/socket.py:719\u001b[39m, in \u001b[36mSocketIO.readinto\u001b[39m\u001b[34m(self, b)\u001b[39m\n\u001b[32m 717\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mOSError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mcannot read from timed out object\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 718\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m719\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_sock\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrecv_into\u001b[49m\u001b[43m(\u001b[49m\u001b[43mb\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 720\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m timeout:\n\u001b[32m 721\u001b[39m \u001b[38;5;28mself\u001b[39m._timeout_occurred = \u001b[38;5;28;01mTrue\u001b[39;00m\n",
629
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/ssl.py:1304\u001b[39m, in \u001b[36mSSLSocket.recv_into\u001b[39m\u001b[34m(self, buffer, nbytes, flags)\u001b[39m\n\u001b[32m 1300\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m flags != \u001b[32m0\u001b[39m:\n\u001b[32m 1301\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 1302\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mnon-zero flags not allowed in calls to recv_into() on \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m\"\u001b[39m %\n\u001b[32m 1303\u001b[39m \u001b[38;5;28mself\u001b[39m.\u001b[34m__class__\u001b[39m)\n\u001b[32m-> \u001b[39m\u001b[32m1304\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnbytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbuffer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1305\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 1306\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28msuper\u001b[39m().recv_into(buffer, nbytes, flags)\n",
630
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/random/lib/python3.13/ssl.py:1138\u001b[39m, in \u001b[36mSSLSocket.read\u001b[39m\u001b[34m(self, len, buffer)\u001b[39m\n\u001b[32m 1136\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 1137\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m buffer \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1138\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_sslobj\u001b[49m\u001b[43m.\u001b[49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mlen\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbuffer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1139\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 1140\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._sslobj.read(\u001b[38;5;28mlen\u001b[39m)\n",
631
+ "\u001b[31mKeyboardInterrupt\u001b[39m: "
632
+ ]
633
+ }
634
+ ],
635
+ "source": [
636
+ "# Minimal scan: kits 1001..1060 — find sensor with most datapoints\n",
637
+ "\n",
638
+ "def find_max_sensor_in_range(start_kit: int = 1015, end_kit: int = 1060, page_size: int = 50) -> dict:\n",
639
+ " best = {\"kit_id\": None, \"sensor\": None, \"count\": -1}\n",
640
+ " for kit_id in range(start_kit, end_kit + 1):\n",
641
+ " kit = get_kit_info(kit_id)\n",
642
+ " if not kit or not isinstance(kit, dict):\n",
643
+ " print(f\"kit {kit_id}: not found\")\n",
644
+ " continue\n",
645
+ " sensors = kit.get(\"sensors\") or []\n",
646
+ " if not sensors:\n",
647
+ " print(f\"kit {kit_id}: no sensors\")\n",
648
+ " continue\n",
649
+ " for s in sensors:\n",
650
+ " name = s.get(\"name\")\n",
651
+ " if not name:\n",
652
+ " continue\n",
653
+ " cnt = count_sensor_measurements(kit_id, name, page_size=page_size)\n",
654
+ " print(f\"kit {kit_id} sensor {name}: {cnt}\")\n",
655
+ " if cnt > best[\"count\"]:\n",
656
+ " best = {\"kit_id\": kit_id, \"sensor\": name, \"count\": cnt}\n",
657
+ " return best\n",
658
+ "\n",
659
+ "best = find_max_sensor_in_range(1001, 1060, page_size=50)\n",
660
+ "print(\"\\nRESULT\")\n",
661
+ "print(best)"
662
+ ]
663
+ },
664
+ {
665
+ "cell_type": "code",
666
+ "execution_count": 18,
667
+ "id": "3216b9fb",
668
+ "metadata": {},
669
+ "outputs": [
670
+ {
671
+ "name": "stdout",
672
+ "output_type": "stream",
673
+ "text": [
674
+ "🔎 Scanning sensors from 1001 to 1060...\n",
675
+ "Sensor 1001: 0 datapoints (via /sensors/1001)\n",
676
+ "Sensor 1002: 0 datapoints (via /sensors/1002)\n",
677
+ "Sensor 1003: 0 datapoints (via /sensors/1003)\n",
678
+ "Sensor 1004: 0 datapoints (via /sensors/1004)\n",
679
+ "Sensor 1005: 0 datapoints (via /sensors/1005)\n",
680
+ "Sensor 1006: 0 datapoints (via /sensors/1006)\n",
681
+ "Sensor 1007: 0 datapoints (via /sensors/1007)\n",
682
+ "Sensor 1008: 0 datapoints (via /sensors/1008)\n",
683
+ "Sensor 1009: 0 datapoints (via /sensors/1009)\n",
684
+ "Sensor 1010: 0 datapoints (via /sensors/1010)\n",
685
+ "Sensor 1011: 0 datapoints (via /sensors/1011)\n",
686
+ "Sensor 1012: 0 datapoints (via /sensors/1012)\n",
687
+ "Sensor 1013: 0 datapoints (via /sensors/1013)\n",
688
+ "Sensor 1014: 0 datapoints (via /sensors/1014)\n",
689
+ "Sensor 1015: 0 datapoints (via /sensors/1015)\n",
690
+ "Sensor 1016: 0 datapoints (via /sensors/1016)\n",
691
+ "Sensor 1017: 0 datapoints (via /sensors/1017)\n",
692
+ "Sensor 1018: 0 datapoints (via /sensors/1018)\n",
693
+ "Sensor 1019: 0 datapoints (via /sensors/1019)\n",
694
+ "Sensor 1020: 0 datapoints (via /sensors/1020)\n",
695
+ "Sensor 1021: 0 datapoints (via /sensors/1021)\n",
696
+ "Sensor 1022: 0 datapoints (via /sensors/1022)\n",
697
+ "Sensor 1023: 0 datapoints (via /sensors/1023)\n",
698
+ "Sensor 1024: 0 datapoints (via /sensors/1024)\n",
699
+ "Sensor 1025: 0 datapoints (via /sensors/1025)\n",
700
+ "Sensor 1026: 0 datapoints (via /sensors/1026)\n",
701
+ "Sensor 1027: 0 datapoints (via /sensors/1027)\n",
702
+ "Sensor 1028: 0 datapoints (via /sensors/1028)\n",
703
+ "Sensor 1029: 0 datapoints (via /sensors/1029)\n",
704
+ "Sensor 1030: 0 datapoints (via /sensors/1030)\n",
705
+ "Sensor 1031: 0 datapoints (via /sensors/1031)\n",
706
+ "Sensor 1032: 0 datapoints (via /sensors/1032)\n",
707
+ "Sensor 1033: 0 datapoints (via /sensors/1033)\n",
708
+ "Sensor 1034: 0 datapoints (via /sensors/1034)\n",
709
+ "Sensor 1035: 0 datapoints (via /sensors/1035)\n",
710
+ "Sensor 1036: 0 datapoints (via /sensors/1036)\n",
711
+ "Sensor 1037: 0 datapoints (via /sensors/1037)\n",
712
+ "Sensor 1038: 0 datapoints (via /sensors/1038)\n",
713
+ "Sensor 1039: 0 datapoints (via /sensors/1039)\n",
714
+ "Sensor 1040: 0 datapoints (via /sensors/1040)\n",
715
+ "Sensor 1041: 0 datapoints (via /sensors/1041)\n",
716
+ "Sensor 1042: 0 datapoints (via /sensors/1042)\n",
717
+ "Sensor 1043: 0 datapoints (via /sensors/1043)\n",
718
+ "Sensor 1044: 0 datapoints (via /sensors/1044)\n",
719
+ "Sensor 1045: 0 datapoints (via /sensors/1045)\n",
720
+ "Sensor 1046: 0 datapoints (via /sensors/1046)\n",
721
+ "Sensor 1047: 0 datapoints (via /sensors/1047)\n",
722
+ "Sensor 1048: 0 datapoints (via /sensors/1048)\n",
723
+ "Sensor 1049: 0 datapoints (via /sensors/1049)\n",
724
+ "Sensor 1050: 0 datapoints (via /sensors/1050)\n",
725
+ "Sensor 1051: 0 datapoints (via /sensors/1051)\n",
726
+ "Sensor 1052: 0 datapoints (via /sensors/1052)\n",
727
+ "Sensor 1053: 0 datapoints (via /sensors/1053/readings)\n",
728
+ "Sensor 1054: 0 datapoints (via /sensors/1054)\n",
729
+ "Sensor 1055: 0 datapoints (via /sensors/1055)\n",
730
+ "Sensor 1056: 0 datapoints (via /sensors/1056)\n",
731
+ "Sensor 1057: 0 datapoints (via /sensors/1057)\n",
732
+ "Sensor 1058: 0 datapoints (via /sensors/1058)\n",
733
+ "Sensor 1059: 0 datapoints (via /sensors/1059)\n",
734
+ "Sensor 1060: 0 datapoints (via /sensors/1060)\n",
735
+ "\n",
736
+ "🏁 Scan complete.\n",
737
+ "🏆 Sensor with most datapoints:\n",
738
+ " 🆔 ID: 1001\n",
739
+ " 📊 Count: 0\n",
740
+ " 🔗 Endpoint: /sensors/1001\n"
741
+ ]
742
+ }
743
+ ],
744
+ "source": [
745
+ "from collections import defaultdict\n",
746
+ "\n",
747
+ "def _count_datapoints_from_response(data) -> int:\n",
748
+ " \"\"\"Best-effort count of datapoints from arbitrary API responses.\"\"\"\n",
749
+ " if data is None:\n",
750
+ " return 0\n",
751
+ " if isinstance(data, list):\n",
752
+ " return len(data)\n",
753
+ " if isinstance(data, dict):\n",
754
+ " # Prefer common array keys\n",
755
+ " for key in [\n",
756
+ " 'data', 'results', 'measurements', 'readings', 'entries', 'values',\n",
757
+ " 'observations', 'records', 'points'\n",
758
+ " ]:\n",
759
+ " if key in data and isinstance(data[key], list):\n",
760
+ " return len(data[key])\n",
761
+ " # Fallback: count scalar series\n",
762
+ " return sum(1 for v in data.values() if isinstance(v, (int, float, str, bool)))\n",
763
+ " return 0\n",
764
+ "\n",
765
+ "\n",
766
+ "def fetch_sensor_datapoints(sensor_id: int) -> tuple[int, dict]:\n",
767
+ " \"\"\"\n",
768
+ " Try multiple likely endpoints for a sensor and return the datapoint count and last successful meta.\n",
769
+ " Returns (count, meta) where meta contains endpoint and status.\n",
770
+ " \"\"\"\n",
771
+ " endpoints = [\n",
772
+ " f\"/sensors/{sensor_id}\",\n",
773
+ " f\"/sensors/{sensor_id}/data\",\n",
774
+ " f\"/sensors/{sensor_id}/readings\",\n",
775
+ " f\"/sensors/{sensor_id}/measurements\",\n",
776
+ " f\"/devices/{sensor_id}/data\",\n",
777
+ " f\"/nodes/{sensor_id}/uplinks\",\n",
778
+ " ]\n",
779
+ "\n",
780
+ " last_error = None\n",
781
+ " for ep in endpoints:\n",
782
+ " url = f\"{BASE_URL.rstrip('/')}{ep}\"\n",
783
+ " try:\n",
784
+ " r = requests.get(url, headers=HEADERS, timeout=30)\n",
785
+ " if r.status_code == 200:\n",
786
+ " try:\n",
787
+ " data = r.json()\n",
788
+ " except Exception:\n",
789
+ " data = None\n",
790
+ " count = _count_datapoints_from_response(data)\n",
791
+ " return count, {\"endpoint\": ep, \"status\": r.status_code}\n",
792
+ " else:\n",
793
+ " last_error = {\"endpoint\": ep, \"status\": r.status_code, \"text\": r.text[:200]}\n",
794
+ " except requests.RequestException as e:\n",
795
+ " last_error = {\"endpoint\": ep, \"error\": str(e)}\n",
796
+ " continue\n",
797
+ " return 0, (last_error or {\"endpoint\": None, \"error\": \"no-endpoint-succeeded\"})\n",
798
+ "\n",
799
+ "\n",
800
+ "def scan_sensors_and_find_max(start_id: int = 1001, end_id: int = 1060):\n",
801
+ " print(f\"🔎 Scanning sensors from {start_id} to {end_id}...\")\n",
802
+ " best = {\n",
803
+ " \"sensor_id\": None,\n",
804
+ " \"count\": -1,\n",
805
+ " \"meta\": {}\n",
806
+ " }\n",
807
+ " results = {}\n",
808
+ "\n",
809
+ " for sid in range(start_id, end_id + 1):\n",
810
+ " count, meta = fetch_sensor_datapoints(sid)\n",
811
+ " results[sid] = {\"count\": count, \"meta\": meta}\n",
812
+ " print(f\"Sensor {sid}: {count} datapoints (via {meta.get('endpoint')})\")\n",
813
+ " if count > best[\"count\"]:\n",
814
+ " best = {\"sensor_id\": sid, \"count\": count, \"meta\": meta}\n",
815
+ "\n",
816
+ " print(\"\\n🏁 Scan complete.\")\n",
817
+ " if best[\"sensor_id\"] is not None:\n",
818
+ " print(\"🏆 Sensor with most datapoints:\")\n",
819
+ " print(f\" 🆔 ID: {best['sensor_id']}\")\n",
820
+ " print(f\" 📊 Count: {best['count']}\")\n",
821
+ " print(f\" 🔗 Endpoint: {best['meta'].get('endpoint')}\")\n",
822
+ " else:\n",
823
+ " print(\"No sensors returned datapoints in the given range.\")\n",
824
+ "\n",
825
+ " return {\"best\": best, \"results\": results}\n",
826
+ "\n",
827
+ "# Run the scan now\n",
828
+ "scan_result = scan_sensors_and_find_max(1001, 1060)"
829
+ ]
830
+ },
831
+ {
832
+ "cell_type": "code",
833
+ "execution_count": 16,
834
+ "id": "46506887",
835
+ "metadata": {},
836
+ "outputs": [
837
+ {
838
+ "name": "stdout",
839
+ "output_type": "stream",
840
+ "text": [
841
+ "⚠️ Run the board analysis first to see visualizations.\n",
842
+ "💡 Make sure to update the API_KEY and BASE_URL in the configuration section.\n"
843
+ ]
844
+ }
845
+ ],
846
+ "source": [
847
+ "def create_board_analysis_chart(board_stats: Dict[str, Dict]):\n",
848
+ " \"\"\"\n",
849
+ " Create visualizations for board data analysis.\n",
850
+ " \n",
851
+ " Args:\n",
852
+ " board_stats (Dict[str, Dict]): Board statistics from get_board_data_counts\n",
853
+ " \"\"\"\n",
854
+ " if not board_stats:\n",
855
+ " print(\"❌ No board statistics available for visualization.\")\n",
856
+ " return\n",
857
+ " \n",
858
+ " # Prepare data for plotting\n",
859
+ " board_names = [stats['name'] for stats in board_stats.values()]\n",
860
+ " data_counts = [stats['data_count'] for stats in board_stats.values()]\n",
861
+ " board_ids = list(board_stats.keys())\n",
862
+ " \n",
863
+ " # Create DataFrame for better handling\n",
864
+ " df = pd.DataFrame({\n",
865
+ " 'Board ID': board_ids,\n",
866
+ " 'Board Name': board_names,\n",
867
+ " 'Data Points': data_counts\n",
868
+ " })\n",
869
+ " \n",
870
+ " # Sort by data points for better visualization\n",
871
+ " df = df.sort_values('Data Points', ascending=True)\n",
872
+ " \n",
873
+ " # Create the plot\n",
874
+ " plt.figure(figsize=(12, 8))\n",
875
+ " \n",
876
+ " # Horizontal bar chart\n",
877
+ " bars = plt.barh(range(len(df)), df['Data Points'], color='skyblue', alpha=0.7)\n",
878
+ " \n",
879
+ " # Customize the plot\n",
880
+ " plt.yticks(range(len(df)), df['Board Name'])\n",
881
+ " plt.xlabel('Number of Data Points')\n",
882
+ " plt.title('Data Points per Board - Teleagriculture API Analysis', fontsize=16, fontweight='bold')\n",
883
+ " plt.grid(axis='x', alpha=0.3)\n",
884
+ " \n",
885
+ " # Add value labels on bars\n",
886
+ " for i, (bar, value) in enumerate(zip(bars, df['Data Points'])):\n",
887
+ " plt.text(value + max(df['Data Points']) * 0.01, bar.get_y() + bar.get_height()/2, \n",
888
+ " str(value), va='center', fontweight='bold')\n",
889
+ " \n",
890
+ " # Highlight the board with most data points\n",
891
+ " max_idx = df['Data Points'].idxmax()\n",
892
+ " bars[df.index.get_loc(max_idx)].set_color('gold')\n",
893
+ " bars[df.index.get_loc(max_idx)].set_alpha(1.0)\n",
894
+ " \n",
895
+ " plt.tight_layout()\n",
896
+ " plt.show()\n",
897
+ " \n",
898
+ " # Print detailed statistics\n",
899
+ " print(\"📊 DETAILED STATISTICS\")\n",
900
+ " print(\"=\" * 50)\n",
901
+ " print(f\"Total boards analyzed: {len(df)}\")\n",
902
+ " print(f\"Total data points across all boards: {df['Data Points'].sum()}\")\n",
903
+ " print(f\"Average data points per board: {df['Data Points'].mean():.1f}\")\n",
904
+ " print(f\"Median data points per board: {df['Data Points'].median():.1f}\")\n",
905
+ " print(f\"Standard deviation: {df['Data Points'].std():.1f}\")\n",
906
+ " print()\n",
907
+ " \n",
908
+ " # Show top 3 boards\n",
909
+ " top_3 = df.nlargest(3, 'Data Points')\n",
910
+ " print(\"🏆 TOP 3 BOARDS:\")\n",
911
+ " for i, (_, row) in enumerate(top_3.iterrows(), 1):\n",
912
+ " emoji = \"🥇\" if i == 1 else \"🥈\" if i == 2 else \"🥉\"\n",
913
+ " print(f\"{emoji} {row['Board Name']}: {row['Data Points']} data points\")\n",
914
+ " \n",
915
+ " return df\n",
916
+ "\n",
917
+ "# Create visualization if we have results\n",
918
+ "if 'result' in locals() and result and result.get('all_stats'):\n",
919
+ " print(\"📈 Creating visualization...\")\n",
920
+ " df_analysis = create_board_analysis_chart(result['all_stats'])\n",
921
+ "else:\n",
922
+ " print(\"⚠️ Run the board analysis first to see visualizations.\")\n",
923
+ " print(\"💡 Make sure to update the API_KEY and BASE_URL in the configuration section.\")"
924
+ ]
925
+ },
926
+ {
927
+ "cell_type": "code",
928
+ "execution_count": null,
929
+ "id": "e01c5bf6",
930
+ "metadata": {},
931
+ "outputs": [],
932
+ "source": []
933
+ },
934
+ {
935
+ "cell_type": "markdown",
936
+ "id": "3553a610",
937
+ "metadata": {},
938
+ "source": [
939
+ "## Simple helper: get all sensor data for a kit id\n",
940
+ "\n",
941
+ "This function fetches all available measurements for a given kit (board) id across all its sensors and returns a tidy pandas DataFrame. It uses the same BASE_URL and HEADERS configured above and follows the API's cursor pagination automatically.\n",
942
+ "\n",
943
+ "- Input: kit_id (int)\n",
944
+ "- Optional: sensors (list[str]) to limit which sensors to fetch; defaults to all sensors on the kit\n",
945
+ "- Output: pandas DataFrame with columns like: kit_id, sensor, timestamp/value/..., depending on the API payload"
946
+ ]
947
+ },
948
+ {
949
+ "cell_type": "code",
950
+ "execution_count": null,
951
+ "id": "051f0aab",
952
+ "metadata": {},
953
+ "outputs": [],
954
+ "source": [
955
+ "from typing import Iterable, Any\n",
956
+ "\n",
957
+ "def _paginate(url: str, params: dict | None = None, headers: dict | None = None, page_size: int = 100, max_pages: int = 500):\n",
958
+ " \"\"\"Generator yielding pages from cursor-paginated endpoint returning {'data': [...], 'meta': {'next_cursor': '...'}}\"\"\"\n",
959
+ " params = dict(params or {})\n",
960
+ " params[\"page[size]\"] = str(page_size)\n",
961
+ " cursor = None\n",
962
+ " pages = 0\n",
963
+ " while pages < max_pages:\n",
964
+ " if cursor:\n",
965
+ " params[\"page[cursor]\"] = cursor\n",
966
+ " try:\n",
967
+ " r = requests.get(url, headers=headers, params=params, timeout=30)\n",
968
+ " except requests.RequestException:\n",
969
+ " break\n",
970
+ " if r.status_code != 200:\n",
971
+ " break\n",
972
+ " try:\n",
973
+ " payload = r.json()\n",
974
+ " except Exception:\n",
975
+ " break\n",
976
+ " data = payload.get(\"data\")\n",
977
+ " meta = payload.get(\"meta\", {})\n",
978
+ " yield data if isinstance(data, list) else []\n",
979
+ " cursor = meta.get(\"next_cursor\")\n",
980
+ " pages += 1\n",
981
+ " if not cursor:\n",
982
+ " break\n",
983
+ "\n",
984
+ "\n",
985
+ "def get_kit_measurements_df(kit_id: int, sensors: Iterable[str] | None = None, page_size: int = 100) -> pd.DataFrame:\n",
986
+ " \"\"\"\n",
987
+ " Fetch all measurements for a given kit across selected sensors and return a tidy DataFrame.\n",
988
+ "\n",
989
+ " - kit_id: numeric id of the kit/board\n",
990
+ " - sensors: optional list of sensor names; if None, will discover sensors via get_kit_info(kit_id)\n",
991
+ " - page_size: page size for cursor pagination\n",
992
+ "\n",
993
+ " Returns a DataFrame with columns: kit_id, sensor, timestamp, value, unit, _raw\n",
994
+ " (Columns may include NaNs if the API doesn't provide those fields.)\n",
995
+ " \"\"\"\n",
996
+ " # Discover sensors if not provided\n",
997
+ " sensor_list: list[str]\n",
998
+ " if sensors is None:\n",
999
+ " kit = get_kit_info(kit_id)\n",
1000
+ " if not kit:\n",
1001
+ " return pd.DataFrame(columns=[\"kit_id\", \"sensor\", \"timestamp\", \"value\", \"unit\", \"_raw\"])\n",
1002
+ " sensor_list = [s.get(\"name\") for s in (kit.get(\"sensors\") or []) if isinstance(s, dict) and s.get(\"name\")]\n",
1003
+ " else:\n",
1004
+ " sensor_list = [s for s in sensors if s]\n",
1005
+ "\n",
1006
+ " rows: list[dict[str, Any]] = []\n",
1007
+ "\n",
1008
+ " for sname in sensor_list:\n",
1009
+ " base = f\"{BASE_URL}/kits/{kit_id}/{sname}/measurements\"\n",
1010
+ " for page in _paginate(base, headers=HEADERS, page_size=page_size):\n",
1011
+ " for item in page:\n",
1012
+ " if not isinstance(item, dict):\n",
1013
+ " continue\n",
1014
+ " rec = item\n",
1015
+ " # Some APIs wrap fields inside 'attributes'\n",
1016
+ " if isinstance(rec.get(\"attributes\"), dict):\n",
1017
+ " # merge attributes shallowly (attributes wins for overlapping keys)\n",
1018
+ " rec = {**{k: v for k, v in rec.items() if k != \"attributes\"}, **rec[\"attributes\"]}\n",
1019
+ " # Normalize common fields\n",
1020
+ " ts = rec.get(\"timestamp\") or rec.get(\"time\") or rec.get(\"created_at\") or rec.get(\"datetime\")\n",
1021
+ " val = rec.get(\"value\") or rec.get(\"reading\") or rec.get(\"measurement\") or rec.get(\"val\")\n",
1022
+ " unit = rec.get(\"unit\") or rec.get(\"units\")\n",
1023
+ " rows.append({\n",
1024
+ " \"kit_id\": kit_id,\n",
1025
+ " \"sensor\": sname,\n",
1026
+ " \"timestamp\": ts,\n",
1027
+ " \"value\": val,\n",
1028
+ " \"unit\": unit,\n",
1029
+ " \"_raw\": item, # keep original\n",
1030
+ " })\n",
1031
+ "\n",
1032
+ " df = pd.DataFrame(rows)\n",
1033
+ " # Coerce timestamp and sort\n",
1034
+ " if not df.empty and \"timestamp\" in df.columns:\n",
1035
+ " try:\n",
1036
+ " df[\"timestamp\"] = pd.to_datetime(df[\"timestamp\"], errors=\"coerce\", utc=True)\n",
1037
+ " df = df.sort_values([\"sensor\", \"timestamp\"], kind=\"stable\")\n",
1038
+ " except Exception:\n",
1039
+ " pass\n",
1040
+ " return df"
1041
+ ]
1042
+ },
1043
+ {
1044
+ "cell_type": "code",
1045
+ "execution_count": null,
1046
+ "id": "e5f429f9",
1047
+ "metadata": {},
1048
+ "outputs": [],
1049
+ "source": [
1050
+ "# Demo: fetch all data for a kit id (adjust kit_id)\n",
1051
+ "KIT_DEMO_ID = 1001 # change as needed\n",
1052
+ "\n",
1053
+ "df_all = get_kit_measurements_df(KIT_DEMO_ID)\n",
1054
+ "print(f\"Fetched {len(df_all)} rows for kit {KIT_DEMO_ID}\")\n",
1055
+ "df_all.head()"
1056
+ ]
1057
+ },
1058
+ {
1059
+ "cell_type": "code",
1060
+ "execution_count": null,
1061
+ "id": "61c9be14",
1062
+ "metadata": {},
1063
+ "outputs": [],
1064
+ "source": [
1065
+ "# Simplest helper: get a DataFrame for a kit id\n",
1066
+ "\n",
1067
+ "def get_kit_df(kit_id: int) -> pd.DataFrame:\n",
1068
+ " return get_kit_measurements_df(kit_id)\n",
1069
+ "\n",
1070
+ "# Example usage:\n",
1071
+ "# df = get_kit_df(1001)\n",
1072
+ "# df.head()"
1073
+ ]
1074
+ }
1075
+ ],
1076
+ "metadata": {
1077
+ "kernelspec": {
1078
+ "display_name": "random",
1079
+ "language": "python",
1080
+ "name": "python3"
1081
+ },
1082
+ "language_info": {
1083
+ "codemirror_mode": {
1084
+ "name": "ipython",
1085
+ "version": 3
1086
+ },
1087
+ "file_extension": ".py",
1088
+ "mimetype": "text/x-python",
1089
+ "name": "python",
1090
+ "nbconvert_exporter": "python",
1091
+ "pygments_lexer": "ipython3",
1092
+ "version": "3.13.3"
1093
+ }
1094
+ },
1095
+ "nbformat": 4,
1096
+ "nbformat_minor": 5
1097
+ }
data/kit_1001_2025-09-22.csv ADDED
The diff for this file is too large to render. See raw diff
 
genai.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ from PIL import Image
3
+ from typing import Optional
4
+ from io import BytesIO
5
+
6
+ load_dotenv() # take environment variables from .env.
7
+
8
+ from google import genai
9
+
10
+ client = genai.Client()
11
+
12
+ # Default creative prompt
13
+ DEFAULT_PROMPT = (
14
+ "Turn the provided data visualization into a painting using an eastern art style."
15
+ )
16
+
17
+
18
+ def generate_genai_image(
19
+ input_image: Optional[Image.Image] = None,
20
+ prompt: Optional[str] = None,
21
+ model: str = "gemini-2.5-flash-image-preview",
22
+ save_to_disk: bool = False,
23
+ ) -> Optional[Image.Image]:
24
+ """Generate a stylized image from an input PIL image using Google GenAI.
25
+
26
+ Args:
27
+ input_image: Source PIL image. If None, calls weather_data_visualisation() to generate in-memory.
28
+ prompt: Optional text prompt; falls back to DEFAULT_PROMPT.
29
+ model: Model name to use.
30
+ save_to_disk: When True, saves to output/generated_image.png (no disk reads occur).
31
+
32
+ Returns:
33
+ A PIL.Image on success, or None if generation failed.
34
+ """
35
+
36
+ # Resolve input image strictly in-memory
37
+ img = input_image
38
+ if img is None:
39
+ try:
40
+ from weather_data_visualisation import weather_data_visualisation
41
+
42
+ img = weather_data_visualisation(save_to_disk=False)
43
+ except Exception:
44
+ img = None
45
+
46
+ if img is None:
47
+ return None
48
+
49
+ # Prepare prompt
50
+ ptxt = prompt or DEFAULT_PROMPT
51
+
52
+ try:
53
+ response = client.models.generate_content(
54
+ model=model,
55
+ contents=[ptxt, img],
56
+ )
57
+ except Exception as e:
58
+ # No credentials or API issue
59
+ print(f"GenAI request failed: {e}")
60
+ return None
61
+
62
+ output_image = None
63
+ try:
64
+ for part in response.candidates[0].content.parts:
65
+ if getattr(part, "text", None) is not None:
66
+ # Optional: print any textual response
67
+ print(part.text)
68
+ elif getattr(part, "inline_data", None) is not None:
69
+ output_image = Image.open(BytesIO(part.inline_data.data)).convert("RGB")
70
+ except Exception as e:
71
+ print(f"Failed to parse GenAI response: {e}")
72
+ return None
73
+
74
+ # Optional save without using as a future fallback
75
+ if output_image is not None and save_to_disk:
76
+ try:
77
+ import os
78
+
79
+ os.makedirs("output", exist_ok=True)
80
+ output_image.save("output/generated_image.png")
81
+ except Exception:
82
+ pass
83
+
84
+ return output_image
85
+
86
+ if __name__ == "__main__":
87
+ generate_genai_image()
gradio_app.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+ from PIL import Image, ImageDraw, ImageFont
3
+
4
+
5
+ WEATHER_PNG_PATH = None # disk fallback removed
6
+ GENERATED_PNG_PATH = None # disk fallback removed
7
+
8
+ def _placeholder_image(size: Tuple[int, int], text: str, bg=(230, 230, 230)) -> Image.Image:
9
+ # Always create a simple placeholder; don't try to read a file here
10
+ img = Image.new("RGB", size, color=bg)
11
+ draw = ImageDraw.Draw(img)
12
+ try:
13
+ font = ImageFont.load_default()
14
+ except Exception:
15
+ font = None
16
+ w, h = draw.textbbox((0, 0), text, font=font)[2:]
17
+ draw.text(((size[0] - w) / 2, (size[1] - h) / 2), text, fill=(80, 80, 80), font=font)
18
+ return img
19
+
20
+
21
+ def load_weather_plot(size: Tuple[int, int] = (1024, 1024)) -> Image.Image:
22
+ """Load the weather plot image produced by weather_data_visualisation.py.
23
+
24
+ Attempts to import the script to generate the file on first run. Falls back
25
+ to a placeholder if unavailable.
26
+ """
27
+ try:
28
+ # Prefer calling the function to get a PIL image directly
29
+ from weather_data_visualisation import weather_data_visualisation
30
+
31
+ img = weather_data_visualisation(save_to_disk=False)
32
+ if isinstance(img, Image.Image):
33
+ if img.size != size:
34
+ img = img.resize(size, Image.LANCZOS)
35
+ return img
36
+ except Exception as e:
37
+ print(f"Weather plot generation failed: {e}")
38
+
39
+ return _placeholder_image(size, "Weather plot unavailable")
40
+
41
+
42
+ def load_genai_output(size: Tuple[int, int] = (1024, 1024)) -> Image.Image:
43
+ """Load the GenAI output image if available; otherwise return a placeholder.
44
+
45
+ If `genai.py` later exposes a function like `generate_genai_image(size)`,
46
+ it will be used here automatically.
47
+ """
48
+ try:
49
+ from genai import generate_genai_image
50
+
51
+ # Provide the latest weather image if possible to guide the GenAI
52
+ base_img = None
53
+ try:
54
+ base_img = load_weather_plot(size)
55
+ except Exception:
56
+ base_img = None
57
+
58
+ img = generate_genai_image(input_image=base_img, save_to_disk=False)
59
+ if isinstance(img, Image.Image):
60
+ if img.size != size:
61
+ img = img.resize(size, Image.LANCZOS)
62
+ return img
63
+ except Exception as e:
64
+ print(f"genai.py not usable yet: {e}")
65
+
66
+ return _placeholder_image(size, "GenAI image pending")
67
+
68
+
69
+ def get_both_images(size: Tuple[int, int] = (1024, 1024)) -> Tuple[Image.Image, Image.Image]:
70
+ left = load_weather_plot(size)
71
+ right = load_genai_output(size)
72
+ return left, right
73
+
74
+
75
+ def create_app():
76
+ """Creates and returns the Gradio app with two side-by-side images."""
77
+ import gradio as gr
78
+
79
+ with gr.Blocks(title="Weather × GenAI") as app:
80
+ gr.Markdown("# Weather visualization and GenAI output")
81
+ with gr.Row():
82
+ left_img = gr.Image(label="Weather plot", type="pil")
83
+ right_img = gr.Image(label="GenAI output", type="pil")
84
+
85
+ # Load both images on app start
86
+ app.load(fn=get_both_images, inputs=None, outputs=[left_img, right_img])
87
+
88
+ # Manual refresh button
89
+ refresh_btn = gr.Button("Refresh")
90
+ refresh_btn.click(fn=get_both_images, inputs=None, outputs=[left_img, right_img])
91
+
92
+ return app
93
+
94
+
95
+ if __name__ == "__main__":
96
+ app = create_app()
97
+ app.launch()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ numpy
2
+ pandas
3
+ matplotlib
4
+ pillow
5
+ gradio
6
+ python-dotenv
7
+ google-genai
utils.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility helpers for Teleagriculture kits API
3
+
4
+ Provides:
5
+ - BASE_URL, HEADERS (with optional Bearer from KIT_API_KEY env)
6
+ - get_kit_info(kit_id)
7
+ - get_kit_measurements_df(kit_id, sensors=None, page_size=100)
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from typing import Any, Iterable, Optional
13
+
14
+ import pandas as pd
15
+ import requests
16
+
17
+ # API configuration
18
+ BASE_URL = os.getenv("KITS_API_BASE", "https://kits.teleagriculture.org/api")
19
+ KIT_API_KEY = os.getenv("KIT_API_KEY")
20
+
21
+ HEADERS: dict[str, str] = {
22
+ "Accept": "application/json",
23
+ }
24
+ if KIT_API_KEY:
25
+ HEADERS["Authorization"] = f"Bearer {KIT_API_KEY}"
26
+
27
+
28
+ def get_kit_info(kit_id: int) -> Optional[dict]:
29
+ """Fetch metadata for a kit (board).
30
+
31
+ Returns the JSON 'data' object or None if not found / error.
32
+ """
33
+ url = f"{BASE_URL}/kits/{kit_id}"
34
+ try:
35
+ r = requests.get(url, headers=HEADERS, timeout=30)
36
+ if r.status_code == 200:
37
+ body = r.json()
38
+ return body.get("data")
39
+ return None
40
+ except requests.RequestException:
41
+ return None
42
+
43
+
44
+ def _paginate(
45
+ url: str,
46
+ *,
47
+ params: Optional[dict] = None,
48
+ headers: Optional[dict] = None,
49
+ page_size: int = 100,
50
+ max_pages: int = 500,
51
+ ):
52
+ """Cursor pagination helper yielding lists of items from {'data': [...]} pages.
53
+
54
+ Stops when no next_cursor is provided or on any non-200/parse error.
55
+ """
56
+ q = dict(params or {})
57
+ q["page[size]"] = str(page_size)
58
+ cursor = None
59
+ pages = 0
60
+ while pages < max_pages:
61
+ if cursor:
62
+ q["page[cursor]"] = cursor
63
+ try:
64
+ r = requests.get(url, headers=headers, params=q, timeout=30)
65
+ except requests.RequestException:
66
+ break
67
+ if r.status_code != 200:
68
+ break
69
+ try:
70
+ payload = r.json()
71
+ except Exception:
72
+ break
73
+ data = payload.get("data")
74
+ meta = payload.get("meta", {})
75
+ yield data if isinstance(data, list) else []
76
+ cursor = meta.get("next_cursor")
77
+ pages += 1
78
+ if not cursor:
79
+ break
80
+
81
+
82
+ def get_kit_measurements_df(
83
+ kit_id: int,
84
+ sensors: Optional[Iterable[str]] = None,
85
+ *,
86
+ page_size: int = 100,
87
+ ) -> pd.DataFrame:
88
+ """Fetch all measurements for the given kit across its sensors as a DataFrame.
89
+
90
+ - If sensors is None, discover sensors via get_kit_info(kit_id).
91
+ - Returns columns: kit_id, sensor, timestamp, value, unit, _raw
92
+ (depending on API, some fields may be None/NaT)
93
+ """
94
+ # Determine sensor list
95
+ if sensors is None:
96
+ kit = get_kit_info(kit_id)
97
+ if not kit:
98
+ return pd.DataFrame(columns=["kit_id", "sensor", "timestamp", "value", "unit", "_raw"])
99
+ sensor_list = [
100
+ s.get("name")
101
+ for s in (kit.get("sensors") or [])
102
+ if isinstance(s, dict) and s.get("name")
103
+ ]
104
+ else:
105
+ sensor_list = [s for s in sensors if s]
106
+
107
+ rows: list[dict[str, Any]] = []
108
+
109
+ for sname in sensor_list:
110
+ endpoint = f"{BASE_URL}/kits/{kit_id}/{sname}/measurements"
111
+ for page in _paginate(endpoint, headers=HEADERS, page_size=page_size):
112
+ for item in page:
113
+ if not isinstance(item, dict):
114
+ continue
115
+
116
+ # Some APIs nest details under 'attributes'
117
+ rec = item.get("attributes", {})
118
+ rec.update({k: v for k, v in item.items() if k != "attributes"})
119
+
120
+ ts = rec.get("timestamp") or rec.get("time") or rec.get("created_at") or rec.get("datetime")
121
+ val = rec.get("value") or rec.get("reading") or rec.get("measurement") or rec.get("val")
122
+ unit = rec.get("unit") or rec.get("units")
123
+ rows.append(
124
+ {
125
+ "kit_id": kit_id,
126
+ "sensor": sname,
127
+ "timestamp": ts,
128
+ "value": val,
129
+ "unit": unit,
130
+ "_raw": item, # preserve original
131
+ }
132
+ )
133
+
134
+ df = pd.DataFrame(rows)
135
+ if not df.empty and "timestamp" in df.columns:
136
+ try:
137
+ df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce", utc=True)
138
+ df = df.sort_values(["sensor", "timestamp"], kind="stable")
139
+ except Exception:
140
+ pass
141
+ return df
142
+
143
+
144
+ def fetch_kit_dataframe(kit_id: int) -> pd.DataFrame:
145
+ """Simplest API: return all measurements for the given kit as a DataFrame.
146
+
147
+ Equivalent to get_kit_measurements_df(kit_id) with sensible defaults.
148
+ """
149
+ return get_kit_measurements_df(kit_id)
weather_data_visualisation.ipynb ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "e7f2dbbd",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "# This script generates a sample \"Monsoon Mandala\" artwork using placeholder data. \n",
11
+ "# Replace the synthetic data block with your real pandas DataFrame columns to recreate the piece with your tea farm data.\n",
12
+ "\n",
13
+ "import numpy as np\n",
14
+ "import pandas as pd\n",
15
+ "import matplotlib.pyplot as plt\n",
16
+ "weatherdata_df = pd.read_csv(\"data/kit_1001_2025-09-22.csv\", index_col=2)\n",
17
+ "weatherdata_df.drop(columns=['kit_id', \"unit\",\"_raw\"], inplace=True)\n",
18
+ "weatherdata_df.dropna(inplace=True)\n",
19
+ "weatherdata_df.index = pd.to_datetime(weatherdata_df.index)\n",
20
+ "weatherdata_df = weatherdata_df.pivot(columns='sensor', values='value')\n",
21
+ "weatherdata_df.columns"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "execution_count": null,
27
+ "id": "18a06b7d",
28
+ "metadata": {},
29
+ "outputs": [],
30
+ "source": [
31
+ "weatherdata_df.columns"
32
+ ]
33
+ },
34
+ {
35
+ "cell_type": "code",
36
+ "execution_count": null,
37
+ "id": "4dadce7a",
38
+ "metadata": {},
39
+ "outputs": [],
40
+ "source": [
41
+ "# ---- Mapping to polar \"Monsoon Mandala\" ----\n",
42
+ "# Angles map to time; radii encode a blended metric; thickness & dot size encode other variables.\n",
43
+ "\n",
44
+ "df = weatherdata_df\n",
45
+ "theta = np.linspace(0, 2*np.pi, len(df), endpoint=False)\n",
46
+ "\n",
47
+ "# Normalize helpers (avoid specifying colors, per instructions).\n",
48
+ "def norm(x):\n",
49
+ " x = np.asarray(x)\n",
50
+ " if np.nanmax(x) - np.nanmin(x) == 0:\n",
51
+ " return np.zeros_like(x)\n",
52
+ " return (x - np.nanmin(x)) / (np.nanmax(x) - np.nanmin(x))\n",
53
+ "\n",
54
+ "T = norm(df['ftTemp'].values)\n",
55
+ "H = norm(df['gbHum'].values)\n",
56
+ "R = norm(df['NH3'].values)\n",
57
+ "W = norm(df['C3H8'].values)\n",
58
+ "L = norm(df['CO'].values)\n",
59
+ "\n",
60
+ "# Radius combines temp (outer breathing), humidity (inner swell), light (diurnal bloom)\n",
61
+ "radius = 0.45 + 0.35*(0.5*T + 0.3*H + 0.2*L)\n",
62
+ "\n",
63
+ "# Stroke width from wind; point size from rainfall intensity\n",
64
+ "stroke = 0.3 + 3.2*W\n",
65
+ "dots = 5 + 60*R\n",
66
+ "\n",
67
+ "# Rolling medians for smooth rings\n",
68
+ "def smooth(x, k=21):\n",
69
+ " if k < 3: \n",
70
+ " return x\n",
71
+ " w = np.ones(k)/k\n",
72
+ " return np.convolve(x, w, mode=\"same\")\n",
73
+ "\n",
74
+ "radius_smooth = smooth(radius, k=31)\n",
75
+ "\n",
76
+ "# ---- Plot (no explicit colors; uses matplotlib defaults) ----\n",
77
+ "plt.figure(figsize=(8, 8))\n",
78
+ "ax = plt.subplot(111, projection=\"polar\")\n",
79
+ "ax.set_theta_direction(-1) # clockwise\n",
80
+ "ax.set_theta_offset(np.pi/2.0) # start at top\n",
81
+ "ax.set_axis_off()\n",
82
+ "\n",
83
+ "# Outer ribbon\n",
84
+ "ax.plot(theta, radius_smooth, linewidth=2.0)\n",
85
+ "\n",
86
+ "# Inner filigree rings\n",
87
+ "for k in [3, 7, 13]:\n",
88
+ " ax.plot(theta, smooth(radius * (0.85 + 0.05*np.sin(k*theta)), k=15), linewidth=0.8)\n",
89
+ "\n",
90
+ "# Rainfall pearls\n",
91
+ "ax.scatter(theta[::3], (radius_smooth*0.92)[::3], s=dots[::3], alpha=0.6)\n",
92
+ "\n",
93
+ "# Wind tick marks (radial sticks)\n",
94
+ "for th, rr, sw in zip(theta[::12], radius_smooth[::12], stroke[::12]):\n",
95
+ " ax.plot([th, th], [rr*0.75, rr*0.98], linewidth=sw*0.12, alpha=0.8)\n",
96
+ "\n",
97
+ "plt.tight_layout()\n",
98
+ "png_path = \"output/monsoon_mandala_example.png\"\n",
99
+ "svg_path = \"output/monsoon_mandala_example.svg\"\n",
100
+ "plt.savefig(png_path, dpi=300, bbox_inches=\"tight\", pad_inches=0.05)\n",
101
+ "plt.savefig(svg_path, bbox_inches=\"tight\", pad_inches=0.05)\n",
102
+ "png_path, svg_path\n"
103
+ ]
104
+ }
105
+ ],
106
+ "metadata": {
107
+ "kernelspec": {
108
+ "display_name": "datascience",
109
+ "language": "python",
110
+ "name": "python3"
111
+ },
112
+ "language_info": {
113
+ "codemirror_mode": {
114
+ "name": "ipython",
115
+ "version": 3
116
+ },
117
+ "file_extension": ".py",
118
+ "mimetype": "text/x-python",
119
+ "name": "python",
120
+ "nbconvert_exporter": "python",
121
+ "pygments_lexer": "ipython3",
122
+ "version": "3.12.4"
123
+ }
124
+ },
125
+ "nbformat": 4,
126
+ "nbformat_minor": 5
127
+ }
weather_data_visualisation.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This script generates a sample "Monsoon Mandala" artwork using placeholder data.
2
+ # Replace the synthetic data block with your real pandas DataFrame columns to recreate the piece with your tea farm data.
3
+
4
+ import os
5
+ import numpy as np
6
+ import pandas as pd
7
+ import matplotlib.pyplot as plt
8
+ from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
9
+ from PIL import Image
10
+
11
+
12
+ def weather_data_visualisation(save_to_disk: bool = False) -> Image.Image:
13
+ weatherdata_df = pd.read_csv("data/kit_1001_2025-09-22.csv", index_col=2)
14
+ weatherdata_df.drop(columns=['kit_id', "unit","_raw"], inplace=True)
15
+ weatherdata_df.dropna(inplace=True)
16
+ weatherdata_df.index = pd.to_datetime(weatherdata_df.index)
17
+ weatherdata_df = weatherdata_df.pivot(columns='sensor', values='value')
18
+
19
+ # ---- Mapping to polar "Monsoon Mandala" ----
20
+ # Angles map to time; radii encode a blended metric; thickness & dot size encode other variables.
21
+
22
+ df = weatherdata_df
23
+
24
+ theta = np.linspace(0, 2*np.pi, len(df), endpoint=False)
25
+
26
+ # Normalize helpers (avoid specifying colors, per instructions).
27
+ def norm(x):
28
+ x = np.asarray(x)
29
+ if np.nanmax(x) - np.nanmin(x) == 0:
30
+ return np.zeros_like(x)
31
+ return (x - np.nanmin(x)) / (np.nanmax(x) - np.nanmin(x))
32
+
33
+ T = norm(df['ftTemp'].values)
34
+ H = norm(df['gbHum'].values)
35
+ R = norm(df['NH3'].values)
36
+ W = norm(df['C3H8'].values)
37
+ L = norm(df['CO'].values)
38
+
39
+ # Radius combines temp (outer breathing), humidity (inner swell), light (diurnal bloom)
40
+ radius = 0.45 + 0.35*(0.5*T + 0.3*H + 0.2*L)
41
+
42
+ # Stroke width from wind; point size from rainfall intensity
43
+ stroke = 0.3 + 3.2*W
44
+ dots = 5 + 60*R
45
+
46
+ # Rolling medians for smooth rings
47
+ def smooth(x, k=21):
48
+ if k < 3:
49
+ return x
50
+ w = np.ones(k)/k
51
+ return np.convolve(x, w, mode="same")
52
+
53
+ radius_smooth = smooth(radius, k=31)
54
+
55
+ # ---- Plot (no explicit colors; uses matplotlib defaults) ----
56
+ fig = plt.figure(figsize=(8, 8))
57
+ ax = plt.subplot(111, projection="polar")
58
+ ax.set_theta_direction(-1) # clockwise
59
+ ax.set_theta_offset(np.pi/2.0) # start at top
60
+ ax.set_axis_off()
61
+
62
+ # Outer ribbon
63
+ ax.plot(theta, radius_smooth, linewidth=2.0)
64
+
65
+ # Inner filigree rings
66
+ for k in [3, 7, 13]:
67
+ ax.plot(theta, smooth(radius * (0.85 + 0.05*np.sin(k*theta)), k=15), linewidth=0.8)
68
+
69
+ # Rainfall pearls
70
+ ax.scatter(theta[::3], (radius_smooth*0.92)[::3], s=dots[::3], alpha=0.6)
71
+
72
+ # Wind tick marks (radial sticks)
73
+ for th, rr, sw in zip(theta[::12], radius_smooth[::12], stroke[::12]):
74
+ ax.plot([th, th], [rr*0.75, rr*0.98], linewidth=sw*0.12, alpha=0.8)
75
+
76
+ plt.tight_layout()
77
+
78
+ # Render figure to RGBA buffer and convert to PIL.Image
79
+ canvas = FigureCanvas(fig)
80
+ canvas.draw()
81
+ buf = np.asarray(canvas.buffer_rgba())
82
+ pil_img = Image.fromarray(buf, mode="RGBA").convert("RGB")
83
+
84
+ # Optionally also save to disk for compatibility with other tools
85
+ if save_to_disk:
86
+ png_path = "output/monsoon_mandala_example.png"
87
+ svg_path = "output/monsoon_mandala_example.svg"
88
+ try:
89
+ os.makedirs(os.path.dirname(png_path), exist_ok=True)
90
+ fig.savefig(png_path, dpi=300, bbox_inches="tight", pad_inches=0.05)
91
+ fig.savefig(svg_path, bbox_inches="tight", pad_inches=0.05)
92
+ except Exception:
93
+ # If saving fails (e.g., directory missing), continue returning the PIL image
94
+ pass
95
+
96
+ plt.close(fig)
97
+ return pil_img
98
+
99
+ if __name__ == "__main__":
100
+ weather_data_visualisation()