Files
AIPass/src/aipass/trigger/apps/handlers/json/json_handler.py
T
AIPassandClaude Opus 4.6 26bb4a3157 feat(system): S65 night shift — fix 14 branches from fresh-eyes CLI testing (#147)
* docs: update HERALD.md — S63-S64, standards 32, fresh-eyes CLI results

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(system): S65 night shift — fix 14 branches from fresh-eyes CLI testing (FPLAN-0154)

14 branches dispatched across 3 phases, all fixes verified.

Phase 1 (critical): backup dry-run flag check, ai_mail per-ID branch detection,
flow introspection gate removal, drone atomic writes + empty file guard,
commons 3-strategy caller detection.

Phase 2 (patterns): spawn argparse --help + create --dry-run, skills create --help,
api introspection routing fix (6 commands), trigger empty JSON guard,
seedgo @ prefix consistency + conftest.

Phase 3 (polish): prax ghost module archived + real status output, daemon update
digest + 13 error cascade fixes + branch-health all-branches, memory search
timeout message + hidden modules in help, cli drone_adapter archived + standalone verified.

3,410 tests (+80), seedgo 99% avg, 0 failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 13:29:48 -07:00

291 lines
8.9 KiB
Python

# =================== AIPass ====================
# Name: json_handler.py
# Description: JSON auto-creating handler for trigger data files
# Version: 1.1.0
# Created: 2025-11-13
# Modified: 2025-11-21
# =============================================
import json
from pathlib import Path
from datetime import datetime, timezone
from typing import Dict, List, Any, Optional
import inspect
# Infrastructure
_LOG_FILE = Path(__file__).parent.parent.parent.parent / "logs" / "json_handler.log"
def _log_warning(msg: str) -> None:
"""File-based warning logger to avoid circular imports with prax."""
try:
_LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(_LOG_FILE, 'a', encoding='utf-8') as f:
f.write(f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} | WARNING | {msg}\n")
except Exception:
pass # Meta-logging: cannot log a failure to log
# Constants
TRIGGER_ROOT = Path(__file__).resolve().parents[3]
TRIGGER_JSON_DIR = TRIGGER_ROOT / "trigger_json"
def _get_caller_module_name() -> str:
"""
Auto-detect calling module name from call stack
Returns:
Module name (e.g., "imports_standard" from imports_standard.py)
"""
stack = inspect.stack()
# Skip frames: [0]=this function, [1]=log_operation, [2]=actual caller
if len(stack) > 2:
caller_frame = stack[2]
caller_path = Path(caller_frame.filename)
module_name = caller_path.stem
# Validate module name
if module_name and not module_name.startswith('_'):
return module_name
# Fallback
return "unknown"
def _get_default_template(json_type: str, module_name: str) -> Any:
"""Return default JSON structure for a given type (inline, no file templates)."""
today = datetime.now().date().isoformat()
if json_type == "config":
return {
"module_name": module_name,
"version": "1.0.0",
"timestamp": today,
"config": {
"auto_save": True,
"enabled": True
}
}
elif json_type == "data":
return {
"module_name": module_name,
"created": today,
"last_updated": today,
"operations_total": 0,
"operations_successful": 0,
"operations_failed": 0
}
elif json_type == "log":
return []
raise ValueError(f"Unknown json_type: {json_type}")
def validate_json_structure(data: Any, json_type: str) -> bool:
"""Validate JSON structure matches expected type"""
if json_type == "config":
if not isinstance(data, dict):
return False
required = ["module_name", "version", "config"]
return all(key in data for key in required)
elif json_type == "data":
if not isinstance(data, dict):
return False
required = ["created", "last_updated"]
return all(key in data for key in required)
elif json_type == "log":
return isinstance(data, list)
return False
def get_json_path(module_name: str, json_type: str) -> Path:
"""Get path for module JSON file"""
filename = f"{module_name}_{json_type}.json"
return TRIGGER_JSON_DIR / filename
def ensure_json_exists(module_name: str, json_type: str) -> bool:
"""Ensure JSON file exists, create from template if missing"""
TRIGGER_JSON_DIR.mkdir(parents=True, exist_ok=True)
json_path = get_json_path(module_name, json_type)
if json_path.exists():
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if validate_json_structure(data, json_type):
return True
# If corrupted, fall through to regenerate
except Exception as exc:
# If unreadable, fall through to regenerate
_log_warning(f"ensure_json_exists failed for {module_name}_{json_type}: {exc}")
pass
template = _get_default_template(json_type, module_name)
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(template, f, indent=2, ensure_ascii=False)
return True
def load_json(module_name: str, json_type: str) -> Optional[Any]:
"""Load JSON file, auto-create if missing.
Guards against empty or corrupt JSON files by regenerating from template.
"""
if not ensure_json_exists(module_name, json_type):
return None
json_path = get_json_path(module_name, json_type)
try:
content = json_path.read_text(encoding='utf-8').strip()
if not content:
_log_warning(f"load_json empty file for {module_name}_{json_type}, will regenerate")
ensure_json_exists(module_name, json_type)
content = json_path.read_text(encoding='utf-8').strip()
return json.loads(content)
except (json.JSONDecodeError, OSError) as exc:
_log_warning(f"load_json failed for {module_name}_{json_type}: {exc}")
ensure_json_exists(module_name, json_type)
try:
return json.loads(json_path.read_text(encoding='utf-8'))
except Exception:
return _get_default_template(json_type, module_name)
def save_json(module_name: str, json_type: str, data: Any) -> bool:
"""Save JSON file"""
json_path = get_json_path(module_name, json_type)
if not validate_json_structure(data, json_type):
raise ValueError(f"Invalid structure for {json_type} JSON")
if json_type == "data" and isinstance(data, dict):
data["last_updated"] = datetime.now().date().isoformat()
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return True
def ensure_module_jsons(module_name: str) -> bool:
"""Ensure all 3 JSON files exist for a module"""
ensure_json_exists(module_name, "config")
ensure_json_exists(module_name, "data")
ensure_json_exists(module_name, "log")
return True
def log_operation(operation: str, data: Dict[str, Any] | None = None, module_name: str | None = None) -> bool:
"""
Add entry to module log with automatic rotation
Auto-detects calling module if module_name not provided.
Implements config-controlled log limits to prevent unbounded growth.
When max_log_entries is reached, removes oldest entries (FIFO).
Args:
operation: Operation name to log
data: Optional data dict
module_name: Optional module name (auto-detected if not provided)
Returns:
True if successful, False otherwise
"""
# Auto-detect module name if not provided
if module_name is None:
module_name = _get_caller_module_name()
ensure_module_jsons(module_name)
# Load config to get max_log_entries
config = load_json(module_name, "config")
max_entries = 100 # Default
if config and "config" in config:
max_entries = config["config"].get("max_log_entries", 100)
# Load existing log
log = load_json(module_name, "log")
if log is None:
log = []
# Create new entry
entry = {
"timestamp": datetime.now().isoformat(),
"operation": operation
}
if data:
entry["data"] = data # type: ignore[assignment]
# Add new entry
log.append(entry)
# Rotate if exceeds max (keep most recent entries)
if len(log) > max_entries:
log = log[-max_entries:]
return save_json(module_name, "log", log)
def increment_counter(module_name: str, counter_name: str, amount: int = 1) -> bool:
"""Increment a counter in data JSON"""
ensure_module_jsons(module_name)
data = load_json(module_name, "data")
if data is None:
return False
if counter_name not in data:
data[counter_name] = 0
data[counter_name] += amount
return save_json(module_name, "data", data)
def update_data_metrics(module_name: str, **metrics) -> bool:
"""Update data metrics"""
ensure_module_jsons(module_name)
data = load_json(module_name, "data")
if data is None:
return False
for key, value in metrics.items():
data[key] = value
return save_json(module_name, "data", data)
if __name__ == "__main__":
from rich.console import Console
from rich.panel import Panel
console = Console()
console.print()
console.print(Panel.fit(
"[bold cyan]JSON HANDLER - Working Implementation[/bold cyan]",
border_style="bright_blue"
))
console.print()
console.print("[yellow]TESTING:[/yellow] Creating trigger JSONs...")
# Test auto-creation
log_operation("test_operation", {"test": "data"}, "trigger")
increment_counter("trigger", "test_counter", 1)
update_data_metrics("trigger", test_metric="working")
console.print()
console.print("[green]Check trigger/trigger_json/ for created files:[/green]")
console.print(" [dim]•[/dim] trigger_config.json")
console.print(" [dim]•[/dim] trigger_data.json")
console.print(" [dim]•[/dim] trigger_log.json")
console.print()