aipa-cli 0.1.6__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.
Files changed (42) hide show
  1. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/CHANGELOG.md +12 -0
  2. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/PKG-INFO +1 -1
  3. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/__init__.py +1 -1
  4. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/cli.py +3 -0
  5. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/settings_tab.py +13 -6
  6. aipa_cli-0.1.8/src/aipriceaction_terminal/user_settings.py +86 -0
  7. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/workflows.py +3 -0
  8. aipa_cli-0.1.6/src/aipriceaction_terminal/user_settings.py +0 -31
  9. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/.gitignore +0 -0
  10. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/LICENSE +0 -0
  11. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/README.md +0 -0
  12. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/pyproject.toml +0 -0
  13. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/__main__.py +0 -0
  14. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/actions.py +0 -0
  15. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/__init__.py +0 -0
  16. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/agent.py +0 -0
  17. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/callbacks.py +0 -0
  18. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/config.py +0 -0
  19. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/personas.py +0 -0
  20. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/agents/tools.py +0 -0
  21. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/app.py +0 -0
  22. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/bindings.py +0 -0
  23. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/chat.py +0 -0
  24. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/cli_commands.py +0 -0
  25. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/cli_setup.py +0 -0
  26. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/deep_research.py +0 -0
  27. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/theme.py +0 -0
  28. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/ticker_data.py +0 -0
  29. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/utils.py +0 -0
  30. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/widgets/__init__.py +0 -0
  31. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/widgets/chat_input.py +0 -0
  32. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/src/aipriceaction_terminal/widgets/ticker_select.py +0 -0
  33. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/conftest.py +0 -0
  34. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/openrouter_responses.py +0 -0
  35. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/test_app.py +0 -0
  36. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/test_chat.py +0 -0
  37. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/test_integration.py +0 -0
  38. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/test_settings_api.py +0 -0
  39. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/test_thinking.py +0 -0
  40. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/test_tool_call_streaming.py +0 -0
  41. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/test_utils.py +0 -0
  42. {aipa_cli-0.1.6 → aipa_cli-0.1.8}/tests/test_workflows.py +0 -0
@@ -5,6 +5,18 @@ 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
+
15
+ ## [0.1.7] - 2026-05-09
16
+
17
+ ### Fixed
18
+ - Bridge settings.json API key to SDK Pydantic settings by seeding env vars (`OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`) at CLI entry point before any SDK import
19
+
8
20
  ## [0.1.6] - 2026-05-09
9
21
 
10
22
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aipa-cli
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Terminal TUI for AI-powered ticker analysis
5
5
  Project-URL: Homepage, https://github.com/quanhua92/aipriceaction
6
6
  Project-URL: Repository, https://github.com/quanhua92/aipriceaction
@@ -1,4 +1,4 @@
1
1
  """AIPriceAction Terminal - TUI chat interface for ticker analysis."""
2
2
 
3
- __version__ = "0.1.6"
3
+ __version__ = "0.1.8"
4
4
 
@@ -12,6 +12,9 @@ def _ensure_setup() -> None:
12
12
 
13
13
 
14
14
  def run():
15
+ from .user_settings import apply_settings_to_env
16
+ apply_settings_to_env()
17
+
15
18
  parser = argparse.ArgumentParser(prog="aipa", description="AIPriceAction terminal")
16
19
  sub = parser.add_subparsers(dest="command")
17
20
 
@@ -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
- if saved_val and saved_val != sdk_val:
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,31 +0,0 @@
1
- """Persistent user settings stored in ~/.aipriceaction/settings.json."""
2
-
3
- import json
4
- from pathlib import Path
5
-
6
- _CONFIG_DIR = Path.home() / ".aipriceaction"
7
- _SETTINGS_FILE = _CONFIG_DIR / "settings.json"
8
-
9
- _DEFAULTS = {
10
- "ticker": "VNINDEX",
11
- "interval": "1D",
12
- "language": "en",
13
- "api_key": "",
14
- "openai_base_url": "",
15
- "openai_model": "",
16
- "setup_done": False,
17
- }
18
-
19
-
20
- def load_settings() -> dict:
21
- """Load settings from disk, falling back to defaults for missing keys."""
22
- if _SETTINGS_FILE.exists():
23
- data = json.loads(_SETTINGS_FILE.read_text())
24
- return {**_DEFAULTS, **data}
25
- return dict(_DEFAULTS)
26
-
27
-
28
- def save_settings(data: dict) -> None:
29
- """Persist settings to disk, creating the config directory if needed."""
30
- _CONFIG_DIR.mkdir(parents=True, exist_ok=True)
31
- _SETTINGS_FILE.write_text(json.dumps(data, indent=2))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes