ownapi 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. cli.py +1017 -0
  2. compiler.py +948 -0
  3. core/__init__.py +19 -0
  4. core/api_keys.py +523 -0
  5. core/auto_repair.py +682 -0
  6. core/base_client.py +165 -0
  7. core/consent.py +169 -0
  8. core/form_automation.py +1044 -0
  9. core/hitl_queue.py +886 -0
  10. core/onboarding.py +1457 -0
  11. core/token_store.py +223 -0
  12. healer.py +2074 -0
  13. master_mcp.py +193 -0
  14. ownapi/__init__.py +94 -0
  15. ownapi/cli.py +309 -0
  16. ownapi/sdk.py +436 -0
  17. ownapi-1.0.0.dist-info/METADATA +287 -0
  18. ownapi-1.0.0.dist-info/RECORD +62 -0
  19. ownapi-1.0.0.dist-info/WHEEL +5 -0
  20. ownapi-1.0.0.dist-info/entry_points.txt +4 -0
  21. ownapi-1.0.0.dist-info/top_level.txt +10 -0
  22. recorder.py +486 -0
  23. registry/coingecko/__init__.py +5 -0
  24. registry/coingecko/client.py +84 -0
  25. registry/coingecko/schema.json +23 -0
  26. registry/edevlet/__init__.py +5 -0
  27. registry/edevlet/client.py +54 -0
  28. registry/edevlet/schema.json +14 -0
  29. registry/enuygun/__init__.py +5 -0
  30. registry/enuygun/client.py +78 -0
  31. registry/enuygun/schema.json +13 -0
  32. registry/github_trending/__init__.py +5 -0
  33. registry/github_trending/client.py +90 -0
  34. registry/github_trending/schema.json +31 -0
  35. registry/hackernews/__init__.py +5 -0
  36. registry/hackernews/client.py +90 -0
  37. registry/hackernews/schema.json +38 -0
  38. registry/hepsiburada/__init__.py +5 -0
  39. registry/hepsiburada/client.py +32 -0
  40. registry/hepsiburada/schema.json +23 -0
  41. registry/linkedin_jobs/__init__.py +5 -0
  42. registry/linkedin_jobs/client.py +241 -0
  43. registry/linkedin_jobs/schema.json +46 -0
  44. registry/pegasus/__init__.py +5 -0
  45. registry/pegasus/client.py +141 -0
  46. registry/pegasus/schema.json +76 -0
  47. registry/thy/__init__.py +5 -0
  48. registry/thy/client.py +1796 -0
  49. registry/thy/schema.json +46 -0
  50. registry/trendyol/__init__.py +5 -0
  51. registry/trendyol/client.py +228 -0
  52. registry/trendyol/schema.json +30 -0
  53. registry/ucak_bileti/client.py +94 -0
  54. registry/ucak_bileti/schema.json +47 -0
  55. registry/wikipedia/__init__.py +5 -0
  56. registry/wikipedia/client.py +64 -0
  57. registry/wikipedia/schema.json +31 -0
  58. registry/yahoo_finance/__init__.py +5 -0
  59. registry/yahoo_finance/client.py +98 -0
  60. registry/yahoo_finance/schema.json +22 -0
  61. server.py +891 -0
  62. static/index.html +2401 -0
core/auto_repair.py ADDED
@@ -0,0 +1,682 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import sys
7
+ import time
8
+ from datetime import datetime, timedelta, timezone
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from urllib.parse import parse_qs, urlparse
12
+
13
+ from playwright.async_api import Response, async_playwright
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+
18
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
19
+ if str(PROJECT_ROOT) not in sys.path:
20
+ sys.path.insert(0, str(PROJECT_ROOT))
21
+
22
+ from compiler import RegistrySchema
23
+ from core.onboarding import (
24
+ MAX_LLM_FIELD_CHARS,
25
+ _compile_with_llm,
26
+ _is_static_asset_url,
27
+ _is_valid_json_response,
28
+ _load_and_call_client,
29
+ _sanitize_endpoint_for_llm,
30
+ _select_meaningful_endpoints,
31
+ _truncate_for_llm,
32
+ validate_and_repair_client,
33
+ )
34
+ from core.token_store import TokenStore
35
+ from recorder import IGNORED_DOMAIN_KEYWORDS, RELEVANT_HEADER_PREFIXES
36
+
37
+ console = Console(stderr=True, legacy_windows=False)
38
+
39
+
40
+ class AutoRepairEngine:
41
+ """
42
+ Self-Healing 2.0: Otomatik Gece Tamiri ve Zamanlayıcı Motoru.
43
+ - `validate_all_services(runs=1, auto_heal=True)`: `registry/` altındaki tüm aktif servisleri
44
+ sırayla test eder.
45
+ - Testten geçemeyen (`BAŞARISIZ`) servisler için otomatik olarak headless/stealth tarayıcıyla
46
+ hedef URL'ye gidip güncel ağ trafiğini yeniden yakalar ve GPT-4o (`validate_and_repair_client`)
47
+ ile `client.py` dosyasını kendi kendine onarıp 3x doğrulamadan geçirir.
48
+ - Tüm tamir ve sağlık geçmişini `.sessions/auto_repair_report.json` dosyasına kaydeder.
49
+ - `run_daemon(schedule_time="03:00", interval_minutes=None)` ile her gece 03:00'te veya
50
+ belirtilen periyotta otomatik bakım yapar.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ registry_root: Path | str = PROJECT_ROOT / "registry",
56
+ sessions_root: Path | str = PROJECT_ROOT / ".sessions",
57
+ tenant_id: str = "default",
58
+ mode: str = "local",
59
+ ) -> None:
60
+ self.registry_root = Path(registry_root)
61
+ self.sessions_root = Path(sessions_root)
62
+ self.sessions_root.mkdir(parents=True, exist_ok=True)
63
+ self.report_path = self.sessions_root / "auto_repair_report.json"
64
+ self.tenant_id = tenant_id or "default"
65
+ self.mode = mode or "local"
66
+ self.token_store = TokenStore(sessions_dir=self.sessions_root)
67
+
68
+ async def _auto_capture_fresh_traffic_async(
69
+ self,
70
+ target_url: str,
71
+ output_path: Path,
72
+ service_name: str,
73
+ wait_seconds: int = 6,
74
+ ) -> list[dict[str, Any]]:
75
+ """
76
+ Bozulan bir servisin hedef URL'sine kullanıcı müdahalesi olmadan (otomatik headless/stealth)
77
+ giderek güncel XHR/Fetch ağ trafiğini ve taze çerezleri yakalar.
78
+ """
79
+ if service_name.lower() == "thy" or "turkishairlines.com" in (target_url or "").lower():
80
+ target_url = "https://www.turkishairlines.com/tr-tr/"
81
+ else:
82
+ parsed_t = urlparse(target_url or "")
83
+ if "/api/" in parsed_t.path.lower() and parsed_t.scheme and parsed_t.netloc:
84
+ target_url = f"{parsed_t.scheme}://{parsed_t.netloc}/"
85
+
86
+ profile_dir = PROJECT_ROOT / ".browser_profile"
87
+ profile_dir.mkdir(parents=True, exist_ok=True)
88
+ captured_endpoints: list[dict[str, Any]] = []
89
+ endpoint_counter = 0
90
+
91
+ console.print(
92
+ f"[bold cyan]🕵️ Self-Healing 2.0 Otomatik Trafik Yakalama:[/bold cyan] "
93
+ f"[white]{target_url}[/white] ({wait_seconds} sn dinleniyor...)"
94
+ )
95
+
96
+ async with async_playwright() as p:
97
+ launch_kwargs: dict[str, Any] = {
98
+ "user_data_dir": str(profile_dir.resolve()),
99
+ "headless": True,
100
+ "args": [
101
+ "--disable-blink-features=AutomationControlled",
102
+ "--no-sandbox",
103
+ "--disable-setuid-sandbox",
104
+ "--disable-dev-shm-usage",
105
+ ],
106
+ }
107
+ context = await p.chromium.launch_persistent_context(**launch_kwargs)
108
+
109
+ stored_pw_cookies = self.token_store.get_playwright_cookies(
110
+ tenant_id=self.tenant_id,
111
+ service=service_name,
112
+ default_url=target_url,
113
+ )
114
+ if stored_pw_cookies:
115
+ try:
116
+ await context.add_cookies(stored_pw_cookies)
117
+ except Exception:
118
+ pass
119
+
120
+ page = context.pages[0] if context.pages else await context.new_page()
121
+
122
+ async def on_response(res: Response) -> None:
123
+ nonlocal endpoint_counter
124
+ try:
125
+ req = res.request
126
+ if req.resource_type not in ("xhr", "fetch"):
127
+ return
128
+ req_url = req.url
129
+ if _is_static_asset_url(req_url):
130
+ return
131
+ try:
132
+ parsed_url = urlparse(req_url)
133
+ hostname = (parsed_url.hostname or "").lower()
134
+ except Exception:
135
+ return
136
+ if any(ign in hostname for ign in IGNORED_DOMAIN_KEYWORDS):
137
+ return
138
+
139
+ try:
140
+ raw_headers = dict(req.headers)
141
+ except (UnicodeDecodeError, Exception):
142
+ raw_headers = {}
143
+
144
+ filtered_headers: dict[str, str] = {}
145
+ for k, v in raw_headers.items():
146
+ lk = k.lower().strip()
147
+ if any(lk == pref or lk.startswith(pref) for pref in RELEVANT_HEADER_PREFIXES):
148
+ filtered_headers[lk] = v
149
+
150
+ payload: Any = None
151
+ try:
152
+ raw_post = req.post_data
153
+ if raw_post:
154
+ clipped_post = raw_post[:MAX_LLM_FIELD_CHARS]
155
+ try:
156
+ payload = _truncate_for_llm(json.loads(raw_post), max_chars=MAX_LLM_FIELD_CHARS)
157
+ except Exception:
158
+ payload = clipped_post
159
+ except (UnicodeDecodeError, Exception):
160
+ payload = None
161
+
162
+ sample = ""
163
+ is_truncated = False
164
+ try:
165
+ text = await res.text()
166
+ if text:
167
+ sample = text[:MAX_LLM_FIELD_CHARS]
168
+ is_truncated = len(text) > MAX_LLM_FIELD_CHARS
169
+ except (UnicodeDecodeError, Exception):
170
+ sample = ""
171
+
172
+ endpoint_counter += 1
173
+ captured_endpoints.append({
174
+ "id": endpoint_counter,
175
+ "timestamp": datetime.now(timezone.utc).isoformat(),
176
+ "method": req.method.upper(),
177
+ "url": f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}",
178
+ "full_url": req_url,
179
+ "domain": hostname,
180
+ "path": parsed_url.path,
181
+ "status_code": res.status,
182
+ "resource_type": req.resource_type,
183
+ "query_params": parse_qs(parsed_url.query),
184
+ "headers": filtered_headers,
185
+ "request_payload": payload,
186
+ "response_sample": sample,
187
+ "response_truncated": is_truncated,
188
+ })
189
+ except (UnicodeDecodeError, Exception):
190
+ return
191
+
192
+ page.on("response", on_response)
193
+
194
+ try:
195
+ await page.goto(target_url, wait_until="domcontentloaded", timeout=20_000)
196
+ except Exception as exc:
197
+ console.print(f"[dim]Otomatik gezinme uyarısı ({service_name}): {exc}[/dim]")
198
+
199
+ await asyncio.sleep(wait_seconds)
200
+
201
+ if service_name.lower() == "thy":
202
+ try:
203
+ from registry.thy.client import _build_validate_payload, build_thy_availability_payload
204
+ thy_body = build_thy_availability_payload()
205
+ thy_val = _build_validate_payload(thy_body)
206
+ await page.evaluate(
207
+ """async ({ valBody, mainBody }) => {
208
+ const hdrs = {
209
+ 'Accept': 'application/json, text/plain, */*',
210
+ 'Content-Type': 'application/json',
211
+ 'Referer': 'https://www.turkishairlines.com/',
212
+ 'Origin': 'https://www.turkishairlines.com',
213
+ 'x-country': 'TR',
214
+ 'x-language': 'tr'
215
+ };
216
+ try {
217
+ await fetch('https://www.turkishairlines.com/api/v1/availability/validate', {
218
+ method: 'POST',
219
+ headers: hdrs,
220
+ credentials: 'include',
221
+ referrer: 'https://www.turkishairlines.com/',
222
+ body: JSON.stringify(valBody)
223
+ });
224
+ } catch (_) {}
225
+ try {
226
+ await fetch('https://www.turkishairlines.com/api/v1/availability', {
227
+ method: 'POST',
228
+ headers: hdrs,
229
+ credentials: 'include',
230
+ referrer: 'https://www.turkishairlines.com/',
231
+ body: JSON.stringify(mainBody)
232
+ });
233
+ } catch (_) {}
234
+ }""",
235
+ {"valBody": thy_val, "mainBody": thy_body},
236
+ )
237
+ await asyncio.sleep(1.5)
238
+ except Exception:
239
+ pass
240
+
241
+ try:
242
+ cookies = await context.cookies()
243
+ user_agent = await page.evaluate("() => navigator.userAgent")
244
+ self.token_store.save_session(
245
+ tenant_id=self.tenant_id,
246
+ service=service_name,
247
+ cookies=cookies,
248
+ user_agent=user_agent,
249
+ metadata={"target_url": target_url, "auto_healed_at": datetime.now(timezone.utc).isoformat()},
250
+ )
251
+ except Exception:
252
+ pass
253
+
254
+ try:
255
+ await context.close()
256
+ except Exception:
257
+ pass
258
+
259
+ if captured_endpoints:
260
+ traffic_session = {
261
+ "target_url": target_url,
262
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
263
+ "total_endpoints": len(captured_endpoints),
264
+ "endpoints": captured_endpoints,
265
+ }
266
+ output_path.write_text(json.dumps(traffic_session, ensure_ascii=False, indent=2), encoding="utf-8")
267
+ console.print(
268
+ f"[bold green]✔ {service_name}: {len(captured_endpoints)} güncel XHR/Fetch uç noktası yakalandı.[/bold green]"
269
+ )
270
+ return captured_endpoints
271
+
272
+ def _run_auto_capture(self, target_url: str, output_path: Path, service_name: str) -> list[dict[str, Any]]:
273
+ try:
274
+ return asyncio.run(self._auto_capture_fresh_traffic_async(target_url, output_path, service_name))
275
+ except RuntimeError:
276
+ import concurrent.futures
277
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
278
+ return pool.submit(
279
+ asyncio.run,
280
+ self._auto_capture_fresh_traffic_async(target_url, output_path, service_name),
281
+ ).result()
282
+
283
+ def validate_all_services(
284
+ self,
285
+ runs: int = 1,
286
+ auto_heal: bool = True,
287
+ services: list[str] | None = None,
288
+ ) -> dict[str, Any]:
289
+ """
290
+ registry/ altındaki tüm aktif servisleri (veya `services` listesini) sırayla test eder.
291
+ Hata alan servisleri otomatik trafik yakalama + GPT-4o 3x doğrulama döngüsüyle tamir eder
292
+ ve raporu `.sessions/auto_repair_report.json` dosyasına yazar.
293
+ """
294
+ if not self.registry_root.exists():
295
+ return {"total_services": 0, "healthy": 0, "repaired": 0, "failed": 0, "results": []}
296
+
297
+ service_dirs = sorted([
298
+ d for d in self.registry_root.iterdir()
299
+ if d.is_dir() and (d / "schema.json").exists() and (d / "client.py").exists()
300
+ ])
301
+ if services:
302
+ wanted = {s.strip().lower() for s in services}
303
+ service_dirs = [d for d in service_dirs if d.name.lower() in wanted]
304
+
305
+ console.print(
306
+ Panel.fit(
307
+ f"[bold cyan]🛠️ Self-Healing 2.0: Toplu Servis Doğrulama & Otomatik Tamir Motoru[/bold cyan]\n"
308
+ f"[white]Taranacak Servis Sayısı:[/white] [bold yellow]{len(service_dirs)}[/bold yellow] | "
309
+ f"[white]Test Tekrarı:[/white] [cyan]{runs}x[/cyan] | "
310
+ f"[white]Otomatik Tamir (Auto-Heal):[/white] [green]{'AKTİF (3x Doğrulama)' if auto_heal else 'KAPALI'}[/green]",
311
+ title="OwnAPI AutoRepairEngine",
312
+ border_style="cyan",
313
+ )
314
+ )
315
+
316
+ # Arka plan taramasında input() kilitlenmesini önlemek için OWNAPI_NON_INTERACTIVE=1 aç
317
+ prev_ni = os.environ.get("OWNAPI_NON_INTERACTIVE")
318
+ prev_pw = os.environ.get("OWNAPI_HITL_PAUSE_AND_WAIT")
319
+ os.environ["OWNAPI_NON_INTERACTIVE"] = "1"
320
+ os.environ["OWNAPI_HITL_PAUSE_AND_WAIT"] = "0"
321
+
322
+ service_reports: list[dict[str, Any]] = []
323
+ healthy_count = 0
324
+ repaired_count = 0
325
+ pending_hitl_count = 0
326
+ failed_count = 0
327
+
328
+ try:
329
+ for sdir in service_dirs:
330
+ svc_name = sdir.name
331
+ schema_path = sdir / "schema.json"
332
+ client_path = sdir / "client.py"
333
+ traffic_path = sdir / "captured_traffic.json"
334
+
335
+ try:
336
+ raw_schema = json.loads(schema_path.read_text(encoding="utf-8"))
337
+ schema_obj = RegistrySchema.model_validate(raw_schema)
338
+ except Exception as exc:
339
+ failed_count += 1
340
+ service_reports.append({
341
+ "service_name": svc_name,
342
+ "initial_status": "BAŞARISIZ",
343
+ "final_status": "BAŞARISIZ",
344
+ "auto_healed": False,
345
+ "detail": f"Şema okuma hatası: {exc}",
346
+ "checked_at": datetime.now(timezone.utc).isoformat(),
347
+ })
348
+ continue
349
+
350
+ console.print(f"\n[bold magenta]🔍 [{svc_name}][/bold magenta] `{schema_obj.function_name}()` test ediliyor...")
351
+ passed = 0
352
+ last_detail = ""
353
+ is_pending_hitl = False
354
+ hitl_task_id = None
355
+ start_t = time.perf_counter()
356
+
357
+ for r_idx in range(1, runs + 1):
358
+ try:
359
+ res = _load_and_call_client(
360
+ client_path=client_path,
361
+ function_name=schema_obj.function_name,
362
+ tenant_id=self.tenant_id,
363
+ mode=self.mode,
364
+ )
365
+ if isinstance(res, dict) and str(res.get("status", "")).upper() in ("PAUSED", "PENDING_USER_APPROVAL", "PENDING_USER_ACTION"):
366
+ is_pending_hitl = True
367
+ hitl_task_id = res.get("task_id")
368
+ last_detail = f"HITL Kuyruğunda PAUSED / Onay Bekliyor (task_id={hitl_task_id})"
369
+ break
370
+
371
+ ok, detail = _is_valid_json_response(res)
372
+ if ok:
373
+ passed += 1
374
+ last_detail = detail
375
+ else:
376
+ last_detail = detail
377
+ break
378
+ except Exception as exc:
379
+ last_detail = f"Çalışma zamanı hatası: {exc}"
380
+ break
381
+
382
+ elapsed_ms = round((time.perf_counter() - start_t) * 1000.0, 2)
383
+
384
+ if is_pending_hitl:
385
+ pending_hitl_count += 1
386
+ service_reports.append({
387
+ "service_name": svc_name,
388
+ "function_name": schema_obj.function_name,
389
+ "initial_status": "PENDING_USER_APPROVAL",
390
+ "final_status": "PENDING_USER_APPROVAL",
391
+ "hitl_task_id": hitl_task_id,
392
+ "auto_healed": False,
393
+ "elapsed_ms": elapsed_ms,
394
+ "detail": last_detail,
395
+ "checked_at": datetime.now(timezone.utc).isoformat(),
396
+ })
397
+ continue
398
+
399
+ if passed == runs:
400
+ healthy_count += 1
401
+ raw_schema["validation_status"] = "BAŞARILI"
402
+ raw_schema["last_validated_at"] = datetime.now(timezone.utc).isoformat()
403
+ schema_path.write_text(json.dumps(raw_schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
404
+ service_reports.append({
405
+ "service_name": svc_name,
406
+ "function_name": schema_obj.function_name,
407
+ "initial_status": "BAŞARILI",
408
+ "final_status": "BAŞARILI",
409
+ "auto_healed": False,
410
+ "elapsed_ms": elapsed_ms,
411
+ "detail": last_detail,
412
+ "checked_at": datetime.now(timezone.utc).isoformat(),
413
+ })
414
+ console.print(f" [bold green]✔ {svc_name} Sağlıklı ({elapsed_ms:.0f} ms)[/bold green] — {last_detail}")
415
+ continue
416
+
417
+ # Servis testten geçemedi -> Auto-Heal (Trafik Yakalama + GPT-4o Onarım + 3x Doğrulama)
418
+ console.print(f" [bold red]✖ {svc_name} Test Başarısız:[/bold red] {last_detail}")
419
+ raw_schema["validation_status"] = "BAŞARISIZ"
420
+
421
+ if not auto_heal:
422
+ failed_count += 1
423
+ schema_path.write_text(json.dumps(raw_schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
424
+ service_reports.append({
425
+ "service_name": svc_name,
426
+ "function_name": schema_obj.function_name,
427
+ "initial_status": "BAŞARISIZ",
428
+ "final_status": "BAŞARISIZ",
429
+ "auto_healed": False,
430
+ "elapsed_ms": elapsed_ms,
431
+ "detail": last_detail,
432
+ "checked_at": datetime.now(timezone.utc).isoformat(),
433
+ })
434
+ continue
435
+
436
+ console.print(f" [bold yellow]🔧 [{svc_name}] Self-Healing 2.0 Otomatik Onarım Başlatılıyor...[/bold yellow]")
437
+ endpoints: list[dict[str, Any]] = []
438
+ if traffic_path.exists():
439
+ try:
440
+ tdata = json.loads(traffic_path.read_text(encoding="utf-8"))
441
+ endpoints = tdata.get("endpoints", [])
442
+ except Exception:
443
+ endpoints = []
444
+
445
+ # Hedef URL'ye gidip taze trafik yakala
446
+ fresh_eps = self._run_auto_capture(schema_obj.target_url, traffic_path, svc_name)
447
+ if fresh_eps:
448
+ endpoints = fresh_eps
449
+
450
+ meaningful = _select_meaningful_endpoints(endpoints)
451
+ raw_matched = next(
452
+ (e for e in meaningful if e.get("url") == schema_obj.endpoint_url),
453
+ meaningful[0] if meaningful else (endpoints[0] if endpoints else {"url": schema_obj.endpoint_url, "method": schema_obj.method}),
454
+ )
455
+ matched_ep = _sanitize_endpoint_for_llm(raw_matched, max_chars=MAX_LLM_FIELD_CHARS)
456
+
457
+ # Mevcut client.py dosyasında SyntaxError varsa önce _compile_with_llm ile temiz yeniden derle
458
+ try:
459
+ import ast
460
+
461
+ ast.parse(client_path.read_text(encoding="utf-8"))
462
+ except Exception:
463
+ try:
464
+ recompiled_code, recompiled_schema, matched_ep = _compile_with_llm(
465
+ svc_name,
466
+ schema_obj.target_url,
467
+ schema_obj.goal,
468
+ meaningful or endpoints,
469
+ )
470
+ schema_obj.function_name = recompiled_schema.function_name
471
+ client_path.write_text(recompiled_code.strip() + "\n", encoding="utf-8")
472
+ except Exception:
473
+ pass
474
+
475
+ repair_report = validate_and_repair_client(
476
+ service_name=svc_name,
477
+ target_url=schema_obj.target_url,
478
+ description=schema_obj.goal,
479
+ client_path=client_path,
480
+ schema_obj=schema_obj,
481
+ matched_endpoint=matched_ep,
482
+ all_endpoints=meaningful or endpoints,
483
+ validation_runs=3,
484
+ max_repair_attempts=3,
485
+ tenant_id=self.tenant_id,
486
+ mode=self.mode,
487
+ )
488
+
489
+ final_st = repair_report.get("validation_status", "BAŞARISIZ")
490
+ if final_st == "BAŞARILI":
491
+ repaired_count += 1
492
+ raw_schema["validation_status"] = "BAŞARILI"
493
+ raw_schema["validation_passed_runs"] = 3
494
+ else:
495
+ failed_count += 1
496
+ raw_schema["validation_status"] = "BAŞARISIZ"
497
+
498
+ raw_schema["last_validated_at"] = datetime.now(timezone.utc).isoformat()
499
+ schema_path.write_text(json.dumps(raw_schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
500
+
501
+ service_reports.append({
502
+ "service_name": svc_name,
503
+ "function_name": schema_obj.function_name,
504
+ "initial_status": "BAŞARISIZ",
505
+ "final_status": final_st,
506
+ "auto_healed": final_st == "BAŞARILI",
507
+ "repair_attempts": repair_report.get("repair_attempts", 0),
508
+ "elapsed_ms": elapsed_ms,
509
+ "detail": "3/3 Auto-Repair Başarılı" if final_st == "BAŞARILI" else repair_report.get("last_error", last_detail),
510
+ "checked_at": datetime.now(timezone.utc).isoformat(),
511
+ })
512
+ finally:
513
+ if prev_ni is None:
514
+ os.environ.pop("OWNAPI_NON_INTERACTIVE", None)
515
+ else:
516
+ os.environ["OWNAPI_NON_INTERACTIVE"] = prev_ni
517
+ if prev_pw is None:
518
+ os.environ.pop("OWNAPI_HITL_PAUSE_AND_WAIT", None)
519
+ else:
520
+ os.environ["OWNAPI_HITL_PAUSE_AND_WAIT"] = prev_pw
521
+
522
+ summary_report = {
523
+ "timestamp": datetime.now(timezone.utc).isoformat(),
524
+ "tenant_id": self.tenant_id,
525
+ "mode": self.mode,
526
+ "total_services": len(service_dirs),
527
+ "healthy": healthy_count,
528
+ "repaired": repaired_count,
529
+ "pending_hitl": pending_hitl_count,
530
+ "failed": failed_count,
531
+ "results": service_reports,
532
+ }
533
+
534
+ self._append_report_to_disk(summary_report)
535
+ self._print_summary_table(summary_report)
536
+ return summary_report
537
+
538
+ def _append_report_to_disk(self, report: dict[str, Any]) -> None:
539
+ """Tamir ve sağlık geçmişini `.sessions/auto_repair_report.json` dosyasına zaman damgasıyla kaydeder."""
540
+ history: list[dict[str, Any]] = []
541
+ if self.report_path.exists():
542
+ try:
543
+ existing = json.loads(self.report_path.read_text(encoding="utf-8"))
544
+ if isinstance(existing, dict) and isinstance(existing.get("history"), list):
545
+ history = existing["history"]
546
+ except Exception:
547
+ history = []
548
+
549
+ history.append(report)
550
+ payload = {
551
+ "updated_at": datetime.now(timezone.utc).isoformat(),
552
+ "total_runs": len(history),
553
+ "latest_summary": {
554
+ "timestamp": report["timestamp"],
555
+ "total_services": report["total_services"],
556
+ "healthy": report["healthy"],
557
+ "repaired": report["repaired"],
558
+ "pending_hitl": report["pending_hitl"],
559
+ "failed": report["failed"],
560
+ },
561
+ "history": history[-50:],
562
+ }
563
+ self.report_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
564
+
565
+ def _print_summary_table(self, report: dict[str, Any]) -> None:
566
+ table = Table(
567
+ title=(
568
+ f"🩺 OwnAPI Self-Healing 2.0 Sağlık & Tamir Raporu "
569
+ f"(Sağlıklı: {report['healthy']} | Onarılan: {report['repaired']} | "
570
+ f"HITL Bekleyen: {report['pending_hitl']} | Hatalı: {report['failed']})"
571
+ ),
572
+ show_header=True,
573
+ header_style="bold magenta",
574
+ show_lines=True,
575
+ )
576
+ table.add_column("Servis", style="bold cyan", no_wrap=True)
577
+ table.add_column("İlk Durum", justify="center")
578
+ table.add_column("Son Durum", justify="center")
579
+ table.add_column("Otomatik Tamir", justify="center")
580
+ table.add_column("Süre", justify="right", style="yellow")
581
+ table.add_column("Açıklama / Detay", style="white", overflow="fold")
582
+
583
+ for item in report.get("results", []):
584
+ init_st = item.get("initial_status", "-")
585
+ fin_st = item.get("final_status", "-")
586
+ init_badge = "[green]🟢 BAŞARILI[/green]" if init_st == "BAŞARILI" else (
587
+ "[yellow]⏳ PENDING_HITL[/yellow]" if init_st == "PENDING_USER_APPROVAL" else "[red]🔴 BAŞARISIZ[/red]"
588
+ )
589
+ fin_badge = "[bold green]🟢 BAŞARILI[/bold green]" if fin_st == "BAŞARILI" else (
590
+ "[bold yellow]⏳ PENDING_HITL[/bold yellow]" if fin_st == "PENDING_USER_APPROVAL" else "[bold red]🔴 BAŞARISIZ[/bold red]"
591
+ )
592
+ healed_badge = "[bold cyan]🛠️ Onarıldı (3/3)[/bold cyan]" if item.get("auto_healed") else "[dim]-[/dim]"
593
+ ms_str = f"{item.get('elapsed_ms', 0):.0f} ms"
594
+ table.add_row(
595
+ str(item.get("service_name")),
596
+ init_badge,
597
+ fin_badge,
598
+ healed_badge,
599
+ ms_str,
600
+ str(item.get("detail", ""))[:140],
601
+ )
602
+
603
+ console.print()
604
+ console.print(table)
605
+ console.print(f"[dim]Rapor kaydedildi: {self.report_path}[/dim]\n")
606
+
607
+ @staticmethod
608
+ def _seconds_until_schedule(schedule_time: str = "03:00") -> float:
609
+ """HH:MM formatındaki hedef saate kalan saniyeyi hesaplar."""
610
+ now = datetime.now()
611
+ try:
612
+ hour, minute = [int(x) for x in schedule_time.strip().split(":", 1)]
613
+ except Exception:
614
+ hour, minute = 3, 0
615
+ target = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
616
+ if target <= now:
617
+ target += timedelta(days=1)
618
+ return max(1.0, (target - now).total_seconds())
619
+
620
+ def run_daemon(
621
+ self,
622
+ schedule_time: str = "03:00",
623
+ interval_minutes: float | None = None,
624
+ runs: int = 1,
625
+ auto_heal: bool = True,
626
+ run_immediately: bool = False,
627
+ max_cycles: int | None = None,
628
+ ) -> None:
629
+ """
630
+ Her gece `schedule_time` (varsayılan '03:00') saatinde veya `interval_minutes`
631
+ dakika periyodunda çalışan hafif arka plan bakım ve otomatik tamir zamanlayıcısı.
632
+ """
633
+ schedule_desc = (
634
+ f"Her {interval_minutes} dakikada bir"
635
+ if interval_minutes
636
+ else f"Her gece saat {schedule_time}'te"
637
+ )
638
+ console.print(
639
+ Panel.fit(
640
+ f"[bold green]🌙 OwnAPI Self-Healing 2.0 Gece Bakım Daemon'u Başlatıldı[/bold green]\n"
641
+ f"[white]Zamanlama:[/white] [bold cyan]{schedule_desc}[/bold cyan]\n"
642
+ f"[white]Otomatik Tamir (GPT-4o + 3x Auto-Test):[/white] [green]{'Aktif' if auto_heal else 'Pasif'}[/green]\n"
643
+ f"[white]Rapor Dosyası:[/white] [dim]{self.report_path}[/dim]\n"
644
+ "[dim]Durdurmak için CTRL+C tuşlarına basın.[/dim]",
645
+ title="OwnAPI Auto-Repair Daemon",
646
+ border_style="green",
647
+ )
648
+ )
649
+
650
+ cycle = 0
651
+ if run_immediately:
652
+ cycle += 1
653
+ console.print(f"\n[bold cyan]⚡ İlk bakım döngüsü (#{cycle}) hemen çalıştırılıyor...[/bold cyan]")
654
+ self.validate_all_services(runs=runs, auto_heal=auto_heal)
655
+ if max_cycles and cycle >= max_cycles:
656
+ return
657
+
658
+ try:
659
+ while True:
660
+ if interval_minutes is not None and interval_minutes > 0:
661
+ wait_sec = float(interval_minutes) * 60.0
662
+ else:
663
+ wait_sec = self._seconds_until_schedule(schedule_time)
664
+
665
+ next_run_dt = datetime.now() + timedelta(seconds=wait_sec)
666
+ console.print(
667
+ f"[dim]⏳ Sonraki otomatik bakım zamanı: "
668
+ f"{next_run_dt.strftime('%Y-%m-%d %H:%M:%S')} ({wait_sec / 60.0:.1f} dk sonra)...[/dim]"
669
+ )
670
+ time.sleep(wait_sec)
671
+
672
+ cycle += 1
673
+ console.print(
674
+ f"\n[bold cyan]🛠️ Zamanlanmış Gece Bakımı Döngüsü (#{cycle}) Başladı "
675
+ f"({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})[/bold cyan]"
676
+ )
677
+ self.validate_all_services(runs=runs, auto_heal=auto_heal)
678
+
679
+ if max_cycles and cycle >= max_cycles:
680
+ break
681
+ except KeyboardInterrupt:
682
+ console.print("\n[yellow]⏹️ OwnAPI Auto-Repair Daemon kullanıcı tarafından durduruldu.[/yellow]")