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
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
|