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

1"""Atlas ecommerce service. 

2 

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""" 

6 

7import asyncio 

8from collections.abc import AsyncIterator 

9from contextlib import asynccontextmanager 

10import logging 

11from logging import getLogger 

12 

13from beanie import init_beanie 

14from fastapi import FastAPI 

15import logfire 

16from pymongo import AsyncMongoClient 

17 

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 

25 

26logger = getLogger(__name__) 

27 

28 

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". 

31 

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") 

44 

45 

46@asynccontextmanager 

47async def lifespan(fastapi_app: FastAPI) -> AsyncIterator[None]: 

48 settings = get_settings() 

49 secrets = await get_secrets() 

50 

51 mongo = AsyncMongoClient(secrets.mongo_uri.get_secret_value()) 

52 await init_beanie(database=mongo[settings.mongo_db_name], document_models=[OrderDb, ConversionDispatchDb]) 

53 

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)) 

58 

59 fastapi_app.state.settings = settings 

60 fastapi_app.state.secrets = secrets 

61 fastapi_app.state.auth_proxy = client 

62 

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() 

75 

76 

77app = FastAPI( 

78 title="Atlas Ecommerce", 

79 version="0.1.0", 

80 lifespan=lifespan, 

81) 

82 

83app.include_router(shopify.router) 

84 

85 

86@app.get("/health", tags=["health"]) 

87async def health() -> dict[str, str]: 

88 return {"status": "ok"}