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.
- cli.py +1017 -0
- compiler.py +948 -0
- core/__init__.py +19 -0
- core/api_keys.py +523 -0
- core/auto_repair.py +682 -0
- core/base_client.py +165 -0
- core/consent.py +169 -0
- core/form_automation.py +1044 -0
- core/hitl_queue.py +886 -0
- core/onboarding.py +1457 -0
- core/token_store.py +223 -0
- healer.py +2074 -0
- master_mcp.py +193 -0
- ownapi/__init__.py +94 -0
- ownapi/cli.py +309 -0
- ownapi/sdk.py +436 -0
- ownapi-1.0.0.dist-info/METADATA +287 -0
- ownapi-1.0.0.dist-info/RECORD +62 -0
- ownapi-1.0.0.dist-info/WHEEL +5 -0
- ownapi-1.0.0.dist-info/entry_points.txt +4 -0
- ownapi-1.0.0.dist-info/top_level.txt +10 -0
- recorder.py +486 -0
- registry/coingecko/__init__.py +5 -0
- registry/coingecko/client.py +84 -0
- registry/coingecko/schema.json +23 -0
- registry/edevlet/__init__.py +5 -0
- registry/edevlet/client.py +54 -0
- registry/edevlet/schema.json +14 -0
- registry/enuygun/__init__.py +5 -0
- registry/enuygun/client.py +78 -0
- registry/enuygun/schema.json +13 -0
- registry/github_trending/__init__.py +5 -0
- registry/github_trending/client.py +90 -0
- registry/github_trending/schema.json +31 -0
- registry/hackernews/__init__.py +5 -0
- registry/hackernews/client.py +90 -0
- registry/hackernews/schema.json +38 -0
- registry/hepsiburada/__init__.py +5 -0
- registry/hepsiburada/client.py +32 -0
- registry/hepsiburada/schema.json +23 -0
- registry/linkedin_jobs/__init__.py +5 -0
- registry/linkedin_jobs/client.py +241 -0
- registry/linkedin_jobs/schema.json +46 -0
- registry/pegasus/__init__.py +5 -0
- registry/pegasus/client.py +141 -0
- registry/pegasus/schema.json +76 -0
- registry/thy/__init__.py +5 -0
- registry/thy/client.py +1796 -0
- registry/thy/schema.json +46 -0
- registry/trendyol/__init__.py +5 -0
- registry/trendyol/client.py +228 -0
- registry/trendyol/schema.json +30 -0
- registry/ucak_bileti/client.py +94 -0
- registry/ucak_bileti/schema.json +47 -0
- registry/wikipedia/__init__.py +5 -0
- registry/wikipedia/client.py +64 -0
- registry/wikipedia/schema.json +31 -0
- registry/yahoo_finance/__init__.py +5 -0
- registry/yahoo_finance/client.py +98 -0
- registry/yahoo_finance/schema.json +22 -0
- server.py +891 -0
- static/index.html +2401 -0
cli.py
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import importlib.util
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
20
|
+
try:
|
|
21
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
22
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
PROJECT_ROOT = Path(__file__).resolve().parent
|
|
27
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
28
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
29
|
+
|
|
30
|
+
from compiler import (
|
|
31
|
+
CompilationResult,
|
|
32
|
+
RegistrySchema,
|
|
33
|
+
ShadowAPICompiler,
|
|
34
|
+
ensure_default_registry,
|
|
35
|
+
)
|
|
36
|
+
from recorder import TrafficRecorder
|
|
37
|
+
|
|
38
|
+
console = Console(legacy_windows=False)
|
|
39
|
+
app = typer.Typer(
|
|
40
|
+
name="own-api",
|
|
41
|
+
help="OwnAPI Enterprise CLI: User-Delegated Browser Middleware, Auto-Validated API Compiler ve MCP Sunucu Motoru.",
|
|
42
|
+
add_completion=False,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _print_compilation_summary(result: CompilationResult) -> None:
|
|
47
|
+
"""Derleme sonuçlarını, Registry kaydını ve dinamik parametreleri Rich ile ekrana basar."""
|
|
48
|
+
param_lines: list[str] = []
|
|
49
|
+
if result.dynamic_params:
|
|
50
|
+
for p in result.dynamic_params:
|
|
51
|
+
param_lines.append(
|
|
52
|
+
f" • [bold yellow]{p.name}[/bold yellow] ({p.param_type}, {p.location}) "
|
|
53
|
+
f"= [green]{repr(p.default_value)}[/green] — [dim]{p.description}[/dim]"
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
param_lines.append(" • [dim]Dinamik parametre tespit edilmedi (statik uç nokta)[/dim]")
|
|
57
|
+
|
|
58
|
+
params_block = "\n".join(param_lines)
|
|
59
|
+
reg_info = (
|
|
60
|
+
f"[bold cyan]📦 Registry İstemcisi:[/bold cyan] [underline]{result.registry_client_path.resolve()}[/underline]\n"
|
|
61
|
+
f"[bold cyan]📋 Registry Şeması:[/bold cyan] [underline]{result.registry_schema_path.resolve()}[/underline]\n"
|
|
62
|
+
if result.registry_client_path and result.registry_schema_path
|
|
63
|
+
else ""
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
console.print(
|
|
67
|
+
Panel(
|
|
68
|
+
f"[bold green]✔ Servis Adı (Registry):[/bold green] [bold magenta]{result.service_name}[/bold magenta]\n"
|
|
69
|
+
f"[bold green]✔ Eşleşen Uç Nokta (#{result.matched_endpoint_id}):[/bold green] "
|
|
70
|
+
f"[bold cyan]{result.matched_method}[/bold cyan] [white]{result.matched_url}[/white]\n"
|
|
71
|
+
f"[bold green]✔ Üretilen Fonksiyon:[/bold green] [bold yellow]{result.function_name}()[/bold yellow]\n"
|
|
72
|
+
f"[bold green]✔ Self-Healing:[/bold green] [bold cyan]Aktif (SessionHealer 401/403/429 Koruması)[/bold cyan]\n"
|
|
73
|
+
f"[bold green]✔ LLM Açıklaması:[/bold green] {result.explanation}\n\n"
|
|
74
|
+
f"[bold magenta]Dinamik Parametreler:[/bold magenta]\n{params_block}\n\n"
|
|
75
|
+
f"{reg_info}"
|
|
76
|
+
f"[bold cyan]📄 Python SDK:[/bold cyan] [underline]{result.client_path.resolve()}[/underline]\n"
|
|
77
|
+
f"[bold cyan]🔌 MCP Sunucusu:[/bold cyan] [underline]{result.mcp_path.resolve()}[/underline]",
|
|
78
|
+
title="✨ Derleme ve Registry Kaydı Tamamlandı",
|
|
79
|
+
border_style="green",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _run_and_measure_client(
|
|
85
|
+
compiler: ShadowAPICompiler,
|
|
86
|
+
result: CompilationResult,
|
|
87
|
+
) -> tuple[float, str]:
|
|
88
|
+
"""
|
|
89
|
+
Üretilen generated_client.py dosyasını çalıştırır, yanıt süresini milisaniye (ms)
|
|
90
|
+
olarak ölçer ve çıktısını döndürür. Hata durumunda deterministik şablonla onarır.
|
|
91
|
+
"""
|
|
92
|
+
env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
|
|
93
|
+
|
|
94
|
+
for attempt in range(2):
|
|
95
|
+
start_time = time.perf_counter()
|
|
96
|
+
proc = subprocess.run(
|
|
97
|
+
[sys.executable, str(result.client_path)],
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
encoding="utf-8",
|
|
101
|
+
errors="replace",
|
|
102
|
+
env=env,
|
|
103
|
+
timeout=60,
|
|
104
|
+
)
|
|
105
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000.0
|
|
106
|
+
|
|
107
|
+
if proc.returncode == 0:
|
|
108
|
+
output = (proc.stdout or "").strip()
|
|
109
|
+
return elapsed_ms, output
|
|
110
|
+
|
|
111
|
+
if attempt == 0:
|
|
112
|
+
console.print(
|
|
113
|
+
"[yellow]⚠ İlk çalıştırmada hata algılandı, deterministik şablonla otomatik onarılıyor...[/yellow]"
|
|
114
|
+
)
|
|
115
|
+
compiler.apply_fallback(result)
|
|
116
|
+
else:
|
|
117
|
+
err_msg = (proc.stderr or proc.stdout or "Bilinmeyen hata").strip()
|
|
118
|
+
raise RuntimeError(f"generated_client.py çalıştırılamadı:\n{err_msg}")
|
|
119
|
+
|
|
120
|
+
return 0.0, ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _print_benchmark_comparison(browser_seconds: float, direct_ms: float) -> None:
|
|
124
|
+
"""Tarayıcı süresi (sn) ile Shadow-API doğrudan istek süresini (ms) karşılaştıran tablo basar."""
|
|
125
|
+
browser_ms = browser_seconds * 1000.0
|
|
126
|
+
speedup = browser_ms / direct_ms if direct_ms > 0 else 0.0
|
|
127
|
+
saved_pct = ((browser_ms - direct_ms) / browser_ms * 100.0) if browser_ms > 0 else 0.0
|
|
128
|
+
|
|
129
|
+
table = Table(
|
|
130
|
+
title="⚡ Performans Karşılaştırması: Tarayıcı vs Shadow-API",
|
|
131
|
+
show_header=True,
|
|
132
|
+
header_style="bold magenta",
|
|
133
|
+
show_lines=True,
|
|
134
|
+
)
|
|
135
|
+
table.add_column("Yöntem", style="bold white", width=34)
|
|
136
|
+
table.add_column("Süre", justify="right", style="bold cyan", width=22)
|
|
137
|
+
table.add_column("Birim Karşılaştırma", justify="right", style="yellow", width=22)
|
|
138
|
+
table.add_column("Performans Kazancı", justify="center", style="bold green", width=24)
|
|
139
|
+
|
|
140
|
+
table.add_row(
|
|
141
|
+
"🌐 Tarayıcı Süresi (Playwright UI)",
|
|
142
|
+
f"{browser_seconds:.2f} sn",
|
|
143
|
+
f"{browser_ms:,.0f} ms",
|
|
144
|
+
"Baz (1.0x)",
|
|
145
|
+
)
|
|
146
|
+
table.add_row(
|
|
147
|
+
"🚀 Shadow-API Doğrudan İstek Süresi",
|
|
148
|
+
f"{direct_ms:.2f} ms",
|
|
149
|
+
f"{direct_ms / 1000.0:.3f} sn",
|
|
150
|
+
f"[bold green]{speedup:.1f}x Daha Hızlı (%{saved_pct:.1f} tasarruf)[/bold green]",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
console.print()
|
|
154
|
+
console.print(table)
|
|
155
|
+
console.print()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _load_sdk_function(client_path: Path) -> tuple[str, Any]:
|
|
159
|
+
"""Verilen client.py içindeki ana SDK fonksiyonunu dinamik olarak yükler."""
|
|
160
|
+
if not client_path.exists():
|
|
161
|
+
raise FileNotFoundError(
|
|
162
|
+
f"{client_path} bulunamadı! Önce `python cli.py compile` veya `python cli.py build` çalıştırın."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
module_name = f"shadow_sdk_{client_path.parent.name}_{int(time.time() * 1000)}"
|
|
166
|
+
spec = importlib.util.spec_from_file_location(module_name, str(client_path.resolve()))
|
|
167
|
+
if spec is None or spec.loader is None:
|
|
168
|
+
raise RuntimeError(f"{client_path} modülü yüklenemedi.")
|
|
169
|
+
|
|
170
|
+
module = importlib.util.module_from_spec(spec)
|
|
171
|
+
sys.modules[module_name] = module
|
|
172
|
+
spec.loader.exec_module(module)
|
|
173
|
+
|
|
174
|
+
candidates = [
|
|
175
|
+
(name, obj)
|
|
176
|
+
for name, obj in inspect.getmembers(module, inspect.isfunction)
|
|
177
|
+
if obj.__module__ == module_name and not name.startswith("_")
|
|
178
|
+
]
|
|
179
|
+
if not candidates:
|
|
180
|
+
raise RuntimeError(f"{client_path} içinde çağrılabilir bir SDK fonksiyonu bulunamadı.")
|
|
181
|
+
|
|
182
|
+
return candidates[0]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command("compile")
|
|
186
|
+
def compile_command(
|
|
187
|
+
goal: str = typer.Option(
|
|
188
|
+
...,
|
|
189
|
+
"--goal",
|
|
190
|
+
"-g",
|
|
191
|
+
help="Derlenecek API'nin amacı (örn: 'Hacker News üzerinde makale araması yap')",
|
|
192
|
+
),
|
|
193
|
+
name: str | None = typer.Option(
|
|
194
|
+
None,
|
|
195
|
+
"--name",
|
|
196
|
+
"-n",
|
|
197
|
+
help="Shadow Registry servis adı (verilmezse hedef domain adından otomatik türetilir, örn: hn_algolia)",
|
|
198
|
+
),
|
|
199
|
+
traffic_file: Path = typer.Option(
|
|
200
|
+
Path("captured_traffic.json"),
|
|
201
|
+
"--traffic",
|
|
202
|
+
"-t",
|
|
203
|
+
help="Okunacak yakalanmış trafik JSON dosyası",
|
|
204
|
+
),
|
|
205
|
+
client_out: Path = typer.Option(
|
|
206
|
+
Path("generated_client.py"),
|
|
207
|
+
"--client-out",
|
|
208
|
+
help="Üretilecek Python SDK dosya yolu",
|
|
209
|
+
),
|
|
210
|
+
mcp_out: Path = typer.Option(
|
|
211
|
+
Path("generated_mcp.py"),
|
|
212
|
+
"--mcp-out",
|
|
213
|
+
help="Üretilecek FastMCP sunucu dosya yolu",
|
|
214
|
+
),
|
|
215
|
+
) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Mevcut captured_traffic.json dosyasını derleyip generated_client.py, generated_mcp.py ve
|
|
218
|
+
registry/<servis_adi>/ dosyalarını üretir, ardından yanıt süresini (ms) ölçer.
|
|
219
|
+
"""
|
|
220
|
+
ensure_default_registry()
|
|
221
|
+
try:
|
|
222
|
+
compiler = ShadowAPICompiler(
|
|
223
|
+
traffic_path=traffic_file,
|
|
224
|
+
client_output_path=client_out,
|
|
225
|
+
mcp_output_path=mcp_out,
|
|
226
|
+
)
|
|
227
|
+
result = compiler.compile(goal=goal, service_name=name)
|
|
228
|
+
_print_compilation_summary(result)
|
|
229
|
+
|
|
230
|
+
with console.status("[bold cyan]generated_client.py test ediliyor ve yanıt süresi ölçülüyor...[/bold cyan]"):
|
|
231
|
+
elapsed_ms, client_output = _run_and_measure_client(compiler, result)
|
|
232
|
+
|
|
233
|
+
console.print(
|
|
234
|
+
Panel(
|
|
235
|
+
f"{client_output or '[Çıktı yok]'}\n\n"
|
|
236
|
+
f"[bold green]⏱ Toplam Çalıştırma ve Yanıt Süresi:[/bold green] "
|
|
237
|
+
f"[bold yellow]{elapsed_ms:.2f} ms[/bold yellow]",
|
|
238
|
+
title=f"🧪 Test Çıktısı ({result.client_path.name})",
|
|
239
|
+
border_style="cyan",
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
console.print(f"[bold red]✖ Derleme Hatası:[/bold red] {exc}")
|
|
244
|
+
raise typer.Exit(code=1)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@app.command("build")
|
|
248
|
+
def build_command(
|
|
249
|
+
url: str = typer.Argument(..., help="Kaydedilecek hedef web sitesi URL'i (örn: https://hn.algolia.com)"),
|
|
250
|
+
goal: str = typer.Option(
|
|
251
|
+
...,
|
|
252
|
+
"--goal",
|
|
253
|
+
"-g",
|
|
254
|
+
help="Derlenecek API'nin amacı (örn: 'Hacker News üzerinde makale araması yap')",
|
|
255
|
+
),
|
|
256
|
+
name: str | None = typer.Option(
|
|
257
|
+
None,
|
|
258
|
+
"--name",
|
|
259
|
+
"-n",
|
|
260
|
+
help="Shadow Registry servis adı (verilmezse hedef domain adından otomatik türetilir, örn: hn_algolia)",
|
|
261
|
+
),
|
|
262
|
+
traffic_file: Path = typer.Option(
|
|
263
|
+
Path("captured_traffic.json"),
|
|
264
|
+
"--traffic",
|
|
265
|
+
"-t",
|
|
266
|
+
help="Yakalanan trafiğin kaydedileceği JSON dosyası",
|
|
267
|
+
),
|
|
268
|
+
client_out: Path = typer.Option(
|
|
269
|
+
Path("generated_client.py"),
|
|
270
|
+
"--client-out",
|
|
271
|
+
help="Üretilecek Python SDK dosya yolu",
|
|
272
|
+
),
|
|
273
|
+
mcp_out: Path = typer.Option(
|
|
274
|
+
Path("generated_mcp.py"),
|
|
275
|
+
"--mcp-out",
|
|
276
|
+
help="Üretilecek FastMCP sunucu dosya yolu",
|
|
277
|
+
),
|
|
278
|
+
) -> None:
|
|
279
|
+
"""
|
|
280
|
+
Önce recorder.py ile tarayıcı kaydını başlatır (tarayıcı süresini saniye olarak ölçer),
|
|
281
|
+
kayıt bitince otomatik derleyip registry/<servis_adi>/ altına kaydeder ve hız karşılaştırmasını basar.
|
|
282
|
+
"""
|
|
283
|
+
ensure_default_registry()
|
|
284
|
+
if not url.startswith(("http://", "https://")):
|
|
285
|
+
url = f"https://{url}"
|
|
286
|
+
|
|
287
|
+
recorder = TrafficRecorder(target_url=url, output_path=traffic_file)
|
|
288
|
+
|
|
289
|
+
browser_start = time.perf_counter()
|
|
290
|
+
try:
|
|
291
|
+
asyncio.run(recorder.run())
|
|
292
|
+
except KeyboardInterrupt:
|
|
293
|
+
console.print("\n[yellow]Kayıt kullanıcı tarafından sonlandırıldı, mevcut veriler kaydediliyor...[/yellow]")
|
|
294
|
+
recorder.save_to_disk()
|
|
295
|
+
recorder.print_summary_table()
|
|
296
|
+
browser_seconds = time.perf_counter() - browser_start
|
|
297
|
+
|
|
298
|
+
if not recorder.captured_endpoints:
|
|
299
|
+
console.print("[bold red]✖ Hiçbir uç nokta yakalanamadığı için derleme aşamasına geçilemiyor.[/bold red]")
|
|
300
|
+
raise typer.Exit(code=1)
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
compiler = ShadowAPICompiler(
|
|
304
|
+
traffic_path=traffic_file,
|
|
305
|
+
client_output_path=client_out,
|
|
306
|
+
mcp_output_path=mcp_out,
|
|
307
|
+
)
|
|
308
|
+
result = compiler.compile(goal=goal, service_name=name)
|
|
309
|
+
_print_compilation_summary(result)
|
|
310
|
+
|
|
311
|
+
with console.status("[bold cyan]generated_client.py çalıştırılıyor ve hız ölçümü yapılıyor...[/bold cyan]"):
|
|
312
|
+
direct_ms, client_output = _run_and_measure_client(compiler, result)
|
|
313
|
+
|
|
314
|
+
console.print(
|
|
315
|
+
Panel(
|
|
316
|
+
client_output or "[Çıktı yok]",
|
|
317
|
+
title=f"🧪 Test Çıktısı ({result.client_path.name})",
|
|
318
|
+
border_style="cyan",
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
_print_benchmark_comparison(browser_seconds=browser_seconds, direct_ms=direct_ms)
|
|
323
|
+
|
|
324
|
+
except Exception as exc:
|
|
325
|
+
console.print(f"[bold red]✖ Derleme/Çalıştırma Hatası:[/bold red] {exc}")
|
|
326
|
+
raise typer.Exit(code=1)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@app.command("list")
|
|
330
|
+
def list_command(
|
|
331
|
+
registry_dir: Path = typer.Option(
|
|
332
|
+
Path("registry"),
|
|
333
|
+
"--registry",
|
|
334
|
+
"-r",
|
|
335
|
+
help="Taranacak OwnAPI Registry dizini",
|
|
336
|
+
),
|
|
337
|
+
tenant_id: str = typer.Option(
|
|
338
|
+
"default",
|
|
339
|
+
"--tenant",
|
|
340
|
+
"-t",
|
|
341
|
+
help="Token ve ToS onay durumu kontrol edilecek kiracı ID'si",
|
|
342
|
+
),
|
|
343
|
+
) -> None:
|
|
344
|
+
"""
|
|
345
|
+
registry/ klasöründeki tüm hazır API'leri (Moat), uç noktalarını,
|
|
346
|
+
fonksiyon adlarını, dinamik parametrelerini, TokenStore ve ToS onay durumlarını listeler.
|
|
347
|
+
"""
|
|
348
|
+
ensure_default_registry(registry_dir)
|
|
349
|
+
|
|
350
|
+
if not registry_dir.exists():
|
|
351
|
+
console.print("[yellow]Henüz hiçbir servis kayıtlı değil (`registry/` klasörü bulunamadı).[/yellow]")
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
service_dirs = sorted([d for d in registry_dir.iterdir() if d.is_dir() and (d / "schema.json").exists()])
|
|
355
|
+
if not service_dirs:
|
|
356
|
+
console.print("[yellow]Registry içinde kayıtlı API bulunamadı.[/yellow]")
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
from core.consent import ConsentManager
|
|
360
|
+
from core.token_store import TokenStore
|
|
361
|
+
|
|
362
|
+
token_store = TokenStore()
|
|
363
|
+
consent_mgr = ConsentManager()
|
|
364
|
+
|
|
365
|
+
table = Table(
|
|
366
|
+
title=f"🗂️ OwnAPI Enterprise Registry (Moat) — Kayıtlı API Envanteri ({len(service_dirs)} Servis)",
|
|
367
|
+
show_header=True,
|
|
368
|
+
header_style="bold magenta",
|
|
369
|
+
show_lines=True,
|
|
370
|
+
)
|
|
371
|
+
table.add_column("Servis", style="bold cyan", no_wrap=True)
|
|
372
|
+
table.add_column("Fonksiyon", style="bold yellow", overflow="fold")
|
|
373
|
+
table.add_column("Metot & Uç Nokta", style="white", overflow="fold", ratio=2)
|
|
374
|
+
table.add_column("Dinamik Parametreler", style="green", overflow="fold", ratio=1)
|
|
375
|
+
table.add_column("Mimari, Oturum & ToS Durumu", justify="center", overflow="fold")
|
|
376
|
+
|
|
377
|
+
for sdir in service_dirs:
|
|
378
|
+
schema_path = sdir / "schema.json"
|
|
379
|
+
client_path = sdir / "client.py"
|
|
380
|
+
try:
|
|
381
|
+
raw_schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
382
|
+
schema = RegistrySchema.model_validate(raw_schema)
|
|
383
|
+
val_status = raw_schema.get("validation_status", "BAŞARILI")
|
|
384
|
+
except Exception as exc:
|
|
385
|
+
table.add_row(sdir.name, "-", f"[red]Şema okuma hatası: {exc}[/red]", "-", "[red]Bozuk[/red]")
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
client_code = client_path.read_text(encoding="utf-8") if client_path.exists() else ""
|
|
389
|
+
has_own_client = ("BaseOwnClient" in client_code) or ("BaseShadowClient" in client_code) or ("SessionHealer" in client_code)
|
|
390
|
+
session_data = token_store.get_session(tenant_id, schema.service_name)
|
|
391
|
+
has_consent = consent_mgr.has_consented(tenant_id, schema.service_name)
|
|
392
|
+
|
|
393
|
+
if schema.dynamic_params:
|
|
394
|
+
param_str = ", ".join(f"{p.name}: {p.param_type}={repr(p.default_value)}" for p in schema.dynamic_params)
|
|
395
|
+
else:
|
|
396
|
+
param_str = "-"
|
|
397
|
+
|
|
398
|
+
consent_badge = "[cyan]⚖️ ToS Onaylı[/cyan]" if has_consent else "[dim]⚖️ İlk Çağrıda Onay[/dim]"
|
|
399
|
+
val_badge = f"[green]✔ Auto-Test: {val_status}[/green]"
|
|
400
|
+
|
|
401
|
+
if session_data and has_own_client:
|
|
402
|
+
updated_at = str(session_data.get("updated_at") or session_data.get("healed_at") or "")[:19].replace("T", " ")
|
|
403
|
+
health_badge = (
|
|
404
|
+
f"[bold green]🟢 User-Delegated (Aktif Oturum)[/bold green]\n"
|
|
405
|
+
f"{val_badge} | {consent_badge}\n"
|
|
406
|
+
f"[dim]Oturum: {updated_at}[/dim]"
|
|
407
|
+
)
|
|
408
|
+
elif has_own_client:
|
|
409
|
+
health_badge = (
|
|
410
|
+
f"[bold green]🟢 User-Delegated (Hazır)[/bold green]\n"
|
|
411
|
+
f"{val_badge} | {consent_badge}\n"
|
|
412
|
+
f"[dim]Local & Cloud Browser Destekli[/dim]"
|
|
413
|
+
)
|
|
414
|
+
elif client_path.exists():
|
|
415
|
+
health_badge = f"[bold yellow]🟡 Standart İstemci[/bold yellow]\n{consent_badge}"
|
|
416
|
+
else:
|
|
417
|
+
health_badge = "[bold red]🔴 client.py Eksik[/bold red]"
|
|
418
|
+
|
|
419
|
+
endpoint_display = f"[bold cyan]{schema.method}[/bold cyan] {schema.endpoint_url}\n[dim]{schema.explanation}[/dim]"
|
|
420
|
+
|
|
421
|
+
table.add_row(
|
|
422
|
+
schema.service_name,
|
|
423
|
+
f"{schema.function_name}()",
|
|
424
|
+
endpoint_display,
|
|
425
|
+
param_str,
|
|
426
|
+
health_badge,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
console.print()
|
|
430
|
+
console.print(table)
|
|
431
|
+
console.print()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@app.command("test")
|
|
435
|
+
def test_command(
|
|
436
|
+
query: str = typer.Option(
|
|
437
|
+
...,
|
|
438
|
+
"--query",
|
|
439
|
+
"-q",
|
|
440
|
+
help="Üretilen SDK fonksiyonuna gönderilecek dinamik test sorgusu (örn: 'yapay zeka')",
|
|
441
|
+
),
|
|
442
|
+
service: str | None = typer.Option(
|
|
443
|
+
None,
|
|
444
|
+
"--service",
|
|
445
|
+
"-s",
|
|
446
|
+
help="Test edilecek belirli bir Registry servis adı (örn: 'hackernews')",
|
|
447
|
+
),
|
|
448
|
+
client_file: Path = typer.Option(
|
|
449
|
+
Path("generated_client.py"),
|
|
450
|
+
"--client",
|
|
451
|
+
"-c",
|
|
452
|
+
help="Test edilecek Python SDK dosyası",
|
|
453
|
+
),
|
|
454
|
+
) -> None:
|
|
455
|
+
"""
|
|
456
|
+
Üretilen generated_client.py (veya registry/<servis>/client.py) içindeki fonksiyonu
|
|
457
|
+
terminalden verilen farklı bir parametreyle dinamik olarak test eder ve ms süresini basar.
|
|
458
|
+
"""
|
|
459
|
+
ensure_default_registry()
|
|
460
|
+
target_client = Path("registry") / service / "client.py" if service else client_file
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
fn_name, sdk_fn = _load_sdk_function(target_client)
|
|
464
|
+
sig = inspect.signature(sdk_fn)
|
|
465
|
+
params = list(sig.parameters.keys())
|
|
466
|
+
|
|
467
|
+
call_kwargs: dict[str, Any] = {}
|
|
468
|
+
call_args: list[Any] = []
|
|
469
|
+
if "query" in sig.parameters:
|
|
470
|
+
call_kwargs["query"] = query
|
|
471
|
+
param_display = f"query={repr(query)}"
|
|
472
|
+
elif params:
|
|
473
|
+
first_param = params[0]
|
|
474
|
+
call_kwargs[first_param] = query
|
|
475
|
+
param_display = f"{first_param}={repr(query)}"
|
|
476
|
+
else:
|
|
477
|
+
param_display = "(argümansız)"
|
|
478
|
+
|
|
479
|
+
with console.status(f"[bold cyan]{fn_name}({param_display}) çağrılıyor...[/bold cyan]"):
|
|
480
|
+
start_ns = time.perf_counter()
|
|
481
|
+
response_data = sdk_fn(*call_args, **call_kwargs)
|
|
482
|
+
elapsed_ms = (time.perf_counter() - start_ns) * 1000.0
|
|
483
|
+
|
|
484
|
+
summary_lines: list[str] = [
|
|
485
|
+
f"[bold green]✔ Fonksiyon:[/bold green] [bold yellow]{fn_name}({param_display})[/bold yellow]",
|
|
486
|
+
f"[bold green]⚡ Doğrudan Yanıt Süresi:[/bold green] [bold cyan]{elapsed_ms:.2f} ms[/bold cyan]",
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
if isinstance(response_data, dict):
|
|
490
|
+
if "hits" in response_data and isinstance(response_data["hits"], list):
|
|
491
|
+
hits = response_data["hits"]
|
|
492
|
+
total_hits = response_data.get("nbHits", len(hits))
|
|
493
|
+
summary_lines.append(
|
|
494
|
+
f"[bold green]📊 Bulunan Sonuç:[/bold green] Toplam [bold white]{total_hits}[/bold white] kayıt "
|
|
495
|
+
f"(İlk {min(5, len(hits))} sonuç listeleniyor)\n"
|
|
496
|
+
)
|
|
497
|
+
for idx, item in enumerate(hits[:5], start=1):
|
|
498
|
+
title = item.get("title") or item.get("story_title") or item.get("name") or str(item)[:80]
|
|
499
|
+
url = item.get("url") or item.get("story_url") or "URL yok"
|
|
500
|
+
points = item.get("points")
|
|
501
|
+
pts_str = f" [yellow]({points} puan)[/yellow]" if points is not None else ""
|
|
502
|
+
summary_lines.append(f" [bold cyan]{idx}.[/bold cyan] {title}{pts_str}\n [dim]{url}[/dim]")
|
|
503
|
+
elif "pages" in response_data and isinstance(response_data["pages"], list):
|
|
504
|
+
pages = response_data["pages"]
|
|
505
|
+
summary_lines.append(
|
|
506
|
+
f"[bold green]📊 Bulunan Sonuç:[/bold green] Toplam [bold white]{len(pages)}[/bold white] sayfa "
|
|
507
|
+
f"(İlk {min(5, len(pages))} sonuç listeleniyor)\n"
|
|
508
|
+
)
|
|
509
|
+
for idx, item in enumerate(pages[:5], start=1):
|
|
510
|
+
title = item.get("title") or item.get("key") or str(item)[:80]
|
|
511
|
+
desc = item.get("description") or "Açıklama yok"
|
|
512
|
+
key = item.get("key") or ""
|
|
513
|
+
wiki_url = f"https://en.wikipedia.org/wiki/{key}" if key else ""
|
|
514
|
+
summary_lines.append(
|
|
515
|
+
f" [bold cyan]{idx}.[/bold cyan] [bold white]{title}[/bold white] — [yellow]{desc}[/yellow]\n"
|
|
516
|
+
f" [dim]{wiki_url}[/dim]"
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
preview = json.dumps(response_data, ensure_ascii=False, indent=2)[:800]
|
|
520
|
+
summary_lines.append(f"\n[bold magenta]JSON Yanıt Önizlemesi:[/bold magenta]\n{preview}")
|
|
521
|
+
else:
|
|
522
|
+
summary_lines.append(f"\n[bold magenta]Yanıt:[/bold magenta] {str(response_data)[:800]}")
|
|
523
|
+
|
|
524
|
+
console.print(
|
|
525
|
+
Panel(
|
|
526
|
+
"\n".join(summary_lines),
|
|
527
|
+
title="🚀 Shadow-API Dinamik Sorgu Testi",
|
|
528
|
+
border_style="green",
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
except Exception as exc:
|
|
533
|
+
console.print(f"[bold red]✖ Test Hatası:[/bold red] {exc}")
|
|
534
|
+
raise typer.Exit(code=1)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _upsert_mcp_config(config_path: Path, server_name: str, server_entry: dict[str, Any]) -> None:
|
|
538
|
+
"""Verilen JSON yapılandırma dosyasına mcpServers altında yeni sunucuyu ekler veya günceller."""
|
|
539
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
540
|
+
existing_data: dict[str, Any] = {}
|
|
541
|
+
if config_path.exists():
|
|
542
|
+
try:
|
|
543
|
+
raw = config_path.read_text(encoding="utf-8").strip()
|
|
544
|
+
if raw:
|
|
545
|
+
existing_data = json.loads(raw)
|
|
546
|
+
except Exception:
|
|
547
|
+
existing_data = {}
|
|
548
|
+
|
|
549
|
+
if not isinstance(existing_data, dict):
|
|
550
|
+
existing_data = {}
|
|
551
|
+
|
|
552
|
+
mcp_servers = existing_data.get("mcpServers")
|
|
553
|
+
if not isinstance(mcp_servers, dict):
|
|
554
|
+
mcp_servers = {}
|
|
555
|
+
|
|
556
|
+
mcp_servers[server_name] = server_entry
|
|
557
|
+
existing_data["mcpServers"] = mcp_servers
|
|
558
|
+
|
|
559
|
+
config_path.write_text(
|
|
560
|
+
json.dumps(existing_data, ensure_ascii=False, indent=2) + "\n",
|
|
561
|
+
encoding="utf-8",
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@app.command("install-mcp")
|
|
566
|
+
def install_mcp_command(
|
|
567
|
+
name: str = typer.Option(
|
|
568
|
+
"shadow-api-master",
|
|
569
|
+
"--name",
|
|
570
|
+
"-n",
|
|
571
|
+
help="Kaydedilecek MCP sunucusunun adı (örn: 'hackernews' veya 'shadow-api-master')",
|
|
572
|
+
),
|
|
573
|
+
mcp_file: Path = typer.Option(
|
|
574
|
+
Path("master_mcp.py"),
|
|
575
|
+
"--mcp-file",
|
|
576
|
+
"-m",
|
|
577
|
+
help="Kurulacak MCP sunucu dosyası (varsayılan: master_mcp.py veya generated_mcp.py)",
|
|
578
|
+
),
|
|
579
|
+
) -> None:
|
|
580
|
+
"""
|
|
581
|
+
Üretilen MCP sunucusunu (master_mcp.py veya generated_mcp.py) hem proje içindeki
|
|
582
|
+
.cursor/mcp.json dosyasına hem de Claude Desktop (%APPDATA%\\Claude\\claude_desktop_config.json)
|
|
583
|
+
yapılandırmasına otomatik olarak ekler.
|
|
584
|
+
"""
|
|
585
|
+
ensure_default_registry()
|
|
586
|
+
target_mcp = mcp_file if mcp_file.exists() else Path("generated_mcp.py")
|
|
587
|
+
if not target_mcp.exists():
|
|
588
|
+
console.print(
|
|
589
|
+
f"[bold red]✖ {target_mcp} bulunamadı![/bold red] "
|
|
590
|
+
"Önce `python cli.py compile` veya `python cli.py build` komutunu çalıştırın."
|
|
591
|
+
)
|
|
592
|
+
raise typer.Exit(code=1)
|
|
593
|
+
|
|
594
|
+
abs_mcp_path = str(target_mcp.resolve())
|
|
595
|
+
python_exe = str(Path(sys.executable).resolve())
|
|
596
|
+
|
|
597
|
+
server_entry: dict[str, Any] = {
|
|
598
|
+
"command": python_exe,
|
|
599
|
+
"args": [abs_mcp_path],
|
|
600
|
+
"env": {
|
|
601
|
+
"PYTHONIOENCODING": "utf-8",
|
|
602
|
+
},
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
table = Table(
|
|
606
|
+
title=f"🔌 MCP Sunucu Kurulum Özeti ({name})",
|
|
607
|
+
show_header=True,
|
|
608
|
+
header_style="bold magenta",
|
|
609
|
+
show_lines=True,
|
|
610
|
+
)
|
|
611
|
+
table.add_column("İstemci / Hedef", style="bold cyan", width=22)
|
|
612
|
+
table.add_column("Yapılandırma Dosyası", style="white", overflow="fold")
|
|
613
|
+
table.add_column("Durum", style="bold green", width=26, justify="center")
|
|
614
|
+
|
|
615
|
+
# 1. Proje içi .cursor/mcp.json
|
|
616
|
+
cursor_config_path = Path(".cursor") / "mcp.json"
|
|
617
|
+
_upsert_mcp_config(cursor_config_path, name, server_entry)
|
|
618
|
+
table.add_row(
|
|
619
|
+
"Cursor IDE",
|
|
620
|
+
str(cursor_config_path.resolve()),
|
|
621
|
+
"[bold green]✔ Eklendi / Güncellendi[/bold green]",
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# 2. Claude Desktop (%APPDATA%\Claude\claude_desktop_config.json)
|
|
625
|
+
appdata = os.getenv("APPDATA")
|
|
626
|
+
if appdata:
|
|
627
|
+
claude_dir = Path(appdata) / "Claude"
|
|
628
|
+
claude_config_path = claude_dir / "claude_desktop_config.json"
|
|
629
|
+
if claude_dir.exists() or claude_config_path.exists():
|
|
630
|
+
_upsert_mcp_config(claude_config_path, name, server_entry)
|
|
631
|
+
table.add_row(
|
|
632
|
+
"Claude Desktop",
|
|
633
|
+
str(claude_config_path.resolve()),
|
|
634
|
+
"[bold green]✔ Eklendi / Güncellendi[/bold green]",
|
|
635
|
+
)
|
|
636
|
+
else:
|
|
637
|
+
_upsert_mcp_config(claude_config_path, name, server_entry)
|
|
638
|
+
table.add_row(
|
|
639
|
+
"Claude Desktop",
|
|
640
|
+
str(claude_config_path.resolve()),
|
|
641
|
+
"[bold yellow]✔ Oluşturuldu ve Eklendi[/bold yellow]",
|
|
642
|
+
)
|
|
643
|
+
else:
|
|
644
|
+
table.add_row(
|
|
645
|
+
"Claude Desktop",
|
|
646
|
+
"%APPDATA%\\Claude\\claude_desktop_config.json",
|
|
647
|
+
"[dim yellow]Atlandı (APPDATA bulunamadı)[/dim yellow]",
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
console.print()
|
|
651
|
+
console.print(table)
|
|
652
|
+
console.print(
|
|
653
|
+
Panel.fit(
|
|
654
|
+
f"[bold cyan]Sunucu Adı:[/bold cyan] [bold yellow]{name}[/bold yellow]\n"
|
|
655
|
+
f"[bold cyan]Komut:[/bold cyan] {python_exe}\n"
|
|
656
|
+
f"[bold cyan]Argüman:[/bold cyan] {abs_mcp_path}",
|
|
657
|
+
title="🚀 MCP Sunucusu Hazır",
|
|
658
|
+
border_style="green",
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@app.command("add")
|
|
664
|
+
def add_command(
|
|
665
|
+
service_name: str = typer.Argument(..., help="Yeni servis adı (örn: biletix)"),
|
|
666
|
+
url: str = typer.Argument(..., help="Giriş sayfası URL'si"),
|
|
667
|
+
desc: str = typer.Option("", "--desc", "-d", help="Servis açıklaması veya amacı"),
|
|
668
|
+
headless: bool = typer.Option(False, "--headless", help="Tarayıcıyı arka planda çalıştır"),
|
|
669
|
+
mode: str = typer.Option("local", "--mode", "-m", help="Çalışma modu: 'local' (yerel tarayıcı) veya 'cloud' (Docker/Remote Browser)"),
|
|
670
|
+
tenant_id: str = typer.Option("default", "--tenant", "-t", help="Kiracı ID'si"),
|
|
671
|
+
auto_validate: bool = typer.Option(True, "--validate/--no-validate", help="Derleme sonrası 3x Auto-Test (Compiler + Validator Loop) çalıştır"),
|
|
672
|
+
validation_runs: int = typer.Option(3, "--runs", help="Auto-Test doğrulama çağrısı sayısı (varsayılan: 3)"),
|
|
673
|
+
interactive_tos: bool = typer.Option(False, "--interactive-tos", help="ToS onayını terminalde etkileşimli E/h sorusuyla al"),
|
|
674
|
+
) -> None:
|
|
675
|
+
"""
|
|
676
|
+
Yeni bir web sitesini tarayıcı ile dinleyip otomatik olarak OwnAPI sistemine ekler,
|
|
677
|
+
ToS onayını loglar ve 3 kez otomatik doğrulama testinden (Compiler + Validator Loop) geçirir.
|
|
678
|
+
"""
|
|
679
|
+
from own_api.sdk import OwnAPIClient
|
|
680
|
+
|
|
681
|
+
client = OwnAPIClient(tenant_id=tenant_id, mode=mode)
|
|
682
|
+
result = client.onboard(
|
|
683
|
+
service_name=service_name,
|
|
684
|
+
start_url=url,
|
|
685
|
+
description=desc,
|
|
686
|
+
headless=headless,
|
|
687
|
+
mode=mode,
|
|
688
|
+
auto_validate=auto_validate,
|
|
689
|
+
validation_runs=validation_runs,
|
|
690
|
+
interactive_tos=interactive_tos,
|
|
691
|
+
)
|
|
692
|
+
if not result.get("success"):
|
|
693
|
+
raise typer.Exit(code=1)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@app.command("call")
|
|
697
|
+
def call_command(
|
|
698
|
+
service_name: str = typer.Argument(..., help="Çağrılacak servis adı (örn: thy, trendyol, enuygun, pegasus, wikipedia, hackernews)"),
|
|
699
|
+
param: list[str] = typer.Option(
|
|
700
|
+
None,
|
|
701
|
+
"--param",
|
|
702
|
+
"-p",
|
|
703
|
+
help="Fonksiyona iletilecek parametreler (key=val formatında, örn: -p query=laptop -p max_results=5)",
|
|
704
|
+
),
|
|
705
|
+
tenant_id: str = typer.Option("default", "--tenant", "-t", help="Kiracı ID'si"),
|
|
706
|
+
mode: str = typer.Option("local", "--mode", "-m", help="Çalışma modu: 'local' (yerel tarayıcı) veya 'cloud' (Docker/Remote Browser)"),
|
|
707
|
+
non_interactive: bool = typer.Option(
|
|
708
|
+
False,
|
|
709
|
+
"--non-interactive",
|
|
710
|
+
help="Captcha/WAF çıktığında terminalde beklemek yerine görevi HITL onay kuyruğuna ekle",
|
|
711
|
+
),
|
|
712
|
+
) -> None:
|
|
713
|
+
"""
|
|
714
|
+
Kayıtlı bir servisi ToS onay kaydı ve seçilen çalışma modu ('local' veya 'cloud') ile doğrudan çağırır.
|
|
715
|
+
"""
|
|
716
|
+
from own_api.sdk import OwnAPIClient
|
|
717
|
+
|
|
718
|
+
kwargs: dict[str, Any] = {}
|
|
719
|
+
if param:
|
|
720
|
+
for p in param:
|
|
721
|
+
if "=" in p:
|
|
722
|
+
k, v = p.split("=", 1)
|
|
723
|
+
k = k.strip()
|
|
724
|
+
v = v.strip()
|
|
725
|
+
if v.lower() == "true":
|
|
726
|
+
val: Any = True
|
|
727
|
+
elif v.lower() == "false":
|
|
728
|
+
val = False
|
|
729
|
+
elif v.isdigit():
|
|
730
|
+
val = int(v)
|
|
731
|
+
else:
|
|
732
|
+
try:
|
|
733
|
+
val = float(v)
|
|
734
|
+
except ValueError:
|
|
735
|
+
val = v
|
|
736
|
+
kwargs[k] = val
|
|
737
|
+
|
|
738
|
+
client = OwnAPIClient(tenant_id=tenant_id, mode=mode, non_interactive=non_interactive)
|
|
739
|
+
try:
|
|
740
|
+
start_t = time.perf_counter()
|
|
741
|
+
res = client.call(service_name, mode=mode, non_interactive=non_interactive, **kwargs)
|
|
742
|
+
elapsed_ms = (time.perf_counter() - start_t) * 1000
|
|
743
|
+
|
|
744
|
+
console.print(f"[dim]Yanıt Süresi: {elapsed_ms:.2f} ms | Tenant: {tenant_id} | Mod: {mode.upper()}[/dim]")
|
|
745
|
+
if isinstance(res, (dict, list)):
|
|
746
|
+
console.print_json(data=res)
|
|
747
|
+
else:
|
|
748
|
+
console.print(res)
|
|
749
|
+
except Exception as exc:
|
|
750
|
+
console.print(f"[bold red]✖ Çağrı Hatası:[/bold red] {exc}")
|
|
751
|
+
raise typer.Exit(code=1)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@app.command("validate")
|
|
755
|
+
def validate_command(
|
|
756
|
+
service_name: str | None = typer.Argument(
|
|
757
|
+
None,
|
|
758
|
+
help="Doğrulanacak servis adı (örn: enuygun, trendyol). --all ile tüm servisler taranır.",
|
|
759
|
+
),
|
|
760
|
+
all_services: bool = typer.Option(
|
|
761
|
+
False,
|
|
762
|
+
"--all",
|
|
763
|
+
"-a",
|
|
764
|
+
help="Tüm kayıtlı servisleri tek seferde tara, bozuk olanları Self-Healing 2.0 ile otomatik tamir et",
|
|
765
|
+
),
|
|
766
|
+
runs: int = typer.Option(3, "--runs", "-n", help="Başarılı olması gereken ardışık test çağrısı sayısı"),
|
|
767
|
+
auto_heal: bool = typer.Option(True, "--heal/--no-heal", help="Hata alan servisleri otomatik tamir et"),
|
|
768
|
+
tenant_id: str = typer.Option("default", "--tenant", "-t", help="Kiracı ID'si"),
|
|
769
|
+
mode: str = typer.Option("local", "--mode", "-m", help="Çalışma modu: 'local' veya 'cloud'"),
|
|
770
|
+
) -> None:
|
|
771
|
+
"""
|
|
772
|
+
Kayıtlı bir servis (veya --all ile tüm servisler) üzerinde Compiler + Validator Loop
|
|
773
|
+
ve Self-Healing 2.0 otomatik tamir katmanını çalıştırır.
|
|
774
|
+
"""
|
|
775
|
+
from core.auto_repair import AutoRepairEngine
|
|
776
|
+
from core.onboarding import validate_and_repair_client
|
|
777
|
+
|
|
778
|
+
if all_services or not service_name:
|
|
779
|
+
engine = AutoRepairEngine(tenant_id=tenant_id, mode=mode)
|
|
780
|
+
scan_runs = 1 if (all_services and runs == 3) else runs
|
|
781
|
+
engine.validate_all_services(runs=scan_runs, auto_heal=auto_heal)
|
|
782
|
+
return
|
|
783
|
+
|
|
784
|
+
service_dir = Path("registry") / service_name
|
|
785
|
+
client_path = service_dir / "client.py"
|
|
786
|
+
schema_path = service_dir / "schema.json"
|
|
787
|
+
traffic_path = service_dir / "captured_traffic.json"
|
|
788
|
+
|
|
789
|
+
if not client_path.exists() or not schema_path.exists():
|
|
790
|
+
console.print(f"[bold red]✖ '{service_name}' servisi registry altında bulunamadı![/bold red]")
|
|
791
|
+
raise typer.Exit(code=1)
|
|
792
|
+
|
|
793
|
+
raw_schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
794
|
+
schema_obj = RegistrySchema.model_validate(raw_schema)
|
|
795
|
+
endpoints: list[dict[str, Any]] = []
|
|
796
|
+
if traffic_path.exists():
|
|
797
|
+
try:
|
|
798
|
+
tdata = json.loads(traffic_path.read_text(encoding="utf-8"))
|
|
799
|
+
endpoints = tdata.get("endpoints", [])
|
|
800
|
+
except Exception:
|
|
801
|
+
endpoints = []
|
|
802
|
+
|
|
803
|
+
matched_ep = next((e for e in endpoints if e.get("url") == schema_obj.endpoint_url), endpoints[0] if endpoints else {})
|
|
804
|
+
report = validate_and_repair_client(
|
|
805
|
+
service_name=service_name,
|
|
806
|
+
target_url=schema_obj.target_url,
|
|
807
|
+
description=schema_obj.goal,
|
|
808
|
+
client_path=client_path,
|
|
809
|
+
schema_obj=schema_obj,
|
|
810
|
+
matched_endpoint=matched_ep,
|
|
811
|
+
all_endpoints=endpoints,
|
|
812
|
+
validation_runs=runs,
|
|
813
|
+
max_repair_attempts=3,
|
|
814
|
+
tenant_id=tenant_id,
|
|
815
|
+
mode=mode,
|
|
816
|
+
)
|
|
817
|
+
raw_schema["validation_status"] = report.get("validation_status", "BAŞARILI")
|
|
818
|
+
raw_schema["validation_passed_runs"] = report.get("passed_runs", runs)
|
|
819
|
+
schema_path.write_text(json.dumps(raw_schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
@app.command("daemon")
|
|
823
|
+
def daemon_command(
|
|
824
|
+
schedule_time: str = typer.Option(
|
|
825
|
+
"03:00",
|
|
826
|
+
"--time",
|
|
827
|
+
"-t",
|
|
828
|
+
help="Her gece otomatik bakımın çalışacağı saat (HH:MM formatında, varsayılan: 03:00)",
|
|
829
|
+
),
|
|
830
|
+
interval_minutes: float | None = typer.Option(
|
|
831
|
+
None,
|
|
832
|
+
"--interval",
|
|
833
|
+
"-i",
|
|
834
|
+
help="Saat yerine dakika bazlı periyot (örn: 60 -> her 60 dakikada bir)",
|
|
835
|
+
),
|
|
836
|
+
runs: int = typer.Option(1, "--runs", "-n", help="Her servis için yapılacak sağlık testi sayısı"),
|
|
837
|
+
auto_heal: bool = typer.Option(True, "--heal/--no-heal", help="Bozulan servisleri otomatik tamir et"),
|
|
838
|
+
run_immediately: bool = typer.Option(False, "--now", help="Daemon başlarken ilk taramayı hemen yap"),
|
|
839
|
+
max_cycles: int | None = typer.Option(None, "--max-cycles", help="Maksimum bakım döngüsü sayısı (test amaçlı)"),
|
|
840
|
+
tenant_id: str = typer.Option("default", "--tenant", help="Kiracı ID'si"),
|
|
841
|
+
mode: str = typer.Option("local", "--mode", "-m", help="Çalışma modu: 'local' veya 'cloud'"),
|
|
842
|
+
) -> None:
|
|
843
|
+
"""
|
|
844
|
+
Self-Healing 2.0 Gece Otomatik Bakım ve Tamir Servisini (varsayılan her gece 03:00) başlatır.
|
|
845
|
+
"""
|
|
846
|
+
from core.auto_repair import AutoRepairEngine
|
|
847
|
+
|
|
848
|
+
engine = AutoRepairEngine(tenant_id=tenant_id, mode=mode)
|
|
849
|
+
engine.run_daemon(
|
|
850
|
+
schedule_time=schedule_time,
|
|
851
|
+
interval_minutes=interval_minutes,
|
|
852
|
+
runs=runs,
|
|
853
|
+
auto_heal=auto_heal,
|
|
854
|
+
run_immediately=run_immediately,
|
|
855
|
+
max_cycles=max_cycles,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
@app.command("queue")
|
|
860
|
+
def queue_command(
|
|
861
|
+
tenant_id: str = typer.Option("default", "--tenant", "-t", help="Kuyruğu görüntülenecek kiracı ID'si (--all-tenants ile tümü)"),
|
|
862
|
+
all_tenants: bool = typer.Option(False, "--all-tenants", "-a", help="Tüm kiracıların HITL kuyruğunu listele"),
|
|
863
|
+
status: str | None = typer.Option(None, "--status", "-s", help="Durum filtresi (PENDING_USER_ACTION veya COMPLETED)"),
|
|
864
|
+
) -> None:
|
|
865
|
+
"""
|
|
866
|
+
Human-in-the-Loop (HITL) onay kuyruğunda bekleyen (PENDING_USER_ACTION) ve tamamlanan (COMPLETED) görevleri listeler.
|
|
867
|
+
"""
|
|
868
|
+
from core.hitl_queue import HITLQueueManager
|
|
869
|
+
|
|
870
|
+
mgr = HITLQueueManager()
|
|
871
|
+
mgr.print_queue_table(
|
|
872
|
+
tenant_id=None if all_tenants else tenant_id,
|
|
873
|
+
status_filter=status,
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
@app.command("resume")
|
|
878
|
+
def resume_command(
|
|
879
|
+
task_id: str = typer.Argument(..., help="Tarayıcıda onaylanıp devam ettirilecek HITL görev ID'si (örn: hitl_a1b2c3d4)"),
|
|
880
|
+
tenant_id: str | None = typer.Option(None, "--tenant", "-t", help="İsteğe bağlı kiracı ID'si"),
|
|
881
|
+
) -> None:
|
|
882
|
+
"""
|
|
883
|
+
HITL kuyruğunda onay bekleyen (PENDING_USER_ACTION) bir görevi tarayıcıda açıp doğrulamayı
|
|
884
|
+
tamamladıktan sonra kaldığı yerden devam ettirir.
|
|
885
|
+
"""
|
|
886
|
+
from core.hitl_queue import HITLQueueManager
|
|
887
|
+
|
|
888
|
+
mgr = HITLQueueManager()
|
|
889
|
+
try:
|
|
890
|
+
res = mgr.resume_task(task_id=task_id, tenant_id=tenant_id)
|
|
891
|
+
console.print_json(data=res)
|
|
892
|
+
except Exception as exc:
|
|
893
|
+
console.print(f"[bold red]✖ HITL Görev Devam Ettirme Hatası:[/bold red] {exc}")
|
|
894
|
+
raise typer.Exit(code=1)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
@app.command("approve")
|
|
898
|
+
def approve_command(
|
|
899
|
+
task_id: str = typer.Argument(..., help="Tarayıcıda onaylanıp devam ettirilecek HITL görev ID'si (örn: hitl_a1b2c3d4)"),
|
|
900
|
+
tenant_id: str | None = typer.Option(None, "--tenant", "-t", help="İsteğe bağlı kiracı ID'si"),
|
|
901
|
+
) -> None:
|
|
902
|
+
"""
|
|
903
|
+
`own-api resume <task_id>` komutu için kısayol: Bekleyen HITL görevini tarayıcıda onaylayıp tamamlar.
|
|
904
|
+
"""
|
|
905
|
+
resume_command(task_id=task_id, tenant_id=tenant_id)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@app.command("consent")
|
|
909
|
+
def consent_command(
|
|
910
|
+
tenant_id: str = typer.Option("default", "--tenant", "-t", help="Denetim günlüğü görüntülenecek kiracı ID'si"),
|
|
911
|
+
) -> None:
|
|
912
|
+
"""
|
|
913
|
+
ToS / Kullanıcı Delegasyon Onay Mekanizması (User Delegation Disclaimer) denetim kayıtlarını listeler.
|
|
914
|
+
"""
|
|
915
|
+
from core.consent import ConsentManager, USER_DELEGATION_DISCLAIMER
|
|
916
|
+
|
|
917
|
+
mgr = ConsentManager()
|
|
918
|
+
records = mgr.get_consents(tenant_id)
|
|
919
|
+
console.print(
|
|
920
|
+
Panel.fit(
|
|
921
|
+
f"[white]{USER_DELEGATION_DISCLAIMER}[/white]",
|
|
922
|
+
title="⚖️ OwnAPI Terms of Service & User Delegation Disclaimer",
|
|
923
|
+
border_style="yellow",
|
|
924
|
+
)
|
|
925
|
+
)
|
|
926
|
+
if not records:
|
|
927
|
+
console.print(f"[yellow]'{tenant_id}' kiracısı için henüz kaydedilmiş bir onay kaydı bulunmuyor.[/yellow]")
|
|
928
|
+
return
|
|
929
|
+
|
|
930
|
+
table = Table(
|
|
931
|
+
title=f"📜 Kullanıcı Delegasyon Onay Günlüğü (.sessions/{tenant_id}/consent_audit.json)",
|
|
932
|
+
show_header=True,
|
|
933
|
+
header_style="bold cyan",
|
|
934
|
+
show_lines=True,
|
|
935
|
+
)
|
|
936
|
+
table.add_column("#", justify="right", style="dim")
|
|
937
|
+
table.add_column("Servis", style="bold magenta")
|
|
938
|
+
table.add_column("İşlem", style="yellow")
|
|
939
|
+
table.add_column("Mod", style="green")
|
|
940
|
+
table.add_column("Yetkilendiren", style="cyan")
|
|
941
|
+
table.add_column("SHA256", style="dim")
|
|
942
|
+
table.add_column("Tarih (UTC)", style="white")
|
|
943
|
+
|
|
944
|
+
for idx, r in enumerate(records, start=1):
|
|
945
|
+
table.add_row(
|
|
946
|
+
str(idx),
|
|
947
|
+
str(r.get("service", "-")),
|
|
948
|
+
str(r.get("action", "-")),
|
|
949
|
+
str(r.get("execution_mode", "local")).upper(),
|
|
950
|
+
str(r.get("delegated_by", "-")),
|
|
951
|
+
str(r.get("disclaimer_sha256", "-")),
|
|
952
|
+
str(r.get("accepted_at", "-"))[:19].replace("T", " "),
|
|
953
|
+
)
|
|
954
|
+
console.print(table)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
@app.command("serve")
|
|
958
|
+
def serve_command(
|
|
959
|
+
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Sunucu dinleme adresi"),
|
|
960
|
+
port: int = typer.Option(8000, "--port", "-p", help="Sunucu port numarası (varsayılan: 8000)"),
|
|
961
|
+
reload: bool = typer.Option(False, "--reload", help="Kod değişikliklerinde otomatik yeniden başlat"),
|
|
962
|
+
) -> None:
|
|
963
|
+
"""
|
|
964
|
+
OwnAPI Enterprise Web Dashboard, Live Playground & REST API Sunucusunu (http://localhost:8000) başlatır.
|
|
965
|
+
"""
|
|
966
|
+
from server import run_server
|
|
967
|
+
|
|
968
|
+
console.print(
|
|
969
|
+
Panel.fit(
|
|
970
|
+
f"[bold green]🌐 OwnAPI Enterprise Web Dashboard & REST API Sunucusu Başlatılıyor![/bold green]\n"
|
|
971
|
+
f"[white]Web Dashboard & Live Playground:[/white] [bold cyan]http://{host}:{port}[/bold cyan]\n"
|
|
972
|
+
f"[white]Interactive OpenAPI / Swagger:[/white] [bold yellow]http://{host}:{port}/docs[/bold yellow]",
|
|
973
|
+
title="OwnAPI Enterprise Server",
|
|
974
|
+
border_style="green",
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
run_server(host=host, port=port, reload=reload)
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
@app.command("mcp")
|
|
981
|
+
def mcp_command() -> None:
|
|
982
|
+
"""
|
|
983
|
+
Dinamik OwnAPI Master MCP sunucusunu (master_mcp.py) başlatır.
|
|
984
|
+
"""
|
|
985
|
+
from master_mcp import mcp
|
|
986
|
+
mcp.run()
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
@app.command("ask")
|
|
990
|
+
def ask_command(
|
|
991
|
+
prompt: str = typer.Argument(..., help="Ajan tarafından yanıtlanacak doğal dil sorusu"),
|
|
992
|
+
tenant_id: str = typer.Option("default", "--tenant", "-t", help="Kiracı ID'si"),
|
|
993
|
+
mode: str = typer.Option("local", "--mode", "-m", help="Çalışma modu: 'local' veya 'cloud'"),
|
|
994
|
+
) -> None:
|
|
995
|
+
"""
|
|
996
|
+
Doğal dil sorusunu kayıtlı tüm servisleri ve Function Calling'i kullanarak yanıtlar.
|
|
997
|
+
"""
|
|
998
|
+
from own_api.sdk import OwnAPIClient
|
|
999
|
+
|
|
1000
|
+
client = OwnAPIClient(tenant_id=tenant_id, mode=mode)
|
|
1001
|
+
try:
|
|
1002
|
+
with console.status("[bold green]OwnAPI Ajanı yanıt hazırlıyor ve araçları çalıştırıyor...[/bold green]"):
|
|
1003
|
+
answer = client.ask(prompt)
|
|
1004
|
+
console.print(Panel(answer, title="🤖 OwnAPI Enterprise Agent", border_style="green"))
|
|
1005
|
+
except Exception as exc:
|
|
1006
|
+
console.print(f"[bold red]✖ Ajan Hatası:[/bold red] {exc}")
|
|
1007
|
+
raise typer.Exit(code=1)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def main() -> None:
|
|
1011
|
+
"""OwnAPI CLI giriş noktası."""
|
|
1012
|
+
import ownapi.cli # noqa: F401
|
|
1013
|
+
app()
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
if __name__ == "__main__":
|
|
1017
|
+
main()
|