import os import time import logging import asyncio import bisect from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from huggingface_hub import snapshot_download import pyarrow.dataset as ds import pyarrow.compute as pc import pyarrow.parquet as pq logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) REPO_ID = "sauravsingh2111/Tgdata" CACHE_DIR = "/data/tgdb_cache" dataset = None user_id_index = None is_ready = False init_error = None stats = {"startup_time": 0, "total_files": 0, "total_row_groups": 0, "total_rows": 0, "queries": 0} ALL_COLS = ["user_id", "username", "first_name", "last_name", "phone", "email", "status", "linked_id", "linked_name", "linked_handle"] def find_parquet(base): files = [] for root, _, names in os.walk(base): for n in names: if n.endswith(".parquet"): files.append(os.path.join(root, n)) return sorted(files) class UserIdIndex: def __init__(self): self.entries = [] self.keys = [] def build(self, arrow_dataset): logger.info("Building user_id index from parquet metadata...") t0 = time.time() schema = arrow_dataset.schema uid_idx = schema.get_field_index("user_id") if uid_idx < 0: logger.error("user_id column not found!") return for frag in arrow_dataset.get_fragments(): meta = frag.metadata path = frag.path for rg_idx in range(meta.num_row_groups): col = meta.row_group(rg_idx).column(uid_idx) s = col.statistics if s and s.min is not None and s.max is not None: self.entries.append((int(s.min), int(s.max), path, rg_idx)) self.entries.sort(key=lambda x: x[0]) self.keys = [e[0] for e in self.entries] elapsed = time.time() - t0 logger.info(f"Index built: {len(self.entries)} row groups in {elapsed:.1f}s") def find(self, user_id): idx = bisect.bisect_right(self.keys, user_id) - 1 if idx >= 0 and idx < len(self.entries): mn, mx, path, rg = self.entries[idx] if mn <= user_id <= mx: return path, rg return None def init_dataset(): global dataset, user_id_index, is_ready, init_error try: os.makedirs(CACHE_DIR, exist_ok=True) files = find_parquet(CACHE_DIR) if not files: logger.info("Downloading dataset (~8GB)...") t0 = time.time() snapshot_download( repo_id=REPO_ID, repo_type="dataset", local_dir=CACHE_DIR, local_dir_use_symlinks=False, ) logger.info(f"Download completed in {time.time()-t0:.1f}s") files = find_parquet(CACHE_DIR) logger.info(f"Creating dataset from {len(files)} files...") d = ds.dataset(files, format="parquet") rg_count = sum(f.metadata.num_row_groups for f in d.get_fragments()) row_count = sum(f.count_rows() for f in d.get_fragments()) idx = UserIdIndex() idx.build(d) stats["total_files"] = len(files) stats["total_row_groups"] = rg_count stats["total_rows"] = row_count stats["startup_time"] = time.time() dataset = d user_id_index = idx is_ready = True logger.info(f"Ready: {len(files)} files, {rg_count} row groups, {row_count:,} rows") except Exception as e: init_error = str(e) logger.error(f"Init failed: {e}") async def background_init(): loop = asyncio.get_event_loop() await loop.run_in_executor(None, init_dataset) @asynccontextmanager async def lifespan(app): asyncio.create_task(background_init()) yield app = FastAPI(title="Telegram DB API", description="859M Telegram users searchable by ID", version="1.0.0", lifespan=lifespan) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) def require_ready(): if not is_ready: if init_error: raise HTTPException(status_code=503, detail=f"Init error: {init_error}") raise HTTPException(status_code=503, detail="Dataset loading, please retry in a moment") def query_user(user_id: int): stats["queries"] += 1 t0 = time.time() result = user_id_index.find(user_id) if result: path, rg_idx = result try: pf = pq.ParquetFile(path) tbl = pf.read_row_group(rg_idx, columns=ALL_COLS) tbl = tbl.filter(pc.equal(pc.field("user_id"), user_id)) if len(tbl): return tbl.to_pylist()[0], time.time() - t0 except Exception as e: logger.warning(f"Index lookup failed for {user_id}: {e}") tbl = dataset.to_table(filter=pc.equal(pc.field("user_id"), user_id), columns=ALL_COLS) elapsed = time.time() - t0 return (tbl.to_pylist()[0], elapsed) if len(tbl) else (None, elapsed) def search_users(params: dict, limit: int = 10): stats["queries"] += 1 t0 = time.time() conditions = [] for key in ("username", "phone", "email", "first_name", "last_name"): val = params.get(key) if val: conditions.append(pc.equal(pc.field(key), val)) if not conditions: return [], 0, time.time() - t0 combined = conditions[0] for c in conditions[1:]: combined = combined & c tbl = dataset.to_table(filter=combined, columns=["user_id", "username", "first_name", "last_name", "phone", "email", "status"]) elapsed = time.time() - t0 count = len(tbl) if not count: return [], 0, elapsed return tbl.to_pylist()[:limit], count, elapsed @app.get("/") async def root(): return { "name": "Telegram DB API", "dataset": REPO_ID, "ready": is_ready, "endpoints": { "/user/{user_id}": "Get full details by Telegram user ID", "/search": "Search by username, phone, email, first_name, last_name", "/health": "System health & stats" } } @app.get("/user/{user_id}") async def get_user(user_id: int): require_ready() user, elapsed = query_user(user_id) if user is None: raise HTTPException(status_code=404, detail=f"User {user_id} not found") return {"found": True, "query_time_ms": round(elapsed * 1000, 2), "user": user} @app.get("/search") async def search( username: str = Query(None), phone: str = Query(None), email: str = Query(None), first_name: str = Query(None), last_name: str = Query(None), limit: int = Query(10, ge=1, le=100), ): require_ready() params = {k: v for k, v in {"username": username, "phone": phone, "email": email, "first_name": first_name, "last_name": last_name}.items() if v} if not params: raise HTTPException(status_code=400, detail="Provide at least one search parameter") rows, total, elapsed = search_users(params, limit) if not rows: raise HTTPException(status_code=404, detail="No users found") return {"found": True, "count": total, "returned": len(rows), "query_time_ms": round(elapsed * 1000, 2), "users": rows} @app.get("/health") async def health(): uptime = round(time.time() - stats["startup_time"], 1) if stats["startup_time"] else 0 return { "status": "loading" if not is_ready else ("error" if init_error else "ok"), "ready": is_ready, "error": init_error, "dataset": REPO_ID, "cached": os.path.exists(CACHE_DIR), "files": stats["total_files"], "row_groups": stats["total_row_groups"], "rows": stats["total_rows"], "queries_served": stats["queries"], "uptime_s": uptime }