feat(devpulse): API branch reinstate.
Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: json_handler.py
|
||||
# Description: JSON Auto-Creating Handler
|
||||
# Version: 1.1.0
|
||||
# Created: 2025-11-21
|
||||
# Modified: 2025-11-21
|
||||
# =============================================
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
import inspect
|
||||
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
# Infrastructure
|
||||
|
||||
# Constants — package-relative paths
|
||||
# Navigate: json_handler.py -> json/ -> handlers/ -> apps/ -> api/
|
||||
API_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
||||
API_JSON_DIR = API_ROOT / "api_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)
|
||||
"""
|
||||
try:
|
||||
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"
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to detect module name: {e}")
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _create_default(json_type: str, module_name: str) -> Any:
|
||||
"""Create default JSON structure for a given type."""
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
if json_type == "data":
|
||||
return {
|
||||
"module_name": module_name,
|
||||
"created": today,
|
||||
"last_updated": today,
|
||||
"operations_total": 0,
|
||||
"operations_successful": 0,
|
||||
"operations_failed": 0,
|
||||
}
|
||||
|
||||
if 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 API_JSON_DIR / filename
|
||||
|
||||
|
||||
def ensure_json_exists(module_name: str, json_type: str) -> bool:
|
||||
"""Ensure JSON file exists, create from template if missing"""
|
||||
API_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
|
||||
else:
|
||||
logger.warning(f"Corrupted JSON structure at {json_path}, regenerating")
|
||||
except Exception as e:
|
||||
logger.warning(f"Unreadable JSON at {json_path}, regenerating: {e}")
|
||||
|
||||
template = _create_default(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"""
|
||||
if not ensure_json_exists(module_name, json_type):
|
||||
return None
|
||||
|
||||
json_path = get_json_path(module_name, json_type)
|
||||
|
||||
try:
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load JSON from {json_path}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
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):
|
||||
return False
|
||||
|
||||
if json_type == "data" and isinstance(data, dict):
|
||||
data["last_updated"] = datetime.now().date().isoformat()
|
||||
|
||||
try:
|
||||
with open(json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save JSON to {json_path}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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 API JSONs...")
|
||||
|
||||
# Test auto-creation
|
||||
log_operation("test_operation", {"test": "data"}, "api")
|
||||
|
||||
console.print()
|
||||
console.print(f"[green]Check {API_JSON_DIR}/ for created files:[/green]")
|
||||
console.print(" [dim]•[/dim] api_config.json")
|
||||
console.print(" [dim]•[/dim] api_data.json")
|
||||
console.print(" [dim]•[/dim] api_log.json")
|
||||
console.print()
|
||||
Reference in New Issue
Block a user