""" DSS JSON Storage Layer Pure JSON file-based storage following DSS canonical structure. No SQLite - everything is JSON for git-friendly diffs. Structure: .dss/data/ ├── _system/ # DSS internal (config, cache, activity) ├── projects/ # Per-project data (tokens, components, etc.) └── teams/ # Team definitions """ import json import time import hashlib import fcntl from pathlib import Path from datetime import datetime, date from typing import Optional, Dict, List, Any, Union from contextlib import contextmanager from dataclasses import dataclass, asdict, field import uuid import os # Base paths DATA_DIR = Path(__file__).parent.parent.parent / ".dss" / "data" SYSTEM_DIR = DATA_DIR / "_system" PROJECTS_DIR = DATA_DIR / "projects" TEAMS_DIR = DATA_DIR / "teams" # Ensure directories exist for d in [DATA_DIR, SYSTEM_DIR, SYSTEM_DIR / "cache", SYSTEM_DIR / "activity", PROJECTS_DIR, TEAMS_DIR]: d.mkdir(parents=True, exist_ok=True) # === File Locking Utilities === @contextmanager def file_lock(path: Path, exclusive: bool = True): """Context manager for file locking.""" lock_path = path.with_suffix(path.suffix + ".lock") lock_path.parent.mkdir(parents=True, exist_ok=True) with open(lock_path, 'w') as lock_file: try: fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH) yield finally: fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) def read_json(path: Path, default: Any = None) -> Any: """Read JSON file with locking.""" if not path.exists(): return default with file_lock(path, exclusive=False): try: return json.loads(path.read_text()) except (json.JSONDecodeError, IOError): return default def write_json(path: Path, data: Any, indent: int = 2) -> None: """Write JSON file with locking.""" path.parent.mkdir(parents=True, exist_ok=True) with file_lock(path, exclusive=True): path.write_text(json.dumps(data, indent=indent, default=str)) def append_jsonl(path: Path, record: Dict) -> None: """Append to JSON Lines file.""" path.parent.mkdir(parents=True, exist_ok=True) with file_lock(path, exclusive=True): with open(path, 'a') as f: f.write(json.dumps(record, default=str) + '\n') def read_jsonl(path: Path, limit: int = None, offset: int = 0) -> List[Dict]: """Read JSON Lines file with pagination.""" if not path.exists(): return [] records = [] with file_lock(path, exclusive=False): with open(path, 'r') as f: lines = f.readlines() # Reverse for newest first lines = list(reversed(lines)) for i, line in enumerate(lines): if i < offset: continue if limit and len(records) >= limit: break try: records.append(json.loads(line.strip())) except json.JSONDecodeError: continue return records # === Cache (TTL-based) === class Cache: """TTL-based cache using JSON files.""" CACHE_DIR = SYSTEM_DIR / "cache" DEFAULT_TTL = 300 # 5 minutes @staticmethod def _key_to_path(key: str) -> Path: """Convert cache key to file path.""" key_hash = hashlib.md5(key.encode()).hexdigest() return Cache.CACHE_DIR / f"{key_hash}.json" @staticmethod def set(key: str, value: Any, ttl: int = None) -> None: """Store a value with TTL.""" ttl = ttl or Cache.DEFAULT_TTL data = { "key": key, "value": value, "created_at": int(time.time()), "expires_at": int(time.time()) + ttl } write_json(Cache._key_to_path(key), data) @staticmethod def get(key: str) -> Optional[Any]: """Get a value if not expired.""" path = Cache._key_to_path(key) data = read_json(path) if not data: return None if data.get("expires_at", 0) <= int(time.time()): path.unlink(missing_ok=True) return None return data.get("value") @staticmethod def delete(key: str) -> None: """Delete a cache entry.""" Cache._key_to_path(key).unlink(missing_ok=True) @staticmethod def clear_expired() -> int: """Remove all expired entries.""" count = 0 now = int(time.time()) for path in Cache.CACHE_DIR.glob("*.json"): data = read_json(path) if data and data.get("expires_at", 0) <= now: path.unlink(missing_ok=True) count += 1 return count @staticmethod def clear_all() -> None: """Clear entire cache.""" for path in Cache.CACHE_DIR.glob("*.json"): path.unlink(missing_ok=True) # === Projects === class Projects: """Project CRUD operations using JSON files.""" @staticmethod def _project_dir(project_id: str) -> Path: return PROJECTS_DIR / project_id @staticmethod def _manifest_path(project_id: str) -> Path: return Projects._project_dir(project_id) / "manifest.json" @staticmethod def _init_project_structure(project_id: str) -> None: """Initialize project folder structure.""" base = Projects._project_dir(project_id) for subdir in ["tokens", "components", "styles", "figma", "metrics"]: (base / subdir).mkdir(parents=True, exist_ok=True) @staticmethod def create(id: str, name: str, description: str = "", figma_file_key: str = "") -> Dict: """Create a new project.""" Projects._init_project_structure(id) now = datetime.utcnow().isoformat() manifest = { "id": id, "name": name, "description": description, "figma_file_key": figma_file_key, "status": "active", "created_at": now, "updated_at": now } write_json(Projects._manifest_path(id), manifest) # Initialize empty token files for canonical structure for token_type in ["colors", "spacing", "typography", "borders", "shadows", "motion"]: token_path = Projects._project_dir(id) / "tokens" / f"{token_type}.json" if not token_path.exists(): write_json(token_path, {"$type": token_type, "tokens": {}}) return manifest @staticmethod def get(id: str) -> Optional[Dict]: """Get project by ID.""" return read_json(Projects._manifest_path(id)) @staticmethod def list(status: str = None) -> List[Dict]: """List all projects.""" projects = [] for project_dir in PROJECTS_DIR.iterdir(): if project_dir.is_dir() and not project_dir.name.startswith("_"): manifest = read_json(project_dir / "manifest.json") if manifest: if status is None or manifest.get("status") == status: projects.append(manifest) # Sort by updated_at descending projects.sort(key=lambda p: p.get("updated_at", ""), reverse=True) return projects @staticmethod def update(id: str, **kwargs) -> Optional[Dict]: """Update project fields.""" manifest = Projects.get(id) if not manifest: return None manifest.update(kwargs) manifest["updated_at"] = datetime.utcnow().isoformat() write_json(Projects._manifest_path(id), manifest) return manifest @staticmethod def delete(id: str) -> bool: """Delete a project (moves to _archived).""" project_dir = Projects._project_dir(id) if not project_dir.exists(): return False # Move to archived instead of hard delete archived_dir = PROJECTS_DIR / "_archived" archived_dir.mkdir(exist_ok=True) import shutil shutil.move(str(project_dir), str(archived_dir / f"{id}_{int(time.time())}")) return True # === Components === class Components: """Component operations using JSON files.""" @staticmethod def _components_dir(project_id: str) -> Path: return PROJECTS_DIR / project_id / "components" @staticmethod def _component_path(project_id: str, component_id: str) -> Path: # Sanitize component name for filesystem safe_name = component_id.replace("/", "_").replace("\\", "_") return Components._components_dir(project_id) / f"{safe_name}.json" @staticmethod def upsert(project_id: str, components: List[Dict]) -> int: """Bulk upsert components.""" count = 0 now = datetime.utcnow().isoformat() for comp in components: comp_id = comp.get("id") or f"{project_id}-{comp['name']}" existing = Components.get(comp_id, project_id) component_data = { "id": comp_id, "project_id": project_id, "name": comp["name"], "figma_key": comp.get("figma_key") or comp.get("key"), "description": comp.get("description", ""), "properties": comp.get("properties", {}), "variants": comp.get("variants", []), "code_generated": comp.get("code_generated", False), "created_at": existing.get("created_at", now) if existing else now, "updated_at": now } write_json(Components._component_path(project_id, comp_id), component_data) count += 1 return count @staticmethod def list(project_id: str) -> List[Dict]: """List all components for a project.""" components = [] comp_dir = Components._components_dir(project_id) if not comp_dir.exists(): return [] for path in comp_dir.glob("*.json"): comp = read_json(path) if comp: components.append(comp) components.sort(key=lambda c: c.get("name", "")) return components @staticmethod def get(id: str, project_id: str = None) -> Optional[Dict]: """Get component by ID.""" if project_id: return read_json(Components._component_path(project_id, id)) # Search all projects for project_dir in PROJECTS_DIR.iterdir(): if project_dir.is_dir(): comp = read_json(Components._component_path(project_dir.name, id)) if comp: return comp return None # === Tokens === class Tokens: """Token operations following DSS canonical structure.""" CANONICAL_TYPES = ["colors", "spacing", "typography", "borders", "shadows", "motion"] @staticmethod def _tokens_dir(project_id: str) -> Path: return PROJECTS_DIR / project_id / "tokens" @staticmethod def get_all(project_id: str) -> Dict[str, Dict]: """Get all tokens for a project, organized by type.""" tokens = {} tokens_dir = Tokens._tokens_dir(project_id) for token_type in Tokens.CANONICAL_TYPES: path = tokens_dir / f"{token_type}.json" data = read_json(path, {"$type": token_type, "tokens": {}}) tokens[token_type] = data.get("tokens", {}) return tokens @staticmethod def get_by_type(project_id: str, token_type: str) -> Dict: """Get tokens of a specific type.""" path = Tokens._tokens_dir(project_id) / f"{token_type}.json" data = read_json(path, {"$type": token_type, "tokens": {}}) return data.get("tokens", {}) @staticmethod def set_by_type(project_id: str, token_type: str, tokens: Dict) -> None: """Set tokens of a specific type.""" path = Tokens._tokens_dir(project_id) / f"{token_type}.json" write_json(path, { "$type": token_type, "updated_at": datetime.utcnow().isoformat(), "tokens": tokens }) @staticmethod def merge(project_id: str, token_type: str, new_tokens: Dict, strategy: str = "LAST") -> Dict: """Merge tokens with strategy.""" existing = Tokens.get_by_type(project_id, token_type) if strategy == "FIRST": # Keep existing, only add new merged = {**new_tokens, **existing} elif strategy == "LAST": # Override with new merged = {**existing, **new_tokens} elif strategy == "MERGE_METADATA": # Deep merge merged = existing.copy() for key, value in new_tokens.items(): if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): merged[key] = {**merged[key], **value} else: merged[key] = value else: merged = {**existing, **new_tokens} Tokens.set_by_type(project_id, token_type, merged) return merged # === Styles === class Styles: """Style operations.""" STYLE_TYPES = ["TEXT", "FILL", "EFFECT", "GRID"] @staticmethod def _styles_dir(project_id: str) -> Path: return PROJECTS_DIR / project_id / "styles" @staticmethod def upsert(project_id: str, style_type: str, styles: List[Dict]) -> int: """Upsert styles of a given type.""" path = Styles._styles_dir(project_id) / f"{style_type.lower()}.json" existing_data = read_json(path, {"$type": style_type, "styles": []}) existing_styles = {s["id"]: s for s in existing_data.get("styles", [])} now = datetime.utcnow().isoformat() for style in styles: style["updated_at"] = now if style["id"] not in existing_styles: style["created_at"] = now existing_styles[style["id"]] = style write_json(path, { "$type": style_type, "updated_at": now, "styles": list(existing_styles.values()) }) return len(styles) @staticmethod def list(project_id: str, style_type: str = None) -> List[Dict]: """List styles, optionally filtered by type.""" styles = [] styles_dir = Styles._styles_dir(project_id) if not styles_dir.exists(): return [] types_to_check = [style_type.lower()] if style_type else [t.lower() for t in Styles.STYLE_TYPES] for st in types_to_check: path = styles_dir / f"{st}.json" data = read_json(path) if data: styles.extend(data.get("styles", [])) return styles # === Sync History === class SyncHistory: """Sync history using JSON Lines.""" @staticmethod def _history_path(project_id: str) -> Path: return PROJECTS_DIR / project_id / "figma" / "sync-history.jsonl" @staticmethod def start(project_id: str, sync_type: str) -> str: """Start a sync, returns sync ID.""" sync_id = str(uuid.uuid4())[:8] record = { "id": sync_id, "project_id": project_id, "sync_type": sync_type, "status": "running", "started_at": datetime.utcnow().isoformat(), "completed_at": None, "items_synced": 0, "changes": None, "error_message": None, "duration_ms": None } append_jsonl(SyncHistory._history_path(project_id), record) return sync_id @staticmethod def complete(project_id: str, sync_id: str, status: str, items_synced: int = 0, changes: Dict = None, error: str = None) -> None: """Complete a sync.""" path = SyncHistory._history_path(project_id) records = read_jsonl(path, limit=1000) # Find and update the record completed_at = datetime.utcnow().isoformat() for record in records: if record.get("id") == sync_id: started = datetime.fromisoformat(record["started_at"]) duration_ms = int((datetime.utcnow() - started).total_seconds() * 1000) # Append completion record completion = { "id": sync_id, "project_id": project_id, "sync_type": record.get("sync_type"), "status": status, "started_at": record["started_at"], "completed_at": completed_at, "items_synced": items_synced, "changes": changes, "error_message": error, "duration_ms": duration_ms } append_jsonl(path, completion) break @staticmethod def recent(project_id: str = None, limit: int = 20) -> List[Dict]: """Get recent sync history.""" if project_id: return read_jsonl(SyncHistory._history_path(project_id), limit=limit) # Aggregate from all projects all_records = [] for project_dir in PROJECTS_DIR.iterdir(): if project_dir.is_dir() and not project_dir.name.startswith("_"): records = read_jsonl(project_dir / "figma" / "sync-history.jsonl", limit=limit) all_records.extend(records) # Sort by started_at descending all_records.sort(key=lambda r: r.get("started_at", ""), reverse=True) return all_records[:limit] # === Activity Log === class ActivityLog: """Activity logging using daily JSON Lines files.""" CATEGORIES = { 'design_system': ['extract_tokens', 'extract_components', 'sync_tokens', 'validate_tokens'], 'code': ['analyze_components', 'find_inline_styles', 'generate_code', 'get_quick_wins'], 'configuration': ['config_updated', 'figma_token_updated', 'mode_changed', 'service_configured'], 'project': ['project_created', 'project_updated', 'project_deleted'], 'team': ['team_context_changed', 'project_context_changed'], 'storybook': ['scan_storybook', 'generate_story', 'generate_theme'] } @staticmethod def _log_path(day: date = None) -> Path: day = day or date.today() return SYSTEM_DIR / "activity" / f"{day.isoformat()}.jsonl" @staticmethod def log(action: str, entity_type: str = None, entity_id: str = None, entity_name: str = None, project_id: str = None, user_id: str = None, user_name: str = None, team_context: str = None, description: str = None, category: str = None, severity: str = 'info', details: Dict = None, ip_address: str = None, user_agent: str = None) -> None: """Log an activity.""" # Auto-detect category if not category: for cat, actions in ActivityLog.CATEGORIES.items(): if action in actions: category = cat break category = category or 'other' # Generate description if not provided if not description: entity_str = f"{entity_type} '{entity_name}'" if entity_name else (entity_type or "item") description = f"{action.replace('_', ' ').title()} {entity_str}" record = { "id": str(uuid.uuid4())[:12], "timestamp": datetime.utcnow().isoformat(), "action": action, "entity_type": entity_type, "entity_id": entity_id, "entity_name": entity_name, "project_id": project_id, "user_id": user_id, "user_name": user_name, "team_context": team_context, "category": category, "severity": severity, "description": description, "details": details, "ip_address": ip_address, "user_agent": user_agent } append_jsonl(ActivityLog._log_path(), record) @staticmethod def recent(project_id: str = None, limit: int = 50, offset: int = 0, days: int = 7) -> List[Dict]: """Get recent activity.""" all_records = [] # Read from recent days for i in range(days): day = date.today() - __import__('datetime').timedelta(days=i) records = read_jsonl(ActivityLog._log_path(day), limit=limit * 2) if project_id: records = [r for r in records if r.get("project_id") == project_id] all_records.extend(records) # Sort by timestamp descending all_records.sort(key=lambda r: r.get("timestamp", ""), reverse=True) return all_records[offset:offset + limit] @staticmethod def search(project_id: str = None, user_id: str = None, action: str = None, category: str = None, severity: str = None, days: int = 30, limit: int = 100, offset: int = 0) -> List[Dict]: """Search activity logs.""" all_records = [] for i in range(days): day = date.today() - __import__('datetime').timedelta(days=i) records = read_jsonl(ActivityLog._log_path(day)) for r in records: if project_id and r.get("project_id") != project_id: continue if user_id and r.get("user_id") != user_id: continue if action and r.get("action") != action: continue if category and r.get("category") != category: continue if severity and r.get("severity") != severity: continue all_records.append(r) all_records.sort(key=lambda r: r.get("timestamp", ""), reverse=True) return all_records[offset:offset + limit] # === Teams === class Teams: """Team management using JSON files.""" @staticmethod def _team_dir(team_id: str) -> Path: return TEAMS_DIR / team_id @staticmethod def _manifest_path(team_id: str) -> Path: return Teams._team_dir(team_id) / "manifest.json" @staticmethod def create(id: str, name: str, description: str = "") -> Dict: """Create a team.""" team_dir = Teams._team_dir(id) team_dir.mkdir(parents=True, exist_ok=True) now = datetime.utcnow().isoformat() manifest = { "id": id, "name": name, "description": description, "settings": {}, "created_at": now } write_json(Teams._manifest_path(id), manifest) write_json(team_dir / "members.json", {"members": []}) write_json(team_dir / "access.json", {"projects": {}}) return manifest @staticmethod def get(id: str) -> Optional[Dict]: """Get team by ID.""" return read_json(Teams._manifest_path(id)) @staticmethod def list() -> List[Dict]: """List all teams.""" teams = [] for team_dir in TEAMS_DIR.iterdir(): if team_dir.is_dir(): manifest = read_json(team_dir / "manifest.json") if manifest: teams.append(manifest) teams.sort(key=lambda t: t.get("name", "")) return teams @staticmethod def add_member(team_id: str, user_id: str, role: str) -> None: """Add or update team member.""" path = Teams._team_dir(team_id) / "members.json" data = read_json(path, {"members": []}) # Update or add members = data.get("members", []) for m in members: if m.get("user_id") == user_id: m["role"] = role m["updated_at"] = datetime.utcnow().isoformat() break else: members.append({ "user_id": user_id, "role": role, "joined_at": datetime.utcnow().isoformat() }) data["members"] = members write_json(path, data) @staticmethod def get_members(team_id: str) -> List[Dict]: """Get team members.""" path = Teams._team_dir(team_id) / "members.json" data = read_json(path, {"members": []}) return data.get("members", []) @staticmethod def get_user_role(team_id: str, user_id: str) -> Optional[str]: """Get user's role in team.""" members = Teams.get_members(team_id) for m in members: if m.get("user_id") == user_id: return m.get("role") return None @staticmethod def set_project_access(team_id: str, project_id: str, access_level: str) -> None: """Set team's access level to a project.""" path = Teams._team_dir(team_id) / "access.json" data = read_json(path, {"projects": {}}) data["projects"][project_id] = { "access_level": access_level, "granted_at": datetime.utcnow().isoformat() } write_json(path, data) # === Figma Files === class FigmaFiles: """Figma file management.""" @staticmethod def _files_path(project_id: str) -> Path: return PROJECTS_DIR / project_id / "figma" / "files.json" @staticmethod def create(project_id: str, figma_url: str, file_name: str, file_key: str) -> Dict: """Add a Figma file to project.""" path = FigmaFiles._files_path(project_id) data = read_json(path, {"files": []}) file_id = str(uuid.uuid4())[:8] now = datetime.utcnow().isoformat() new_file = { "id": file_id, "project_id": project_id, "figma_url": figma_url, "file_name": file_name, "file_key": file_key, "sync_status": "pending", "last_synced": None, "created_at": now } data["files"].append(new_file) write_json(path, data) return new_file @staticmethod def list(project_id: str) -> List[Dict]: """List Figma files for project.""" data = read_json(FigmaFiles._files_path(project_id), {"files": []}) return data.get("files", []) @staticmethod def update_sync_status(project_id: str, file_id: str, status: str) -> Optional[Dict]: """Update sync status.""" path = FigmaFiles._files_path(project_id) data = read_json(path, {"files": []}) for f in data.get("files", []): if f.get("id") == file_id: f["sync_status"] = status if status == "synced": f["last_synced"] = datetime.utcnow().isoformat() write_json(path, data) return f return None # === Metrics === class CodeMetrics: """Code metrics storage.""" @staticmethod def _metrics_path(project_id: str) -> Path: return PROJECTS_DIR / project_id / "metrics" / "code.json" @staticmethod def record(project_id: str, component_id: str, metrics: Dict) -> None: """Record code metrics for component.""" path = CodeMetrics._metrics_path(project_id) data = read_json(path, {"components": {}}) metrics["updated_at"] = datetime.utcnow().isoformat() data["components"][component_id] = metrics write_json(path, data) @staticmethod def get(project_id: str, component_id: str = None) -> Union[Dict, List[Dict]]: """Get metrics.""" data = read_json(CodeMetrics._metrics_path(project_id), {"components": {}}) if component_id: return data["components"].get(component_id) return data["components"] class TestResults: """Test results storage.""" @staticmethod def _results_path(project_id: str) -> Path: return PROJECTS_DIR / project_id / "metrics" / "tests.json" @staticmethod def record(project_id: str, component_id: str, test_type: str, passed: bool, score: float = None, failures: List[str] = None) -> Dict: """Record test result.""" path = TestResults._results_path(project_id) data = read_json(path, {"results": []}) result = { "id": str(uuid.uuid4())[:8], "component_id": component_id, "test_type": test_type, "passed": passed, "score": score, "failures": failures or [], "run_at": datetime.utcnow().isoformat() } data["results"].append(result) write_json(path, data) return result @staticmethod def list(project_id: str, component_id: str = None, test_type: str = None) -> List[Dict]: """List test results.""" data = read_json(TestResults._results_path(project_id), {"results": []}) results = data.get("results", []) if component_id: results = [r for r in results if r.get("component_id") == component_id] if test_type: results = [r for r in results if r.get("test_type") == test_type] results.sort(key=lambda r: r.get("run_at", ""), reverse=True) return results class TokenDrift: """Token drift tracking.""" @staticmethod def _drift_path(project_id: str) -> Path: return PROJECTS_DIR / project_id / "metrics" / "drift.json" @staticmethod def record(project_id: str, component_id: str, property_name: str, hardcoded_value: str, file_path: str, line_number: int, severity: str = "warning", suggested_token: str = None) -> Dict: """Record token drift.""" path = TokenDrift._drift_path(project_id) data = read_json(path, {"drift": []}) drift = { "id": str(uuid.uuid4())[:8], "component_id": component_id, "property_name": property_name, "hardcoded_value": hardcoded_value, "suggested_token": suggested_token, "severity": severity, "file_path": file_path, "line_number": line_number, "status": "pending", "detected_at": datetime.utcnow().isoformat() } data["drift"].append(drift) write_json(path, data) return drift @staticmethod def list(project_id: str, status: str = None, severity: str = None) -> List[Dict]: """List drift issues.""" data = read_json(TokenDrift._drift_path(project_id), {"drift": []}) drift = data.get("drift", []) if status: drift = [d for d in drift if d.get("status") == status] if severity: drift = [d for d in drift if d.get("severity") == severity] return drift @staticmethod def update_status(project_id: str, drift_id: str, status: str) -> Optional[Dict]: """Update drift status.""" path = TokenDrift._drift_path(project_id) data = read_json(path, {"drift": []}) for d in data.get("drift", []): if d.get("id") == drift_id: d["status"] = status write_json(path, data) return d return None # === Integrations === class Integrations: """Project integration configuration storage.""" @staticmethod def _integrations_path(project_id: str) -> Path: return PROJECTS_DIR / project_id / "integrations.json" @staticmethod def list(project_id: str, user_id: int = None) -> List[Dict]: """List integrations for a project.""" data = read_json(Integrations._integrations_path(project_id), {"integrations": []}) integrations = data.get("integrations", []) if user_id is not None: integrations = [i for i in integrations if i.get("user_id") == user_id] return integrations @staticmethod def get(project_id: str, user_id: int, integration_type: str) -> Optional[Dict]: """Get specific integration.""" integrations = Integrations.list(project_id, user_id) for i in integrations: if i.get("integration_type") == integration_type: return i return None @staticmethod def upsert(project_id: str, user_id: int, integration_type: str, config: str, enabled: bool = True) -> Dict: """Create or update integration.""" path = Integrations._integrations_path(project_id) data = read_json(path, {"integrations": []}) now = datetime.utcnow().isoformat() # Find existing for i in data["integrations"]: if i.get("user_id") == user_id and i.get("integration_type") == integration_type: i["config"] = config i["enabled"] = enabled i["updated_at"] = now write_json(path, data) return i # Create new new_integration = { "id": str(uuid.uuid4())[:8], "project_id": project_id, "user_id": user_id, "integration_type": integration_type, "config": config, "enabled": enabled, "created_at": now, "updated_at": now, "last_used_at": None } data["integrations"].append(new_integration) write_json(path, data) return new_integration @staticmethod def update(project_id: str, user_id: int, integration_type: str, config: str = None, enabled: bool = None) -> Optional[Dict]: """Update integration fields.""" path = Integrations._integrations_path(project_id) data = read_json(path, {"integrations": []}) for i in data["integrations"]: if i.get("user_id") == user_id and i.get("integration_type") == integration_type: if config is not None: i["config"] = config if enabled is not None: i["enabled"] = enabled i["updated_at"] = datetime.utcnow().isoformat() write_json(path, data) return i return None @staticmethod def delete(project_id: str, user_id: int, integration_type: str) -> bool: """Delete integration.""" path = Integrations._integrations_path(project_id) data = read_json(path, {"integrations": []}) original_len = len(data["integrations"]) data["integrations"] = [ i for i in data["integrations"] if not (i.get("user_id") == user_id and i.get("integration_type") == integration_type) ] if len(data["integrations"]) < original_len: write_json(path, data) return True return False class IntegrationHealth: """Integration health tracking.""" @staticmethod def _health_path() -> Path: return SYSTEM_DIR / "integration_health.json" @staticmethod def list_all() -> List[Dict]: """List all integration health status.""" data = read_json(IntegrationHealth._health_path(), {"health": {}}) return [ {"integration_type": k, **v} for k, v in data.get("health", {}).items() ] @staticmethod def get(integration_type: str) -> Optional[Dict]: """Get health for specific integration.""" data = read_json(IntegrationHealth._health_path(), {"health": {}}) if integration_type in data.get("health", {}): return {"integration_type": integration_type, **data["health"][integration_type]} return None @staticmethod def update(integration_type: str, is_healthy: bool = True, failure_count: int = None, circuit_open_until: str = None) -> Dict: """Update integration health.""" path = IntegrationHealth._health_path() data = read_json(path, {"health": {}}) if integration_type not in data["health"]: data["health"][integration_type] = { "is_healthy": True, "failure_count": 0, "last_failure_at": None, "last_success_at": None, "circuit_open_until": None } now = datetime.utcnow().isoformat() data["health"][integration_type]["is_healthy"] = is_healthy if is_healthy: data["health"][integration_type]["last_success_at"] = now else: data["health"][integration_type]["last_failure_at"] = now if failure_count is not None: data["health"][integration_type]["failure_count"] = failure_count if circuit_open_until is not None: data["health"][integration_type]["circuit_open_until"] = circuit_open_until write_json(path, data) return {"integration_type": integration_type, **data["health"][integration_type]} # === Stats === def get_stats() -> Dict: """Get storage statistics.""" stats = { "projects": len(list(PROJECTS_DIR.iterdir())) - 1 if PROJECTS_DIR.exists() else 0, # -1 for _archived "teams": len(list(TEAMS_DIR.iterdir())) if TEAMS_DIR.exists() else 0, "cache_files": len(list((SYSTEM_DIR / "cache").glob("*.json"))) if (SYSTEM_DIR / "cache").exists() else 0, "activity_days": len(list((SYSTEM_DIR / "activity").glob("*.jsonl"))) if (SYSTEM_DIR / "activity").exists() else 0, } # Calculate total size total_size = 0 for path in DATA_DIR.rglob("*"): if path.is_file(): total_size += path.stat().st_size stats["total_size_mb"] = round(total_size / (1024 * 1024), 2) return stats # === Initialization === def init_storage() -> None: """Initialize storage directories.""" for d in [DATA_DIR, SYSTEM_DIR, SYSTEM_DIR / "cache", SYSTEM_DIR / "activity", PROJECTS_DIR, TEAMS_DIR]: d.mkdir(parents=True, exist_ok=True) print(f"[Storage] JSON storage initialized at {DATA_DIR}") # Initialize on import init_storage() # === CLI === if __name__ == "__main__": import sys if len(sys.argv) > 1: cmd = sys.argv[1] if cmd == "stats": print(json.dumps(get_stats(), indent=2)) elif cmd == "init": init_storage() print("Storage initialized") elif cmd == "cache-test": Cache.set("test_key", {"foo": "bar"}, ttl=60) print(f"Set: test_key") print(f"Get: {Cache.get('test_key')}") elif cmd == "clear-cache": Cache.clear_all() print("Cache cleared") else: print("Usage: python json_store.py [stats|init|cache-test|clear-cache]") print(f"\nData directory: {DATA_DIR}") print(f"Stats: {json.dumps(get_stats(), indent=2)}")