build(agent): new-agents-2#7e3bbc iteration
This commit is contained in:
parent
02455e101c
commit
7c4187ffd1
|
|
@ -0,0 +1,21 @@
|
||||||
|
node_modules/
|
||||||
|
.npmrc
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
__tests__/
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
tmp/
|
||||||
|
.tmp/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
READY_TO_PUBLISH
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# SolarSphere Offline-First MVP Agents
|
||||||
|
|
||||||
|
This repository contains a production-oriented Python MVP for SolarSphere, an offline-first geospatial visualization scaffold. The code focuses on data contracts, a pluggable data-source adapter surface, deterministic delta reconciliation, and a lightweight render summary suitable for testing and extension.
|
||||||
|
|
||||||
|
Architecture overview
|
||||||
|
- Core data primitives: Asset, TelemetryPoint, WeatherLayer, Event.
|
||||||
|
- Data sources: pluggable adapters (CSV, REST, MQTT, etc.). CSVAdapter is implemented for MVP.
|
||||||
|
- Delta reconciliation: deterministic, replay-friendly merge of local and remote deltas.
|
||||||
|
- Lightweight renderer: builds a serializable snapshot suitable for downstream visualization without a browser runtime.
|
||||||
|
- Packaging: Python package under idea78_solarsphere_offline_first with a small, exportable API.
|
||||||
|
|
||||||
|
Tech stack decisions
|
||||||
|
- Language: Python 3.9+ for rapid prototyping, strong typing, and easy packaging.
|
||||||
|
- No external GUI dependencies yet; a JSON-friendly render path is provided to support future WebGL/Canvas rendering layers.
|
||||||
|
- Tests: pytest-based unit tests ensure data models, adapters, and delta logic behave deterministically.
|
||||||
|
|
||||||
|
Testing and commands
|
||||||
|
- Run tests locally: ./test.sh
|
||||||
|
- Build package: python3 -m build
|
||||||
|
- Inspect package: pytest tests -q
|
||||||
|
|
||||||
|
Contributing rules
|
||||||
|
- Add small, well-scoped changes; prefer minimal surface area.
|
||||||
|
- Update AGENTS.md if you add new architecture or data contracts.
|
||||||
|
- Ensure tests cover new behavior.
|
||||||
31
README.md
31
README.md
|
|
@ -1,3 +1,30 @@
|
||||||
# idea78-solarsphere-offline-first
|
# SolarSphere Offline-First MVP
|
||||||
|
|
||||||
Source logic for Idea #78
|
Overview
|
||||||
|
- A Python-based MVP for offline-first geospatial visualization and telemetry replay. Focuses on simple, well-defined data contracts and deterministic delta reconciliation that can run entirely offline with optional data-sync when connectivity returns.
|
||||||
|
|
||||||
|
What’s included
|
||||||
|
- Core data primitives: Asset, TelemetryPoint, WeatherLayer, Event
|
||||||
|
- CSV-based telemetry adapter for MVP data ingestion
|
||||||
|
- Delta reconciliation utilities for deterministic merges
|
||||||
|
- Lightweight render summary producing JSON suitable for downstream tooling
|
||||||
|
- Minimal CLI and a test suite to validate core behavior
|
||||||
|
|
||||||
|
Getting started
|
||||||
|
- Install dependencies and run tests: `bash test.sh`
|
||||||
|
- Package: `python3 -m build` (also in test script)
|
||||||
|
|
||||||
|
Project structure (high level)
|
||||||
|
- src/idea78_solarsphere_offline_first/: Python package with core models, adapters, delta logic, and rendering helpers
|
||||||
|
- tests/: Unit tests for models, delta logic, and adapters
|
||||||
|
- AGENTS.md: Architecture and contribution guidance
|
||||||
|
- test.sh: Test runner that executes tests and packaging checks
|
||||||
|
|
||||||
|
Usage example (quick)
|
||||||
|
- Load a CSV telemetry file via CSVAdapter, reconcile with a local delta, and render a snapshot:
|
||||||
|
- adapter = CSVAdapter("telemetry.csv")
|
||||||
|
- telem = adapter.load_telemetry()
|
||||||
|
- snapshot = render.render_snapshot([asset], telem, weather, events)
|
||||||
|
- print(snapshot)
|
||||||
|
|
||||||
|
This repository adheres to the contribution guidelines described in AGENTS.md and is designed to be extended in sprint fashion.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=42", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "idea78_solarsphere_offline_first"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Offline-first geospatial visualization scaffolding for SolarSphere MVP"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
package-dir = {"" = "src"}
|
||||||
|
|
||||||
|
# Removed legacy setuptools.find block to ensure compatibility with current build backends
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Idea78 SolarSphere Offline-First package initializer"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
from .core import Asset, TelemetryPoint, WeatherLayer, Event # re-export core types
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
"""Core data primitives for SolarSphere offline-first MVP."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Dict
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Asset:
|
||||||
|
site_id: str
|
||||||
|
location: Dict[str, float] # e.g., {"lat": 12.34, "lon": 56.78}
|
||||||
|
layers: List[str] = field(default_factory=list)
|
||||||
|
version: int = 1
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TelemetryPoint:
|
||||||
|
site_id: str
|
||||||
|
ts: int # epoch milliseconds
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
metric: str
|
||||||
|
value: float
|
||||||
|
quality: str = "good"
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WeatherLayer:
|
||||||
|
region: str
|
||||||
|
ts: int
|
||||||
|
param: str
|
||||||
|
value: float
|
||||||
|
source: str
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Event:
|
||||||
|
ts: int
|
||||||
|
site_id: str
|
||||||
|
type: str
|
||||||
|
description: str
|
||||||
|
severity: str
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
"""CSV-based adapter for ingesting telemetry data (MVP)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .core import TelemetryPoint
|
||||||
|
|
||||||
|
|
||||||
|
class CSVAdapter:
|
||||||
|
"""Simple CSV data adapter for TelemetryPoint ingest.
|
||||||
|
|
||||||
|
Expected CSV header:
|
||||||
|
site_id,ts,lat,lon,metric,value,quality
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path: str):
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def load_assets(self) -> List:
|
||||||
|
# MVP: not implemented for tests; return empty list
|
||||||
|
return []
|
||||||
|
|
||||||
|
def load_telemetry(self) -> List[TelemetryPoint]:
|
||||||
|
pts: List[TelemetryPoint] = []
|
||||||
|
with open(self.path, newline="") as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
pts.append(
|
||||||
|
TelemetryPoint(
|
||||||
|
site_id=row["site_id"],
|
||||||
|
ts=int(row["ts"]),
|
||||||
|
lat=float(row["lat"]),
|
||||||
|
lon=float(row["lon"]),
|
||||||
|
metric=row["metric"],
|
||||||
|
value=float(row["value"]),
|
||||||
|
quality=row.get("quality", "good"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return pts
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Deterministic, replay-friendly delta reconciliation helpers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Dict, Tuple
|
||||||
|
from .core import TelemetryPoint
|
||||||
|
|
||||||
|
|
||||||
|
def _key(pt: TelemetryPoint) -> Tuple[str, int, str]:
|
||||||
|
# Unique key for a telemetry point by site, timestamp, and metric
|
||||||
|
return (pt.site_id, pt.ts, pt.metric)
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_deltas(local: List[TelemetryPoint], remote: List[TelemetryPoint]) -> List[TelemetryPoint]:
|
||||||
|
"""Deterministically merge local and remote telemetry points.
|
||||||
|
|
||||||
|
Remote data takes precedence when keys collide. The function returns a new
|
||||||
|
list containing the merged view. Order is not guaranteed but deterministic
|
||||||
|
with respect to the input keys.
|
||||||
|
"""
|
||||||
|
merged: Dict[Tuple[str, int, str], TelemetryPoint] = {}
|
||||||
|
|
||||||
|
for p in local:
|
||||||
|
merged[_key(p)] = p
|
||||||
|
for p in remote:
|
||||||
|
merged[_key(p)] = p
|
||||||
|
|
||||||
|
return list(merged.values())
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryDelta:
|
||||||
|
def __init__(self, delta: List[TelemetryPoint], ts: int, anchor: str, signature: str):
|
||||||
|
self.delta = delta
|
||||||
|
self.ts = ts
|
||||||
|
self.anchor = anchor
|
||||||
|
self.signature = signature
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""Lightweight render summary for offline-first MVP.
|
||||||
|
|
||||||
|
This is intentionally data-centric (JSON-friendly) to support downstream
|
||||||
|
visualization layers without a browser runtime in this MVP.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
from .core import Asset, TelemetryPoint, WeatherLayer, Event
|
||||||
|
|
||||||
|
|
||||||
|
def render_snapshot(assets: List[Asset], telemetry: List[TelemetryPoint], weather: List[WeatherLayer], events: List[Event]) -> dict:
|
||||||
|
return {
|
||||||
|
"assets": [a.__dict__ for a in assets],
|
||||||
|
"telemetry_count": len(telemetry),
|
||||||
|
"weather_count": len(weather),
|
||||||
|
"events": len(events),
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Basic test suite and packaging verification for SolarSphere MVP
|
||||||
|
echo "Running tests..."
|
||||||
|
pytest -q
|
||||||
|
|
||||||
|
echo "Building package..."
|
||||||
|
python3 -m build
|
||||||
|
|
||||||
|
echo "All tests passed and package built."
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Ensure the local src package is importable by pytest
|
||||||
|
SRC_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
if SRC_PATH not in sys.path:
|
||||||
|
sys.path.insert(0, SRC_PATH)
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
from idea78_solarsphere_offline_first.csv_adapter import CSVAdapter
|
||||||
|
from idea78_solarsphere_offline_first.core import TelemetryPoint
|
||||||
|
|
||||||
|
|
||||||
|
def test_csv_adapter_loads_telemetry_points():
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".csv") as tf:
|
||||||
|
tf.write("site_id,ts,lat,lon,metric,value,quality\n")
|
||||||
|
tf.write("siteA,1000,1.0,2.0,temperature,25.0,good\n")
|
||||||
|
tf.flush()
|
||||||
|
path = tf.name
|
||||||
|
|
||||||
|
adapter = CSVAdapter(path)
|
||||||
|
pts = adapter.load_telemetry()
|
||||||
|
assert isinstance(pts, list) and len(pts) == 1
|
||||||
|
p = pts[0]
|
||||||
|
assert isinstance(p, TelemetryPoint)
|
||||||
|
assert p.site_id == "siteA" and p.metric == "temperature" and p.value == 25.0
|
||||||
|
os.unlink(path)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from idea78_solarsphere_offline_first.core import TelemetryPoint
|
||||||
|
from idea78_solarsphere_offline_first.delta import reconcile_deltas
|
||||||
|
|
||||||
|
|
||||||
|
def test_reconcile_deltas_basic_override():
|
||||||
|
local = [TelemetryPoint(site_id="S1", ts=1000, lat=0.0, lon=0.0, metric="temp", value=20.0)]
|
||||||
|
remote = [TelemetryPoint(site_id="S1", ts=1000, lat=0.0, lon=0.0, metric="temp", value=22.5)]
|
||||||
|
merged = reconcile_deltas(local, remote)
|
||||||
|
assert len(merged) == 1
|
||||||
|
assert merged[0].value == 22.5
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
from idea78_solarsphere_offline_first.core import Asset, TelemetryPoint, WeatherLayer, Event
|
||||||
|
|
||||||
|
|
||||||
|
def test_asset_dataclass():
|
||||||
|
a = Asset(site_id="site-01", location={"lat": 12.34, "lon": 56.78}, layers=["terrain", "overlay"], version=1)
|
||||||
|
assert a.site_id == "site-01"
|
||||||
|
assert isinstance(a.layers, list) and a.layers[0] == "terrain"
|
||||||
|
|
||||||
|
|
||||||
|
def test_telemetry_point_dataclass():
|
||||||
|
t = TelemetryPoint(site_id="site-01", ts=1234567890, lat=12.3, lon=45.6, metric="temp", value=22.5)
|
||||||
|
assert t.metric == "temp" and t.value == 22.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_weather_event_dataclass():
|
||||||
|
w = WeatherLayer(region="region-1", ts=1000, param="wind", value=5.4, source="sim")
|
||||||
|
e = Event(ts=1000, site_id="site-01", type="maintenance", description="routine check", severity="low")
|
||||||
|
assert w.param == "wind" and e.severity == "low"
|
||||||
Loading…
Reference in New Issue