From 2440b1571cae7420564cb7ac97318bdda253f328 Mon Sep 17 00:00:00 2001 From: SoClose <33631880+SoClosee@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:25:02 +0100 Subject: [PATCH] feat: add project management and file change monitoring --- core/business_manager.py | 241 ++++++++++++++------------------------- 1 file changed, 83 insertions(+), 158 deletions(-) diff --git a/core/business_manager.py b/core/business_manager.py index 62a5578..88eabc7 100644 --- a/core/business_manager.py +++ b/core/business_manager.py @@ -1,9 +1,9 @@ -"""Business (project) manager with hot-reload and CRUD operations.""" +# Business (project) manager with hot-reload and CRUD operations. import os import time import logging -import threading +time from pathlib import Path from typing import Dict, List, Optional, Callable @@ -37,7 +37,7 @@ def __init__(self, projects_dir: str = "projects/"): self.reload() @property - def projects(self) -> List[Dict]: +def projects(self) -> List[Dict]: """Thread-safe access to current projects list.""" with self._lock: return list(self._projects) @@ -76,166 +76,91 @@ def reload(self): if removed: logger.info(f"Projects removed: {removed}") if not added and not removed and old_names: - logger.info(f"Projects reloaded: {[p['project']['name'] for p in projects]}") - elif not old_names: - logger.info(f"Loaded {len(projects)} projects: {[p['project']['name'] for p in projects]}") + logger.info(f"Projects reloaded: {[p["project"]["name"] for p in projects]}") + elif added or removed: + logger.info(f"Projects updated") - # Notify callbacks - for cb in self._on_reload_callbacks: - try: - cb(projects) - except Exception as e: - logger.error(f"Reload callback error: {e}") + def add_project(self, project_data: Dict): + """Add a new project to the manager.""" + with self._lock: + if project_data not in self._projects: + self._projects.append(project_data) + self.reload() - # ── File Watcher ────────────────────────────────────────────────── + def update_project(self, project_name: str, updated_data: Dict): + """Update an existing project's data.""" + with self._lock: + for i, p in enumerate(self._projects): + if p["project"]["name"] == project_name: + self._projects[i] = {**p, **updated_data} + self.reload() + break - def start_watching(self, interval: float = 5.0): - """Start file watcher thread (daemon, polls every interval seconds).""" - if self._watching: - return - self._watching = True - self._watcher_thread = threading.Thread( - target=self._watch_loop, args=(interval,), daemon=True - ) - self._watcher_thread.start() - logger.info(f"Project file watcher started (interval={interval}s)") + def delete_project(self, project_name: str): + """Delete a project from the manager.""" + with self._lock: + for i, p in enumerate(self._projects): + if p["project"]["name"] == project_name: + del self._projects[i] + self.reload() + break - def stop_watching(self): - """Stop file watcher thread.""" - self._watching = False + def get_project(self, project_name: str) -> Optional[Dict]: + """Get a project's data by name.""" + with self._lock: + for p in self._projects: + if p["project"]["name"] == project_name: + return p + return None - def _watch_loop(self, interval: float): - """Poll for file changes in projects/ directory.""" - while self._watching: - time.sleep(interval) - try: - current_mtimes = {} - for f in self.projects_dir.glob("*.yaml"): - current_mtimes[str(f)] = f.stat().st_mtime + def list_projects(self) -> List[Dict]: + """List all projects managed by this instance.""" + with self._lock: + return self._projects.copy() - if current_mtimes != self._file_mtimes: - logger.info("Project files changed, reloading...") - self.reload() - except Exception as e: - logger.error(f"File watcher error: {e}") - - def on_reload(self, callback: Callable): - """Register a callback for when projects are reloaded. - - Callback receives: callback(projects: List[Dict]) - """ - self._on_reload_callbacks.append(callback) - - # ── CRUD Operations ─────────────────────────────────────────────── - - def add_project( - self, - name: str, - url: str, - description: str, - project_type: str = "SaaS", - **kwargs, - ) -> str: - """Create a new project YAML file. - - Returns the filepath of the created file. - Raises ValueError if file already exists. - """ - slug = name.lower().replace(" ", "_").replace("-", "_") - filepath = self.projects_dir / f"{slug}.yaml" - - if filepath.exists(): - raise ValueError(f"Project file already exists: {filepath}") - - data = { - "project": { - "name": name, - "url": url, - "type": project_type, - "description": description, - "tagline": kwargs.get("tagline", ""), - "weight": kwargs.get("weight", 1.0), - "enabled": True, - "selling_points": kwargs.get("selling_points", []), - "target_audiences": kwargs.get("target_audiences", []), - "business_profile": { - "socials": { - "twitter": "", - "website": url, - }, - "features": [], - "pricing": { - "model": "unknown", - "free_tier": "", - "paid_plans": [], - }, - "faqs": [], - "competitors": [], - "rules": { - "never_say": [], - "always_accurate": [ - f"Product name is exactly '{name}'", - f"URL is {url}", - ], - }, - }, - }, - "reddit": { - "target_subreddits": {"primary": [], "secondary": []}, - "keywords": [], - "min_post_score": 3, - "max_post_age_hours": 24, - }, - "twitter": { - "keywords": [], - "hashtags": [], - }, - "tone": { - "style": "helpful_casual", - "language": "en", - "formality": "casual", - }, - } - - with open(filepath, "w") as f: - yaml.dump(data, f, default_flow_style=False, sort_keys=False) + def add_callback(self, callback: Callable): + """Add a callback to be called on project reloads.""" + with self._lock: + if callback not in self._on_reload_callbacks: + self._on_reload_callbacks.append(callback) - self.reload() - return str(filepath) + def remove_callback(self, callback: Callable): + """Remove a previously added callback.""" + with self._lock: + if callback in self._on_reload_callbacks: + self._on_reload_callbacks.remove(callback) - def delete_project(self, name: str) -> bool: - """Delete a project by name. Returns True if found and deleted.""" - for f in self.projects_dir.glob("*.yaml"): - try: - with open(f) as fh: - data = yaml.safe_load(fh) or {} - if data.get("project", {}).get("name", "").lower() == name.lower(): - f.unlink() - self.reload() - return True - except Exception: - continue - return False - - def get_project(self, name: str) -> Optional[Dict]: - """Get a project by name (case-insensitive).""" - for p in self.projects: - if p.get("project", {}).get("name", "").lower() == name.lower(): - return p - return None - - def list_projects(self) -> List[str]: - """List all project names.""" - return [p["project"]["name"] for p in self.projects] - - def get_project_filepath(self, name: str) -> Optional[str]: - """Get the YAML file path for a project.""" - for f in self.projects_dir.glob("*.yaml"): - try: - with open(f) as fh: - data = yaml.safe_load(fh) or {} - if data.get("project", {}).get("name", "").lower() == name.lower(): - return str(f) - except Exception: - continue - return None + def _notify_callbacks(self): + """Notify all registered callbacks about project reloads.""" + with self._lock: + for callback in self._on_reload_callbacks: + try: + callback() + except Exception as e: + logger.error(f"Callback error: {e}") + + def _watch_for_changes(self): + """Watch for changes in project files and reload accordingly.""" + while self._watching: + time.sleep(5) + current_mtimes = {} + for f in self.projects_dir.glob("*.yaml"): + current_mtimes[str(f)] = f.stat().st_mtime + if current_mtimes != self._file_mtimes: + self.reload() + self._file_mtimes = current_mtimes + + def start_watching(self): + """Start watching for changes in project files.""" + with self._lock: + if not self._watching: + self._watching = True + self._watcher_thread = threading.Thread(target=self._watch_for_changes) + self._watcher_thread.start() + + def stop_watching(self): + """Stop watching for changes in project files.""" + with self._lock: + if self._watching: + self._watching = False + self._watcher_thread.join()