aipa-cli 0.1.7__tar.gz → 0.1.8__tar.gz
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.
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/CHANGELOG.md +7 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/PKG-INFO +1 -1
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/__init__.py +1 -1
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/settings_tab.py +13 -6
- aipa_cli-0.1.8/src/aipriceaction_terminal/user_settings.py +86 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/workflows.py +3 -0
- aipa_cli-0.1.7/src/aipriceaction_terminal/user_settings.py +0 -50
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/.gitignore +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/LICENSE +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/README.md +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/pyproject.toml +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/__main__.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/actions.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/__init__.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/agent.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/callbacks.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/config.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/personas.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/tools.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/app.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/bindings.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/chat.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/cli.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/cli_commands.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/cli_setup.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/deep_research.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/theme.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/ticker_data.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/utils.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/widgets/__init__.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/widgets/chat_input.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/src/aipriceaction_terminal/widgets/ticker_select.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/conftest.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/openrouter_responses.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/test_app.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/test_chat.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/test_integration.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/test_settings_api.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/test_thinking.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/test_tool_call_streaming.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/test_utils.py +0 -0
- {aipa_cli-0.1.7 → aipa_cli-0.1.8}/tests/test_workflows.py +0 -0
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.8] - 2026-05-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Respect `.env` file priority over `settings.json` by parsing `.env` first in `apply_settings_to_env()`, giving correct order: real env vars > `.env` > `settings.json`
|
|
12
|
+
- Fix `_value_from_env_or_dotenv()` inverted logic and check `.env` directly when both sources match, so settings tab hints show the correct source
|
|
13
|
+
- Guard AnalyzePane with `_ensure_agent()` to prevent `'NoneType' has no attribute 'stream'` crash when no API key is configured at TUI launch
|
|
14
|
+
|
|
8
15
|
## [0.1.7] - 2026-05-09
|
|
9
16
|
|
|
10
17
|
### Fixed
|
|
@@ -22,15 +22,22 @@ def _value_from_env_or_dotenv(sdk_attr: str, settings_json_key: str) -> bool:
|
|
|
22
22
|
sdk_val = getattr(settings, sdk_attr, "")
|
|
23
23
|
if not sdk_val:
|
|
24
24
|
return False
|
|
25
|
-
# If settings.json has a different (non-empty) value, the user explicitly
|
|
26
|
-
# configured it there — treat settings.json as the source.
|
|
27
25
|
saved_val = load_settings().get(settings_json_key, "")
|
|
28
|
-
|
|
29
|
-
return False
|
|
30
|
-
# If settings.json is empty (or matches SDK), the value originates from
|
|
31
|
-
# .env / env / SDK default.
|
|
26
|
+
# No value in settings.json → effective value is from env/.env/SDK default.
|
|
32
27
|
if not saved_val:
|
|
33
28
|
return True
|
|
29
|
+
# settings.json has a different value → env/.env overrode it.
|
|
30
|
+
if saved_val != sdk_val:
|
|
31
|
+
return True
|
|
32
|
+
# Both match — check if .env also provides this value (it has higher
|
|
33
|
+
# priority than settings.json, so .env is the real source).
|
|
34
|
+
from .user_settings import _load_dotenv_values, _find_dotenv
|
|
35
|
+
env_var = next((ev for _, jk, ev, _ in _API_FIELDS if jk == settings_json_key), None)
|
|
36
|
+
if env_var:
|
|
37
|
+
dotenv_val = _load_dotenv_values(_find_dotenv()).get(env_var, "")
|
|
38
|
+
if dotenv_val == sdk_val:
|
|
39
|
+
return True
|
|
40
|
+
# Value only exists in settings.json.
|
|
34
41
|
return False
|
|
35
42
|
|
|
36
43
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Persistent user settings stored in ~/.aipriceaction/settings.json."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_CONFIG_DIR = Path.home() / ".aipriceaction"
|
|
8
|
+
_SETTINGS_FILE = _CONFIG_DIR / "settings.json"
|
|
9
|
+
|
|
10
|
+
_DEFAULTS = {
|
|
11
|
+
"ticker": "VNINDEX",
|
|
12
|
+
"interval": "1D",
|
|
13
|
+
"language": "en",
|
|
14
|
+
"api_key": "",
|
|
15
|
+
"openai_base_url": "",
|
|
16
|
+
"openai_model": "",
|
|
17
|
+
"setup_done": False,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_settings() -> dict:
|
|
22
|
+
"""Load settings from disk, falling back to defaults for missing keys."""
|
|
23
|
+
if _SETTINGS_FILE.exists():
|
|
24
|
+
data = json.loads(_SETTINGS_FILE.read_text())
|
|
25
|
+
return {**_DEFAULTS, **data}
|
|
26
|
+
return dict(_DEFAULTS)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def save_settings(data: dict) -> None:
|
|
30
|
+
"""Persist settings to disk, creating the config directory if needed."""
|
|
31
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
_SETTINGS_FILE.write_text(json.dumps(data, indent=2))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_dotenv_values(path: Path) -> dict[str, str]:
|
|
36
|
+
"""Parse a .env file into a dict (key=value per line, ignores blanks/comments)."""
|
|
37
|
+
values: dict[str, str] = {}
|
|
38
|
+
if not path.exists():
|
|
39
|
+
return values
|
|
40
|
+
for line in path.read_text().splitlines():
|
|
41
|
+
line = line.strip()
|
|
42
|
+
if not line or line.startswith("#"):
|
|
43
|
+
continue
|
|
44
|
+
if "=" not in line:
|
|
45
|
+
continue
|
|
46
|
+
key, _, value = line.partition("=")
|
|
47
|
+
values[key.strip()] = value.strip().strip("\"'")
|
|
48
|
+
return values
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _find_dotenv() -> Path:
|
|
52
|
+
"""Walk up from CWD to find .env, falling back to home directory."""
|
|
53
|
+
current = Path.cwd()
|
|
54
|
+
for _ in range(5):
|
|
55
|
+
candidate = current / ".env"
|
|
56
|
+
if candidate.exists():
|
|
57
|
+
return candidate
|
|
58
|
+
current = current.parent
|
|
59
|
+
return Path.home() / ".env"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def apply_settings_to_env() -> None:
|
|
63
|
+
"""Seed environment variables so CLI commands pick up config from .env and settings.json.
|
|
64
|
+
|
|
65
|
+
Priority (highest wins): real env vars > .env file > settings.json.
|
|
66
|
+
All seeding uses ``os.environ.setdefault`` so real env vars are never overwritten.
|
|
67
|
+
Must be called before any SDK import that reads env vars.
|
|
68
|
+
"""
|
|
69
|
+
_env_keys = {"OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_MODEL"}
|
|
70
|
+
|
|
71
|
+
# 1. Seed from .env file (middle priority)
|
|
72
|
+
for key, value in _load_dotenv_values(_find_dotenv()).items():
|
|
73
|
+
if key in _env_keys and value:
|
|
74
|
+
os.environ.setdefault(key, value)
|
|
75
|
+
|
|
76
|
+
# 2. Seed from settings.json (lowest priority)
|
|
77
|
+
settings = load_settings()
|
|
78
|
+
_mapping = {
|
|
79
|
+
"OPENAI_API_KEY": "api_key",
|
|
80
|
+
"OPENAI_BASE_URL": "openai_base_url",
|
|
81
|
+
"OPENAI_MODEL": "openai_model",
|
|
82
|
+
}
|
|
83
|
+
for env_key, settings_key in _mapping.items():
|
|
84
|
+
value = settings.get(settings_key, "")
|
|
85
|
+
if value:
|
|
86
|
+
os.environ.setdefault(env_key, value)
|
|
@@ -125,6 +125,9 @@ class AnalyzePane(Vertical):
|
|
|
125
125
|
"""Build context and stream AI analysis for a ticker."""
|
|
126
126
|
log = self.query_one("#wf-output", RichLog)
|
|
127
127
|
try:
|
|
128
|
+
if not self.app._ensure_agent():
|
|
129
|
+
log.write("[red]Error: No API key configured. Run 'aipa setup' or set OPENAI_API_KEY.[/red]")
|
|
130
|
+
return
|
|
128
131
|
builder = self.app.builder
|
|
129
132
|
|
|
130
133
|
# Build context without system prompt (agent has it already)
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Persistent user settings stored in ~/.aipriceaction/settings.json."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
_CONFIG_DIR = Path.home() / ".aipriceaction"
|
|
8
|
-
_SETTINGS_FILE = _CONFIG_DIR / "settings.json"
|
|
9
|
-
|
|
10
|
-
_DEFAULTS = {
|
|
11
|
-
"ticker": "VNINDEX",
|
|
12
|
-
"interval": "1D",
|
|
13
|
-
"language": "en",
|
|
14
|
-
"api_key": "",
|
|
15
|
-
"openai_base_url": "",
|
|
16
|
-
"openai_model": "",
|
|
17
|
-
"setup_done": False,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def load_settings() -> dict:
|
|
22
|
-
"""Load settings from disk, falling back to defaults for missing keys."""
|
|
23
|
-
if _SETTINGS_FILE.exists():
|
|
24
|
-
data = json.loads(_SETTINGS_FILE.read_text())
|
|
25
|
-
return {**_DEFAULTS, **data}
|
|
26
|
-
return dict(_DEFAULTS)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def save_settings(data: dict) -> None:
|
|
30
|
-
"""Persist settings to disk, creating the config directory if needed."""
|
|
31
|
-
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
-
_SETTINGS_FILE.write_text(json.dumps(data, indent=2))
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def apply_settings_to_env() -> None:
|
|
36
|
-
"""Seed environment variables from settings.json.
|
|
37
|
-
|
|
38
|
-
Uses ``os.environ.setdefault`` so that existing env vars always win.
|
|
39
|
-
Must be called before any SDK import that reads env vars.
|
|
40
|
-
"""
|
|
41
|
-
settings = load_settings()
|
|
42
|
-
_mapping = {
|
|
43
|
-
"OPENAI_API_KEY": "api_key",
|
|
44
|
-
"OPENAI_BASE_URL": "openai_base_url",
|
|
45
|
-
"OPENAI_MODEL": "openai_model",
|
|
46
|
-
}
|
|
47
|
-
for env_key, settings_key in _mapping.items():
|
|
48
|
-
value = settings.get(settings_key, "")
|
|
49
|
-
if value:
|
|
50
|
-
os.environ.setdefault(env_key, value)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|