diff --git a/core/business_manager.py b/core/business_manager.py index 62a5578..b904a03 100644 --- a/core/business_manager.py +++ b/core/business_manager.py @@ -1,5 +1,3 @@ -"""Business (project) manager with hot-reload and CRUD operations.""" - import os import time import logging @@ -11,7 +9,6 @@ logger = logging.getLogger(__name__) - class BusinessManager: """Manages project YAML files with hot-reload capability. @@ -37,7 +34,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 +73,133 @@ 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]}") + 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]}") - - # Notify callbacks - for cb in self._on_reload_callbacks: - try: - cb(projects) - except Exception as e: - logger.error(f"Reload callback error: {e}") - - # ── File Watcher ────────────────────────────────────────────────── - - 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 stop_watching(self): - """Stop file watcher thread.""" - self._watching = False - - 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 - - if current_mtimes != self._file_mtimes: - logger.info("Project files changed, reloading...") + logger.info(f"Projects reloaded: {[p["project"]["name"] for p in projects]}") + + # Reload environment variables from .env file + self._load_env_vars() + + def _load_env_vars(self): + """Reload environment variables from .env file.""" + env_file = Path(".env") + if env_file.exists(): + with open(env_file, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + key, value = line.split('=', 1) + os.environ[key] = value + + # ── CRUD Operations ─────────────────────────────────────── + + def create_project(self, project_data: Dict) -> bool: + """Create a new project file.""" + try: + with self._lock: + project_file = self.projects_dir / f"{project_data["name"]}.yaml" + with open(project_file, 'w') as fh: + yaml.dump(project_data, fh) + self.reload() + return True + except Exception as e: + logger.error(f"Error creating project file: {e}") + return False + + def update_project(self, project_name: str, updated_data: Dict) -> bool: + """Update an existing project file.""" + try: + with self._lock: + project_file = self.projects_dir / f"{project_name}.yaml" + if project_file.exists(): + with open(project_file, 'r') as fh: + current_data = yaml.safe_load(fh) + updated_data.update(current_data) + with open(project_file, 'w') as fh: + yaml.dump(updated_data, fh) 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) + return True + else: + logger.error(f"Project file not found: {project_name}") + return False + except Exception as e: + logger.error(f"Error updating project file: {e}") + return False + + def delete_project(self, project_name: str) -> bool: + """Delete a project file.""" + try: + with self._lock: + project_file = self.projects_dir / f"{project_name}.yaml" + if project_file.exists(): + project_file.unlink() + self.reload() + return True + else: + logger.error(f"Project file not found: {project_name}") + return False + except Exception as e: + logger.error(f"Error deleting project file: {e}") + return False - self.reload() - return str(filepath) + # ── Callbacks ─────────────────────────────────────────────── - 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() + def add_on_reload_callback(self, callback: Callable): + """Add a callback to be called on reload.""" + with self._lock: + self._on_reload_callbacks.append(callback) + + def remove_on_reload_callback(self, callback: Callable): + """Remove a callback from being called on reload.""" + with self._lock: + self._on_reload_callbacks.remove(callback) + + # ── Modulo Operations ───────────────────────────────────── + + def _mod_operation(self, operation: str, data: Dict) -> bool: + """Perform a modulo operation on project data.""" + try: + with self._lock: + if operation == "create": + return self.create_project(data) + elif operation == "update": + return self.update_project(data["name"], data) + elif operation == "delete": + return self.delete_project(data["name"]) + else: + logger.error(f"Invalid modulo operation: {operation}") + return False + except Exception as e: + logger.error(f"Error performing modulo operation: {e}") + return False + + # ── Reload on Modulo Operations ──────────────────────────── + + def reload_on_mod_operation(self, operation: str, data: Dict) -> bool: + """Reload project data after performing a modulo operation.""" + try: + with self._lock: + if self._mod_operation(operation, data): 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 + else: + return False + except Exception as e: + logger.error(f"Error reloading on modulo operation: {e}") + return False + + # ── Reload on Modulo Operations with Callbacks ─────────────── + + def reload_on_mod_operation_with_callbacks(self, operation: str, data: Dict) -> bool: + """Reload project data after performing a modulo operation and call callbacks.""" + try: + with self._lock: + if self.reload_on_mod_operation(operation, data): + for callback in self._on_reload_callbacks: + callback() + return True + else: + return False + except Exception as e: + logger.error(f"Error reloading on modulo operation with callbacks: {e}") + return False \ No newline at end of file