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
compiler.py ADDED
@@ -0,0 +1,948 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import copy
5
+ import json
6
+ import os
7
+ import re
8
+ import sys
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from urllib.parse import urlparse
13
+
14
+ from dotenv import load_dotenv
15
+ from pydantic import BaseModel, Field
16
+ from rich.console import Console
17
+ from rich.panel import Panel
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
+ from recorder import CapturedEndpoint, CaptureSession
27
+
28
+ console = Console(stderr=True, legacy_windows=False)
29
+
30
+ # Varsayılan Tarayıcı Parmak İzi (Browser Fingerprint Spoofing)
31
+ DEFAULT_USER_AGENT: str = (
32
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
33
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
34
+ )
35
+
36
+ # httpx tarafından otomatik yönetilen alt seviye aktarım başlıkları
37
+ EXCLUDED_HEADERS: frozenset[str] = frozenset(
38
+ {
39
+ "accept-encoding",
40
+ "accept-language",
41
+ "content-length",
42
+ "host",
43
+ "connection",
44
+ "keep-alive",
45
+ "transfer-encoding",
46
+ "te",
47
+ "trailer",
48
+ "upgrade",
49
+ "proxy-authorization",
50
+ "proxy-authenticate",
51
+ "cache-control",
52
+ "pragma",
53
+ "priority",
54
+ }
55
+ )
56
+
57
+ # Korunması gereken kritik kimlik doğrulama, oturum ve tarayıcı parmak izi başlıkları
58
+ ALLOWED_HEADER_PREFIXES: tuple[str, ...] = (
59
+ "authorization",
60
+ "cookie",
61
+ "content-type",
62
+ "accept",
63
+ "user-agent",
64
+ "referer",
65
+ "origin",
66
+ "sec-ch-ua",
67
+ "sec-ch-ua-mobile",
68
+ "sec-ch-ua-platform",
69
+ "sec-fetch-dest",
70
+ "sec-fetch-mode",
71
+ "sec-fetch-site",
72
+ "x-algolia-",
73
+ "x-api-key",
74
+ "api-key",
75
+ "x-csrf",
76
+ "x-xsrf",
77
+ "csrf",
78
+ "xsrf",
79
+ "x-auth",
80
+ "x-session",
81
+ "x-token",
82
+ "x-requested-with",
83
+ "x-px",
84
+ "px-",
85
+ "_px",
86
+ "pxcts",
87
+ "x-perimeterx",
88
+ "tafsessionid",
89
+ "taftrackingid",
90
+ "x-current-page-url",
91
+ )
92
+
93
+ # LLM promptuna dahil edilmeyecek 3. taraf reklam, piksel, telemetri ve analitik domain parçaları
94
+ THIRD_PARTY_IGNORE_KEYWORDS: tuple[str, ...] = (
95
+ "yandex",
96
+ "evergage",
97
+ "useinsider",
98
+ "openai",
99
+ "creativecdn",
100
+ "firebase",
101
+ "google-analytics",
102
+ "googletagmanager",
103
+ "doubleclick",
104
+ "googleadservices",
105
+ "facebook",
106
+ "criteo",
107
+ "hotjar",
108
+ "clarity.ms",
109
+ "sentry",
110
+ "mixpanel",
111
+ "amplitude",
112
+ "datadog",
113
+ "newrelic",
114
+ "adroll",
115
+ "taboola",
116
+ "outbrain",
117
+ "tiktok",
118
+ "snapchat",
119
+ "quantummetric",
120
+ "dynatrace",
121
+ "adform",
122
+ "tapad",
123
+ "yabs",
124
+ "pixel",
125
+ "telemetry",
126
+ "beacon",
127
+ )
128
+
129
+
130
+ def extract_root_domain(url_or_domain: str) -> str:
131
+ """URL veya domain adından kök domaini (örn: web.flypgs.com -> flypgs.com) döndürür."""
132
+ parsed = urlparse(url_or_domain if "://" in url_or_domain else f"https://{url_or_domain}")
133
+ host = (parsed.hostname or url_or_domain).lower().strip()
134
+ parts = host.split(".")
135
+ if len(parts) >= 3 and parts[-2] in ("com", "org", "gov", "edu", "net", "co"):
136
+ return ".".join(parts[-3:])
137
+ if len(parts) >= 2:
138
+ return ".".join(parts[-2:])
139
+ return host
140
+
141
+
142
+ class DynamicParamSpec(BaseModel):
143
+ """LLM tarafından tespit edilen dinamik parametre tanımı."""
144
+
145
+ name: str = Field(..., description="Python fonksiyon argümanı adı (snake_case, örn: query, page)")
146
+ original_key: str = Field(..., description="Payload veya query_params içindeki orijinal anahtar adı")
147
+ location: str = Field(..., description="Parametrenin yeri: 'body', 'query' veya 'path'")
148
+ param_type: str = Field(default="str", description="Python tipi: 'str', 'int', 'float', 'bool'")
149
+ default_value: Any = Field(..., description="Yakalanan son istekteki örnek/varsayılan değer")
150
+ description: str = Field(default="", description="Parametrenin kısa açıklaması")
151
+
152
+
153
+ class RegistrySchema(BaseModel):
154
+ """registry/<servis_adi>/schema.json için kalıcı servis şeması."""
155
+
156
+ service_name: str
157
+ target_url: str
158
+ goal: str
159
+ function_name: str
160
+ explanation: str
161
+ method: str
162
+ endpoint_url: str
163
+ dynamic_params: list[DynamicParamSpec] = Field(default_factory=list)
164
+ created_at: str
165
+
166
+
167
+ class CompilationResult(BaseModel):
168
+ """Derleme sonucunda üretilen meta veri ve kaynak kodlar."""
169
+
170
+ service_name: str = "default"
171
+ provider: str
172
+ model: str
173
+ matched_endpoint_id: int
174
+ matched_method: str
175
+ matched_url: str
176
+ function_name: str
177
+ explanation: str
178
+ dynamic_params: list[DynamicParamSpec] = Field(default_factory=list)
179
+ client_code: str
180
+ mcp_code: str
181
+ client_path: Path = Path("generated_client.py")
182
+ mcp_path: Path = Path("generated_mcp.py")
183
+ registry_client_path: Path | None = None
184
+ registry_schema_path: Path | None = None
185
+
186
+
187
+ def derive_service_name(url_or_domain: str) -> str:
188
+ """Hedef URL veya domain adından temiz bir servis adı türetir (örn: https://hn.algolia.com -> hn_algolia)."""
189
+ parsed = urlparse(url_or_domain if "://" in url_or_domain else f"https://{url_or_domain}")
190
+ host = (parsed.hostname or url_or_domain).lower().strip()
191
+ if host.startswith("www."):
192
+ host = host[4:]
193
+ # Bilinen üst seviye uzantıları temizle
194
+ for tld in (".com.tr", ".org.tr", ".gov.tr", ".edu.tr", ".co.uk", ".com", ".net", ".org", ".io", ".dev", ".app", ".ai", ".co", ".me"):
195
+ if host.endswith(tld) and len(host) > len(tld):
196
+ host = host[: -len(tld)]
197
+ break
198
+ sanitized = re.sub(r"[^a-z0-9]+", "_", host).strip("_")
199
+ return sanitized or "shadow_service"
200
+
201
+
202
+ SYSTEM_PROMPT = """\
203
+ Sen kıdemli bir Sistem Mühendisi ve API Tersine Mühendislik (Reverse Engineering) uzmanısın.
204
+ Görevin, yakalanmış ve tekilleştirilmiş XHR/Fetch ağ trafiği (JSON) içinden kullanıcının belirttiği hedefe (`goal`) karşılık gelen ASIL uç noktayı (endpoint) tespit etmek ve bu uç noktayı doğrudan çağıran iki adet üretim kalitesinde Python dosyası (`generated_client.py` ve `generated_mcp.py`) üretmektir.
205
+
206
+ KESİN KURallar:
207
+ 1. Sağlık kontrolü (`isalive`, `ping`, `health`), telemetri veya analitik uç noktalarını DEĞİL, doğrudan kullanıcının hedeflediği işi (örn. arama, listeleme, veri çekme) yapan asıl veri uç noktasını seç.
208
+ 2. Başlık (Header) Filtrelemesi ve Browser Fingerprint Spoofing (ZORUNLU):
209
+ - `accept-encoding`, `content-length`, `host`, `connection` gibi aktarım başlıklarını koda ekleme.
210
+ - Buna karşılık tarayıcı parmak izi (fingerprint) ve kimlik başlıklarını (`user-agent`, `referer`, `origin`, `accept`, `sec-ch-ua*`, `sec-fetch-*`, `x-requested-with`, `x-algolia-*`, `Authorization`, `Cookie`, `Content-Type`) MUTLAKA koru.
211
+ - Üretilen `headers` sözlüğünde eğer `user-agent` yoksa varsayılan olarak `"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"`, hedef sitenin `referer` başlığı ve `"accept": "application/json"` başlığı ZORUNLU olarak bulunmalıdır.
212
+ - ÖNEMLİ: Eğer yakalanan isteğin `request_payload` alanı bir JSON nesnesi (dict/list) ise ve `content-type` başlığı `application/x-www-form-urlencoded` olarak görünüyorsa, `httpx` çağrısında gövdeyi mutlaka `json=payload` olarak gönder (`data=payload` KULLANMA).
213
+ 3. Dinamik Parametre Çıkarımı:
214
+ - Kullanıcının amacına göre (ör. arama sorgusu `query`, sayfa numarası `page`, limit `hitsPerPage` vb.) dinamik olması gereken parametreleri fonksiyon argümanı haline getir.
215
+ - Fonksiyon argümanlarına yakalanan son istekteki değerleri varsayılan (default) değer olarak ata ki fonksiyon argümansız çağrıldığında bile doğrudan çalışabilsin.
216
+ - Statik kalan diğer payload ve query parametrelerini fonksiyon içinde koru.
217
+ - Uçuş/Seyahat Arama ve Gidiş-Dönüş (Round-Trip) Desteği:
218
+ * Kullanıcı gidiş-dönüş sorgusu yaptığında (`return_date` parametresi dolu olduğunda), THY ve havayolu API'lerine gönderilen `originDestinationInformationList` payload şemasına hem gidiş (örn. IST -> ESB) hem de dönüş (örn. ESB -> IST) segmentlerini eksiksiz ekle ve `selectedBookerSearch` değerini 'R' yap.
219
+ * `return_date` parametresi boş veya None olduğunda ise tek yönlü (One-Way) arama olarak tek segmentli yapıyı koru ve `selectedBookerSearch` değerini 'O' yap.
220
+ 4. `generated_client.py` Gereksinimleri (TARAYICI İÇİ JAVASCRIPT ÇALIŞTIRMA VE ENTERPRISE WAF/BOT BYPASS ZORUNLU):
221
+ - DIŞARIDAN DOĞRUDAN HTTP veya `curl_cffi` ISTEĞI ATMA. Akamai, Cloudflare ve cpr_chlge korumaları dışarıdan atılan istekleri anında 403 Forbidden veya 406 ile bloklar.
222
+ - Bunun yerine tarayıcı oturumu üzerinden tarayıcının kendi JavaScript ortamında (`page.evaluate("fetch(...)")`) istek çalıştırılmalıdır.
223
+ - Orijinal Gezinme URL'si (Referer):
224
+ * Tarayıcı oturumunu başlatırken ve `headers` içindeki `referer` alanında kök domain yerine doğrudan genel rezervasyon ana giriş sayfasını (örn. `https://www.turkishairlines.com/tr-tr/ucak-bileti/rezervasyon/`) kullan ki timeout hatası oluşmadan sayfa hızlıca açılsın ve PerimeterX "basılı tut" doğrulaması doğrudan ekrana gelsin.
225
+ - Dosyanın en başına `import sys, pathlib; sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))` ve `from healer import SessionHealer, update_payload_deep` içe aktarımını ekle.
226
+ - Temiz, tip güvenli (type-hinted) ve docstring içeren bir ana fonksiyon yaz.
227
+ - Fonksiyon içinde dinamik parametreleri güncelle (`update_payload_deep`), ardından `healer = SessionHealer()` oluştur.
228
+ - İsteği tarayıcı context'i içinde çalıştırmak ve sonucu almak için doğrudan `return healer.execute_in_browser(method=..., url=..., headers=headers, params=params, json_payload=payload)` çağrısını yap.
229
+ - `if __name__ == "__main__":` bloğunda örnek bir sorgu çalıştır, süreyi ölç ve dönen JSON yanıtının anlamlı bir özetini terminale anlaşılır biçimde yazdır.
230
+ 5. `generated_mcp.py` Gereksinimleri:
231
+ - `from mcp.server.fastmcp import FastMCP` kullan (`mcp = FastMCP("ShadowAPI")`).
232
+ - Aynı işlevi `@mcp.tool()` dekoratörüyle dışa aç.
233
+ - `if __name__ == "__main__": mcp.run()` bloğunu ekle.
234
+ 6. Yanıtını SADECE aşağıdaki JSON şemasında döndür (Markdown kod bloğu olmadan veya tek bir ```json bloğu içinde):
235
+ {
236
+ "matched_endpoint_id": <int>,
237
+ "function_name": "<snake_case_fonksiyon_adi>",
238
+ "explanation": "<Neden bu uç noktanın seçildiği ve hangi parametrelerin dinamikleştirildiğinin kısa Türkçe açıklaması>",
239
+ "dynamic_params": [
240
+ {
241
+ "name": "<arguman_adi>",
242
+ "original_key": "<orijinal_anahtar>",
243
+ "location": "body | query | path",
244
+ "param_type": "str | int | float | bool",
245
+ "default_value": <varsayilan_deger>,
246
+ "description": "<açıklama>"
247
+ }
248
+ ],
249
+ "client_code": "<generated_client.py tam Python kodu>",
250
+ "mcp_code": "<generated_mcp.py tam Python kodu>"
251
+ }
252
+ """
253
+
254
+
255
+ class ShadowAPICompiler:
256
+ """Yakalanan ağ trafiğini LLM yardımıyla Python SDK ve MCP sunucusuna derler."""
257
+
258
+ def __init__(
259
+ self,
260
+ traffic_path: Path = Path("captured_traffic.json"),
261
+ client_output_path: Path = Path("generated_client.py"),
262
+ mcp_output_path: Path = Path("generated_mcp.py"),
263
+ registry_root: Path = Path("registry"),
264
+ ) -> None:
265
+ load_dotenv(override=False)
266
+ self.traffic_path = traffic_path
267
+ self.client_output_path = client_output_path
268
+ self.mcp_output_path = mcp_output_path
269
+ self.registry_root = registry_root
270
+ self.provider, self.api_key, self.model = self._detect_llm_provider()
271
+
272
+ @staticmethod
273
+ def _detect_llm_provider() -> tuple[str, str, str]:
274
+ """Ortam değişkenlerinden (.env) uygun LLM sağlayıcısını (Anthropic veya OpenAI) seçer."""
275
+ anthropic_key = os.getenv("ANTHROPIC_API_KEY", "").strip()
276
+ openai_key = os.getenv("OPENAI_API_KEY", "").strip()
277
+
278
+ if anthropic_key:
279
+ model = os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-20250514").strip()
280
+ return ("anthropic", anthropic_key, model)
281
+ if openai_key:
282
+ model = os.getenv("OPENAI_MODEL", "gpt-4o").strip()
283
+ return ("openai", openai_key, model)
284
+
285
+ raise RuntimeError(
286
+ "LLM API anahtarı bulunamadı! Lütfen `.env` dosyasına veya ortam değişkenlerine "
287
+ "`ANTHROPIC_API_KEY` veya `OPENAI_API_KEY` ekleyin."
288
+ )
289
+
290
+ @staticmethod
291
+ def sanitize_headers(headers: dict[str, str], target_url: str = "") -> dict[str, str]:
292
+ """
293
+ httpx tarafından otomatik yönetilen aktarım başlıklarını temizler ve
294
+ Browser Fingerprint Spoofing (User-Agent, Referer, Accept) başlıklarını zorunlu olarak ekler.
295
+ """
296
+ cleaned: dict[str, str] = {}
297
+ for k, v in headers.items():
298
+ lower_k = k.lower().strip()
299
+ if lower_k in EXCLUDED_HEADERS:
300
+ continue
301
+ if any(lower_k == prefix or lower_k.startswith(prefix) for prefix in ALLOWED_HEADER_PREFIXES):
302
+ cleaned[lower_k] = v
303
+
304
+ # Browser Fingerprint Spoofing zorunlu varsayılanlar
305
+ if "user-agent" not in cleaned:
306
+ cleaned["user-agent"] = DEFAULT_USER_AGENT
307
+ if "referer" not in cleaned and target_url:
308
+ cleaned["referer"] = target_url if target_url.endswith("/") else f"{target_url}/"
309
+ if "accept" not in cleaned:
310
+ cleaned["accept"] = "application/json"
311
+
312
+ return cleaned
313
+
314
+ @classmethod
315
+ def filter_relevant_endpoints(
316
+ cls,
317
+ endpoints: list[CapturedEndpoint],
318
+ target_url: str = "",
319
+ ) -> list[CapturedEndpoint]:
320
+ """
321
+ LLM promptuna gönderilecek uç noktaları süzer:
322
+ 1. 3. taraf reklam, piksel, telemetri ve analitik domainlerini (yandex, evergage, useinsider, openai, creativecdn, firebase vb.) eler.
323
+ 2. Eğer hedef URL belirtilmişse hedef root domain ile eşleşen ana veri uç noktalarını korur.
324
+ 3. Bot algılama sensör/challenge uç noktalarını (örn: /dRRXeAPrg/, /LSrVcDPgS/) veri API'si olmadığı için eler.
325
+ """
326
+ target_root = extract_root_domain(target_url) if target_url else ""
327
+ filtered: list[CapturedEndpoint] = []
328
+ for ep in endpoints:
329
+ dom = ep.domain.lower()
330
+ path = ep.path.lower()
331
+
332
+ # 1. 3. taraf reklam, piksel ve analitik filtrelemesi
333
+ if any(kw in dom for kw in THIRD_PARTY_IGNORE_KEYWORDS):
334
+ continue
335
+
336
+ # 2. Akamai / WAF sensör ve challenge veri yollarını filtrele
337
+ if any(s in path for s in ("/drrxeaprg/", "/lsrvcdpgs/image", "/_incapsula_resource", "/challenge")):
338
+ continue
339
+
340
+ # 3. Hedef domain eşleşmesi (sadece hedef servise ait uç noktalar)
341
+ if target_root:
342
+ ep_root = extract_root_domain(dom)
343
+ is_match = (
344
+ ep_root == target_root
345
+ or target_root in dom
346
+ or (target_root in ("algolia.com", "hackernews") and "algolia" in dom)
347
+ )
348
+ if not is_match:
349
+ continue
350
+
351
+ filtered.append(ep)
352
+
353
+ return filtered or endpoints
354
+
355
+ @classmethod
356
+ def deduplicate_endpoints(
357
+ cls,
358
+ endpoints: list[CapturedEndpoint],
359
+ target_url: str = "",
360
+ ) -> list[CapturedEndpoint]:
361
+ """
362
+ Aynı (method, domain, path) kombinasyonuna sahip tekrar eden istekleri tekilleştirir
363
+ ve her uç noktanın EN SON yakalanan örneğini (kullanıcının yazmayı bitirdiği son payload'u) korur.
364
+ """
365
+ dedup_map: dict[tuple[str, str, str], CapturedEndpoint] = {}
366
+ for ep in endpoints:
367
+ key = (ep.method.upper(), ep.domain.lower(), ep.path)
368
+ # Her yeni eşleşme öncekini ezer; böylece en son yakalanan örnek kalır.
369
+ cleaned_ep = ep.model_copy(deep=True)
370
+ ref_url = target_url or f"https://{ep.domain}"
371
+ cleaned_ep.headers = cls.sanitize_headers(cleaned_ep.headers, target_url=ref_url)
372
+ dedup_map[key] = cleaned_ep
373
+
374
+ return list(dedup_map.values())
375
+
376
+ def load_and_prepare_traffic(self) -> tuple[CaptureSession, list[CapturedEndpoint]]:
377
+ """captured_traffic.json dosyasını okur, 3. taraf domainleri filtreler ve tekilleştirilmiş uç noktaları döndürür."""
378
+ if not self.traffic_path.exists():
379
+ raise FileNotFoundError(
380
+ f"Trafik dosyası bulunamadı: {self.traffic_path}. Önce kayıt (record/build) yapmalısınız."
381
+ )
382
+
383
+ raw_content = self.traffic_path.read_text(encoding="utf-8")
384
+ session = CaptureSession.model_validate_json(raw_content)
385
+
386
+ if not session.endpoints:
387
+ raise ValueError(f"{self.traffic_path} içinde yakalanmış hiçbir uç nokta bulunmuyor.")
388
+
389
+ # 1. 3. taraf reklam, piksel, telemetri ve analitikleri filtrele (drop et)
390
+ relevant_endpoints = self.filter_relevant_endpoints(session.endpoints, target_url=session.target_url)
391
+
392
+ # 2. Tekilleştir ve son istek halini koru
393
+ deduped = self.deduplicate_endpoints(relevant_endpoints, target_url=session.target_url)
394
+ return session, deduped
395
+
396
+ def _build_user_prompt(self, goal: str, target_url: str, endpoints: list[CapturedEndpoint]) -> str:
397
+ """LLM'e gönderilecek kullanıcı istemini hazırlar."""
398
+ serialized_endpoints = [
399
+ {
400
+ "id": ep.id,
401
+ "method": ep.method,
402
+ "url": ep.url,
403
+ "domain": ep.domain,
404
+ "path": ep.path,
405
+ "status_code": ep.status_code,
406
+ "query_params": ep.query_params,
407
+ "headers": ep.headers,
408
+ "request_payload": ep.request_payload,
409
+ "response_sample": ep.response_sample[:1200],
410
+ }
411
+ for ep in endpoints
412
+ ]
413
+
414
+ return (
415
+ f"Hedef Web Sitesi: {target_url}\n"
416
+ f"Kullanıcının Hedefi (Goal): {goal}\n\n"
417
+ f"Tekilleştirilmiş Uç Nokta Listesi (Her uç noktanın EN SON yakalanan hali):\n"
418
+ f"{json.dumps(serialized_endpoints, ensure_ascii=False, indent=2)}\n\n"
419
+ "Lütfen hedefe en uygun uç noktayı seç ve `generated_client.py` ile `generated_mcp.py` "
420
+ "dosyalarını istenen JSON formatında üret."
421
+ )
422
+
423
+ def _call_anthropic(self, user_prompt: str) -> str:
424
+ """Anthropic Claude API çağrısını gerçekleştirir (model yedeklemeli)."""
425
+ from anthropic import Anthropic
426
+
427
+ client = Anthropic(api_key=self.api_key)
428
+ candidate_models = [
429
+ self.model,
430
+ "claude-sonnet-4-20250514",
431
+ "claude-3-7-sonnet-latest",
432
+ "claude-3-5-sonnet-latest",
433
+ "claude-3-5-haiku-latest",
434
+ ]
435
+ # Sırayı koruyarak tekilleştir
436
+ models_to_try = list(dict.fromkeys(candidate_models))
437
+
438
+ last_err: Exception | None = None
439
+ for model_name in models_to_try:
440
+ try:
441
+ response = client.messages.create(
442
+ model=model_name,
443
+ max_tokens=4096,
444
+ temperature=0.1,
445
+ system=SYSTEM_PROMPT,
446
+ messages=[{"role": "user", "content": user_prompt}],
447
+ )
448
+ self.model = model_name
449
+ text_blocks = [block.text for block in response.content if hasattr(block, "text")]
450
+ return "\n".join(text_blocks)
451
+ except Exception as exc:
452
+ last_err = exc
453
+ continue
454
+
455
+ raise RuntimeError(f"Anthropic API çağrısı başarısız oldu: {last_err}")
456
+
457
+ def _call_openai(self, user_prompt: str) -> str:
458
+ """OpenAI API çağrısını gerçekleştirir."""
459
+ from openai import OpenAI
460
+
461
+ base_url = os.getenv("OPENAI_BASE_URL")
462
+ client = OpenAI(api_key=self.api_key, base_url=base_url) if base_url else OpenAI(api_key=self.api_key)
463
+
464
+ response = client.chat.completions.create(
465
+ model=self.model,
466
+ temperature=0.1,
467
+ response_format={"type": "json_object"},
468
+ messages=[
469
+ {"role": "system", "content": SYSTEM_PROMPT},
470
+ {"role": "user", "content": user_prompt},
471
+ ],
472
+ )
473
+ return response.choices[0].message.content or "{}"
474
+
475
+ @staticmethod
476
+ def _extract_json_dict(raw_text: str) -> dict[str, Any]:
477
+ """LLM yanıtından JSON nesnesini güvenli biçimde ayıklar."""
478
+ cleaned = raw_text.strip()
479
+ # Eğer ```json ... ``` bloğu içindeyse ayıkla
480
+ fence_match = re.search(r"```(?:json)?\s*(\{.*\})\s*```", cleaned, flags=re.DOTALL)
481
+ if fence_match:
482
+ cleaned = fence_match.group(1).strip()
483
+ else:
484
+ start = cleaned.find("{")
485
+ end = cleaned.rfind("}")
486
+ if start != -1 and end != -1 and end > start:
487
+ cleaned = cleaned[start : end + 1]
488
+
489
+ return json.loads(cleaned)
490
+
491
+ @staticmethod
492
+ def _validate_python_syntax(code: str, filename: str) -> bool:
493
+ """Üretilen Python kodunun AST sözdizimi doğrulamasını yapar."""
494
+ try:
495
+ ast.parse(code, filename=filename)
496
+ return True
497
+ except SyntaxError:
498
+ return False
499
+
500
+ @staticmethod
501
+ def _build_fallback_codes(
502
+ endpoint: CapturedEndpoint,
503
+ function_name: str,
504
+ dynamic_params: list[DynamicParamSpec],
505
+ explanation: str,
506
+ ) -> tuple[str, str]:
507
+ """LLM'in ürettiği kodda sözdizimi hatası olursa devreye giren deterministik kod üretici."""
508
+ fn_name = re.sub(r"[^a-zA-Z0-9_]", "_", function_name or "call_shadow_api").lower()
509
+ if not fn_name or fn_name[0].isdigit():
510
+ fn_name = f"api_{fn_name}"
511
+
512
+ # Eğer LLM dinamik parametre döndürmediyse, payload veya query içinden otomatik tahmin et
513
+ if not dynamic_params:
514
+ if isinstance(endpoint.request_payload, dict) and "query" in endpoint.request_payload:
515
+ dynamic_params = [
516
+ DynamicParamSpec(
517
+ name="query",
518
+ original_key="query",
519
+ location="body",
520
+ param_type="str",
521
+ default_value=endpoint.request_payload.get("query", ""),
522
+ description="Arama sorgusu",
523
+ )
524
+ ]
525
+ elif "query" in endpoint.query_params or "q" in endpoint.query_params:
526
+ k = "query" if "query" in endpoint.query_params else "q"
527
+ dynamic_params = [
528
+ DynamicParamSpec(
529
+ name=k,
530
+ original_key=k,
531
+ location="query",
532
+ param_type="str",
533
+ default_value=endpoint.query_params[k],
534
+ description="Arama sorgusu",
535
+ )
536
+ ]
537
+
538
+ # Fonksiyon imzası oluştur
539
+ sig_parts: list[str] = []
540
+ for p in dynamic_params:
541
+ py_type = p.param_type if p.param_type in ("str", "int", "float", "bool") else "Any"
542
+ sig_parts.append(f"{p.name}: {py_type} = {repr(p.default_value)}")
543
+ sig_str = ", ".join(sig_parts)
544
+
545
+ # Eğer payload JSON ise ve content-type form-urlencoded ise httpx'in json= göndermesi için ayarla
546
+ headers_dict = ShadowAPICompiler.sanitize_headers(
547
+ endpoint.headers,
548
+ target_url=f"https://{endpoint.domain}",
549
+ )
550
+ if headers_dict.get("sec-fetch-dest") == "document":
551
+ headers_dict["sec-fetch-dest"] = "empty"
552
+ if headers_dict.get("sec-fetch-mode") == "navigate":
553
+ headers_dict["sec-fetch-mode"] = "cors"
554
+
555
+ cleaned_payload = copy.deepcopy(endpoint.request_payload)
556
+ if isinstance(cleaned_payload, dict):
557
+ for k in list(cleaned_payload.keys()):
558
+ v = cleaned_payload[k]
559
+ if v is None:
560
+ cleaned_payload.pop(k, None)
561
+ elif isinstance(v, dict) and all(sub_v is None for sub_v in v.values()):
562
+ cleaned_payload.pop(k, None)
563
+ elif k.lower() in ("operationcode", "personnelflightsearch", "expatpnr", "ffredemption") and v in (False, None, "TK"):
564
+ cleaned_payload.pop(k, None)
565
+
566
+ if isinstance(cleaned_payload, (dict, list)):
567
+ if headers_dict.get("content-type", "").lower() == "application/x-www-form-urlencoded":
568
+ headers_dict.pop("content-type", None)
569
+
570
+ body_override_pairs: list[str] = []
571
+ query_override_lines: list[str] = []
572
+ for p in dynamic_params:
573
+ if p.location == "body" and isinstance(cleaned_payload, (dict, list)):
574
+ body_override_pairs.append(f"{repr(p.original_key)}: {p.name}")
575
+ elif p.location == "query":
576
+ query_override_lines.append(f" params[{repr(p.original_key)}] = {p.name}")
577
+
578
+ is_flight_search = isinstance(cleaned_payload, dict) and "originDestinationInformationList" in cleaned_payload
579
+ if is_flight_search:
580
+ body_updates = f""" is_round_trip = bool(locals().get('return_date') and str(locals().get('return_date')).strip())
581
+ if isinstance(payload, dict) and "originDestinationInformationList" in payload:
582
+ od_list = payload["originDestinationInformationList"]
583
+ if is_round_trip:
584
+ payload["selectedBookerSearch"] = "R"
585
+ if len(od_list) == 1:
586
+ import copy
587
+ outbound = od_list[0]
588
+ inbound = copy.deepcopy(outbound)
589
+ inbound["originAirportCode"] = outbound.get("destinationAirportCode")
590
+ inbound["destinationAirportCode"] = outbound.get("originAirportCode")
591
+ if "departureDate" in inbound and locals().get("return_date"):
592
+ inbound["departureDate"] = locals().get("return_date")
593
+ payload["originDestinationInformationList"] = [outbound, inbound]
594
+ else:
595
+ payload["selectedBookerSearch"] = "O"
596
+ if len(od_list) > 1:
597
+ payload["originDestinationInformationList"] = [od_list[0]]
598
+ if {repr(bool(body_override_pairs))}:
599
+ payload = update_payload_deep(payload, {{{', '.join(body_override_pairs)}}})"""
600
+ else:
601
+ body_updates = (
602
+ f" payload = update_payload_deep(payload, {{{', '.join(body_override_pairs)}}})"
603
+ if body_override_pairs
604
+ else " # Payload parametresi güncellemesi yok"
605
+ )
606
+ query_updates = "\n".join(query_override_lines)
607
+
608
+ has_json_body = isinstance(cleaned_payload, (dict, list))
609
+ has_raw_body = cleaned_payload is not None and not has_json_body
610
+
611
+ request_call_args = [
612
+ f"method={repr(endpoint.method)}",
613
+ f"url={repr(endpoint.url)}",
614
+ "params=params",
615
+ "headers=headers",
616
+ ]
617
+ if has_json_body:
618
+ request_call_args.append("json=payload")
619
+ elif has_raw_body:
620
+ request_call_args.append("content=payload")
621
+
622
+ call_args_str = ", ".join(request_call_args)
623
+
624
+ client_code = f'''from __future__ import annotations
625
+
626
+ import json
627
+ import pathlib
628
+ import sys
629
+ import time
630
+ from typing import Any
631
+
632
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))
633
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
634
+ from healer import SessionHealer, update_payload_deep
635
+
636
+
637
+ def {fn_name}({sig_str}) -> dict[str, Any]:
638
+ """
639
+ {explanation}
640
+ Hedef Endpoint: {endpoint.method} {endpoint.url}
641
+ """
642
+ params: dict[str, Any] = {repr(endpoint.query_params)}
643
+ headers: dict[str, str] = {repr(headers_dict)}
644
+ payload: Any = {repr(cleaned_payload)}
645
+ {query_updates if query_updates else " # Query parametresi güncellemesi yok"}
646
+ {body_updates}
647
+
648
+ healer = SessionHealer()
649
+ # Dışarıdan doğrudan HTTP atmak yerine tarayıcı oturumu üzerinden
650
+ # tarayıcının kendi JavaScript ortamında isteği çalıştır (WAF / Akamai bypass)
651
+ return healer.execute_in_browser(
652
+ method={repr(endpoint.method)},
653
+ url={repr(endpoint.url)},
654
+ headers=headers,
655
+ params=params,
656
+ json_payload=payload if {repr(has_json_body)} else None,
657
+ )
658
+
659
+
660
+ if __name__ == "__main__":
661
+ start_ns = time.perf_counter()
662
+ data = {fn_name}()
663
+ elapsed_ms = (time.perf_counter() - start_ns) * 1000
664
+
665
+ print(f"[Shadow-API] Yanıt Süresi: {{elapsed_ms:.2f}} ms")
666
+ if isinstance(data, dict):
667
+ if "hits" in data and isinstance(data["hits"], list):
668
+ hits = data["hits"]
669
+ print(f"[Shadow-API] Toplam Sonuç (nbHits): {{data.get('nbHits', len(hits))}} | Dönen: {{len(hits)}}")
670
+ for idx, item in enumerate(hits[:5], start=1):
671
+ title = item.get("title") or item.get("story_title") or item.get("name") or str(item)[:80]
672
+ url = item.get("url") or item.get("story_url") or ""
673
+ print(f" {{idx}}. {{title}} {{f'({{url}})' if url else ''}}")
674
+ elif "pages" in data and isinstance(data["pages"], list):
675
+ pages = data["pages"]
676
+ print(f"[Shadow-API] Bulunan Sayfa Sayısı: {{len(pages)}}")
677
+ for idx, item in enumerate(pages[:5], start=1):
678
+ title = item.get("title") or item.get("key") or str(item)[:80]
679
+ desc = item.get("description") or ""
680
+ key = item.get("key") or ""
681
+ wiki_url = f"https://en.wikipedia.org/wiki/{{key}}" if key else ""
682
+ print(f" {{idx}}. {{title}} — {{desc}} {{f'({{wiki_url}})' if wiki_url else ''}}")
683
+ else:
684
+ print(f"[Shadow-API] Üst Düzey Anahtarlar: {{list(data.keys())}}")
685
+ preview = json.dumps(data, ensure_ascii=False, indent=2)[:600]
686
+ print(preview)
687
+ else:
688
+ print(str(data)[:600])
689
+ '''
690
+
691
+ mcp_code = f'''from __future__ import annotations
692
+
693
+ from typing import Any
694
+
695
+ from mcp.server.fastmcp import FastMCP
696
+
697
+ from generated_client import {fn_name} as _shadow_client_fn
698
+
699
+ mcp = FastMCP("Shadow-API MCP Server")
700
+
701
+
702
+ @mcp.tool()
703
+ def {fn_name}({sig_str}) -> dict[str, Any]:
704
+ """
705
+ {explanation}
706
+ """
707
+ return _shadow_client_fn({", ".join(f"{p.name}={p.name}" for p in dynamic_params)})
708
+
709
+
710
+ if __name__ == "__main__":
711
+ mcp.run()
712
+ '''
713
+ return client_code, mcp_code
714
+
715
+ def _save_to_registry(
716
+ self,
717
+ service_name: str,
718
+ target_url: str,
719
+ goal: str,
720
+ function_name: str,
721
+ explanation: str,
722
+ method: str,
723
+ endpoint_url: str,
724
+ dynamic_params: list[DynamicParamSpec],
725
+ client_code: str,
726
+ ) -> tuple[Path, Path]:
727
+ """Derlenen istemciyi ve meta veri şemasını registry/<servis_adi>/ altına kaydeder."""
728
+ service_dir = self.registry_root / service_name
729
+ service_dir.mkdir(parents=True, exist_ok=True)
730
+
731
+ reg_client_path = service_dir / "client.py"
732
+ reg_schema_path = service_dir / "schema.json"
733
+
734
+ reg_client_path.write_text(client_code + "\n", encoding="utf-8")
735
+
736
+ schema_obj = RegistrySchema(
737
+ service_name=service_name,
738
+ target_url=target_url,
739
+ goal=goal,
740
+ function_name=function_name,
741
+ explanation=explanation,
742
+ method=method,
743
+ endpoint_url=endpoint_url,
744
+ dynamic_params=dynamic_params,
745
+ created_at=datetime.now(timezone.utc).isoformat(),
746
+ )
747
+ reg_schema_path.write_text(
748
+ schema_obj.model_dump_json(indent=2) + "\n",
749
+ encoding="utf-8",
750
+ )
751
+ return reg_client_path, reg_schema_path
752
+
753
+ def compile(self, goal: str, service_name: str | None = None) -> CompilationResult:
754
+ """Trafiği analiz eder, LLM ile derler ve hem kök dosyaları hem de registry kaydını yazar."""
755
+ session, deduped_endpoints = self.load_and_prepare_traffic()
756
+ resolved_service = (
757
+ re.sub(r"[^a-z0-9_]+", "_", service_name.lower().strip()).strip("_")
758
+ if service_name
759
+ else derive_service_name(session.target_url)
760
+ )
761
+
762
+ console.print(
763
+ Panel.fit(
764
+ f"[bold cyan]Servis Adı:[/bold cyan] [bold yellow]{resolved_service}[/bold yellow]\n"
765
+ f"[bold cyan]Sağlayıcı:[/bold cyan] {self.provider.upper()} ([yellow]{self.model}[/yellow])\n"
766
+ f"[bold cyan]Hedef (Goal):[/bold cyan] {goal}\n"
767
+ f"[bold cyan]Filtreleme & Tekilleştirme:[/bold cyan] {len(session.endpoints)} toplam istek ➔ "
768
+ f"[bold green]{len(deduped_endpoints)} ilgili uç nokta (3. taraf reklam/analitikler elendi)[/bold green]",
769
+ title="🧠 Shadow-API Compiler",
770
+ border_style="magenta",
771
+ )
772
+ )
773
+
774
+ user_prompt = self._build_user_prompt(
775
+ goal=goal,
776
+ target_url=session.target_url,
777
+ endpoints=deduped_endpoints,
778
+ )
779
+
780
+ with console.status("[bold magenta]LLM uç noktaları analiz ediyor ve SDK/MCP kodlarını derliyor...[/bold magenta]"):
781
+ if self.provider == "anthropic":
782
+ raw_response = self._call_anthropic(user_prompt)
783
+ else:
784
+ raw_response = self._call_openai(user_prompt)
785
+
786
+ parsed = self._extract_json_dict(raw_response)
787
+
788
+ matched_id = int(parsed.get("matched_endpoint_id", deduped_endpoints[-1].id))
789
+ matched_ep = next((ep for ep in deduped_endpoints if ep.id == matched_id), deduped_endpoints[-1])
790
+
791
+ function_name = str(parsed.get("function_name", "call_shadow_api"))
792
+ explanation = str(parsed.get("explanation", "Hedef uç nokta başarıyla eşleştirildi."))
793
+ raw_params = parsed.get("dynamic_params", [])
794
+ dynamic_params = [DynamicParamSpec.model_validate(p) for p in raw_params if isinstance(p, dict)]
795
+
796
+ client_code = str(parsed.get("client_code", "")).strip()
797
+ mcp_code = str(parsed.get("mcp_code", "")).strip()
798
+
799
+ # Üretilen kodların geçerliliğini, Self-Healing tünelini ve Browser Fingerprint Spoofing entegrasyonunu denetle
800
+ lower_client = client_code.lower()
801
+ if (
802
+ not client_code
803
+ or not mcp_code
804
+ or "SessionHealer" not in client_code
805
+ or "retry_with_fresh_session" not in client_code
806
+ or "user-agent" not in lower_client
807
+ or "referer" not in lower_client
808
+ or not self._validate_python_syntax(client_code, "generated_client.py")
809
+ or not self._validate_python_syntax(mcp_code, "generated_mcp.py")
810
+ ):
811
+ console.print(
812
+ "[yellow]⚠ Deterministik Self-Healing & Taze Oturum Tüneli şablonu ile istemci kodu üretiliyor...[/yellow]"
813
+ )
814
+ client_code, mcp_code = self._build_fallback_codes(
815
+ endpoint=matched_ep,
816
+ function_name=function_name,
817
+ dynamic_params=dynamic_params,
818
+ explanation=explanation,
819
+ )
820
+
821
+ if "sys.path.insert" not in client_code and "from healer import" in client_code:
822
+ client_code = client_code.replace(
823
+ "from healer import",
824
+ "import sys, pathlib; sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))\nfrom healer import",
825
+ 1,
826
+ )
827
+
828
+ # Eşleşen uç noktanın güncel başlık ve tokenlarını session_tokens.json'a yaz
829
+ token_bundle = {
830
+ "target_url": matched_ep.headers.get("referer") or session.target_url,
831
+ "healed_at": datetime.now(timezone.utc).isoformat(),
832
+ "headers": matched_ep.headers,
833
+ "query_params": matched_ep.query_params,
834
+ "body_tokens": {},
835
+ **matched_ep.headers,
836
+ }
837
+ Path("session_tokens.json").write_text(
838
+ json.dumps(token_bundle, ensure_ascii=False, indent=2),
839
+ encoding="utf-8",
840
+ )
841
+
842
+ self.client_output_path.write_text(client_code + "\n", encoding="utf-8")
843
+ self.mcp_output_path.write_text(mcp_code + "\n", encoding="utf-8")
844
+
845
+ reg_client_path, reg_schema_path = self._save_to_registry(
846
+ service_name=resolved_service,
847
+ target_url=session.target_url,
848
+ goal=goal,
849
+ function_name=function_name,
850
+ explanation=explanation,
851
+ method=matched_ep.method,
852
+ endpoint_url=matched_ep.url,
853
+ dynamic_params=dynamic_params,
854
+ client_code=client_code,
855
+ )
856
+
857
+ return CompilationResult(
858
+ service_name=resolved_service,
859
+ provider=self.provider,
860
+ model=self.model,
861
+ matched_endpoint_id=matched_ep.id,
862
+ matched_method=matched_ep.method,
863
+ matched_url=matched_ep.url,
864
+ function_name=function_name,
865
+ explanation=explanation,
866
+ dynamic_params=dynamic_params,
867
+ client_code=client_code,
868
+ mcp_code=mcp_code,
869
+ client_path=self.client_output_path,
870
+ mcp_path=self.mcp_output_path,
871
+ registry_client_path=reg_client_path,
872
+ registry_schema_path=reg_schema_path,
873
+ )
874
+
875
+ def apply_fallback(self, result: CompilationResult) -> CompilationResult:
876
+ """Çalışma zamanı hatası durumunda deterministik şablonla dosyaları yeniden üretir."""
877
+ _, deduped_endpoints = self.load_and_prepare_traffic()
878
+ matched_ep = next(
879
+ (ep for ep in deduped_endpoints if ep.id == result.matched_endpoint_id),
880
+ deduped_endpoints[-1],
881
+ )
882
+ client_code, mcp_code = self._build_fallback_codes(
883
+ endpoint=matched_ep,
884
+ function_name=result.function_name,
885
+ dynamic_params=result.dynamic_params,
886
+ explanation=result.explanation,
887
+ )
888
+ self.client_output_path.write_text(client_code + "\n", encoding="utf-8")
889
+ self.mcp_output_path.write_text(mcp_code + "\n", encoding="utf-8")
890
+ if result.registry_client_path:
891
+ result.registry_client_path.write_text(client_code + "\n", encoding="utf-8")
892
+ result.client_code = client_code
893
+ result.mcp_code = mcp_code
894
+ return result
895
+
896
+
897
+ def ensure_default_registry(registry_root: Path = Path("registry")) -> None:
898
+ """
899
+ Başlangıçta mevcut generated_client.py varsa ve registry/hackernews/ henüz yoksa
900
+ otomatik olarak registry/hackernews/ altına kaydeder.
901
+ """
902
+ hn_dir = registry_root / "hackernews"
903
+ hn_client = hn_dir / "client.py"
904
+ hn_schema = hn_dir / "schema.json"
905
+ gen_client = Path("generated_client.py")
906
+
907
+ if gen_client.exists() and (not hn_client.exists() or not hn_schema.exists()):
908
+ hn_dir.mkdir(parents=True, exist_ok=True)
909
+ hn_client.write_text(gen_client.read_text(encoding="utf-8"), encoding="utf-8")
910
+ default_schema = RegistrySchema(
911
+ service_name="hackernews",
912
+ target_url="https://hn.algolia.com",
913
+ goal="Hacker News üzerinde makale araması yap",
914
+ function_name="search_hacker_news_articles",
915
+ explanation="Hacker News (Algolia) arama API uç noktası. Sorgu kelimesine göre makaleleri, puanlarını ve bağlantıları döndürür.",
916
+ method="POST",
917
+ endpoint_url="https://uj5wyc0l7x-dsn.algolia.net/1/indexes/Item_dev/query",
918
+ dynamic_params=[
919
+ DynamicParamSpec(
920
+ name="query",
921
+ original_key="query",
922
+ location="body",
923
+ param_type="str",
924
+ default_value="startup",
925
+ description="Aranacak makale başlığı, anahtar kelime veya konu",
926
+ ),
927
+ DynamicParamSpec(
928
+ name="page",
929
+ original_key="page",
930
+ location="body",
931
+ param_type="int",
932
+ default_value=0,
933
+ description="Getirilecek sayfa numarası (0'dan başlar)",
934
+ ),
935
+ DynamicParamSpec(
936
+ name="hits_per_page",
937
+ original_key="hitsPerPage",
938
+ location="body",
939
+ param_type="int",
940
+ default_value=30,
941
+ description="Sayfa başına döndürülecek maksimum makale sayısı",
942
+ ),
943
+ ],
944
+ created_at=datetime.now(timezone.utc).isoformat(),
945
+ )
946
+ hn_schema.write_text(default_schema.model_dump_json(indent=2) + "\n", encoding="utf-8")
947
+
948
+