filtrix-cli 0.1.0__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 (43) hide show
  1. filtrix_cli-0.1.0/LICENSE +21 -0
  2. filtrix_cli-0.1.0/PKG-INFO +80 -0
  3. filtrix_cli-0.1.0/README.md +48 -0
  4. filtrix_cli-0.1.0/pyproject.toml +51 -0
  5. filtrix_cli-0.1.0/src/filtrix_cli/AGENTS.md +47 -0
  6. filtrix_cli-0.1.0/src/filtrix_cli/__init__.py +1 -0
  7. filtrix_cli-0.1.0/src/filtrix_cli/__main__.py +60 -0
  8. filtrix_cli-0.1.0/src/filtrix_cli/auth.py +76 -0
  9. filtrix_cli-0.1.0/src/filtrix_cli/client.py +132 -0
  10. filtrix_cli-0.1.0/src/filtrix_cli/commands/__init__.py +0 -0
  11. filtrix_cli-0.1.0/src/filtrix_cli/commands/_helpers.py +44 -0
  12. filtrix_cli-0.1.0/src/filtrix_cli/commands/account.py +9 -0
  13. filtrix_cli-0.1.0/src/filtrix_cli/commands/authcmd.py +166 -0
  14. filtrix_cli-0.1.0/src/filtrix_cli/commands/backtest.py +25 -0
  15. filtrix_cli-0.1.0/src/filtrix_cli/commands/configcmd.py +40 -0
  16. filtrix_cli-0.1.0/src/filtrix_cli/commands/filters.py +21 -0
  17. filtrix_cli-0.1.0/src/filtrix_cli/commands/keys.py +29 -0
  18. filtrix_cli-0.1.0/src/filtrix_cli/commands/llms.py +12 -0
  19. filtrix_cli-0.1.0/src/filtrix_cli/commands/screen.py +22 -0
  20. filtrix_cli-0.1.0/src/filtrix_cli/commands/ticker.py +11 -0
  21. filtrix_cli-0.1.0/src/filtrix_cli/config.py +86 -0
  22. filtrix_cli-0.1.0/src/filtrix_cli/filters.py +98 -0
  23. filtrix_cli-0.1.0/src/filtrix_cli/output.py +56 -0
  24. filtrix_cli-0.1.0/src/filtrix_cli/render.py +120 -0
  25. filtrix_cli-0.1.0/src/filtrix_cli/state.py +30 -0
  26. filtrix_cli-0.1.0/tests/__init__.py +0 -0
  27. filtrix_cli-0.1.0/tests/test_auth.py +63 -0
  28. filtrix_cli-0.1.0/tests/test_client.py +95 -0
  29. filtrix_cli-0.1.0/tests/test_client_register.py +62 -0
  30. filtrix_cli-0.1.0/tests/test_cmd_auth.py +55 -0
  31. filtrix_cli-0.1.0/tests/test_cmd_backtest_screen.py +70 -0
  32. filtrix_cli-0.1.0/tests/test_cmd_config.py +39 -0
  33. filtrix_cli-0.1.0/tests/test_cmd_keys.py +62 -0
  34. filtrix_cli-0.1.0/tests/test_cmd_llms.py +17 -0
  35. filtrix_cli-0.1.0/tests/test_cmd_profile.py +34 -0
  36. filtrix_cli-0.1.0/tests/test_cmd_read.py +71 -0
  37. filtrix_cli-0.1.0/tests/test_cmd_register.py +81 -0
  38. filtrix_cli-0.1.0/tests/test_config.py +56 -0
  39. filtrix_cli-0.1.0/tests/test_filters.py +125 -0
  40. filtrix_cli-0.1.0/tests/test_helpers.py +37 -0
  41. filtrix_cli-0.1.0/tests/test_output.py +50 -0
  42. filtrix_cli-0.1.0/tests/test_render.py +30 -0
  43. filtrix_cli-0.1.0/tests/test_smoke.py +17 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Filtrix
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: filtrix-cli
3
+ Version: 0.1.0
4
+ Summary: Agent-first command-line interface for the FILTRIX backtest engine.
5
+ Project-URL: Homepage, https://filtrix.net
6
+ Project-URL: Documentation, https://pypi.org/project/filtrix-cli/
7
+ Author-email: Filtrix <team@filtrix.net>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai-agents,backtest,cli,filtrix,quant,screener,trading
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Office/Business :: Financial :: Investment
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: platformdirs>=4
24
+ Requires-Dist: rich>=13
25
+ Requires-Dist: tomli-w>=1.0
26
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
27
+ Requires-Dist: typer>=0.12
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8; extra == 'dev'
30
+ Requires-Dist: respx>=0.21; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # filtrix-cli
34
+
35
+ Agent-first command-line interface to [FILTRIX](https://filtrix.net) — historical trading-pattern backtesting.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pipx install filtrix-cli # or: uvx filtrix-cli ...
41
+ ```
42
+
43
+ ## Authenticate
44
+
45
+ ```bash
46
+ export FILTRIX_API_KEY=flt_your_key # for backtests/screens/ticker/account
47
+ filtrix auth login # for managing API keys (stores a JWT)
48
+ ```
49
+
50
+ ### Sign up (terminal)
51
+
52
+ ```bash
53
+ filtrix auth register --email you@example.com --accept-tos
54
+ # FILTRIX emails a 6-digit code; paste it when prompted; you're logged in.
55
+ ```
56
+
57
+ ## Examples
58
+
59
+ ```bash
60
+ filtrix backtest --filter "gap_gup>=50" --filter "price_price<5"
61
+ filtrix screen --filter "gap_gup>=50" --limit 20
62
+ filtrix --json screen --filter "gap_gup>=50" # force JSON in a terminal
63
+ filtrix ticker NVDA
64
+ filtrix filters --search gap
65
+ filtrix account
66
+ filtrix keys create --name laptop
67
+ filtrix llms # machine-readable usage guide for AI agents
68
+ ```
69
+
70
+ ## Output & scripting
71
+
72
+ Output is human-readable in a terminal and JSON when piped or with the global `--json` flag.
73
+ Global flags (`--json`, `--quiet`, `--profile`, `--api-key`, `--base-url`) go BEFORE the subcommand.
74
+ Exit codes: 0 ok, 2 usage, 3 not-found, 4 auth, 5 rate-limited, 6 network, 1 server.
75
+
76
+ ## Configuration
77
+
78
+ Settings resolve in order: CLI flag > environment variable > config file
79
+ (`filtrix config path` shows the location). Use `--profile NAME` for multiple environments.
80
+ Credentials (API key / JWT) are stored in cleartext in this TOML file (best-effort `chmod 600` on POSIX) — treat it like an SSH key.
@@ -0,0 +1,48 @@
1
+ # filtrix-cli
2
+
3
+ Agent-first command-line interface to [FILTRIX](https://filtrix.net) — historical trading-pattern backtesting.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pipx install filtrix-cli # or: uvx filtrix-cli ...
9
+ ```
10
+
11
+ ## Authenticate
12
+
13
+ ```bash
14
+ export FILTRIX_API_KEY=flt_your_key # for backtests/screens/ticker/account
15
+ filtrix auth login # for managing API keys (stores a JWT)
16
+ ```
17
+
18
+ ### Sign up (terminal)
19
+
20
+ ```bash
21
+ filtrix auth register --email you@example.com --accept-tos
22
+ # FILTRIX emails a 6-digit code; paste it when prompted; you're logged in.
23
+ ```
24
+
25
+ ## Examples
26
+
27
+ ```bash
28
+ filtrix backtest --filter "gap_gup>=50" --filter "price_price<5"
29
+ filtrix screen --filter "gap_gup>=50" --limit 20
30
+ filtrix --json screen --filter "gap_gup>=50" # force JSON in a terminal
31
+ filtrix ticker NVDA
32
+ filtrix filters --search gap
33
+ filtrix account
34
+ filtrix keys create --name laptop
35
+ filtrix llms # machine-readable usage guide for AI agents
36
+ ```
37
+
38
+ ## Output & scripting
39
+
40
+ Output is human-readable in a terminal and JSON when piped or with the global `--json` flag.
41
+ Global flags (`--json`, `--quiet`, `--profile`, `--api-key`, `--base-url`) go BEFORE the subcommand.
42
+ Exit codes: 0 ok, 2 usage, 3 not-found, 4 auth, 5 rate-limited, 6 network, 1 server.
43
+
44
+ ## Configuration
45
+
46
+ Settings resolve in order: CLI flag > environment variable > config file
47
+ (`filtrix config path` shows the location). Use `--profile NAME` for multiple environments.
48
+ Credentials (API key / JWT) are stored in cleartext in this TOML file (best-effort `chmod 600` on POSIX) — treat it like an SSH key.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "filtrix-cli"
7
+ version = "0.1.0"
8
+ description = "Agent-first command-line interface for the FILTRIX backtest engine."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Filtrix", email = "team@filtrix.net"},
14
+ ]
15
+ keywords = ["filtrix", "cli", "trading", "backtest", "screener", "ai-agents", "quant"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: Financial and Insurance Industry",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Office/Business :: Financial :: Investment",
27
+ ]
28
+ dependencies = [
29
+ "typer>=0.12",
30
+ "rich>=13",
31
+ "httpx>=0.27",
32
+ "platformdirs>=4",
33
+ "tomli-w>=1.0",
34
+ "tomli>=2.0; python_version < '3.11'",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ dev = ["pytest>=8", "respx>=0.21"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://filtrix.net"
42
+ Documentation = "https://pypi.org/project/filtrix-cli/"
43
+
44
+ [project.scripts]
45
+ filtrix = "filtrix_cli.__main__:main"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/filtrix_cli"]
49
+
50
+ [tool.hatch.build.targets.wheel.force-include]
51
+ "src/filtrix_cli/AGENTS.md" = "filtrix_cli/AGENTS.md"
@@ -0,0 +1,47 @@
1
+ # filtrix-cli — agent guide
2
+
3
+ Structured command-line access to the FILTRIX historical backtest engine.
4
+
5
+ ## Output contract
6
+ - Add `--json` (or pipe stdout) to get JSON on stdout. Human tables otherwise.
7
+ - Data goes to stdout; errors/progress go to stderr.
8
+ - Errors (stderr, JSON): {"error":{"type","message","status","retryable","suggestion","input"}}.
9
+
10
+ ## Exit codes
11
+ - 0 success
12
+ - 1 server/unexpected error (retryable)
13
+ - 2 usage / invalid filter
14
+ - 3 not found
15
+ - 4 authentication required/failed
16
+ - 5 rate limited (retryable)
17
+ - 6 network/timeout (retryable)
18
+
19
+ ## Auth
20
+ - Reads/backtests: set env FILTRIX_API_KEY=flt_... (or `filtrix config set api_key ...`).
21
+ - Key management: `filtrix auth login` first (stores a JWT).
22
+
23
+ ## Commands
24
+ - filtrix backtest --filter "COL OP VAL" [--filter ...] [--outcome ...] [--start YYYY-MM-DD] [--end YYYY-MM-DD]
25
+ - filtrix screen --filter ... [--limit N] [--offset N]
26
+ - filtrix ticker SYMBOL
27
+ - filtrix filters [--search TEXT] [--type number|date|string]
28
+ - filtrix account
29
+ - filtrix keys list|create|revoke
30
+ - filtrix auth login|logout|status
31
+ - filtrix auth register --email E --password P --accept-tos (emails a 6-digit code, then prompts for it, then auto-logs-in)
32
+ - filtrix auth verify-code CODE --email E --password P (resume a pending signup)
33
+ - filtrix auth resend-code --email E
34
+ - filtrix config get|set|list|path
35
+
36
+ Note: global flags like `--json` go BEFORE the subcommand (e.g. `filtrix --json backtest ...`).
37
+
38
+ ## Filter DSL
39
+ "COL OP VAL", operators: >= <= != = > < like in. Repeat --filter for AND.
40
+ Examples: "gap_gup>=10", "price_price<5", "earnings_date_days_earningd=0.25", "sic_code in 7372,7389".
41
+ Use the RAW schema column names from `filtrix filters` (e.g. price_price, float_shares_float) — these
42
+ can differ from screen's friendlier output field names (price, float_shares). Unknown columns return exit 2.
43
+ For exact control, pass `--filters-json '[{"column":...,"operator":...,"value":...,"value_type":...}]'`
44
+ or `--filters-json -` to read the JSON array from stdin.
45
+
46
+ ## Discover filters
47
+ Run `filtrix filters --json` to enumerate valid columns, value types, and operators.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,60 @@
1
+ import typer
2
+
3
+ from filtrix_cli import __version__
4
+ from filtrix_cli.state import set_state
5
+ from filtrix_cli.commands import backtest as backtest_cmd
6
+ from filtrix_cli.commands import screen as screen_cmd
7
+ from filtrix_cli.commands import ticker as ticker_cmd
8
+ from filtrix_cli.commands import filters as filters_cmd
9
+ from filtrix_cli.commands import account as account_cmd
10
+ from filtrix_cli.commands import keys as keys_cmd
11
+ from filtrix_cli.commands import authcmd
12
+ from filtrix_cli.commands import configcmd
13
+ from filtrix_cli.commands import llms as llms_cmd
14
+
15
+ app = typer.Typer(
16
+ add_completion=False,
17
+ help="FILTRIX CLI — terminal access to the FILTRIX backtest engine.",
18
+ )
19
+
20
+
21
+ @app.callback(invoke_without_command=True)
22
+ def _root(
23
+ ctx: typer.Context,
24
+ json: bool = typer.Option(False, "--json", help="Force JSON output to stdout."),
25
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="Bare values only."),
26
+ no_color: bool = typer.Option(False, "--no-color", help="Disable colored output."),
27
+ profile: str = typer.Option(None, "--profile", help="Config profile to use."),
28
+ api_key: str = typer.Option(None, "--api-key", help="Override API key for this run."),
29
+ base_url: str = typer.Option(None, "--base-url", help="Override API base URL for this run."),
30
+ ) -> None:
31
+ """FILTRIX CLI — terminal access to the FILTRIX backtest engine."""
32
+ set_state(json=json, quiet=quiet, no_color=no_color,
33
+ profile=profile, api_key=api_key, base_url=base_url)
34
+ if ctx.invoked_subcommand is None:
35
+ typer.echo(ctx.get_help())
36
+
37
+
38
+ @app.command()
39
+ def version() -> None:
40
+ """Print the CLI version."""
41
+ typer.echo(f"filtrix-cli {__version__}")
42
+
43
+
44
+ app.command("backtest")(backtest_cmd.backtest)
45
+ app.command("screen")(screen_cmd.screen)
46
+ app.command("ticker")(ticker_cmd.ticker)
47
+ app.command("filters")(filters_cmd.filters)
48
+ app.command("account")(account_cmd.account)
49
+ app.add_typer(keys_cmd.app, name="keys")
50
+ app.add_typer(authcmd.app, name="auth")
51
+ app.add_typer(configcmd.app, name="config")
52
+ app.command("llms")(llms_cmd.llms)
53
+
54
+
55
+ def main() -> None:
56
+ app()
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
@@ -0,0 +1,76 @@
1
+ """Interactive login flows: direct, Telegram-2FA poll, and backup code."""
2
+ import time
3
+ from typing import Callable, Optional
4
+
5
+ import httpx
6
+
7
+ from filtrix_cli import __version__
8
+ from filtrix_cli.output import CLIError
9
+
10
+ _POLL_DEADLINE_SECONDS = 300
11
+
12
+
13
+ def _ua() -> dict:
14
+ return {"User-Agent": f"filtrix-cli/{__version__}", "X-Agent-Request": "true"}
15
+
16
+
17
+ def login(
18
+ base_url: str,
19
+ email: str,
20
+ password: str,
21
+ *,
22
+ poll_interval: float = 2.0,
23
+ on_pending: Optional[Callable[[str, Optional[str]], None]] = None,
24
+ ) -> str:
25
+ """Return a JWT. Handles direct login and Telegram 2FA. Raises CLIError
26
+ (type 'backup_code_required') when only a backup code can proceed."""
27
+ base = base_url.rstrip("/")
28
+ with httpx.Client(timeout=30) as client:
29
+ resp = client.post(f"{base}/api/auth/login", data={"username": email, "password": password}, headers=_ua())
30
+ if resp.status_code == 200:
31
+ return resp.json()["access_token"]
32
+ if resp.status_code == 400:
33
+ raise CLIError("auth_failed", "Incorrect email or password.", status=400, exit_code=4)
34
+ if resp.status_code == 202:
35
+ body = resp.json()
36
+ required = body.get("auth_required")
37
+ if required == "backup_code_only":
38
+ raise CLIError("backup_code_required",
39
+ "2FA is on but Telegram is unlinked. Re-run with --backup-code.",
40
+ status=202, exit_code=4)
41
+ if required == "telegram":
42
+ pending_id = body["pending_id"]
43
+ if on_pending:
44
+ on_pending(pending_id, body.get("expires_at"))
45
+ return _poll_login(client, base, pending_id, poll_interval)
46
+ raise CLIError("auth_failed", f"Unexpected auth_required: {required}", status=202, exit_code=4)
47
+ raise CLIError("server_error", f"Login failed (HTTP {resp.status_code}).",
48
+ status=resp.status_code, exit_code=1)
49
+
50
+
51
+ def _poll_login(client: httpx.Client, base: str, pending_id: str, interval: float) -> str:
52
+ deadline = time.time() + _POLL_DEADLINE_SECONDS
53
+ while time.time() < deadline:
54
+ status = client.get(f"{base}/api/auth/login/status/{pending_id}", headers=_ua()).json().get("status")
55
+ if status == "approved":
56
+ done = client.post(f"{base}/api/auth/login/complete/{pending_id}", headers=_ua())
57
+ if done.status_code == 200:
58
+ return done.json()["access_token"]
59
+ raise CLIError("auth_failed", f"Could not finalize login (HTTP {done.status_code}).",
60
+ status=done.status_code, exit_code=4)
61
+ if status == "denied":
62
+ raise CLIError("auth_failed", "Login was denied from Telegram.", exit_code=4)
63
+ if status == "expired":
64
+ raise CLIError("auth_failed", "Login approval expired. Try again.", exit_code=4)
65
+ time.sleep(interval)
66
+ raise CLIError("auth_failed", "Timed out waiting for Telegram approval.", retryable=True, exit_code=6)
67
+
68
+
69
+ def login_backup_code(base_url: str, email: str, password: str, code: str) -> str:
70
+ base = base_url.rstrip("/")
71
+ with httpx.Client(timeout=30) as client:
72
+ resp = client.post(f"{base}/api/auth/login/backup-code",
73
+ json={"email": email, "password": password, "code": code}, headers=_ua())
74
+ if resp.status_code == 200:
75
+ return resp.json()["access_token"]
76
+ raise CLIError("auth_failed", "Backup-code login failed.", status=resp.status_code, exit_code=4)
@@ -0,0 +1,132 @@
1
+ """Synchronous httpx client for the FILTRIX API, with error -> CLIError mapping."""
2
+ import json
3
+ from typing import Any, List, Optional
4
+
5
+ import httpx
6
+
7
+ from filtrix_cli import __version__
8
+ from filtrix_cli.output import CLIError
9
+
10
+
11
+ def _safe_detail(resp: httpx.Response) -> str:
12
+ try:
13
+ data = resp.json()
14
+ except Exception:
15
+ return resp.text or f"HTTP {resp.status_code}"
16
+ if isinstance(data, dict) and "detail" in data:
17
+ detail = data["detail"]
18
+ return detail if isinstance(detail, str) else json.dumps(detail)
19
+ return json.dumps(data)
20
+
21
+
22
+ class FiltrixClient:
23
+ def __init__(self, base_url: str, api_key: Optional[str] = None,
24
+ jwt: Optional[str] = None, timeout: float = 60.0):
25
+ self.base_url = base_url.rstrip("/")
26
+ self.api_key = api_key
27
+ self.jwt = jwt
28
+ self.timeout = timeout
29
+
30
+ def _headers(self, auth: str) -> dict:
31
+ """auth in {'any', 'jwt', 'none'}."""
32
+ headers = {
33
+ "User-Agent": f"filtrix-cli/{__version__}",
34
+ "X-Agent-Request": "true",
35
+ "Content-Type": "application/json",
36
+ }
37
+ if auth == "none":
38
+ return headers
39
+ if auth == "jwt":
40
+ if not self.jwt:
41
+ raise CLIError("auth_failed", "This command requires login. Run `filtrix auth login`.",
42
+ suggestion="filtrix auth login", exit_code=4)
43
+ headers["Authorization"] = f"Bearer {self.jwt}"
44
+ return headers
45
+ # auth == "any"
46
+ if self.api_key:
47
+ headers["X-API-Key"] = self.api_key
48
+ elif self.jwt:
49
+ headers["Authorization"] = f"Bearer {self.jwt}"
50
+ else:
51
+ raise CLIError("auth_failed", "No credentials. Set FILTRIX_API_KEY or run `filtrix auth login`.",
52
+ suggestion="filtrix auth login", exit_code=4)
53
+ return headers
54
+
55
+ def _request(self, method: str, path: str, *, json_body: Any = None, auth: str = "any") -> Any:
56
+ url = f"{self.base_url}{path}"
57
+ headers = self._headers(auth)
58
+ try:
59
+ with httpx.Client(timeout=self.timeout) as client:
60
+ resp = client.request(method, url, headers=headers, json=json_body)
61
+ except httpx.TimeoutException as e:
62
+ raise CLIError("network", f"Request timed out: {e}", retryable=True, exit_code=6)
63
+ except httpx.HTTPError as e:
64
+ raise CLIError("network", f"Network error: {e}", retryable=True, exit_code=6)
65
+ return self._handle_response(resp)
66
+
67
+ def _handle_response(self, resp: httpx.Response) -> Any:
68
+ if resp.status_code < 400:
69
+ return resp.json() if resp.content else {}
70
+ detail = _safe_detail(resp)
71
+ code = resp.status_code
72
+ if code in (401, 403):
73
+ raise CLIError("auth_failed", detail, status=code,
74
+ suggestion="Run `filtrix auth login` or check your API key.", exit_code=4)
75
+ if code == 404:
76
+ raise CLIError("not_found", detail, status=404, exit_code=3)
77
+ if code == 429:
78
+ raise CLIError("rate_limited", detail, status=429, retryable=True,
79
+ suggestion="Run `filtrix account` to see your quota.", exit_code=5)
80
+ if 400 <= code < 500:
81
+ raise CLIError("invalid_request", detail, status=code, exit_code=2)
82
+ raise CLIError("server_error", detail, status=code, retryable=True, exit_code=1)
83
+
84
+ # ---- Public API v1 ----
85
+ def backtest(self, filters: List[dict], outcome: str = "day_close_position",
86
+ flat_threshold: float = 0.002, start_date: Optional[str] = None,
87
+ end_date: Optional[str] = None) -> Any:
88
+ body: dict = {"filters": filters, "outcome": outcome, "flat_threshold": flat_threshold}
89
+ if start_date:
90
+ body["start_date"] = start_date
91
+ if end_date:
92
+ body["end_date"] = end_date
93
+ return self._request("POST", "/api/v1/backtest", json_body=body)
94
+
95
+ def screen(self, filters: List[dict], limit: int = 50, offset: int = 0) -> Any:
96
+ return self._request("POST", "/api/v1/screen",
97
+ json_body={"filters": filters, "limit": limit, "offset": offset})
98
+
99
+ def ticker_info(self, symbol: str) -> Any:
100
+ return self._request("GET", f"/api/v1/ticker/{symbol.upper()}")
101
+
102
+ def list_filters(self) -> Any:
103
+ return self._request("GET", "/api/v1/filters", auth="none")
104
+
105
+ def account(self) -> Any:
106
+ return self._request("GET", "/api/v1/account")
107
+
108
+ # ---- Key management (JWT) ----
109
+ def keys_list(self) -> Any:
110
+ return self._request("GET", "/api/keys", auth="jwt")
111
+
112
+ def keys_create(self, name: str = "default") -> Any:
113
+ return self._request("POST", "/api/keys", json_body={"name": name}, auth="jwt")
114
+
115
+ def keys_revoke(self, key_id: int) -> Any:
116
+ return self._request("DELETE", f"/api/keys/{key_id}", auth="jwt")
117
+
118
+ # ---- Registration (pre-auth) ----
119
+ def register(self, email: str, password: str, delivery: str = "code",
120
+ ref_source: Optional[str] = None) -> Any:
121
+ body: dict = {"email": email, "password": password, "delivery": delivery}
122
+ if ref_source:
123
+ body["ref_data"] = {"ref_source": ref_source}
124
+ return self._request("POST", "/api/auth/register", json_body=body, auth="none")
125
+
126
+ def verify_email_code(self, email: str, code: str) -> Any:
127
+ return self._request("POST", "/api/auth/verify-email-code",
128
+ json_body={"email": email, "code": code}, auth="none")
129
+
130
+ def resend_code(self, email: str) -> Any:
131
+ return self._request("POST", "/api/auth/resend-code",
132
+ json_body={"email": email}, auth="none")
File without changes
@@ -0,0 +1,44 @@
1
+ """Shared plumbing for Typer commands: error handling, client construction, output."""
2
+ import functools
3
+ from typing import Any, Callable, Tuple
4
+
5
+ import typer
6
+
7
+ from filtrix_cli import config as config_mod
8
+ from filtrix_cli.client import FiltrixClient
9
+ from filtrix_cli.output import CLIError, emit_error, emit_json, is_json_mode
10
+ from filtrix_cli.render import render_human
11
+ from filtrix_cli.state import get_state
12
+
13
+
14
+ def command(func: Callable) -> Callable:
15
+ """Wrap a Typer command: catch CLIError, emit it, exit with its code.
16
+ functools.wraps keeps the original signature so Typer still sees the params."""
17
+ @functools.wraps(func)
18
+ def wrapper(*args, **kwargs):
19
+ json_mode = is_json_mode(get_state().json)
20
+ try:
21
+ return func(*args, **kwargs)
22
+ except CLIError as err:
23
+ emit_error(err, json_mode)
24
+ raise typer.Exit(err.exit_code)
25
+ return wrapper
26
+
27
+
28
+ def build_client() -> Tuple[FiltrixClient, dict]:
29
+ state = get_state()
30
+ settings = config_mod.resolve_settings(
31
+ profile=state.profile, api_key=state.api_key, base_url=state.base_url
32
+ )
33
+ client = FiltrixClient(
34
+ base_url=settings["base_url"], api_key=settings["api_key"], jwt=settings["jwt"]
35
+ )
36
+ return client, settings
37
+
38
+
39
+ def output_result(result: Any, *, kind: str) -> None:
40
+ state = get_state()
41
+ if is_json_mode(state.json):
42
+ emit_json(result)
43
+ else:
44
+ render_human(result, kind=kind, quiet=state.quiet, no_color=state.no_color)
@@ -0,0 +1,9 @@
1
+ from filtrix_cli.commands._helpers import build_client, command, output_result
2
+
3
+
4
+ @command
5
+ def account() -> None:
6
+ """Show your tier and remaining daily quota."""
7
+ client, _ = build_client()
8
+ result = client.account()
9
+ output_result(result, kind="account")