mycopunk-swag/cli/mycopunk/config.py

173 lines
4.9 KiB
Python

"""
Configuration management for mycopunk CLI.
"""
import os
from pathlib import Path
from typing import Optional, Any
import yaml
from dotenv import load_dotenv
def get_project_root() -> Path:
"""Find the project root (directory containing designs/)."""
current = Path.cwd()
while current != current.parent:
if (current / "designs").is_dir():
return current
current = current.parent
return Path.cwd()
PROJECT_ROOT = get_project_root()
CONFIG_DIR = PROJECT_ROOT / "config"
class Config:
"""Configuration manager for mycopunk."""
_instance: Optional["Config"] = None
def __new__(cls) -> "Config":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self) -> None:
if self._initialized:
return
self._initialized = True
self._products: dict = {}
self._pricing: dict = {}
# Load environment variables
env_path = CONFIG_DIR / ".env"
if env_path.exists():
load_dotenv(env_path)
# Load product configuration
products_path = CONFIG_DIR / "products.yaml"
if products_path.exists():
with open(products_path) as f:
self._products = yaml.safe_load(f) or {}
# Load pricing configuration
pricing_path = CONFIG_DIR / "pricing.yaml"
if pricing_path.exists():
with open(pricing_path) as f:
self._pricing = yaml.safe_load(f) or {}
@property
def env(self) -> str:
"""Get current environment (development/production)."""
return os.getenv("MYCOPUNK_ENV", "development")
@property
def is_production(self) -> bool:
"""Check if running in production mode."""
return self.env == "production"
@property
def debug(self) -> bool:
"""Check if debug mode is enabled."""
return os.getenv("DEBUG", "false").lower() == "true"
@property
def default_provider(self) -> str:
"""Get default POD provider."""
return os.getenv("DEFAULT_PROVIDER", "prodigi")
# API Keys
@property
def printful_token(self) -> Optional[str]:
"""Get Printful API token."""
return os.getenv("PRINTFUL_API_TOKEN")
@property
def prodigi_key(self) -> Optional[str]:
"""Get Prodigi API key (sandbox or live based on env)."""
if self.is_production:
return os.getenv("PRODIGI_API_KEY_LIVE")
return os.getenv("PRODIGI_API_KEY_SANDBOX")
@property
def stripe_key(self) -> Optional[str]:
"""Get Stripe secret key."""
return os.getenv("STRIPE_SECRET_KEY")
# Product Configuration
def get_product_config(self, product_type: str, size: str = "small") -> dict:
"""Get product configuration by type and size."""
type_config = self._products.get(product_type, {})
return type_config.get(size, {})
def get_provider_sku(
self,
product_type: str,
size: str,
provider: str
) -> Optional[str]:
"""Get provider-specific SKU for a product."""
config = self.get_product_config(product_type, size)
providers = config.get("providers", {})
provider_config = providers.get(provider, {})
return provider_config.get("sku")
def get_base_cost(
self,
product_type: str,
size: str,
provider: str,
variant: Optional[str] = None
) -> float:
"""Get base cost for a product from provider."""
config = self.get_product_config(product_type, size)
providers = config.get("providers", {})
provider_config = providers.get(provider, {})
base_cost = provider_config.get("base_cost", 0)
# Handle size-specific pricing (apparel)
if isinstance(base_cost, dict) and variant:
return base_cost.get(variant, 0)
return float(base_cost) if not isinstance(base_cost, dict) else 0
# Pricing
def calculate_retail_price(
self,
base_cost: float,
product_type: str
) -> float:
"""Calculate retail price from base cost with markup."""
pricing = self._pricing or self._products.get("pricing", {})
rules = pricing.get("rules", {})
type_rules = rules.get(product_type, {})
markup = type_rules.get("markup", pricing.get("default_markup", 2.0))
minimum = type_rules.get("minimum_price", 0)
calculated = base_cost * markup
price = max(calculated, minimum)
# Apply rounding
rounding = pricing.get("rounding", "nearest_99")
if rounding == "nearest_99":
price = round(price) - 0.01
elif rounding == "nearest_50":
price = round(price * 2) / 2
return max(price, minimum)
# Singleton accessor
def get_config() -> Config:
"""Get the configuration singleton."""
return Config()