sandeepmudhiraj commited on
Commit
a0709fa
·
verified ·
1 Parent(s): c76423d

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. Dockerfile +21 -32
  2. app.py +71 -172
  3. run.sh +2 -0
  4. settings.yml +1 -1
Dockerfile CHANGED
@@ -1,41 +1,30 @@
1
- FROM ubuntu:22.04
2
 
3
- ENV DEBIAN_FRONTEND=noninteractive
4
 
5
- RUN apt-get update && apt-get install -y --no-install-recommends \
6
- python3 python3-pip python3-dev python3-venv \
7
- git curl build-essential libffi-dev libssl-dev \
8
- libyaml-dev libxml2-dev libxslt1-dev zlib1g-dev \
9
- uwsgi uwsgi-plugin-python3 nginx supervisor tini \
10
- && rm -rf /var/lib/apt/lists/*
11
 
12
- WORKDIR /opt
 
13
 
14
- # Clone engine (shallow)
15
- RUN git clone --depth 1 https://github.com/searxng/searxng.git engine
16
-
17
- # Install engine deps in system python
18
- RUN cd engine && pip3 install --no-cache-dir --break-system-packages .
19
-
20
- # Copy config
21
- COPY settings.yml /etc/engine/settings.yml
22
- COPY uwsgi.ini /etc/engine/uwsgi.ini
23
- COPY nginx.conf /etc/nginx/nginx.conf
24
- COPY supervisord.conf /etc/supervisor/conf.d/app.conf
25
- COPY entrypoint.sh /entrypoint.sh
26
-
27
- RUN chmod +x /entrypoint.sh
28
-
29
- # Setup permissions
30
- RUN mkdir -p /var/log/supervisor /var/run/supervisor /var/cache/engine && \
31
- chown -R 1000:1000 /opt/engine /etc/engine /var/log /var/run /var/cache/engine && \
32
- chown -R 1000:1000 /var/lib/nginx /etc/nginx && \
33
- touch /run/nginx.pid && chown 1000:1000 /run/nginx.pid
34
 
35
  USER 1000
36
 
37
- ENV SEARXNG_SETTINGS_PATH=/etc/engine/settings.yml
 
 
 
38
  EXPOSE 7860
39
 
40
- ENTRYPOINT ["/usr/bin/tini", "--"]
41
- CMD ["/entrypoint.sh"]
 
 
1
+ FROM paulgoio/searxng:production
2
 
3
+ USER root
4
 
5
+ # Fix permissions for HF user 1000
6
+ RUN chown -R 1000:1000 /usr/local/searxng \
7
+ && mkdir -p /etc/searxng /var/cache/searxng \
8
+ && chown -R 1000:1000 /etc/searxng \
9
+ && chown -R 1000:1000 /var/cache/searxng
 
10
 
11
+ # Install our thin wrapper deps
12
+ RUN pip install --no-cache-dir --break-system-packages fastapi uvicorn httpx
13
 
14
+ # Copy our files
15
+ COPY --chown=1000:1000 app.py /usr/local/searxng/app.py
16
+ COPY --chown=1000:1000 settings.yml /etc/searxng/settings.yml
17
+ COPY --chown=1000:1000 run.sh /usr/local/searxng/run.sh
18
+ RUN chmod +x /usr/local/searxng/run.sh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  USER 1000
21
 
22
+ ENV GRANIAN_PORT=7860
23
+ ENV GRANIAN_HOST=0.0.0.0
24
+ ENV SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml
25
+
26
  EXPOSE 7860
27
 
28
+ # Override: run our Python wrapper instead of granian
29
+ ENTRYPOINT ["/sbin/tini", "--"]
30
+ CMD ["python3", "/usr/local/searxng/app.py"]
app.py CHANGED
@@ -1,21 +1,16 @@
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
16
  from fastapi import FastAPI, Query
17
  from fastapi.middleware.cors import CORSMiddleware
18
- from fastapi.responses import JSONResponse
19
 
20
  app = FastAPI(title="Meta API", docs_url=None, redoc_url=None)
21
 
@@ -26,99 +21,74 @@ app.add_middleware(
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")
@@ -127,97 +97,26 @@ async def search(
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",
143
- "categories": categories,
144
- "language": language,
145
- "pageno": pageno,
146
- }
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__":
222
- port = int(os.environ.get("PORT", 7860))
223
  uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
 
1
  #!/usr/bin/env python3
2
+ """Meta API wrapper — starts engine via werkzeug, exposes on port 7860."""
 
 
 
 
 
 
3
  import os
4
+ import sys
5
+ import subprocess
6
  import time
7
+ import threading
8
 
9
  import httpx
10
  import uvicorn
11
  from fastapi import FastAPI, Query
12
  from fastapi.middleware.cors import CORSMiddleware
13
+ from fastapi.responses import JSONResponse, Response
14
 
15
  app = FastAPI(title="Meta API", docs_url=None, redoc_url=None)
16
 
 
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
+ _engine_ready = False
28
+
29
+
30
+ def start_engine():
31
+ """Start internal engine via werkzeug (process = python3, NOT granian/searxng)."""
32
+ global _engine_proc, _engine_ready
33
+
34
+ os.environ["SEARXNG_SETTINGS_PATH"] = "/etc/searxng/settings.yml"
35
+
36
+ launcher = "/tmp/eng.py"
37
+ with open(launcher, "w") as lf:
38
+ lf.write(f"""
39
+ import os, sys
40
+ os.environ["SEARXNG_SETTINGS_PATH"] = "/etc/searxng/settings.yml"
41
+ sys.path.insert(0, "/usr/local/searxng")
42
+ os.chdir("/usr/local/searxng")
43
+ from searx.webapp import app
44
+ from werkzeug.serving import run_simple
45
+ run_simple("127.0.0.1", {INTERNAL_PORT}, app, threaded=True, use_reloader=False)
46
+ """)
47
+
48
+ _engine_proc = subprocess.Popen(
49
+ [sys.executable, launcher],
50
+ stdout=subprocess.PIPE,
51
+ stderr=subprocess.PIPE,
52
+ cwd="/usr/local/searxng",
53
+ )
54
+ print(f"Engine starting (pid={_engine_proc.pid})...", flush=True)
55
+
56
+ for i in range(120):
57
+ if _engine_proc.poll() is not None:
58
+ stderr = _engine_proc.stderr.read().decode()[-500:]
59
+ print(f"Engine died: {stderr}", flush=True)
60
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  try:
62
+ resp = httpx.get(f"{INTERNAL_URL}/", timeout=2.0)
63
+ if resp.status_code == 200:
64
+ _engine_ready = True
65
+ print(f"Engine ready after {i+1}s", flush=True)
66
+ return
67
  except Exception:
68
  pass
69
+ time.sleep(1)
70
+ print("Engine timeout after 120s", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
+ @app.on_event("startup")
74
+ async def startup():
75
+ threading.Thread(target=start_engine, daemon=True).start()
76
 
77
 
78
+ @app.on_event("shutdown")
79
+ async def shutdown():
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": "6.0.0", "engine": "ready" if _engine_ready else "starting"}
87
 
88
 
89
  @app.get("/health")
90
  async def health():
91
+ return {"status": "healthy" if _engine_ready else "starting", "engine": "ready" if _engine_ready else "initializing"}
 
92
 
93
 
94
  @app.get("/search")
 
97
  format: str = Query("json"),
98
  categories: str = Query("general"),
99
  language: str = Query("en"),
100
+ time_range: str = Query(None),
101
  pageno: int = Query(1),
102
  ):
103
+ if not _engine_ready:
104
+ return JSONResponse(status_code=503, content={"error": "Engine still starting", "results": []})
105
+
106
+ params = {"q": q, "format": "json", "categories": categories, "language": language, "pageno": pageno}
 
 
 
 
 
 
 
 
 
 
107
  if time_range:
108
  params["time_range"] = time_range
109
 
110
+ try:
111
+ async with httpx.AsyncClient(timeout=20.0) as client:
112
+ resp = await client.get(f"{INTERNAL_URL}/search", params=params)
 
 
 
 
 
 
 
 
 
 
113
  if resp.status_code == 200:
114
+ return JSONResponse(content=resp.json())
115
+ return JSONResponse(status_code=resp.status_code, content={"error": f"Engine {resp.status_code}", "results": []})
116
+ except Exception as e:
117
+ return JSONResponse(status_code=503, content={"error": str(e), "results": []})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
 
120
  if __name__ == "__main__":
121
+ port = int(os.environ.get("GRANIAN_PORT", os.environ.get("PORT", 7860)))
122
  uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
run.sh ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ #!/bin/bash
2
+ python3 /usr/local/searxng/app.py
settings.yml CHANGED
@@ -2,7 +2,7 @@ use_default_settings: true
2
 
3
  server:
4
  bind_address: "127.0.0.1:8888"
5
- secret_key: "meta_api_v5_key_2026"
6
  limiter: false
7
  image_proxy: false
8
  public_instance: false
 
2
 
3
  server:
4
  bind_address: "127.0.0.1:8888"
5
+ secret_key: "meta_api_v6_key_2026"
6
  limiter: false
7
  image_proxy: false
8
  public_instance: false