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,241 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import pathlib
5
+ import re
6
+ import sys
7
+ from html import unescape
8
+ from typing import Any
9
+ from urllib.parse import quote_plus
10
+
11
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))
12
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
13
+
14
+ from core.base_client import BaseOwnClient
15
+ from core.token_store import TokenStore
16
+
17
+
18
+ def _clean_html_text(raw: str) -> str:
19
+ """HTML etiketlerini ve fazla boşlukları temizler."""
20
+ text = unescape(re.sub(r"<[^>]+>", " ", raw or ""))
21
+ return re.sub(r"\s+", " ", text).strip()
22
+
23
+
24
+ def _parse_linkedin_job_cards(raw_html_or_text: str) -> list[dict[str, Any]]:
25
+ """
26
+ `seeMoreJobPostings/search` veya LinkedIn iş arama yanıtındaki TÜM iş ilanı kartlarını
27
+ (`<li>`, `.base-card`, `.job-search-card`) döngüyle ayıklar.
28
+ """
29
+ jobs: list[dict[str, Any]] = []
30
+ if not raw_html_or_text:
31
+ return jobs
32
+
33
+ # Kart bloklarını hem <li>...</li> üzerinden hem de base-card / job-search-card bölümlerinden yakala
34
+ card_blocks = re.findall(r"<li\b[^>]*>(.*?)</li>", raw_html_or_text, flags=re.DOTALL | re.IGNORECASE)
35
+ if len(card_blocks) <= 1 and "base-search-card__title" in raw_html_or_text:
36
+ # <li> etiketi kapanmamış veya farklı sarmalayıcı kullanılmışsa kart başlangıçlarından böl
37
+ parts = re.split(r'(?=<div[^>]+class="[^"]*(?:base-card|job-search-card)[^"]*")', raw_html_or_text, flags=re.IGNORECASE)
38
+ card_blocks = [p for p in parts if "base-search-card__title" in p]
39
+
40
+ seen_keys: set[str] = set()
41
+ for block in card_blocks:
42
+ title_m = re.search(
43
+ r'class="[^"]*base-search-card__title[^"]*"[^>]*>(.*?)</h[1-6]>',
44
+ block,
45
+ flags=re.DOTALL | re.IGNORECASE,
46
+ )
47
+ if not title_m:
48
+ continue
49
+
50
+ company_m = re.search(
51
+ r'class="[^"]*base-search-card__subtitle[^"]*"[^>]*>(.*?)</h[1-6]>',
52
+ block,
53
+ flags=re.DOTALL | re.IGNORECASE,
54
+ )
55
+ loc_m = re.search(
56
+ r'class="[^"]*job-search-card__location[^"]*"[^>]*>(.*?)</span>',
57
+ block,
58
+ flags=re.DOTALL | re.IGNORECASE,
59
+ )
60
+ link_m = re.search(
61
+ r'href="(https://[^"]*linkedin\.com/jobs/view/[^"?\s]+)',
62
+ block,
63
+ flags=re.IGNORECASE,
64
+ )
65
+ urn_m = re.search(
66
+ r'data-entity-urn="urn:li:jobPosting:(\d+)"',
67
+ block,
68
+ flags=re.IGNORECASE,
69
+ )
70
+ date_m = re.search(r'<time[^>]*datetime="([^"]+)"', block, flags=re.IGNORECASE)
71
+
72
+ clean_title = _clean_html_text(title_m.group(1))
73
+ clean_company = _clean_html_text(company_m.group(1)) if company_m else ""
74
+ clean_loc = _clean_html_text(loc_m.group(1)) if loc_m else ""
75
+ job_id = urn_m.group(1).strip() if urn_m else ""
76
+ job_url = link_m.group(1).strip() if link_m else (f"https://www.linkedin.com/jobs/view/{job_id}/" if job_id else "")
77
+ listed_at = date_m.group(1).strip() if date_m else ""
78
+
79
+ if not clean_title:
80
+ continue
81
+
82
+ dedup_key = job_id or job_url or f"{clean_title}|{clean_company}|{clean_loc}"
83
+ if dedup_key in seen_keys:
84
+ continue
85
+ seen_keys.add(dedup_key)
86
+
87
+ jobs.append(
88
+ {
89
+ "job_id": job_id,
90
+ "title": clean_title,
91
+ "company": clean_company,
92
+ "location": clean_loc,
93
+ "job_url": job_url,
94
+ "listed_at": listed_at,
95
+ }
96
+ )
97
+
98
+ return jobs
99
+
100
+
101
+ class LinkedinJobsClient(BaseOwnClient):
102
+ """LinkedIn Jobs iş ilanı arama ve dinamik sayfalama istemcisi (User-Delegated Browser Middleware)."""
103
+
104
+ def __init__(
105
+ self,
106
+ tenant_id: str = "default",
107
+ token_store: TokenStore | None = None,
108
+ mode: str = "local",
109
+ ) -> None:
110
+ super().__init__(
111
+ service_name="linkedin_jobs",
112
+ base_url="https://www.linkedin.com",
113
+ tenant_id=tenant_id,
114
+ token_store=token_store,
115
+ mode=mode,
116
+ )
117
+
118
+ def search_jobs(
119
+ self,
120
+ keywords: str = "AI Engineer",
121
+ location: str = "",
122
+ limit: int = 10,
123
+ start: int = 0,
124
+ ) -> dict[str, Any]:
125
+ """
126
+ LinkedIn üzerinde belirtilen `keywords` (örn. 'AI Engineer'), `location` ve `limit`
127
+ değerlerine göre iş ilanlarını sorgular.
128
+ Tek sayfadan dönen ilan sayısı `limit` değerinden azsa (örn. limit=15, sayfa=10),
129
+ `start` parametresini otomatik artırarak (`start=0`, `start=10`...) hedef `limit`
130
+ sayısına ulaşana kadar sayfalama yapar ve tam olarak `jobs[:limit]` döndürür.
131
+ """
132
+ clean_keywords = (keywords or "AI Engineer").strip()
133
+ target_limit = max(1, min(int(limit or 10), 50))
134
+ current_start = max(0, int(start or 0))
135
+ encoded_kw = quote_plus(clean_keywords)
136
+ search_page_url = f"https://www.linkedin.com/jobs/search/?keywords={encoded_kw}"
137
+
138
+ headers = {
139
+ "accept": "application/json, text/plain, */*",
140
+ "referer": search_page_url,
141
+ "sec-ch-ua-mobile": "?0",
142
+ "sec-ch-ua-platform": '"Windows"',
143
+ }
144
+
145
+ collected_jobs: list[dict[str, Any]] = []
146
+ seen_keys: set[str] = set()
147
+ pages_fetched = 0
148
+ max_pages = max(1, (target_limit // 10) + 2)
149
+ last_status = 200
150
+ last_raw_text = ""
151
+
152
+ while len(collected_jobs) < target_limit and pages_fetched < max_pages:
153
+ params: dict[str, Any] = {
154
+ "keywords": clean_keywords,
155
+ "start": current_start,
156
+ }
157
+ if location and location.strip():
158
+ params["location"] = location.strip()
159
+
160
+ res = self.request(
161
+ method="GET",
162
+ url="https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search",
163
+ headers=headers,
164
+ params=params,
165
+ target_url="https://www.linkedin.com/robots.txt",
166
+ )
167
+
168
+ raw_text = ""
169
+ if isinstance(res, dict):
170
+ last_status = int(res.get("status_code", 200))
171
+ raw_text = str(res.get("text") or res.get("response_sample") or "")
172
+ else:
173
+ raw_text = str(res or "")
174
+
175
+ last_raw_text = raw_text
176
+ page_jobs = _parse_linkedin_job_cards(raw_text)
177
+ pages_fetched += 1
178
+
179
+ if not page_jobs:
180
+ break
181
+
182
+ added_in_page = 0
183
+ for job in page_jobs:
184
+ key = job.get("job_id") or job.get("job_url") or f"{job.get('title')}|{job.get('company')}"
185
+ if key not in seen_keys:
186
+ seen_keys.add(key)
187
+ collected_jobs.append(job)
188
+ added_in_page += 1
189
+ if len(collected_jobs) >= target_limit:
190
+ break
191
+
192
+ # Bu sayfada yeni ilan gelmediyse veya hedef limite ulaşıldıysa döngüyü bitir
193
+ if added_in_page == 0 or len(collected_jobs) >= target_limit:
194
+ break
195
+
196
+ current_start += len(page_jobs)
197
+
198
+ final_jobs = collected_jobs[:target_limit]
199
+
200
+ return {
201
+ "service": "linkedin_jobs",
202
+ "keywords": clean_keywords,
203
+ "location": location or "Global",
204
+ "requested_limit": target_limit,
205
+ "start": int(start),
206
+ "pages_fetched": pages_fetched,
207
+ "total_jobs_returned": len(final_jobs),
208
+ "jobs": final_jobs,
209
+ "search_url": search_page_url,
210
+ "status_code": last_status,
211
+ "response_preview": (
212
+ f"{len(final_jobs)}/{target_limit} iş ilanı ({pages_fetched} sayfa taranarak) başarıyla getirildi."
213
+ if final_jobs
214
+ else last_raw_text[:500]
215
+ ),
216
+ }
217
+
218
+ # Geriye dönük uyumluluk
219
+ def get_user_metadata(self, keywords: str = "AI Engineer") -> dict[str, Any]:
220
+ return self.search_jobs(keywords=keywords)
221
+
222
+
223
+ def search_jobs(
224
+ keywords: str = "AI Engineer",
225
+ location: str = "",
226
+ limit: int = 10,
227
+ start: int = 0,
228
+ tenant_id: str = "default",
229
+ mode: str = "local",
230
+ ) -> dict[str, Any]:
231
+ """
232
+ LinkedIn Jobs üzerinde anahtar kelime (`keywords`), konum (`location`) ve
233
+ istenilen ilan sayısına (`limit`) göre iş ilanlarını arar; gerekirse otomatik sayfalama yapar.
234
+ """
235
+ client = LinkedinJobsClient(tenant_id=tenant_id, mode=mode)
236
+ return client.search_jobs(keywords=keywords, location=location, limit=limit, start=start)
237
+
238
+
239
+ if __name__ == "__main__":
240
+ result = search_jobs(keywords="AI Engineer", limit=5)
241
+ print(json.dumps(result, ensure_ascii=False, indent=2)[:1500])
@@ -0,0 +1,46 @@
1
+ {
2
+ "service_name": "linkedin_jobs",
3
+ "target_url": "https://www.linkedin.com/jobs/search/?keywords=AI%20Engineer",
4
+ "goal": "LinkedIn Jobs üzerinde anahtar kelime (keywords), konum (location) ve istenen ilan adedine (limit) göre güncel iş ilanlarını arama ve listeleme servisi",
5
+ "function_name": "search_jobs",
6
+ "explanation": "LinkedIn iş arama API uç noktası üzerinden `keywords` (örn. 'AI Engineer'), `location`, `limit` (getirilecek ilan sayısı, örn. 2, 10, 15) ve `start` parametreleriyle gerçek zamanlı iş ilanlarını otomatik sayfalamayla döndürür.",
7
+ "method": "GET",
8
+ "endpoint_url": "https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search",
9
+ "dynamic_params": [
10
+ {
11
+ "name": "keywords",
12
+ "original_key": "keywords",
13
+ "location": "query",
14
+ "param_type": "str",
15
+ "default_value": "AI Engineer",
16
+ "description": "Aranacak iş unvanı, pozisyon veya yetenek anahtar kelimesi (örn. 'AI Engineer', 'Python Developer', 'Product Manager')."
17
+ },
18
+ {
19
+ "name": "location",
20
+ "original_key": "location",
21
+ "location": "query",
22
+ "param_type": "str",
23
+ "default_value": "",
24
+ "description": "İsteğe bağlı şehir, ülke veya çalışma modeli filtresi (örn. 'Turkey', 'Istanbul', 'Remote', 'United States')."
25
+ },
26
+ {
27
+ "name": "limit",
28
+ "original_key": "limit",
29
+ "location": "query",
30
+ "param_type": "int",
31
+ "default_value": 10,
32
+ "description": "Döndürülecek maksimum iş ilanı sayısı (örn. 2, 5, 10, 15). Tek sayfada yeterli ilan yoksa otomatik sayfalama yapılır."
33
+ },
34
+ {
35
+ "name": "start",
36
+ "original_key": "start",
37
+ "location": "query",
38
+ "param_type": "int",
39
+ "default_value": 0,
40
+ "description": "Sayfalama başlangıç indeksi (0, 10, 20 vb.)."
41
+ }
42
+ ],
43
+ "created_at": "2026-11-06T20:27:02.643887+00:00",
44
+ "validation_status": "BAŞARILI",
45
+ "validation_passed_runs": 3
46
+ }
@@ -0,0 +1,5 @@
1
+ """OwnAPI Registry Module: pegasus."""
2
+
3
+ from .client import PegasusClient, get_flight_availability
4
+
5
+ __all__ = ["get_flight_availability", "PegasusClient"]
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import pathlib
5
+ import sys
6
+ import time
7
+ from typing import Any
8
+
9
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))
10
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
11
+
12
+ from core.base_client import BaseOwnClient
13
+ from core.token_store import TokenStore
14
+ from healer import update_payload_deep
15
+
16
+
17
+ class PegasusClient(BaseOwnClient):
18
+ """Pegasus Hava Yolları (flypgs.com) için OwnAPI User-Delegated Browser Middleware istemcisi."""
19
+
20
+ def __init__(
21
+ self,
22
+ tenant_id: str = "default",
23
+ token_store: TokenStore | None = None,
24
+ mode: str = "local",
25
+ ) -> None:
26
+ super().__init__(
27
+ service_name="pegasus",
28
+ base_url="https://web.flypgs.com/booking",
29
+ tenant_id=tenant_id,
30
+ token_store=token_store,
31
+ mode=mode,
32
+ )
33
+
34
+ def get_flight_availability(
35
+ self,
36
+ departure_port: str = "SAW",
37
+ arrival_port: str = "ESB",
38
+ departure_date: str = "2027-03-03",
39
+ return_date: str = "2027-03-08",
40
+ adult_count: int = 1,
41
+ child_count: int = 0,
42
+ infant_count: int = 0,
43
+ currency: str = "TRY",
44
+ ) -> dict[str, Any]:
45
+ params: dict[str, Any] = {}
46
+ headers: dict[str, str] = {
47
+ "accept": "application/json, text/plain, */*",
48
+ "content-type": "application/json",
49
+ "origin": "https://web.flypgs.com",
50
+ "referer": (
51
+ f"https://web.flypgs.com/booking?language=tr&adultCount={adult_count}"
52
+ f"&arrivalPort={arrival_port}&departurePort={departure_port}&currency={currency}"
53
+ f"&dateOption=1&departureDate={departure_date}&returnDate={return_date}"
54
+ ),
55
+ }
56
+ payload: Any = {
57
+ "flightSearchList": [
58
+ {
59
+ "departurePort": departure_port,
60
+ "arrivalPort": arrival_port,
61
+ "departureDate": departure_date,
62
+ "returnDate": return_date,
63
+ }
64
+ ],
65
+ "adultCount": adult_count,
66
+ "childCount": child_count,
67
+ "infantCount": infant_count,
68
+ "currency": currency,
69
+ "dateOption": 1,
70
+ "ffRedemption": False,
71
+ "totalPoints": None,
72
+ "personnelFlightSearch": False,
73
+ "expatPnr": False,
74
+ "operationCode": "TK",
75
+ "affiliate": {"id": None},
76
+ "bookingType": "BOOKING",
77
+ }
78
+ payload = update_payload_deep(
79
+ payload,
80
+ {
81
+ "departurePort": departure_port,
82
+ "arrivalPort": arrival_port,
83
+ "departureDate": departure_date,
84
+ "returnDate": return_date,
85
+ "adultCount": adult_count,
86
+ "childCount": child_count,
87
+ "infantCount": infant_count,
88
+ "currency": currency,
89
+ },
90
+ )
91
+
92
+ return self.request(
93
+ method="POST",
94
+ url="https://web.flypgs.com/pegasus/availability",
95
+ headers=headers,
96
+ params=params,
97
+ json_payload=payload,
98
+ form_params={
99
+ "origin": departure_port,
100
+ "destination": arrival_port,
101
+ "departure_date": departure_date,
102
+ "return_date": return_date,
103
+ },
104
+ )
105
+
106
+
107
+ def get_flight_availability(
108
+ departure_port: str = "SAW",
109
+ arrival_port: str = "ESB",
110
+ departure_date: str = "2027-03-03",
111
+ return_date: str = "2027-03-08",
112
+ adult_count: int = 1,
113
+ child_count: int = 0,
114
+ infant_count: int = 0,
115
+ currency: str = "TRY",
116
+ tenant_id: str = "default",
117
+ mode: str = "local",
118
+ ) -> dict[str, Any]:
119
+ """
120
+ Pegasus Hava Yolları uçuş seferlerini ve fiyatlarını kullanıcının gerçek tarayıcı oturumu üzerinden sorgular.
121
+ Hedef Endpoint: POST https://web.flypgs.com/pegasus/availability
122
+ """
123
+ client = PegasusClient(tenant_id=tenant_id, mode=mode)
124
+ return client.get_flight_availability(
125
+ departure_port=departure_port,
126
+ arrival_port=arrival_port,
127
+ departure_date=departure_date,
128
+ return_date=return_date,
129
+ adult_count=adult_count,
130
+ child_count=child_count,
131
+ infant_count=infant_count,
132
+ currency=currency,
133
+ )
134
+
135
+
136
+ if __name__ == "__main__":
137
+ start_ns = time.perf_counter()
138
+ data = get_flight_availability()
139
+ elapsed_ms = (time.perf_counter() - start_ns) * 1000
140
+ print(f"[OwnAPI] Yanıt Süresi: {elapsed_ms:.2f} ms")
141
+ print(json.dumps(data, ensure_ascii=False, indent=2)[:800])
@@ -0,0 +1,76 @@
1
+ {
2
+ "service_name": "pegasus",
3
+ "target_url": "https://www.flypgs.com/tr",
4
+ "goal": "İstanbul'dan Ankara'ya uçuş seferlerini ve fiyatlarını sorgula",
5
+ "function_name": "get_flight_availability",
6
+ "explanation": "İstanbul'dan Ankara'ya uçuş seferlerini ve fiyatlarını sorgulamak için en uygun uç nokta, uçuş arama ve fiyat bilgilerini içeren '/pegasus/availability' uç noktasıdır. Bu uç nokta, kullanıcı tarafından belirtilen kalkış ve varış noktaları ile tarihleri içeren bir arama yapar ve uçuş bilgilerini döndürür.",
7
+ "method": "POST",
8
+ "endpoint_url": "https://web.flypgs.com/pegasus/availability",
9
+ "dynamic_params": [
10
+ {
11
+ "name": "departure_port",
12
+ "original_key": "departurePort",
13
+ "location": "body",
14
+ "param_type": "str",
15
+ "default_value": "SAW",
16
+ "description": "Kalkış yapılacak havaalanı kodu."
17
+ },
18
+ {
19
+ "name": "arrival_port",
20
+ "original_key": "arrivalPort",
21
+ "location": "body",
22
+ "param_type": "str",
23
+ "default_value": "ESB",
24
+ "description": "Varış yapılacak havaalanı kodu."
25
+ },
26
+ {
27
+ "name": "departure_date",
28
+ "original_key": "departureDate",
29
+ "location": "body",
30
+ "param_type": "str",
31
+ "default_value": "2027-03-03",
32
+ "description": "Kalkış tarihi."
33
+ },
34
+ {
35
+ "name": "return_date",
36
+ "original_key": "returnDate",
37
+ "location": "body",
38
+ "param_type": "str",
39
+ "default_value": "2027-03-08",
40
+ "description": "Dönüş tarihi."
41
+ },
42
+ {
43
+ "name": "adult_count",
44
+ "original_key": "adultCount",
45
+ "location": "body",
46
+ "param_type": "int",
47
+ "default_value": 1,
48
+ "description": "Yetişkin yolcu sayısı."
49
+ },
50
+ {
51
+ "name": "child_count",
52
+ "original_key": "childCount",
53
+ "location": "body",
54
+ "param_type": "int",
55
+ "default_value": 0,
56
+ "description": "Çocuk yolcu sayısı."
57
+ },
58
+ {
59
+ "name": "infant_count",
60
+ "original_key": "infantCount",
61
+ "location": "body",
62
+ "param_type": "int",
63
+ "default_value": 0,
64
+ "description": "Bebek yolcu sayısı."
65
+ },
66
+ {
67
+ "name": "currency",
68
+ "original_key": "currency",
69
+ "location": "body",
70
+ "param_type": "str",
71
+ "default_value": "TRY",
72
+ "description": "Para birimi."
73
+ }
74
+ ],
75
+ "created_at": "2026-11-01T12:39:07.722926+00:00"
76
+ }
@@ -0,0 +1,5 @@
1
+ """OwnAPI Registry Module: thy."""
2
+
3
+ from .client import THYClient, ThyClient, search_flights
4
+
5
+ __all__ = ["search_flights", "ThyClient", "THYClient"]