Coverage for app.py: 95%
93 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:56 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:56 +0000
1import logging
2import time
3from contextlib import asynccontextmanager
5import psycopg_pool
6from apscheduler.schedulers.asyncio import AsyncIOScheduler
7from decouple import config
8from fastapi import FastAPI, Request, Response
9from fastapi.middleware.cors import CORSMiddleware
10from fastapi.middleware.gzip import GZipMiddleware
11from psycopg import AsyncConnection
12from psycopg.rows import dict_row
13from starlette.responses import StreamingResponse
15from chalicelib.utils import helper
16from chalicelib.utils import pg_client, ch_client
17from chalicelib.utils.log import sanitize
18from crons import core_crons, core_dynamic_crons
19from routers import core, core_dynamic
20from routers.subs import health, spot, mcp
22loglevel = config("LOGLEVEL", default=logging.WARNING)
23print(f">Loglevel set to: {loglevel}")
24logging.basicConfig(level=loglevel)
25logger = logging.getLogger(__name__)
28class ORPYAsyncConnection(AsyncConnection):
29 def __init__(self, *args, **kwargs):
30 super().__init__(*args, row_factory=dict_row, **kwargs)
33@asynccontextmanager
34async def lifespan(app: FastAPI):
35 # Startup
36 logging.info(">>>>> starting up <<<<<")
37 ap_logger = logging.getLogger("apscheduler")
38 ap_logger.setLevel(loglevel)
40 app.schedule = AsyncIOScheduler()
41 await pg_client.init()
42 await ch_client.init()
43 app.schedule.start()
45 for job in core_crons.cron_jobs + core_dynamic_crons.cron_jobs:
46 app.schedule.add_job(id=job["func"].__name__, **job)
48 ap_logger.info(">Scheduled jobs:")
49 for job in app.schedule.get_jobs():
50 ap_logger.info(
51 {
52 "Name": str(job.id),
53 "Run Frequency": str(job.trigger),
54 "Next Run": str(job.next_run_time),
55 }
56 )
58 database = {
59 "host": config("pg_host", default="localhost"),
60 "dbname": config("pg_dbname", default="orpy"),
61 "user": config("pg_user", default="orpy"),
62 "password": config("pg_password", default="orpy"),
63 "port": config("pg_port", cast=int, default=5432),
64 "application_name": "AIO" + config("APP_NAME", default="PY"),
65 }
67 database = psycopg_pool.AsyncConnectionPool(
68 kwargs=database,
69 connection_class=ORPYAsyncConnection,
70 min_size=config("PG_AIO_MINCONN", cast=int, default=1),
71 max_size=config("PG_AIO_MAXCONN", cast=int, default=5),
72 )
73 await database.open()
74 app.state.postgresql = database
76 # App listening
77 yield
79 # Shutdown
80 await database.close()
81 logging.info(">>>>> shutting down <<<<<")
82 app.schedule.shutdown(wait=True)
83 await pg_client.terminate()
86app = FastAPI(
87 root_path=config("root_path", default="/api"),
88 docs_url=config("docs_url", default=""),
89 redoc_url=config("redoc_url", default=""),
90 lifespan=lifespan,
91)
92app.add_middleware(GZipMiddleware, minimum_size=1000)
94IGNORE_ENDPOINT_LOG = ["/"]
97@app.middleware("http")
98async def log_all_requests(request: Request, call_next):
99 method = sanitize(request.method, max_length=16)
100 endpoint = sanitize(request.url.path)
101 response: Response = await call_next(request)
102 # Log all endpoints except health check
103 if request.url.path not in IGNORE_ENDPOINT_LOG or response.status_code != 200:
104 logger.info(f"{method}:{endpoint} {response.status_code}")
105 return response
108@app.middleware("http")
109async def or_middleware(request: Request, call_next):
110 if helper.TRACK_TIME: 110 ↛ 112line 110 didn't jump to line 112 because the condition on line 110 was always true
111 now = time.time()
112 try:
113 response: StreamingResponse = await call_next(request)
114 except:
115 logging.error(f"{sanitize(request.method, max_length=16)}: {sanitize(request.url.path)} FAILED!")
116 raise
117 if response.status_code // 100 != 2:
118 logging.warning(
119 f"{sanitize(request.method, max_length=16)}:{sanitize(request.url.path)} {response.status_code}!")
120 if helper.TRACK_TIME: 120 ↛ 126line 120 didn't jump to line 126 because the condition on line 120 was always true
121 now = time.time() - now
122 if now > 2:
123 now = round(now, 2)
124 logging.warning(
125 f"Execution time: {now} s for {sanitize(request.method, max_length=16)}: {sanitize(request.url.path)}")
126 response.headers["x-robots-tag"] = 'noindex, nofollow'
127 return response
130origins = [
131 "*",
132]
134app.add_middleware(
135 CORSMiddleware,
136 allow_origins=origins,
137 allow_credentials=True,
138 allow_methods=["*"],
139 allow_headers=["*"],
140)
141app.include_router(core.public_app)
142app.include_router(core.app)
143app.include_router(core.app_apikey)
144app.include_router(core_dynamic.public_app)
145app.include_router(core_dynamic.app)
146app.include_router(core_dynamic.app_apikey)
147app.include_router(health.public_app)
148app.include_router(health.app)
149app.include_router(health.app_apikey)
151app.include_router(spot.public_app)
152app.include_router(spot.app)
153app.include_router(spot.app_apikey)
155app.include_router(mcp.app)
156app.include_router(mcp.public_app)