import os import httpx from fastapi import FastAPI, Query from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles CH_HOST = os.environ.get("CLICKHOUSE_HOST", "ClickHouse") CH_PORT = os.environ.get("CLICKHOUSE_PORT", "8123") CH_USER = os.environ.get("CLICKHOUSE_USER", "default") CH_PASSWORD = os.environ.get("CLICKHOUSE_PASSWORD", "1234567890") CH_URL = f"http://{CH_HOST}:{CH_PORT}/" app = FastAPI() FILTER_SQL = """ char_length(city) > 0 AND (length({cities:Array(String)}) = 0 OR city IN {cities:Array(String)}) AND (length({dimensions:Array(String)}) = 0 OR dimension IN {dimensions:Array(String)}) AND ( {search:String} = '' OR positionCaseInsensitive(address, {search:String}) > 0 OR positionCaseInsensitive(board_id, {search:String}) > 0 ) """ async def ch_query(sql: str, params: dict): form = {"query": sql, "default_format": "JSONEachRow"} for k, v in params.items(): if isinstance(v, list): form[f"param_{k}"] = "[" + ",".join("'" + str(x).replace("'", "\\'") + "'" for x in v) + "]" else: form[f"param_{k}"] = str(v) async with httpx.AsyncClient(timeout=15) as client: r = await client.post(CH_URL, params=form, auth=(CH_USER, CH_PASSWORD)) r.raise_for_status() text = r.text.strip() if not text: return [] import json return [json.loads(line) for line in text.splitlines()] def parse_filters(city: str, dimension: str, search: str): cities = [c for c in city.split(",") if c] if city else [] dimensions = [d for d in dimension.split(",") if d] if dimension else [] return {"cities": cities, "dimensions": dimensions, "search": search or ""} @app.get("/api/meta") async def meta(): cities = await ch_query( "SELECT DISTINCT city FROM default.pf_board WHERE char_length(city) > 0 ORDER BY city", {}, ) dimensions = await ch_query( "SELECT DISTINCT dimension FROM default.pf_board WHERE char_length(dimension) > 0 ORDER BY dimension", {}, ) return { "cities": [r["city"] for r in cities], "dimensions": [r["dimension"] for r in dimensions], } @app.get("/api/boards") async def boards(city: str = "", dimension: str = "", search: str = ""): params = parse_filters(city, dimension, search) sql = f""" SELECT board_id, argMax(address, last_activity) AS board_address, argMax(city, last_activity) AS board_city, argMax(dimension, last_activity) AS board_dimension FROM default.pf_board WHERE {FILTER_SQL} GROUP BY board_id ORDER BY board_address """ rows = await ch_query(sql, params) return rows @app.get("/api/bookings") async def bookings(city: str = "", dimension: str = "", search: str = ""): params = parse_filters(city, dimension, search) sql = f""" SELECT board_id, toString(task_id) AS task_id, task_status, toString(start_date) AS start_date, toString(end_date) AS end_date, brand, company_name FROM default.pf_board WHERE {FILTER_SQL} ORDER BY board_id, start_date """ rows = await ch_query(sql, params) return rows app.mount("/", StaticFiles(directory="static", html=True), name="static")