add production readiness and recovery assets
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
"""Environment configuration validation for portable deployments."""
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
"""Raised when deployment configuration is unsafe or malformed."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Config:
|
||||
app_env: str
|
||||
data_dir: Path
|
||||
session_secret: str
|
||||
outreach_enabled: bool
|
||||
log_level: str
|
||||
|
||||
|
||||
def _env(values, key, default=""):
|
||||
return str(values.get(key, default) or "").strip()
|
||||
|
||||
|
||||
def load_config(values=None):
|
||||
values = os.environ if values is None else values
|
||||
app_env = _env(values, "APP_ENV", "development").lower()
|
||||
if app_env not in {"development", "test", "staging", "production"}:
|
||||
raise ConfigError("APP_ENV must be development, test, staging, or production")
|
||||
data_dir = Path(_env(values, "DATA_DIR", ".") or ".").expanduser()
|
||||
if not data_dir.is_absolute():
|
||||
data_dir = (Path.cwd() / data_dir).resolve()
|
||||
if data_dir.exists() and not data_dir.is_dir():
|
||||
raise ConfigError("DATA_DIR must be a directory")
|
||||
secret = _env(values, "SESSION_SECRET")
|
||||
if app_env == "production" and (len(secret) < 32 or secret.lower() in {"change-me", "development", "dev"}):
|
||||
raise ConfigError("SESSION_SECRET must be at least 32 characters in production")
|
||||
outreach = _env(values, "AUTOMATED_OUTREACH_ENABLED", "false").lower()
|
||||
if outreach not in {"", "0", "false", "no", "off"}:
|
||||
raise ConfigError("automated outreach is disabled in this release")
|
||||
log_level = _env(values, "LOG_LEVEL", "INFO").upper()
|
||||
if log_level not in {"QUIET", "ERROR", "WARNING", "INFO", "DEBUG"}:
|
||||
raise ConfigError("LOG_LEVEL is invalid")
|
||||
return Config(app_env, data_dir, secret, False, log_level)
|
||||
+10
-1
@@ -14,6 +14,7 @@ if __package__ in (None, ""):
|
||||
from app.contact_extractor import extract_contacts, MAX_HTML_BYTES, MAX_RESULTS
|
||||
from app.scoring import DEFAULT_RULES, signals_for_business, evaluate_score, SCORE_VERSION
|
||||
from app.ai_assistance import generate as generate_ai, MAX_INPUT_ITEMS, MAX_OUTPUT_CHARS
|
||||
from app.config import load_config
|
||||
else:
|
||||
from .domain import deduplication_key, deduplicate_businesses, is_suppressed, normalize_business, score_business, normalize_domain, normalize_phone, match_businesses
|
||||
from .sources import adapter_for, contains_secret
|
||||
@@ -22,6 +23,7 @@ else:
|
||||
from .contact_extractor import extract_contacts, MAX_HTML_BYTES, MAX_RESULTS
|
||||
from .scoring import DEFAULT_RULES, signals_for_business, evaluate_score, SCORE_VERSION
|
||||
from .ai_assistance import generate as generate_ai, MAX_INPUT_ITEMS, MAX_OUTPUT_CHARS
|
||||
from .config import load_config
|
||||
ORGANIZATION_ID = "demo-tenant"
|
||||
SCHEMA = Path(__file__).resolve().parents[1] / "schema.sql"
|
||||
SESSION_DAYS = 7
|
||||
@@ -521,7 +523,13 @@ class ApiHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def do_GET(self):
|
||||
parsed=urlparse(self.path); path=parsed.path.rstrip("/")
|
||||
if path=="/api/v1/health/live": return self.send_json(200,{"status":"ok","organization_id":ORGANIZATION_ID})
|
||||
if path=="/api/v1/health/live": return self.send_json(200,{"status":"ok","organization_id":ORGANIZATION_ID,"outreach_enabled":False})
|
||||
if path=="/api/v1/health/ready":
|
||||
try:
|
||||
check=connect(self.server.db_path); check.execute("SELECT 1"); check.close()
|
||||
return self.send_json(200,{"status":"ok","ready":True,"database":"ok","outreach_enabled":False})
|
||||
except (sqlite3.Error, OSError):
|
||||
return self.send_json(503,{"status":"not_ready","ready":False,"database":"error","outreach_enabled":False})
|
||||
db=self.db()
|
||||
try:
|
||||
user=self.require_auth(db)
|
||||
@@ -1284,6 +1292,7 @@ def _job_worker(server):
|
||||
finally: db.close()
|
||||
|
||||
def create_server(host="127.0.0.1",port=8000,db_path="prospects.db"):
|
||||
load_config()
|
||||
server=ThreadingHTTPServer((host,port),ApiHandler);server.db_path=db_path;connect(db_path).close();server.job_stop=threading.Event();server.job_wakeup=threading.Event();server.job_thread=threading.Thread(target=_job_worker,args=(server,),daemon=True);server.job_thread.start()
|
||||
original_close=server.server_close
|
||||
def close():
|
||||
|
||||
Reference in New Issue
Block a user