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/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """OwnAPI Core Engine — TokenStore, ConsentManager, BaseOwnClient, Onboarding, AutoRepairEngine & HITLQueueManager."""
2
+
3
+ from core.auto_repair import AutoRepairEngine
4
+ from core.base_client import BaseOwnClient, BaseShadowClient
5
+ from core.consent import ConsentManager, USER_DELEGATION_DISCLAIMER
6
+ from core.hitl_queue import HITLQueueManager
7
+ from core.onboarding import onboard_new_site
8
+ from core.token_store import TokenStore
9
+
10
+ __all__ = [
11
+ "AutoRepairEngine",
12
+ "BaseOwnClient",
13
+ "BaseShadowClient",
14
+ "ConsentManager",
15
+ "HITLQueueManager",
16
+ "USER_DELEGATION_DISCLAIMER",
17
+ "TokenStore",
18
+ "onboard_new_site",
19
+ ]
core/api_keys.py ADDED
@@ -0,0 +1,523 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import secrets
7
+ import threading
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
13
+ SESSIONS_ROOT = PROJECT_ROOT / ".sessions"
14
+ API_KEYS_STATE_FILE = SESSIONS_ROOT / "api_keys.json"
15
+
16
+ _STATE_LOCK = threading.RLock()
17
+
18
+ PLAN_CATALOG: dict[str, dict[str, Any]] = {
19
+ "free": {
20
+ "plan": "free",
21
+ "label": "Free Local Plan",
22
+ "credits": 1_000,
23
+ "mode": "local",
24
+ "price_usd": 0,
25
+ "max_sites": 2,
26
+ },
27
+ "starter_cloud": {
28
+ "plan": "starter_cloud",
29
+ "label": "Starter Cloud Plan ($10)",
30
+ "credits": 10_000,
31
+ "mode": "cloud",
32
+ "price_usd": 10,
33
+ "max_sites": 5,
34
+ },
35
+ "growth_cloud": {
36
+ "plan": "growth_cloud",
37
+ "label": "Growth Cloud Plan ($49)",
38
+ "credits": 100_000,
39
+ "mode": "cloud",
40
+ "price_usd": 49,
41
+ "max_sites": 50,
42
+ },
43
+ }
44
+
45
+ SITE_LIMIT_WARNING_MESSAGE = "Paketinizin maksimum site limitine ulaştınız. Bulut paketine yükseltin."
46
+
47
+
48
+ class InsufficientCreditsError(ValueError):
49
+ """Kullanıcının kalan kredisi talep edilen işlem miktarından az olduğunda fırlatılır."""
50
+
51
+
52
+ class SiteLimitExceededError(ValueError):
53
+ """Kullanıcının paketindeki maksimum site/konektör limiti aşıldığında fırlatılır."""
54
+
55
+ def __init__(self, message: str = SITE_LIMIT_WARNING_MESSAGE) -> None:
56
+ super().__init__(message)
57
+
58
+
59
+ class APIKeyResult(str):
60
+ """
61
+ Hem doğrudan `str` (`"sk-ownapi-..."`) olarak kullanılabilen,
62
+ hem de sözlük/nesne (`res["api_key"]`, `res["credits"]`, `res.plan`, `res.to_dict()`)
63
+ erişimini destekleyen hibrit dönüş sınıfı.
64
+ """
65
+
66
+ def __new__(cls, api_key: str, metadata: dict[str, Any] | None = None) -> APIKeyResult:
67
+ obj = super().__new__(cls, api_key)
68
+ obj._meta = dict(metadata or {})
69
+ obj._meta.setdefault("api_key", api_key)
70
+ return obj
71
+
72
+ @property
73
+ def api_key(self) -> str:
74
+ return str(self)
75
+
76
+ @property
77
+ def tenant_id(self) -> str:
78
+ return str(self._meta.get("tenant_id", "default"))
79
+
80
+ @property
81
+ def plan(self) -> str:
82
+ return str(self._meta.get("plan", "free"))
83
+
84
+ @property
85
+ def credits(self) -> int:
86
+ return int(self._meta.get("credits", 0))
87
+
88
+ @property
89
+ def mode(self) -> str:
90
+ return str(self._meta.get("mode", "local"))
91
+
92
+ @property
93
+ def price_usd(self) -> int:
94
+ return int(self._meta.get("price_usd", 0))
95
+
96
+ def __getitem__(self, item: str | int | slice) -> Any:
97
+ if isinstance(item, (int, slice)):
98
+ return super().__getitem__(item)
99
+ return self._meta[item]
100
+
101
+ def get(self, key: str, default: Any = None) -> Any:
102
+ return self._meta.get(key, default)
103
+
104
+ def keys(self) -> Any:
105
+ return self._meta.keys()
106
+
107
+ def items(self) -> Any:
108
+ return self._meta.items()
109
+
110
+ def to_dict(self) -> dict[str, Any]:
111
+ return dict(self._meta)
112
+
113
+
114
+ class CreditBalance(int):
115
+ """
116
+ Hem doğrudan `int` (kalan kredi sayısı) olarak kullanılabilen,
117
+ hem de `.get(...)` / `["credits"]` / `.to_dict()` ile detay bilgisi sunan hibrit tam sayı sınıfı.
118
+ """
119
+
120
+ def __new__(cls, remaining_credits: int, metadata: dict[str, Any] | None = None) -> CreditBalance:
121
+ obj = super().__new__(cls, int(remaining_credits))
122
+ obj._meta = dict(metadata or {})
123
+ obj._meta["credits"] = int(remaining_credits)
124
+ obj._meta["remaining_credits"] = int(remaining_credits)
125
+ return obj
126
+
127
+ @property
128
+ def remaining_credits(self) -> int:
129
+ return int(self)
130
+
131
+ @property
132
+ def credits(self) -> int:
133
+ return int(self)
134
+
135
+ @property
136
+ def tenant_id(self) -> str:
137
+ return str(self._meta.get("tenant_id", "default"))
138
+
139
+ @property
140
+ def plan(self) -> str:
141
+ return str(self._meta.get("plan", "free"))
142
+
143
+ @property
144
+ def mode(self) -> str:
145
+ return str(self._meta.get("mode", "local"))
146
+
147
+ def __getitem__(self, item: str) -> Any:
148
+ return self._meta[item]
149
+
150
+ def get(self, key: str, default: Any = None) -> Any:
151
+ return self._meta.get(key, default)
152
+
153
+ def to_dict(self) -> dict[str, Any]:
154
+ return dict(self._meta)
155
+
156
+
157
+ def _sanitize_tenant_id(tenant_id: str) -> str:
158
+ clean = re.sub(r"[^a-zA-Z0-9_\-\.]", "_", (tenant_id or "default").strip())
159
+ return clean or "default"
160
+
161
+
162
+ def _normalize_plan(plan: str | None) -> str:
163
+ clean = (plan or "free").strip().lower()
164
+ if clean not in PLAN_CATALOG:
165
+ raise ValueError(
166
+ f"Geçersiz paket tipi: '{plan}'. Desteklenen paketler: {list(PLAN_CATALOG.keys())}"
167
+ )
168
+ return clean
169
+
170
+
171
+ def _build_secret_key() -> str:
172
+ """`sk-ownapi-` ile başlayan kriptografik olarak güvenli ve benzersiz bir API anahtarı üretir."""
173
+ return f"sk-ownapi-{secrets.token_urlsafe(24)}"
174
+
175
+
176
+ class APIKeyManager:
177
+ """
178
+ `.sessions/api_keys.json` üzerinde kiracıların (`tenant_id`) API anahtarlarını,
179
+ paket tiplerini (`free`, `starter_cloud`, `growth_cloud`), çalışma modlarını (`local` / `cloud`)
180
+ ve kalan kredilerini kalıcı olarak yöneten sınıf.
181
+ """
182
+
183
+ def __init__(self, state_file: Path | str | None = None) -> None:
184
+ self.state_file = Path(state_file) if state_file else API_KEYS_STATE_FILE
185
+ self.state_file.parent.mkdir(parents=True, exist_ok=True)
186
+
187
+ def _load_state(self) -> dict[str, Any]:
188
+ with _STATE_LOCK:
189
+ if not self.state_file.exists():
190
+ return {"updated_at": None, "tenants": {}, "key_index": {}}
191
+ try:
192
+ raw = json.loads(self.state_file.read_text(encoding="utf-8"))
193
+ if isinstance(raw, dict):
194
+ raw.setdefault("tenants", {})
195
+ raw.setdefault("key_index", {})
196
+ return raw
197
+ except Exception:
198
+ pass
199
+ return {"updated_at": None, "tenants": {}, "key_index": {}}
200
+
201
+ def _save_state(self, state: dict[str, Any]) -> None:
202
+ with _STATE_LOCK:
203
+ self.state_file.parent.mkdir(parents=True, exist_ok=True)
204
+ now_iso = datetime.now(timezone.utc).isoformat()
205
+ state["updated_at"] = now_iso
206
+
207
+ # key_index tablosunu tenants üzerinden senkronize et
208
+ tenants = state.get("tenants", {})
209
+ key_index: dict[str, str] = {}
210
+ if isinstance(tenants, dict):
211
+ for t_id, t_data in tenants.items():
212
+ if isinstance(t_data, dict) and t_data.get("api_key"):
213
+ key_index[str(t_data["api_key"])] = t_id
214
+ state["key_index"] = key_index
215
+
216
+ tmp_file = self.state_file.with_suffix(f".tmp.{os.getpid()}")
217
+ tmp_file.write_text(
218
+ json.dumps(state, ensure_ascii=False, indent=2),
219
+ encoding="utf-8",
220
+ )
221
+ tmp_file.replace(self.state_file)
222
+
223
+ @staticmethod
224
+ def _ensure_site_fields(record: dict[str, Any]) -> bool:
225
+ """Kayıt üzerinde `max_sites` ve `active_sites` alanlarının varlığını garanti eder."""
226
+ changed = False
227
+ plan_key = str(record.get("plan") or "free").strip().lower()
228
+ plan_meta = PLAN_CATALOG.get(plan_key, PLAN_CATALOG["free"])
229
+ expected_max = int(plan_meta.get("max_sites", 2))
230
+ if "max_sites" not in record or not isinstance(record.get("max_sites"), int):
231
+ record["max_sites"] = expected_max
232
+ changed = True
233
+ raw_sites = record.get("active_sites")
234
+ if not isinstance(raw_sites, list):
235
+ record["active_sites"] = []
236
+ changed = True
237
+ else:
238
+ normalized_sites: list[str] = []
239
+ for s in raw_sites:
240
+ cs = str(s or "").strip().lower()
241
+ if cs and cs not in normalized_sites:
242
+ normalized_sites.append(cs)
243
+ if normalized_sites != raw_sites:
244
+ record["active_sites"] = normalized_sites
245
+ changed = True
246
+ return changed
247
+
248
+ def get_or_create_tenant(self, tenant_id: str = "default") -> dict[str, Any]:
249
+ """
250
+ Kullanıcı (`tenant_id`) kaydını döndürür.
251
+ Eğer kullanıcı henüz kayıtlı değilse varsayılan olarak `free` paket
252
+ (1.000 kredi, `mode="local"`, `max_sites=2`, `active_sites=[]`) ve yeni bir `sk-ownapi-...` anahtarı ile oluşturur.
253
+ """
254
+ clean_tenant = _sanitize_tenant_id(tenant_id)
255
+ with _STATE_LOCK:
256
+ state = self._load_state()
257
+ tenants = state.setdefault("tenants", {})
258
+ if clean_tenant in tenants and isinstance(tenants[clean_tenant], dict):
259
+ rec = tenants[clean_tenant]
260
+ if self._ensure_site_fields(rec):
261
+ self._save_state(state)
262
+ return dict(rec)
263
+
264
+ plan_meta = PLAN_CATALOG["free"]
265
+ now_iso = datetime.now(timezone.utc).isoformat()
266
+ api_key = _build_secret_key()
267
+ record: dict[str, Any] = {
268
+ "tenant_id": clean_tenant,
269
+ "api_key": api_key,
270
+ "plan": plan_meta["plan"],
271
+ "plan_label": plan_meta["label"],
272
+ "credits": int(plan_meta["credits"]),
273
+ "mode": plan_meta["mode"],
274
+ "price_usd": int(plan_meta["price_usd"]),
275
+ "max_sites": int(plan_meta["max_sites"]),
276
+ "active_sites": [],
277
+ "total_consumed": 0,
278
+ "created_at": now_iso,
279
+ "updated_at": now_iso,
280
+ }
281
+ tenants[clean_tenant] = record
282
+ self._save_state(state)
283
+ return dict(record)
284
+
285
+ def generate_api_key(self, tenant_id: str = "default", plan: str = "free") -> APIKeyResult:
286
+ """
287
+ Belirtilen `tenant_id` ve `plan` (`free`, `starter_cloud`, `growth_cloud`) için
288
+ `sk-ownapi-` ile başlayan güvenli bir API anahtarı üretir ve `.sessions/api_keys.json`
289
+ dosyasına kaydeder.
290
+ """
291
+ clean_tenant = _sanitize_tenant_id(tenant_id)
292
+ clean_plan = _normalize_plan(plan)
293
+ plan_meta = PLAN_CATALOG[clean_plan]
294
+
295
+ with _STATE_LOCK:
296
+ state = self._load_state()
297
+ tenants = state.setdefault("tenants", {})
298
+ existing = tenants.get(clean_tenant) if isinstance(tenants.get(clean_tenant), dict) else {}
299
+ now_iso = datetime.now(timezone.utc).isoformat()
300
+ new_key = _build_secret_key()
301
+ existing_sites = existing.get("active_sites") if isinstance(existing.get("active_sites"), list) else []
302
+
303
+ record: dict[str, Any] = {
304
+ "tenant_id": clean_tenant,
305
+ "api_key": new_key,
306
+ "plan": plan_meta["plan"],
307
+ "plan_label": plan_meta["label"],
308
+ "credits": int(plan_meta["credits"]),
309
+ "mode": plan_meta["mode"],
310
+ "price_usd": int(plan_meta["price_usd"]),
311
+ "max_sites": int(plan_meta["max_sites"]),
312
+ "active_sites": list(existing_sites),
313
+ "total_consumed": int(existing.get("total_consumed", 0)),
314
+ "created_at": existing.get("created_at") or now_iso,
315
+ "updated_at": now_iso,
316
+ }
317
+ self._ensure_site_fields(record)
318
+ tenants[clean_tenant] = record
319
+ self._save_state(state)
320
+ return APIKeyResult(new_key, record)
321
+
322
+ def resolve_tenant_id(self, tenant_id_or_key: str) -> str:
323
+ """
324
+ Verilen değerin `sk-ownapi-...` anahtarı mı yoksa `tenant_id` mi olduğunu çözer.
325
+ """
326
+ raw = (tenant_id_or_key or "default").strip()
327
+ with _STATE_LOCK:
328
+ state = self._load_state()
329
+ key_index = state.get("key_index", {})
330
+ if raw in key_index:
331
+ return str(key_index[raw])
332
+ for t_id, t_data in state.get("tenants", {}).items():
333
+ if isinstance(t_data, dict) and t_data.get("api_key") == raw:
334
+ return str(t_id)
335
+ if raw.startswith("sk-ownapi-"):
336
+ raise KeyError(f"Geçersiz veya tanımsız API anahtarı: '{raw}'")
337
+ return _sanitize_tenant_id(raw)
338
+
339
+ def register_site_usage(self, tenant_id_or_key: str, service_name: str | None) -> dict[str, Any]:
340
+ """
341
+ Kullanıcının bir site/konektör kullanımını `active_sites` listesine kaydeder.
342
+ Eğer site zaten `active_sites` içindeyse izin verir; yeni bir site eklenirken
343
+ `len(active_sites) >= max_sites` ise `SiteLimitExceededError` fırlatır:
344
+ "Paketinizin maksimum site limitine ulaştınız. Bulut paketine yükseltin."
345
+ """
346
+ clean_site = re.sub(r"[^a-zA-Z0-9_\-\.]", "_", (service_name or "").strip().lower()).strip("_")
347
+ with _STATE_LOCK:
348
+ clean_tenant = self.resolve_tenant_id(tenant_id_or_key)
349
+ state = self._load_state()
350
+ tenants = state.setdefault("tenants", {})
351
+ if clean_tenant not in tenants or not isinstance(tenants[clean_tenant], dict):
352
+ self.get_or_create_tenant(clean_tenant)
353
+ state = self._load_state()
354
+ tenants = state.setdefault("tenants", {})
355
+
356
+ record = tenants[clean_tenant]
357
+ self._ensure_site_fields(record)
358
+
359
+ if not clean_site:
360
+ return dict(record)
361
+
362
+ active_sites: list[str] = list(record.get("active_sites", []))
363
+ max_sites = int(record.get("max_sites", PLAN_CATALOG.get(record.get("plan", "free"), PLAN_CATALOG["free"])["max_sites"]))
364
+
365
+ if clean_site not in active_sites:
366
+ if len(active_sites) >= max_sites:
367
+ raise SiteLimitExceededError(SITE_LIMIT_WARNING_MESSAGE)
368
+ active_sites.append(clean_site)
369
+ record["active_sites"] = active_sites
370
+ record["updated_at"] = datetime.now(timezone.utc).isoformat()
371
+ tenants[clean_tenant] = record
372
+ self._save_state(state)
373
+
374
+ return dict(record)
375
+
376
+ def consume_credit(
377
+ self,
378
+ tenant_id_or_key: str,
379
+ amount: int = 1,
380
+ service: str | None = None,
381
+ ) -> CreditBalance:
382
+ """
383
+ Her kullanımda kullanıcının (`tenant_id` veya `sk-ownapi-...` anahtarı) kredisinden
384
+ `amount` kadar düşer, `.sessions/api_keys.json` dosyasını günceller ve kalan krediyi döndürür.
385
+ Yeni bir `tenant_id` ile çağrılırsa önce varsayılan `free` paketi (1.000 kredi, `mode="local"`) tanımlar.
386
+ Eğer `service` belirtilmişse önce site limitini kontrol edip `active_sites` listesine ekler.
387
+ """
388
+ if amount < 0:
389
+ raise ValueError("Düşülecek kredi miktarı (amount) negatif olamaz.")
390
+
391
+ with _STATE_LOCK:
392
+ clean_tenant = self.resolve_tenant_id(tenant_id_or_key)
393
+ state = self._load_state()
394
+ tenants = state.setdefault("tenants", {})
395
+ if clean_tenant not in tenants or not isinstance(tenants[clean_tenant], dict):
396
+ self.get_or_create_tenant(clean_tenant)
397
+ state = self._load_state()
398
+ tenants = state.setdefault("tenants", {})
399
+
400
+ record = tenants[clean_tenant]
401
+ self._ensure_site_fields(record)
402
+
403
+ if service:
404
+ self.register_site_usage(clean_tenant, service)
405
+ state = self._load_state()
406
+ tenants = state.setdefault("tenants", {})
407
+ record = tenants[clean_tenant]
408
+
409
+ current_credits = int(record.get("credits", 0))
410
+ if current_credits < amount:
411
+ raise InsufficientCreditsError(
412
+ f"Yetersiz kredi! Kiracı '{clean_tenant}' ({record.get('plan')}) için "
413
+ f"kalan kredi: {current_credits}, gereken: {amount}."
414
+ )
415
+
416
+ remaining = current_credits - int(amount)
417
+ record["credits"] = remaining
418
+ record["total_consumed"] = int(record.get("total_consumed", 0)) + int(amount)
419
+ record["updated_at"] = datetime.now(timezone.utc).isoformat()
420
+ tenants[clean_tenant] = record
421
+ self._save_state(state)
422
+ return CreditBalance(remaining, record)
423
+
424
+ def upgrade_plan(self, tenant_id: str, plan: str = "starter_cloud") -> APIKeyResult:
425
+ """
426
+ Kullanıcıyı belirtilen pakete geçirir ve yeni `sk-ownapi-...` anahtarını döndürür:
427
+ - `starter_cloud`: $10 bulut paketi -> 10.000 kredi, `mode="cloud"`, `max_sites=5`
428
+ - `growth_cloud`: $49 bulut paketi -> 100.000 kredi, `mode="cloud"`, `max_sites=50`
429
+ - `free`: $0 yerel paket -> 1.000 kredi, `mode="local"`, `max_sites=2`
430
+ """
431
+ clean_tenant = _sanitize_tenant_id(tenant_id)
432
+ clean_plan = _normalize_plan(plan)
433
+ plan_meta = PLAN_CATALOG[clean_plan]
434
+
435
+ with _STATE_LOCK:
436
+ state = self._load_state()
437
+ tenants = state.setdefault("tenants", {})
438
+ existing = tenants.get(clean_tenant) if isinstance(tenants.get(clean_tenant), dict) else {}
439
+ now_iso = datetime.now(timezone.utc).isoformat()
440
+ new_key = _build_secret_key()
441
+ existing_sites = existing.get("active_sites") if isinstance(existing.get("active_sites"), list) else []
442
+
443
+ record: dict[str, Any] = {
444
+ "tenant_id": clean_tenant,
445
+ "api_key": new_key,
446
+ "plan": plan_meta["plan"],
447
+ "plan_label": plan_meta["label"],
448
+ "credits": int(plan_meta["credits"]),
449
+ "mode": plan_meta["mode"],
450
+ "price_usd": int(plan_meta["price_usd"]),
451
+ "max_sites": int(plan_meta["max_sites"]),
452
+ "active_sites": list(existing_sites),
453
+ "total_consumed": int(existing.get("total_consumed", 0)),
454
+ "created_at": existing.get("created_at") or now_iso,
455
+ "upgraded_at": now_iso,
456
+ "updated_at": now_iso,
457
+ }
458
+ self._ensure_site_fields(record)
459
+ tenants[clean_tenant] = record
460
+ self._save_state(state)
461
+ return APIKeyResult(new_key, record)
462
+
463
+ def get_tenant_info(self, tenant_id_or_key: str = "default") -> dict[str, Any]:
464
+ """Kullanıcının güncel paket, mod, anahtar, site limiti ve kalan kredi bilgilerini döndürür."""
465
+ clean_tenant = self.resolve_tenant_id(tenant_id_or_key)
466
+ return self.get_or_create_tenant(clean_tenant)
467
+
468
+
469
+ # Modül düzeyinde varsayılan yönetici ve doğrudan çağrılabilir fonksiyonlar
470
+ _default_manager = APIKeyManager()
471
+
472
+
473
+ def get_or_create_tenant(
474
+ tenant_id: str = "default",
475
+ state_file: Path | str | None = None,
476
+ ) -> dict[str, Any]:
477
+ mgr = APIKeyManager(state_file=state_file) if state_file else _default_manager
478
+ return mgr.get_or_create_tenant(tenant_id=tenant_id)
479
+
480
+
481
+ def generate_api_key(
482
+ tenant_id: str = "default",
483
+ plan: str = "free",
484
+ state_file: Path | str | None = None,
485
+ ) -> APIKeyResult:
486
+ mgr = APIKeyManager(state_file=state_file) if state_file else _default_manager
487
+ return mgr.generate_api_key(tenant_id=tenant_id, plan=plan)
488
+
489
+
490
+ def register_site_usage(
491
+ tenant_id_or_key: str,
492
+ service_name: str | None,
493
+ state_file: Path | str | None = None,
494
+ ) -> dict[str, Any]:
495
+ mgr = APIKeyManager(state_file=state_file) if state_file else _default_manager
496
+ return mgr.register_site_usage(tenant_id_or_key=tenant_id_or_key, service_name=service_name)
497
+
498
+
499
+ def consume_credit(
500
+ tenant_id_or_key: str,
501
+ amount: int = 1,
502
+ state_file: Path | str | None = None,
503
+ service: str | None = None,
504
+ ) -> CreditBalance:
505
+ mgr = APIKeyManager(state_file=state_file) if state_file else _default_manager
506
+ return mgr.consume_credit(tenant_id_or_key=tenant_id_or_key, amount=amount, service=service)
507
+
508
+
509
+ def upgrade_plan(
510
+ tenant_id: str,
511
+ plan: str = "starter_cloud",
512
+ state_file: Path | str | None = None,
513
+ ) -> APIKeyResult:
514
+ mgr = APIKeyManager(state_file=state_file) if state_file else _default_manager
515
+ return mgr.upgrade_plan(tenant_id=tenant_id, plan=plan)
516
+
517
+
518
+ def get_tenant_info(
519
+ tenant_id_or_key: str = "default",
520
+ state_file: Path | str | None = None,
521
+ ) -> dict[str, Any]:
522
+ mgr = APIKeyManager(state_file=state_file) if state_file else _default_manager
523
+ return mgr.get_tenant_info(tenant_id_or_key=tenant_id_or_key)