Bc-AI commited on
Commit
4cbb604
Β·
verified Β·
1 Parent(s): e264351

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1013 -0
app.py ADDED
@@ -0,0 +1,1013 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SharePUTER Head Node - Runs on HF Spaces (Free CPU)
3
+ Orchestrates task fragmentation, scheduling, and reassembly.
4
+ FastAPI application.
5
+ """
6
+
7
+ from fastapi import FastAPI, HTTPException, Request, BackgroundTasks
8
+ from fastapi.responses import HTMLResponse, JSONResponse
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from pydantic import BaseModel
11
+ from typing import Optional, List, Dict, Any
12
+ import httpx
13
+ import asyncio
14
+ import uuid
15
+ import time
16
+ import json
17
+ import ast
18
+ import textwrap
19
+ import math
20
+ import os
21
+ from datetime import datetime, timedelta
22
+
23
+ app = FastAPI(title="SharePUTER Head Node", version="1.0.0")
24
+
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"],
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+ # ─── Configuration ──────────────────────────────────────────────────────────
33
+ DB_NODE_URL = os.environ.get("SACCP_DB_URL", "http://localhost:5000")
34
+ DB_SECRET = os.environ.get("SACCP_DB_SECRET", "saccp-master-key-change-me")
35
+ HEAD_NODE_ID = os.environ.get("HEAD_NODE_ID", f"head-{uuid.uuid4().hex[:8]}")
36
+ HEAD_NODE_URL = os.environ.get("HEAD_NODE_URL", "http://localhost:7860")
37
+
38
+ # Pricing tiers (SACCP per hour)
39
+ PRICING = {
40
+ "basic_cpu": {"price_per_hour": 1.0, "vcpus": 1, "ram_gb": 4, "gpu": False, "label": "Basic CPU"},
41
+ "medium_cpu": {"price_per_hour": 4.0, "vcpus": 10, "ram_gb": 20, "gpu": False, "label": "Medium CPU"},
42
+ "large_cpu": {"price_per_hour": 10.0, "vcpus": 30, "ram_gb": 50, "gpu": False, "label": "Large CPU"},
43
+ "basic_gpu": {"price_per_hour": 8.0, "vcpus": 4, "ram_gb": 16, "gpu": True, "gpu_type": "T4", "label": "Basic GPU"},
44
+ "medium_gpu": {"price_per_hour": 20.0, "vcpus": 8, "ram_gb": 32, "gpu": True, "gpu_type": "A10G", "label": "Medium GPU"},
45
+ "large_gpu": {"price_per_hour": 50.0, "vcpus": 16, "ram_gb": 64, "gpu": True, "gpu_type": "A100", "label": "Large GPU"},
46
+ "ram_only": {"price_per_hour": 0.5, "vcpus": 0, "ram_gb": 8, "gpu": False, "label": "RAM Only"},
47
+ }
48
+
49
+ # Node payment rates (SACCP per hour for contributing)
50
+ NODE_PAY_RATES = {
51
+ "RAM": 0.3,
52
+ "CPU": 1.5,
53
+ "GPU": 5.0,
54
+ }
55
+
56
+ # In-memory caches (will be lost on restart, that's fine β€” DB is source of truth)
57
+ active_nodes: Dict[str, dict] = {}
58
+ running_tasks: Dict[str, dict] = {}
59
+
60
+
61
+ def db_headers():
62
+ return {"X-SACCP-Secret": DB_SECRET, "Content-Type": "application/json"}
63
+
64
+
65
+ async def db_request(method: str, path: str, **kwargs):
66
+ """Make a request to the database node."""
67
+ async with httpx.AsyncClient(timeout=30.0) as client:
68
+ url = f"{DB_NODE_URL}{path}"
69
+ resp = await getattr(client, method)(url, headers=db_headers(), **kwargs)
70
+ return resp
71
+
72
+
73
+ # ─── Models ─────────────────────────────────────────────────────────────────
74
+ class UserCreate(BaseModel):
75
+ username: str
76
+ password: str
77
+
78
+ class UserLogin(BaseModel):
79
+ username: str
80
+ password: str
81
+
82
+ class TaskSubmit(BaseModel):
83
+ api_key: str
84
+ code: str
85
+ tier: str = "basic_cpu"
86
+
87
+ class NodeRegister(BaseModel):
88
+ node_type: str # RAM, CPU, GPU
89
+ node_url: str
90
+ owner: str = "anonymous"
91
+ specs: dict = {}
92
+
93
+ class FragmentResult(BaseModel):
94
+ fragment_id: str
95
+ node_id: str
96
+ status: str # completed, failed
97
+ result: Any = None
98
+ error: str = None
99
+ stdout: str = ""
100
+
101
+
102
+ # ─── Homepage ───────────────────────────────────────────────────────────────
103
+ @app.get("/", response_class=HTMLResponse)
104
+ async def homepage():
105
+ try:
106
+ stats_resp = await db_request("get", "/stats")
107
+ stats = stats_resp.json() if stats_resp.status_code == 200 else {}
108
+ except Exception:
109
+ stats = {}
110
+
111
+ pricing_rows = ""
112
+ for tier_id, tier in PRICING.items():
113
+ gpu_badge = '<span class="badge gpu">GPU</span>' if tier.get("gpu") else '<span class="badge cpu">CPU</span>'
114
+ gpu_info = f' ({tier.get("gpu_type", "")})' if tier.get("gpu") else ""
115
+ pricing_rows += f"""
116
+ <tr>
117
+ <td>{gpu_badge} {tier['label']}</td>
118
+ <td>{tier['vcpus']} vCPU</td>
119
+ <td>{tier['ram_gb']} GB</td>
120
+ <td>{gpu_info if gpu_info.strip() else 'β€”'}</td>
121
+ <td class="price">{tier['price_per_hour']} SACCP/hr</td>
122
+ </tr>"""
123
+
124
+ node_pay_rows = ""
125
+ for ntype, rate in NODE_PAY_RATES.items():
126
+ node_pay_rows += f"<tr><td>{ntype} Node</td><td class='price'>{rate} SACCP/hr</td></tr>"
127
+
128
+ html = f"""<!DOCTYPE html>
129
+ <html lang="en">
130
+ <head>
131
+ <meta charset="UTF-8">
132
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
133
+ <title>SharePUTERβ„’ β€” SACCP Network</title>
134
+ <style>
135
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Inter:wght@300;400;600;700;900&display=swap');
136
+
137
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
138
+
139
+ body {{
140
+ font-family: 'Inter', sans-serif;
141
+ background: #0a0a0f;
142
+ color: #e0e0e0;
143
+ min-height: 100vh;
144
+ overflow-x: hidden;
145
+ }}
146
+
147
+ .bg-grid {{
148
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
149
+ background-image:
150
+ linear-gradient(rgba(0,255,136,0.03) 1px, transparent 1px),
151
+ linear-gradient(90deg, rgba(0,255,136,0.03) 1px, transparent 1px);
152
+ background-size: 50px 50px;
153
+ z-index: 0;
154
+ }}
155
+
156
+ .glow-orb {{
157
+ position: fixed;
158
+ border-radius: 50%;
159
+ filter: blur(120px);
160
+ z-index: 0;
161
+ animation: float 8s ease-in-out infinite;
162
+ }}
163
+ .glow-orb.green {{ width: 400px; height: 400px; background: rgba(0,255,136,0.08); top: 10%; left: 10%; }}
164
+ .glow-orb.blue {{ width: 350px; height: 350px; background: rgba(0,136,255,0.06); top: 60%; right: 10%; animation-delay: 3s; }}
165
+ .glow-orb.purple {{ width: 300px; height: 300px; background: rgba(136,0,255,0.05); bottom: 10%; left: 40%; animation-delay: 5s; }}
166
+
167
+ @keyframes float {{
168
+ 0%, 100% {{ transform: translate(0, 0); }}
169
+ 50% {{ transform: translate(30px, -30px); }}
170
+ }}
171
+
172
+ .container {{ max-width: 1100px; margin: 0 auto; padding: 40px 20px; position: relative; z-index: 1; }}
173
+
174
+ header {{
175
+ text-align: center;
176
+ margin-bottom: 60px;
177
+ padding: 40px 0;
178
+ }}
179
+
180
+ .logo {{
181
+ font-size: 4rem;
182
+ font-weight: 900;
183
+ letter-spacing: -2px;
184
+ background: linear-gradient(135deg, #00ff88, #00aaff, #8800ff);
185
+ -webkit-background-clip: text;
186
+ -webkit-text-fill-color: transparent;
187
+ text-shadow: none;
188
+ margin-bottom: 8px;
189
+ }}
190
+
191
+ .logo span {{
192
+ font-size: 1.2rem;
193
+ vertical-align: super;
194
+ -webkit-text-fill-color: #00ff88;
195
+ }}
196
+
197
+ .tagline {{
198
+ font-family: 'JetBrains Mono', monospace;
199
+ color: #00ff88;
200
+ font-size: 0.9rem;
201
+ letter-spacing: 4px;
202
+ text-transform: uppercase;
203
+ }}
204
+
205
+ .subtitle {{
206
+ color: #888;
207
+ margin-top: 16px;
208
+ font-size: 1.1rem;
209
+ font-weight: 300;
210
+ }}
211
+
212
+ .status-bar {{
213
+ display: flex;
214
+ justify-content: center;
215
+ gap: 40px;
216
+ flex-wrap: wrap;
217
+ margin: 40px 0;
218
+ }}
219
+
220
+ .stat-card {{
221
+ background: linear-gradient(135deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
222
+ border: 1px solid rgba(255,255,255,0.06);
223
+ border-radius: 16px;
224
+ padding: 24px 32px;
225
+ text-align: center;
226
+ backdrop-filter: blur(10px);
227
+ min-width: 140px;
228
+ transition: all 0.3s;
229
+ }}
230
+ .stat-card:hover {{
231
+ border-color: rgba(0,255,136,0.3);
232
+ transform: translateY(-4px);
233
+ box-shadow: 0 8px 32px rgba(0,255,136,0.1);
234
+ }}
235
+ .stat-card .number {{
236
+ font-size: 2.5rem;
237
+ font-weight: 700;
238
+ color: #00ff88;
239
+ font-family: 'JetBrains Mono', monospace;
240
+ }}
241
+ .stat-card .label {{
242
+ color: #888;
243
+ font-size: 0.8rem;
244
+ text-transform: uppercase;
245
+ letter-spacing: 2px;
246
+ margin-top: 4px;
247
+ }}
248
+
249
+ .section {{
250
+ margin: 50px 0;
251
+ }}
252
+
253
+ .section h2 {{
254
+ font-size: 1.8rem;
255
+ margin-bottom: 24px;
256
+ color: #fff;
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 12px;
260
+ }}
261
+ .section h2 .icon {{ font-size: 1.4rem; }}
262
+
263
+ table {{
264
+ width: 100%;
265
+ border-collapse: collapse;
266
+ background: rgba(255,255,255,0.02);
267
+ border-radius: 12px;
268
+ overflow: hidden;
269
+ border: 1px solid rgba(255,255,255,0.06);
270
+ }}
271
+ th {{
272
+ background: rgba(0,255,136,0.08);
273
+ color: #00ff88;
274
+ font-family: 'JetBrains Mono', monospace;
275
+ font-size: 0.8rem;
276
+ letter-spacing: 1px;
277
+ text-transform: uppercase;
278
+ padding: 14px 20px;
279
+ text-align: left;
280
+ }}
281
+ td {{
282
+ padding: 14px 20px;
283
+ border-bottom: 1px solid rgba(255,255,255,0.04);
284
+ font-size: 0.95rem;
285
+ }}
286
+ tr:hover td {{ background: rgba(0,255,136,0.02); }}
287
+ .price {{
288
+ color: #00ff88;
289
+ font-weight: 600;
290
+ font-family: 'JetBrains Mono', monospace;
291
+ }}
292
+
293
+ .badge {{
294
+ display: inline-block;
295
+ padding: 3px 10px;
296
+ border-radius: 20px;
297
+ font-size: 0.7rem;
298
+ font-weight: 600;
299
+ letter-spacing: 1px;
300
+ text-transform: uppercase;
301
+ }}
302
+ .badge.cpu {{ background: rgba(0,136,255,0.15); color: #00aaff; border: 1px solid rgba(0,136,255,0.3); }}
303
+ .badge.gpu {{ background: rgba(136,0,255,0.15); color: #aa66ff; border: 1px solid rgba(136,0,255,0.3); }}
304
+
305
+ .node-id {{
306
+ font-family: 'JetBrains Mono', monospace;
307
+ font-size: 0.85rem;
308
+ color: #888;
309
+ margin-top: 20px;
310
+ text-align: center;
311
+ padding: 12px;
312
+ background: rgba(255,255,255,0.02);
313
+ border-radius: 8px;
314
+ border: 1px solid rgba(255,255,255,0.04);
315
+ }}
316
+
317
+ .endpoint-list {{
318
+ display: grid;
319
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
320
+ gap: 12px;
321
+ margin-top: 16px;
322
+ }}
323
+ .endpoint {{
324
+ background: rgba(255,255,255,0.02);
325
+ border: 1px solid rgba(255,255,255,0.06);
326
+ border-radius: 8px;
327
+ padding: 14px 18px;
328
+ font-family: 'JetBrains Mono', monospace;
329
+ font-size: 0.8rem;
330
+ transition: all 0.2s;
331
+ }}
332
+ .endpoint:hover {{
333
+ border-color: rgba(0,255,136,0.3);
334
+ }}
335
+ .endpoint .method {{
336
+ display: inline-block;
337
+ padding: 2px 8px;
338
+ border-radius: 4px;
339
+ font-size: 0.7rem;
340
+ font-weight: 700;
341
+ margin-right: 8px;
342
+ }}
343
+ .method.get {{ background: rgba(0,255,136,0.15); color: #00ff88; }}
344
+ .method.post {{ background: rgba(0,136,255,0.15); color: #00aaff; }}
345
+ .method.patch {{ background: rgba(255,170,0,0.15); color: #ffaa00; }}
346
+
347
+ .pulse {{
348
+ display: inline-block;
349
+ width: 10px; height: 10px;
350
+ border-radius: 50%;
351
+ background: #00ff88;
352
+ margin-right: 8px;
353
+ animation: pulse 2s ease-in-out infinite;
354
+ }}
355
+ @keyframes pulse {{
356
+ 0%, 100% {{ opacity: 1; box-shadow: 0 0 0 0 rgba(0,255,136,0.4); }}
357
+ 50% {{ opacity: 0.7; box-shadow: 0 0 0 8px rgba(0,255,136,0); }}
358
+ }}
359
+
360
+ footer {{
361
+ text-align: center;
362
+ color: #555;
363
+ padding: 40px 0;
364
+ font-size: 0.8rem;
365
+ border-top: 1px solid rgba(255,255,255,0.04);
366
+ margin-top: 60px;
367
+ }}
368
+ </style>
369
+ </head>
370
+ <body>
371
+ <div class="bg-grid"></div>
372
+ <div class="glow-orb green"></div>
373
+ <div class="glow-orb blue"></div>
374
+ <div class="glow-orb purple"></div>
375
+
376
+ <div class="container">
377
+ <header>
378
+ <div class="logo">SharePUTER<span>β„’</span></div>
379
+ <div class="tagline">━━━ SACCP Network ━━━</div>
380
+ <p class="subtitle">Shared Cloud Computing Protocol β€” Link computers. Share power. Earn SACCP.</p>
381
+ <p style="margin-top:12px;"><span class="pulse"></span> Head Node Online</p>
382
+ </header>
383
+
384
+ <div class="status-bar">
385
+ <div class="stat-card">
386
+ <div class="number">{stats.get('online_nodes', 0)}</div>
387
+ <div class="label">Nodes Online</div>
388
+ </div>
389
+ <div class="stat-card">
390
+ <div class="number">{stats.get('total_users', 0)}</div>
391
+ <div class="label">Users</div>
392
+ </div>
393
+ <div class="stat-card">
394
+ <div class="number">{stats.get('active_tasks', 0)}</div>
395
+ <div class="label">Active Tasks</div>
396
+ </div>
397
+ <div class="stat-card">
398
+ <div class="number">{stats.get('completed_fragments', 0)}</div>
399
+ <div class="label">Fragments Done</div>
400
+ </div>
401
+ <div class="stat-card">
402
+ <div class="number">{stats.get('total_tasks', 0)}</div>
403
+ <div class="label">Total Tasks</div>
404
+ </div>
405
+ </div>
406
+
407
+ <div class="section">
408
+ <h2><span class="icon">πŸ’°</span> Compute Pricing</h2>
409
+ <table>
410
+ <thead><tr><th>Tier</th><th>vCPUs</th><th>RAM</th><th>GPU</th><th>Price</th></tr></thead>
411
+ <tbody>{pricing_rows}</tbody>
412
+ </table>
413
+ </div>
414
+
415
+ <div class="section">
416
+ <h2><span class="icon">πŸ–₯️</span> Node Contribution Rewards</h2>
417
+ <table>
418
+ <thead><tr><th>Node Type</th><th>Earn Rate</th></tr></thead>
419
+ <tbody>{node_pay_rows}</tbody>
420
+ </table>
421
+ </div>
422
+
423
+ <div class="section">
424
+ <h2><span class="icon">πŸ”Œ</span> API Endpoints</h2>
425
+ <div class="endpoint-list">
426
+ <div class="endpoint"><span class="method post">POST</span>/api/register</div>
427
+ <div class="endpoint"><span class="method post">POST</span>/api/login</div>
428
+ <div class="endpoint"><span class="method post">POST</span>/api/submit_task</div>
429
+ <div class="endpoint"><span class="method get">GET</span>/api/task/{{task_id}}</div>
430
+ <div class="endpoint"><span class="method get">GET</span>/api/my_tasks?api_key=...</div>
431
+ <div class="endpoint"><span class="method post">POST</span>/api/register_node</div>
432
+ <div class="endpoint"><span class="method post">POST</span>/api/node_heartbeat</div>
433
+ <div class="endpoint"><span class="method get">GET</span>/api/get_work?node_id=...</div>
434
+ <div class="endpoint"><span class="method post">POST</span>/api/submit_result</div>
435
+ <div class="endpoint"><span class="method get">GET</span>/api/stats</div>
436
+ <div class="endpoint"><span class="method get">GET</span>/api/pricing</div>
437
+ <div class="endpoint"><span class="method get">GET</span>/api/balance?api_key=...</div>
438
+ </div>
439
+ </div>
440
+
441
+ <div class="node-id">Head Node ID: {HEAD_NODE_ID} | DB: {DB_NODE_URL}</div>
442
+
443
+ <footer>
444
+ SharePUTERβ„’ SACCP Network &copy; 2024 β€” Decentralized Shared Cloud Computing<br>
445
+ <span style="color:#00ff88;">Prototype v1.0</span>
446
+ </footer>
447
+ </div>
448
+ </body>
449
+ </html>"""
450
+ return HTMLResponse(content=html)
451
+
452
+
453
+ # ─── User Endpoints ─────────────────────────────────────────────────────────
454
+ @app.post("/api/register")
455
+ async def register_user(user: UserCreate):
456
+ resp = await db_request("post", "/users", json={"username": user.username, "password": user.password})
457
+ if resp.status_code != 201:
458
+ raise HTTPException(status_code=resp.status_code, detail=resp.json().get("error", "Registration failed"))
459
+ return resp.json()
460
+
461
+
462
+ @app.post("/api/login")
463
+ async def login_user(user: UserLogin):
464
+ resp = await db_request("post", "/users/auth", json={"username": user.username, "password": user.password})
465
+ if resp.status_code != 200:
466
+ raise HTTPException(status_code=401, detail="Invalid credentials")
467
+ return resp.json()
468
+
469
+
470
+ @app.get("/api/balance")
471
+ async def get_balance(api_key: str):
472
+ resp = await db_request("post", "/users/by_api_key", json={"api_key": api_key})
473
+ if resp.status_code != 200:
474
+ raise HTTPException(status_code=401, detail="Invalid API key")
475
+ user = resp.json()
476
+ return {"username": user["username"], "balance": user["balance"]}
477
+
478
+
479
+ # ─── Task Fragmentation Engine ──────────────────────────────────────────────
480
+ def fragment_python_code(code: str, tier: str, task_id: str) -> List[dict]:
481
+ """
482
+ Split Python code into executable fragments.
483
+ Strategy:
484
+ 1. Parse the code AST
485
+ 2. Separate imports, functions/classes, and top-level statements
486
+ 3. Create fragments: setup fragment + computation fragments + aggregation fragment
487
+ 4. For loops/comprehensions, split iterations across fragments
488
+ """
489
+ fragments = []
490
+ tier_info = PRICING.get(tier, PRICING["basic_cpu"])
491
+
492
+ try:
493
+ tree = ast.parse(code)
494
+ except SyntaxError as e:
495
+ # Can't parse β€” send as single fragment
496
+ frag_id = f"{task_id}_frag_0"
497
+ fragments.append({
498
+ "fragment_id": frag_id,
499
+ "task_id": task_id,
500
+ "fragment_index": 0,
501
+ "fragment_type": "compute",
502
+ "code": code,
503
+ "input_data": json.dumps({"full_execution": True}),
504
+ })
505
+ return fragments
506
+
507
+ imports = []
508
+ functions = []
509
+ classes = []
510
+ statements = []
511
+
512
+ for node in ast.iter_child_nodes(tree):
513
+ segment = ast.get_source_segment(code, node)
514
+ if segment is None:
515
+ # Fallback: reconstruct from AST
516
+ try:
517
+ segment = ast.unparse(node)
518
+ except Exception:
519
+ continue
520
+
521
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
522
+ imports.append(segment)
523
+ elif isinstance(node, ast.FunctionDef):
524
+ functions.append(segment)
525
+ elif isinstance(node, ast.ClassDef):
526
+ classes.append(segment)
527
+ else:
528
+ statements.append(segment)
529
+
530
+ preamble = "\n".join(imports + [""] + classes + [""] + functions)
531
+
532
+ # Setup fragment β€” defines all imports, functions, classes
533
+ setup_frag_id = f"{task_id}_setup"
534
+ fragments.append({
535
+ "fragment_id": setup_frag_id,
536
+ "task_id": task_id,
537
+ "fragment_index": 0,
538
+ "fragment_type": "setup",
539
+ "code": preamble,
540
+ "input_data": json.dumps({"type": "setup"}),
541
+ })
542
+
543
+ # Try to detect for-loops over ranges and split them
544
+ loop_fragments = []
545
+ other_statements = []
546
+
547
+ for stmt_str in statements:
548
+ try:
549
+ stmt_tree = ast.parse(stmt_str)
550
+ stmt_node = stmt_tree.body[0] if stmt_tree.body else None
551
+
552
+ if isinstance(stmt_node, ast.For):
553
+ # Try to detect range-based loops
554
+ if (isinstance(stmt_node.iter, ast.Call) and
555
+ isinstance(stmt_node.iter.func, ast.Name) and
556
+ stmt_node.iter.func.id == "range"):
557
+ args = stmt_node.iter.args
558
+ if len(args) == 1:
559
+ try:
560
+ total = ast.literal_eval(args[0])
561
+ loop_fragments.append({
562
+ "original": stmt_str,
563
+ "var": stmt_node.target.id if isinstance(stmt_node.target, ast.Name) else "i",
564
+ "start": 0,
565
+ "end": total,
566
+ "body": ast.get_source_segment(code, stmt_node) or stmt_str
567
+ })
568
+ continue
569
+ except (ValueError, TypeError):
570
+ pass
571
+ elif len(args) >= 2:
572
+ try:
573
+ start = ast.literal_eval(args[0])
574
+ end = ast.literal_eval(args[1])
575
+ loop_fragments.append({
576
+ "original": stmt_str,
577
+ "var": stmt_node.target.id if isinstance(stmt_node.target, ast.Name) else "i",
578
+ "start": start,
579
+ "end": end,
580
+ "body": stmt_str
581
+ })
582
+ continue
583
+ except (ValueError, TypeError):
584
+ pass
585
+ except Exception:
586
+ pass
587
+ other_statements.append(stmt_str)
588
+
589
+ frag_index = 1
590
+
591
+ # Split loops into sub-fragments
592
+ target_frags_per_loop = max(4, tier_info["vcpus"] * 2) # More fragments for bigger tiers
593
+ for loop_info in loop_fragments:
594
+ total_iters = loop_info["end"] - loop_info["start"]
595
+ num_frags = min(target_frags_per_loop, max(1, total_iters))
596
+ chunk_size = math.ceil(total_iters / num_frags)
597
+
598
+ for i in range(num_frags):
599
+ chunk_start = loop_info["start"] + i * chunk_size
600
+ chunk_end = min(loop_info["start"] + (i + 1) * chunk_size, loop_info["end"])
601
+ if chunk_start >= loop_info["end"]:
602
+ break
603
+
604
+ # Rewrite the loop for this chunk
605
+ var = loop_info["var"]
606
+ # Extract loop body
607
+ try:
608
+ loop_tree = ast.parse(loop_info["original"])
609
+ loop_node = loop_tree.body[0]
610
+ body_source = ""
611
+ for child in loop_node.body:
612
+ seg = ast.get_source_segment(loop_info["original"], child)
613
+ if seg:
614
+ body_source += f" {seg}\n"
615
+ else:
616
+ body_source += f" {ast.unparse(child)}\n"
617
+ except Exception:
618
+ body_source = " pass\n"
619
+
620
+ chunk_code = f"""
621
+ # Fragment {frag_index} β€” Loop chunk [{chunk_start}:{chunk_end})
622
+ __fragment_results__ = []
623
+ for {var} in range({chunk_start}, {chunk_end}):
624
+ {body_source}
625
+ """
626
+ frag_id = f"{task_id}_frag_{frag_index}"
627
+ fragments.append({
628
+ "fragment_id": frag_id,
629
+ "task_id": task_id,
630
+ "fragment_index": frag_index,
631
+ "fragment_type": "compute",
632
+ "code": preamble + "\n" + chunk_code,
633
+ "input_data": json.dumps({
634
+ "chunk_start": chunk_start,
635
+ "chunk_end": chunk_end,
636
+ "loop_var": var
637
+ }),
638
+ })
639
+ frag_index += 1
640
+
641
+ # Non-loop statements as individual fragments
642
+ if other_statements:
643
+ for stmt in other_statements:
644
+ frag_id = f"{task_id}_frag_{frag_index}"
645
+ fragments.append({
646
+ "fragment_id": frag_id,
647
+ "task_id": task_id,
648
+ "fragment_index": frag_index,
649
+ "fragment_type": "compute",
650
+ "code": preamble + "\n" + stmt,
651
+ "input_data": json.dumps({"type": "statement"}),
652
+ })
653
+ frag_index += 1
654
+
655
+ # If no compute fragments were created, make one with all statements
656
+ if frag_index == 1:
657
+ all_stmts = "\n".join(statements) if statements else "pass"
658
+ frag_id = f"{task_id}_frag_1"
659
+ fragments.append({
660
+ "fragment_id": frag_id,
661
+ "task_id": task_id,
662
+ "fragment_index": 1,
663
+ "fragment_type": "compute",
664
+ "code": preamble + "\n" + all_stmts,
665
+ "input_data": json.dumps({"type": "full"}),
666
+ })
667
+ frag_index = 2
668
+
669
+ # Aggregation fragment
670
+ agg_frag_id = f"{task_id}_agg"
671
+ fragments.append({
672
+ "fragment_id": agg_frag_id,
673
+ "task_id": task_id,
674
+ "fragment_index": 999999,
675
+ "fragment_type": "aggregate",
676
+ "code": "# Aggregation β€” collect all fragment results",
677
+ "input_data": json.dumps({"type": "aggregate", "total_compute_fragments": frag_index - 1}),
678
+ })
679
+
680
+ return fragments
681
+
682
+
683
+ # ─── Task Submission ─────────────────────────────────────────────────────────
684
+ @app.post("/api/submit_task")
685
+ async def submit_task(task: TaskSubmit, background_tasks: BackgroundTasks):
686
+ # Verify API key
687
+ user_resp = await db_request("post", "/users/by_api_key", json={"api_key": task.api_key})
688
+ if user_resp.status_code != 200:
689
+ raise HTTPException(status_code=401, detail="Invalid API key")
690
+ user = user_resp.json()
691
+
692
+ # Check balance
693
+ tier_info = PRICING.get(task.tier, PRICING["basic_cpu"])
694
+ min_cost = tier_info["price_per_hour"] * 0.01 # Minimum charge: ~36 seconds
695
+ if user["balance"] < min_cost:
696
+ raise HTTPException(status_code=402, detail=f"Insufficient balance. Need at least {min_cost} SACCP")
697
+
698
+ # Create task in DB
699
+ task_resp = await db_request("post", "/tasks", json={
700
+ "owner": user["username"],
701
+ "code": task.code,
702
+ "tier": task.tier
703
+ })
704
+ if task_resp.status_code != 201:
705
+ raise HTTPException(status_code=500, detail="Failed to create task")
706
+
707
+ task_data = task_resp.json()
708
+ task_id = task_data["task_id"]
709
+
710
+ # Fragment the code
711
+ fragments = fragment_python_code(task.code, task.tier, task_id)
712
+
713
+ # Save fragments to DB
714
+ frag_resp = await db_request("post", "/fragments/batch", json={"fragments": fragments})
715
+ if frag_resp.status_code != 201:
716
+ raise HTTPException(status_code=500, detail="Failed to create fragments")
717
+
718
+ # Update task with fragment count
719
+ compute_frags = [f for f in fragments if f["fragment_type"] == "compute"]
720
+ await db_request("patch", f"/tasks/{task_id}", json={
721
+ "status": "fragmenting",
722
+ "total_fragments": len(compute_frags)
723
+ })
724
+
725
+ # Start processing in background
726
+ background_tasks.add_task(process_task, task_id)
727
+
728
+ return {
729
+ "task_id": task_id,
730
+ "status": "fragmenting",
731
+ "total_fragments": len(compute_frags),
732
+ "tier": task.tier,
733
+ "estimated_cost": f"{min_cost:.2f} - {tier_info['price_per_hour']:.2f} SACCP"
734
+ }
735
+
736
+
737
+ async def process_task(task_id: str):
738
+ """Background task processor β€” marks task as running."""
739
+ await asyncio.sleep(1) # Brief delay
740
+ await db_request("patch", f"/tasks/{task_id}", json={"status": "running"})
741
+
742
+ # Monitor loop: check if all fragments completed
743
+ max_wait = 3600 # 1 hour max
744
+ start = time.time()
745
+ while time.time() - start < max_wait:
746
+ await asyncio.sleep(5)
747
+
748
+ task_resp = await db_request("get", f"/tasks/{task_id}")
749
+ if task_resp.status_code != 200:
750
+ break
751
+ task = task_resp.json()
752
+ if task["status"] in ("completed", "failed"):
753
+ break
754
+
755
+ frags_resp = await db_request("get", f"/fragments/by_task/{task_id}")
756
+ if frags_resp.status_code != 200:
757
+ continue
758
+ frags = frags_resp.json()
759
+
760
+ compute_frags = [f for f in frags if f["fragment_type"] == "compute"]
761
+ completed = [f for f in compute_frags if f["status"] == "completed"]
762
+ failed = [f for f in compute_frags if f["status"] == "failed" and f.get("retries", 0) >= 3]
763
+
764
+ await db_request("patch", f"/tasks/{task_id}", json={
765
+ "completed_fragments": len(completed),
766
+ "failed_fragments": len(failed)
767
+ })
768
+
769
+ if len(completed) + len(failed) >= len(compute_frags):
770
+ # All done β€” assemble results
771
+ await assemble_results(task_id, frags)
772
+ break
773
+
774
+
775
+ async def assemble_results(task_id: str, fragments: list):
776
+ """Piece together all fragment results into final output."""
777
+ compute_frags = [f for f in fragments if f["fragment_type"] == "compute"]
778
+ compute_frags.sort(key=lambda x: x["fragment_index"])
779
+
780
+ all_results = []
781
+ all_stdout = []
782
+ all_errors = []
783
+
784
+ for frag in compute_frags:
785
+ if frag["status"] == "completed":
786
+ if frag.get("result") is not None:
787
+ all_results.append(frag["result"])
788
+ if frag.get("stdout"):
789
+ all_stdout.append(f"[Fragment {frag['fragment_index']}]\n{frag['stdout']}")
790
+ elif frag["status"] == "failed":
791
+ all_errors.append(f"[Fragment {frag['fragment_index']}] {frag.get('error', 'Unknown error')}")
792
+
793
+ final_result = {
794
+ "fragments_total": len(compute_frags),
795
+ "fragments_succeeded": len([f for f in compute_frags if f["status"] == "completed"]),
796
+ "fragments_failed": len([f for f in compute_frags if f["status"] == "failed"]),
797
+ "results": all_results,
798
+ "stdout": "\n\n".join(all_stdout),
799
+ "errors": all_errors
800
+ }
801
+
802
+ status = "completed" if not all_errors else ("completed" if len(all_errors) < len(compute_frags) else "failed")
803
+
804
+ # Calculate cost
805
+ task_resp = await db_request("get", f"/tasks/{task_id}")
806
+ task = task_resp.json()
807
+ tier_info = PRICING.get(task.get("tier", "basic_cpu"), PRICING["basic_cpu"])
808
+
809
+ created = datetime.fromisoformat(task["created_at"])
810
+ elapsed_hours = max((datetime.utcnow() - created).total_seconds() / 3600, 0.01)
811
+ cost = round(tier_info["price_per_hour"] * elapsed_hours, 4)
812
+
813
+ # Charge user
814
+ await db_request("patch", f"/users/{task['owner']}/balance", json={
815
+ "amount": -cost,
816
+ "reason": f"task_{task_id}"
817
+ })
818
+
819
+ await db_request("patch", f"/tasks/{task_id}", json={
820
+ "status": status,
821
+ "result": json.dumps(final_result),
822
+ "cost": cost,
823
+ "completed_at": datetime.utcnow().isoformat(),
824
+ "error": "\n".join(all_errors) if all_errors else None
825
+ })
826
+
827
+
828
+ # ─── Task Status ─────────────────────────────────────────────────────────────
829
+ @app.get("/api/task/{task_id}")
830
+ async def get_task_status(task_id: str):
831
+ resp = await db_request("get", f"/tasks/{task_id}")
832
+ if resp.status_code != 200:
833
+ raise HTTPException(status_code=404, detail="Task not found")
834
+ return resp.json()
835
+
836
+
837
+ @app.get("/api/my_tasks")
838
+ async def get_my_tasks(api_key: str):
839
+ user_resp = await db_request("post", "/users/by_api_key", json={"api_key": api_key})
840
+ if user_resp.status_code != 200:
841
+ raise HTTPException(status_code=401, detail="Invalid API key")
842
+ user = user_resp.json()
843
+
844
+ resp = await db_request("get", f"/tasks?owner={user['username']}")
845
+ return resp.json()
846
+
847
+
848
+ @app.get("/api/task/{task_id}/fragments")
849
+ async def get_task_fragments(task_id: str):
850
+ resp = await db_request("get", f"/fragments/by_task/{task_id}")
851
+ return resp.json()
852
+
853
+
854
+ # ─── Node Management ────────────────────────────────────────────────────────
855
+ @app.post("/api/register_node")
856
+ async def register_node(node: NodeRegister):
857
+ node_id = f"node-{uuid.uuid4().hex[:12]}"
858
+ resp = await db_request("post", "/nodes", json={
859
+ "node_id": node_id,
860
+ "node_type": node.node_type,
861
+ "node_url": node.node_url,
862
+ "owner": node.owner,
863
+ "specs": node.specs
864
+ })
865
+ if resp.status_code != 201:
866
+ raise HTTPException(status_code=500, detail="Failed to register node")
867
+
868
+ active_nodes[node_id] = {
869
+ "node_id": node_id,
870
+ "node_type": node.node_type,
871
+ "node_url": node.node_url,
872
+ "last_heartbeat": time.time()
873
+ }
874
+
875
+ pay_rate = NODE_PAY_RATES.get(node.node_type, 0)
876
+ return {
877
+ "node_id": node_id,
878
+ "message": f"Node registered as {node.node_type}",
879
+ "pay_rate": f"{pay_rate} SACCP/hr"
880
+ }
881
+
882
+
883
+ @app.post("/api/node_heartbeat")
884
+ async def node_heartbeat(data: dict):
885
+ node_id = data.get("node_id", "")
886
+ if node_id in active_nodes:
887
+ active_nodes[node_id]["last_heartbeat"] = time.time()
888
+
889
+ await db_request("post", f"/nodes/{node_id}/heartbeat", json=data)
890
+ return {"message": "OK"}
891
+
892
+
893
+ @app.get("/api/get_work")
894
+ async def get_work(node_id: str):
895
+ """Worker nodes call this to get fragments to execute."""
896
+ # Get pending fragments
897
+ resp = await db_request("get", "/fragments/pending?limit=1")
898
+ if resp.status_code != 200:
899
+ return {"work": None}
900
+
901
+ pending = resp.json()
902
+ if not pending:
903
+ return {"work": None}
904
+
905
+ fragment = pending[0]
906
+
907
+ # Assign to this node
908
+ await db_request("patch", f"/fragments/{fragment['fragment_id']}", json={
909
+ "status": "assigned",
910
+ "assigned_node": node_id,
911
+ "started_at": datetime.utcnow().isoformat()
912
+ })
913
+
914
+ return {"work": fragment}
915
+
916
+
917
+ @app.post("/api/submit_result")
918
+ async def submit_result(result: FragmentResult):
919
+ """Worker nodes submit fragment results here."""
920
+ await db_request("patch", f"/fragments/{result.fragment_id}", json={
921
+ "status": result.status,
922
+ "result": result.result,
923
+ "error": result.error,
924
+ "stdout": result.stdout,
925
+ "completed_at": datetime.utcnow().isoformat()
926
+ })
927
+
928
+ # Pay the node
929
+ if result.status == "completed" and result.node_id:
930
+ node_info = active_nodes.get(result.node_id, {})
931
+ node_type = node_info.get("node_type", "CPU")
932
+ # Pay a small amount per fragment
933
+ pay_rate = NODE_PAY_RATES.get(node_type, 0)
934
+ fragment_pay = pay_rate / 360 # ~10 seconds of work
935
+ if fragment_pay > 0:
936
+ await db_request("post", f"/nodes/{result.node_id}/pay", json={"amount": fragment_pay})
937
+
938
+ return {"message": "Result recorded"}
939
+
940
+
941
+ # ─── Stats & Pricing ──��─────────────────────────────────────────────────────
942
+ @app.get("/api/stats")
943
+ async def network_stats():
944
+ try:
945
+ resp = await db_request("get", "/stats")
946
+ return resp.json()
947
+ except Exception:
948
+ return {"status": "database unavailable"}
949
+
950
+
951
+ @app.get("/api/pricing")
952
+ async def get_pricing():
953
+ return PRICING
954
+
955
+
956
+ @app.get("/api/node_pay_rates")
957
+ async def get_node_pay_rates():
958
+ return NODE_PAY_RATES
959
+
960
+
961
+ @app.get("/health")
962
+ async def health():
963
+ return {
964
+ "status": "healthy",
965
+ "head_node_id": HEAD_NODE_ID,
966
+ "active_nodes_cached": len(active_nodes),
967
+ "timestamp": datetime.utcnow().isoformat()
968
+ }
969
+
970
+
971
+ # ─── Background: Pay nodes periodically ────────────────────────────────────
972
+ @app.on_event("startup")
973
+ async def startup():
974
+ asyncio.create_task(node_payment_loop())
975
+ asyncio.create_task(node_cleanup_loop())
976
+
977
+
978
+ async def node_payment_loop():
979
+ """Every 5 minutes, pay online nodes."""
980
+ while True:
981
+ await asyncio.sleep(300) # 5 minutes
982
+ try:
983
+ resp = await db_request("get", "/nodes")
984
+ if resp.status_code == 200:
985
+ nodes = resp.json()
986
+ for node in nodes:
987
+ if node["status"] == "online":
988
+ rate = NODE_PAY_RATES.get(node["node_type"], 0)
989
+ payment = rate * (5 / 60) # 5 minutes worth
990
+ if payment > 0:
991
+ await db_request("post", f"/nodes/{node['node_id']}/pay",
992
+ json={"amount": round(payment, 4)})
993
+ except Exception:
994
+ pass
995
+
996
+
997
+ async def node_cleanup_loop():
998
+ """Mark nodes as offline if no heartbeat in 2 minutes."""
999
+ while True:
1000
+ await asyncio.sleep(60)
1001
+ cutoff = time.time() - 120
1002
+ dead = [nid for nid, n in active_nodes.items() if n["last_heartbeat"] < cutoff]
1003
+ for nid in dead:
1004
+ active_nodes.pop(nid, None)
1005
+ try:
1006
+ await db_request("post", f"/nodes/{nid}/heartbeat", json={"status": "offline"})
1007
+ except Exception:
1008
+ pass
1009
+
1010
+
1011
+ if __name__ == "__main__":
1012
+ import uvicorn
1013
+ uvicorn.run(app, host="0.0.0.0", port=7860)