Subham9126 commited on
Commit
d88b60b
Β·
verified Β·
1 Parent(s): 41237a9

Upload 5 files

Browse files
Files changed (4) hide show
  1. Dockerfile +15 -10
  2. clickhouse-config.xml +36 -0
  3. init_clickhouse.sh +40 -24
  4. 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
- # Install Python requirements
 
 
14
  COPY requirements.txt .
15
  RUN pip3 install --no-cache-dir -r requirements.txt
16
 
17
- WORKDIR /app
18
- # Copy application files to root level as requested
19
  COPY . .
20
 
21
- # IMPORTANT: Make /app writable by any user, as HF Spaces runs containers as user 1000 by default
22
- RUN chmod 777 -R /app && chmod +x init_clickhouse.sh
 
 
 
23
 
24
- # HF Spaces run on port 7860 by default
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
- # Define data directory locally on HF Spaces persistent/ephemeral storage
4
- DATA_DIR="/app/data"
5
- mkdir -p $DATA_DIR/user_files
6
- cd $DATA_DIR
7
 
8
- # Clone the repository if it doesn't exist
9
- if [ ! -d "ohlc_data" ]; then
10
- git clone https://github.com/subhamgiri460/ohlc_data.git
 
11
  else
12
- # Perform a pull if it does exist
13
- cd ohlc_data
14
- git pull
15
- cd ..
16
  fi
17
 
18
- # Link it to user_files
19
- ln -sfn $DATA_DIR/ohlc_data $DATA_DIR/user_files/ohlc_data
 
20
 
21
- # Start ClickHouse server in the background
22
- clickhouse server --daemon --user_files_path=$DATA_DIR/user_files
 
 
23
 
24
- # Give ClickHouse a little time to spin up
25
- echo "Waiting for ClickHouse to start..."
26
- sleep 5
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- # Create the view pointing to the parquet files
29
- # Note: Parquet files inside directories like year=.../month=...
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
- # Start the FastAPI server on Hugging Face Spaces port 7860
38
- cd /app
39
- uvicorn server:app --host 0.0.0.0 --port 7860
 
 
 
 
 
 
 
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
- # Simply cd into the local clone and git pull to hydrate the "lake"
15
- result = subprocess.check_output("cd /app/data/ohlc_data && git pull", shell=True)
16
- return {"status": "updated", "details": result.decode()}
 
 
17
  except subprocess.CalledProcessError as e:
18
- error_msg = e.output.decode() if getattr(e, 'output', None) else str(e)
19
- raise HTTPException(status_code=500, detail={"error": "Refresh Failed", "message": error_msg})
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- # Reverse proxy all other routes to ClickHouse so you get the full UI
 
22
  @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"])
23
- async def proxy_clickhouse(request: Request, path: str):
24
- # Reconstruct the URL for the proxy
25
- url = httpx.URL(path=request.url.path, query=request.url.query.encode("utf-8"))
26
-
27
- # Forward the headers, stripping any Hop-by-hop headers
28
  headers = dict(request.headers)
29
- headers.pop("host", None)
30
- headers.pop("content-length", None)
31
 
32
  try:
33
- # Stream the request body to ClickHouse
34
  req = ch_client.build_request(
35
- request.method,
36
- url,
37
  headers=headers,
38
- content=request.stream()
39
  )
40
- # Send the proxy request
41
  r = await ch_client.send(req, stream=True)
42
-
43
- # Prepare response headers
44
  resp_headers = dict(r.headers)
45
- resp_headers.pop("content-length", None)
46
- resp_headers.pop("content-encoding", None)
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"Proxy error to ClickHouse: {exc}")
 
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}")