sandeepmudhiraj commited on
Commit
5c1b7a8
·
verified ·
1 Parent(s): 24fd400

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. Dockerfile +1 -17
  2. app.py +163 -81
Dockerfile CHANGED
@@ -1,32 +1,16 @@
1
  FROM python:3.11-slim
2
 
3
- RUN apt-get update && apt-get install -y --no-install-recommends \
4
- git curl build-essential libffi-dev libssl-dev \
5
- libyaml-dev libxml2-dev libxslt1-dev zlib1g-dev \
6
- && rm -rf /var/lib/apt/lists/*
7
-
8
  WORKDIR /app
9
 
10
- # Clone and install the engine from source
11
- RUN git clone --depth 1 https://github.com/searxng/searxng.git /opt/engine && \
12
- cd /opt/engine && \
13
- pip install --no-cache-dir -e .
14
-
15
- # Install wrapper deps
16
  COPY requirements.txt .
17
  RUN pip install --no-cache-dir -r requirements.txt
18
 
19
  COPY . .
20
 
21
- RUN mkdir -p /etc/sxng
22
- COPY settings.yml /etc/sxng/settings.yml
23
-
24
- RUN useradd -m appuser 2>/dev/null || true
25
- RUN chown -R 1000:1000 /app /etc/sxng /opt/engine
26
  USER 1000
27
 
28
  ENV PORT=7860
29
- ENV SEARXNG_SETTINGS_PATH=/etc/sxng/settings.yml
30
  EXPOSE 7860
31
 
32
  CMD ["python", "app.py"]
 
1
  FROM python:3.11-slim
2
 
 
 
 
 
 
3
  WORKDIR /app
4
 
 
 
 
 
 
 
5
  COPY requirements.txt .
6
  RUN pip install --no-cache-dir -r requirements.txt
7
 
8
  COPY . .
9
 
10
+ RUN useradd -m appuser 2>/dev/null || true
 
 
 
 
11
  USER 1000
12
 
13
  ENV PORT=7860
 
14
  EXPOSE 7860
15
 
16
  CMD ["python", "app.py"]
app.py CHANGED
@@ -1,10 +1,15 @@
1
  #!/usr/bin/env python3
2
- """Meta API — data aggregation service with internal engine."""
 
 
 
 
 
 
3
  import os
4
- import sys
5
- import subprocess
6
  import time
7
- import threading
8
 
9
  import httpx
10
  import uvicorn
@@ -21,95 +26,117 @@ app.add_middleware(
21
  allow_headers=["*"],
22
  )
23
 
24
- INTERNAL_PORT = 8888
25
- INTERNAL_URL = f"http://127.0.0.1:{INTERNAL_PORT}"
26
- _engine_proc = None
27
-
28
-
29
- def start_engine():
30
- """Start the internal engine as a background process."""
31
- global _engine_proc
32
-
33
- # Write the engine launcher script
34
- launcher = "/tmp/launcher.py"
35
- lines = [
36
- "import sys, os",
37
- 'os.environ["SEARXNG_SETTINGS_PATH"] = "/etc/sxng/settings.yml"',
38
- "sys.path.insert(0, '/opt/engine')",
39
- "from searx.webapp import app as webapp",
40
- "from werkzeug.serving import run_simple",
41
- f'run_simple("127.0.0.1", {INTERNAL_PORT}, webapp, threaded=True)',
42
- ]
43
- with open(launcher, "w") as lf:
44
- lf.write("\n".join(lines) + "\n")
45
-
46
- _engine_proc = subprocess.Popen(
47
- [sys.executable, launcher],
48
- stdout=subprocess.PIPE,
49
- stderr=subprocess.PIPE,
50
- )
51
- print(f"Engine started (pid={_engine_proc.pid})", flush=True)
52
-
53
- # Wait for readiness
54
- for i in range(90):
55
- if _engine_proc.poll() is not None:
56
- stderr = _engine_proc.stderr.read().decode()
57
- print(f"Engine crashed: {stderr[:1000]}", flush=True)
58
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  try:
60
- resp = httpx.get(f"{INTERNAL_URL}/", timeout=2.0)
61
- if resp.status_code == 200:
62
- print(f"Engine ready after {i+1}s", flush=True)
63
- return True
64
  except Exception:
65
  pass
66
- time.sleep(1)
67
 
68
- print("Engine timeout after 90s", flush=True)
69
- return False
70
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- @app.on_event("startup")
73
- async def startup():
74
- threading.Thread(target=start_engine, daemon=True).start()
75
 
 
 
76
 
77
- @app.on_event("shutdown")
78
- async def shutdown():
79
- global _engine_proc
80
- if _engine_proc:
81
- _engine_proc.terminate()
 
 
 
82
 
83
 
84
  @app.get("/")
85
  async def root():
86
- return {"status": "ok", "service": "meta-api", "version": "2.0.0"}
87
 
88
 
89
  @app.get("/health")
90
  async def health():
91
- try:
92
- async with httpx.AsyncClient(timeout=5.0) as client:
93
- resp = await client.get(f"{INTERNAL_URL}/")
94
- engine_ok = resp.status_code == 200
95
- except Exception:
96
- engine_ok = False
97
-
98
- return {
99
- "status": "healthy" if engine_ok else "starting",
100
- "engine": "ready" if engine_ok else "initializing",
101
- }
102
 
103
 
104
  @app.get("/search")
105
  async def search(
106
- q: str = Query(..., description="Query"),
107
  format: str = Query("json"),
108
  categories: str = Query("general"),
109
  language: str = Query("en"),
110
- time_range: str = Query(None),
111
  pageno: int = Query(1),
112
  ):
 
 
 
 
 
 
 
113
  params = {
114
  "q": q,
115
  "format": "json",
@@ -120,20 +147,75 @@ async def search(
120
  if time_range:
121
  params["time_range"] = time_range
122
 
123
- try:
124
- async with httpx.AsyncClient(timeout=15.0) as client:
125
- resp = await client.get(f"{INTERNAL_URL}/search", params=params)
126
- if resp.status_code == 200:
127
- return JSONResponse(content=resp.json())
128
- return JSONResponse(
129
- status_code=resp.status_code,
130
- content={"error": f"Engine returned {resp.status_code}", "results": []},
 
 
 
131
  )
132
- except Exception as e:
133
- return JSONResponse(
134
- status_code=503,
135
- content={"error": str(e), "results": []},
136
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
 
139
  if __name__ == "__main__":
 
1
  #!/usr/bin/env python3
2
+ """
3
+ Meta API — Smart aggregated data service.
4
+ Maintains persistent sessions with multiple data sources.
5
+ """
6
+ import asyncio
7
+ import hashlib
8
+ import json
9
  import os
10
+ import random
 
11
  import time
12
+ from typing import Optional
13
 
14
  import httpx
15
  import uvicorn
 
26
  allow_headers=["*"],
27
  )
28
 
29
+ # Curated instances verified from searx.space with high uptime and search success
30
+ INSTANCES = [
31
+ "https://search.charliewhiskey.net",
32
+ "https://search.bladerunn.in",
33
+ "https://priv.au",
34
+ "https://opnxng.com",
35
+ "https://etsi.me",
36
+ "https://kantan.cat",
37
+ "https://o5.gg",
38
+ "https://grep.vim.wtf",
39
+ "https://search.2b9t.xyz",
40
+ "https://ooglester.com",
41
+ "https://search.abohiccups.com",
42
+ "https://search.datenkrake.ch",
43
+ "https://paulgo.io",
44
+ ]
45
+
46
+ # Instance health tracking
47
+ _failures: dict[str, int] = {}
48
+ _last_fail: dict[str, float] = {}
49
+ _cookies: dict[str, dict] = {}
50
+ FAIL_THRESHOLD = 5
51
+ COOLDOWN = 600
52
+
53
+ # Response cache
54
+ _cache: dict[str, tuple[float, dict]] = {}
55
+ CACHE_TTL = 300
56
+
57
+ # Persistent HTTP clients per instance (for cookie persistence)
58
+ _clients: dict[str, httpx.AsyncClient] = {}
59
+
60
+ HEADERS = {
61
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
62
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
63
+ "Accept-Language": "en-US,en;q=0.9",
64
+ "Accept-Encoding": "gzip, deflate, br",
65
+ "DNT": "1",
66
+ "Connection": "keep-alive",
67
+ "Upgrade-Insecure-Requests": "1",
68
+ }
69
+
70
+
71
+ async def get_client(instance: str) -> httpx.AsyncClient:
72
+ """Get or create a persistent client for an instance."""
73
+ if instance not in _clients or _clients[instance].is_closed:
74
+ _clients[instance] = httpx.AsyncClient(
75
+ timeout=15.0,
76
+ follow_redirects=True,
77
+ headers=HEADERS,
78
+ )
79
+ # Do initial GET to get cookies/session
80
  try:
81
+ await _clients[instance].get(instance + "/", timeout=10.0)
 
 
 
82
  except Exception:
83
  pass
84
+ return _clients[instance]
85
 
 
 
86
 
87
+ def get_healthy() -> list[str]:
88
+ now = time.time()
89
+ healthy = []
90
+ for inst in INSTANCES:
91
+ fails = _failures.get(inst, 0)
92
+ if fails < FAIL_THRESHOLD:
93
+ healthy.append(inst)
94
+ elif now - _last_fail.get(inst, 0) > COOLDOWN:
95
+ _failures[inst] = 0
96
+ healthy.append(inst)
97
+ return healthy if healthy else INSTANCES
98
 
 
 
 
99
 
100
+ def record_success(inst: str):
101
+ _failures[inst] = max(0, _failures.get(inst, 0) - 1)
102
 
103
+
104
+ def record_failure(inst: str):
105
+ _failures[inst] = _failures.get(inst, 0) + 1
106
+ _last_fail[inst] = time.time()
107
+
108
+
109
+ def cache_key(q: str, **kw) -> str:
110
+ return hashlib.md5(json.dumps({"q": q, **kw}, sort_keys=True).encode()).hexdigest()
111
 
112
 
113
  @app.get("/")
114
  async def root():
115
+ return {"status": "ok", "service": "meta-api", "version": "4.0.0"}
116
 
117
 
118
  @app.get("/health")
119
  async def health():
120
+ h = get_healthy()
121
+ return {"status": "healthy", "sources": len(h), "total": len(INSTANCES)}
 
 
 
 
 
 
 
 
 
122
 
123
 
124
  @app.get("/search")
125
  async def search(
126
+ q: str = Query(...),
127
  format: str = Query("json"),
128
  categories: str = Query("general"),
129
  language: str = Query("en"),
130
+ time_range: Optional[str] = Query(None),
131
  pageno: int = Query(1),
132
  ):
133
+ # Check cache
134
+ ck = cache_key(q, cat=categories, lang=language, page=pageno)
135
+ if ck in _cache:
136
+ t, data = _cache[ck]
137
+ if time.time() - t < CACHE_TTL:
138
+ return JSONResponse(content=data)
139
+
140
  params = {
141
  "q": q,
142
  "format": "json",
 
147
  if time_range:
148
  params["time_range"] = time_range
149
 
150
+ healthy = get_healthy()
151
+ random.shuffle(healthy)
152
+
153
+ # Try instances sequentially with proper session management
154
+ last_error = ""
155
+ for inst in healthy:
156
+ try:
157
+ client = await get_client(inst)
158
+ resp = await client.get(
159
+ f"{inst}/search",
160
+ params=params,
161
  )
162
+
163
+ if resp.status_code == 200:
164
+ try:
165
+ data = resp.json()
166
+ except Exception:
167
+ record_failure(inst)
168
+ continue
169
+
170
+ results = data.get("results", [])
171
+ if len(results) > 0:
172
+ record_success(inst)
173
+ _cache[ck] = (time.time(), data)
174
+ # Cleanup old cache
175
+ now = time.time()
176
+ for k in list(_cache.keys()):
177
+ if now - _cache[k][0] > CACHE_TTL:
178
+ del _cache[k]
179
+ return JSONResponse(content=data)
180
+
181
+ elif resp.status_code == 429:
182
+ # Rate limited — this instance needs cooldown
183
+ record_failure(inst)
184
+ record_failure(inst) # Double penalty for 429
185
+ last_error = f"{inst}: rate limited"
186
+ # Reset client to get fresh cookies
187
+ if inst in _clients:
188
+ await _clients[inst].aclose()
189
+ del _clients[inst]
190
+ continue
191
+ else:
192
+ record_failure(inst)
193
+ last_error = f"{inst}: HTTP {resp.status_code}"
194
+
195
+ except Exception as e:
196
+ record_failure(inst)
197
+ last_error = f"{inst}: {str(e)[:50]}"
198
+ if inst in _clients:
199
+ try:
200
+ await _clients[inst].aclose()
201
+ except Exception:
202
+ pass
203
+ del _clients[inst]
204
+
205
+ return JSONResponse(
206
+ status_code=503,
207
+ content={
208
+ "error": f"All sources unavailable. Last: {last_error}",
209
+ "query": q,
210
+ "results": [],
211
+ },
212
+ )
213
+
214
+
215
+ @app.on_event("shutdown")
216
+ async def shutdown():
217
+ for client in _clients.values():
218
+ await client.aclose()
219
 
220
 
221
  if __name__ == "__main__":