129 lines
4.7 KiB
Python
129 lines
4.7 KiB
Python
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()
|
|
|
|
CITY_COND = "(length({cities:Array(String)}) = 0 OR city IN {cities:Array(String)})"
|
|
DIM_COND = "(length({dimensions:Array(String)}) = 0 OR dimension IN {dimensions:Array(String)})"
|
|
BRAND_COND = "(length({brands:Array(String)}) = 0 OR brand IN {brands:Array(String)})"
|
|
SEARCH_COND = (
|
|
"({search:String} = ''"
|
|
" OR positionCaseInsensitive(address, {search:String}) > 0"
|
|
" OR positionCaseInsensitive(board_id, {search:String}) > 0)"
|
|
)
|
|
|
|
|
|
def build_where(city=False, dimension=False, brand=False, search=False):
|
|
# Faceted WHERE: include only the requested facet conditions. For the
|
|
# dependent-filter option lists we exclude a facet's own condition so its
|
|
# available values reflect the OTHER filters (all-but-self).
|
|
parts = ["char_length(city) > 0"]
|
|
if city:
|
|
parts.append(CITY_COND)
|
|
if dimension:
|
|
parts.append(DIM_COND)
|
|
if brand:
|
|
parts.append(BRAND_COND)
|
|
if search:
|
|
parts.append(SEARCH_COND)
|
|
return "\n AND ".join(parts)
|
|
|
|
|
|
WHERE_ALL = build_where(city=True, dimension=True, brand=True, search=True)
|
|
|
|
|
|
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, brand: 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 []
|
|
brands = [b for b in brand.split(",") if b] if brand else []
|
|
return {"cities": cities, "dimensions": dimensions, "brands": brands, "search": search or ""}
|
|
|
|
|
|
@app.get("/api/meta")
|
|
async def meta(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
|
|
params = parse_filters(city, dimension, brand, search)
|
|
cities = await ch_query(
|
|
f"SELECT DISTINCT city FROM default.pf_board WHERE {build_where(dimension=True, brand=True, search=True)} AND char_length(city) > 0 ORDER BY city",
|
|
params,
|
|
)
|
|
brands = await ch_query(
|
|
f"SELECT DISTINCT brand FROM default.pf_board WHERE {build_where(city=True, dimension=True, search=True)} AND char_length(brand) > 0 ORDER BY brand",
|
|
params,
|
|
)
|
|
dimensions = await ch_query(
|
|
f"SELECT DISTINCT dimension FROM default.pf_board WHERE {build_where(city=True, brand=True, search=True)} AND char_length(dimension) > 0 ORDER BY dimension",
|
|
params,
|
|
)
|
|
return {
|
|
"cities": [r["city"] for r in cities],
|
|
"dimensions": [r["dimension"] for r in dimensions],
|
|
"brands": [r["brand"] for r in brands],
|
|
}
|
|
|
|
|
|
@app.get("/api/boards")
|
|
async def boards(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
|
|
params = parse_filters(city, dimension, brand, 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 {WHERE_ALL}
|
|
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 = "", brand: str = "", search: str = ""):
|
|
params = parse_filters(city, dimension, brand, 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 {WHERE_ALL}
|
|
ORDER BY board_id, start_date
|
|
"""
|
|
rows = await ch_query(sql, params)
|
|
return rows
|
|
|
|
|
|
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|