Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 83 additions & 158 deletions core/business_manager.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()