Coverage for atlas_ecommerce/main.py: 57%
53 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-25 16:36 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-25 16:36 +0000
1"""Atlas ecommerce service.
3Receives Shopify order webhooks and provisions credential-less Atlas accounts so
4a preorder customer can claim theirs from the app when the device arrives.
5"""
7import asyncio
8from collections.abc import AsyncIterator
9from contextlib import asynccontextmanager
10import logging
11from logging import getLogger
13from beanie import init_beanie
14from fastapi import FastAPI
15import logfire
16from pymongo import AsyncMongoClient
18from atlas_ecommerce.clients.auth_proxy import EcommerceAuthProxyClient
19from atlas_ecommerce.config import get_secrets, get_settings
20from atlas_ecommerce.models import ConversionDispatchDb, OrderDb
21from atlas_ecommerce.purchases.shopify import ShopifyPurchases
22from atlas_ecommerce.routers import shopify
23from atlas_ecommerce.workers import conversions
24from atlas_ecommerce.workers.provision import run_worker
26logger = getLogger(__name__)
29def configure_logfire(fastapi_app: FastAPI, token: str | None, environment: str) -> None:
30 """Send traces to Logfire unless the token is absent or set to "local".
32 Must run before the server starts serving. instrument_fastapi() applied
33 later - from the lifespan, say - silently no-ops: Starlette has already
34 built its middleware stack, so nothing raises and no spans are ever
35 produced. See tests/test_observability.py.
36 """
37 if not token or token == "local":
38 return
39 logfire.configure(token=token, console=False, environment=environment)
40 logfire.instrument_fastapi(fastapi_app, excluded_urls="/health", capture_headers=True)
41 # The workers log outside any request span; without this handler a failed conversion never reaches Logfire.
42 logging.getLogger().addHandler(logfire.LogfireLoggingHandler(level=logging.WARNING))
43 logger.info("Logfire logging configured")
46@asynccontextmanager
47async def lifespan(fastapi_app: FastAPI) -> AsyncIterator[None]:
48 settings = get_settings()
49 secrets = await get_secrets()
51 mongo = AsyncMongoClient(secrets.mongo_uri.get_secret_value())
52 await init_beanie(database=mongo[settings.mongo_db_name], document_models=[OrderDb, ConversionDispatchDb])
54 client = EcommerceAuthProxyClient(base_url=settings.auth_proxy_base_url)
55 worker = asyncio.create_task(run_worker(client, settings))
56 conversion_clients = conversions.build_clients(settings, secrets)
57 conversion_worker = asyncio.create_task(conversions.run_worker([ShopifyPurchases()], conversion_clients, settings))
59 fastapi_app.state.settings = settings
60 fastapi_app.state.secrets = secrets
61 fastapi_app.state.auth_proxy = client
63 try:
64 yield
65 finally:
66 for task in (worker, conversion_worker):
67 task.cancel()
68 try:
69 await task
70 except asyncio.CancelledError:
71 pass
72 await client.close()
73 await conversion_clients.close()
74 await mongo.close()
77app = FastAPI(
78 title="Atlas Ecommerce",
79 version="0.1.0",
80 lifespan=lifespan,
81)
83app.include_router(shopify.router)
86@app.get("/health", tags=["health"])
87async def health() -> dict[str, str]:
88 return {"status": "ok"}