Download t2s/llm.py from SevdanurGenc/Text-to-SQL: direct link, hf CLI and curl.
- Browser
- Download file 21.6 kB
-
https://huggingface.co/SevdanurGenc/Text-to-SQL/resolve/main/t2s/llm.py
- Command line
-
hf download hf://SevdanurGenc/Text-to-SQL/t2s/llm.py
-
curl -L -o llm.py https://huggingface.co/SevdanurGenc/Text-to-SQL/resolve/main/t2s/llm.py
21.6 kB
| """LLM arka uçları ve model senkronizasyonu. | |
| Sağlayıcılar | |
| claude : Anthropic Messages API (ANTHROPIC_API_KEY, isteğe bağlı ANTHROPIC_BASE_URL) | |
| openai : OpenAI veya OpenAI uyumlu uç nokta (OPENAI_API_KEY, OPENAI_BASE_URL; vLLM, LM Studio, OpenRouter…) | |
| ollama : Yerel Ollama sunucusu (OLLAMA_HOST veya OLLAMA_BASE_URL, vars. http://localhost:11434) | |
| Model listeleri doğrudan sağlayıcının API'sinden çekilir (list_models); kullanıcı model adını elle yazmak | |
| zorunda kalmaz. Her çağrının sayısı ve belirteç kullanımı USAGE içinde tutulur. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import re | |
| import time | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| import requests | |
| ANTHROPIC_VERSION = "2023-06-01" | |
| DEFAULT_TIMEOUT = 120 | |
| SYSTEM_PROMPT = """[GÖREV:SQL] | |
| Sen, Türkçe resmi belgelerden (rapor, mevzuat, finansal tablo) otomatik çıkarılmış tablolar üzerinde çalışan bir SQLite uzmanısın. Kullanıcının sorusunu (Türkçe veya İngilizce olabilir) TEK bir SQLite SELECT sorgusuna çevir. | |
| Veritabanı kuralları: | |
| - Sayılar veritabanında sayısal tipte ve nokta ondalık ayraçla saklanır. Belgedeki '1.234,56' değeri 1234.56'dır. SQL'e asla Türkçe biçimli sayı ('1.234,56', '4,5') yazma. | |
| - Yüzde sütunları (birim: %) yüzde puanı olarak saklanır: '%94,3' -> 94.3. | |
| - Tarihler ISO biçimindedir: tam tarih 'YYYY-AA-GG', ay-yıl 'YYYY-AA'. '15.03.2025' -> '2025-03-15', 'Mart 2025' -> '2025-03'. | |
| - Tablolardaki metin değerleri Türkçedir; İngilizce soruda geçen adları Türkçe karşılıklarıyla eşle (ör. 'personnel expenses' -> 'Personel Giderleri'). | |
| - Metin karşılaştırmalarında Türkçe İ/ı sorunlarına karşı TR_NORM(sutun) = TR_NORM('değer') kullanabilirsin. | |
| - Birimler sütun açıklamalarında verilir (ör. 'bin TL', 'bin MWh'). Soru başka bir birim isterse dönüştür (bin TL -> TL için *1000). | |
| - 'toplam_satiri' sütunu olan tablolarda toplam_satiri = 1 satırı belgedeki hazır Toplam satırıdır. Toplama, ortalama, sayım ve sıralamalarda WHERE toplam_satiri = 0 kullan. | |
| - Negatif değerler giderleri gösterebilir (finansal tablolarda parantezli sayılar). | |
| - Yüzde değişim = (yeni - eski) * 100.0 / eski. Tamsayı bölmesinden kaçınmak için 100.0 veya 1.0 ile çarp. | |
| - Yalnızca verilen tablo ve sütunları kullan. Sütun adlarını çift tırnakla yazabilirsin. | |
| Cevaplanabilirlik: | |
| - Sorunun cevabı tablolarda yoksa (tabloda olmayan yıl, dönem, il, kalem, kurum vb.) cevap UYDURMA; "cevaplanabilir": false döndür ve gerekçesini sorunun dilinde yaz. | |
| Çıktı: Yalnızca şu JSON nesnesi, başka metin yok: | |
| {"dusunce": "<kısa gerekçe: hangi tablo/sütun/koşul>", "cevaplanabilir": true|false, "sql": "<SQLite sorgusu veya boş>", "gerekce": "<cevaplanamıyorsa nedeni>"}""" | |
| def build_user_prompt(question: str, schema: str, evidence: str | None = None) -> str: | |
| s = f"### Şema\n{schema}\n\n### Soru\n{question}\n" | |
| if evidence: | |
| s += f"\n### Ek bilgi (kanıt)\n{evidence}\n" | |
| return s | |
| def parse_json_reply(text: str) -> dict: | |
| text = (text or "").strip() | |
| m = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.S) | |
| if m: | |
| text = m.group(1) | |
| else: | |
| m = re.search(r"\{.*\}", text, re.S) | |
| if m: | |
| text = m.group(0) | |
| try: | |
| d = json.loads(text) | |
| except json.JSONDecodeError: | |
| m = re.search(r"(select|with)\b.*", text, re.I | re.S) | |
| return {"cevaplanabilir": bool(m), "sql": m.group(0).strip().strip("`") if m else "", | |
| "gerekce": "" if m else "Model geçerli JSON üretmedi.", "dusunce": ""} | |
| if not isinstance(d, dict): | |
| return {"cevaplanabilir": False, "sql": "", "gerekce": "Model geçerli JSON üretmedi.", "dusunce": ""} | |
| d.setdefault("cevaplanabilir", bool(d.get("sql"))) | |
| d.setdefault("sql", "") | |
| d.setdefault("gerekce", "") | |
| return d | |
| # ======================================================================= kullanım sayacı | |
| class Usage: | |
| calls: int = 0 | |
| errors: int = 0 | |
| input_tokens: int = 0 | |
| output_tokens: int = 0 | |
| seconds: float = 0.0 | |
| by_task: dict = field(default_factory=dict) | |
| def add(self, task: str, inp: int, out: int, secs: float, ok: bool = True): | |
| self.calls += 1 | |
| self.errors += 0 if ok else 1 | |
| self.input_tokens += inp or 0 | |
| self.output_tokens += out or 0 | |
| self.seconds += secs | |
| self.by_task[task] = self.by_task.get(task, 0) + 1 | |
| USAGE = Usage() | |
| def _task_of(system: str) -> str: | |
| m = re.search(r"\[GÖREV:([A-Z_]+)\]", system or "") | |
| return m.group(1) if m else "DIGER" | |
| # ======================================================================= arka uçlar | |
| class LLMBackend: | |
| provider = "llm" | |
| name = "llm" | |
| model = "" | |
| def chat(self, system: str, messages: list[dict], max_tokens: int = 800) -> str: | |
| raise NotImplementedError | |
| def ping(self) -> tuple[bool, str]: | |
| try: | |
| t0 = time.time() | |
| out = self.chat("[GÖREV:PING] Yalnızca 'ok' yaz.", [{"role": "user", "content": "ping"}], max_tokens=5) | |
| return True, f"{out.strip()[:40]} · {time.time() - t0:.1f} sn" | |
| except Exception as e: # noqa: BLE001 | |
| return False, str(e)[:300] | |
| # Bazı yeni modeller belirli parametreleri reddeder (ör. Claude'un yeni modellerinde `temperature`, | |
| # OpenAI'nin yeni modellerinde `max_tokens`). Reddedilen parametre model başına öğrenilir ve istek | |
| # o parametre olmadan bir kez daha gönderilir. | |
| _DROPPED: dict[str, set[str]] = {} | |
| _PARAM_RE = re.compile(r"(temperature|max_tokens)") | |
| def _learn_rejected_param(name: str, r) -> bool: | |
| """400 yanıtı bir parametrenin desteklenmediğini söylüyorsa onu kaydeder ve True döndürür.""" | |
| if r.status_code != 400: | |
| return False | |
| txt = r.text[:600].lower() | |
| if not any(w in txt for w in ("unsupported", "deprecated", "not supported", "does not support")): | |
| return False | |
| m = _PARAM_RE.search(txt) | |
| if not m or m.group(1) in _DROPPED.get(name, set()): | |
| return False | |
| _DROPPED.setdefault(name, set()).add(m.group(1)) | |
| return True | |
| class AnthropicBackend(LLMBackend): | |
| provider = "claude" | |
| def __init__(self, model: str | None = None, api_key: str | None = None, temperature: float = 0.0, | |
| base_url: str | None = None): | |
| self.model = model or os.getenv("ANTHROPIC_MODEL", "") | |
| self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY") | |
| self.base_url = (base_url or os.getenv("ANTHROPIC_BASE_URL") or "https://api.anthropic.com").rstrip("/") | |
| self.temperature = temperature | |
| self.name = f"claude:{self.model}" | |
| if not self.api_key: | |
| raise RuntimeError("ANTHROPIC_API_KEY tanımlı değil.") | |
| if not self.model: | |
| raise RuntimeError("Claude modeli seçilmedi.") | |
| def chat(self, system, messages, max_tokens=800): | |
| t0 = time.time() | |
| task = _task_of(system) | |
| try: | |
| for _ in range(3): | |
| body = {"model": self.model, "max_tokens": max_tokens, "system": system, "messages": messages} | |
| if "temperature" not in _DROPPED.get(self.name, set()): | |
| body["temperature"] = self.temperature | |
| r = requests.post(f"{self.base_url}/v1/messages", timeout=DEFAULT_TIMEOUT, | |
| headers={"x-api-key": self.api_key, "anthropic-version": ANTHROPIC_VERSION, | |
| "content-type": "application/json"}, json=body) | |
| if not _learn_rejected_param(self.name, r): | |
| break | |
| if r.status_code >= 400: | |
| raise RuntimeError(f"Anthropic API {r.status_code}: {r.text[:300]}") | |
| d = r.json() | |
| u = d.get("usage", {}) | |
| USAGE.add(task, u.get("input_tokens", 0), u.get("output_tokens", 0), time.time() - t0) | |
| return "".join(b.get("text", "") for b in d.get("content", []) if b.get("type") == "text") | |
| except Exception: | |
| USAGE.add(task, 0, 0, time.time() - t0, ok=False) | |
| raise | |
| class OpenAICompatBackend(LLMBackend): | |
| provider = "openai" | |
| def __init__(self, model: str, base_url: str | None = None, api_key: str | None = None, | |
| temperature: float = 0.0, timeout: int = DEFAULT_TIMEOUT): | |
| if not model: | |
| raise RuntimeError("Model seçilmedi.") | |
| self.model = model | |
| self.base_url = (base_url or os.getenv("OPENAI_BASE_URL") or "https://api.openai.com/v1").rstrip("/") | |
| self.api_key = api_key or os.getenv("OPENAI_API_KEY") or "yok" | |
| self.temperature = temperature | |
| self.timeout = timeout | |
| self.name = f"openai:{model}" | |
| def _payload(self, system, messages, max_tokens): | |
| p = {"model": self.model, "messages": [{"role": "system", "content": system}] + messages} | |
| dropped = _DROPPED.get(self.name, set()) | |
| # Yeni OpenAI akıl yürütme modelleri max_tokens / temperature kabul etmez | |
| new_style = "api.openai.com" in self.base_url and re.match(r"^(o\d|gpt-([5-9]|\d\d))", self.model) | |
| if new_style or "max_tokens" in dropped: | |
| p["max_completion_tokens"] = max(max_tokens, 2000) | |
| else: | |
| p["max_tokens"] = max_tokens | |
| if not new_style and "temperature" not in dropped: | |
| p["temperature"] = self.temperature | |
| return p | |
| def chat(self, system, messages, max_tokens=800): | |
| t0 = time.time() | |
| task = _task_of(system) | |
| try: | |
| for _ in range(3): | |
| r = requests.post(f"{self.base_url}/chat/completions", timeout=self.timeout, | |
| headers={"Authorization": f"Bearer {self.api_key}"}, | |
| json=self._payload(system, messages, max_tokens)) | |
| if not _learn_rejected_param(self.name, r): | |
| break | |
| if r.status_code >= 400: | |
| raise RuntimeError(f"API {r.status_code}: {r.text[:300]}") | |
| d = r.json() | |
| u = d.get("usage") or {} | |
| USAGE.add(task, u.get("prompt_tokens", 0), u.get("completion_tokens", 0), time.time() - t0) | |
| return d["choices"][0]["message"]["content"] or "" | |
| except Exception: | |
| USAGE.add(task, 0, 0, time.time() - t0, ok=False) | |
| raise | |
| def ollama_host() -> str: | |
| h = os.getenv("OLLAMA_HOST") or os.getenv("OLLAMA_BASE_URL") or "http://localhost:11434" | |
| if not h.startswith("http"): | |
| h = "http://" + h | |
| return re.sub(r"/v1/?$", "", h.rstrip("/")) | |
| OLLAMA_DEVICES = ("auto", "gpu", "cpu") | |
| def ollama_device_options(device: str | None) -> dict: | |
| """Hesaplama aygıtı -> Ollama 'options'. auto: Ollama karar verir (sığmayan katmanlar CPU'da); | |
| gpu: tüm katmanları ekran kartına yüklemeyi dener; cpu: ekran kartını hiç kullanmaz.""" | |
| d = (device or "auto").lower() | |
| if d == "cpu": | |
| return {"num_gpu": 0} | |
| if d == "gpu": | |
| return {"num_gpu": 999} | |
| return {} | |
| class OllamaBackend(LLMBackend): | |
| """Ollama'nın yerel /api/chat uç noktası: hesaplama aygıtı (num_gpu) ancak burada seçilebilir.""" | |
| provider = "ollama" | |
| def __init__(self, model: str, host: str | None = None, temperature: float = 0.0, device: str | None = None, | |
| timeout: int = 600): | |
| if not model: | |
| raise RuntimeError("Ollama modeli seçilmedi.") | |
| self.model = model | |
| self.host = (host or ollama_host()).rstrip("/") | |
| self.host = re.sub(r"/v1$", "", self.host) | |
| self.temperature = temperature | |
| self.device = (device or "auto").lower() | |
| self.timeout = timeout | |
| self.name = f"ollama:{model}" | |
| def chat(self, system, messages, max_tokens=800): | |
| t0 = time.time() | |
| task = _task_of(system) | |
| opts = {"temperature": self.temperature, "num_predict": max_tokens, **ollama_device_options(self.device)} | |
| try: | |
| r = requests.post(f"{self.host}/api/chat", timeout=self.timeout, | |
| json={"model": self.model, "stream": False, "options": opts, | |
| "messages": [{"role": "system", "content": system}] + messages}) | |
| if r.status_code >= 400: | |
| raise RuntimeError(f"Ollama {r.status_code}: {r.text[:300]}") | |
| d = r.json() | |
| USAGE.add(task, d.get("prompt_eval_count", 0), d.get("eval_count", 0), time.time() - t0) | |
| return (d.get("message") or {}).get("content", "") | |
| except Exception: | |
| USAGE.add(task, 0, 0, time.time() - t0, ok=False) | |
| raise | |
| def ollama_backend(model: str = "qwen2.5-coder:7b") -> OllamaBackend: | |
| return OllamaBackend(model) | |
| class ScriptedBackend(LLMBackend): | |
| """Testler için: önceden yazılmış yanıtları sırayla döndürür (kendini düzeltme döngüsünü sınamak için).""" | |
| provider = "test" | |
| def __init__(self, replies: list[str]): | |
| self.replies = list(replies) | |
| self.calls: list[list[dict]] = [] | |
| self.name = "betikli-test" | |
| def chat(self, system, messages, max_tokens=800): | |
| self.calls.append(messages) | |
| return self.replies.pop(0) if self.replies else '{"cevaplanabilir": false, "sql": "", "gerekce": "bitti"}' | |
| # ======================================================================= model senkronizasyonu | |
| class ModelInfo: | |
| id: str | |
| label: str | |
| created: str | int | None = None | |
| size: int | None = None | |
| note: str = "" | |
| _OPENAI_SKIP = re.compile(r"(embed|tts|whisper|dall-e|davinci|babbage|moderation|image|audio|realtime|transcribe|" | |
| r"search|instruct|computer-use|codex-mini|sora|-vision-preview)", re.I) | |
| _OPENAI_CHAT = re.compile(r"^(gpt-|o\d|chatgpt)", re.I) | |
| def list_claude_models(api_key: str | None = None, base_url: str | None = None, timeout: int = 15) -> list[ModelInfo]: | |
| key = api_key or os.getenv("ANTHROPIC_API_KEY") | |
| if not key: | |
| raise RuntimeError("ANTHROPIC_API_KEY gerekli.") | |
| base = (base_url or os.getenv("ANTHROPIC_BASE_URL") or "https://api.anthropic.com").rstrip("/") | |
| out, after = [], None | |
| for _ in range(10): # sayfalama | |
| params = {"limit": 100} | |
| if after: | |
| params["after_id"] = after | |
| r = requests.get(f"{base}/v1/models", params=params, timeout=timeout, | |
| headers={"x-api-key": key, "anthropic-version": ANTHROPIC_VERSION}) | |
| if r.status_code >= 400: | |
| raise RuntimeError(f"Anthropic API {r.status_code}: {r.text[:200]}") | |
| d = r.json() | |
| for m in d.get("data", []): | |
| out.append(ModelInfo(m["id"], m.get("display_name") or m["id"], m.get("created_at"))) | |
| if not d.get("has_more"): | |
| break | |
| after = d.get("last_id") | |
| out.sort(key=lambda m: str(m.created or ""), reverse=True) # en yeni üstte | |
| return out | |
| def list_openai_models(api_key: str | None = None, base_url: str | None = None, timeout: int = 15) -> list[ModelInfo]: | |
| base = (base_url or os.getenv("OPENAI_BASE_URL") or "https://api.openai.com/v1").rstrip("/") | |
| key = api_key or os.getenv("OPENAI_API_KEY") or "" | |
| if "api.openai.com" in base and not key: | |
| raise RuntimeError("OPENAI_API_KEY gerekli.") | |
| r = requests.get(f"{base}/models", timeout=timeout, headers={"Authorization": f"Bearer {key or 'yok'}"}) | |
| if r.status_code >= 400: | |
| raise RuntimeError(f"API {r.status_code}: {r.text[:200]}") | |
| return filter_openai_models(r.json().get("data", []), official="api.openai.com" in base) | |
| def filter_openai_models(data: list[dict], official: bool) -> list[ModelInfo]: | |
| """Resmî OpenAI uç noktasında yalnızca sohbet modellerini bırakır (gömme, ses, görüntü vb. elenir). | |
| OpenAI uyumlu diğer sunucularda (vLLM, LM Studio, OpenRouter) tüm modeller listelenir.""" | |
| out = [] | |
| for m in data: | |
| mid = m.get("id", "") | |
| if official and (not _OPENAI_CHAT.match(mid) or _OPENAI_SKIP.search(mid)): | |
| continue | |
| out.append(ModelInfo(mid, mid, m.get("created"), note=m.get("owned_by", ""))) | |
| out.sort(key=lambda m: (m.created or 0) if isinstance(m.created, (int, float)) else 0, reverse=True) | |
| return out | |
| def list_ollama_models(host: str | None = None, timeout: int = 5) -> list[ModelInfo]: | |
| h = (host or ollama_host()).rstrip("/") | |
| r = requests.get(f"{h}/api/tags", timeout=timeout) | |
| if r.status_code >= 400: | |
| raise RuntimeError(f"Ollama {r.status_code}: {r.text[:200]}") | |
| out = [] | |
| for m in r.json().get("models", []): | |
| det = m.get("details") or {} | |
| note = " · ".join(x for x in (det.get("parameter_size"), det.get("quantization_level")) if x) | |
| out.append(ModelInfo(m.get("name") or m.get("model"), m.get("name") or m.get("model"), | |
| m.get("modified_at"), m.get("size"), note)) | |
| return out | |
| def list_models(provider: str, api_key: str | None = None, base_url: str | None = None) -> list[ModelInfo]: | |
| if provider == "claude": | |
| return list_claude_models(api_key, base_url) | |
| if provider == "openai": | |
| return list_openai_models(api_key, base_url) | |
| if provider == "ollama": | |
| return list_ollama_models(base_url) | |
| return [] | |
| class OllamaRec: | |
| id: str | |
| size_gb: float # indirme boyutu (yaklaşık, 4-bit) | |
| min_ram_gb: int # rahat çalışma için önerilen bellek (VRAM ya da RAM) | |
| note_tr: str | |
| note_en: str | |
| # Text-to-SQL için önerilen yerel modeller (boyutlar yaklaşık; Ollama kütüphanesindeki varsayılan 4-bit sürümler) | |
| OLLAMA_RECS = [ | |
| OllamaRec("qwen2.5-coder:7b", 4.7, 8, "Önerilen başlangıç: SQL ve kodda güçlü, 8 GB'lık kartta çalışır.", | |
| "Recommended start: strong at SQL and code, runs on an 8 GB card."), | |
| OllamaRec("qwen2.5-coder:14b", 9.0, 16, "Daha isabetli SQL; 12–16 GB VRAM ya da 16 GB+ RAM.", | |
| "More accurate SQL; 12–16 GB VRAM or 16 GB+ RAM."), | |
| OllamaRec("qwen2.5-coder:32b", 20.0, 32, "Yerelde en güçlü seçeneklerden; 24 GB+ VRAM.", | |
| "Among the strongest local options; 24 GB+ VRAM."), | |
| OllamaRec("qwen3:8b", 5.2, 10, "Genel amaçlı, Türkçesi iyi; düşünme kipi yanıtı uzatabilir.", | |
| "General purpose with good Turkish; thinking mode can lengthen replies."), | |
| OllamaRec("gpt-oss:20b", 14.0, 16, "OpenAI'nin açık ağırlıklı modeli; 16 GB bellekte çalışır.", | |
| "OpenAI's open-weight model; runs in 16 GB of memory."), | |
| OllamaRec("gemma3:12b", 8.1, 12, "Çok dilli; Türkçe soruları anlamada iyi.", "Multilingual; good at Turkish questions."), | |
| OllamaRec("llama3.1:8b", 4.9, 8, "Yaygın temel model; karşılaştırma için.", "Widely used baseline; for comparison."), | |
| ] | |
| RECOMMENDED_OLLAMA = [r.id for r in OLLAMA_RECS] | |
| def ollama_version(host: str | None = None, timeout: int = 4) -> str: | |
| h = (host or ollama_host()).rstrip("/") | |
| r = requests.get(f"{h}/api/version", timeout=timeout) | |
| r.raise_for_status() | |
| return r.json().get("version", "?") | |
| def ollama_pull(model: str, host: str | None = None): | |
| """Ollama'ya model indirtir; (durum, tamamlanan, toplam) üçlülerini akış hâlinde verir.""" | |
| h = (host or ollama_host()).rstrip("/") | |
| with requests.post(f"{h}/api/pull", json={"model": model, "stream": True}, stream=True, timeout=3600) as r: | |
| if r.status_code >= 400: | |
| raise RuntimeError(f"Ollama {r.status_code}: {r.text[:200]}") | |
| for line in r.iter_lines(): | |
| if not line: | |
| continue | |
| d = json.loads(line) | |
| if d.get("error"): | |
| raise RuntimeError(d["error"]) | |
| yield d.get("status", ""), d.get("completed"), d.get("total") | |
| def make_backend(provider: str, model: str, api_key: str | None = None, base_url: str | None = None, | |
| device: str | None = None) -> LLMBackend: | |
| if provider == "claude": | |
| return AnthropicBackend(model, api_key, base_url=base_url) | |
| if provider == "openai": | |
| return OpenAICompatBackend(model, base_url, api_key) | |
| if provider == "ollama": | |
| return OllamaBackend(model, base_url, device=device) | |
| raise ValueError(provider) | |
| def available_backends() -> dict[str, str]: | |
| """Ortamda hazır olan sağlayıcılar (anahtar veya çalışan sunucu).""" | |
| out = {"kural": "Kural tabanlı (çevrimdışı, anahtar gerekmez)"} | |
| if os.getenv("ANTHROPIC_API_KEY"): | |
| out["claude"] = "Claude (ANTHROPIC_API_KEY bulundu)" | |
| if os.getenv("OPENAI_API_KEY"): | |
| out["openai"] = "OpenAI uyumlu (OPENAI_API_KEY bulundu)" | |
| try: | |
| if requests.get(ollama_host() + "/api/tags", timeout=0.8).ok: | |
| out["ollama"] = "Ollama (yerel)" | |
| except Exception: # noqa: BLE001 | |
| pass | |
| return out | |
| def load_dotenv(path: str | Path) -> list[str]: | |
| """Basit .env okuyucu: mevcut ortam değişkenlerini ezmez. Yüklenen anahtar adlarını döndürür.""" | |
| p = Path(path) | |
| loaded = [] | |
| if not p.exists(): | |
| return loaded | |
| for line in p.read_text(encoding="utf-8").splitlines(): | |
| line = line.strip() | |
| if not line or line.startswith("#") or "=" not in line: | |
| continue | |
| k, v = line.split("=", 1) | |
| k, v = k.strip().removeprefix("export ").strip(), v.strip().strip('"').strip("'") | |
| if k and k not in os.environ: | |
| os.environ[k] = v | |
| loaded.append(k) | |
| return loaded | |