Voxxium commited on
Commit
6a82861
·
verified ·
1 Parent(s): 7c7e9ec

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +248 -0
app.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HF Space — API principale
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ import threading
8
+ from contextlib import asynccontextmanager
9
+
10
+ from fastapi import FastAPI, Query, HTTPException
11
+ from fastapi.responses import JSONResponse, PlainTextResponse
12
+ from apscheduler.schedulers.background import BackgroundScheduler
13
+
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
17
+ datefmt="%H:%M:%S",
18
+ )
19
+ logger = logging.getLogger("api")
20
+
21
+ from proxy_pool import ProxyPool
22
+ from orchestrator import Orchestrator
23
+
24
+ WORKER_URL = os.environ.get(
25
+ "WORKER_URL",
26
+ "https://CHANGE-MOI.onrender.com"
27
+ )
28
+
29
+ pool = ProxyPool()
30
+ orch = Orchestrator(pool, WORKER_URL)
31
+ scheduler = BackgroundScheduler(daemon=True)
32
+
33
+
34
+ def do_tick():
35
+ try:
36
+ orch.tick()
37
+ except Exception as e:
38
+ logger.error(f"Tick error: {e}")
39
+
40
+
41
+ def ping_worker():
42
+ orch.ping_worker()
43
+
44
+
45
+ def self_ping():
46
+ host = os.environ.get("SPACE_HOST", "")
47
+ if not host:
48
+ return
49
+ try:
50
+ import requests
51
+ requests.get(f"https://{host}/health", timeout=5)
52
+ except Exception:
53
+ pass
54
+
55
+
56
+ @asynccontextmanager
57
+ async def lifespan(app: FastAPI):
58
+ logger.info("🚀 Proxy API")
59
+ logger.info(f" Worker: {WORKER_URL}")
60
+
61
+ scheduler.add_job(
62
+ do_tick, "interval",
63
+ seconds=30, id="tick",
64
+ max_instances=1, coalesce=True,
65
+ )
66
+ scheduler.add_job(
67
+ ping_worker, "interval",
68
+ minutes=10, id="wpng",
69
+ max_instances=1,
70
+ )
71
+ scheduler.add_job(
72
+ self_ping, "interval",
73
+ minutes=4, id="spng",
74
+ max_instances=1,
75
+ )
76
+ scheduler.start()
77
+ logger.info("⏰ Tick/30s | WorkerPing/10m | SelfPing/4m")
78
+
79
+ threading.Thread(target=do_tick, daemon=True).start()
80
+
81
+ yield
82
+ scheduler.shutdown(wait=False)
83
+
84
+
85
+ app = FastAPI(
86
+ title="🔌 Free Proxy API",
87
+ description="SOCKS5 + HTTP verified proxies",
88
+ version="3.0.0",
89
+ lifespan=lifespan,
90
+ )
91
+
92
+
93
+ def _resp(entry, strategy):
94
+ if entry is None:
95
+ raise HTTPException(503, "No proxy available")
96
+ d = entry.to_dict()
97
+ d["strategy"] = strategy
98
+ return d
99
+
100
+
101
+ @app.get("/")
102
+ async def root():
103
+ return {
104
+ "name": "Free Proxy API",
105
+ "version": "3.0.0",
106
+ "pool": pool.size,
107
+ "worker_ok": orch._worker_ok,
108
+ "docs": "/docs",
109
+ }
110
+
111
+
112
+ @app.get("/health")
113
+ async def health():
114
+ return {
115
+ "status": "ok" if pool.size > 0 else "warming",
116
+ "pool": pool.size,
117
+ "worker": orch._worker_ok,
118
+ }
119
+
120
+
121
+ @app.get("/stats")
122
+ async def stats():
123
+ s = pool.get_stats()
124
+ s["orchestrator"] = orch.status
125
+ return s
126
+
127
+
128
+ @app.get("/proxy")
129
+ async def get_proxy(
130
+ protocol: str = Query(None),
131
+ verified: bool = Query(False),
132
+ strategy: str = Query("round-robin"),
133
+ ):
134
+ funcs = {
135
+ "round-robin": pool.get_round_robin,
136
+ "random": pool.get_random,
137
+ "fastest": pool.get_fastest,
138
+ "least-used": pool.get_least_used,
139
+ }
140
+ f = funcs.get(strategy)
141
+ if not f:
142
+ raise HTTPException(
143
+ 400, f"Strategies: {list(funcs.keys())}"
144
+ )
145
+ return _resp(f(protocol, verified), strategy)
146
+
147
+
148
+ @app.get("/proxy/random")
149
+ async def rand(
150
+ protocol: str = Query(None),
151
+ verified: bool = Query(False),
152
+ ):
153
+ return _resp(
154
+ pool.get_random(protocol, verified), "random"
155
+ )
156
+
157
+
158
+ @app.get("/proxy/best")
159
+ async def best(
160
+ protocol: str = Query(None),
161
+ verified: bool = Query(False),
162
+ ):
163
+ return _resp(
164
+ pool.get_fastest(protocol, verified), "fastest"
165
+ )
166
+
167
+
168
+ @app.get("/proxy/least")
169
+ async def least(
170
+ protocol: str = Query(None),
171
+ verified: bool = Query(False),
172
+ ):
173
+ return _resp(
174
+ pool.get_least_used(protocol, verified), "least-used"
175
+ )
176
+
177
+
178
+ @app.get("/rotate")
179
+ async def rotate(
180
+ count: int = Query(5, ge=1, le=50),
181
+ protocol: str = Query(None),
182
+ verified: bool = Query(False),
183
+ ):
184
+ results, seen = [], set()
185
+ for _ in range(count * 3):
186
+ p = pool.get_round_robin(protocol, verified)
187
+ if not p:
188
+ break
189
+ if p.proxy_url not in seen:
190
+ seen.add(p.proxy_url)
191
+ results.append(p.to_dict())
192
+ if len(results) >= count:
193
+ break
194
+ if not results:
195
+ raise HTTPException(503, "No proxies")
196
+ return {
197
+ "count": len(results),
198
+ "proxies": results,
199
+ }
200
+
201
+
202
+ @app.get("/all")
203
+ async def get_all(
204
+ protocol: str = Query(None),
205
+ verified: bool = Query(False),
206
+ limit: int = Query(200, le=500),
207
+ ):
208
+ px = pool.get_all(protocol, verified, limit)
209
+ return {"count": len(px), "proxies": px}
210
+
211
+
212
+ @app.get("/plain")
213
+ async def plain(
214
+ protocol: str = Query(None),
215
+ verified: bool = Query(False),
216
+ limit: int = Query(200, le=500),
217
+ ):
218
+ px = pool.get_all(protocol, verified, limit)
219
+ return PlainTextResponse(
220
+ "\n".join(p["proxy_url"] for p in px)
221
+ )
222
+
223
+
224
+ @app.post("/feedback")
225
+ async def feedback(
226
+ proxy_url: str = Query(...),
227
+ success: bool = Query(...),
228
+ ):
229
+ if success:
230
+ pool.report_success(proxy_url)
231
+ else:
232
+ pool.report_failure(proxy_url)
233
+ return {"ok": True}
234
+
235
+
236
+ @app.post("/force-check")
237
+ async def force():
238
+ if orch._phase.value != "idle":
239
+ return {"status": "already_running"}
240
+ threading.Thread(
241
+ target=do_tick, daemon=True
242
+ ).start()
243
+ return {"status": "started"}
244
+
245
+
246
+ if __name__ == "__main__":
247
+ import uvicorn
248
+ uvicorn.run(app, host="0.0.0.0", port=7860)