feat: versioning, feat: in app configueration, feat: single exe, feat: reasoning, action: inital version, fix: config saving
Build Release EXE / build-windows-exe (release) Successful in 1m5s
Build Release EXE / build-windows-exe (release) Successful in 1m5s
This commit is contained in:
+126
-4
@@ -1,11 +1,64 @@
|
||||
from functools import lru_cache
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import Field
|
||||
from functools import lru_cache
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
CONFIG_FIELDS: dict[str, dict[str, Any]] = {
|
||||
"ollama_base_url": {"env": "OLLAMA_BASE_URL", "type": "string", "secret": False},
|
||||
"ollama_model": {"env": "OLLAMA_MODEL", "type": "string", "secret": False},
|
||||
"ollama_num_ctx": {"env": "OLLAMA_NUM_CTX", "type": "integer", "secret": False},
|
||||
"uex_base_url": {"env": "UEX_BASE_URL", "type": "string", "secret": False},
|
||||
"uex_secret_key": {"env": "UEX_SECRET_KEY", "type": "string", "secret": True},
|
||||
"uex_bearer_token": {"env": "UEX_BEARER_TOKEN", "type": "string", "secret": True},
|
||||
"traderai_user_name": {"env": "TRADERAI_USER_NAME", "type": "string", "secret": False},
|
||||
"traderai_memory_path": {"env": "TRADERAI_MEMORY_PATH", "type": "string", "secret": False},
|
||||
"uex_notification_poll_seconds": {"env": "UEX_NOTIFICATION_POLL_SECONDS", "type": "integer", "secret": False},
|
||||
"require_write_approval": {"env": "REQUIRE_WRITE_APPROVAL", "type": "boolean", "secret": False},
|
||||
}
|
||||
|
||||
|
||||
def app_data_dir() -> Path:
|
||||
if sys.platform == "win32":
|
||||
root = os.environ.get("LOCALAPPDATA")
|
||||
if root:
|
||||
return Path(root) / "TraderAI"
|
||||
return Path.home() / ".traderai"
|
||||
|
||||
|
||||
def ensure_app_data_dir() -> Path:
|
||||
path = app_data_dir()
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def user_config_path() -> Path:
|
||||
return ensure_app_data_dir() / ".env"
|
||||
|
||||
|
||||
def default_memory_path() -> Path:
|
||||
return ensure_app_data_dir() / "traderai.sqlite3"
|
||||
|
||||
|
||||
def log_path() -> Path:
|
||||
return ensure_app_data_dir() / "TraderAI.log"
|
||||
|
||||
|
||||
def edge_profile_dir() -> Path:
|
||||
return ensure_app_data_dir() / "EdgeProfile"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=(".env", str(user_config_path())),
|
||||
env_file_encoding="utf-8",
|
||||
)
|
||||
|
||||
ollama_base_url: str = "http://localhost:11434"
|
||||
ollama_model: str = "qwen3.5:9b"
|
||||
@@ -14,11 +67,80 @@ class Settings(BaseSettings):
|
||||
uex_secret_key: str | None = Field(default=None)
|
||||
uex_bearer_token: str | None = Field(default=None)
|
||||
traderai_user_name: str | None = Field(default=None)
|
||||
traderai_memory_path: str = "data/traderai.sqlite3"
|
||||
traderai_memory_path: str = Field(default_factory=lambda: str(default_memory_path()))
|
||||
uex_notification_poll_seconds: int = 60
|
||||
require_write_approval: bool = True
|
||||
|
||||
@field_validator("uex_secret_key", "uex_bearer_token", "traderai_user_name", mode="before")
|
||||
@classmethod
|
||||
def _blank_optional(cls, value: Any) -> Any:
|
||||
return None if value == "" else value
|
||||
|
||||
@field_validator("traderai_memory_path", mode="before")
|
||||
@classmethod
|
||||
def _blank_memory_path(cls, value: Any) -> Any:
|
||||
return str(default_memory_path()) if value == "" or value is None else value
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
|
||||
def settings_payload(settings: Settings | None = None) -> dict[str, Any]:
|
||||
current = settings or get_settings()
|
||||
values = current.model_dump()
|
||||
return {
|
||||
"app_data_dir": str(ensure_app_data_dir()),
|
||||
"config_path": str(user_config_path()),
|
||||
"log_path": str(log_path()),
|
||||
"edge_profile_dir": str(edge_profile_dir()),
|
||||
"values": values,
|
||||
"fields": CONFIG_FIELDS,
|
||||
}
|
||||
|
||||
|
||||
def save_settings(values: dict[str, Any]) -> dict[str, Any]:
|
||||
current = get_settings().model_dump()
|
||||
next_values = dict(current)
|
||||
for key, value in values.items():
|
||||
if key not in CONFIG_FIELDS:
|
||||
continue
|
||||
next_values[key] = _coerce_value(key, value)
|
||||
|
||||
path = user_config_path()
|
||||
lines = [
|
||||
"# TraderAI desktop configuration",
|
||||
"# Saved by the app. Environment variables still override these values.",
|
||||
"",
|
||||
]
|
||||
for key, meta in CONFIG_FIELDS.items():
|
||||
value = next_values.get(key)
|
||||
lines.append(f"{meta['env']}={_env_value(value)}")
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
get_settings.cache_clear()
|
||||
return settings_payload(get_settings())
|
||||
|
||||
|
||||
def _coerce_value(key: str, value: Any) -> Any:
|
||||
field_type = CONFIG_FIELDS[key]["type"]
|
||||
if value == "":
|
||||
return None if key in {"uex_secret_key", "uex_bearer_token", "traderai_user_name"} else ""
|
||||
if field_type == "integer":
|
||||
return int(value)
|
||||
if field_type == "boolean":
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return str(value).strip().casefold() in {"1", "true", "yes", "on"}
|
||||
return str(value)
|
||||
|
||||
|
||||
def _env_value(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
text = str(value)
|
||||
if not text or any(char.isspace() for char in text) or "#" in text:
|
||||
return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
return text
|
||||
|
||||
Reference in New Issue
Block a user