All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 17s
Co-authored-by: Cursor <cursoragent@cursor.com>
189 lines
5.7 KiB
Python
189 lines
5.7 KiB
Python
"""三级优先级配置读取 + 首次 .env 落盘(进程 env > 数据目录 .env > .env.example)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import shutil
|
||
from typing import Any
|
||
|
||
from .runtime_env import get_data_root, get_user_id
|
||
|
||
_skill_slug: str | None = None
|
||
_example_path: str | None = None
|
||
_env_file_path: str | None = None
|
||
_user_env_cache: dict[str, str] | None = None
|
||
_example_defaults_cache: dict[str, str] | None = None
|
||
|
||
|
||
def _parse_env_file(path: str) -> dict[str, str]:
|
||
"""标准库手写 .env 解析(不引 python-dotenv)。"""
|
||
result: dict[str, str] = {}
|
||
if not path or not os.path.isfile(path):
|
||
return result
|
||
with open(path, encoding="utf-8") as f:
|
||
for raw_line in f:
|
||
line = raw_line.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
if "=" not in line:
|
||
continue
|
||
key, _, value = line.partition("=")
|
||
key = key.strip()
|
||
value = value.strip()
|
||
if not key:
|
||
continue
|
||
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
||
value = value[1:-1]
|
||
elif " #" in value:
|
||
value = value.split(" #", 1)[0].strip()
|
||
result[key] = value
|
||
return result
|
||
|
||
|
||
def _get_user_env() -> dict[str, str]:
|
||
global _user_env_cache
|
||
if _user_env_cache is not None:
|
||
return _user_env_cache
|
||
if _env_file_path and os.path.isfile(_env_file_path):
|
||
_user_env_cache = _parse_env_file(_env_file_path)
|
||
else:
|
||
_user_env_cache = {}
|
||
return _user_env_cache
|
||
|
||
|
||
def _get_example_defaults() -> dict[str, str]:
|
||
global _example_defaults_cache
|
||
if _example_defaults_cache is not None:
|
||
return _example_defaults_cache
|
||
if _example_path and os.path.isfile(_example_path):
|
||
_example_defaults_cache = _parse_env_file(_example_path)
|
||
else:
|
||
_example_defaults_cache = {}
|
||
return _example_defaults_cache
|
||
|
||
|
||
def reset_cache() -> None:
|
||
"""测试用:清空解析缓存。"""
|
||
global _user_env_cache, _example_defaults_cache
|
||
_user_env_cache = None
|
||
_example_defaults_cache = None
|
||
|
||
|
||
def get_env_file_path() -> str | None:
|
||
return _env_file_path
|
||
|
||
|
||
def get_example_path() -> str | None:
|
||
return _example_path
|
||
|
||
|
||
def _iter_env_assignments(path: str) -> list[tuple[str, str]]:
|
||
"""按文件顺序返回 (key, 完整赋值行)。"""
|
||
result: list[tuple[str, str]] = []
|
||
if not path or not os.path.isfile(path):
|
||
return result
|
||
with open(path, encoding="utf-8") as f:
|
||
for raw_line in f:
|
||
line = raw_line.rstrip("\n\r")
|
||
stripped = line.strip()
|
||
if not stripped or stripped.startswith("#"):
|
||
continue
|
||
if "=" not in stripped:
|
||
continue
|
||
key, _, _ = stripped.partition("=")
|
||
key = key.strip()
|
||
if key:
|
||
result.append((key, stripped))
|
||
return result
|
||
|
||
|
||
def merge_missing_env_keys(
|
||
example_path: str,
|
||
dest_path: str,
|
||
*,
|
||
comment_skill: str | None = None,
|
||
) -> list[str]:
|
||
"""将 example 中有而用户 .env 没有的 key 追加到 dest 末尾,不修改已有项。"""
|
||
if not example_path or not os.path.isfile(example_path):
|
||
return []
|
||
if not dest_path or not os.path.isfile(dest_path):
|
||
return []
|
||
|
||
dest_keys = set(_parse_env_file(dest_path).keys())
|
||
to_append: list[tuple[str, str]] = []
|
||
for key, assignment in _iter_env_assignments(example_path):
|
||
if key not in dest_keys:
|
||
to_append.append((key, assignment))
|
||
|
||
if not to_append:
|
||
return []
|
||
|
||
header = (
|
||
f"# Added by {comment_skill} from .env.example"
|
||
if comment_skill
|
||
else "# Added from .env.example"
|
||
)
|
||
with open(dest_path, "a", encoding="utf-8") as f:
|
||
f.write("\n\n" + header + "\n")
|
||
for _, assignment in to_append:
|
||
f.write(assignment + "\n")
|
||
|
||
reset_cache()
|
||
return [key for key, _ in to_append]
|
||
|
||
|
||
def ensure_env_file(skill_slug: str, example_path: str) -> str:
|
||
"""首次把 .env.example copy 到 {data_root}/{user}/{slug}/.env(已存在不覆盖),返回路径。"""
|
||
global _skill_slug, _example_path, _env_file_path
|
||
_skill_slug = skill_slug
|
||
_example_path = os.path.abspath(example_path)
|
||
dest_dir = os.path.join(get_data_root(), get_user_id(), skill_slug)
|
||
os.makedirs(dest_dir, exist_ok=True)
|
||
dest = os.path.join(dest_dir, ".env")
|
||
_env_file_path = dest
|
||
if not os.path.isfile(dest) and os.path.isfile(_example_path):
|
||
shutil.copy2(_example_path, dest)
|
||
reset_cache()
|
||
return dest
|
||
|
||
|
||
def get(key: str, default: Any = None) -> str | None:
|
||
"""进程环境变量 > 数据目录 .env > .env.example 默认值。"""
|
||
env_val = os.environ.get(key)
|
||
if env_val is not None and env_val != "":
|
||
return env_val
|
||
user_val = _get_user_env().get(key)
|
||
if user_val is not None and user_val != "":
|
||
return user_val
|
||
example_val = _get_example_defaults().get(key)
|
||
if example_val is not None and example_val != "":
|
||
return example_val
|
||
return default
|
||
|
||
|
||
def get_bool(key: str, default: bool = False) -> bool:
|
||
val = get(key)
|
||
if val is None:
|
||
return default
|
||
return str(val).strip().lower() in ("1", "true", "yes", "on")
|
||
|
||
|
||
def get_float(key: str, default: float) -> float:
|
||
val = get(key)
|
||
if val is None:
|
||
return default
|
||
try:
|
||
return float(val)
|
||
except (TypeError, ValueError):
|
||
return default
|
||
|
||
|
||
def get_int(key: str, default: int) -> int:
|
||
val = get(key)
|
||
if val is None:
|
||
return default
|
||
try:
|
||
return int(val)
|
||
except (TypeError, ValueError):
|
||
return default
|