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
ownapi/sdk.py ADDED
@@ -0,0 +1,436 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import inspect
5
+ import os
6
+ import pathlib
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Callable
10
+
11
+ PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[1]
12
+ if str(PROJECT_ROOT) not in sys.path:
13
+ sys.path.insert(0, str(PROJECT_ROOT))
14
+
15
+ from core.api_keys import (
16
+ APIKeyManager,
17
+ CreditBalance,
18
+ consume_credit,
19
+ generate_api_key,
20
+ get_or_create_tenant,
21
+ get_tenant_info,
22
+ upgrade_plan,
23
+ )
24
+ from core.auto_repair import AutoRepairEngine
25
+ from core.base_client import BaseOwnClient, BaseShadowClient
26
+ from core.consent import ConsentManager
27
+ from core.hitl_queue import HITLQueueManager
28
+ from core.onboarding import onboard_new_site
29
+ from core.token_store import TokenStore
30
+ from master_mcp import load_registry_tools
31
+
32
+
33
+ def _load_connector_module(service_name: str, registry_root: Path | None = None) -> Any:
34
+ """`registry/<service_name>/client.py` modülünü dinamik olarak yükler ve döndürür."""
35
+ reg_dir = Path(registry_root) if registry_root else (PROJECT_ROOT / "registry")
36
+ clean_svc = service_name.strip().lower()
37
+ client_file = reg_dir / clean_svc / "client.py"
38
+ if not client_file.exists():
39
+ raise AttributeError(
40
+ f"OwnAPI konektörü bulunamadı: 'registry/{clean_svc}/client.py'"
41
+ )
42
+
43
+ mod_name = f"ownapi_connector_{clean_svc}"
44
+ if mod_name in sys.modules:
45
+ return sys.modules[mod_name]
46
+
47
+ spec = importlib.util.spec_from_file_location(mod_name, str(client_file.resolve()))
48
+ if spec is None or spec.loader is None:
49
+ raise ImportError(f"Konektör modülü yüklenemedi: {client_file}")
50
+ module = importlib.util.module_from_spec(spec)
51
+ sys.modules[mod_name] = module
52
+ spec.loader.exec_module(module)
53
+ return module
54
+
55
+
56
+ class ServiceNamespaceProxy:
57
+ """
58
+ `from ownapi import thy, trendyol, pegasus` veya `api.thy.search_flights(...)`
59
+ kullanımlarını yöneten akıllı servis vekil (proxy) sınıfı.
60
+
61
+ Her çağrıda:
62
+ 1. `core/api_keys.py` üzerinden aktif kullanıcı (`default` veya `api_key` sahibi kiracı)
63
+ kredisinden `1` kredi düşer (`consume_credit`).
64
+ 2. Kullanıcının paketine ait çalışma modunu (`mode="local"` veya `mode="cloud"`) ve
65
+ `tenant_id` bilgisini hedef konektör fonksiyonuna otomatik enjekte eder.
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ service_name: str,
71
+ owner: OwnAPI | None = None,
72
+ default_tenant_id: str = "default",
73
+ default_mode: str = "local",
74
+ registry_root: Path | None = None,
75
+ state_file: Path | str | None = None,
76
+ ) -> None:
77
+ self._service_name = service_name.strip().lower()
78
+ self._owner = owner
79
+ self._default_tenant_id = default_tenant_id
80
+ self._default_mode = default_mode
81
+ self._registry_root = Path(registry_root) if registry_root else (PROJECT_ROOT / "registry")
82
+ self._state_file = state_file
83
+ self._module: Any = None
84
+
85
+ def _get_module(self) -> Any:
86
+ if self._module is None:
87
+ self._module = _load_connector_module(self._service_name, self._registry_root)
88
+ return self._module
89
+
90
+ def _resolve_execution_context(self) -> tuple[str, str, str | None, CreditBalance]:
91
+ """
92
+ Çağrı öncesi `core/api_keys.py` üzerinden kredi düşümünü yapar ve
93
+ `(tenant_id, mode, api_key, credit_balance)` dörtlüsünü döndürür.
94
+ """
95
+ if self._owner is not None:
96
+ auth_identifier = self._owner.api_key or self._owner.tenant_id
97
+ state_path = self._owner._state_file
98
+ info = get_tenant_info(auth_identifier, state_file=state_path)
99
+ balance = consume_credit(
100
+ auth_identifier,
101
+ amount=1,
102
+ state_file=state_path,
103
+ service=self._service_name,
104
+ )
105
+ active_tenant = str(info.get("tenant_id") or self._owner.tenant_id or "default")
106
+ active_mode = str(self._owner._explicit_mode or info.get("mode") or self._owner.mode or "local")
107
+ return active_tenant, active_mode, self._owner.api_key, balance
108
+
109
+ # Bedava yerel mod (`from ownapi import thy, trendyol, pegasus`)
110
+ info = get_or_create_tenant(self._default_tenant_id, state_file=self._state_file)
111
+ balance = consume_credit(
112
+ self._default_tenant_id,
113
+ amount=1,
114
+ state_file=self._state_file,
115
+ service=self._service_name,
116
+ )
117
+ active_tenant = str(info.get("tenant_id") or self._default_tenant_id)
118
+ active_mode = str(info.get("mode") or self._default_mode or "local")
119
+ return active_tenant, active_mode, info.get("api_key"), balance
120
+
121
+ def _resolve_callable(self, attr_name: str) -> Callable[..., Any]:
122
+ mod = self._get_module()
123
+
124
+ # 1. Doğrudan modül fonksiyonu varsa (örn. thy.search_flights, pegasus.get_flight_availability)
125
+ target = getattr(mod, attr_name, None)
126
+ if callable(target) and not inspect.isclass(target):
127
+ return target
128
+
129
+ # 2. Modül içindeki BaseOwnClient alt sınıfı üzerinde metot varsa (örn. trendyol.search)
130
+ client_cls = None
131
+ for _, obj in inspect.getmembers(mod, inspect.isclass):
132
+ if issubclass(obj, BaseOwnClient) and obj is not BaseOwnClient:
133
+ client_cls = obj
134
+ break
135
+
136
+ if client_cls is not None and hasattr(client_cls, attr_name):
137
+ def _bound_method_runner(
138
+ *args: Any,
139
+ tenant_id: str = "default",
140
+ mode: str = "local",
141
+ **kwargs: Any,
142
+ ) -> Any:
143
+ init_sig = inspect.signature(client_cls.__init__)
144
+ init_kwargs: dict[str, Any] = {}
145
+ if "tenant_id" in init_sig.parameters:
146
+ init_kwargs["tenant_id"] = tenant_id
147
+ if "mode" in init_sig.parameters:
148
+ init_kwargs["mode"] = mode
149
+ instance = client_cls(**init_kwargs)
150
+ instance.tenant_id = tenant_id
151
+ instance.mode = mode
152
+ method_fn = getattr(instance, attr_name)
153
+ m_sig = inspect.signature(method_fn)
154
+ if "tenant_id" in m_sig.parameters:
155
+ kwargs["tenant_id"] = tenant_id
156
+ if "mode" in m_sig.parameters:
157
+ kwargs["mode"] = mode
158
+ return method_fn(*args, **kwargs)
159
+
160
+ return _bound_method_runner
161
+
162
+ # 3. Yaygın takma adlar (search / search_flights / call) için ana fonksiyonu bul
163
+ if attr_name in ("search", "search_flights", "get_flight_availability", "call"):
164
+ public_funcs = [
165
+ obj
166
+ for name, obj in inspect.getmembers(mod, inspect.isfunction)
167
+ if obj.__module__ == mod.__name__ and not name.startswith("_")
168
+ ]
169
+ if public_funcs:
170
+ return public_funcs[0]
171
+
172
+ raise AttributeError(
173
+ f"'{self._service_name}' servisinde '{attr_name}' fonksiyonu bulunamadı."
174
+ )
175
+
176
+ def __getattr__(self, name: str) -> Any:
177
+ if name.startswith("_"):
178
+ raise AttributeError(name)
179
+
180
+ mod = self._get_module()
181
+ raw_attr = getattr(mod, name, None)
182
+ if raw_attr is not None and not callable(raw_attr):
183
+ return raw_attr
184
+
185
+ target_fn = self._resolve_callable(name)
186
+
187
+ def _wrapped(*args: Any, **kwargs: Any) -> Any:
188
+ active_tenant, active_mode, _, _ = self._resolve_execution_context()
189
+ sig = inspect.signature(target_fn)
190
+ call_kwargs = dict(kwargs)
191
+ if "tenant_id" in sig.parameters and "tenant_id" not in call_kwargs:
192
+ call_kwargs["tenant_id"] = active_tenant
193
+ if "mode" in sig.parameters and "mode" not in call_kwargs:
194
+ call_kwargs["mode"] = active_mode
195
+ return target_fn(*args, **call_kwargs)
196
+
197
+ _wrapped.__name__ = name
198
+ _wrapped.__doc__ = getattr(target_fn, "__doc__", None)
199
+ return _wrapped
200
+
201
+ def __repr__(self) -> str:
202
+ mode_str = self._owner.mode if self._owner else self._default_mode
203
+ return f"<OwnAPI ServiceProxy '{self._service_name}' (mode='{mode_str}')>"
204
+
205
+
206
+ class OwnAPI:
207
+ """
208
+ OwnAPI Birleşik Python SDK İstemcisi (`from ownapi import OwnAPI`).
209
+ - Bedava Yerel Mod veya `$10` (`starter_cloud`) / `$49` (`growth_cloud`) bulut anahtarıyla çalışır:
210
+ `api = OwnAPI(api_key="sk-ownapi-...")`
211
+ `api.thy.search_flights(origin_airport_code="IST", destination_airport_code="ESB")`
212
+ `api.trendyol.search(query="laptop")`
213
+ """
214
+
215
+ def __init__(
216
+ self,
217
+ api_key: str | None = None,
218
+ tenant_id: str | None = None,
219
+ mode: str | None = None,
220
+ cloud_cdp_url: str | None = None,
221
+ non_interactive: bool = False,
222
+ registry_root: pathlib.Path | str = PROJECT_ROOT / "registry",
223
+ state_file: pathlib.Path | str | None = None,
224
+ **kwargs: Any,
225
+ ) -> None:
226
+ self._state_file = state_file
227
+ self.api_key = (api_key or os.getenv("OWNAPI_API_KEY", "")).strip() or None
228
+ self._explicit_mode = mode.strip().lower() if isinstance(mode, str) and mode.strip() else None
229
+
230
+ if self.api_key:
231
+ tenant_record = get_tenant_info(self.api_key, state_file=self._state_file)
232
+ self.tenant_id = str(tenant_record.get("tenant_id", tenant_id or "default"))
233
+ self.plan = str(tenant_record.get("plan", "free"))
234
+ self.mode = str(self._explicit_mode or tenant_record.get("mode", "local")).lower()
235
+ else:
236
+ resolved_tenant = (tenant_id or "default").strip() or "default"
237
+ tenant_record = get_or_create_tenant(resolved_tenant, state_file=self._state_file)
238
+ self.tenant_id = str(tenant_record.get("tenant_id", resolved_tenant))
239
+ self.api_key = str(tenant_record.get("api_key", "")) or None
240
+ self.plan = str(tenant_record.get("plan", "free"))
241
+ self.mode = str(self._explicit_mode or tenant_record.get("mode", "local")).lower()
242
+
243
+ if self.mode not in ("local", "cloud"):
244
+ self.mode = "local"
245
+
246
+ self.cloud_cdp_url = cloud_cdp_url or os.getenv("OWNAPI_CLOUD_CDP_URL", "").strip() or None
247
+ self.non_interactive = bool(
248
+ non_interactive
249
+ or os.getenv("OWNAPI_NON_INTERACTIVE", "").strip().lower() in ("1", "true", "yes")
250
+ )
251
+ self.registry_root = pathlib.Path(registry_root)
252
+ self.token_store = TokenStore()
253
+ self.consent_manager = ConsentManager()
254
+ self.hitl_queue = HITLQueueManager()
255
+ self.auto_repair = AutoRepairEngine(
256
+ registry_root=self.registry_root,
257
+ tenant_id=self.tenant_id,
258
+ mode=self.mode,
259
+ )
260
+ self._agent = None
261
+ self._service_proxies: dict[str, ServiceNamespaceProxy] = {}
262
+
263
+ @property
264
+ def credits(self) -> int:
265
+ """Kullanıcının güncel kalan kredi bakiyesini döndürür."""
266
+ info = get_tenant_info(self.api_key or self.tenant_id, state_file=self._state_file)
267
+ return int(info.get("credits", 0))
268
+
269
+ def status(self) -> dict[str, Any]:
270
+ """Kullanıcının paket, kalan kredi, çalışma modu ve anahtar durumunu döndürür."""
271
+ return get_tenant_info(self.api_key or self.tenant_id, state_file=self._state_file)
272
+
273
+ def __getattr__(self, name: str) -> ServiceNamespaceProxy:
274
+ if name.startswith("_"):
275
+ raise AttributeError(name)
276
+ clean_name = name.strip().lower()
277
+ if clean_name not in self._service_proxies:
278
+ client_file = self.registry_root / clean_name / "client.py"
279
+ if not client_file.exists():
280
+ raise AttributeError(f"OwnAPI üzerinde '{name}' adında bir servis veya öznitelik bulunamadı.")
281
+ self._service_proxies[clean_name] = ServiceNamespaceProxy(
282
+ service_name=clean_name,
283
+ owner=self,
284
+ default_tenant_id=self.tenant_id,
285
+ default_mode=self.mode,
286
+ registry_root=self.registry_root,
287
+ state_file=self._state_file,
288
+ )
289
+ return self._service_proxies[clean_name]
290
+
291
+ def list_services(self) -> list[dict[str, Any]]:
292
+ """Kayıtlı tüm servislerin şemalarını, fonksiyon adlarını ve parametrelerini listeler."""
293
+ tools = load_registry_tools(self.registry_root)
294
+ result: list[dict[str, Any]] = []
295
+ for schema, _ in tools:
296
+ result.append({
297
+ "service_name": schema.service_name,
298
+ "function_name": schema.function_name,
299
+ "goal": schema.goal,
300
+ "method": schema.method,
301
+ "endpoint_url": schema.endpoint_url,
302
+ "explanation": schema.explanation,
303
+ "parameters": [
304
+ {
305
+ "name": p.name,
306
+ "type": p.param_type,
307
+ "default": p.default_value,
308
+ "description": p.description,
309
+ "location": p.location,
310
+ }
311
+ for p in schema.dynamic_params
312
+ ],
313
+ })
314
+ return result
315
+
316
+ def call(
317
+ self,
318
+ service_name: str,
319
+ mode: str | None = None,
320
+ non_interactive: bool | None = None,
321
+ _consume_credit: bool = True,
322
+ **kwargs: Any,
323
+ ) -> Any:
324
+ """
325
+ Belirtilen servisi API anahtarı / kredi kontrolü, ToS delegasyon onayı ve
326
+ aktif çalışma modu ('local' veya 'cloud') ile çağırır.
327
+ """
328
+ tools = load_registry_tools(self.registry_root)
329
+ target_fn = None
330
+ target_schema = None
331
+
332
+ clean_req = service_name.strip().lower()
333
+ for schema, fn in tools:
334
+ if schema.service_name.lower() == clean_req or schema.function_name.lower() == clean_req:
335
+ target_fn = fn
336
+ target_schema = schema
337
+ break
338
+
339
+ if target_fn is None or target_schema is None:
340
+ available = [s.service_name for s, _ in tools]
341
+ raise ValueError(f"Servis '{service_name}' bulunamadı! Mevcut servisler: {available}")
342
+
343
+ # Kredi düşümü ve mod çözümlemesi
344
+ auth_id = self.api_key or self.tenant_id
345
+ info = get_tenant_info(auth_id, state_file=self._state_file)
346
+ if _consume_credit:
347
+ consume_credit(
348
+ auth_id,
349
+ amount=1,
350
+ state_file=self._state_file,
351
+ service=target_schema.service_name,
352
+ )
353
+
354
+ active_mode = (mode or self._explicit_mode or info.get("mode") or self.mode or "local").strip().lower()
355
+ if active_mode not in ("local", "cloud"):
356
+ active_mode = "local"
357
+ active_ni = self.non_interactive if non_interactive is None else bool(non_interactive)
358
+
359
+ self.consent_manager.ensure_consent(
360
+ tenant_id=self.tenant_id,
361
+ service=target_schema.service_name,
362
+ action="sdk_call",
363
+ mode=active_mode,
364
+ )
365
+
366
+ sig = inspect.signature(target_fn)
367
+ call_args = dict(kwargs)
368
+
369
+ if "tenant_id" in sig.parameters and "tenant_id" not in call_args:
370
+ call_args["tenant_id"] = self.tenant_id
371
+ if "mode" in sig.parameters and "mode" not in call_args:
372
+ call_args["mode"] = active_mode
373
+
374
+ prev_ni = os.environ.get("OWNAPI_NON_INTERACTIVE")
375
+ if active_ni:
376
+ os.environ["OWNAPI_NON_INTERACTIVE"] = "1"
377
+ try:
378
+ return target_fn(**call_args)
379
+ finally:
380
+ if active_ni:
381
+ if prev_ni is None:
382
+ os.environ.pop("OWNAPI_NON_INTERACTIVE", None)
383
+ else:
384
+ os.environ["OWNAPI_NON_INTERACTIVE"] = prev_ni
385
+
386
+ def validate_all(self, runs: int = 1, auto_heal: bool = True, services: list[str] | None = None) -> dict[str, Any]:
387
+ return self.auto_repair.validate_all_services(runs=runs, auto_heal=auto_heal, services=services)
388
+
389
+ def list_hitl_queue(self, status_filter: str | None = None) -> list[dict[str, Any]]:
390
+ return self.hitl_queue.list_tasks(tenant_id=self.tenant_id, status_filter=status_filter)
391
+
392
+ def resume_hitl_task(self, task_id: str, otp_code: str | None = None) -> dict[str, Any]:
393
+ return self.hitl_queue.resume_task(task_id=task_id, tenant_id=self.tenant_id, otp_code=otp_code)
394
+
395
+ def onboard(
396
+ self,
397
+ service_name: str,
398
+ start_url: str,
399
+ description: str = "",
400
+ headless: bool = False,
401
+ mode: str | None = None,
402
+ auto_validate: bool = True,
403
+ validation_runs: int = 3,
404
+ interactive_tos: bool = False,
405
+ ) -> dict[str, Any]:
406
+ active_mode = (mode or self.mode or "local").strip().lower()
407
+ return onboard_new_site(
408
+ service_name=service_name,
409
+ start_url=start_url,
410
+ description=description,
411
+ headless=headless,
412
+ registry_root=self.registry_root,
413
+ tenant_id=self.tenant_id,
414
+ mode=active_mode,
415
+ auto_validate=auto_validate,
416
+ validation_runs=validation_runs,
417
+ interactive_tos=interactive_tos,
418
+ )
419
+
420
+ def ask(self, prompt: str, _consume_credit: bool = True) -> str:
421
+ from agent_demo import ShadowAgent
422
+
423
+ auth_id = self.api_key or self.tenant_id
424
+ info = get_tenant_info(auth_id, state_file=self._state_file)
425
+ if _consume_credit:
426
+ consume_credit(auth_id, amount=1, state_file=self._state_file)
427
+ self.mode = str(self._explicit_mode or info.get("mode") or self.mode or "local").lower()
428
+
429
+ if self._agent is None:
430
+ self._agent = ShadowAgent(registry_root=self.registry_root)
431
+ return self._agent.ask(prompt)
432
+
433
+
434
+ # Geriye dönük uyumluluk takma adları
435
+ OwnAPIClient = OwnAPI
436
+ ShadowClient = OwnAPI