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/__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)
|