Bruhletme commited on
Commit
ee32ee8
·
verified ·
1 Parent(s): 60a9efd

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +73 -61
app.py CHANGED
@@ -1,7 +1,9 @@
1
  import os
2
  import time
3
  import logging
 
4
  import bisect
 
5
  from fastapi import FastAPI, HTTPException, Query
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from huggingface_hub import snapshot_download
@@ -17,8 +19,12 @@ CACHE_DIR = "/data/tgdb_cache"
17
 
18
  dataset = None
19
  user_id_index = None
 
 
20
  stats = {"startup_time": 0, "total_files": 0, "total_row_groups": 0, "total_rows": 0, "queries": 0}
21
 
 
 
22
  def find_parquet(base):
23
  files = []
24
  for root, _, names in os.walk(base):
@@ -27,7 +33,6 @@ def find_parquet(base):
27
  files.append(os.path.join(root, n))
28
  return sorted(files)
29
 
30
-
31
  class UserIdIndex:
32
  def __init__(self):
33
  self.entries = []
@@ -39,7 +44,7 @@ class UserIdIndex:
39
  schema = arrow_dataset.schema
40
  uid_idx = schema.get_field_index("user_id")
41
  if uid_idx < 0:
42
- logger.error("user_id column not found in schema!")
43
  return
44
  for frag in arrow_dataset.get_fragments():
45
  meta = frag.metadata
@@ -56,49 +61,71 @@ class UserIdIndex:
56
 
57
  def find(self, user_id):
58
  idx = bisect.bisect_right(self.keys, user_id) - 1
59
- if idx >= 0:
60
  mn, mx, path, rg = self.entries[idx]
61
  if mn <= user_id <= mx:
62
  return path, rg
63
  return None
64
 
65
-
66
  def init_dataset():
67
- global dataset, user_id_index
68
-
69
- os.makedirs(CACHE_DIR, exist_ok=True)
70
- files = find_parquet(CACHE_DIR)
71
-
72
- if not files:
73
- logger.info("Downloading dataset...")
74
- snapshot_download(
75
- repo_id=REPO_ID,
76
- repo_type="dataset",
77
- local_dir=CACHE_DIR,
78
- local_dir_use_symlinks=False,
79
- )
80
- files = find_parquet(CACHE_DIR)
81
- logger.info(f"Downloaded {len(files)} parquet files")
82
-
83
- logger.info(f"Creating dataset from {len(files)} files...")
84
- dataset = ds.dataset(files, format="parquet")
85
 
86
- rg_count = sum(frag.metadata.num_row_groups for frag in dataset.get_fragments())
87
- row_count = sum(frag.count_rows() for frag in dataset.get_fragments())
88
-
89
- stats["total_files"] = len(files)
90
- stats["total_row_groups"] = rg_count
91
- stats["total_rows"] = row_count
92
-
93
- logger.info(f"Dataset: {len(files)} files, {rg_count} row groups, {row_count:,} rows")
94
-
95
- user_id_index = UserIdIndex()
96
- user_id_index.build(dataset)
97
 
98
- stats["startup_time"] = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
-
101
- ALL_COLS = ["user_id", "username", "first_name", "last_name", "phone", "email", "status", "linked_id", "linked_name", "linked_handle"]
 
 
 
102
 
103
  def query_user(user_id: int):
104
  stats["queries"] += 1
@@ -112,17 +139,13 @@ def query_user(user_id: int):
112
  tbl = pf.read_row_group(rg_idx, columns=ALL_COLS)
113
  tbl = tbl.filter(pc.equal(pc.field("user_id"), user_id))
114
  if len(tbl):
115
- elapsed = time.time() - t0
116
- return tbl.to_pylist()[0], elapsed
117
  except Exception as e:
118
  logger.warning(f"Index lookup failed for {user_id}: {e}")
119
 
120
  tbl = dataset.to_table(filter=pc.equal(pc.field("user_id"), user_id), columns=ALL_COLS)
121
  elapsed = time.time() - t0
122
- if len(tbl) == 0:
123
- return None, elapsed
124
- return tbl.to_pylist()[0], elapsed
125
-
126
 
127
  def search_users(params: dict, limit: int = 10):
128
  stats["queries"] += 1
@@ -141,31 +164,19 @@ def search_users(params: dict, limit: int = 10):
141
  for c in conditions[1:]:
142
  combined = combined & c
143
 
144
- tbl = dataset.to_table(
145
- filter=combined,
146
- columns=["user_id", "username", "first_name", "last_name", "phone", "email", "status"]
147
- )
148
  elapsed = time.time() - t0
149
  count = len(tbl)
150
- if count == 0:
151
  return [], 0, elapsed
152
  return tbl.to_pylist()[:limit], count, elapsed
153
 
154
-
155
- app = FastAPI(title="Telegram DB API", description="859M Telegram users searchable by ID", version="1.0.0")
156
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
157
-
158
-
159
- @app.on_event("startup")
160
- async def startup():
161
- init_dataset()
162
-
163
-
164
  @app.get("/")
165
  async def root():
166
  return {
167
  "name": "Telegram DB API",
168
  "dataset": REPO_ID,
 
169
  "endpoints": {
170
  "/user/{user_id}": "Get full details by Telegram user ID",
171
  "/search": "Search by username, phone, email, first_name, last_name",
@@ -173,15 +184,14 @@ async def root():
173
  }
174
  }
175
 
176
-
177
  @app.get("/user/{user_id}")
178
  async def get_user(user_id: int):
 
179
  user, elapsed = query_user(user_id)
180
  if user is None:
181
  raise HTTPException(status_code=404, detail=f"User {user_id} not found")
182
  return {"found": True, "query_time_ms": round(elapsed * 1000, 2), "user": user}
183
 
184
-
185
  @app.get("/search")
186
  async def search(
187
  username: str = Query(None),
@@ -191,6 +201,7 @@ async def search(
191
  last_name: str = Query(None),
192
  limit: int = Query(10, ge=1, le=100),
193
  ):
 
194
  params = {k: v for k, v in {"username": username, "phone": phone, "email": email, "first_name": first_name, "last_name": last_name}.items() if v}
195
  if not params:
196
  raise HTTPException(status_code=400, detail="Provide at least one search parameter")
@@ -200,12 +211,13 @@ async def search(
200
  raise HTTPException(status_code=404, detail="No users found")
201
  return {"found": True, "count": total, "returned": len(rows), "query_time_ms": round(elapsed * 1000, 2), "users": rows}
202
 
203
-
204
  @app.get("/health")
205
  async def health():
206
  uptime = round(time.time() - stats["startup_time"], 1) if stats["startup_time"] else 0
207
  return {
208
- "status": "ok",
 
 
209
  "dataset": REPO_ID,
210
  "cached": os.path.exists(CACHE_DIR),
211
  "files": stats["total_files"],
 
1
  import os
2
  import time
3
  import logging
4
+ import asyncio
5
  import bisect
6
+ from contextlib import asynccontextmanager
7
  from fastapi import FastAPI, HTTPException, Query
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from huggingface_hub import snapshot_download
 
19
 
20
  dataset = None
21
  user_id_index = None
22
+ is_ready = False
23
+ init_error = None
24
  stats = {"startup_time": 0, "total_files": 0, "total_row_groups": 0, "total_rows": 0, "queries": 0}
25
 
26
+ ALL_COLS = ["user_id", "username", "first_name", "last_name", "phone", "email", "status", "linked_id", "linked_name", "linked_handle"]
27
+
28
  def find_parquet(base):
29
  files = []
30
  for root, _, names in os.walk(base):
 
33
  files.append(os.path.join(root, n))
34
  return sorted(files)
35
 
 
36
  class UserIdIndex:
37
  def __init__(self):
38
  self.entries = []
 
44
  schema = arrow_dataset.schema
45
  uid_idx = schema.get_field_index("user_id")
46
  if uid_idx < 0:
47
+ logger.error("user_id column not found!")
48
  return
49
  for frag in arrow_dataset.get_fragments():
50
  meta = frag.metadata
 
61
 
62
  def find(self, user_id):
63
  idx = bisect.bisect_right(self.keys, user_id) - 1
64
+ if idx >= 0 and idx < len(self.entries):
65
  mn, mx, path, rg = self.entries[idx]
66
  if mn <= user_id <= mx:
67
  return path, rg
68
  return None
69
 
 
70
  def init_dataset():
71
+ global dataset, user_id_index, is_ready, init_error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ try:
74
+ os.makedirs(CACHE_DIR, exist_ok=True)
75
+ files = find_parquet(CACHE_DIR)
 
 
 
 
 
 
 
 
76
 
77
+ if not files:
78
+ logger.info("Downloading dataset (~8GB)...")
79
+ t0 = time.time()
80
+ snapshot_download(
81
+ repo_id=REPO_ID,
82
+ repo_type="dataset",
83
+ local_dir=CACHE_DIR,
84
+ local_dir_use_symlinks=False,
85
+ )
86
+ logger.info(f"Download completed in {time.time()-t0:.1f}s")
87
+ files = find_parquet(CACHE_DIR)
88
+
89
+ logger.info(f"Creating dataset from {len(files)} files...")
90
+ d = ds.dataset(files, format="parquet")
91
+
92
+ rg_count = sum(f.metadata.num_row_groups for f in d.get_fragments())
93
+ row_count = sum(f.count_rows() for f in d.get_fragments())
94
+
95
+ idx = UserIdIndex()
96
+ idx.build(d)
97
+
98
+ stats["total_files"] = len(files)
99
+ stats["total_row_groups"] = rg_count
100
+ stats["total_rows"] = row_count
101
+ stats["startup_time"] = time.time()
102
+
103
+ dataset = d
104
+ user_id_index = idx
105
+ is_ready = True
106
+
107
+ logger.info(f"Ready: {len(files)} files, {rg_count} row groups, {row_count:,} rows")
108
+ except Exception as e:
109
+ init_error = str(e)
110
+ logger.error(f"Init failed: {e}")
111
+
112
+ async def background_init():
113
+ loop = asyncio.get_event_loop()
114
+ await loop.run_in_executor(None, init_dataset)
115
+
116
+ @asynccontextmanager
117
+ async def lifespan(app):
118
+ asyncio.create_task(background_init())
119
+ yield
120
+
121
+ app = FastAPI(title="Telegram DB API", description="859M Telegram users searchable by ID", version="1.0.0", lifespan=lifespan)
122
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
123
 
124
+ def require_ready():
125
+ if not is_ready:
126
+ if init_error:
127
+ raise HTTPException(status_code=503, detail=f"Init error: {init_error}")
128
+ raise HTTPException(status_code=503, detail="Dataset loading, please retry in a moment")
129
 
130
  def query_user(user_id: int):
131
  stats["queries"] += 1
 
139
  tbl = pf.read_row_group(rg_idx, columns=ALL_COLS)
140
  tbl = tbl.filter(pc.equal(pc.field("user_id"), user_id))
141
  if len(tbl):
142
+ return tbl.to_pylist()[0], time.time() - t0
 
143
  except Exception as e:
144
  logger.warning(f"Index lookup failed for {user_id}: {e}")
145
 
146
  tbl = dataset.to_table(filter=pc.equal(pc.field("user_id"), user_id), columns=ALL_COLS)
147
  elapsed = time.time() - t0
148
+ return (tbl.to_pylist()[0], elapsed) if len(tbl) else (None, elapsed)
 
 
 
149
 
150
  def search_users(params: dict, limit: int = 10):
151
  stats["queries"] += 1
 
164
  for c in conditions[1:]:
165
  combined = combined & c
166
 
167
+ tbl = dataset.to_table(filter=combined, columns=["user_id", "username", "first_name", "last_name", "phone", "email", "status"])
 
 
 
168
  elapsed = time.time() - t0
169
  count = len(tbl)
170
+ if not count:
171
  return [], 0, elapsed
172
  return tbl.to_pylist()[:limit], count, elapsed
173
 
 
 
 
 
 
 
 
 
 
 
174
  @app.get("/")
175
  async def root():
176
  return {
177
  "name": "Telegram DB API",
178
  "dataset": REPO_ID,
179
+ "ready": is_ready,
180
  "endpoints": {
181
  "/user/{user_id}": "Get full details by Telegram user ID",
182
  "/search": "Search by username, phone, email, first_name, last_name",
 
184
  }
185
  }
186
 
 
187
  @app.get("/user/{user_id}")
188
  async def get_user(user_id: int):
189
+ require_ready()
190
  user, elapsed = query_user(user_id)
191
  if user is None:
192
  raise HTTPException(status_code=404, detail=f"User {user_id} not found")
193
  return {"found": True, "query_time_ms": round(elapsed * 1000, 2), "user": user}
194
 
 
195
  @app.get("/search")
196
  async def search(
197
  username: str = Query(None),
 
201
  last_name: str = Query(None),
202
  limit: int = Query(10, ge=1, le=100),
203
  ):
204
+ require_ready()
205
  params = {k: v for k, v in {"username": username, "phone": phone, "email": email, "first_name": first_name, "last_name": last_name}.items() if v}
206
  if not params:
207
  raise HTTPException(status_code=400, detail="Provide at least one search parameter")
 
211
  raise HTTPException(status_code=404, detail="No users found")
212
  return {"found": True, "count": total, "returned": len(rows), "query_time_ms": round(elapsed * 1000, 2), "users": rows}
213
 
 
214
  @app.get("/health")
215
  async def health():
216
  uptime = round(time.time() - stats["startup_time"], 1) if stats["startup_time"] else 0
217
  return {
218
+ "status": "loading" if not is_ready else ("error" if init_error else "ok"),
219
+ "ready": is_ready,
220
+ "error": init_error,
221
  "dataset": REPO_ID,
222
  "cached": os.path.exists(CACHE_DIR),
223
  "files": stats["total_files"],