@drico2008/fincli 0.3.1 → 0.4.0

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 (37) hide show
  1. package/README.md +217 -217
  2. package/fincli/__init__.py +1 -1
  3. package/fincli/app/analysis/ai_prompts.py +29 -27
  4. package/fincli/app/analysis/analyzer.py +34 -34
  5. package/fincli/app/analysis/assistant_context.py +3 -3
  6. package/fincli/app/cli/commands.py +33 -27
  7. package/fincli/app/cli/router.py +1633 -1105
  8. package/fincli/app/diagnostics/__init__.py +2 -0
  9. package/fincli/app/diagnostics/capabilities.py +44 -0
  10. package/fincli/app/diagnostics/runtime.py +106 -0
  11. package/fincli/app/main.py +6 -1
  12. package/fincli/app/modules/economic_calendar.py +512 -512
  13. package/fincli/app/modules/portfolio_risk.py +305 -305
  14. package/fincli/app/modules/trading.py +142 -0
  15. package/fincli/app/plugins/loader.py +72 -72
  16. package/fincli/app/providers/market/finnhub_provider.py +51 -2
  17. package/fincli/app/providers/market/symbols.py +95 -2
  18. package/fincli/app/providers/reliability.py +82 -65
  19. package/fincli/app/research/__init__.py +8 -8
  20. package/fincli/app/research/engine.py +119 -112
  21. package/fincli/app/research/exporter.py +91 -91
  22. package/fincli/app/research/formatter.py +25 -24
  23. package/fincli/app/research/models.py +22 -21
  24. package/fincli/app/research/prompt_builder.py +53 -51
  25. package/fincli/app/services/data_quality.py +27 -0
  26. package/fincli/app/services/data_trust.py +117 -0
  27. package/fincli/app/services/macro_data.py +158 -50
  28. package/fincli/app/services/market_data.py +183 -79
  29. package/fincli/app/services/market_overview.py +131 -142
  30. package/fincli/app/services/news_aggregator.py +95 -95
  31. package/fincli/app/storage/config.py +6 -3
  32. package/fincli/app/storage/database.py +130 -117
  33. package/fincli/app/storage/provider_metrics.py +61 -61
  34. package/fincli/app/storage/secrets.py +128 -128
  35. package/npm/bin/fincli.js +65 -65
  36. package/package.json +7 -7
  37. package/pyproject.toml +1 -1
@@ -1,128 +1,128 @@
1
- """Local secret storage for globally installed FinCLI."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- from pathlib import Path
7
-
8
- from fincli.app.storage.config_paths import APP_DIR
9
- from fincli.app.utils.errors import ConfigError
10
-
11
-
12
- SECRETS_FILE = APP_DIR / "secrets.env"
13
-
14
-
15
- def load_local_secrets(
16
- path: Path | None = None,
17
- *,
18
- override: bool = False,
19
- override_keys: set[str] | None = None,
20
- ) -> None:
21
- """Load persisted secrets into process environment."""
22
- path = path or SECRETS_FILE
23
- override_keys = override_keys or set()
24
- if not path.exists():
25
- return
26
- for line in path.read_text(encoding="utf-8").splitlines():
27
- stripped = line.strip()
28
- if not stripped or stripped.startswith("#") or "=" not in stripped:
29
- continue
30
- key, value = stripped.split("=", 1)
31
- key = key.strip()
32
- value = _unquote(value.strip())
33
- if key and (override or key in override_keys or key not in os.environ or os.environ.get(key, "") == ""):
34
- os.environ[key] = value
35
-
36
-
37
- def save_secret(env_key: str, value: str, path: Path | None = None) -> None:
38
- """Persist a secret locally and expose it to the current process."""
39
- path = path or SECRETS_FILE
40
- key = _validate_env_key(env_key)
41
- secret = _sanitize_value(value)
42
- if not secret:
43
- raise ConfigError(f"Nilai {key} kosong.")
44
-
45
- secrets = read_secrets(path)
46
- secrets[key] = secret
47
-
48
- try:
49
- path.parent.mkdir(parents=True, exist_ok=True)
50
- lines = ["# FinCLI local secrets. Do not commit or share this file."]
51
- lines.extend(f"{item_key}={_quote(item_value)}" for item_key, item_value in sorted(secrets.items()))
52
- path.write_text("\n".join(lines) + "\n", encoding="utf-8")
53
- try:
54
- os.chmod(path, 0o600)
55
- except OSError:
56
- pass
57
- except OSError as exc:
58
- raise ConfigError("Secret lokal gagal disimpan.", f"Path: {path}") from exc
59
-
60
- os.environ[key] = secret
61
-
62
-
63
- def clear_secrets(path: Path | None = None) -> int:
64
- """Clear persisted local secrets and remove them from the current process."""
65
- path = path or SECRETS_FILE
66
- secrets = read_secrets(path)
67
- for key in secrets:
68
- os.environ.pop(key, None)
69
- try:
70
- path.parent.mkdir(parents=True, exist_ok=True)
71
- path.write_text("# FinCLI local secrets. Do not commit or share this file.\n", encoding="utf-8")
72
- try:
73
- os.chmod(path, 0o600)
74
- except OSError:
75
- pass
76
- except OSError as exc:
77
- raise ConfigError("Secret lokal gagal dibersihkan.", f"Path: {path}") from exc
78
- return len(secrets)
79
-
80
-
81
- def read_secrets(path: Path | None = None) -> dict[str, str]:
82
- """Read local secrets without printing or masking them."""
83
- path = path or SECRETS_FILE
84
- if not path.exists():
85
- return {}
86
- result: dict[str, str] = {}
87
- for line in path.read_text(encoding="utf-8").splitlines():
88
- stripped = line.strip()
89
- if not stripped or stripped.startswith("#") or "=" not in stripped:
90
- continue
91
- key, value = stripped.split("=", 1)
92
- result[key.strip()] = _unquote(value.strip())
93
- return result
94
-
95
-
96
- def secret_source(env_key: str, path: Path | None = None) -> str:
97
- """Return a display-safe source for a secret."""
98
- path = path or SECRETS_FILE
99
- current = os.getenv(env_key)
100
- if not current:
101
- return "-"
102
- if env_key in os.environ:
103
- if read_secrets(path).get(env_key) == current:
104
- return "~/.fincli/secrets.env"
105
- return "environment/.env"
106
- return "-"
107
-
108
-
109
- def _validate_env_key(env_key: str) -> str:
110
- key = env_key.strip().upper()
111
- if not key or not all(char.isalnum() or char == "_" for char in key):
112
- raise ConfigError(f"Nama environment key tidak valid: {env_key}")
113
- return key
114
-
115
-
116
- def _sanitize_value(value: str) -> str:
117
- return value.strip().replace("\r", "").replace("\n", "")
118
-
119
-
120
- def _quote(value: str) -> str:
121
- escaped = value.replace("\\", "\\\\").replace('"', '\\"')
122
- return f'"{escaped}"'
123
-
124
-
125
- def _unquote(value: str) -> str:
126
- if len(value) >= 2 and value[0] == value[-1] == '"':
127
- return value[1:-1].replace('\\"', '"').replace("\\\\", "\\")
128
- return value
1
+ """Local secret storage for globally installed FinCLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from fincli.app.storage.config_paths import APP_DIR
9
+ from fincli.app.utils.errors import ConfigError
10
+
11
+
12
+ SECRETS_FILE = APP_DIR / "secrets.env"
13
+
14
+
15
+ def load_local_secrets(
16
+ path: Path | None = None,
17
+ *,
18
+ override: bool = False,
19
+ override_keys: set[str] | None = None,
20
+ ) -> None:
21
+ """Load persisted secrets into process environment."""
22
+ path = path or SECRETS_FILE
23
+ override_keys = override_keys or set()
24
+ if not path.exists():
25
+ return
26
+ for line in path.read_text(encoding="utf-8").splitlines():
27
+ stripped = line.strip()
28
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
29
+ continue
30
+ key, value = stripped.split("=", 1)
31
+ key = key.strip()
32
+ value = _unquote(value.strip())
33
+ if key and (override or key in override_keys or key not in os.environ or os.environ.get(key, "") == ""):
34
+ os.environ[key] = value
35
+
36
+
37
+ def save_secret(env_key: str, value: str, path: Path | None = None) -> None:
38
+ """Persist a secret locally and expose it to the current process."""
39
+ path = path or SECRETS_FILE
40
+ key = _validate_env_key(env_key)
41
+ secret = _sanitize_value(value)
42
+ if not secret:
43
+ raise ConfigError(f"Nilai {key} kosong.")
44
+
45
+ secrets = read_secrets(path)
46
+ secrets[key] = secret
47
+
48
+ try:
49
+ path.parent.mkdir(parents=True, exist_ok=True)
50
+ lines = ["# FinCLI local secrets. Do not commit or share this file."]
51
+ lines.extend(f"{item_key}={_quote(item_value)}" for item_key, item_value in sorted(secrets.items()))
52
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
53
+ try:
54
+ os.chmod(path, 0o600)
55
+ except OSError:
56
+ pass
57
+ except OSError as exc:
58
+ raise ConfigError("Secret lokal gagal disimpan.", f"Path: {path}") from exc
59
+
60
+ os.environ[key] = secret
61
+
62
+
63
+ def clear_secrets(path: Path | None = None) -> int:
64
+ """Clear persisted local secrets and remove them from the current process."""
65
+ path = path or SECRETS_FILE
66
+ secrets = read_secrets(path)
67
+ for key in secrets:
68
+ os.environ.pop(key, None)
69
+ try:
70
+ path.parent.mkdir(parents=True, exist_ok=True)
71
+ path.write_text("# FinCLI local secrets. Do not commit or share this file.\n", encoding="utf-8")
72
+ try:
73
+ os.chmod(path, 0o600)
74
+ except OSError:
75
+ pass
76
+ except OSError as exc:
77
+ raise ConfigError("Secret lokal gagal dibersihkan.", f"Path: {path}") from exc
78
+ return len(secrets)
79
+
80
+
81
+ def read_secrets(path: Path | None = None) -> dict[str, str]:
82
+ """Read local secrets without printing or masking them."""
83
+ path = path or SECRETS_FILE
84
+ if not path.exists():
85
+ return {}
86
+ result: dict[str, str] = {}
87
+ for line in path.read_text(encoding="utf-8").splitlines():
88
+ stripped = line.strip()
89
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
90
+ continue
91
+ key, value = stripped.split("=", 1)
92
+ result[key.strip()] = _unquote(value.strip())
93
+ return result
94
+
95
+
96
+ def secret_source(env_key: str, path: Path | None = None) -> str:
97
+ """Return a display-safe source for a secret."""
98
+ path = path or SECRETS_FILE
99
+ current = os.getenv(env_key)
100
+ if not current:
101
+ return "-"
102
+ if env_key in os.environ:
103
+ if read_secrets(path).get(env_key) == current:
104
+ return "~/.fincli/secrets.env"
105
+ return "environment/.env"
106
+ return "-"
107
+
108
+
109
+ def _validate_env_key(env_key: str) -> str:
110
+ key = env_key.strip().upper()
111
+ if not key or not all(char.isalnum() or char == "_" for char in key):
112
+ raise ConfigError(f"Nama environment key tidak valid: {env_key}")
113
+ return key
114
+
115
+
116
+ def _sanitize_value(value: str) -> str:
117
+ return value.strip().replace("\r", "").replace("\n", "")
118
+
119
+
120
+ def _quote(value: str) -> str:
121
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
122
+ return f'"{escaped}"'
123
+
124
+
125
+ def _unquote(value: str) -> str:
126
+ if len(value) >= 2 and value[0] == value[-1] == '"':
127
+ return value[1:-1].replace('\\"', '"').replace("\\\\", "\\")
128
+ return value
package/npm/bin/fincli.js CHANGED
@@ -1,65 +1,65 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { spawn, spawnSync } = require("child_process");
6
-
7
- const packageRoot = path.resolve(__dirname, "..", "..");
8
- const packageJson = require(path.join(packageRoot, "package.json"));
9
- const venvDir = path.join(packageRoot, ".npm-python");
10
- const pythonBin = process.platform === "win32"
11
- ? path.join(venvDir, "Scripts", "python.exe")
12
- : path.join(venvDir, "bin", "python");
13
-
14
- function run() {
15
- const args = process.argv.slice(2);
16
- if (args.includes("--version") || args.includes("-v")) {
17
- console.log(packageJson.version);
18
- return;
19
- }
20
-
21
- if (!fs.existsSync(pythonBin)) {
22
- console.error("FinCLI Python runtime is missing.");
23
- console.error("Try reinstalling with: npm install -g @drico2008/fincli");
24
- console.error("Python 3.11+ must be available during npm install.");
25
- process.exit(1);
26
- }
27
-
28
- ensurePythonRuntime();
29
-
30
- const child = spawn(pythonBin, ["-m", "fincli.app.main", ...args], {
31
- cwd: packageRoot,
32
- stdio: "inherit"
33
- });
34
-
35
- child.on("exit", (code, signal) => {
36
- if (signal) {
37
- process.kill(process.pid, signal);
38
- return;
39
- }
40
- process.exit(code ?? 0);
41
- });
42
- }
43
-
44
- function ensurePythonRuntime() {
45
- const probe = spawnSync(pythonBin, ["-c", "import textual, rich, httpx, pydantic, yfinance, pandas, numpy"], {
46
- cwd: packageRoot,
47
- stdio: "ignore"
48
- });
49
- if (probe.status === 0) {
50
- return;
51
- }
52
-
53
- console.error("FinCLI Python dependencies are incomplete. Repairing local npm runtime...");
54
- const repair = spawnSync(pythonBin, ["-m", "pip", "install", "."], {
55
- cwd: packageRoot,
56
- stdio: "inherit"
57
- });
58
- if (repair.status !== 0) {
59
- console.error("FinCLI runtime repair failed.");
60
- console.error("Try reinstalling with: npm install -g @drico2008/fincli");
61
- process.exit(repair.status ?? 1);
62
- }
63
- }
64
-
65
- run();
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { spawn, spawnSync } = require("child_process");
6
+
7
+ const packageRoot = path.resolve(__dirname, "..", "..");
8
+ const packageJson = require(path.join(packageRoot, "package.json"));
9
+ const venvDir = path.join(packageRoot, ".npm-python");
10
+ const pythonBin = process.platform === "win32"
11
+ ? path.join(venvDir, "Scripts", "python.exe")
12
+ : path.join(venvDir, "bin", "python");
13
+
14
+ function run() {
15
+ const args = process.argv.slice(2);
16
+ if (args.includes("--version") || args.includes("-v")) {
17
+ console.log(packageJson.version);
18
+ return;
19
+ }
20
+
21
+ if (!fs.existsSync(pythonBin)) {
22
+ console.error("FinCLI Python runtime is missing.");
23
+ console.error("Try reinstalling with: npm install -g @drico2008/fincli");
24
+ console.error("Python 3.11+ must be available during npm install.");
25
+ process.exit(1);
26
+ }
27
+
28
+ ensurePythonRuntime();
29
+
30
+ const child = spawn(pythonBin, ["-m", "fincli.app.main", ...args], {
31
+ cwd: packageRoot,
32
+ stdio: "inherit"
33
+ });
34
+
35
+ child.on("exit", (code, signal) => {
36
+ if (signal) {
37
+ process.kill(process.pid, signal);
38
+ return;
39
+ }
40
+ process.exit(code ?? 0);
41
+ });
42
+ }
43
+
44
+ function ensurePythonRuntime() {
45
+ const probe = spawnSync(pythonBin, ["-c", "import textual, rich, httpx, pydantic, yfinance, pandas, numpy"], {
46
+ cwd: packageRoot,
47
+ stdio: "ignore"
48
+ });
49
+ if (probe.status === 0) {
50
+ return;
51
+ }
52
+
53
+ console.error("FinCLI Python dependencies are incomplete. Repairing local npm runtime...");
54
+ const repair = spawnSync(pythonBin, ["-m", "pip", "install", "."], {
55
+ cwd: packageRoot,
56
+ stdio: "inherit"
57
+ });
58
+ if (repair.status !== 0) {
59
+ console.error("FinCLI runtime repair failed.");
60
+ console.error("Try reinstalling with: npm install -g @drico2008/fincli");
61
+ process.exit(repair.status ?? 1);
62
+ }
63
+ }
64
+
65
+ run();
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@drico2008/fincli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Modern financial CLI/TUI terminal for market monitoring and analysis.",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "fincli": "npm/bin/fincli.js"
8
8
  },
9
- "scripts": {
10
- "postinstall": "node npm/postinstall.js",
11
- "check": "node --check npm/bin/fincli.js && node --check npm/postinstall.js",
12
- "prepublish:safety": "python scripts/prepublish_check.py",
13
- "prepublishOnly": "python scripts/prepublish_check.py"
14
- },
9
+ "scripts": {
10
+ "postinstall": "node npm/postinstall.js",
11
+ "check": "node --check npm/bin/fincli.js && node --check npm/postinstall.js",
12
+ "prepublish:safety": "python scripts/prepublish_check.py",
13
+ "prepublishOnly": "python scripts/prepublish_check.py"
14
+ },
15
15
  "files": [
16
16
  "fincli/**/*.py",
17
17
  "npm",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fincli"
7
- version = "0.3.1"
7
+ version = "0.4.0"
8
8
  description = "Modern financial CLI/TUI terminal for market monitoring and analysis."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"