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
@@ -0,0 +1,1044 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import re
6
+ from typing import Any
7
+ from urllib.parse import parse_qs, unquote_plus, urlparse
8
+
9
+ from rich.console import Console
10
+
11
+ console = Console(stderr=True, legacy_windows=False)
12
+
13
+ # Havalimanı kodu <-> Şehir eşleştirmeleri (Autocomplete listesinde hem IATA kodu hem şehir adı eşleşmesi için)
14
+ AIRPORT_CITY_ALIASES: dict[str, list[str]] = {
15
+ "IST": ["IST", "İstanbul", "Istanbul", "İstanbul Havalimanı"],
16
+ "SAW": ["SAW", "Sabiha Gökçen", "İstanbul", "Istanbul"],
17
+ "ESB": ["ESB", "Ankara", "Esenboğa", "Esenboga"],
18
+ "ADB": ["ADB", "İzmir", "Izmir", "Adnan Menderes"],
19
+ "AYT": ["AYT", "Antalya"],
20
+ "TZX": ["TZX", "Trabzon"],
21
+ "ADA": ["ADA", "Adana", "Şakirpaşa", "Çukurova"],
22
+ "DLM": ["DLM", "Dalaman", "Muğla"],
23
+ "BJV": ["BJV", "Bodrum", "Milas", "Muğla"],
24
+ "GZT": ["GZT", "Gaziantep"],
25
+ "ASR": ["ASR", "Kayseri"],
26
+ "VAN": ["VAN", "Van"],
27
+ "DIY": ["DIY", "Diyarbakır", "Diyarbakir"],
28
+ "ERZ": ["ERZ", "Erzurum"],
29
+ "SZF": ["SZF", "Samsun"],
30
+ "ECN": ["ECN", "Lefkoşa", "Ercan", "Kıbrıs"],
31
+ "LHR": ["LHR", "London", "Londra", "Heathrow"],
32
+ "CDG": ["CDG", "Paris", "Charles de Gaulle"],
33
+ "FRA": ["FRA", "Frankfurt"],
34
+ "AMS": ["AMS", "Amsterdam"],
35
+ "JFK": ["JFK", "New York"],
36
+ "250_0": ["İstanbul", "IST"],
37
+ "1912_170": ["Ankara", "ESB"],
38
+ }
39
+
40
+ # Semantik alan anahtar kelimeleri (Türkçe + İngilizce, normalize edilmiş küçük harf)
41
+ SEMANTIC_FIELD_KEYWORDS: dict[str, list[str]] = {
42
+ "origin": [
43
+ "nereden",
44
+ "kalkis",
45
+ "kalkis yeri",
46
+ "kalkis havalimani",
47
+ "departure port",
48
+ "departure airport",
49
+ "departure city",
50
+ "departureport",
51
+ "originairport",
52
+ "originport",
53
+ "origin",
54
+ "from airport",
55
+ "flying from",
56
+ "from",
57
+ "hareket",
58
+ ],
59
+ "destination": [
60
+ "nereye",
61
+ "varis",
62
+ "varis yeri",
63
+ "varis havalimani",
64
+ "arrival port",
65
+ "arrival airport",
66
+ "arrival city",
67
+ "arrivalport",
68
+ "destinationairport",
69
+ "destinationport",
70
+ "destination",
71
+ "to airport",
72
+ "flying to",
73
+ "to",
74
+ "inis",
75
+ ],
76
+ "departure_date": [
77
+ "gidis tarihi",
78
+ "gidis",
79
+ "kalkis tarihi",
80
+ "hareket tarihi",
81
+ "departure date",
82
+ "departuredate",
83
+ "depart date",
84
+ "outbound date",
85
+ "start date",
86
+ "depart",
87
+ "check-in",
88
+ "checkin",
89
+ ],
90
+ "return_date": [
91
+ "donus tarihi",
92
+ "donus",
93
+ "varis tarihi",
94
+ "return date",
95
+ "returndate",
96
+ "inbound date",
97
+ "end date",
98
+ "return",
99
+ "check-out",
100
+ "checkout",
101
+ ],
102
+ "query": [
103
+ "urun, kategori veya marka ara",
104
+ "arama yap",
105
+ "is ara",
106
+ "kelime veya is unvani",
107
+ "search jobs",
108
+ "keywords",
109
+ "search",
110
+ "ara",
111
+ ],
112
+ }
113
+
114
+ SEARCH_BUTTON_KEYWORDS: list[str] = [
115
+ "ucus ara",
116
+ "ucus bul",
117
+ "ucuslari listele",
118
+ "ucuslari goster",
119
+ "bilet bul",
120
+ "bilet ara",
121
+ "ucak bileti bul",
122
+ "seferleri goster",
123
+ "sefer ara",
124
+ "search flights",
125
+ "find flights",
126
+ "show flights",
127
+ "ara",
128
+ "bul",
129
+ "sorgula",
130
+ "listele",
131
+ "search",
132
+ ]
133
+
134
+ NEGATIVE_BUTTON_KEYWORDS: list[str] = [
135
+ "iptal",
136
+ "kapat",
137
+ "temizle",
138
+ "vazgec",
139
+ "geri",
140
+ "onceki",
141
+ "sonraki",
142
+ "cerez",
143
+ "cookie",
144
+ "reddet",
145
+ "kabul et",
146
+ "üye ol",
147
+ "uye ol",
148
+ "giris yap",
149
+ "login",
150
+ "sign in",
151
+ "sign up",
152
+ "close",
153
+ "cancel",
154
+ "clear",
155
+ "reset",
156
+ ]
157
+
158
+
159
+ def _normalize_tr(text: str) -> str:
160
+ """Türkçe karakterleri ASCII karşılıklarına dönüştürür ve küçük harfe çevirir."""
161
+ if not text:
162
+ return ""
163
+ tr_map = str.maketrans("ıİğĞüÜşŞöÖçÇ", "iigguussoocc")
164
+ cleaned = str(text).translate(tr_map).lower()
165
+ return re.sub(r"\s+", " ", cleaned).strip()
166
+
167
+
168
+ def _expand_aliases(value: str) -> list[str]:
169
+ """Havalimanı kodu veya şehir değeri için arama ve autocomplete eşleşme adaylarını döndürür."""
170
+ clean = str(value or "").strip()
171
+ if not clean:
172
+ return []
173
+ upper = clean.upper()
174
+ aliases = list(AIRPORT_CITY_ALIASES.get(upper, []))
175
+ if clean not in aliases:
176
+ aliases.insert(0, clean)
177
+ return aliases
178
+
179
+
180
+ def _format_date_variants(date_str: str) -> list[str]:
181
+ """
182
+ Farklı sitelerin bekleyebileceği tarih formatı varyasyonlarını üretir:
183
+ (orijinal, DD.MM.YYYY, DD/MM/YYYY, DD-MM-YYYY, YYYY-MM-DD).
184
+ """
185
+ clean = str(date_str or "").strip()
186
+ if not clean:
187
+ return []
188
+ variants: list[str] = [clean]
189
+
190
+ # YYYYMMDD (örn. 20270707)
191
+ if len(clean) == 8 and clean.isdigit():
192
+ yyyy, mm, dd = clean[:4], clean[4:6], clean[6:8]
193
+ for v in (f"{dd}.{mm}.{yyyy}", f"{dd}-{mm}-{yyyy}", f"{yyyy}-{mm}-{dd}", f"{dd}/{mm}/{yyyy}"):
194
+ if v not in variants:
195
+ variants.append(v)
196
+ return variants
197
+
198
+ for sep in ("-", ".", "/"):
199
+ if sep in clean:
200
+ parts = clean.split(sep)
201
+ if len(parts) == 3:
202
+ if len(parts[0]) == 4:
203
+ yyyy, mm, dd = parts[0], parts[1].zfill(2), parts[2].zfill(2)
204
+ else:
205
+ dd, mm, yyyy = parts[0].zfill(2), parts[1].zfill(2), parts[2]
206
+ for v in (
207
+ f"{dd}.{mm}.{yyyy}",
208
+ f"{dd}-{mm}-{yyyy}",
209
+ f"{yyyy}-{mm}-{dd}",
210
+ f"{dd}/{mm}/{yyyy}",
211
+ ):
212
+ if v not in variants:
213
+ variants.append(v)
214
+ break
215
+ return variants
216
+
217
+
218
+ def extract_semantic_form_params(
219
+ json_payload: Any = None,
220
+ params: dict[str, Any] | None = None,
221
+ url: str | None = None,
222
+ target_url: str | None = None,
223
+ explicit_form_params: dict[str, Any] | None = None,
224
+ ) -> dict[str, str]:
225
+ """
226
+ İstek gövdesi (`json_payload`), sorgu parametreleri (`params`), hedef URL (`url` / `target_url`)
227
+ veya doğrudan verilen sözlükten semantik form alanlarını (`origin`, `destination`,
228
+ `departure_date`, `return_date`, `query`, `location`) otonom olarak çıkarır.
229
+ """
230
+ extracted: dict[str, str] = {}
231
+
232
+ if isinstance(explicit_form_params, dict):
233
+ for k, v in explicit_form_params.items():
234
+ if v is not None and str(v).strip():
235
+ extracted[str(k).strip()] = str(v).strip()
236
+
237
+ def _scan_obj(obj: Any) -> None:
238
+ if isinstance(obj, str):
239
+ s = obj.strip()
240
+ if s.startswith("{") or s.startswith("["):
241
+ try:
242
+ _scan_obj(json.loads(s))
243
+ except Exception:
244
+ pass
245
+ return
246
+
247
+ if isinstance(obj, list):
248
+ # Çoklu uçuş segmenti listesi (örn. originDestinationInformationList veya flightSearchList)
249
+ if len(obj) >= 1 and isinstance(obj[0], dict):
250
+ seg0 = obj[0]
251
+ for ok in ("originAirportCode", "origin_airport_code", "departurePort", "departure_port", "origin", "from"):
252
+ if seg0.get(ok) and not extracted.get("origin"):
253
+ extracted["origin"] = str(seg0[ok]).strip()
254
+ for dk in ("destinationAirportCode", "destination_airport_code", "arrivalPort", "arrival_port", "destination", "to"):
255
+ if seg0.get(dk) and not extracted.get("destination"):
256
+ extracted["destination"] = str(seg0[dk]).strip()
257
+ for dep_k in ("departureDate", "departure_date", "date", "depDate"):
258
+ if seg0.get(dep_k) and not extracted.get("departure_date"):
259
+ extracted["departure_date"] = str(seg0[dep_k]).strip()
260
+ for ret_k in ("returnDate", "return_date", "retDate"):
261
+ if seg0.get(ret_k) and not extracted.get("return_date"):
262
+ extracted["return_date"] = str(seg0[ret_k]).strip()
263
+
264
+ if len(obj) >= 2 and isinstance(obj[1], dict) and not extracted.get("return_date"):
265
+ seg1 = obj[1]
266
+ for dep_k in ("departureDate", "departure_date", "returnDate", "return_date", "date"):
267
+ if seg1.get(dep_k):
268
+ extracted["return_date"] = str(seg1[dep_k]).strip()
269
+ break
270
+
271
+ for item in obj:
272
+ _scan_obj(item)
273
+ return
274
+
275
+ if isinstance(obj, dict):
276
+ # Önce iç listeleri kontrol et
277
+ for list_key in ("originDestinationInformationList", "flightSearchList", "segments", "legs", "itineraries"):
278
+ if isinstance(obj.get(list_key), list):
279
+ _scan_obj(obj[list_key])
280
+
281
+ for k, v in obj.items():
282
+ if v is None or isinstance(v, (dict, list, bool)):
283
+ if isinstance(v, (dict, list)):
284
+ _scan_obj(v)
285
+ continue
286
+ val_str = str(v).strip()
287
+ if not val_str:
288
+ continue
289
+ kl = re.sub(r"[_\-\s]", "", str(k).lower())
290
+
291
+ if not extracted.get("origin") and kl in (
292
+ "origin",
293
+ "originairportcode",
294
+ "originairport",
295
+ "origincitycode",
296
+ "originid",
297
+ "departureport",
298
+ "departureairport",
299
+ "from",
300
+ "fromport",
301
+ "fromairport",
302
+ "kalkis",
303
+ "nereden",
304
+ ):
305
+ extracted["origin"] = val_str
306
+ elif not extracted.get("destination") and kl in (
307
+ "destination",
308
+ "destinationairportcode",
309
+ "destinationairport",
310
+ "destinationcitycode",
311
+ "destinationid",
312
+ "arrivalport",
313
+ "arrivalairport",
314
+ "to",
315
+ "toport",
316
+ "toairport",
317
+ "varis",
318
+ "nereye",
319
+ ):
320
+ extracted["destination"] = val_str
321
+ elif not extracted.get("departure_date") and kl in (
322
+ "departuredate",
323
+ "depdate",
324
+ "gidistarihi",
325
+ "outbounddate",
326
+ "startdate",
327
+ ):
328
+ extracted["departure_date"] = val_str
329
+ elif not extracted.get("return_date") and kl in (
330
+ "returndate",
331
+ "retdate",
332
+ "donustarihi",
333
+ "inbounddate",
334
+ "enddate",
335
+ ):
336
+ extracted["return_date"] = val_str
337
+ elif not extracted.get("query") and kl in (
338
+ "keywords",
339
+ "query",
340
+ "q",
341
+ "search",
342
+ "keyword",
343
+ ):
344
+ extracted["query"] = val_str
345
+ elif not extracted.get("location") and kl in ("location", "city"):
346
+ extracted["location"] = val_str
347
+
348
+ _scan_obj(json_payload)
349
+ _scan_obj(params)
350
+
351
+ # URL query parametrelerinden çıkarım
352
+ for u in (target_url, url):
353
+ if not u:
354
+ continue
355
+ try:
356
+ parsed = urlparse(str(u))
357
+ qs = parse_qs(parsed.query)
358
+ flat_qs = {k: unquote_plus(str(v[0])).strip() for k, v in qs.items() if v}
359
+ _scan_obj(flat_qs)
360
+ except Exception:
361
+ pass
362
+
363
+ return extracted
364
+
365
+
366
+ class GenericFormAutomator:
367
+ """
368
+ Tüm konektörlerde (`registry/*`) ve `SessionHealer` (`healer.py`) içinde ortak çalışan
369
+ Genel Amaçlı Akıllı Form Otomasyonu (Generic Form Automation) Katmanı:
370
+ 1. Sayfa yüklendiğinde havalimanı/şehir (`origin` / `destination`), tarih (`departure` / `return`)
371
+ veya arama (`query`) input'larını `placeholder`, `name`, `id`, `aria-label`, `data-testid` ve `<label>`
372
+ etiketlerinden semantik olarak bulup otonom doldurur.
373
+ 2. Autocomplete (şehir/havalimanı öneri listesi) açılırsa ilk eşleşen sonuca otonom tıklar.
374
+ 3. Formun arama / uçuş bul butonunu otomatik tespit edip tıklayarak SPA state'ini (frontend oturumunu)
375
+ tam anlamıyla oluşturur.
376
+ """
377
+
378
+ def __init__(self, settle_wait_sec: float = 2.5) -> None:
379
+ self.settle_wait_sec = settle_wait_sec
380
+
381
+ async def automate_page_form(
382
+ self,
383
+ page: Any,
384
+ form_params: dict[str, str] | None = None,
385
+ json_payload: Any = None,
386
+ params: dict[str, Any] | None = None,
387
+ url: str | None = None,
388
+ target_url: str | None = None,
389
+ submit_form: bool = True,
390
+ ) -> dict[str, Any]:
391
+ """
392
+ Verilen Playwright `page` üzerinde semantik form otomasyonunu yürütür ve
393
+ yapılan işlemlerin özet raporunu döndürür.
394
+ """
395
+ resolved = extract_semantic_form_params(
396
+ json_payload=json_payload,
397
+ params=params,
398
+ url=url,
399
+ target_url=target_url,
400
+ explicit_form_params=form_params,
401
+ )
402
+
403
+ report: dict[str, Any] = {
404
+ "executed": False,
405
+ "detected_params": resolved,
406
+ "filled_fields": {},
407
+ "autocomplete_selected": {},
408
+ "submitted": False,
409
+ "submit_button": None,
410
+ }
411
+
412
+ if not resolved:
413
+ return report
414
+
415
+ has_flight_fields = bool(
416
+ resolved.get("origin")
417
+ or resolved.get("destination")
418
+ or resolved.get("departure_date")
419
+ or resolved.get("return_date")
420
+ )
421
+ has_search_query = bool(resolved.get("query"))
422
+
423
+ if not has_flight_fields and not has_search_query:
424
+ return report
425
+
426
+ try:
427
+ # Çerez / bildirim banner'ı formu kapatıyorsa sessizce kapatmayı dene
428
+ await self._dismiss_blocking_banners(page)
429
+
430
+ # 1. Origin (Nereden / Kalkış Havalimanı veya Şehir)
431
+ if resolved.get("origin"):
432
+ orig_val = resolved["origin"]
433
+ orig_ok, orig_ac = await self._fill_location_field(
434
+ page=page,
435
+ role_key="origin",
436
+ value=orig_val,
437
+ )
438
+ if orig_ok:
439
+ report["executed"] = True
440
+ report["filled_fields"]["origin"] = orig_val
441
+ report["autocomplete_selected"]["origin"] = orig_ac
442
+
443
+ # 2. Destination (Nereye / Varış Havalimanı veya Şehir)
444
+ if resolved.get("destination"):
445
+ dest_val = resolved["destination"]
446
+ dest_ok, dest_ac = await self._fill_location_field(
447
+ page=page,
448
+ role_key="destination",
449
+ value=dest_val,
450
+ )
451
+ if dest_ok:
452
+ report["executed"] = True
453
+ report["filled_fields"]["destination"] = dest_val
454
+ report["autocomplete_selected"]["destination"] = dest_ac
455
+
456
+ # 3. Departure Date (Gidiş Tarihi)
457
+ if resolved.get("departure_date"):
458
+ dep_val = resolved["departure_date"]
459
+ dep_ok = await self._fill_date_field(
460
+ page=page,
461
+ role_key="departure_date",
462
+ date_str=dep_val,
463
+ )
464
+ if dep_ok:
465
+ report["executed"] = True
466
+ report["filled_fields"]["departure_date"] = dep_val
467
+
468
+ # 4. Return Date (Dönüş Tarihi)
469
+ if resolved.get("return_date"):
470
+ ret_val = resolved["return_date"]
471
+ ret_ok = await self._fill_date_field(
472
+ page=page,
473
+ role_key="return_date",
474
+ date_str=ret_val,
475
+ )
476
+ if ret_ok:
477
+ report["executed"] = True
478
+ report["filled_fields"]["return_date"] = ret_val
479
+
480
+ # 5. Genel Arama Input'u (Eğer origin/destination yoksa ama query varsa)
481
+ if not has_flight_fields and has_search_query:
482
+ q_val = resolved["query"]
483
+ q_ok, q_ac = await self._fill_location_field(
484
+ page=page,
485
+ role_key="query",
486
+ value=q_val,
487
+ )
488
+ if q_ok:
489
+ report["executed"] = True
490
+ report["filled_fields"]["query"] = q_val
491
+ report["autocomplete_selected"]["query"] = q_ac
492
+
493
+ # 6. Arama / Uçuş Bul Butonunu Otomatik Tespit Et ve Tıkla
494
+ if submit_form and report["filled_fields"]:
495
+ btn_info = await self._click_search_submit_button(page)
496
+ if btn_info.get("clicked"):
497
+ report["submitted"] = True
498
+ report["submit_button"] = btn_info.get("label")
499
+ console.print(
500
+ f"[bold green]🤖 [Generic Form Automation] Form dolduruldu "
501
+ f"({report['filled_fields']}) ve '{btn_info.get('label')}' butonuna tıklandı![/bold green]"
502
+ )
503
+ await asyncio.sleep(self.settle_wait_sec)
504
+ else:
505
+ console.print(
506
+ f"[cyan]🤖 [Generic Form Automation] Alanlar dolduruldu: {report['filled_fields']}[/cyan]"
507
+ )
508
+ await asyncio.sleep(1.0)
509
+ except Exception as exc:
510
+ console.print(f"[dim]Generic Form Automation bildirimi: {exc}[/dim]")
511
+
512
+ return report
513
+
514
+ async def _dismiss_blocking_banners(self, page: Any) -> None:
515
+ """Form elemanlarının tıklanmasını engelleyebilecek çerez/modal pencerelerini sessizce kapatır."""
516
+ try:
517
+ await page.evaluate(
518
+ """() => {
519
+ const selectors = [
520
+ '#onetrust-accept-btn-handler',
521
+ '[id*="cookie" i] button[id*="accept" i]',
522
+ '[class*="cookie" i] button[class*="accept" i]',
523
+ 'button[data-testid*="cookie-accept" i]'
524
+ ];
525
+ for (const sel of selectors) {
526
+ const el = document.querySelector(sel);
527
+ if (el && el.offsetParent !== null) {
528
+ el.click();
529
+ break;
530
+ }
531
+ }
532
+ }"""
533
+ )
534
+ except Exception:
535
+ pass
536
+
537
+ async def _locate_semantic_element(self, page: Any, role_key: str) -> str | None:
538
+ """
539
+ Sayfadaki görünür input/combobox elemanlarını `placeholder`, `name`, `id`, `aria-label`,
540
+ `data-testid` ve `<label>` metinlerine göre puanlayarak en uygun elemana geçici bir
541
+ `data-ownapi-field="<role_key>"` özniteliği atar ve CSS seçicisini döndürür.
542
+ """
543
+ keywords = SEMANTIC_FIELD_KEYWORDS.get(role_key, [])
544
+ if not keywords:
545
+ return None
546
+
547
+ attr_val = f"ownapi-{role_key}"
548
+ found = await page.evaluate(
549
+ """({ roleKey, keywords, attrVal }) => {
550
+ const norm = (s) => (s || '')
551
+ .toString()
552
+ .replace(/[ıİ]/g, 'i')
553
+ .replace(/[ğĞ]/g, 'g')
554
+ .replace(/[üÜ]/g, 'u')
555
+ .replace(/[şŞ]/g, 's')
556
+ .replace(/[öÖ]/g, 'o')
557
+ .replace(/[çÇ]/g, 'c')
558
+ .toLowerCase()
559
+ .replace(/\\s+/g, ' ')
560
+ .trim();
561
+
562
+ const isVisible = (el) => {
563
+ if (!el) return false;
564
+ const style = window.getComputedStyle(el);
565
+ if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) {
566
+ return false;
567
+ }
568
+ const r = el.getBoundingClientRect();
569
+ return r.width > 8 && r.height > 8;
570
+ };
571
+
572
+ // Önceki işareti temizle
573
+ document.querySelectorAll(`[data-ownapi-target="${attrVal}"]`).forEach(
574
+ el => el.removeAttribute('data-ownapi-target')
575
+ );
576
+
577
+ const candidates = Array.from(document.querySelectorAll(
578
+ 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]), ' +
579
+ '[role="combobox"], [role="searchbox"], [contenteditable="true"], ' +
580
+ 'button[aria-haspopup="listbox"], button[aria-haspopup="dialog"], ' +
581
+ '[data-testid*="origin" i], [data-testid*="destination" i], [data-testid*="departure" i], [data-testid*="return" i], ' +
582
+ '[class*="port-input" i], [class*="airport-input" i]'
583
+ ));
584
+
585
+ let bestEl = null;
586
+ let bestScore = 0;
587
+
588
+ for (const el of candidates) {
589
+ if (!isVisible(el)) continue;
590
+ // Zaten başka bir role atanmışsa atla
591
+ const existingRole = el.getAttribute('data-ownapi-assigned');
592
+ if (existingRole && existingRole !== roleKey) continue;
593
+
594
+ const placeholder = norm(el.getAttribute('placeholder'));
595
+ const name = norm(el.getAttribute('name'));
596
+ const id = norm(el.getAttribute('id'));
597
+ const ariaLabel = norm(el.getAttribute('aria-label'));
598
+ const testId = norm(el.getAttribute('data-testid') || el.getAttribute('data-cy') || el.getAttribute('data-qa'));
599
+ const title = norm(el.getAttribute('title'));
600
+
601
+ let labelText = '';
602
+ if (el.id) {
603
+ const lbl = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
604
+ if (lbl) labelText += ' ' + norm(lbl.innerText || lbl.textContent);
605
+ }
606
+ const parentLabel = el.closest('label');
607
+ if (parentLabel) {
608
+ labelText += ' ' + norm(parentLabel.innerText || parentLabel.textContent);
609
+ }
610
+ const fieldWrap = el.closest('[class*="field" i], [class*="input" i], [class*="port" i], [class*="date" i], [class*="search" i]');
611
+ if (fieldWrap) {
612
+ const subLbl = fieldWrap.querySelector('label, [class*="label" i], [class*="title" i]');
613
+ if (subLbl) labelText += ' ' + norm(subLbl.innerText || subLbl.textContent);
614
+ }
615
+
616
+ const primaryBlob = `${placeholder} ${ariaLabel} ${name} ${id} ${testId} ${title}`;
617
+ const fullBlob = `${primaryBlob} ${labelText}`;
618
+
619
+ let score = 0;
620
+ for (let i = 0; i < keywords.length; i++) {
621
+ const kw = keywords[i];
622
+ const weight = Math.max(10, 40 - i * 2);
623
+ if (placeholder === kw || ariaLabel === kw || name === kw || id === kw) {
624
+ score += weight + 35;
625
+ } else if (primaryBlob.includes(kw)) {
626
+ score += weight + 15;
627
+ } else if (fullBlob.includes(kw)) {
628
+ score += weight;
629
+ }
630
+ }
631
+
632
+ // Input etiketine ek bonus
633
+ if (el.tagName.toLowerCase() === 'input') {
634
+ score += 8;
635
+ }
636
+
637
+ // Karşıt rol kelimeleri geçiyorsa puan kır (örn. origin ararken nereye/destination geçmesi)
638
+ if (roleKey === 'origin' && (primaryBlob.includes('nereye') || primaryBlob.includes('destination') || primaryBlob.includes('arrival') || primaryBlob.includes('varis'))) {
639
+ score -= 60;
640
+ }
641
+ if (roleKey === 'destination' && (primaryBlob.includes('nereden') || primaryBlob.includes('origin') || primaryBlob.includes('departure') || primaryBlob.includes('kalkis'))) {
642
+ score -= 60;
643
+ }
644
+ if (roleKey === 'departure_date' && (primaryBlob.includes('donus') || primaryBlob.includes('return'))) {
645
+ score -= 60;
646
+ }
647
+ if (roleKey === 'return_date' && (primaryBlob.includes('gidis') || primaryBlob.includes('departure') || primaryBlob.includes('kalkis'))) {
648
+ score -= 60;
649
+ }
650
+
651
+ if (score > bestScore) {
652
+ bestScore = score;
653
+ bestEl = el;
654
+ }
655
+ }
656
+
657
+ if (bestEl && bestScore >= 12) {
658
+ bestEl.setAttribute('data-ownapi-target', attrVal);
659
+ bestEl.setAttribute('data-ownapi-assigned', roleKey);
660
+ return `[data-ownapi-target="${attrVal}"]`;
661
+ }
662
+ return null;
663
+ }""",
664
+ {"roleKey": role_key, "keywords": keywords, "attrVal": attr_val},
665
+ )
666
+ return found
667
+
668
+ async def _fill_location_field(
669
+ self,
670
+ page: Any,
671
+ role_key: str,
672
+ value: str,
673
+ ) -> tuple[bool, bool]:
674
+ """
675
+ Havalimanı/şehir (`origin` / `destination`) veya arama (`query`) input'unu bulur,
676
+ değeri yazar ve Autocomplete listesi açılırsa ilk eşleşen sonuca tıklar.
677
+ Dönüş: `(filled_ok, autocomplete_clicked)`
678
+ """
679
+ selector = await self._locate_semantic_element(page, role_key)
680
+ if not selector:
681
+ return False, False
682
+
683
+ aliases = _expand_aliases(value)
684
+ type_value = aliases[0] if aliases else str(value).strip()
685
+ norm_aliases = [_normalize_tr(a) for a in aliases if a]
686
+
687
+ try:
688
+ loc = page.locator(selector).first
689
+ await loc.click(timeout=3000)
690
+ await asyncio.sleep(0.2)
691
+
692
+ # Eğer tıklanan öğe bir combobox/button ise ve tıklama sonrası aktif bir <input> açıldıysa onu yakala
693
+ active_input_sel = await page.evaluate(
694
+ """(origSel) => {
695
+ document.querySelectorAll('[data-ownapi-active-input]').forEach(
696
+ el => el.removeAttribute('data-ownapi-active-input')
697
+ );
698
+ const orig = document.querySelector(origSel);
699
+ if (orig && orig.tagName && orig.tagName.toLowerCase() === 'input') {
700
+ return origSel;
701
+ }
702
+ const active = document.activeElement;
703
+ if (active && active.tagName && active.tagName.toLowerCase() === 'input') {
704
+ active.setAttribute('data-ownapi-active-input', '1');
705
+ return '[data-ownapi-active-input="1"]';
706
+ }
707
+ if (orig) {
708
+ const innerInput = orig.querySelector('input') || document.querySelector('input:focus, [class*="modal" i] input, [class*="dropdown" i] input, [class*="popup" i] input, [class*="search" i] input');
709
+ if (innerInput) {
710
+ innerInput.setAttribute('data-ownapi-active-input', '1');
711
+ return '[data-ownapi-active-input="1"]';
712
+ }
713
+ }
714
+ return origSel;
715
+ }""",
716
+ selector,
717
+ )
718
+
719
+ target_loc = page.locator(active_input_sel).first
720
+ try:
721
+ await target_loc.fill("", timeout=2000)
722
+ except Exception:
723
+ await page.keyboard.press("Control+A")
724
+ await page.keyboard.press("Backspace")
725
+
726
+ await target_loc.type(type_value, delay=45)
727
+ await asyncio.sleep(0.45)
728
+
729
+ # Autocomplete listesi açıldıysa ilk eşleşen sonuca tıkla
730
+ ac_clicked = await self._select_first_autocomplete_match(
731
+ page=page,
732
+ norm_aliases=norm_aliases,
733
+ input_selector=active_input_sel,
734
+ )
735
+ return True, ac_clicked
736
+ except Exception:
737
+ return False, False
738
+
739
+ async def _select_first_autocomplete_match(
740
+ self,
741
+ page: Any,
742
+ norm_aliases: list[str],
743
+ input_selector: str,
744
+ ) -> bool:
745
+ """
746
+ Açılan şehir/havalimanı Autocomplete listesinden (`[role="option"]`, `listbox`, `.autocomplete-item` vb.)
747
+ ilk eşleşen görünür öğeyi bulup tıklar.
748
+ """
749
+ try:
750
+ clicked = await page.evaluate(
751
+ """({ aliases, inputSel }) => {
752
+ const norm = (s) => (s || '')
753
+ .toString()
754
+ .replace(/[ıİ]/g, 'i')
755
+ .replace(/[ğĞ]/g, 'g')
756
+ .replace(/[üÜ]/g, 'u')
757
+ .replace(/[şŞ]/g, 's')
758
+ .replace(/[öÖ]/g, 'o')
759
+ .replace(/[çÇ]/g, 'c')
760
+ .toLowerCase()
761
+ .replace(/\\s+/g, ' ')
762
+ .trim();
763
+
764
+ const isVisible = (el) => {
765
+ if (!el) return false;
766
+ const style = window.getComputedStyle(el);
767
+ if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) {
768
+ return false;
769
+ }
770
+ const r = el.getBoundingClientRect();
771
+ return r.width > 10 && r.height > 10;
772
+ };
773
+
774
+ const inputEl = document.querySelector(inputSel);
775
+ const optionSelectors = [
776
+ '[role="option"]',
777
+ '[role="listbox"] li',
778
+ '[role="listbox"] [role="button"]',
779
+ '[class*="autocomplete" i] li',
780
+ '[class*="autocomplete" i] [class*="item" i]',
781
+ '[class*="suggestion" i] li',
782
+ '[class*="suggestion" i] [class*="item" i]',
783
+ '[class*="port-list" i] li',
784
+ '[class*="airport-list" i] li',
785
+ '[class*="dropdown-menu" i] li',
786
+ '[class*="search-result" i] li',
787
+ '[data-testid*="option" i]',
788
+ '[data-testid*="suggestion" i]',
789
+ '[data-testid*="airport" i]',
790
+ '.ui-menu-item'
791
+ ];
792
+
793
+ const rawOptions = Array.from(document.querySelectorAll(optionSelectors.join(', ')));
794
+ const visibleOptions = rawOptions.filter(el => {
795
+ if (!isVisible(el)) return false;
796
+ if (inputEl && (el === inputEl || el.contains(inputEl))) return false;
797
+ const txt = norm(el.innerText || el.textContent);
798
+ return txt.length >= 2 && txt.length <= 250;
799
+ });
800
+
801
+ if (visibleOptions.length === 0) {
802
+ return false;
803
+ }
804
+
805
+ // 1. Önce kelime sınırı (word-boundary) ile tam kod veya şehir eşleşen ilk seçeneği ara
806
+ for (const alias of aliases) {
807
+ if (!alias) continue;
808
+ const escaped = alias.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
809
+ const wordRe = new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, 'i');
810
+ for (const opt of visibleOptions) {
811
+ const txt = norm(opt.innerText || opt.textContent);
812
+ if (wordRe.test(txt)) {
813
+ opt.click();
814
+ return true;
815
+ }
816
+ }
817
+ }
818
+
819
+ // 2. Alt dizge (substring) eşleşmesi ara
820
+ for (const alias of aliases) {
821
+ if (!alias) continue;
822
+ for (const opt of visibleOptions) {
823
+ const txt = norm(opt.innerText || opt.textContent);
824
+ if (txt.includes(alias)) {
825
+ opt.click();
826
+ return true;
827
+ }
828
+ }
829
+ }
830
+
831
+ // 3. Eşleşme yoksa açılan listedeki ilk geçerli öneriye tıkla
832
+ visibleOptions[0].click();
833
+ return true;
834
+ }""",
835
+ {"aliases": norm_aliases, "inputSel": input_selector},
836
+ )
837
+ if clicked:
838
+ await asyncio.sleep(0.25)
839
+ return True
840
+
841
+ # DOM üzerinden bulunamadıysa klavye ile ilk öneriyi seçmeyi dene
842
+ await page.keyboard.press("ArrowDown")
843
+ await asyncio.sleep(0.1)
844
+ await page.keyboard.press("Enter")
845
+ await asyncio.sleep(0.2)
846
+ return True
847
+ except Exception:
848
+ return False
849
+
850
+ async def _fill_date_field(
851
+ self,
852
+ page: Any,
853
+ role_key: str,
854
+ date_str: str,
855
+ ) -> bool:
856
+ """
857
+ Gidiş (`departure_date`) veya dönüş (`return_date`) tarih alanını semantik olarak bulur,
858
+ hem input değerini (`input`/`change` event'leriyle) ayarlar hem de açık takvim hücresi varsa seçer.
859
+ """
860
+ selector = await self._locate_semantic_element(page, role_key)
861
+ if not selector:
862
+ return False
863
+
864
+ variants = _format_date_variants(date_str)
865
+ if not variants:
866
+ return False
867
+
868
+ primary_val = variants[0]
869
+ iso_val = next((v for v in variants if len(v) == 10 and v[4] == "-"), primary_val)
870
+
871
+ try:
872
+ filled = await page.evaluate(
873
+ """({ sel, primaryVal, isoVal, allVariants }) => {
874
+ const el = document.querySelector(sel);
875
+ if (!el) return false;
876
+ el.click();
877
+ el.focus();
878
+
879
+ const targetInput = (el.tagName && el.tagName.toLowerCase() === 'input')
880
+ ? el
881
+ : (el.querySelector('input') || el);
882
+
883
+ if (targetInput && targetInput.tagName && targetInput.tagName.toLowerCase() === 'input') {
884
+ const inType = (targetInput.getAttribute('type') || 'text').toLowerCase();
885
+ const valToSet = inType === 'date' ? isoVal : primaryVal;
886
+
887
+ try {
888
+ targetInput.removeAttribute('readonly');
889
+ } catch (_) {}
890
+
891
+ const nativeSetter = Object.getOwnPropertyDescriptor(
892
+ window.HTMLInputElement.prototype,
893
+ 'value'
894
+ )?.set;
895
+ if (nativeSetter) {
896
+ nativeSetter.call(targetInput, valToSet);
897
+ } else {
898
+ targetInput.value = valToSet;
899
+ }
900
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
901
+ targetInput.dispatchEvent(new Event('change', { bubbles: true }));
902
+ targetInput.dispatchEvent(new Event('blur', { bubbles: true }));
903
+ }
904
+
905
+ // Takvim popover açıksa eşleşen gün hücresine tıkla
906
+ for (const v of allVariants) {
907
+ const cell = document.querySelector(
908
+ `[data-date="${v}"], [data-day="${v}"], [aria-label*="${v}"], td[title*="${v}"]`
909
+ );
910
+ if (cell && cell.offsetParent !== null) {
911
+ cell.click();
912
+ break;
913
+ }
914
+ }
915
+ return true;
916
+ }""",
917
+ {
918
+ "sel": selector,
919
+ "primaryVal": primary_val,
920
+ "isoVal": iso_val,
921
+ "allVariants": variants,
922
+ },
923
+ )
924
+ await asyncio.sleep(0.25)
925
+ return bool(filled)
926
+ except Exception:
927
+ return False
928
+
929
+ async def _click_search_submit_button(self, page: Any) -> dict[str, Any]:
930
+ """
931
+ Formun arama / uçuş bul butonunu (`Uçuş Ara`, `Uçuş Bul`, `Bilet Bul`, `Ara`, `Search` vb.)
932
+ otomatik tespit edip tıklayarak SPA state'ini başlatır.
933
+ """
934
+ try:
935
+ result = await page.evaluate(
936
+ """({ posKeywords, negKeywords }) => {
937
+ const norm = (s) => (s || '')
938
+ .toString()
939
+ .replace(/[ıİ]/g, 'i')
940
+ .replace(/[ğĞ]/g, 'g')
941
+ .replace(/[üÜ]/g, 'u')
942
+ .replace(/[şŞ]/g, 's')
943
+ .replace(/[öÖ]/g, 'o')
944
+ .replace(/[çÇ]/g, 'c')
945
+ .toLowerCase()
946
+ .replace(/\\s+/g, ' ')
947
+ .trim();
948
+
949
+ const isVisible = (el) => {
950
+ if (!el || el.disabled) return false;
951
+ const style = window.getComputedStyle(el);
952
+ if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) {
953
+ return false;
954
+ }
955
+ const r = el.getBoundingClientRect();
956
+ return r.width > 16 && r.height > 16;
957
+ };
958
+
959
+ const candidates = Array.from(document.querySelectorAll(
960
+ 'button, input[type="submit"], input[type="button"], [role="button"], a[class*="btn" i], a[class*="button" i], a[class*="search" i]'
961
+ ));
962
+
963
+ let bestBtn = null;
964
+ let bestScore = 0;
965
+ let bestLabel = '';
966
+
967
+ for (const btn of candidates) {
968
+ if (!isVisible(btn)) continue;
969
+
970
+ const rawText = (btn.innerText || btn.value || btn.getAttribute('aria-label') || btn.getAttribute('title') || '').trim();
971
+ const txt = norm(rawText);
972
+ const aria = norm(btn.getAttribute('aria-label'));
973
+ const testId = norm(btn.getAttribute('data-testid') || btn.getAttribute('data-cy') || btn.getAttribute('id') || btn.getAttribute('name'));
974
+ const cls = norm(btn.className);
975
+ const blob = `${txt} ${aria} ${testId} ${cls}`;
976
+
977
+ if (!blob.trim()) continue;
978
+ if (negKeywords.some(neg => txt === neg || blob.includes(neg))) {
979
+ continue;
980
+ }
981
+
982
+ let score = 0;
983
+ for (let i = 0; i < posKeywords.length; i++) {
984
+ const kw = posKeywords[i];
985
+ const weight = Math.max(12, 50 - i * 2);
986
+ if (txt === kw || aria === kw) {
987
+ score += weight + 40;
988
+ } else if (txt.includes(kw) || aria.includes(kw)) {
989
+ score += weight + 20;
990
+ } else if (testId.includes(kw) || cls.includes(kw)) {
991
+ score += weight;
992
+ }
993
+ }
994
+
995
+ if ((btn.getAttribute('type') || '').toLowerCase() === 'submit') {
996
+ score += 25;
997
+ }
998
+
999
+ if (score > bestScore) {
1000
+ bestScore = score;
1001
+ bestBtn = btn;
1002
+ bestLabel = rawText || btn.getAttribute('aria-label') || btn.getAttribute('id') || 'submit';
1003
+ }
1004
+ }
1005
+
1006
+ if (bestBtn && bestScore >= 20) {
1007
+ bestBtn.click();
1008
+ return { clicked: true, label: bestLabel.slice(0, 60), score: bestScore };
1009
+ }
1010
+ return { clicked: false, label: null, score: 0 };
1011
+ }""",
1012
+ {
1013
+ "posKeywords": SEARCH_BUTTON_KEYWORDS,
1014
+ "negKeywords": NEGATIVE_BUTTON_KEYWORDS,
1015
+ },
1016
+ )
1017
+ if isinstance(result, dict) and result.get("clicked"):
1018
+ return result
1019
+ except Exception:
1020
+ pass
1021
+ return {"clicked": False, "label": None, "score": 0}
1022
+
1023
+
1024
+ async def run_generic_form_automation(
1025
+ page: Any,
1026
+ form_params: dict[str, str] | None = None,
1027
+ json_payload: Any = None,
1028
+ params: dict[str, Any] | None = None,
1029
+ url: str | None = None,
1030
+ target_url: str | None = None,
1031
+ submit_form: bool = True,
1032
+ settle_wait_sec: float = 2.5,
1033
+ ) -> dict[str, Any]:
1034
+ """Kısayol yardımcı fonksiyon: Verilen Playwright sayfasında GenericFormAutomator çalıştırır."""
1035
+ automator = GenericFormAutomator(settle_wait_sec=settle_wait_sec)
1036
+ return await automator.automate_page_form(
1037
+ page=page,
1038
+ form_params=form_params,
1039
+ json_payload=json_payload,
1040
+ params=params,
1041
+ url=url,
1042
+ target_url=target_url,
1043
+ submit_form=submit_form,
1044
+ )