Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- Dockerfile +15 -10
- clickhouse-config.xml +36 -0
- init_clickhouse.sh +40 -24
- server.py +41 -27
Dockerfile
CHANGED
|
@@ -1,27 +1,32 @@
|
|
| 1 |
FROM ubuntu:22.04
|
| 2 |
|
| 3 |
-
# Avoid tzdata interactive prompt during apt-get install
|
| 4 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 5 |
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
-
git curl python3 python3-pip
|
|
|
|
| 8 |
|
| 9 |
-
# Install ClickHouse
|
| 10 |
RUN curl https://clickhouse.com/ | sh && \
|
| 11 |
-
mv clickhouse /usr/local/bin/
|
|
|
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
COPY requirements.txt .
|
| 15 |
RUN pip3 install --no-cache-dir -r requirements.txt
|
| 16 |
|
| 17 |
-
|
| 18 |
-
# Copy application files to root level as requested
|
| 19 |
COPY . .
|
| 20 |
|
| 21 |
-
#
|
| 22 |
-
RUN
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
# HF Spaces
|
| 25 |
EXPOSE 7860
|
| 26 |
|
| 27 |
CMD ["bash", "init_clickhouse.sh"]
|
|
|
|
| 1 |
FROM ubuntu:22.04
|
| 2 |
|
|
|
|
| 3 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 4 |
|
| 5 |
RUN apt-get update && apt-get install -y \
|
| 6 |
+
git curl python3 python3-pip \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
|
| 9 |
+
# Install ClickHouse static binary
|
| 10 |
RUN curl https://clickhouse.com/ | sh && \
|
| 11 |
+
mv clickhouse /usr/local/bin/ && \
|
| 12 |
+
chmod +x /usr/local/bin/clickhouse
|
| 13 |
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
# Install Python deps first (layer cache)
|
| 17 |
COPY requirements.txt .
|
| 18 |
RUN pip3 install --no-cache-dir -r requirements.txt
|
| 19 |
|
| 20 |
+
# Copy all app files
|
|
|
|
| 21 |
COPY . .
|
| 22 |
|
| 23 |
+
# Create all directories ClickHouse needs β writable by uid 1000 (HF default user)
|
| 24 |
+
RUN mkdir -p /app/ch/data /app/ch/tmp /app/ch/user_files /app/ch/format_schemas \
|
| 25 |
+
/app/ch/access /app/ch/log /app/ch/metadata /app/ch/store \
|
| 26 |
+
/app/data && \
|
| 27 |
+
chmod -R 777 /app
|
| 28 |
|
| 29 |
+
# HF Spaces only exposes this port
|
| 30 |
EXPOSE 7860
|
| 31 |
|
| 32 |
CMD ["bash", "init_clickhouse.sh"]
|
clickhouse-config.xml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<clickhouse>
|
| 2 |
+
<!-- All paths under /app/ch which we chmod 777 at build time -->
|
| 3 |
+
<path>/app/ch/data/</path>
|
| 4 |
+
<tmp_path>/app/ch/tmp/</tmp_path>
|
| 5 |
+
<user_files_path>/app/ch/user_files/</user_files_path>
|
| 6 |
+
<format_schema_path>/app/ch/format_schemas/</format_schema_path>
|
| 7 |
+
<access_control_path>/app/ch/access/</access_control_path>
|
| 8 |
+
|
| 9 |
+
<logger>
|
| 10 |
+
<log>/app/ch/log/clickhouse-server.log</log>
|
| 11 |
+
<errorlog>/app/ch/log/clickhouse-server.err.log</errorlog>
|
| 12 |
+
<level>warning</level>
|
| 13 |
+
<console>0</console>
|
| 14 |
+
</logger>
|
| 15 |
+
|
| 16 |
+
<!-- Only listen locally β HF exposes only 7860, we proxy internally -->
|
| 17 |
+
<listen_host>127.0.0.1</listen_host>
|
| 18 |
+
<http_port>8123</http_port>
|
| 19 |
+
<tcp_port>9000</tcp_port>
|
| 20 |
+
|
| 21 |
+
<!-- Disable interserver and other unneeded ports -->
|
| 22 |
+
<mysql_port remove="remove"/>
|
| 23 |
+
<postgresql_port remove="remove"/>
|
| 24 |
+
<interserver_http_port remove="remove"/>
|
| 25 |
+
<grpc_port remove="remove"/>
|
| 26 |
+
|
| 27 |
+
<!-- Relax security for single-user embedded mode -->
|
| 28 |
+
<openSSL>
|
| 29 |
+
<server>
|
| 30 |
+
<verificationMode>none</verificationMode>
|
| 31 |
+
</server>
|
| 32 |
+
</openSSL>
|
| 33 |
+
|
| 34 |
+
<mark_cache_size>1073741824</mark_cache_size>
|
| 35 |
+
<max_concurrent_queries>10</max_concurrent_queries>
|
| 36 |
+
</clickhouse>
|
init_clickhouse.sh
CHANGED
|
@@ -1,39 +1,55 @@
|
|
| 1 |
#!/bin/bash
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
DATA_DIR="/app/data"
|
| 5 |
-
mkdir -p $DATA_DIR/user_files
|
| 6 |
-
cd $DATA_DIR
|
| 7 |
|
| 8 |
-
# Clone
|
| 9 |
-
|
| 10 |
-
|
|
|
|
| 11 |
else
|
| 12 |
-
|
| 13 |
-
cd ohlc_data
|
| 14 |
-
git pull
|
| 15 |
-
cd ..
|
| 16 |
fi
|
| 17 |
|
| 18 |
-
#
|
| 19 |
-
|
|
|
|
| 20 |
|
| 21 |
-
# Start ClickHouse
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
#
|
| 25 |
-
echo "Waiting for ClickHouse to
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
|
| 30 |
-
# will be successfully read by ClickHouse format engine
|
| 31 |
clickhouse client --query "
|
| 32 |
CREATE VIEW IF NOT EXISTS ohlc AS
|
| 33 |
SELECT *
|
| 34 |
FROM file('ohlc_data/**/*.parquet', Parquet)
|
| 35 |
"
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
|
| 4 |
+
echo "=== Market-Data Observatory Boot Sequence ==="
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
+
# ββ 1. Clone / refresh the parquet repo ββ
|
| 7 |
+
echo "[1/4] Syncing parquet data from GitHub..."
|
| 8 |
+
if [ ! -d "/app/data/ohlc_data" ]; then
|
| 9 |
+
git clone --depth 1 https://github.com/subhamgiri460/ohlc_data.git /app/data/ohlc_data
|
| 10 |
else
|
| 11 |
+
cd /app/data/ohlc_data && git pull && cd /app
|
|
|
|
|
|
|
|
|
|
| 12 |
fi
|
| 13 |
|
| 14 |
+
# ββ 2. Symlink repo into ClickHouse user_files ββ
|
| 15 |
+
echo "[2/4] Linking data into ClickHouse user_files..."
|
| 16 |
+
ln -sfn /app/data/ohlc_data /app/ch/user_files/ohlc_data
|
| 17 |
|
| 18 |
+
# ββ 3. Start ClickHouse in background (NOT --daemon, use & for reliability) ββ
|
| 19 |
+
echo "[3/4] Starting ClickHouse server..."
|
| 20 |
+
clickhouse server --config-file=/app/clickhouse-config.xml &
|
| 21 |
+
CH_PID=$!
|
| 22 |
|
| 23 |
+
# Wait until ClickHouse is genuinely ready (poll the HTTP port)
|
| 24 |
+
echo " Waiting for ClickHouse to become ready..."
|
| 25 |
+
for i in $(seq 1 30); do
|
| 26 |
+
if curl -sf http://127.0.0.1:8123/ping > /dev/null 2>&1; then
|
| 27 |
+
echo " ClickHouse is UP (took ${i}s)"
|
| 28 |
+
break
|
| 29 |
+
fi
|
| 30 |
+
if [ $i -eq 30 ]; then
|
| 31 |
+
echo " ERROR: ClickHouse failed to start within 30s"
|
| 32 |
+
echo " Last log lines:"
|
| 33 |
+
tail -20 /app/ch/log/clickhouse-server.err.log 2>/dev/null || true
|
| 34 |
+
exit 1
|
| 35 |
+
fi
|
| 36 |
+
sleep 1
|
| 37 |
+
done
|
| 38 |
|
| 39 |
+
# ββ 4. Create the lakehouse view ββ
|
| 40 |
+
echo "[4/4] Creating OHLC view over parquet files..."
|
|
|
|
| 41 |
clickhouse client --query "
|
| 42 |
CREATE VIEW IF NOT EXISTS ohlc AS
|
| 43 |
SELECT *
|
| 44 |
FROM file('ohlc_data/**/*.parquet', Parquet)
|
| 45 |
"
|
| 46 |
|
| 47 |
+
echo ""
|
| 48 |
+
echo "=== Observatory is LIVE on port 7860 ==="
|
| 49 |
+
echo " /play β ClickHouse SQL playground"
|
| 50 |
+
echo " /dashboard β ClickHouse dashboard"
|
| 51 |
+
echo " POST /refresh β git pull latest data"
|
| 52 |
+
echo ""
|
| 53 |
+
|
| 54 |
+
# Start FastAPI β this is the foreground process that keeps the container alive
|
| 55 |
+
exec uvicorn server:app --host 0.0.0.0 --port 7860
|
server.py
CHANGED
|
@@ -1,54 +1,68 @@
|
|
| 1 |
from fastapi import FastAPI, Request, HTTPException
|
| 2 |
-
from fastapi.responses import StreamingResponse
|
| 3 |
import subprocess
|
| 4 |
import httpx
|
| 5 |
|
| 6 |
app = FastAPI(title="Market-Data Observatory")
|
| 7 |
|
| 8 |
-
# HTTP client for proxying to ClickHouse
|
| 9 |
-
ch_client = httpx.AsyncClient(base_url="http://127.0.0.1:8123")
|
| 10 |
|
|
|
|
|
|
|
| 11 |
@app.post("/refresh")
|
| 12 |
def refresh():
|
| 13 |
try:
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
except subprocess.CalledProcessError as e:
|
| 18 |
-
|
| 19 |
-
raise HTTPException(status_code=500, detail={"error": "Refresh
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
#
|
|
|
|
| 22 |
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"])
|
| 23 |
-
async def
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
# Forward the headers, stripping any Hop-by-hop headers
|
| 28 |
headers = dict(request.headers)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
|
| 32 |
try:
|
| 33 |
-
|
| 34 |
req = ch_client.build_request(
|
| 35 |
-
request.method,
|
| 36 |
-
url,
|
| 37 |
headers=headers,
|
| 38 |
-
content=
|
| 39 |
)
|
| 40 |
-
# Send the proxy request
|
| 41 |
r = await ch_client.send(req, stream=True)
|
| 42 |
-
|
| 43 |
-
#
|
| 44 |
resp_headers = dict(r.headers)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
|
| 48 |
return StreamingResponse(
|
| 49 |
r.aiter_raw(),
|
| 50 |
status_code=r.status_code,
|
| 51 |
-
headers=resp_headers
|
| 52 |
)
|
| 53 |
except httpx.RequestError as exc:
|
| 54 |
-
raise HTTPException(status_code=502, detail=f"
|
|
|
|
| 1 |
from fastapi import FastAPI, Request, HTTPException
|
| 2 |
+
from fastapi.responses import StreamingResponse, HTMLResponse
|
| 3 |
import subprocess
|
| 4 |
import httpx
|
| 5 |
|
| 6 |
app = FastAPI(title="Market-Data Observatory")
|
| 7 |
|
| 8 |
+
# Async HTTP client for proxying to ClickHouse's internal HTTP interface
|
| 9 |
+
ch_client = httpx.AsyncClient(base_url="http://127.0.0.1:8123", timeout=60.0)
|
| 10 |
|
| 11 |
+
|
| 12 |
+
# ββ Refresh endpoint (git pull) ββ
|
| 13 |
@app.post("/refresh")
|
| 14 |
def refresh():
|
| 15 |
try:
|
| 16 |
+
result = subprocess.check_output(
|
| 17 |
+
"cd /app/data/ohlc_data && git pull",
|
| 18 |
+
shell=True, stderr=subprocess.STDOUT
|
| 19 |
+
)
|
| 20 |
+
return {"status": "updated", "details": result.decode().strip()}
|
| 21 |
except subprocess.CalledProcessError as e:
|
| 22 |
+
msg = e.output.decode() if e.output else str(e)
|
| 23 |
+
raise HTTPException(status_code=500, detail={"error": "Refresh failed", "message": msg})
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ββ Health check ββ
|
| 27 |
+
@app.get("/health")
|
| 28 |
+
async def health():
|
| 29 |
+
try:
|
| 30 |
+
r = await ch_client.get("/ping")
|
| 31 |
+
return {"clickhouse": r.text.strip(), "status": "ok"}
|
| 32 |
+
except Exception as e:
|
| 33 |
+
raise HTTPException(status_code=503, detail={"status": "unhealthy", "error": str(e)})
|
| 34 |
+
|
| 35 |
|
| 36 |
+
# ββ Catch-all reverse proxy to ClickHouse ββ
|
| 37 |
+
# This gives you /play, /dashboard, native HTTP API, everything.
|
| 38 |
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"])
|
| 39 |
+
async def proxy_to_clickhouse(request: Request, path: str):
|
| 40 |
+
url = httpx.URL(path=f"/{path}", query=request.url.query.encode("utf-8"))
|
| 41 |
+
|
| 42 |
+
# Forward headers, strip hop-by-hop
|
|
|
|
| 43 |
headers = dict(request.headers)
|
| 44 |
+
for h in ("host", "content-length", "transfer-encoding"):
|
| 45 |
+
headers.pop(h, None)
|
| 46 |
|
| 47 |
try:
|
| 48 |
+
body = await request.body()
|
| 49 |
req = ch_client.build_request(
|
| 50 |
+
method=request.method,
|
| 51 |
+
url=url,
|
| 52 |
headers=headers,
|
| 53 |
+
content=body,
|
| 54 |
)
|
|
|
|
| 55 |
r = await ch_client.send(req, stream=True)
|
| 56 |
+
|
| 57 |
+
# Pass through ClickHouse response headers
|
| 58 |
resp_headers = dict(r.headers)
|
| 59 |
+
for h in ("content-length", "content-encoding", "transfer-encoding"):
|
| 60 |
+
resp_headers.pop(h, None)
|
| 61 |
|
| 62 |
return StreamingResponse(
|
| 63 |
r.aiter_raw(),
|
| 64 |
status_code=r.status_code,
|
| 65 |
+
headers=resp_headers,
|
| 66 |
)
|
| 67 |
except httpx.RequestError as exc:
|
| 68 |
+
raise HTTPException(status_code=502, detail=f"ClickHouse proxy error: {exc}")
|