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
registry/thy/client.py
ADDED
|
@@ -0,0 +1,1796 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
13
|
+
try:
|
|
14
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
16
|
+
except Exception:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))
|
|
20
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
|
21
|
+
from healer import SessionHealer, update_payload_deep
|
|
22
|
+
from core.token_store import TokenStore
|
|
23
|
+
from core.base_client import BaseShadowClient
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _normalize_thy_date(date_str: str) -> str:
|
|
27
|
+
"""Tarih dizgesini THY'nin güncel API şemasının beklediği DD-MM-YYYY formatına dönüştürür."""
|
|
28
|
+
clean = str(date_str).strip()
|
|
29
|
+
for sep in ("-", ".", "/"):
|
|
30
|
+
if sep in clean:
|
|
31
|
+
parts = clean.split(sep)
|
|
32
|
+
if len(parts) == 3:
|
|
33
|
+
# YYYY-MM-DD -> DD-MM-YYYY
|
|
34
|
+
if len(parts[0]) == 4:
|
|
35
|
+
return f"{parts[2].zfill(2)}-{parts[1].zfill(2)}-{parts[0]}"
|
|
36
|
+
# DD-MM-YYYY
|
|
37
|
+
return f"{parts[0].zfill(2)}-{parts[1].zfill(2)}-{parts[2]}"
|
|
38
|
+
return clean
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
THY_HOME_URL = "https://www.turkishairlines.com/tr-tr/"
|
|
42
|
+
THY_AVAILABILITY_URL = "https://www.turkishairlines.com/api/v1/availability"
|
|
43
|
+
|
|
44
|
+
THY_AIRPORT_META: dict[str, dict[str, Any]] = {
|
|
45
|
+
"IST": {"city_code": "IST", "city": "İstanbul", "country": "TR", "multi_port": True, "domestic": True},
|
|
46
|
+
"SAW": {"city_code": "IST", "city": "İstanbul", "country": "TR", "multi_port": False, "domestic": True},
|
|
47
|
+
"ESB": {"city_code": "ANK", "city": "Ankara", "country": "TR", "multi_port": False, "domestic": True},
|
|
48
|
+
"ADB": {"city_code": "IZM", "city": "İzmir", "country": "TR", "multi_port": False, "domestic": True},
|
|
49
|
+
"AYT": {"city_code": "AYT", "city": "Antalya", "country": "TR", "multi_port": False, "domestic": True},
|
|
50
|
+
"TZX": {"city_code": "TZX", "city": "Trabzon", "country": "TR", "multi_port": False, "domestic": True},
|
|
51
|
+
"ADA": {"city_code": "ADA", "city": "Adana", "country": "TR", "multi_port": False, "domestic": True},
|
|
52
|
+
"DLM": {"city_code": "DLM", "city": "Muğla", "country": "TR", "multi_port": False, "domestic": True},
|
|
53
|
+
"BJV": {"city_code": "BJV", "city": "Muğla", "country": "TR", "multi_port": False, "domestic": True},
|
|
54
|
+
"GZT": {"city_code": "GZT", "city": "Gaziantep", "country": "TR", "multi_port": False, "domestic": True},
|
|
55
|
+
"ASR": {"city_code": "ASR", "city": "Kayseri", "country": "TR", "multi_port": False, "domestic": True},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _airport_info(code: str) -> dict[str, Any]:
|
|
60
|
+
clean = (code or "IST").upper().strip()
|
|
61
|
+
return THY_AIRPORT_META.get(
|
|
62
|
+
clean,
|
|
63
|
+
{"city_code": clean, "city": clean, "country": "TR", "multi_port": clean == "IST", "domestic": True},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_captured_availability_template() -> dict[str, Any]:
|
|
68
|
+
"""THY /api/v1/availability POST isteğinin güncel ve doğrulanmış DTO şablonunu döndürür."""
|
|
69
|
+
return {
|
|
70
|
+
"selectedBookerSearch": "R",
|
|
71
|
+
"selectedCabinClass": "ECONOMY",
|
|
72
|
+
"moduleType": "TICKETING",
|
|
73
|
+
"passengerTypeList": [
|
|
74
|
+
{
|
|
75
|
+
"quantity": 1,
|
|
76
|
+
"code": "ADULT",
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"originDestinationInformationList": [
|
|
80
|
+
{
|
|
81
|
+
"originAirportCode": "IST",
|
|
82
|
+
"originCountryCode": "TR",
|
|
83
|
+
"originMultiPort": True,
|
|
84
|
+
"originDomestic": True,
|
|
85
|
+
"destinationAirportCode": "ESB",
|
|
86
|
+
"destinationCountryCode": "TR",
|
|
87
|
+
"destinationMultiPort": False,
|
|
88
|
+
"destinationDomestic": True,
|
|
89
|
+
"departureDate": "02-03-2027",
|
|
90
|
+
"originCityCode": "IST",
|
|
91
|
+
"destinationCityCode": "ANK",
|
|
92
|
+
"originCity": "İstanbul",
|
|
93
|
+
"destinationCity": "Ankara",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"originAirportCode": "ESB",
|
|
97
|
+
"originCountryCode": "TR",
|
|
98
|
+
"originMultiPort": False,
|
|
99
|
+
"originDomestic": True,
|
|
100
|
+
"destinationAirportCode": "IST",
|
|
101
|
+
"destinationCountryCode": "TR",
|
|
102
|
+
"destinationMultiPort": True,
|
|
103
|
+
"destinationDomestic": True,
|
|
104
|
+
"departureDate": "09-03-2027",
|
|
105
|
+
"originCityCode": "ANK",
|
|
106
|
+
"destinationCityCode": "IST",
|
|
107
|
+
"originCity": "Ankara",
|
|
108
|
+
"destinationCity": "İstanbul",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
"savedDate": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
|
112
|
+
"preselectedOptionDetails": [],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_thy_availability_payload(
|
|
117
|
+
departure_date: str = "02-03-2027",
|
|
118
|
+
return_date: str | None = "09-03-2027",
|
|
119
|
+
origin_airport_code: str = "IST",
|
|
120
|
+
destination_airport_code: str = "ESB",
|
|
121
|
+
cabin_class: str = "ECONOMY",
|
|
122
|
+
adults: int = 1,
|
|
123
|
+
) -> dict[str, Any]:
|
|
124
|
+
"""
|
|
125
|
+
Parametre eksikliğinden kaynaklı 400/404 hatalarını önlemek için THY'nin güncel
|
|
126
|
+
/api/v1/availability arama şemasına birebir uyumlu JSON gövdesini (body) oluşturur.
|
|
127
|
+
"""
|
|
128
|
+
is_round_trip = bool(return_date and str(return_date).strip())
|
|
129
|
+
norm_departure_date = _normalize_thy_date(departure_date)
|
|
130
|
+
orig_code = (origin_airport_code or "IST").upper().strip()
|
|
131
|
+
dest_code = (destination_airport_code or "ESB").upper().strip()
|
|
132
|
+
orig_meta = _airport_info(orig_code)
|
|
133
|
+
dest_meta = _airport_info(dest_code)
|
|
134
|
+
|
|
135
|
+
outbound_segment: dict[str, Any] = {
|
|
136
|
+
"originAirportCode": orig_code,
|
|
137
|
+
"originCountryCode": orig_meta["country"],
|
|
138
|
+
"originMultiPort": bool(orig_meta["multi_port"]),
|
|
139
|
+
"originDomestic": bool(orig_meta["domestic"]),
|
|
140
|
+
"destinationAirportCode": dest_code,
|
|
141
|
+
"destinationCountryCode": dest_meta["country"],
|
|
142
|
+
"destinationMultiPort": bool(dest_meta["multi_port"]),
|
|
143
|
+
"destinationDomestic": bool(dest_meta["domestic"]),
|
|
144
|
+
"departureDate": norm_departure_date,
|
|
145
|
+
"originCityCode": orig_meta["city_code"],
|
|
146
|
+
"destinationCityCode": dest_meta["city_code"],
|
|
147
|
+
"originCity": orig_meta["city"],
|
|
148
|
+
"destinationCity": dest_meta["city"],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
segments: list[dict[str, Any]] = [outbound_segment]
|
|
152
|
+
if is_round_trip and return_date:
|
|
153
|
+
inbound_segment: dict[str, Any] = {
|
|
154
|
+
"originAirportCode": dest_code,
|
|
155
|
+
"originCountryCode": dest_meta["country"],
|
|
156
|
+
"originMultiPort": bool(dest_meta["multi_port"]),
|
|
157
|
+
"originDomestic": bool(dest_meta["domestic"]),
|
|
158
|
+
"destinationAirportCode": orig_code,
|
|
159
|
+
"destinationCountryCode": orig_meta["country"],
|
|
160
|
+
"destinationMultiPort": bool(orig_meta["multi_port"]),
|
|
161
|
+
"destinationDomestic": bool(orig_meta["domestic"]),
|
|
162
|
+
"departureDate": _normalize_thy_date(return_date),
|
|
163
|
+
"originCityCode": dest_meta["city_code"],
|
|
164
|
+
"destinationCityCode": orig_meta["city_code"],
|
|
165
|
+
"originCity": dest_meta["city"],
|
|
166
|
+
"destinationCity": orig_meta["city"],
|
|
167
|
+
}
|
|
168
|
+
segments.append(inbound_segment)
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
"selectedBookerSearch": "R" if is_round_trip else "O",
|
|
172
|
+
"selectedCabinClass": (cabin_class or "ECONOMY").upper().strip(),
|
|
173
|
+
"moduleType": "TICKETING",
|
|
174
|
+
"passengerTypeList": [
|
|
175
|
+
{
|
|
176
|
+
"quantity": max(1, int(adults or 1)),
|
|
177
|
+
"code": "ADULT",
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
"originDestinationInformationList": segments,
|
|
181
|
+
"savedDate": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
|
182
|
+
"preselectedOptionDetails": [],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _build_validate_payload(p_load: Any) -> dict[str, Any]:
|
|
187
|
+
"""/api/v1/availability/validate uç noktası için oturum durumunu başlatan DTO payload'unu üretir."""
|
|
188
|
+
if not isinstance(p_load, dict) or not p_load.get("originDestinationInformationList"):
|
|
189
|
+
p_load = build_thy_availability_payload()
|
|
190
|
+
val = {
|
|
191
|
+
"selectedBookerSearch": p_load.get("selectedBookerSearch", "R"),
|
|
192
|
+
"selectedCabinClass": p_load.get("selectedCabinClass", "ECONOMY"),
|
|
193
|
+
"moduleType": p_load.get("moduleType", "TICKETING"),
|
|
194
|
+
"passengerTypeList": p_load.get("passengerTypeList", [{"quantity": 1, "code": "ADULT"}]),
|
|
195
|
+
"originDestinationInformationList": [],
|
|
196
|
+
}
|
|
197
|
+
for s in p_load.get("originDestinationInformationList", []):
|
|
198
|
+
if isinstance(s, dict):
|
|
199
|
+
val["originDestinationInformationList"].append({
|
|
200
|
+
"originAirportCode": s.get("originAirportCode"),
|
|
201
|
+
"originMultiPort": s.get("originMultiPort", False),
|
|
202
|
+
"destinationAirportCode": s.get("destinationAirportCode"),
|
|
203
|
+
"destinationMultiPort": s.get("destinationMultiPort", False),
|
|
204
|
+
"departureDate": s.get("departureDate"),
|
|
205
|
+
})
|
|
206
|
+
return val
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def build_thy_search_url(
|
|
210
|
+
departure_date: str = "02-03-2027",
|
|
211
|
+
return_date: str | None = "09-03-2027",
|
|
212
|
+
origin_airport_code: str = "IST",
|
|
213
|
+
destination_airport_code: str = "ESB",
|
|
214
|
+
cabin_class: str = "ECONOMY",
|
|
215
|
+
adults: int = 1,
|
|
216
|
+
cid: str | None = None,
|
|
217
|
+
) -> str:
|
|
218
|
+
"""
|
|
219
|
+
THY frontend uygulamasının sayfa/oturum state'ini başlatması için gereken
|
|
220
|
+
gidiş-dönüş (veya tek yön) arama parametrelerini ve cId'yi içeren tam arama URL'sini üretir.
|
|
221
|
+
"""
|
|
222
|
+
from urllib.parse import urlencode
|
|
223
|
+
|
|
224
|
+
active_cid = cid or str(uuid.uuid4())
|
|
225
|
+
orig_clean = (origin_airport_code or "IST").upper().strip()
|
|
226
|
+
dest_clean = (destination_airport_code or "ESB").upper().strip()
|
|
227
|
+
orig_meta = _airport_info(orig_clean)
|
|
228
|
+
dest_meta = _airport_info(dest_clean)
|
|
229
|
+
is_domestic = bool(orig_meta.get("domestic") and dest_meta.get("domestic"))
|
|
230
|
+
|
|
231
|
+
base_path = (
|
|
232
|
+
"/tr-tr/ucak-bileti/rezervasyon/ic-hat-musaitlik/"
|
|
233
|
+
if is_domestic
|
|
234
|
+
else "/tr-tr/ucak-bileti/rezervasyon/rezervasyon-durumu/"
|
|
235
|
+
)
|
|
236
|
+
dep_norm = _normalize_thy_date(departure_date)
|
|
237
|
+
is_round_trip = bool(return_date and str(return_date).strip())
|
|
238
|
+
ret_norm = _normalize_thy_date(return_date) if is_round_trip and return_date else None
|
|
239
|
+
|
|
240
|
+
query_params: dict[str, Any] = {
|
|
241
|
+
"cId": active_cid,
|
|
242
|
+
"origin": orig_clean,
|
|
243
|
+
"destination": dest_clean,
|
|
244
|
+
"departureDate": dep_norm,
|
|
245
|
+
}
|
|
246
|
+
if ret_norm:
|
|
247
|
+
query_params["returnDate"] = ret_norm
|
|
248
|
+
query_params["tripType"] = "R" if is_round_trip else "O"
|
|
249
|
+
query_params["cabinClass"] = (cabin_class or "ECONOMY").upper()
|
|
250
|
+
query_params["ADT"] = max(1, int(adults or 1))
|
|
251
|
+
|
|
252
|
+
return f"https://www.turkishairlines.com{base_path}?{urlencode(query_params)}"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def build_thy_search_url_from_payload(
|
|
256
|
+
payload: dict[str, Any] | None,
|
|
257
|
+
cid: str | None = None,
|
|
258
|
+
) -> str:
|
|
259
|
+
"""
|
|
260
|
+
/api/v1/availability payload sözlüğündeki originDestinationInformationList verisinden
|
|
261
|
+
gidiş-dönüş arama URL'sini oluşturur.
|
|
262
|
+
"""
|
|
263
|
+
if not isinstance(payload, dict):
|
|
264
|
+
return build_thy_search_url(cid=cid)
|
|
265
|
+
|
|
266
|
+
od_list = payload.get("originDestinationInformationList")
|
|
267
|
+
orig_code = "IST"
|
|
268
|
+
dest_code = "ESB"
|
|
269
|
+
dep_date = "02-03-2027"
|
|
270
|
+
ret_date: str | None = "09-03-2027"
|
|
271
|
+
|
|
272
|
+
if isinstance(od_list, list) and len(od_list) >= 1 and isinstance(od_list[0], dict):
|
|
273
|
+
first_seg = od_list[0]
|
|
274
|
+
orig_code = str(first_seg.get("originAirportCode") or orig_code)
|
|
275
|
+
dest_code = str(first_seg.get("destinationAirportCode") or dest_code)
|
|
276
|
+
dep_date = str(first_seg.get("departureDate") or dep_date)
|
|
277
|
+
if len(od_list) >= 2 and isinstance(od_list[1], dict):
|
|
278
|
+
ret_date = str(od_list[1].get("departureDate") or "") or None
|
|
279
|
+
elif str(payload.get("selectedBookerSearch", "R")).upper() == "O":
|
|
280
|
+
ret_date = None
|
|
281
|
+
|
|
282
|
+
cabin_class = str(payload.get("selectedCabinClass") or "ECONOMY")
|
|
283
|
+
adults = 1
|
|
284
|
+
pax_list = payload.get("passengerTypeList")
|
|
285
|
+
if isinstance(pax_list, list):
|
|
286
|
+
for p in pax_list:
|
|
287
|
+
if isinstance(p, dict) and str(p.get("code", "")).upper() in ("ADULT", "ADT"):
|
|
288
|
+
try:
|
|
289
|
+
adults = max(1, int(p.get("quantity", 1)))
|
|
290
|
+
except Exception:
|
|
291
|
+
adults = 1
|
|
292
|
+
break
|
|
293
|
+
|
|
294
|
+
return build_thy_search_url(
|
|
295
|
+
departure_date=dep_date,
|
|
296
|
+
return_date=ret_date,
|
|
297
|
+
origin_airport_code=orig_code,
|
|
298
|
+
destination_airport_code=dest_code,
|
|
299
|
+
cabin_class=cabin_class,
|
|
300
|
+
adults=adults,
|
|
301
|
+
cid=cid,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def search_flights(
|
|
306
|
+
departure_date: str = '02-03-2027',
|
|
307
|
+
return_date: str | None = '09-03-2027',
|
|
308
|
+
origin_airport_code: str = 'IST',
|
|
309
|
+
destination_airport_code: str = 'ESB',
|
|
310
|
+
cid: str | None = None,
|
|
311
|
+
tenant_id: str = "default",
|
|
312
|
+
mode: str = "local",
|
|
313
|
+
non_interactive: bool | None = None,
|
|
314
|
+
) -> dict[str, Any]:
|
|
315
|
+
"""
|
|
316
|
+
Türk Hava Yolları (THY) İstanbul - Ankara gidiş-dönüş veya tek yön uçuş seferlerini ve müsaitlik/fiyat bilgilerini
|
|
317
|
+
doğrudan User-Delegated In-Browser Fetch tüneli üzerinden sorgular.
|
|
318
|
+
Önce THY ana sayfasında ve ilgili gidiş-dönüş arama URL'sinde sayfa/oturum state'ini oluşturur,
|
|
319
|
+
ardından Referer ve Origin başlıklarıyla /api/v1/availability POST çağrısını yürütür.
|
|
320
|
+
"""
|
|
321
|
+
from urllib.parse import urlparse
|
|
322
|
+
|
|
323
|
+
params: dict[str, Any] = {}
|
|
324
|
+
headers: dict[str, str] = {
|
|
325
|
+
"Accept": "application/json, text/plain, */*",
|
|
326
|
+
"Content-Type": "application/json",
|
|
327
|
+
"Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
328
|
+
"Referer": "https://www.turkishairlines.com/",
|
|
329
|
+
"Origin": "https://www.turkishairlines.com",
|
|
330
|
+
"referer": "https://www.turkishairlines.com/",
|
|
331
|
+
"origin": "https://www.turkishairlines.com",
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
payload = build_thy_availability_payload(
|
|
335
|
+
departure_date=departure_date,
|
|
336
|
+
return_date=return_date,
|
|
337
|
+
origin_airport_code=origin_airport_code,
|
|
338
|
+
destination_airport_code=destination_airport_code,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
healer = SessionHealer(
|
|
342
|
+
target_url=THY_HOME_URL,
|
|
343
|
+
tenant_id=tenant_id,
|
|
344
|
+
service="thy",
|
|
345
|
+
mode=mode,
|
|
346
|
+
non_interactive=bool(non_interactive) if non_interactive is not None else False,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# TokenStore (.sessions/{tenant_id}/thy_tokens.json) içindeki gerçek çerezleri ve User-Agent başlığını yükle
|
|
350
|
+
saved_tokens = healer.load_tokens(tenant_id=tenant_id, service="thy")
|
|
351
|
+
real_cookie = saved_tokens.get("cookie") or saved_tokens.get("headers", {}).get("cookie", "")
|
|
352
|
+
real_ua = saved_tokens.get("user-agent") or saved_tokens.get("headers", {}).get("user-agent", "")
|
|
353
|
+
|
|
354
|
+
if saved_tokens:
|
|
355
|
+
tok_headers = saved_tokens.get("headers", {})
|
|
356
|
+
if isinstance(tok_headers, dict):
|
|
357
|
+
for k in ("authorization", "x-session-id", "x-session", "tafsessionid", "x-requested-with", "accept-language"):
|
|
358
|
+
if tok_headers.get(k):
|
|
359
|
+
headers[k] = str(tok_headers[k])
|
|
360
|
+
|
|
361
|
+
if real_cookie:
|
|
362
|
+
headers["cookie"] = real_cookie
|
|
363
|
+
headers["Cookie"] = real_cookie
|
|
364
|
+
if real_ua:
|
|
365
|
+
headers["user-agent"] = real_ua
|
|
366
|
+
|
|
367
|
+
# Kalıcı profil ve PerimeterX (_px...) çerezlerini istek başlıklarına bağla
|
|
368
|
+
headers, params, payload = healer.apply_to_request(
|
|
369
|
+
headers,
|
|
370
|
+
params,
|
|
371
|
+
payload,
|
|
372
|
+
endpoint_url=THY_AVAILABILITY_URL,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
for k in list(headers.keys()):
|
|
376
|
+
if k.lower().startswith("sec-") or k.lower() == "accept-encoding":
|
|
377
|
+
del headers[k]
|
|
378
|
+
|
|
379
|
+
# Dinamik cId parametresini belirle
|
|
380
|
+
if not cid:
|
|
381
|
+
raw_referer = headers.get("referer", "")
|
|
382
|
+
if "cid=" in raw_referer.lower():
|
|
383
|
+
try:
|
|
384
|
+
from urllib.parse import parse_qs
|
|
385
|
+
qs = parse_qs(urlparse(raw_referer).query)
|
|
386
|
+
found = qs.get("cId", qs.get("cid", [None]))[0]
|
|
387
|
+
if found:
|
|
388
|
+
cid = found
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
if not cid:
|
|
392
|
+
cid = str(uuid.uuid4())
|
|
393
|
+
|
|
394
|
+
# Backend DTO JSON şemasını bozmamak için payload gövdesindeki fazla alanları temizle
|
|
395
|
+
ALLOWED_PAYLOAD_KEYS = {
|
|
396
|
+
"selectedBookerSearch",
|
|
397
|
+
"selectedCabinClass",
|
|
398
|
+
"moduleType",
|
|
399
|
+
"passengerTypeList",
|
|
400
|
+
"originDestinationInformationList",
|
|
401
|
+
"savedDate",
|
|
402
|
+
"preselectedOptionDetails",
|
|
403
|
+
}
|
|
404
|
+
for k in list(payload.keys()):
|
|
405
|
+
if k not in ALLOWED_PAYLOAD_KEYS:
|
|
406
|
+
del payload[k]
|
|
407
|
+
|
|
408
|
+
# Gidiş-dönüş parametrelerini içeren arama URL'sini oluştur
|
|
409
|
+
search_url = build_thy_search_url_from_payload(payload, cid=cid)
|
|
410
|
+
parsed_search = urlparse(search_url)
|
|
411
|
+
route_page = (
|
|
412
|
+
f"{parsed_search.path}?{parsed_search.query}"
|
|
413
|
+
if parsed_search.query
|
|
414
|
+
else parsed_search.path
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Zorunlu Referer ve Origin başlıklarını ve THY özel başlıklarını sabitle
|
|
418
|
+
headers["accept"] = "application/json, text/plain, */*"
|
|
419
|
+
headers["content-type"] = "application/json"
|
|
420
|
+
headers["Referer"] = search_url
|
|
421
|
+
headers["Origin"] = "https://www.turkishairlines.com"
|
|
422
|
+
headers["referer"] = search_url
|
|
423
|
+
headers["origin"] = "https://www.turkishairlines.com"
|
|
424
|
+
headers["x-current-page-url"] = route_page
|
|
425
|
+
headers["x-client-id"] = cid
|
|
426
|
+
headers["x-clientid"] = cid
|
|
427
|
+
headers["x-country"] = "TR"
|
|
428
|
+
headers["x-language"] = "tr"
|
|
429
|
+
|
|
430
|
+
# TAF / Session ID başlıkları
|
|
431
|
+
for cookie_part in headers.get("cookie", "").split(";"):
|
|
432
|
+
if "TAFSessionId=" in cookie_part:
|
|
433
|
+
taf_val = cookie_part.split("TAFSessionId=")[1].strip()
|
|
434
|
+
if taf_val:
|
|
435
|
+
headers.setdefault("tafsessionid", taf_val)
|
|
436
|
+
headers.setdefault("x-session-id", taf_val)
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
# User-Delegated Browser Middleware:
|
|
440
|
+
# Önce tarayıcıda ana sayfaya ve gidiş-dönüş arama URL'sine (search_url) gidip Generic Form Automation
|
|
441
|
+
# ve sayfa state'ini oluştur, ardından fetch tüneli üzerinden /api/v1/availability POST isteğini yürüt.
|
|
442
|
+
raw_response = healer.execute_in_browser(
|
|
443
|
+
method="POST",
|
|
444
|
+
url=THY_AVAILABILITY_URL,
|
|
445
|
+
headers=headers,
|
|
446
|
+
params=params if params else None,
|
|
447
|
+
json_payload=payload,
|
|
448
|
+
target_url=search_url,
|
|
449
|
+
tenant_id=tenant_id,
|
|
450
|
+
mode=mode,
|
|
451
|
+
non_interactive=non_interactive,
|
|
452
|
+
form_params={
|
|
453
|
+
"origin": origin_airport_code,
|
|
454
|
+
"destination": destination_airport_code,
|
|
455
|
+
"departure_date": departure_date,
|
|
456
|
+
"return_date": return_date or "",
|
|
457
|
+
},
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Hata Detayı: Eğer 400 Bad Request dönerse gönderilen payload ve dönen yanıt detayını yazdır
|
|
461
|
+
if isinstance(raw_response, dict):
|
|
462
|
+
status_code = raw_response.get("status_code")
|
|
463
|
+
if raw_response.get("status") == 400 or status_code == 400:
|
|
464
|
+
print("\n" + "=" * 80)
|
|
465
|
+
print("❌ [Shadow-API] 400 Bad Request Hatası Alındı!")
|
|
466
|
+
print(f"HTTP Status: {status_code}")
|
|
467
|
+
print("Gönderilen Payload (JSON):")
|
|
468
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
469
|
+
print("Sunucu Hata Yanıtı (response.text):")
|
|
470
|
+
print(raw_response.get("text") or raw_response)
|
|
471
|
+
print("=" * 80 + "\n")
|
|
472
|
+
|
|
473
|
+
if raw_response.get("success") is True:
|
|
474
|
+
data_obj = raw_response.get("data")
|
|
475
|
+
if isinstance(data_obj, dict):
|
|
476
|
+
print(f"\n[Shadow-API] 🔑 API 'success: True' döndürdü. 'data' anahtarları: {list(data_obj.keys())}")
|
|
477
|
+
elif isinstance(data_obj, list):
|
|
478
|
+
print(f"\n[Shadow-API] 🔑 API 'success: True' döndürdü. 'data' (liste) uzunluğu: {len(data_obj)}")
|
|
479
|
+
raw_response["parsed"] = parse_thy_availability(raw_response)
|
|
480
|
+
return raw_response
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
_PRICE_KEYS_EXACT = (
|
|
484
|
+
"amount",
|
|
485
|
+
"totalAmount",
|
|
486
|
+
"price",
|
|
487
|
+
"totalPrice",
|
|
488
|
+
"value",
|
|
489
|
+
"fare",
|
|
490
|
+
"totalFare",
|
|
491
|
+
"baseFare",
|
|
492
|
+
"startingPrice",
|
|
493
|
+
"starting_price",
|
|
494
|
+
"amountInPreferredCurrency",
|
|
495
|
+
"fareAmount",
|
|
496
|
+
"cashAmount",
|
|
497
|
+
"netAmount",
|
|
498
|
+
"cheapestPrice",
|
|
499
|
+
"lowestPrice",
|
|
500
|
+
"minPrice",
|
|
501
|
+
"bestFare",
|
|
502
|
+
"economyStartingPrice",
|
|
503
|
+
"businessStartingPrice",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
_NON_PRICE_KEYS = {
|
|
507
|
+
"decimalplaces",
|
|
508
|
+
"quantity",
|
|
509
|
+
"seatcount",
|
|
510
|
+
"availableseatcount",
|
|
511
|
+
"availableseats",
|
|
512
|
+
"seats",
|
|
513
|
+
"flightpagelimit",
|
|
514
|
+
"minflightcountforsorting",
|
|
515
|
+
"minflightcountfortimerangefiltering",
|
|
516
|
+
"minflightcountforoptionsfiltering",
|
|
517
|
+
"maximumdaysforavailability",
|
|
518
|
+
"datedifference",
|
|
519
|
+
"duration",
|
|
520
|
+
"flightduration",
|
|
521
|
+
"flightnumber",
|
|
522
|
+
"carrierflightnumber",
|
|
523
|
+
"id",
|
|
524
|
+
"index",
|
|
525
|
+
"status",
|
|
526
|
+
"status_code",
|
|
527
|
+
"statuscode",
|
|
528
|
+
"maxage",
|
|
529
|
+
"minage",
|
|
530
|
+
"maxcount",
|
|
531
|
+
"numberofextensions",
|
|
532
|
+
"countdowntime",
|
|
533
|
+
"pagestimeout",
|
|
534
|
+
"prefetcheditemcount",
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
_THY_BRAND_ALIASES: dict[str, str] = {
|
|
538
|
+
"ef": "EcoFly",
|
|
539
|
+
"ec": "EcoFly",
|
|
540
|
+
"eco": "EcoFly",
|
|
541
|
+
"ecofly": "EcoFly",
|
|
542
|
+
"eco_fly": "EcoFly",
|
|
543
|
+
"eco fly": "EcoFly",
|
|
544
|
+
"brand.ecofly": "EcoFly",
|
|
545
|
+
"ecojet": "EcoFly",
|
|
546
|
+
"xf": "ExtraFly",
|
|
547
|
+
"ex": "ExtraFly",
|
|
548
|
+
"fl": "ExtraFly",
|
|
549
|
+
"ef2": "ExtraFly",
|
|
550
|
+
"extrafly": "ExtraFly",
|
|
551
|
+
"extra_fly": "ExtraFly",
|
|
552
|
+
"extra fly": "ExtraFly",
|
|
553
|
+
"brand.extrafly": "ExtraFly",
|
|
554
|
+
"flexfly": "ExtraFly",
|
|
555
|
+
"flex_fly": "ExtraFly",
|
|
556
|
+
"flex fly": "ExtraFly",
|
|
557
|
+
"brand.flexfly": "ExtraFly",
|
|
558
|
+
"extra": "ExtraFly",
|
|
559
|
+
"pf": "PrimeFly",
|
|
560
|
+
"pr": "PrimeFly",
|
|
561
|
+
"primefly": "PrimeFly",
|
|
562
|
+
"prime_fly": "PrimeFly",
|
|
563
|
+
"prime fly": "PrimeFly",
|
|
564
|
+
"brand.primefly": "PrimeFly",
|
|
565
|
+
"prime": "PrimeFly",
|
|
566
|
+
"bu": "Business",
|
|
567
|
+
"bc": "Business",
|
|
568
|
+
"bl": "Business",
|
|
569
|
+
"bus": "Business",
|
|
570
|
+
"business": "Business",
|
|
571
|
+
"businessfly": "Business",
|
|
572
|
+
"business_fly": "Business",
|
|
573
|
+
"business fly": "Business",
|
|
574
|
+
"businessprime": "Business",
|
|
575
|
+
"business_prime": "Business",
|
|
576
|
+
"brand.business": "Business",
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _normalize_thy_package_name(
|
|
581
|
+
raw_name: Any,
|
|
582
|
+
cabin_class: str = "",
|
|
583
|
+
idx_in_cabin: int | None = None,
|
|
584
|
+
) -> str:
|
|
585
|
+
"""
|
|
586
|
+
THY marka/paket kodlarını ve isimlerini ('EF', 'XF', 'FL', 'PF', 'BU', 'brand.ecoFly' vb.)
|
|
587
|
+
standart 'EcoFly', 'ExtraFly', 'PrimeFly' veya 'Business' paket adlarına dönüştürür.
|
|
588
|
+
"""
|
|
589
|
+
if isinstance(raw_name, list) and raw_name:
|
|
590
|
+
raw_name = raw_name[0]
|
|
591
|
+
clean = str(raw_name or "").strip()
|
|
592
|
+
low = clean.lower().replace("-", "_").strip()
|
|
593
|
+
if low.startswith("brand."):
|
|
594
|
+
low_no_prefix = low[6:].strip()
|
|
595
|
+
else:
|
|
596
|
+
low_no_prefix = low
|
|
597
|
+
|
|
598
|
+
if low in _THY_BRAND_ALIASES:
|
|
599
|
+
return _THY_BRAND_ALIASES[low]
|
|
600
|
+
if low_no_prefix in _THY_BRAND_ALIASES:
|
|
601
|
+
return _THY_BRAND_ALIASES[low_no_prefix]
|
|
602
|
+
|
|
603
|
+
for key_sub, canonical in (
|
|
604
|
+
("ecofly", "EcoFly"),
|
|
605
|
+
("eco_fly", "EcoFly"),
|
|
606
|
+
("extrafly", "ExtraFly"),
|
|
607
|
+
("extra_fly", "ExtraFly"),
|
|
608
|
+
("flexfly", "ExtraFly"),
|
|
609
|
+
("primefly", "PrimeFly"),
|
|
610
|
+
("prime_fly", "PrimeFly"),
|
|
611
|
+
("business", "Business"),
|
|
612
|
+
):
|
|
613
|
+
if key_sub in low:
|
|
614
|
+
return canonical
|
|
615
|
+
|
|
616
|
+
cabin_up = str(cabin_class or "").upper().strip()
|
|
617
|
+
if cabin_up == "BUSINESS" and (not clean or low in ("business", "standart", "standard", "default")):
|
|
618
|
+
return "Business"
|
|
619
|
+
|
|
620
|
+
if cabin_up in ("ECONOMY", "") and (not clean or low in ("economy", "standart", "standard", "default")):
|
|
621
|
+
if idx_in_cabin == 0:
|
|
622
|
+
return "EcoFly"
|
|
623
|
+
if idx_in_cabin == 1:
|
|
624
|
+
return "ExtraFly"
|
|
625
|
+
if idx_in_cabin == 2:
|
|
626
|
+
return "PrimeFly"
|
|
627
|
+
if cabin_up == "ECONOMY" and idx_in_cabin is None:
|
|
628
|
+
return "EcoFly"
|
|
629
|
+
|
|
630
|
+
return clean or "EcoFly"
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _parse_numeric_price_str(val_str: str) -> tuple[float | None, str]:
|
|
634
|
+
"""Metin formatındaki fiyat dizgesinden ('1.671,71 TRY', '₺1671.71' vb.) tutar ve para birimini ayıklar."""
|
|
635
|
+
import re
|
|
636
|
+
|
|
637
|
+
s = str(val_str or "").strip()
|
|
638
|
+
if not s:
|
|
639
|
+
return None, ""
|
|
640
|
+
curr = ""
|
|
641
|
+
s_up = s.upper()
|
|
642
|
+
for code, norm_c in (("TRY", "TRY"), ("TL", "TRY"), ("₺", "TRY"), ("USD", "USD"), ("$", "USD"), ("EUR", "EUR"), ("€", "EUR"), ("GBP", "GBP")):
|
|
643
|
+
if code in s_up or code in s:
|
|
644
|
+
curr = norm_c
|
|
645
|
+
break
|
|
646
|
+
|
|
647
|
+
m = re.search(r"[-+]?\d[\d.,]*", s)
|
|
648
|
+
if not m:
|
|
649
|
+
return None, curr
|
|
650
|
+
num_tok = m.group(0)
|
|
651
|
+
try:
|
|
652
|
+
if "," in num_tok and "." in num_tok:
|
|
653
|
+
if num_tok.rfind(",") > num_tok.rfind("."):
|
|
654
|
+
num_tok = num_tok.replace(".", "").replace(",", ".")
|
|
655
|
+
else:
|
|
656
|
+
num_tok = num_tok.replace(",", "")
|
|
657
|
+
elif "," in num_tok:
|
|
658
|
+
parts = num_tok.split(",")
|
|
659
|
+
if len(parts[-1]) == 3 and len(parts) > 1 and all(p.isdigit() for p in parts):
|
|
660
|
+
num_tok = "".join(parts)
|
|
661
|
+
else:
|
|
662
|
+
num_tok = num_tok.replace(",", ".")
|
|
663
|
+
amt = float(num_tok)
|
|
664
|
+
return (amt if amt > 0 else None), curr
|
|
665
|
+
except ValueError:
|
|
666
|
+
return None, curr
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _extract_amount_and_currency(obj: Any, _depth: int = 0) -> tuple[float | None, str]:
|
|
670
|
+
"""Herhangi bir fiyat/tutar nesnesinden sayısal tutar ve para birimini derinlemesine ayıklar."""
|
|
671
|
+
if obj is None or _depth > 6:
|
|
672
|
+
return None, ""
|
|
673
|
+
if isinstance(obj, bool):
|
|
674
|
+
return None, ""
|
|
675
|
+
if isinstance(obj, (int, float)):
|
|
676
|
+
val = float(obj)
|
|
677
|
+
return (val if val > 0 else None), "TRY"
|
|
678
|
+
if isinstance(obj, str):
|
|
679
|
+
amt, curr = _parse_numeric_price_str(obj)
|
|
680
|
+
return amt, (curr or "TRY") if amt is not None else ""
|
|
681
|
+
if isinstance(obj, list):
|
|
682
|
+
best_amt: float | None = None
|
|
683
|
+
best_curr = ""
|
|
684
|
+
for item in obj:
|
|
685
|
+
sub_amt, sub_curr = _extract_amount_and_currency(item, _depth + 1)
|
|
686
|
+
if sub_amt is not None and sub_amt > 0:
|
|
687
|
+
if best_amt is None or sub_amt < best_amt:
|
|
688
|
+
best_amt = sub_amt
|
|
689
|
+
best_curr = sub_curr or best_curr
|
|
690
|
+
return best_amt, str(best_curr or "TRY") if best_amt is not None else ""
|
|
691
|
+
if isinstance(obj, dict):
|
|
692
|
+
curr_raw = (
|
|
693
|
+
obj.get("currency")
|
|
694
|
+
or obj.get("currencyCode")
|
|
695
|
+
or obj.get("currency_code")
|
|
696
|
+
or obj.get("currencySign")
|
|
697
|
+
or ""
|
|
698
|
+
)
|
|
699
|
+
curr = "TRY" if str(curr_raw).strip() == "₺" else str(curr_raw or "").strip()
|
|
700
|
+
|
|
701
|
+
amt: float | None = None
|
|
702
|
+
for k in _PRICE_KEYS_EXACT:
|
|
703
|
+
if k not in obj:
|
|
704
|
+
continue
|
|
705
|
+
v = obj.get(k)
|
|
706
|
+
if v is None or isinstance(v, bool):
|
|
707
|
+
continue
|
|
708
|
+
if isinstance(v, (int, float)):
|
|
709
|
+
if float(v) > 0:
|
|
710
|
+
amt = float(v)
|
|
711
|
+
break
|
|
712
|
+
elif isinstance(v, str):
|
|
713
|
+
p_amt, p_curr = _parse_numeric_price_str(v)
|
|
714
|
+
if p_amt is not None and p_amt > 0:
|
|
715
|
+
amt = p_amt
|
|
716
|
+
curr = curr or p_curr
|
|
717
|
+
break
|
|
718
|
+
elif isinstance(v, (dict, list)):
|
|
719
|
+
sub_amt, sub_curr = _extract_amount_and_currency(v, _depth + 1)
|
|
720
|
+
if sub_amt is not None and sub_amt > 0:
|
|
721
|
+
amt = sub_amt
|
|
722
|
+
curr = curr or sub_curr
|
|
723
|
+
break
|
|
724
|
+
|
|
725
|
+
if amt is None:
|
|
726
|
+
for k, v in obj.items():
|
|
727
|
+
k_low = str(k).lower().strip()
|
|
728
|
+
if k_low in _NON_PRICE_KEYS:
|
|
729
|
+
continue
|
|
730
|
+
if any(pk in k_low for pk in ("price", "amount", "fare", "cost", "total", "tutar", "fiyat", "ecofly", "extrafly", "primefly")):
|
|
731
|
+
sub_amt, sub_curr = _extract_amount_and_currency(v, _depth + 1)
|
|
732
|
+
if sub_amt is not None and sub_amt > 0:
|
|
733
|
+
if amt is None or sub_amt < amt:
|
|
734
|
+
amt = sub_amt
|
|
735
|
+
curr = curr or sub_curr
|
|
736
|
+
|
|
737
|
+
return amt, str(curr or "TRY") if amt is not None else ""
|
|
738
|
+
return None, ""
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _unwrap_thy_root(data: Any) -> Any:
|
|
742
|
+
"""
|
|
743
|
+
Gelen yanıt sözlük, liste veya JSON metni (hatta kesilmiş JSON metni) olsa dahi
|
|
744
|
+
içindeki esas veri kökünü (`root`) güvenli şekilde çözümler.
|
|
745
|
+
"""
|
|
746
|
+
import re
|
|
747
|
+
|
|
748
|
+
if isinstance(data, str):
|
|
749
|
+
raw_str = data.strip()
|
|
750
|
+
if not raw_str:
|
|
751
|
+
return {}
|
|
752
|
+
try:
|
|
753
|
+
return _unwrap_thy_root(json.loads(raw_str))
|
|
754
|
+
except Exception:
|
|
755
|
+
# Kesilmiş (truncated) JSON dizgesinden anlamlı alanları kurtar
|
|
756
|
+
recovered: dict[str, Any] = {"_raw_truncated_text": raw_str}
|
|
757
|
+
for key_name in ("economyStartingPrice", "businessStartingPrice", "bestFare"):
|
|
758
|
+
m = re.search(rf'"{key_name}"\s*:\s*(\{{[^{{}}]*\{{[^{{}}]*\}}[^{{}}]*\}}|\{{[^{{}}]*\}})', raw_str)
|
|
759
|
+
if m:
|
|
760
|
+
try:
|
|
761
|
+
recovered[key_name] = json.loads(m.group(1))
|
|
762
|
+
except Exception:
|
|
763
|
+
pass
|
|
764
|
+
return recovered
|
|
765
|
+
|
|
766
|
+
if isinstance(data, dict):
|
|
767
|
+
for wrapper_key in ("data", "result", "response", "payload", "body"):
|
|
768
|
+
inner = data.get(wrapper_key)
|
|
769
|
+
if isinstance(inner, (dict, list)) and inner:
|
|
770
|
+
return inner
|
|
771
|
+
if isinstance(inner, str) and inner.strip().startswith(("{", "[")):
|
|
772
|
+
parsed_inner = _unwrap_thy_root(inner)
|
|
773
|
+
if parsed_inner:
|
|
774
|
+
return parsed_inner
|
|
775
|
+
|
|
776
|
+
for text_key in ("text", "response_sample", "raw_text"):
|
|
777
|
+
raw_txt = data.get(text_key)
|
|
778
|
+
if isinstance(raw_txt, str) and raw_txt.strip().startswith(("{", "[")):
|
|
779
|
+
parsed_txt = _unwrap_thy_root(raw_txt)
|
|
780
|
+
if isinstance(parsed_txt, (dict, list)) and parsed_txt:
|
|
781
|
+
return parsed_txt
|
|
782
|
+
|
|
783
|
+
return data
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _extract_packages_deep(f_opt: Any, default_cabin: str = "ECONOMY") -> list[dict[str, Any]]:
|
|
787
|
+
"""
|
|
788
|
+
Bir uçuş seçeneği (veya alt JSON ağacı) içindeki tüm paketleri (EcoFly, ExtraFly, PrimeFly, Business)
|
|
789
|
+
standart anahtarlara bağlı kalmaksızın derinlemesine (deep search) tarayarak ayıklar.
|
|
790
|
+
"""
|
|
791
|
+
fares_out: list[dict[str, Any]] = []
|
|
792
|
+
seen_pkg_keys: set[tuple[str, float]] = set()
|
|
793
|
+
|
|
794
|
+
def _add_fare(
|
|
795
|
+
raw_pkg_name: Any,
|
|
796
|
+
cabin_name: str,
|
|
797
|
+
amt: float | None,
|
|
798
|
+
curr: str,
|
|
799
|
+
seats: Any = None,
|
|
800
|
+
idx_in_cabin: int | None = None,
|
|
801
|
+
) -> None:
|
|
802
|
+
if amt is None or amt <= 0:
|
|
803
|
+
return
|
|
804
|
+
norm_name = _normalize_thy_package_name(raw_pkg_name, cabin_class=cabin_name, idx_in_cabin=idx_in_cabin)
|
|
805
|
+
norm_cabin = str(cabin_name or ("BUSINESS" if norm_name == "Business" else default_cabin)).upper().strip() or "ECONOMY"
|
|
806
|
+
dedup_key = (norm_name, round(float(amt), 2))
|
|
807
|
+
if dedup_key in seen_pkg_keys:
|
|
808
|
+
return
|
|
809
|
+
seen_pkg_keys.add(dedup_key)
|
|
810
|
+
fares_out.append({
|
|
811
|
+
"package_name": norm_name,
|
|
812
|
+
"cabin_class": norm_cabin,
|
|
813
|
+
"amount": round(float(amt), 2),
|
|
814
|
+
"currency": str(curr or "TRY"),
|
|
815
|
+
"available_seats": seats,
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
if not isinstance(f_opt, (dict, list)):
|
|
819
|
+
return fares_out
|
|
820
|
+
|
|
821
|
+
# 1. Standart fareCategory yapısı
|
|
822
|
+
if isinstance(f_opt, dict):
|
|
823
|
+
fare_category = f_opt.get("fareCategory") or f_opt.get("fareCategories") or f_opt.get("cabins")
|
|
824
|
+
if isinstance(fare_category, dict):
|
|
825
|
+
for cabin_name, cabin_obj in fare_category.items():
|
|
826
|
+
if isinstance(cabin_obj, dict):
|
|
827
|
+
b_list = (
|
|
828
|
+
cabin_obj.get("bookingPriceInfoList")
|
|
829
|
+
or cabin_obj.get("brandList")
|
|
830
|
+
or cabin_obj.get("packages")
|
|
831
|
+
or cabin_obj.get("fares")
|
|
832
|
+
or cabin_obj.get("subGroupList")
|
|
833
|
+
or []
|
|
834
|
+
)
|
|
835
|
+
if isinstance(b_list, list) and b_list:
|
|
836
|
+
for b_idx, b_info in enumerate(b_list):
|
|
837
|
+
if not isinstance(b_info, dict):
|
|
838
|
+
continue
|
|
839
|
+
raw_brand = (
|
|
840
|
+
b_info.get("brandName")
|
|
841
|
+
or b_info.get("brandKey")
|
|
842
|
+
or b_info.get("brandCode")
|
|
843
|
+
or b_info.get("brandCodeList")
|
|
844
|
+
or b_info.get("fareFamilyCode")
|
|
845
|
+
or b_info.get("subGroupName")
|
|
846
|
+
or b_info.get("name")
|
|
847
|
+
or b_info.get("code")
|
|
848
|
+
)
|
|
849
|
+
pkg_amt, pkg_curr = _extract_amount_and_currency(
|
|
850
|
+
b_info.get("startingPrice")
|
|
851
|
+
or b_info.get("price")
|
|
852
|
+
or b_info.get("totalPrice")
|
|
853
|
+
or b_info.get("fare")
|
|
854
|
+
or b_info
|
|
855
|
+
)
|
|
856
|
+
seat_count = (
|
|
857
|
+
b_info.get("availableSeatCount")
|
|
858
|
+
or b_info.get("seatCount")
|
|
859
|
+
or b_info.get("availableSeats")
|
|
860
|
+
or b_info.get("seats")
|
|
861
|
+
)
|
|
862
|
+
_add_fare(raw_brand, str(cabin_name), pkg_amt, pkg_curr, seat_count, idx_in_cabin=b_idx)
|
|
863
|
+
else:
|
|
864
|
+
c_amt, c_curr = _extract_amount_and_currency(cabin_obj)
|
|
865
|
+
_add_fare(cabin_name, str(cabin_name), c_amt, c_curr, idx_in_cabin=0)
|
|
866
|
+
|
|
867
|
+
# 2. Derinlemesine (Deep Search) paket/marka taraması
|
|
868
|
+
def _scan_packages(node: Any, current_cabin: str = default_cabin) -> None:
|
|
869
|
+
if isinstance(node, dict):
|
|
870
|
+
node_cabin = str(
|
|
871
|
+
node.get("cabinClass")
|
|
872
|
+
or node.get("cabin")
|
|
873
|
+
or node.get("cabinType")
|
|
874
|
+
or current_cabin
|
|
875
|
+
).upper().strip() or default_cabin
|
|
876
|
+
|
|
877
|
+
# Sözlük anahtarları doğrudan EcoFly / ExtraFly / PrimeFly / Business veya EF / XF / PF ise
|
|
878
|
+
for k, v in node.items():
|
|
879
|
+
k_str = str(k).strip()
|
|
880
|
+
k_low = k_str.lower().replace("-", "_").replace(" ", "_")
|
|
881
|
+
if k_low in (
|
|
882
|
+
"ecofly", "eco_fly", "extrafly", "extra_fly", "flexfly", "flex_fly",
|
|
883
|
+
"primefly", "prime_fly", "business", "businessfly",
|
|
884
|
+
"ecofly_price", "extrafly_price", "primefly_price", "business_price",
|
|
885
|
+
) or k_str in ("EF", "XF", "FL", "PF", "BU"):
|
|
886
|
+
p_amt, p_curr = _extract_amount_and_currency(v)
|
|
887
|
+
seats = v.get("availableSeatCount") or v.get("seatCount") or v.get("seats") if isinstance(v, dict) else None
|
|
888
|
+
_add_fare(k_str, node_cabin, p_amt, p_curr, seats)
|
|
889
|
+
|
|
890
|
+
# Eğer düğümün kendisi bir brand / paket nesnesiyse
|
|
891
|
+
has_brand_field = any(
|
|
892
|
+
bk in node
|
|
893
|
+
for bk in (
|
|
894
|
+
"brandCode", "brandName", "brandKey", "brandCodeList",
|
|
895
|
+
"subGroupName", "fareFamilyCode", "packageName", "package_name",
|
|
896
|
+
"brand", "tier", "tariffName",
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
if has_brand_field:
|
|
900
|
+
raw_b = (
|
|
901
|
+
node.get("brandName")
|
|
902
|
+
or node.get("brandKey")
|
|
903
|
+
or node.get("brandCode")
|
|
904
|
+
or node.get("brandCodeList")
|
|
905
|
+
or node.get("subGroupName")
|
|
906
|
+
or node.get("fareFamilyCode")
|
|
907
|
+
or node.get("packageName")
|
|
908
|
+
or node.get("package_name")
|
|
909
|
+
or node.get("brand")
|
|
910
|
+
or node.get("tier")
|
|
911
|
+
or node.get("tariffName")
|
|
912
|
+
)
|
|
913
|
+
p_amt, p_curr = _extract_amount_and_currency(
|
|
914
|
+
node.get("startingPrice")
|
|
915
|
+
or node.get("price")
|
|
916
|
+
or node.get("totalPrice")
|
|
917
|
+
or node.get("fare")
|
|
918
|
+
or node
|
|
919
|
+
)
|
|
920
|
+
seats = (
|
|
921
|
+
node.get("availableSeatCount")
|
|
922
|
+
or node.get("seatCount")
|
|
923
|
+
or node.get("availableSeats")
|
|
924
|
+
or node.get("seats")
|
|
925
|
+
)
|
|
926
|
+
_add_fare(raw_b, node_cabin, p_amt, p_curr, seats)
|
|
927
|
+
|
|
928
|
+
# Alt liste anahtarlarında sıralı paket listesi varsa
|
|
929
|
+
for list_key in (
|
|
930
|
+
"bookingPriceInfoList", "fareSubGroupList", "subGroupList",
|
|
931
|
+
"fares", "fareList", "brandList", "brands", "packages",
|
|
932
|
+
"packageList", "tariffs", "bundles", "priceOptions",
|
|
933
|
+
):
|
|
934
|
+
sub_list = node.get(list_key)
|
|
935
|
+
if isinstance(sub_list, list):
|
|
936
|
+
for idx_item, sub_item in enumerate(sub_list):
|
|
937
|
+
if isinstance(sub_item, dict):
|
|
938
|
+
raw_b = (
|
|
939
|
+
sub_item.get("brandName")
|
|
940
|
+
or sub_item.get("brandKey")
|
|
941
|
+
or sub_item.get("brandCode")
|
|
942
|
+
or sub_item.get("brandCodeList")
|
|
943
|
+
or sub_item.get("subGroupName")
|
|
944
|
+
or sub_item.get("fareFamilyCode")
|
|
945
|
+
or sub_item.get("packageName")
|
|
946
|
+
or sub_item.get("package_name")
|
|
947
|
+
or sub_item.get("name")
|
|
948
|
+
or sub_item.get("code")
|
|
949
|
+
)
|
|
950
|
+
item_cabin = str(sub_item.get("cabinClass") or sub_item.get("cabin") or node_cabin)
|
|
951
|
+
p_amt, p_curr = _extract_amount_and_currency(
|
|
952
|
+
sub_item.get("startingPrice")
|
|
953
|
+
or sub_item.get("price")
|
|
954
|
+
or sub_item.get("totalPrice")
|
|
955
|
+
or sub_item.get("fare")
|
|
956
|
+
or sub_item
|
|
957
|
+
)
|
|
958
|
+
seats = (
|
|
959
|
+
sub_item.get("availableSeatCount")
|
|
960
|
+
or sub_item.get("seatCount")
|
|
961
|
+
or sub_item.get("availableSeats")
|
|
962
|
+
or sub_item.get("seats")
|
|
963
|
+
)
|
|
964
|
+
_add_fare(raw_b, item_cabin, p_amt, p_curr, seats, idx_in_cabin=idx_item)
|
|
965
|
+
|
|
966
|
+
for k, v in node.items():
|
|
967
|
+
if k in ("flightSegmentList", "segments"):
|
|
968
|
+
continue
|
|
969
|
+
next_cabin = k if str(k).upper() in ("ECONOMY", "BUSINESS") else node_cabin
|
|
970
|
+
_scan_packages(v, next_cabin)
|
|
971
|
+
elif isinstance(node, list):
|
|
972
|
+
for elem in node:
|
|
973
|
+
_scan_packages(elem, current_cabin)
|
|
974
|
+
|
|
975
|
+
_scan_packages(f_opt, default_cabin)
|
|
976
|
+
|
|
977
|
+
# Eğer hiç paket bulunamadıysa ve f_opt üzerinde doğrudan bir fiyat varsa
|
|
978
|
+
if not fares_out and isinstance(f_opt, dict):
|
|
979
|
+
opt_amt, opt_curr = _extract_amount_and_currency(
|
|
980
|
+
f_opt.get("price")
|
|
981
|
+
or f_opt.get("cheapestPrice")
|
|
982
|
+
or f_opt.get("totalPrice")
|
|
983
|
+
or f_opt.get("startingPrice")
|
|
984
|
+
or f_opt.get("lowestPrice")
|
|
985
|
+
or f_opt.get("fare")
|
|
986
|
+
)
|
|
987
|
+
if opt_amt is not None and opt_amt > 0:
|
|
988
|
+
_add_fare("EcoFly", default_cabin, opt_amt, opt_curr, idx_in_cabin=0)
|
|
989
|
+
|
|
990
|
+
# Paketleri EcoFly -> ExtraFly -> PrimeFly -> Business sırasıyla ve fiyata göre diz
|
|
991
|
+
pkg_order = {"EcoFly": 0, "ExtraFly": 1, "PrimeFly": 2, "Business": 3}
|
|
992
|
+
fares_out.sort(key=lambda x: (pkg_order.get(x["package_name"], 99), x.get("amount") or 0.0))
|
|
993
|
+
return fares_out
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _looks_like_flight_segment_dict(d: dict[str, Any]) -> bool:
|
|
997
|
+
"""Bir sözlüğün uçuş segmenti veya uçuş seçeneği olup olmadığını yapısal olarak tespit eder."""
|
|
998
|
+
if not isinstance(d, dict):
|
|
999
|
+
return False
|
|
1000
|
+
if any(k in d for k in ("flightSegmentList", "fareCategory", "bookingPriceInfoList", "fareSubGroupList")):
|
|
1001
|
+
return True
|
|
1002
|
+
has_flight_id = any(k in d for k in ("flightNumber", "carrierFlightNumber", "flightNo", "flight_number", "flightCode", "marketingFlightNumber"))
|
|
1003
|
+
has_time = any(k in d for k in ("departureDateTime", "departureTime", "arrivalDateTime", "arrivalTime", "departure", "arrival"))
|
|
1004
|
+
has_route = any(k in d for k in ("originAirportCode", "departureAirportCode", "destinationAirportCode", "arrivalAirportCode"))
|
|
1005
|
+
has_price_or_pkg = any(k in d for k in ("packages", "fares", "brands", "price", "startingPrice", "EcoFly", "ecofly", "EF"))
|
|
1006
|
+
return bool((has_flight_id and (has_time or has_route or has_price_or_pkg)) or (has_time and has_price_or_pkg))
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _parse_single_flight_option(
|
|
1010
|
+
f_opt: dict[str, Any],
|
|
1011
|
+
fallback_orig: str = "",
|
|
1012
|
+
fallback_dest: str = "",
|
|
1013
|
+
fallback_packages: list[dict[str, Any]] | None = None,
|
|
1014
|
+
) -> dict[str, Any]:
|
|
1015
|
+
"""Tek bir uçuş seçeneği/segment düğümünden uçuş numarası, saatler, paketler ve en düşük fiyatı çıkarır."""
|
|
1016
|
+
seg_list = (
|
|
1017
|
+
f_opt.get("flightSegmentList")
|
|
1018
|
+
or f_opt.get("segments")
|
|
1019
|
+
or f_opt.get("legs")
|
|
1020
|
+
or f_opt.get("flightLegs")
|
|
1021
|
+
or []
|
|
1022
|
+
)
|
|
1023
|
+
flight_no = ""
|
|
1024
|
+
dep_time = ""
|
|
1025
|
+
arr_time = ""
|
|
1026
|
+
dur_str = ""
|
|
1027
|
+
orig_code = fallback_orig
|
|
1028
|
+
dest_code = fallback_dest
|
|
1029
|
+
is_direct = True
|
|
1030
|
+
|
|
1031
|
+
if isinstance(seg_list, list) and seg_list and isinstance(seg_list[0], dict):
|
|
1032
|
+
is_direct = len(seg_list) == 1
|
|
1033
|
+
first_seg = seg_list[0]
|
|
1034
|
+
last_seg = seg_list[-1] if isinstance(seg_list[-1], dict) else first_seg
|
|
1035
|
+
|
|
1036
|
+
fn = (
|
|
1037
|
+
first_seg.get("flightNumber")
|
|
1038
|
+
or first_seg.get("carrierFlightNumber")
|
|
1039
|
+
or first_seg.get("marketingFlightNumber")
|
|
1040
|
+
or first_seg.get("flightNo")
|
|
1041
|
+
or first_seg.get("flight_number")
|
|
1042
|
+
or first_seg.get("flightCode")
|
|
1043
|
+
or ""
|
|
1044
|
+
)
|
|
1045
|
+
op_air = first_seg.get("operatingAirline") or first_seg.get("marketingAirline") or {}
|
|
1046
|
+
carrier = (
|
|
1047
|
+
(op_air.get("code") if isinstance(op_air, dict) else None)
|
|
1048
|
+
or first_seg.get("carrierCode")
|
|
1049
|
+
or first_seg.get("airlineCode")
|
|
1050
|
+
or "TK"
|
|
1051
|
+
)
|
|
1052
|
+
fn_str = str(fn).strip()
|
|
1053
|
+
if fn_str:
|
|
1054
|
+
flight_no = fn_str if fn_str.upper().startswith(str(carrier).upper()) else f"{carrier} {fn_str}".strip()
|
|
1055
|
+
|
|
1056
|
+
dep_time = str(
|
|
1057
|
+
first_seg.get("departureDateTime")
|
|
1058
|
+
or first_seg.get("departureTime")
|
|
1059
|
+
or first_seg.get("departureDate")
|
|
1060
|
+
or first_seg.get("departure")
|
|
1061
|
+
or ""
|
|
1062
|
+
)
|
|
1063
|
+
arr_time = str(
|
|
1064
|
+
last_seg.get("arrivalDateTime")
|
|
1065
|
+
or last_seg.get("arrivalTime")
|
|
1066
|
+
or last_seg.get("arrivalDate")
|
|
1067
|
+
or last_seg.get("arrival")
|
|
1068
|
+
or ""
|
|
1069
|
+
)
|
|
1070
|
+
dur_str = str(
|
|
1071
|
+
f_opt.get("duration")
|
|
1072
|
+
or first_seg.get("duration")
|
|
1073
|
+
or first_seg.get("flightDuration")
|
|
1074
|
+
or first_seg.get("journeyDuration")
|
|
1075
|
+
or ""
|
|
1076
|
+
)
|
|
1077
|
+
orig_code = str(
|
|
1078
|
+
first_seg.get("originAirportCode")
|
|
1079
|
+
or first_seg.get("departureAirportCode")
|
|
1080
|
+
or (first_seg.get("departureAirport", {}) or {}).get("code")
|
|
1081
|
+
or first_seg.get("origin")
|
|
1082
|
+
or orig_code
|
|
1083
|
+
)
|
|
1084
|
+
dest_code = str(
|
|
1085
|
+
last_seg.get("destinationAirportCode")
|
|
1086
|
+
or last_seg.get("arrivalAirportCode")
|
|
1087
|
+
or (last_seg.get("arrivalAirport", {}) or {}).get("code")
|
|
1088
|
+
or last_seg.get("destination")
|
|
1089
|
+
or dest_code
|
|
1090
|
+
)
|
|
1091
|
+
else:
|
|
1092
|
+
fn = (
|
|
1093
|
+
f_opt.get("flightNumber")
|
|
1094
|
+
or f_opt.get("carrierFlightNumber")
|
|
1095
|
+
or f_opt.get("marketingFlightNumber")
|
|
1096
|
+
or f_opt.get("flightNo")
|
|
1097
|
+
or f_opt.get("flight_number")
|
|
1098
|
+
or f_opt.get("flightCode")
|
|
1099
|
+
or ""
|
|
1100
|
+
)
|
|
1101
|
+
op_air = f_opt.get("operatingAirline") or f_opt.get("marketingAirline") or {}
|
|
1102
|
+
carrier = (
|
|
1103
|
+
(op_air.get("code") if isinstance(op_air, dict) else None)
|
|
1104
|
+
or f_opt.get("carrierCode")
|
|
1105
|
+
or f_opt.get("airlineCode")
|
|
1106
|
+
or "TK"
|
|
1107
|
+
)
|
|
1108
|
+
fn_str = str(fn).strip()
|
|
1109
|
+
if fn_str:
|
|
1110
|
+
flight_no = fn_str if fn_str.upper().startswith(str(carrier).upper()) else f"{carrier} {fn_str}".strip()
|
|
1111
|
+
|
|
1112
|
+
dep_time = str(
|
|
1113
|
+
f_opt.get("departureDateTime")
|
|
1114
|
+
or f_opt.get("departureTime")
|
|
1115
|
+
or f_opt.get("departureDate")
|
|
1116
|
+
or f_opt.get("departure")
|
|
1117
|
+
or ""
|
|
1118
|
+
)
|
|
1119
|
+
arr_time = str(
|
|
1120
|
+
f_opt.get("arrivalDateTime")
|
|
1121
|
+
or f_opt.get("arrivalTime")
|
|
1122
|
+
or f_opt.get("arrivalDate")
|
|
1123
|
+
or f_opt.get("arrival")
|
|
1124
|
+
or ""
|
|
1125
|
+
)
|
|
1126
|
+
dur_str = str(f_opt.get("duration") or f_opt.get("flightDuration") or "")
|
|
1127
|
+
orig_code = str(
|
|
1128
|
+
f_opt.get("originAirportCode")
|
|
1129
|
+
or f_opt.get("departureAirportCode")
|
|
1130
|
+
or f_opt.get("origin")
|
|
1131
|
+
or orig_code
|
|
1132
|
+
)
|
|
1133
|
+
dest_code = str(
|
|
1134
|
+
f_opt.get("destinationAirportCode")
|
|
1135
|
+
or f_opt.get("arrivalAirportCode")
|
|
1136
|
+
or f_opt.get("destination")
|
|
1137
|
+
or dest_code
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
fares_out = _extract_packages_deep(f_opt)
|
|
1141
|
+
if not fares_out and fallback_packages:
|
|
1142
|
+
fares_out = [dict(p) for p in fallback_packages]
|
|
1143
|
+
|
|
1144
|
+
packages_by_name: dict[str, float] = {}
|
|
1145
|
+
for p in fares_out:
|
|
1146
|
+
pname = p.get("package_name")
|
|
1147
|
+
pamt = p.get("amount")
|
|
1148
|
+
if pname and pamt is not None:
|
|
1149
|
+
if pname not in packages_by_name or pamt < packages_by_name[pname]:
|
|
1150
|
+
packages_by_name[pname] = pamt
|
|
1151
|
+
|
|
1152
|
+
flight_amts = [p["amount"] for p in fares_out if p.get("amount") is not None and p["amount"] > 0]
|
|
1153
|
+
flight_min_price = min(flight_amts) if flight_amts else None
|
|
1154
|
+
flight_curr = fares_out[0]["currency"] if fares_out else "TRY"
|
|
1155
|
+
|
|
1156
|
+
return {
|
|
1157
|
+
"flight_number": flight_no or "TK Uçuşu",
|
|
1158
|
+
"origin": orig_code,
|
|
1159
|
+
"destination": dest_code,
|
|
1160
|
+
"departure": dep_time,
|
|
1161
|
+
"arrival": arr_time,
|
|
1162
|
+
"duration": dur_str,
|
|
1163
|
+
"is_direct": is_direct,
|
|
1164
|
+
"min_price": flight_min_price,
|
|
1165
|
+
"currency": flight_curr,
|
|
1166
|
+
"packages": fares_out,
|
|
1167
|
+
"packages_by_name": packages_by_name,
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def parse_thy_availability(data: dict[str, Any] | Any) -> dict[str, Any]:
|
|
1172
|
+
"""
|
|
1173
|
+
THY /api/v1/availability yanıtını derinlemesine (deep search) analiz eder.
|
|
1174
|
+
Standart anahtarlar ('originDestinationInformationList', 'originDestinationOptionList' vb.)
|
|
1175
|
+
bulunamasa veya farklı bir derinlikte/isimlendirmede gelse bile tüm JSON ağacını tarayarak:
|
|
1176
|
+
- Uçuş segmentlerini ve seferlerini,
|
|
1177
|
+
- EcoFly, ExtraFly, PrimeFly ve Business paket fiyatlarını,
|
|
1178
|
+
- Tüm keşfedilen fiyatları ('discovered_prices') ve en düşük fiyatı ('min_price' / 'lowest_price')
|
|
1179
|
+
kesin olarak yakalayıp döndürür.
|
|
1180
|
+
"""
|
|
1181
|
+
if not isinstance(data, (dict, list, str)):
|
|
1182
|
+
return {
|
|
1183
|
+
"success": False,
|
|
1184
|
+
"error": "Geçersiz yanıt formatı",
|
|
1185
|
+
"min_price": None,
|
|
1186
|
+
"lowest_price": None,
|
|
1187
|
+
"currency": "TRY",
|
|
1188
|
+
"packages": {},
|
|
1189
|
+
"segments": [],
|
|
1190
|
+
"flights": [],
|
|
1191
|
+
"discovered_prices": [],
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
root = _unwrap_thy_root(data)
|
|
1195
|
+
|
|
1196
|
+
# 1. Tüm JSON ağacında derinlemesine fiyat ve paket taraması (Deep Search)
|
|
1197
|
+
all_discovered_prices: list[dict[str, Any]] = []
|
|
1198
|
+
seen_price_paths: set[tuple[str, float]] = set()
|
|
1199
|
+
global_packages_candidates: list[dict[str, Any]] = _extract_packages_deep(root)
|
|
1200
|
+
|
|
1201
|
+
def _deep_search_prices(node: Any, path: str = "", parent_brand: str = "") -> None:
|
|
1202
|
+
if isinstance(node, dict):
|
|
1203
|
+
node_brand_raw = (
|
|
1204
|
+
node.get("brandName")
|
|
1205
|
+
or node.get("brandKey")
|
|
1206
|
+
or node.get("brandCode")
|
|
1207
|
+
or node.get("brandCodeList")
|
|
1208
|
+
or node.get("subGroupName")
|
|
1209
|
+
or node.get("fareFamilyCode")
|
|
1210
|
+
or node.get("packageName")
|
|
1211
|
+
or parent_brand
|
|
1212
|
+
)
|
|
1213
|
+
# Doğrudan fiyat taşıyan nesne mi?
|
|
1214
|
+
if any(k in node for k in ("amount", "totalAmount", "price", "totalPrice", "fare", "startingPrice", "baseFare", "totalFare", "fareAmount", "cashAmount", "lowestPrice", "minPrice")):
|
|
1215
|
+
a, c = _extract_amount_and_currency(node)
|
|
1216
|
+
if a is not None and a > 0:
|
|
1217
|
+
key_sig = (path, round(float(a), 2))
|
|
1218
|
+
if key_sig not in seen_price_paths:
|
|
1219
|
+
seen_price_paths.add(key_sig)
|
|
1220
|
+
pkg_tag = _normalize_thy_package_name(node_brand_raw) if node_brand_raw else None
|
|
1221
|
+
all_discovered_prices.append({
|
|
1222
|
+
"path": path or "root",
|
|
1223
|
+
"amount": round(float(a), 2),
|
|
1224
|
+
"currency": c or "TRY",
|
|
1225
|
+
"package_name": pkg_tag,
|
|
1226
|
+
})
|
|
1227
|
+
|
|
1228
|
+
for k, v in node.items():
|
|
1229
|
+
k_str = str(k)
|
|
1230
|
+
k_low = k_str.lower().strip()
|
|
1231
|
+
if k_low in _NON_PRICE_KEYS:
|
|
1232
|
+
continue
|
|
1233
|
+
child_path = f"{path}.{k_str}" if path else k_str
|
|
1234
|
+
inferred_brand = k_str if k_low in ("ecofly", "eco_fly", "extrafly", "extra_fly", "flexfly", "primefly", "prime_fly", "business", "ef", "xf", "fl", "pf", "bu") else (str(node_brand_raw) if node_brand_raw else "")
|
|
1235
|
+
|
|
1236
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool) and float(v) > 0:
|
|
1237
|
+
if any(pk in k_low for pk in ("price", "amount", "fare", "cost", "total", "tutar", "fiyat", "ecofly", "extrafly", "primefly", "business")) or k_str in ("EF", "XF", "FL", "PF", "BU"):
|
|
1238
|
+
key_sig = (child_path, round(float(v), 2))
|
|
1239
|
+
if key_sig not in seen_price_paths:
|
|
1240
|
+
seen_price_paths.add(key_sig)
|
|
1241
|
+
pkg_tag = _normalize_thy_package_name(inferred_brand) if inferred_brand else None
|
|
1242
|
+
all_discovered_prices.append({
|
|
1243
|
+
"path": child_path,
|
|
1244
|
+
"amount": round(float(v), 2),
|
|
1245
|
+
"currency": "TRY",
|
|
1246
|
+
"package_name": pkg_tag,
|
|
1247
|
+
})
|
|
1248
|
+
elif isinstance(v, str):
|
|
1249
|
+
if any(pk in k_low for pk in ("price", "amount", "fare", "cost", "total", "tutar", "fiyat", "ecofly", "extrafly", "primefly", "business")) or k_str in ("EF", "XF", "FL", "PF", "BU"):
|
|
1250
|
+
s_amt, s_curr = _parse_numeric_price_str(v)
|
|
1251
|
+
if s_amt is not None and s_amt > 0:
|
|
1252
|
+
key_sig = (child_path, round(float(s_amt), 2))
|
|
1253
|
+
if key_sig not in seen_price_paths:
|
|
1254
|
+
seen_price_paths.add(key_sig)
|
|
1255
|
+
pkg_tag = _normalize_thy_package_name(inferred_brand) if inferred_brand else None
|
|
1256
|
+
all_discovered_prices.append({
|
|
1257
|
+
"path": child_path,
|
|
1258
|
+
"amount": round(float(s_amt), 2),
|
|
1259
|
+
"currency": s_curr or "TRY",
|
|
1260
|
+
"package_name": pkg_tag,
|
|
1261
|
+
})
|
|
1262
|
+
elif isinstance(v, (dict, list)):
|
|
1263
|
+
_deep_search_prices(v, child_path, inferred_brand)
|
|
1264
|
+
elif isinstance(node, list):
|
|
1265
|
+
for i, elem in enumerate(node):
|
|
1266
|
+
_deep_search_prices(elem, f"{path}[{i}]", parent_brand)
|
|
1267
|
+
|
|
1268
|
+
_deep_search_prices(root)
|
|
1269
|
+
all_discovered_prices.sort(key=lambda x: x["amount"])
|
|
1270
|
+
|
|
1271
|
+
# 2. Rota / Segment listesini bul (önce standart anahtarlar, yoksa derin tarama)
|
|
1272
|
+
od_list: list[dict[str, Any]] = []
|
|
1273
|
+
if isinstance(root, dict):
|
|
1274
|
+
for k in (
|
|
1275
|
+
"originDestinationInformationList",
|
|
1276
|
+
"flightAvailabilityList",
|
|
1277
|
+
"journeys",
|
|
1278
|
+
"boundList",
|
|
1279
|
+
"itineraries",
|
|
1280
|
+
"sliceList",
|
|
1281
|
+
"slices",
|
|
1282
|
+
"routes",
|
|
1283
|
+
"segments",
|
|
1284
|
+
"originDestinationOptionList",
|
|
1285
|
+
"flightList",
|
|
1286
|
+
"flightOptionList",
|
|
1287
|
+
"flights",
|
|
1288
|
+
"options",
|
|
1289
|
+
):
|
|
1290
|
+
val = root.get(k)
|
|
1291
|
+
if isinstance(val, list) and val:
|
|
1292
|
+
if isinstance(val[0], dict) and _looks_like_flight_segment_dict(val[0]) and not any(
|
|
1293
|
+
ok in val[0] for ok in ("originDestinationOptionList", "flightOptionList", "flightList", "options", "flights")
|
|
1294
|
+
):
|
|
1295
|
+
od_list = [{"originDestinationOptionList": val, "originAirportCode": "IST", "destinationAirportCode": "ESB"}]
|
|
1296
|
+
else:
|
|
1297
|
+
od_list = val
|
|
1298
|
+
break
|
|
1299
|
+
|
|
1300
|
+
elif isinstance(root, list) and root:
|
|
1301
|
+
if isinstance(root[0], dict) and any(
|
|
1302
|
+
ok in root[0] for ok in ("originDestinationOptionList", "flightOptionList", "flightList", "options", "flights")
|
|
1303
|
+
):
|
|
1304
|
+
od_list = root
|
|
1305
|
+
elif isinstance(root[0], dict) and _looks_like_flight_segment_dict(root[0]):
|
|
1306
|
+
od_list = [{"originDestinationOptionList": root, "originAirportCode": "IST", "destinationAirportCode": "ESB"}]
|
|
1307
|
+
|
|
1308
|
+
air = root.get("availabilityInfoResponse") if isinstance(root, dict) else None
|
|
1309
|
+
if not od_list and isinstance(air, list) and air:
|
|
1310
|
+
for idx, item in enumerate(air):
|
|
1311
|
+
if isinstance(item, list) and item:
|
|
1312
|
+
od_list.append({
|
|
1313
|
+
"originDestinationOptionList": item,
|
|
1314
|
+
"originAirportCode": "IST" if idx == 0 else "ESB",
|
|
1315
|
+
"destinationAirportCode": "ESB" if idx == 0 else "IST",
|
|
1316
|
+
})
|
|
1317
|
+
elif isinstance(item, dict):
|
|
1318
|
+
od_list.append(item)
|
|
1319
|
+
|
|
1320
|
+
# Eğer standart anahtarlarla od_list bulunamadıysa tüm JSON ağacında derinlemesine OD / uçuş listesi ara
|
|
1321
|
+
if not od_list:
|
|
1322
|
+
deep_flight_nodes: list[dict[str, Any]] = []
|
|
1323
|
+
seen_node_ids: set[int] = set()
|
|
1324
|
+
|
|
1325
|
+
def _deep_find_flights(node: Any) -> None:
|
|
1326
|
+
if isinstance(node, dict):
|
|
1327
|
+
# Eğer düğüm altında bir uçuş seçeneği listesi varsa
|
|
1328
|
+
for k, v in node.items():
|
|
1329
|
+
if isinstance(v, list) and v and all(isinstance(x, dict) for x in v):
|
|
1330
|
+
if any(_looks_like_flight_segment_dict(x) for x in v):
|
|
1331
|
+
if id(node) not in seen_node_ids:
|
|
1332
|
+
seen_node_ids.add(id(node))
|
|
1333
|
+
od_list.append({
|
|
1334
|
+
"originAirportCode": node.get("originAirportCode") or node.get("origin") or node.get("departureAirportCode") or "",
|
|
1335
|
+
"destinationAirportCode": node.get("destinationAirportCode") or node.get("destination") or node.get("arrivalAirportCode") or "",
|
|
1336
|
+
"departureDate": node.get("departureDate") or node.get("date") or "",
|
|
1337
|
+
"originDestinationOptionList": v,
|
|
1338
|
+
})
|
|
1339
|
+
return
|
|
1340
|
+
if _looks_like_flight_segment_dict(node) and id(node) not in seen_node_ids:
|
|
1341
|
+
seen_node_ids.add(id(node))
|
|
1342
|
+
deep_flight_nodes.append(node)
|
|
1343
|
+
return
|
|
1344
|
+
for k, v in node.items():
|
|
1345
|
+
_deep_find_flights(v)
|
|
1346
|
+
elif isinstance(node, list):
|
|
1347
|
+
for elem in node:
|
|
1348
|
+
_deep_find_flights(elem)
|
|
1349
|
+
|
|
1350
|
+
_deep_find_flights(root)
|
|
1351
|
+
if not od_list and deep_flight_nodes:
|
|
1352
|
+
od_list = [{
|
|
1353
|
+
"originAirportCode": deep_flight_nodes[0].get("originAirportCode") or deep_flight_nodes[0].get("departureAirportCode") or deep_flight_nodes[0].get("origin") or "IST",
|
|
1354
|
+
"destinationAirportCode": deep_flight_nodes[0].get("destinationAirportCode") or deep_flight_nodes[0].get("arrivalAirportCode") or deep_flight_nodes[0].get("destination") or "ESB",
|
|
1355
|
+
"departureDate": deep_flight_nodes[0].get("departureDate") or deep_flight_nodes[0].get("date") or "",
|
|
1356
|
+
"originDestinationOptionList": deep_flight_nodes,
|
|
1357
|
+
}]
|
|
1358
|
+
|
|
1359
|
+
root_econ_amt, root_econ_curr = _extract_amount_and_currency(root.get("economyStartingPrice") if isinstance(root, dict) else None)
|
|
1360
|
+
root_bus_amt, root_bus_curr = _extract_amount_and_currency(root.get("businessStartingPrice") if isinstance(root, dict) else None)
|
|
1361
|
+
|
|
1362
|
+
segments_out: list[dict[str, Any]] = []
|
|
1363
|
+
|
|
1364
|
+
for idx, od in enumerate(od_list):
|
|
1365
|
+
if not isinstance(od, dict):
|
|
1366
|
+
continue
|
|
1367
|
+
direction = "GİDİŞ" if idx == 0 else ("DÖNÜŞ" if idx == 1 else f"SEGMENT_{idx+1}")
|
|
1368
|
+
orig = str(
|
|
1369
|
+
od.get("originAirportCode")
|
|
1370
|
+
or od.get("departureAirportCode")
|
|
1371
|
+
or od.get("originCityCode")
|
|
1372
|
+
or od.get("origin")
|
|
1373
|
+
or ("IST" if idx == 0 else ("ESB" if idx == 1 else ""))
|
|
1374
|
+
)
|
|
1375
|
+
dest = str(
|
|
1376
|
+
od.get("destinationAirportCode")
|
|
1377
|
+
or od.get("arrivalAirportCode")
|
|
1378
|
+
or od.get("destinationCityCode")
|
|
1379
|
+
or od.get("destination")
|
|
1380
|
+
or ("ESB" if idx == 0 else ("IST" if idx == 1 else ""))
|
|
1381
|
+
)
|
|
1382
|
+
date_val = str(od.get("departureDate") or od.get("arrivalDate") or od.get("date") or "")
|
|
1383
|
+
|
|
1384
|
+
econ_amt, econ_curr = _extract_amount_and_currency(od.get("economyStartingPrice"))
|
|
1385
|
+
if econ_amt is None:
|
|
1386
|
+
econ_amt, econ_curr = root_econ_amt, root_econ_curr
|
|
1387
|
+
bus_amt, bus_curr = _extract_amount_and_currency(od.get("businessStartingPrice"))
|
|
1388
|
+
if bus_amt is None:
|
|
1389
|
+
bus_amt, bus_curr = root_bus_amt, root_bus_curr
|
|
1390
|
+
|
|
1391
|
+
options = (
|
|
1392
|
+
od.get("originDestinationOptionList")
|
|
1393
|
+
or od.get("flightOptionList")
|
|
1394
|
+
or od.get("flightList")
|
|
1395
|
+
or od.get("options")
|
|
1396
|
+
or od.get("flights")
|
|
1397
|
+
or od.get("itineraries")
|
|
1398
|
+
or od.get("items")
|
|
1399
|
+
or []
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
# Eğer options standart anahtarlarda yoksa od içindeki alt dizileri derinlemesine tara
|
|
1403
|
+
if not options:
|
|
1404
|
+
for k, v in od.items():
|
|
1405
|
+
if isinstance(v, list) and v and isinstance(v[0], dict) and _looks_like_flight_segment_dict(v[0]):
|
|
1406
|
+
options = v
|
|
1407
|
+
break
|
|
1408
|
+
|
|
1409
|
+
# Eğer od'nin kendisi doğrudan bir uçuş nesnesiyse
|
|
1410
|
+
if not options and _looks_like_flight_segment_dict(od):
|
|
1411
|
+
options = [od]
|
|
1412
|
+
|
|
1413
|
+
od_fallback_packages = _extract_packages_deep(od)
|
|
1414
|
+
flights_out: list[dict[str, Any]] = []
|
|
1415
|
+
|
|
1416
|
+
if isinstance(options, list):
|
|
1417
|
+
for f_opt in options:
|
|
1418
|
+
if not isinstance(f_opt, dict):
|
|
1419
|
+
continue
|
|
1420
|
+
parsed_fl = _parse_single_flight_option(
|
|
1421
|
+
f_opt,
|
|
1422
|
+
fallback_orig=orig,
|
|
1423
|
+
fallback_dest=dest,
|
|
1424
|
+
fallback_packages=od_fallback_packages or global_packages_candidates,
|
|
1425
|
+
)
|
|
1426
|
+
flights_out.append(parsed_fl)
|
|
1427
|
+
|
|
1428
|
+
# Segment bazında paket özetini (EcoFly, ExtraFly, PrimeFly, Business) derle
|
|
1429
|
+
seg_pkg_summary: dict[str, dict[str, Any]] = {}
|
|
1430
|
+
for fl in flights_out:
|
|
1431
|
+
for p in fl.get("packages", []):
|
|
1432
|
+
pname = p.get("package_name")
|
|
1433
|
+
pamt = p.get("amount")
|
|
1434
|
+
pcurr = p.get("currency", "TRY")
|
|
1435
|
+
if pname and pamt is not None and pamt > 0:
|
|
1436
|
+
if pname not in seg_pkg_summary or pamt < seg_pkg_summary[pname]["amount"]:
|
|
1437
|
+
seg_pkg_summary[pname] = {"amount": pamt, "currency": pcurr}
|
|
1438
|
+
|
|
1439
|
+
for p in od_fallback_packages:
|
|
1440
|
+
pname = p.get("package_name")
|
|
1441
|
+
pamt = p.get("amount")
|
|
1442
|
+
pcurr = p.get("currency", "TRY")
|
|
1443
|
+
if pname and pamt is not None and pamt > 0:
|
|
1444
|
+
if pname not in seg_pkg_summary or pamt < seg_pkg_summary[pname]["amount"]:
|
|
1445
|
+
seg_pkg_summary[pname] = {"amount": pamt, "currency": pcurr}
|
|
1446
|
+
|
|
1447
|
+
if econ_amt is not None and econ_amt > 0 and "EcoFly" not in seg_pkg_summary:
|
|
1448
|
+
seg_pkg_summary["EcoFly"] = {"amount": round(float(econ_amt), 2), "currency": econ_curr or "TRY"}
|
|
1449
|
+
if bus_amt is not None and bus_amt > 0 and "Business" not in seg_pkg_summary:
|
|
1450
|
+
seg_pkg_summary["Business"] = {"amount": round(float(bus_amt), 2), "currency": bus_curr or "TRY"}
|
|
1451
|
+
|
|
1452
|
+
# Eğer uçuş listesi kesilmiş/boş kalmış ama başlangıç veya paket fiyatları mevcutsa özet uçuş kaydı ekle
|
|
1453
|
+
if not flights_out and seg_pkg_summary:
|
|
1454
|
+
synth_pkgs = [
|
|
1455
|
+
{
|
|
1456
|
+
"package_name": pk_name,
|
|
1457
|
+
"cabin_class": "BUSINESS" if pk_name == "Business" else "ECONOMY",
|
|
1458
|
+
"amount": pk_info["amount"],
|
|
1459
|
+
"currency": pk_info["currency"],
|
|
1460
|
+
"available_seats": None,
|
|
1461
|
+
}
|
|
1462
|
+
for pk_name, pk_info in seg_pkg_summary.items()
|
|
1463
|
+
]
|
|
1464
|
+
synth_amts = [p["amount"] for p in synth_pkgs if p.get("amount")]
|
|
1465
|
+
flights_out.append({
|
|
1466
|
+
"flight_number": "TK Uçuşu",
|
|
1467
|
+
"origin": orig or "IST",
|
|
1468
|
+
"destination": dest or "ESB",
|
|
1469
|
+
"departure": date_val,
|
|
1470
|
+
"arrival": date_val,
|
|
1471
|
+
"duration": "",
|
|
1472
|
+
"is_direct": True,
|
|
1473
|
+
"min_price": min(synth_amts) if synth_amts else None,
|
|
1474
|
+
"currency": synth_pkgs[0]["currency"] if synth_pkgs else "TRY",
|
|
1475
|
+
"packages": synth_pkgs,
|
|
1476
|
+
"packages_by_name": {p["package_name"]: p["amount"] for p in synth_pkgs},
|
|
1477
|
+
})
|
|
1478
|
+
|
|
1479
|
+
all_amts = [
|
|
1480
|
+
f["amount"]
|
|
1481
|
+
for fl in flights_out
|
|
1482
|
+
for f in fl.get("packages", [])
|
|
1483
|
+
if f.get("amount") is not None and f["amount"] > 0
|
|
1484
|
+
]
|
|
1485
|
+
if econ_amt is not None and econ_amt > 0:
|
|
1486
|
+
all_amts.append(econ_amt)
|
|
1487
|
+
if bus_amt is not None and bus_amt > 0:
|
|
1488
|
+
all_amts.append(bus_amt)
|
|
1489
|
+
for pk_info in seg_pkg_summary.values():
|
|
1490
|
+
if pk_info.get("amount"):
|
|
1491
|
+
all_amts.append(pk_info["amount"])
|
|
1492
|
+
|
|
1493
|
+
min_price = round(min(all_amts), 2) if all_amts else None
|
|
1494
|
+
curr_code = (
|
|
1495
|
+
econ_curr
|
|
1496
|
+
or bus_curr
|
|
1497
|
+
or (flights_out[0]["packages"][0]["currency"] if flights_out and flights_out[0].get("packages") else "TRY")
|
|
1498
|
+
)
|
|
1499
|
+
|
|
1500
|
+
segments_out.append({
|
|
1501
|
+
"direction": direction,
|
|
1502
|
+
"origin": orig,
|
|
1503
|
+
"destination": dest,
|
|
1504
|
+
"date": date_val,
|
|
1505
|
+
"min_price": min_price,
|
|
1506
|
+
"lowest_price": min_price,
|
|
1507
|
+
"currency": curr_code,
|
|
1508
|
+
"economy_starting_price": econ_amt,
|
|
1509
|
+
"business_starting_price": bus_amt,
|
|
1510
|
+
"packages": seg_pkg_summary,
|
|
1511
|
+
"flights": flights_out,
|
|
1512
|
+
})
|
|
1513
|
+
|
|
1514
|
+
# 3. Eğer od_list hiç bulunamadıysa fakat derin aramada fiyatlar veya paketler yakalandıysa sentetik segment oluştur
|
|
1515
|
+
if not segments_out and (global_packages_candidates or all_discovered_prices or root_econ_amt or root_bus_amt):
|
|
1516
|
+
synth_pkgs_list: list[dict[str, Any]] = list(global_packages_candidates)
|
|
1517
|
+
if not synth_pkgs_list:
|
|
1518
|
+
for dp in all_discovered_prices:
|
|
1519
|
+
pname = dp.get("package_name") or "EcoFly"
|
|
1520
|
+
synth_pkgs_list.append({
|
|
1521
|
+
"package_name": pname,
|
|
1522
|
+
"cabin_class": "BUSINESS" if pname == "Business" else "ECONOMY",
|
|
1523
|
+
"amount": dp["amount"],
|
|
1524
|
+
"currency": dp["currency"],
|
|
1525
|
+
"available_seats": None,
|
|
1526
|
+
})
|
|
1527
|
+
|
|
1528
|
+
seg_pkg_map: dict[str, dict[str, Any]] = {}
|
|
1529
|
+
for p in synth_pkgs_list:
|
|
1530
|
+
pname = p["package_name"]
|
|
1531
|
+
pamt = p["amount"]
|
|
1532
|
+
if pname not in seg_pkg_map or pamt < seg_pkg_map[pname]["amount"]:
|
|
1533
|
+
seg_pkg_map[pname] = {"amount": pamt, "currency": p.get("currency", "TRY")}
|
|
1534
|
+
|
|
1535
|
+
all_candidate_amts = [dp["amount"] for dp in all_discovered_prices if dp.get("amount")] + [
|
|
1536
|
+
p["amount"] for p in synth_pkgs_list if p.get("amount")
|
|
1537
|
+
]
|
|
1538
|
+
if root_econ_amt:
|
|
1539
|
+
all_candidate_amts.append(root_econ_amt)
|
|
1540
|
+
if root_bus_amt:
|
|
1541
|
+
all_candidate_amts.append(root_bus_amt)
|
|
1542
|
+
seg_min = round(min(all_candidate_amts), 2) if all_candidate_amts else None
|
|
1543
|
+
seg_curr = (
|
|
1544
|
+
all_discovered_prices[0]["currency"]
|
|
1545
|
+
if all_discovered_prices
|
|
1546
|
+
else (root_econ_curr or root_bus_curr or "TRY")
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
segments_out.append({
|
|
1550
|
+
"direction": "GİDİŞ",
|
|
1551
|
+
"origin": "IST",
|
|
1552
|
+
"destination": "ESB",
|
|
1553
|
+
"date": "",
|
|
1554
|
+
"min_price": seg_min,
|
|
1555
|
+
"lowest_price": seg_min,
|
|
1556
|
+
"currency": seg_curr,
|
|
1557
|
+
"economy_starting_price": root_econ_amt or seg_min,
|
|
1558
|
+
"business_starting_price": root_bus_amt,
|
|
1559
|
+
"packages": seg_pkg_map,
|
|
1560
|
+
"flights": [
|
|
1561
|
+
{
|
|
1562
|
+
"flight_number": "TK Uçuşu",
|
|
1563
|
+
"origin": "IST",
|
|
1564
|
+
"destination": "ESB",
|
|
1565
|
+
"departure": "",
|
|
1566
|
+
"arrival": "",
|
|
1567
|
+
"duration": "",
|
|
1568
|
+
"is_direct": True,
|
|
1569
|
+
"min_price": seg_min,
|
|
1570
|
+
"currency": seg_curr,
|
|
1571
|
+
"packages": synth_pkgs_list,
|
|
1572
|
+
"packages_by_name": {k: v["amount"] for k, v in seg_pkg_map.items()},
|
|
1573
|
+
}
|
|
1574
|
+
] if synth_pkgs_list else [],
|
|
1575
|
+
})
|
|
1576
|
+
|
|
1577
|
+
# 4. Küresel paket özeti (EcoFly, ExtraFly, PrimeFly, Business) ve genel en düşük fiyat (min_price / lowest_price)
|
|
1578
|
+
global_packages_summary: dict[str, dict[str, Any]] = {}
|
|
1579
|
+
for seg in segments_out:
|
|
1580
|
+
for pk_name, pk_val in (seg.get("packages") or {}).items():
|
|
1581
|
+
amt = pk_val.get("amount") if isinstance(pk_val, dict) else pk_val
|
|
1582
|
+
curr = pk_val.get("currency", seg.get("currency", "TRY")) if isinstance(pk_val, dict) else seg.get("currency", "TRY")
|
|
1583
|
+
if amt is not None and amt > 0:
|
|
1584
|
+
if pk_name not in global_packages_summary or amt < global_packages_summary[pk_name]["amount"]:
|
|
1585
|
+
global_packages_summary[pk_name] = {"amount": round(float(amt), 2), "currency": curr}
|
|
1586
|
+
|
|
1587
|
+
for p in global_packages_candidates:
|
|
1588
|
+
pname = p.get("package_name")
|
|
1589
|
+
pamt = p.get("amount")
|
|
1590
|
+
pcurr = p.get("currency", "TRY")
|
|
1591
|
+
if pname and pamt is not None and pamt > 0:
|
|
1592
|
+
if pname not in global_packages_summary or pamt < global_packages_summary[pname]["amount"]:
|
|
1593
|
+
global_packages_summary[pname] = {"amount": round(float(pamt), 2), "currency": pcurr}
|
|
1594
|
+
|
|
1595
|
+
for dp in all_discovered_prices:
|
|
1596
|
+
pname = dp.get("package_name")
|
|
1597
|
+
pamt = dp.get("amount")
|
|
1598
|
+
pcurr = dp.get("currency", "TRY")
|
|
1599
|
+
if pname and pamt is not None and pamt > 0:
|
|
1600
|
+
if pname not in global_packages_summary or pamt < global_packages_summary[pname]["amount"]:
|
|
1601
|
+
global_packages_summary[pname] = {"amount": round(float(pamt), 2), "currency": pcurr}
|
|
1602
|
+
|
|
1603
|
+
all_global_amounts: list[float] = [
|
|
1604
|
+
seg["min_price"] for seg in segments_out if seg.get("min_price") is not None and seg["min_price"] > 0
|
|
1605
|
+
] + [
|
|
1606
|
+
dp["amount"] for dp in all_discovered_prices if dp.get("amount") is not None and dp["amount"] > 0
|
|
1607
|
+
] + [
|
|
1608
|
+
pk["amount"] for pk in global_packages_summary.values() if pk.get("amount") is not None and pk["amount"] > 0
|
|
1609
|
+
]
|
|
1610
|
+
|
|
1611
|
+
overall_min_price = round(min(all_global_amounts), 2) if all_global_amounts else None
|
|
1612
|
+
overall_currency = (
|
|
1613
|
+
segments_out[0].get("currency")
|
|
1614
|
+
if segments_out and segments_out[0].get("currency")
|
|
1615
|
+
else (all_discovered_prices[0]["currency"] if all_discovered_prices else "TRY")
|
|
1616
|
+
)
|
|
1617
|
+
|
|
1618
|
+
if overall_min_price is not None and "EcoFly" not in global_packages_summary:
|
|
1619
|
+
global_packages_summary["EcoFly"] = {"amount": overall_min_price, "currency": overall_currency}
|
|
1620
|
+
|
|
1621
|
+
all_flights_flat = [fl for seg in segments_out for fl in seg.get("flights", [])]
|
|
1622
|
+
|
|
1623
|
+
has_show_banner = isinstance(root, dict) and "showBanner" in root
|
|
1624
|
+
is_success = isinstance(data, dict) and data.get("success") is True
|
|
1625
|
+
if has_show_banner or (is_success and not segments_out and not all_discovered_prices):
|
|
1626
|
+
print("\n" + "=" * 80)
|
|
1627
|
+
if has_show_banner:
|
|
1628
|
+
print("⚠️ [Shadow-API] 'showBanner' içeren yanıt tespit edildi (/api/v1/availability yerine alt servis yanıtı)!")
|
|
1629
|
+
else:
|
|
1630
|
+
print("⚠️ [Shadow-API] API 'success: True' döndürdü ancak hiçbir uçuş segmenti veya fiyatı ayrıştırılamadı!")
|
|
1631
|
+
print("API Yanıtı ('data' / tam JSON gövdesi):")
|
|
1632
|
+
try:
|
|
1633
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
1634
|
+
except Exception:
|
|
1635
|
+
print(str(data)[:2000])
|
|
1636
|
+
print("=" * 80 + "\n")
|
|
1637
|
+
|
|
1638
|
+
return {
|
|
1639
|
+
"success": bool(segments_out or all_discovered_prices or overall_min_price is not None),
|
|
1640
|
+
"min_price": overall_min_price,
|
|
1641
|
+
"lowest_price": overall_min_price,
|
|
1642
|
+
"currency": overall_currency,
|
|
1643
|
+
"packages": global_packages_summary,
|
|
1644
|
+
"ecofly_min_price": global_packages_summary.get("EcoFly", {}).get("amount"),
|
|
1645
|
+
"extrafly_min_price": global_packages_summary.get("ExtraFly", {}).get("amount"),
|
|
1646
|
+
"primefly_min_price": global_packages_summary.get("PrimeFly", {}).get("amount"),
|
|
1647
|
+
"business_min_price": global_packages_summary.get("Business", {}).get("amount"),
|
|
1648
|
+
"segments": segments_out,
|
|
1649
|
+
"flights": all_flights_flat,
|
|
1650
|
+
"discovered_prices": all_discovered_prices,
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
def print_availability_summary(parsed: dict[str, Any], elapsed_ms: float = 0.0) -> None:
|
|
1655
|
+
"""Ayrıştırılmış uçuş ve fiyat verilerini terminale ve ajana zengin, anlaşılır bir formatta yansıtır."""
|
|
1656
|
+
print("\n" + "=" * 80)
|
|
1657
|
+
print("✈ TÜRK HAVA YOLLARI (THY) UÇUŞ MÜSAİTLİK VE FİYAT RAPORU")
|
|
1658
|
+
if elapsed_ms > 0:
|
|
1659
|
+
print(f"⏱ Yanıt Süresi: {elapsed_ms:.2f} ms")
|
|
1660
|
+
print("=" * 80)
|
|
1661
|
+
|
|
1662
|
+
segments = parsed.get("segments", [])
|
|
1663
|
+
if not segments:
|
|
1664
|
+
discovered = parsed.get("discovered_prices", [])
|
|
1665
|
+
if discovered:
|
|
1666
|
+
print("✔ Uçuş Fiyat Verileri (Derin Tarama ile Bulundu):")
|
|
1667
|
+
for item in discovered[:12]:
|
|
1668
|
+
print(f" • {item['path']}: {item['amount']:,.2f} {item['currency']}")
|
|
1669
|
+
else:
|
|
1670
|
+
print("⚠️ Yapılandırılmış uçuş veya fiyat verisi tespit edilemedi.")
|
|
1671
|
+
print("=" * 80 + "\n")
|
|
1672
|
+
return
|
|
1673
|
+
|
|
1674
|
+
for seg in segments:
|
|
1675
|
+
dir_label = seg.get("direction", "UÇUŞ")
|
|
1676
|
+
icon = "🟢" if "GİDİŞ" in dir_label else ("🔵" if "DÖNÜŞ" in dir_label else "✈")
|
|
1677
|
+
orig = seg.get("origin", "")
|
|
1678
|
+
dest = seg.get("destination", "")
|
|
1679
|
+
date_str = seg.get("date", "")
|
|
1680
|
+
curr = seg.get("currency", "TRY")
|
|
1681
|
+
min_p = seg.get("min_price")
|
|
1682
|
+
|
|
1683
|
+
print(f"\n{icon} [{dir_label}] {orig} ➔ {dest} | Tarih: {date_str}")
|
|
1684
|
+
if min_p is not None:
|
|
1685
|
+
print(f" 💰 En Düşük Fiyat: {min_p:,.2f} {curr}")
|
|
1686
|
+
econ = seg.get("economy_starting_price")
|
|
1687
|
+
bus = seg.get("business_starting_price")
|
|
1688
|
+
price_tags: list[str] = []
|
|
1689
|
+
if econ is not None:
|
|
1690
|
+
price_tags.append(f"Economy Başlangıç: {econ:,.2f} {curr}")
|
|
1691
|
+
if bus is not None:
|
|
1692
|
+
price_tags.append(f"Business Başlangıç: {bus:,.2f} {curr}")
|
|
1693
|
+
if price_tags:
|
|
1694
|
+
print(f" ℹ {' | '.join(price_tags)}")
|
|
1695
|
+
|
|
1696
|
+
flights = seg.get("flights", [])
|
|
1697
|
+
if flights:
|
|
1698
|
+
print(" " + "-" * 74)
|
|
1699
|
+
for f_idx, fl in enumerate(flights, start=1):
|
|
1700
|
+
f_no = fl.get("flight_number", "Uçuş")
|
|
1701
|
+
raw_dep = fl.get("departure", "")
|
|
1702
|
+
raw_arr = fl.get("arrival", "")
|
|
1703
|
+
dep = raw_dep[-8:-3] if len(raw_dep) >= 16 else raw_dep
|
|
1704
|
+
arr = raw_arr[-8:-3] if len(raw_arr) >= 16 else raw_arr
|
|
1705
|
+
dur = fl.get("duration", "")
|
|
1706
|
+
mode = "Direkt" if fl.get("is_direct") else "Aktarmalı"
|
|
1707
|
+
time_info = f"{dep} ➔ {arr}" if dep and arr else ""
|
|
1708
|
+
meta = " | ".join(p for p in (time_info, f"Süre: {dur}" if dur else "", mode) if p)
|
|
1709
|
+
|
|
1710
|
+
print(f" {f_idx}. {f_no} ({meta})")
|
|
1711
|
+
packages = fl.get("packages", [])
|
|
1712
|
+
for pkg in packages:
|
|
1713
|
+
p_name = pkg.get("package_name", "Paket")
|
|
1714
|
+
p_amt = pkg.get("amount")
|
|
1715
|
+
p_curr = pkg.get("currency", "TRY")
|
|
1716
|
+
seats = pkg.get("available_seats")
|
|
1717
|
+
seat_info = f" ({seats} koltuk)" if seats else ""
|
|
1718
|
+
if p_amt is not None:
|
|
1719
|
+
print(f" • {p_name:<16}: {p_amt:>10,.2f} {p_curr}{seat_info}")
|
|
1720
|
+
print(" " + "-" * 74)
|
|
1721
|
+
|
|
1722
|
+
print("\n" + "=" * 80 + "\n")
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
class ThyClient(BaseShadowClient):
|
|
1726
|
+
"""THY servisi için OwnAPI User-Delegated Browser Middleware istemcisi."""
|
|
1727
|
+
|
|
1728
|
+
def __init__(
|
|
1729
|
+
self,
|
|
1730
|
+
tenant_id: str = "default",
|
|
1731
|
+
token_store: TokenStore | None = None,
|
|
1732
|
+
mode: str = "local",
|
|
1733
|
+
) -> None:
|
|
1734
|
+
super().__init__(
|
|
1735
|
+
service_name="thy",
|
|
1736
|
+
base_url=THY_HOME_URL,
|
|
1737
|
+
tenant_id=tenant_id,
|
|
1738
|
+
token_store=token_store,
|
|
1739
|
+
mode=mode,
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
def search_flights(
|
|
1743
|
+
self,
|
|
1744
|
+
departure_date: str = '02-03-2027',
|
|
1745
|
+
return_date: str | None = '09-03-2027',
|
|
1746
|
+
origin_airport_code: str = 'IST',
|
|
1747
|
+
destination_airport_code: str = 'ESB',
|
|
1748
|
+
cid: str | None = None,
|
|
1749
|
+
) -> dict[str, Any]:
|
|
1750
|
+
return search_flights(
|
|
1751
|
+
departure_date=departure_date,
|
|
1752
|
+
return_date=return_date,
|
|
1753
|
+
origin_airport_code=origin_airport_code,
|
|
1754
|
+
destination_airport_code=destination_airport_code,
|
|
1755
|
+
cid=cid,
|
|
1756
|
+
tenant_id=self.tenant_id,
|
|
1757
|
+
mode=self.mode,
|
|
1758
|
+
non_interactive=self.non_interactive,
|
|
1759
|
+
)
|
|
1760
|
+
|
|
1761
|
+
def search(
|
|
1762
|
+
self,
|
|
1763
|
+
departure_date: str = '02-03-2027',
|
|
1764
|
+
return_date: str | None = '09-03-2027',
|
|
1765
|
+
origin_airport_code: str = 'IST',
|
|
1766
|
+
destination_airport_code: str = 'ESB',
|
|
1767
|
+
cid: str | None = None,
|
|
1768
|
+
) -> dict[str, Any]:
|
|
1769
|
+
return self.search_flights(
|
|
1770
|
+
departure_date=departure_date,
|
|
1771
|
+
return_date=return_date,
|
|
1772
|
+
origin_airport_code=origin_airport_code,
|
|
1773
|
+
destination_airport_code=destination_airport_code,
|
|
1774
|
+
cid=cid,
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
THYClient = ThyClient
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
if __name__ == "__main__":
|
|
1782
|
+
start_ns = time.perf_counter()
|
|
1783
|
+
data = search_flights()
|
|
1784
|
+
elapsed_ms = (time.perf_counter() - start_ns) * 1000
|
|
1785
|
+
|
|
1786
|
+
if isinstance(data, dict):
|
|
1787
|
+
parsed = data.get("parsed") or parse_thy_availability(data)
|
|
1788
|
+
print_availability_summary(parsed, elapsed_ms=elapsed_ms)
|
|
1789
|
+
if not parsed.get("segments") and not parsed.get("discovered_prices"):
|
|
1790
|
+
print(f"[Shadow-API] Üst Düzey Anahtarlar: {list(data.keys())}")
|
|
1791
|
+
if "data" in data and isinstance(data["data"], dict):
|
|
1792
|
+
print(f"[Shadow-API] Data Alt Anahtarlar: {list(data['data'].keys())[:10]}")
|
|
1793
|
+
print(json.dumps(data, ensure_ascii=False, indent=2)[:800])
|
|
1794
|
+
else:
|
|
1795
|
+
print(f"[Shadow-API] Yanıt Süresi: {elapsed_ms:.2f} ms")
|
|
1796
|
+
print(str(data)[:600])
|