feat: extract shared infrastructure from shared_lib
Phase 1-3 of decoupling plan: - Shared DB, models, infrastructure, browser, config, utils - Event infrastructure (domain_events outbox, bus, processor) - Structured logging - Generic container concept (container_type/container_id) - Alembic migrations for all schema changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
153
infrastructure/factory.py
Normal file
153
infrastructure/factory.py
Normal file
@@ -0,0 +1,153 @@
|
||||
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 models imported
|
||||
from shared.logging 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,
|
||||
) -> 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).
|
||||
"""
|
||||
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():
|
||||
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
|
||||
Reference in New Issue
Block a user