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
core/base_client.py ADDED
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from abc import ABC
6
+ from typing import Any
7
+ from urllib.parse import urljoin
8
+
9
+ from core.consent import ConsentManager
10
+ from core.form_automation import GenericFormAutomator, extract_semantic_form_params
11
+ from core.token_store import TokenStore
12
+
13
+ logger = logging.getLogger("own_api.base_client")
14
+
15
+
16
+ class BaseOwnClient(ABC):
17
+ """
18
+ OwnAPI User-Delegated Browser Middleware Taban Sınıfı (Plaid-style Browser Automation).
19
+ Tüm OwnAPI servis istemcilerinin (THY, Trendyol, Pegasus, Enuygun, Wikipedia, HackerNews vb.)
20
+ miras aldığı kurumsal sınıf:
21
+ - Çok kiracılı (multi-tenant) oturum ve çerez izolasyonu (`TokenStore` -> `.sessions/{tenant_id}/{service}_tokens.json`).
22
+ - ToS / Kullanıcı Delegasyon Onay Kaydı (`ConsentManager` -> `.sessions/{tenant_id}/consent_audit.json`).
23
+ - 2 Modlu Çalışma Altyapısı:
24
+ * `mode="local"` (varsayılan): Kullanıcının kendi yerel Chrome oturumu ve IP'si üzerinden çalışır.
25
+ * `mode="cloud"`: Bulut tabanlı Docker / Remote Browser (CDP / Browserless) üzerinden çalışır.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ service_name: str,
31
+ base_url: str,
32
+ tenant_id: str = "default",
33
+ token_store: TokenStore | None = None,
34
+ consent_manager: ConsentManager | None = None,
35
+ mode: str = "local",
36
+ cloud_cdp_url: str | None = None,
37
+ non_interactive: bool = False,
38
+ **kwargs: Any,
39
+ ) -> None:
40
+ self.service_name = service_name
41
+ self.base_url = base_url
42
+ self.tenant_id = tenant_id or "default"
43
+ self.mode = (mode or os.getenv("OWNAPI_BROWSER_MODE", "local")).strip().lower()
44
+ if self.mode not in ("local", "cloud"):
45
+ self.mode = "local"
46
+ self.cloud_cdp_url = cloud_cdp_url or os.getenv("OWNAPI_CLOUD_CDP_URL", "").strip() or None
47
+ self.non_interactive = bool(
48
+ non_interactive
49
+ or os.getenv("OWNAPI_NON_INTERACTIVE", "").strip().lower() in ("1", "true", "yes")
50
+ )
51
+ self.token_store = token_store or TokenStore()
52
+ self.consent_manager = consent_manager or ConsentManager()
53
+ self.form_automator = GenericFormAutomator()
54
+
55
+ from healer import SessionHealer
56
+
57
+ self.healer = SessionHealer(
58
+ target_url=base_url,
59
+ tenant_id=self.tenant_id,
60
+ service=self.service_name,
61
+ token_store=self.token_store,
62
+ mode=self.mode,
63
+ cloud_cdp_url=self.cloud_cdp_url,
64
+ non_interactive=self.non_interactive,
65
+ )
66
+
67
+ def get_stored_tokens(self) -> dict[str, Any]:
68
+ """Kiracıya ve servise özel kayıtlı oturum çerezlerini TokenStore'dan çeker."""
69
+ return self.token_store.get_session(self.tenant_id, self.service_name)
70
+
71
+ def save_tokens(self, cookies: Any, user_agent: str = "", metadata: dict[str, Any] | None = None) -> None:
72
+ """Taze çerez ve başlıkları TokenStore'a (.sessions/{tenant_id}/{service}_tokens.json) işler."""
73
+ self.token_store.save_session(
74
+ tenant_id=self.tenant_id,
75
+ service=self.service_name,
76
+ cookies=cookies,
77
+ user_agent=user_agent,
78
+ metadata=metadata,
79
+ )
80
+
81
+ def request(
82
+ self,
83
+ method: str,
84
+ url: str,
85
+ headers: dict[str, str] | None = None,
86
+ params: dict[str, Any] | None = None,
87
+ json_payload: Any = None,
88
+ data: Any = None,
89
+ target_url: str | None = None,
90
+ mode: str | None = None,
91
+ non_interactive: bool | None = None,
92
+ form_params: dict[str, Any] | None = None,
93
+ auto_fill_form: bool | None = None,
94
+ **kwargs: Any,
95
+ ) -> dict[str, Any]:
96
+ """
97
+ İsteği ilk çağrıda ToS / Kullanıcı Delegasyon Onay kaydını doğrulayarak
98
+ kullanıcının gerçek tarayıcı oturumu (In-Browser Fetch) üzerinden yürütür.
99
+ Sayfa yüklendiğinde Generic Form Automation katmanı ile semantik input'ları
100
+ (origin/destination, departure/return date, autocomplete ve arama butonu) otonom doldurur.
101
+ TokenStore'daki izole çerezler tarayıcıya otomatik enjekte edilir ve işlem sonunda güncellenir.
102
+ """
103
+ active_mode = (mode or self.mode or "local").strip().lower()
104
+ if active_mode not in ("local", "cloud"):
105
+ active_mode = "local"
106
+ env_ni = os.getenv("OWNAPI_NON_INTERACTIVE", "").strip().lower() in ("1", "true", "yes")
107
+ active_non_interactive = (
108
+ bool(non_interactive or env_ni)
109
+ if non_interactive is not None
110
+ else bool(self.non_interactive or env_ni)
111
+ )
112
+
113
+ # İlk çağrıda kullanıcının kendi oturumuyla bağlandığını onaylayan ToS / Delegation kaydını tut
114
+ self.consent_manager.ensure_consent(
115
+ tenant_id=self.tenant_id,
116
+ service=self.service_name,
117
+ action="first_invocation",
118
+ mode=active_mode,
119
+ )
120
+
121
+ req_headers = dict(headers or {})
122
+ full_url = url if url.startswith("http") else urljoin(self.base_url, url)
123
+ method_upper = method.upper().strip()
124
+
125
+ # TokenStore'dan mevcut çerezleri ve User-Agent bilgisini başlıklara hazırla
126
+ tokens = self.get_stored_tokens()
127
+ if tokens:
128
+ stored_cookie = tokens.get("cookie")
129
+ stored_ua = tokens.get("user-agent")
130
+ if stored_cookie and "cookie" not in {k.lower() for k in req_headers}:
131
+ req_headers["cookie"] = stored_cookie
132
+ if stored_ua and "user-agent" not in {k.lower() for k in req_headers}:
133
+ req_headers["user-agent"] = stored_ua
134
+
135
+ payload = json_payload if json_payload is not None else data
136
+ resolved_form_params = extract_semantic_form_params(
137
+ json_payload=payload,
138
+ params=params,
139
+ url=full_url,
140
+ target_url=target_url or self.base_url,
141
+ explicit_form_params=form_params,
142
+ )
143
+
144
+ from healer import ResponseDict
145
+
146
+ res = self.healer.execute_in_browser(
147
+ method=method_upper,
148
+ url=full_url,
149
+ headers=req_headers,
150
+ params=params,
151
+ json_payload=payload,
152
+ target_url=target_url or self.base_url,
153
+ tenant_id=self.tenant_id,
154
+ mode=active_mode,
155
+ non_interactive=active_non_interactive,
156
+ form_params=resolved_form_params,
157
+ auto_fill_form=auto_fill_form,
158
+ )
159
+ if isinstance(res, dict) and not isinstance(res, ResponseDict):
160
+ return ResponseDict(res)
161
+ return res
162
+
163
+
164
+ # Geriye dönük uyumluluk takma adı
165
+ BaseShadowClient = BaseOwnClient
core/consent.py ADDED
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import socket
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+
14
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
15
+ console = Console(stderr=True, legacy_windows=False)
16
+
17
+ TOS_VERSION = "1.0-Enterprise"
18
+
19
+ USER_DELEGATION_DISCLAIMER = (
20
+ "OwnAPI (User-Delegated Browser Middleware) Kullanım Koşulları ve Yetkilendirme Beyanı:\n"
21
+ "1. Kullanıcı Delegasyonu (Plaid-Style Delegation): Bu yazılım gizli bir bot veya sunucu taraflı\n"
22
+ " veri kazıyıcı (scraper) DEĞİLDİR. Tüm işlemler yalnızca kullanıcının kendi açık rızasıyla,\n"
23
+ " kendi tarayıcı oturumu, kendi kimlik bilgileri ve kendi IP adresi üzerinden vekaleten yürütülür.\n"
24
+ "2. Veri İzolasyonu: Yakalanan oturum çerezleri yalnızca yerel/izole `.sessions/{tenant_id}/`\n"
25
+ " kasasında saklanır ve üçüncü taraflarla paylaşılmaz.\n"
26
+ "3. Sorumluluk: Kullanıcı, hedef web servisinin kullanım koşullarına (ToS) kendi hesabı ve\n"
27
+ " oturumu kapsamında uymaktan sorumludur."
28
+ )
29
+
30
+
31
+ class ConsentManager:
32
+ """
33
+ ToS / Kullanıcı Onay Mekanizması (User Delegation Disclaimer & Audit Trail).
34
+ Yeni bir site eklenirken (onboarding) veya bir servis ilk kez çağrıldığında
35
+ kullanıcının kendi tarayıcısı/hesabıyla bağlandığını onaylayan yasal kayıt katmanıdır.
36
+ Onay kayıtlarını `.sessions/{tenant_id}/consent_audit.json` altında tutar.
37
+ """
38
+
39
+ def __init__(self, sessions_root: Path | str = PROJECT_ROOT / ".sessions") -> None:
40
+ self.sessions_root = Path(sessions_root)
41
+ self.sessions_root.mkdir(parents=True, exist_ok=True)
42
+
43
+ @staticmethod
44
+ def _sanitize(value: str) -> str:
45
+ clean = "".join(c if (c.isalnum() or c in ("-", "_")) else "_" for c in value.strip())
46
+ return clean or "default"
47
+
48
+ def _audit_file_path(self, tenant_id: str) -> Path:
49
+ t_dir = self.sessions_root / self._sanitize(tenant_id)
50
+ t_dir.mkdir(parents=True, exist_ok=True)
51
+ return t_dir / "consent_audit.json"
52
+
53
+ def get_consents(self, tenant_id: str = "default") -> list[dict[str, Any]]:
54
+ """Belirtilen kiracı (tenant) için kaydedilmiş tüm yasal onay kayıtlarını döndürür."""
55
+ audit_file = self._audit_file_path(tenant_id)
56
+ if not audit_file.exists():
57
+ return []
58
+ try:
59
+ data = json.loads(audit_file.read_text(encoding="utf-8"))
60
+ if isinstance(data, dict) and isinstance(data.get("consents"), list):
61
+ return data["consents"]
62
+ except Exception:
63
+ pass
64
+ return []
65
+
66
+ def has_consented(self, tenant_id: str, service: str) -> bool:
67
+ """Kullanıcının belirtilen servis için daha önce onay verip vermediğini kontrol eder."""
68
+ clean_svc = self._sanitize(service)
69
+ for entry in self.get_consents(tenant_id):
70
+ if entry.get("service") in (clean_svc, "*") and entry.get("consent_granted") is True:
71
+ return True
72
+ return False
73
+
74
+ def record_consent(
75
+ self,
76
+ tenant_id: str,
77
+ service: str,
78
+ action: str = "first_invocation",
79
+ mode: str = "local",
80
+ accepted_by: str | None = None,
81
+ ) -> dict[str, Any]:
82
+ """Kullanıcı onayını zaman damgası ve kriptografik özet ile denetim günlüğüne (audit log) yazar."""
83
+ audit_file = self._audit_file_path(tenant_id)
84
+ existing = self.get_consents(tenant_id)
85
+
86
+ disclaimer_hash = hashlib.sha256(USER_DELEGATION_DISCLAIMER.encode("utf-8")).hexdigest()[:16]
87
+ host_user = accepted_by or os.getenv("USERNAME") or os.getenv("USER") or "authenticated_user"
88
+ try:
89
+ hostname = socket.gethostname()
90
+ except Exception:
91
+ hostname = "localhost"
92
+
93
+ record: dict[str, Any] = {
94
+ "tenant_id": self._sanitize(tenant_id),
95
+ "service": self._sanitize(service),
96
+ "action": action,
97
+ "execution_mode": mode,
98
+ "tos_version": TOS_VERSION,
99
+ "disclaimer_sha256": disclaimer_hash,
100
+ "consent_granted": True,
101
+ "delegated_by": f"{host_user}@{hostname}",
102
+ "accepted_at": datetime.now(timezone.utc).isoformat(),
103
+ }
104
+
105
+ existing.append(record)
106
+ payload = {
107
+ "tenant_id": self._sanitize(tenant_id),
108
+ "tos_version": TOS_VERSION,
109
+ "updated_at": datetime.now(timezone.utc).isoformat(),
110
+ "total_records": len(existing),
111
+ "consents": existing,
112
+ }
113
+ audit_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
114
+ return record
115
+
116
+ def ensure_consent(
117
+ self,
118
+ tenant_id: str,
119
+ service: str,
120
+ action: str = "first_invocation",
121
+ mode: str = "local",
122
+ interactive: bool = False,
123
+ force_banner: bool = False,
124
+ ) -> dict[str, Any]:
125
+ """
126
+ Servis çağrılmadan veya yeni site eklenmeden önce ToS / Kullanıcı Delegasyon onayını doğrular.
127
+ İlk kullanımda veya onboarding sırasında yasal onay bildirimini ekrana basar ve loglar.
128
+ """
129
+ already_consented = self.has_consented(tenant_id, service)
130
+ if already_consented and not force_banner:
131
+ consents = self.get_consents(tenant_id)
132
+ return next(
133
+ (c for c in reversed(consents) if c.get("service") in (self._sanitize(service), "*")),
134
+ {},
135
+ )
136
+
137
+ console.print(
138
+ Panel.fit(
139
+ f"[bold yellow]⚖️ Terms of Service Agreement / User Delegation Disclaimer[/bold yellow]\n"
140
+ f"[white]{USER_DELEGATION_DISCLAIMER}[/white]\n\n"
141
+ f"[dim]Tenant:[/dim] [cyan]{tenant_id}[/cyan] | "
142
+ f"[dim]Servis:[/dim] [magenta]{service}[/magenta] | "
143
+ f"[dim]Çalışma Modu:[/dim] [green]{mode.upper()}[/green] | "
144
+ f"[dim]İşlem:[/dim] [yellow]{action}[/yellow]",
145
+ title="OwnAPI Legal Shield & User Delegation Consent",
146
+ border_style="yellow",
147
+ )
148
+ )
149
+
150
+ if interactive and sys.stdin and sys.stdin.isatty():
151
+ ans = input("👉 Kendi tarayıcı oturumunuzla vekaleten işlem yapılmasını onaylıyor musunuz? [E/h]: ").strip().lower()
152
+ if ans in ("h", "n", "hayir", "hayır", "no"):
153
+ raise PermissionError("Kullanıcı ToS / User Delegation onayını reddetti. İşlem iptal edildi.")
154
+
155
+ record = self.record_consent(
156
+ tenant_id=tenant_id,
157
+ service=service,
158
+ action=action,
159
+ mode=mode,
160
+ )
161
+ console.print(
162
+ f"[bold green]✔ Kullanıcı Delegasyon Onayı Kaydedildi:[/bold green] "
163
+ f"[dim].sessions/{self._sanitize(tenant_id)}/consent_audit.json "
164
+ f"(SHA256: {record['disclaimer_sha256']})[/dim]"
165
+ )
166
+ return record
167
+
168
+
169
+ import sys # noqa: E402