Add decision journal, metrics, ideas, weather, standup - decide.py: Decision journal with outcomes and lessons - metrics.py: Track arbitrary metrics over time - ideas.py: Idea incubator with stages - weather.py: Quick weather via wttr.in - standup.py: Daily standup generator - 27 tools total now
This commit is contained in:
208
tools/metrics.py
Executable file
208
tools/metrics.py
Executable file
@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
metrics - Track arbitrary metrics over time
|
||||
|
||||
Log numeric values and see trends.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from statistics import mean, stdev
|
||||
|
||||
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
|
||||
METRICS_FILE = WORKSPACE / "data" / "metrics.json"
|
||||
|
||||
def load_metrics() -> dict:
|
||||
"""Load metrics data."""
|
||||
METRICS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
if METRICS_FILE.exists():
|
||||
with open(METRICS_FILE) as f:
|
||||
return json.load(f)
|
||||
return {'metrics': {}}
|
||||
|
||||
def save_metrics(data: dict):
|
||||
"""Save metrics data."""
|
||||
with open(METRICS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def define_metric(name: str, unit: str = '', description: str = ''):
|
||||
"""Define a new metric to track."""
|
||||
data = load_metrics()
|
||||
|
||||
key = name.lower().replace(' ', '_')
|
||||
if key in data['metrics']:
|
||||
print(f"Metric already exists: {name}")
|
||||
return
|
||||
|
||||
data['metrics'][key] = {
|
||||
'name': name,
|
||||
'unit': unit,
|
||||
'description': description,
|
||||
'entries': [],
|
||||
'created': datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
save_metrics(data)
|
||||
print(f"📊 Created metric: {name}")
|
||||
|
||||
def log_value(name: str, value: float, note: str = None):
|
||||
"""Log a value for a metric."""
|
||||
data = load_metrics()
|
||||
key = name.lower().replace(' ', '_')
|
||||
|
||||
# Find metric by partial match
|
||||
matches = [k for k in data['metrics'] if key in k]
|
||||
if not matches:
|
||||
print(f"Metric not found: {name}")
|
||||
print("Create with: metrics define <name>")
|
||||
return
|
||||
|
||||
key = matches[0]
|
||||
metric = data['metrics'][key]
|
||||
|
||||
entry = {
|
||||
'value': value,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'note': note,
|
||||
}
|
||||
|
||||
metric['entries'].append(entry)
|
||||
save_metrics(data)
|
||||
|
||||
unit = metric.get('unit', '')
|
||||
print(f"📈 {metric['name']}: {value}{unit}")
|
||||
|
||||
def show_metric(name: str, days: int = 7):
|
||||
"""Show metric details and trend."""
|
||||
data = load_metrics()
|
||||
key = name.lower().replace(' ', '_')
|
||||
|
||||
matches = [k for k in data['metrics'] if key in k]
|
||||
if not matches:
|
||||
print(f"Metric not found: {name}")
|
||||
return
|
||||
|
||||
key = matches[0]
|
||||
metric = data['metrics'][key]
|
||||
entries = metric['entries']
|
||||
|
||||
print(f"\n📊 {metric['name']}")
|
||||
if metric.get('description'):
|
||||
print(f" {metric['description']}")
|
||||
print("=" * 40)
|
||||
|
||||
if not entries:
|
||||
print("No data yet")
|
||||
return
|
||||
|
||||
# Filter to date range
|
||||
cutoff = datetime.now() - timedelta(days=days)
|
||||
recent = [e for e in entries if datetime.fromisoformat(e['timestamp']) > cutoff]
|
||||
|
||||
if not recent:
|
||||
print(f"No data in last {days} days")
|
||||
return
|
||||
|
||||
values = [e['value'] for e in recent]
|
||||
unit = metric.get('unit', '')
|
||||
|
||||
# Stats
|
||||
avg = mean(values)
|
||||
latest = values[-1]
|
||||
high = max(values)
|
||||
low = min(values)
|
||||
|
||||
print(f"\n Last {days} days ({len(recent)} entries):")
|
||||
print(f" Latest: {latest}{unit}")
|
||||
print(f" Average: {avg:.1f}{unit}")
|
||||
print(f" High: {high}{unit}")
|
||||
print(f" Low: {low}{unit}")
|
||||
|
||||
# Trend
|
||||
if len(values) >= 2:
|
||||
first_half = mean(values[:len(values)//2])
|
||||
second_half = mean(values[len(values)//2:])
|
||||
if second_half > first_half * 1.05:
|
||||
print(f" Trend: 📈 Up")
|
||||
elif second_half < first_half * 0.95:
|
||||
print(f" Trend: 📉 Down")
|
||||
else:
|
||||
print(f" Trend: ➡️ Stable")
|
||||
|
||||
# Recent entries
|
||||
print(f"\n Recent:")
|
||||
for e in recent[-5:]:
|
||||
date = e['timestamp'][:10]
|
||||
note = f" ({e['note']})" if e.get('note') else ""
|
||||
print(f" {date}: {e['value']}{unit}{note}")
|
||||
|
||||
print()
|
||||
|
||||
def list_metrics():
|
||||
"""List all metrics."""
|
||||
data = load_metrics()
|
||||
|
||||
if not data['metrics']:
|
||||
print("No metrics defined yet")
|
||||
print("Create with: metrics define <name> [unit] [description]")
|
||||
return
|
||||
|
||||
print(f"\n📊 Metrics ({len(data['metrics'])})")
|
||||
print("=" * 40)
|
||||
|
||||
for key, metric in data['metrics'].items():
|
||||
entries = len(metric['entries'])
|
||||
unit = metric.get('unit', '')
|
||||
|
||||
if entries > 0:
|
||||
latest = metric['entries'][-1]['value']
|
||||
print(f" {metric['name']}: {latest}{unit} ({entries} entries)")
|
||||
else:
|
||||
print(f" {metric['name']}: (no data)")
|
||||
|
||||
print()
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage:")
|
||||
print(" metrics define <name> [unit] [description]")
|
||||
print(" metrics log <name> <value> [note]")
|
||||
print(" metrics show <name> [days]")
|
||||
print(" metrics list")
|
||||
print("")
|
||||
print("Examples:")
|
||||
print(" metrics define weight kg")
|
||||
print(" metrics log weight 75.5")
|
||||
print(" metrics show weight 30")
|
||||
list_metrics()
|
||||
return
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == 'define' and len(sys.argv) > 2:
|
||||
name = sys.argv[2]
|
||||
unit = sys.argv[3] if len(sys.argv) > 3 else ''
|
||||
desc = ' '.join(sys.argv[4:]) if len(sys.argv) > 4 else ''
|
||||
define_metric(name, unit, desc)
|
||||
|
||||
elif cmd == 'log' and len(sys.argv) > 3:
|
||||
name = sys.argv[2]
|
||||
value = float(sys.argv[3])
|
||||
note = ' '.join(sys.argv[4:]) if len(sys.argv) > 4 else None
|
||||
log_value(name, value, note)
|
||||
|
||||
elif cmd == 'show' and len(sys.argv) > 2:
|
||||
name = sys.argv[2]
|
||||
days = int(sys.argv[3]) if len(sys.argv) > 3 else 7
|
||||
show_metric(name, days)
|
||||
|
||||
elif cmd == 'list':
|
||||
list_metrics()
|
||||
|
||||
else:
|
||||
print("Unknown command. Run 'metrics' for help.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user