from __future__ import annotations import asyncio import os from pathlib import Path from typing import Callable, Awaitable, Sequence from quart import Quart, request, g, send_from_directory from shared.config import init_config, config, pretty from shared.models import KV # ensure shared models imported # Register all app model classes with SQLAlchemy so cross-domain # relationship() string references resolve correctly. for _mod in ("blog.models", "market.models", "cart.models", "events.models"): try: __import__(_mod) except ImportError: pass from shared.log_config import configure_logging from shared.events import EventProcessor from shared.db.session import register_db from shared.browser.app.middleware import register as register_middleware from shared.browser.app.redis_cacher import register as register_redis from shared.browser.app.csrf import protect from shared.browser.app.errors import errors from .jinja_setup import setup_jinja from .user_loader import load_current_user # Async init of config (runs once at import) asyncio.run(init_config()) BASE_DIR = Path(__file__).resolve().parent.parent STATIC_DIR = str(BASE_DIR / "static") TEMPLATE_DIR = str(BASE_DIR / "browser" / "templates") def create_base_app( name: str, *, context_fn: Callable[[], Awaitable[dict]] | None = None, before_request_fns: Sequence[Callable[[], Awaitable[None]]] | None = None, domain_services_fn: Callable[[], None] | None = None, ) -> Quart: """ Create a Quart app with shared infrastructure. Parameters ---------- name: Application name (also used as CACHE_APP_PREFIX). context_fn: Async function returning a dict for template context. Each app provides its own — the cart app queries locally, while coop/market apps fetch via internal API. If not provided, a minimal default context is used. before_request_fns: Extra before-request hooks (e.g. cart_loader for the cart app). domain_services_fn: Callable that registers domain services on the shared registry. Each app provides its own — registering real impls for owned domains and stubs (or real impls) for others. """ if domain_services_fn is not None: domain_services_fn() from shared.services.widgets import register_all_widgets register_all_widgets() app = Quart( name, static_folder=STATIC_DIR, static_url_path="/static", template_folder=TEMPLATE_DIR, ) configure_logging(name) app.secret_key = os.getenv("SECRET_KEY", "dev-secret-key-change-me-777") # Session cookie shared across subdomains cookie_domain = os.getenv("SESSION_COOKIE_DOMAIN") # e.g. ".rose-ash.com" if cookie_domain: app.config["SESSION_COOKIE_DOMAIN"] = cookie_domain app.config["SESSION_COOKIE_NAME"] = "coop_session" # Ghost / Redis config app.config["GHOST_API_URL"] = os.getenv("GHOST_API_URL") app.config["GHOST_PUBLIC_URL"] = os.getenv("GHOST_PUBLIC_URL") app.config["GHOST_CONTENT_KEY"] = os.getenv("GHOST_CONTENT_API_KEY") app.config["REDIS_URL"] = os.getenv("REDIS_URL") # Cache app prefix for key namespacing app.config["CACHE_APP_PREFIX"] = name # --- infrastructure --- register_middleware(app) register_db(app) register_redis(app) setup_jinja(app) errors(app) # --- before-request hooks --- @app.before_request async def _route_log(): g.root = request.headers.get("x-forwarded-prefix", "/") g.scheme = request.scheme g.host = request.host @app.before_request async def _load_user(): await load_current_user() # Register any app-specific before-request hooks (e.g. cart loader) if before_request_fns: for fn in before_request_fns: app.before_request(fn) @app.before_request async def _csrf_protect(): await protect() # --- after-request hooks --- @app.after_request async def _add_hx_preserve_search_header(response): value = request.headers.get("X-Search") if value is not None: response.headers["HX-Preserve-Search"] = value return response # --- context processor --- if context_fn is not None: @app.context_processor async def _inject_base(): return await context_fn() else: # Minimal fallback (no cart, no menu_items) from .context import base_context @app.context_processor async def _inject_base(): return await base_context() # --- cleanup internal API client on shutdown --- @app.after_serving async def _close_internal_client(): from .internal_api import close_client await close_client() # --- event processor --- _event_processor = EventProcessor() # --- startup --- @app.before_serving async def _startup(): from shared.events.handlers import register_shared_handlers register_shared_handlers() await init_config() print(pretty()) await _event_processor.start() @app.after_serving async def _stop_event_processor(): await _event_processor.stop() # --- favicon --- @app.get("/favicon.ico") async def favicon(): return await send_from_directory("static", "favicon.ico") return app