finradar-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.
- finradar_cli-0.1.0/.gitignore +16 -0
- finradar_cli-0.1.0/CHANGELOG.md +34 -0
- finradar_cli-0.1.0/Makefile +15 -0
- finradar_cli-0.1.0/PKG-INFO +19 -0
- finradar_cli-0.1.0/README.md +239 -0
- finradar_cli-0.1.0/RELEASE.md +65 -0
- finradar_cli-0.1.0/SECURITY.md +72 -0
- finradar_cli-0.1.0/_finradar_main.py +7 -0
- finradar_cli-0.1.0/finradar.spec +13 -0
- finradar_cli-0.1.0/finradar_cli/__init__.py +1 -0
- finradar_cli-0.1.0/finradar_cli/__main__.py +4 -0
- finradar_cli-0.1.0/finradar_cli/_generated/__init__.py +96 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_13dg.py +23 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_13f.py +291 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_billing.py +68 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_core.py +38 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_cusip.py +124 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_download.py +23 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_facts.py +38 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_financials.py +141 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_holders.py +53 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_index.py +21 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_insider.py +322 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_market.py +23 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_ownership.py +111 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_ownership_analytics.py +38 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_positions.py +83 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_pricing.py +53 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_progress.py +23 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_search.py +171 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_sec.py +141 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_status.py +36 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_ticker.py +92 -0
- finradar_cli-0.1.0/finradar_cli/_generated/cmd_user.py +21 -0
- finradar_cli-0.1.0/finradar_cli/_generated/command_index.json +1308 -0
- finradar_cli-0.1.0/finradar_cli/_generated/completion/commands.json +373 -0
- finradar_cli-0.1.0/finradar_cli/app.py +103 -0
- finradar_cli-0.1.0/finradar_cli/commands/__init__.py +0 -0
- finradar_cli-0.1.0/finradar_cli/commands/api.py +124 -0
- finradar_cli-0.1.0/finradar_cli/commands/auth_cmds.py +64 -0
- finradar_cli-0.1.0/finradar_cli/commands/meta.py +83 -0
- finradar_cli-0.1.0/finradar_cli/commands/update_cmd.py +234 -0
- finradar_cli-0.1.0/finradar_cli/core/__init__.py +0 -0
- finradar_cli-0.1.0/finradar_cli/core/auth.py +178 -0
- finradar_cli-0.1.0/finradar_cli/core/client.py +95 -0
- finradar_cli-0.1.0/finradar_cli/core/coerce.py +59 -0
- finradar_cli-0.1.0/finradar_cli/core/config.py +80 -0
- finradar_cli-0.1.0/finradar_cli/core/curation.py +17 -0
- finradar_cli-0.1.0/finradar_cli/core/errors.py +44 -0
- finradar_cli-0.1.0/finradar_cli/core/oauth.py +206 -0
- finradar_cli-0.1.0/finradar_cli/core/output.py +49 -0
- finradar_cli-0.1.0/finradar_cli/core/spec.py +42 -0
- finradar_cli-0.1.0/finradar_cli/data/openapi.json +12797 -0
- finradar_cli-0.1.0/finradar_cli/marquee.toml +37 -0
- finradar_cli-0.1.0/finradar_cli/type_overrides.toml +6 -0
- finradar_cli-0.1.0/generate.py +460 -0
- finradar_cli-0.1.0/pyproject.toml +41 -0
- finradar_cli-0.1.0/tests/conftest.py +19 -0
- finradar_cli-0.1.0/tests/fixtures/mini_openapi.json +24 -0
- finradar_cli-0.1.0/tests/test_api_command.py +140 -0
- finradar_cli-0.1.0/tests/test_app_smoke.py +29 -0
- finradar_cli-0.1.0/tests/test_auth.py +103 -0
- finradar_cli-0.1.0/tests/test_auth_cmds.py +39 -0
- finradar_cli-0.1.0/tests/test_build_client_oauth.py +52 -0
- finradar_cli-0.1.0/tests/test_client.py +136 -0
- finradar_cli-0.1.0/tests/test_codegen.py +124 -0
- finradar_cli-0.1.0/tests/test_coerce.py +40 -0
- finradar_cli-0.1.0/tests/test_command_index.py +67 -0
- finradar_cli-0.1.0/tests/test_completion.py +99 -0
- finradar_cli-0.1.0/tests/test_completion_cmd.py +11 -0
- finradar_cli-0.1.0/tests/test_config.py +46 -0
- finradar_cli-0.1.0/tests/test_curation.py +18 -0
- finradar_cli-0.1.0/tests/test_errors.py +42 -0
- finradar_cli-0.1.0/tests/test_generated_commands_e2e.py +21 -0
- finradar_cli-0.1.0/tests/test_login_command.py +36 -0
- finradar_cli-0.1.0/tests/test_oauth_discovery.py +109 -0
- finradar_cli-0.1.0/tests/test_oauth_pkce.py +15 -0
- finradar_cli-0.1.0/tests/test_oauth_request_retry.py +138 -0
- finradar_cli-0.1.0/tests/test_oauth_store_refresh.py +58 -0
- finradar_cli-0.1.0/tests/test_oauth_token_exchange.py +34 -0
- finradar_cli-0.1.0/tests/test_output.py +46 -0
- finradar_cli-0.1.0/tests/test_output_encoding.py +49 -0
- finradar_cli-0.1.0/tests/test_spec.py +58 -0
- finradar_cli-0.1.0/tests/test_update_check.py +27 -0
- finradar_cli-0.1.0/tests/test_update_cmd.py +327 -0
- finradar_cli-0.1.0/tests/test_version.py +7 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the FinRadar CLI are documented here.
|
|
4
|
+
This project adheres to [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] — first public release
|
|
7
|
+
|
|
8
|
+
First public release of `finradar`, a command-line client for the FinRadar REST API.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **119 generated noun-verb commands** over the API (`finradar sec filings search`,
|
|
12
|
+
`finradar 13f fund show`, `finradar insider transactions by-ticker`, …) plus a raw
|
|
13
|
+
`finradar api METHOD PATH key=value` escape hatch for any endpoint.
|
|
14
|
+
- **Authentication:** `finradar configure` (API key → OS keychain) and `finradar login`
|
|
15
|
+
(OAuth 2.1 loopback PKCE; requires the backend `cli_access` feature, which ships OFF).
|
|
16
|
+
- **Output formats:** `-o json | table | yaml`; scriptable exit codes (0–7); a
|
|
17
|
+
no-telemetry, channel-aware version nudge.
|
|
18
|
+
- **Distribution:** PyPI wheel (pip/pipx), standalone PyInstaller binaries, a Homebrew
|
|
19
|
+
tap, and a Scoop bucket. `finradar update` self-updates a standalone binary
|
|
20
|
+
(sha256-verified, atomic, frozen-only).
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
- Shipped after an independent pre-release security review (**GO-WITH-FIXES**). The
|
|
24
|
+
one release-gating item (a Windows self-update brick path) and all confirmed
|
|
25
|
+
should-fix findings were remediated before release:
|
|
26
|
+
- Atomic Windows self-update with rollback (no bricked binary on a failed swap).
|
|
27
|
+
- Strict `cli-vN.N.N` tag-format gate (closes a release-pipeline command-injection).
|
|
28
|
+
- SHA-pinned third-party GitHub Actions; least-privilege workflow tokens.
|
|
29
|
+
- Owner-only ACLs on the credential file fallback (Windows + POSIX), created atomically.
|
|
30
|
+
- Transient-safe OAuth token retry (no longer logs the user out on a network blip).
|
|
31
|
+
- OAuth metadata origin + https pinning; idempotent-only HTTP retries; constant-time
|
|
32
|
+
`state` comparison.
|
|
33
|
+
- See `SECURITY.md` for the full posture, the accepted-risk items, and the GA roadmap
|
|
34
|
+
(code-signing/Sigstore, PyPI Trusted Publishing, dependency lock, scope enforcement).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
SPEC_SRC = ../sandbox_frontend/public/openapi.json
|
|
2
|
+
SPEC_DST = finradar_cli/data/openapi.json
|
|
3
|
+
|
|
4
|
+
.PHONY: sync-spec generate check-drift
|
|
5
|
+
|
|
6
|
+
sync-spec:
|
|
7
|
+
cp $(SPEC_SRC) $(SPEC_DST)
|
|
8
|
+
@python -c "import json;print('synced', json.load(open('$(SPEC_DST)'))['info']['version'])"
|
|
9
|
+
|
|
10
|
+
generate:
|
|
11
|
+
python generate.py
|
|
12
|
+
|
|
13
|
+
check-drift: generate
|
|
14
|
+
git diff --exit-code finradar_cli/_generated finradar_cli/data || \
|
|
15
|
+
(echo 'Generated artifacts out of date — run `make sync-spec` (if the spec moved) then `make generate`, and commit the result' && exit 1)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: finradar-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line client for the FinRadar API
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: click<9,>=8.2
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Requires-Dist: keyring>=25
|
|
9
|
+
Requires-Dist: platformdirs>=4
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Requires-Dist: rich>=13
|
|
12
|
+
Requires-Dist: tomli-w>=1.0
|
|
13
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
14
|
+
Requires-Dist: typer>=0.15
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
18
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
19
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# finradar CLI
|
|
2
|
+
|
|
3
|
+
A typed command-line client for the [FinRadar](https://uat.finradarapi.com) REST API.
|
|
4
|
+
Every public endpoint is available as a `finradar <group> <verb>` command; the raw
|
|
5
|
+
`finradar api` escape hatch reaches anything else.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
**pipx (recommended for Python users)**
|
|
12
|
+
```bash
|
|
13
|
+
pipx install finradar-cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Homebrew (macOS / Linux)**
|
|
17
|
+
```bash
|
|
18
|
+
brew install MarounAntoun/tap/finradar
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Scoop (Windows)**
|
|
22
|
+
```powershell
|
|
23
|
+
scoop bucket add finradar https://github.com/MarounAntoun/scoop-finradar
|
|
24
|
+
scoop install finradar
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Standalone binary — no Python required**
|
|
28
|
+
|
|
29
|
+
Download the pre-built binary for your OS from the
|
|
30
|
+
[GitHub Releases page](https://github.com/MarounAntoun/finradar-cli/releases/latest):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# macOS / Linux
|
|
34
|
+
curl -sSL https://github.com/MarounAntoun/finradar-cli/releases/latest/download/finradar-linux \
|
|
35
|
+
-o finradar
|
|
36
|
+
chmod +x finradar && sudo mv finradar /usr/local/bin/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Windows: download `finradar-windows.exe` from the Releases page and place it on your `PATH`.
|
|
40
|
+
|
|
41
|
+
**Verify**
|
|
42
|
+
```bash
|
|
43
|
+
finradar --version
|
|
44
|
+
# finradar 0.1.0 (API spec 3.61.0; base https://uat.finradarapi.com)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Updates
|
|
50
|
+
|
|
51
|
+
The update method depends on how you installed:
|
|
52
|
+
|
|
53
|
+
| Install method | Update command |
|
|
54
|
+
|---|---|
|
|
55
|
+
| Standalone binary | `finradar update` |
|
|
56
|
+
| pipx | `pipx upgrade finradar-cli` |
|
|
57
|
+
| pip | `pip install -U finradar-cli` |
|
|
58
|
+
| Homebrew | `brew upgrade finradar` |
|
|
59
|
+
| Scoop | `scoop update finradar` |
|
|
60
|
+
|
|
61
|
+
**`finradar update`** — standalone-binary users only. Downloads the latest binary from
|
|
62
|
+
[MarounAntoun/finradar-cli](https://github.com/MarounAntoun/finradar-cli/releases/latest),
|
|
63
|
+
verifies the sha256 sidecar, and atomically replaces the running binary. Exits with code 2
|
|
64
|
+
on any hash mismatch or network error (the existing binary is left untouched).
|
|
65
|
+
|
|
66
|
+
If you run a pip/pipx install and call `finradar update`, the command will tell you to
|
|
67
|
+
use `pipx upgrade finradar-cli` instead — it never touches your Python environment.
|
|
68
|
+
|
|
69
|
+
**Version-skew nudge** — after any successful command, if the live API spec is newer
|
|
70
|
+
than the bundled one the CLI prints a one-line hint to stderr (once per hour; suppress
|
|
71
|
+
with `FINRADAR_NO_UPDATE_CHECK=1`). Standalone-binary users see `finradar update`;
|
|
72
|
+
pip/pipx users see `pipx upgrade finradar-cli`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Quickstart
|
|
77
|
+
|
|
78
|
+
### 1. Configure your API key
|
|
79
|
+
|
|
80
|
+
Get your key from the [Credentials page](https://uat.finradarapi.com/account/credentials),
|
|
81
|
+
then run:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
finradar configure
|
|
85
|
+
# API key: ••••••••••••••••
|
|
86
|
+
# Saved profile 'default' → https://uat.finradarapi.com (key in keychain).
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The API key is stored in your **OS keychain** (never in the config file).
|
|
90
|
+
Only the profile's `base_url` and `auth_type` are written to the config file.
|
|
91
|
+
You can also set `FINRADAR_API_KEY` in your environment to override the stored key.
|
|
92
|
+
|
|
93
|
+
### 2. Search SEC filings
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
finradar sec filings search --ticker AAPL --form-type 10-K --limit 5
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. Call a raw endpoint
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
finradar api GET /api/v1/sec/filings ticker=AAPL formType=10-K limit=5
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 4. (Optional) Log in with OAuth
|
|
106
|
+
|
|
107
|
+
Browser-based login via OAuth 2.1 PKCE — no API key required:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
finradar login
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Command model
|
|
116
|
+
|
|
117
|
+
Commands follow a `finradar <group> <verb> [options]` noun-verb pattern. Groups map
|
|
118
|
+
to API tag families; verbs map to HTTP operations.
|
|
119
|
+
|
|
120
|
+
| Group | Example command |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `sec` | `finradar sec filings search --ticker TSLA` |
|
|
123
|
+
| `insiders` | `finradar insiders transactions list --ticker NVDA --transaction-type P` |
|
|
124
|
+
| `holdings` | `finradar holdings list --cik 1067983 --quarter 2024Q4` |
|
|
125
|
+
| `cusip` | `finradar cusip lookup --cusip 037833100` |
|
|
126
|
+
|
|
127
|
+
Run `finradar --help` for the full list of groups, or `finradar <group> --help` for
|
|
128
|
+
operations within a group.
|
|
129
|
+
|
|
130
|
+
### Check your identity
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
finradar whoami
|
|
134
|
+
# profile: default
|
|
135
|
+
# auth_type: api_key
|
|
136
|
+
# base_url: https://uat.finradarapi.com
|
|
137
|
+
# balance: 198432 (paid)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Quick help
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
finradar --help # full group list
|
|
144
|
+
finradar <group> --help # verbs within a group
|
|
145
|
+
finradar <group> <verb> --help # flags for a specific command
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Raw escape hatch
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Any method, any endpoint — key=value pairs become query params (GET/DELETE)
|
|
152
|
+
# or JSON body fields (POST/PUT/PATCH)
|
|
153
|
+
finradar api GET /api/v1/sec/filings ticker=AAPL limit=5
|
|
154
|
+
finradar api POST /api/v1/search query="insider buys"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Output formats
|
|
160
|
+
|
|
161
|
+
Pass `-o` / `--output` to any command:
|
|
162
|
+
|
|
163
|
+
| Flag | Description |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `-o json` | JSON — compact when piped, pretty on a TTY **(default)** |
|
|
166
|
+
| `-o table` | Pretty table |
|
|
167
|
+
| `-o yaml` | YAML |
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
finradar sec filings search --ticker MSFT -o json | jq '.[0].accessionNo'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Profiles
|
|
176
|
+
|
|
177
|
+
Store multiple base URL / API key combinations as named profiles:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Create a profile
|
|
181
|
+
finradar configure --profile staging
|
|
182
|
+
|
|
183
|
+
# Use it for one command
|
|
184
|
+
finradar --profile staging sec filings search --ticker AAPL
|
|
185
|
+
|
|
186
|
+
# Or set it for the whole session
|
|
187
|
+
export FINRADAR_PROFILE=staging
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Config is stored in your OS config directory:
|
|
191
|
+
`~/.config/finradar/config.toml` (Linux), `~/Library/Application Support/finradar/config.toml` (macOS),
|
|
192
|
+
`%APPDATA%\finradar\config.toml` (Windows).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Shell completion
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Bash
|
|
200
|
+
finradar completion install bash
|
|
201
|
+
|
|
202
|
+
# Zsh
|
|
203
|
+
finradar completion install zsh
|
|
204
|
+
|
|
205
|
+
# Fish
|
|
206
|
+
finradar completion install fish
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Exit codes
|
|
212
|
+
|
|
213
|
+
| Code | Meaning |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `0` | OK — request succeeded |
|
|
216
|
+
| `1` | Usage error — bad flags or missing argument |
|
|
217
|
+
| `2` | Network error — DNS failure, timeout, unreachable host |
|
|
218
|
+
| `3` | Auth error — invalid or missing API key / token |
|
|
219
|
+
| `4` | Payment error — insufficient token balance (HTTP 402) |
|
|
220
|
+
| `5` | Rate limit — too many requests (HTTP 429) |
|
|
221
|
+
| `6` | Not found — resource does not exist (HTTP 404) |
|
|
222
|
+
| `7` | Server error — upstream 5xx |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Version skew
|
|
227
|
+
|
|
228
|
+
`finradar --version` prints the CLI version, the bundled API version, and the active
|
|
229
|
+
base URL. If the live API has been updated since the CLI was installed, a version-skew
|
|
230
|
+
notice is printed to stderr after any successful command (once per hour, suppressible
|
|
231
|
+
with `FINRADAR_NO_UPDATE_CHECK=1`).
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Source
|
|
236
|
+
|
|
237
|
+
This CLI is generated from the FinRadar OpenAPI spec. Commands are built from the
|
|
238
|
+
same `openapi.json` that powers the `/integrations/openapi` docs — if a new endpoint
|
|
239
|
+
appears in the spec, the next CLI release will expose it as a typed command.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# FinRadar CLI — Release Runbook
|
|
2
|
+
|
|
3
|
+
The release is fully automated by `.github/workflows/cli-release.yml`, triggered by
|
|
4
|
+
pushing a `cli-vX.Y.Z` tag. This runbook is the human checklist around it.
|
|
5
|
+
|
|
6
|
+
## One-time setup (maintainer only — required before the FIRST release)
|
|
7
|
+
|
|
8
|
+
These are account credentials the automation cannot create for itself. Add them as
|
|
9
|
+
**repository → Settings → Secrets and variables → Actions**:
|
|
10
|
+
|
|
11
|
+
1. **`DIST_PUSH_TOKEN`** — a fine-grained GitHub PAT with **Contents: write** on the
|
|
12
|
+
three public distribution repos: `MarounAntoun/finradar-cli`,
|
|
13
|
+
`MarounAntoun/homebrew-tap`, `MarounAntoun/scoop-finradar`. (The built-in
|
|
14
|
+
`GITHUB_TOKEN` cannot write cross-repo, which is why a PAT is required.)
|
|
15
|
+
2. **`PYPI_TOKEN`** — a PyPI **project-scoped** API token for `finradar-cli`.
|
|
16
|
+
(GA hardening: replace with OIDC Trusted Publishing — see SECURITY.md.)
|
|
17
|
+
|
|
18
|
+
Recommended repo hardening (supply-chain — see SECURITY.md): branch protection on
|
|
19
|
+
`main` with required reviews; enable Dependabot for `github-actions`.
|
|
20
|
+
|
|
21
|
+
## Pre-flight (before tagging)
|
|
22
|
+
|
|
23
|
+
- [ ] `main` is green: `cli-ci.yml` passing on the latest commit (all OS × Python legs + drift).
|
|
24
|
+
- [ ] Security review fixes merged (this is in `main` as of the v0.1.0 hardening PR).
|
|
25
|
+
- [ ] Version is intentional. The version lives in **one** place: `cli/finradar_cli/__init__.py`
|
|
26
|
+
(`__version__`). The tag MUST be `cli-v` + that exact value. For the first public
|
|
27
|
+
release that is `cli-v0.1.0`.
|
|
28
|
+
- [ ] `CHANGELOG.md` updated for the version.
|
|
29
|
+
- [ ] The 2 secrets above exist (first release only).
|
|
30
|
+
- [ ] Local sanity: `cd cli && python -m build --wheel && pyinstaller finradar.spec --clean --noconfirm`
|
|
31
|
+
both succeed; `dist/finradar.exe --version` prints the expected version.
|
|
32
|
+
|
|
33
|
+
## Cut the release
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git checkout main && git pull
|
|
37
|
+
git tag cli-v0.1.0 # must match __version__ exactly; the workflow gate rejects any other shape
|
|
38
|
+
git push origin cli-v0.1.0
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The workflow then, in order:
|
|
42
|
+
1. **Validates the tag** against `^cli-vN.N.N$` (fails closed on anything else).
|
|
43
|
+
2. **wheel** → builds + `twine upload --skip-existing` to PyPI; pre-creates the GitHub
|
|
44
|
+
Release on the public `finradar-cli` repo.
|
|
45
|
+
3. **binaries** (ubuntu/macos/windows) → builds each from the wheel, computes a
|
|
46
|
+
`*.sha256` sidecar, uploads binary + sidecar to the public Release.
|
|
47
|
+
4. **publish-managers** → patches the canonical Homebrew formula + Scoop manifest with
|
|
48
|
+
the version + per-OS sha256 and pushes them (a guard fails the run if any
|
|
49
|
+
`REPLACE_*` placeholder survives).
|
|
50
|
+
|
|
51
|
+
## Post-release verification
|
|
52
|
+
|
|
53
|
+
- [ ] `pip install finradar-cli==0.1.0` (fresh venv) → `finradar --version` correct.
|
|
54
|
+
- [ ] GitHub Release on `MarounAntoun/finradar-cli` has 3 binaries + 3 `.sha256` sidecars;
|
|
55
|
+
spot-check one: `sha256sum finradar-linux` matches `finradar-linux.sha256`.
|
|
56
|
+
- [ ] `brew install MarounAntoun/tap/finradar` → `finradar --version` correct.
|
|
57
|
+
- [ ] `scoop bucket add finradar https://github.com/MarounAntoun/scoop-finradar; scoop install finradar`.
|
|
58
|
+
- [ ] From a frozen binary: `finradar update --check` reports "up to date" (it now sees the release).
|
|
59
|
+
|
|
60
|
+
## Rollback
|
|
61
|
+
|
|
62
|
+
- A bad PyPI release cannot be overwritten — yank it on PyPI and ship `0.1.1`.
|
|
63
|
+
- A bad binary: delete the GitHub Release assets (or the Release) on `finradar-cli`;
|
|
64
|
+
the formula/manifest can be reverted via a follow-up tag. The self-updater is
|
|
65
|
+
fail-closed (it never installs an artifact whose sha256 does not match its sidecar).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# FinRadar CLI — Security Policy & v0.1.0 Posture
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Email security reports to **marounantoun97@gmail.com** with `[finradar-cli security]`
|
|
6
|
+
in the subject. Please do not open public issues for vulnerabilities. We aim to
|
|
7
|
+
acknowledge within 72 hours.
|
|
8
|
+
|
|
9
|
+
## Pre-release security review (v0.1.0)
|
|
10
|
+
|
|
11
|
+
Before the first public release the CLI, its release pipeline, and the additive
|
|
12
|
+
backend `cli_access` token underwent an independent, multi-dimension security review
|
|
13
|
+
(self-updater supply-chain, credential storage, OAuth/PKCE, release CI, backend
|
|
14
|
+
token, dependencies, input handling, transport). Verdict: **GO-WITH-FIXES**.
|
|
15
|
+
|
|
16
|
+
- **0 Critical.** The 3 High findings were downgraded to Medium/Low after adversarial
|
|
17
|
+
verification (none anonymous/external).
|
|
18
|
+
- The one release-gating item — a Windows self-updater path that could leave no
|
|
19
|
+
binary on a failed swap — was fixed (atomic rollback) and mutation-tested.
|
|
20
|
+
- The confirmed should-fix findings were all remediated before release: release-tag
|
|
21
|
+
command-injection gate, SHA-pinned GitHub Actions, owner-only credential-file ACLs
|
|
22
|
+
(Windows + POSIX), transient-safe OAuth token retry, OAuth metadata origin/https
|
|
23
|
+
pinning, idempotent-only HTTP retries, constant-time state comparison.
|
|
24
|
+
|
|
25
|
+
### Confirmed-correct controls (verified by the review)
|
|
26
|
+
|
|
27
|
+
- **Self-updater:** verify-before-replace (the binary is never swapped in unless its
|
|
28
|
+
sha256 matches the sidecar), fail-closed cleanup of unverified artifacts, TLS
|
|
29
|
+
verification on every fetch, strict 64-hex checksum parsing, frozen-only gating, and
|
|
30
|
+
fail-safe version comparison.
|
|
31
|
+
- **Backend `cli_access` token:** full RS256 verification (signature + exp + aud + iss),
|
|
32
|
+
HS256↔RS256 algorithm-confusion blocked, no JWKS/SSRF (in-process key), structural
|
|
33
|
+
audience separation from MCP, token-class confusion blocked both directions,
|
|
34
|
+
fail-closed when disabled — and it **ships OFF** (`CLI_ACCESS_ENABLED=false`).
|
|
35
|
+
- **Credential storage:** OS keychain preferred; secrets never written to `config.toml`
|
|
36
|
+
or logs; PKCE S256 with a CSPRNG verifier/state; loopback callback binds 127.0.0.1 only.
|
|
37
|
+
|
|
38
|
+
## Accepted risk for v0.1.0 (tracked for GA)
|
|
39
|
+
|
|
40
|
+
These are real and documented; they are acceptable for an initial `0.1.0` release and
|
|
41
|
+
are scheduled for hardening before a `1.0` GA:
|
|
42
|
+
|
|
43
|
+
1. **Update integrity == GitHub release integrity.** Binaries are protected by HTTPS +
|
|
44
|
+
a sha256 sidecar fetched from the same release. This defends against transport
|
|
45
|
+
corruption, **not** a compromised release account. Anyone who can publish a release
|
|
46
|
+
(a stolen publishing token, compromised CI, or account takeover) controls both the
|
|
47
|
+
binary and its checksum. **Mitigations in place now:** branch protection, the release
|
|
48
|
+
workflow runs only on a strictly-validated `cli-vN.N.N` tag, secrets are env-only,
|
|
49
|
+
third-party Actions are SHA-pinned. **GA hardening:** sign releases with
|
|
50
|
+
Sigstore/cosign (keyless OIDC) or minisign and verify the *signature* (not just the
|
|
51
|
+
hash) in the updater; add OS code-signing (Authenticode / notarization); require a
|
|
52
|
+
hardware-key-protected, reviewed publish.
|
|
53
|
+
2. **PyPI publishing uses a long-lived API token**, not OIDC Trusted Publishing.
|
|
54
|
+
*GA:* migrate to `pypa/gh-action-pypi-publish` with `id-token: write`.
|
|
55
|
+
3. **No hash-pinned dependency lock.** Runtime deps use bounded ranges; the binary /
|
|
56
|
+
brew / scoop channels ship a CI-frozen set. *GA:* add a `--generate-hashes` lock and
|
|
57
|
+
pin build tooling for reproducible builds.
|
|
58
|
+
4. **`cli_access` is full-access; the `cli` scope is not enforced at the resource
|
|
59
|
+
server.** Intentional for v1, and the feature **ships OFF**. **Hard gate:** before
|
|
60
|
+
`CLI_ACCESS_ENABLED` is ever turned on with anything narrower than a single
|
|
61
|
+
full-access `cli` scope, resource-server scope enforcement MUST land — otherwise a
|
|
62
|
+
future narrower-scope token would still reach write endpoints.
|
|
63
|
+
|
|
64
|
+
## Supply-chain notes for operators
|
|
65
|
+
|
|
66
|
+
- The official binaries and `*.sha256` sidecars are published only at
|
|
67
|
+
`https://github.com/MarounAntoun/finradar-cli/releases`. The Homebrew formula and
|
|
68
|
+
Scoop manifest reference those release assets.
|
|
69
|
+
- `finradar update` only self-updates a **frozen standalone binary**. pip/pipx
|
|
70
|
+
installs are never modified in place (the updater tells you to `pipx upgrade`).
|
|
71
|
+
- Two repository secrets gate publishing: `DIST_PUSH_TOKEN` (cross-repo `contents:write`)
|
|
72
|
+
and `PYPI_TOKEN`. Both are env-only in CI and never logged.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# PyInstaller bootstrap: absolute-import entry point for the one-file binary.
|
|
2
|
+
# The package __main__.py uses relative imports which PyInstaller cannot handle
|
|
3
|
+
# as a top-level script; this thin wrapper uses absolute imports instead.
|
|
4
|
+
from finradar_cli.app import app
|
|
5
|
+
|
|
6
|
+
if __name__ == "__main__":
|
|
7
|
+
app()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# PyInstaller one-file `finradar` binary: bundle package data + dynamic keyring backends.
|
|
2
|
+
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
|
3
|
+
|
|
4
|
+
datas = collect_data_files("finradar_cli",
|
|
5
|
+
includes=["data/*", "_generated/**/*", "marquee.toml", "type_overrides.toml"])
|
|
6
|
+
hidden = (collect_submodules("keyring.backends")
|
|
7
|
+
+ ["finradar_cli._generated"])
|
|
8
|
+
|
|
9
|
+
a = Analysis(["_finradar_main.py"], pathex=["."], datas=datas, hiddenimports=hidden,
|
|
10
|
+
hookspath=[], runtime_hooks=[], excludes=[], noarchive=False)
|
|
11
|
+
pyz = PYZ(a.pure, a.zipped_data)
|
|
12
|
+
exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [],
|
|
13
|
+
name="finradar", debug=False, strip=False, upx=True, console=True)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""GENERATED by generate.py — do not edit. Run `make generate`."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from . import cmd_13dg
|
|
7
|
+
from . import cmd_13f
|
|
8
|
+
from . import cmd_billing
|
|
9
|
+
from . import cmd_core
|
|
10
|
+
from . import cmd_cusip
|
|
11
|
+
from . import cmd_download
|
|
12
|
+
from . import cmd_facts
|
|
13
|
+
from . import cmd_financials
|
|
14
|
+
from . import cmd_holders
|
|
15
|
+
from . import cmd_index
|
|
16
|
+
from . import cmd_insider
|
|
17
|
+
from . import cmd_market
|
|
18
|
+
from . import cmd_ownership
|
|
19
|
+
from . import cmd_ownership_analytics
|
|
20
|
+
from . import cmd_positions
|
|
21
|
+
from . import cmd_pricing
|
|
22
|
+
from . import cmd_progress
|
|
23
|
+
from . import cmd_search
|
|
24
|
+
from . import cmd_sec
|
|
25
|
+
from . import cmd_status
|
|
26
|
+
from . import cmd_ticker
|
|
27
|
+
from . import cmd_user
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register(app: typer.Typer) -> None:
|
|
31
|
+
_g_13dg = typer.Typer(no_args_is_help=True)
|
|
32
|
+
app.add_typer(_g_13dg, name='13dg')
|
|
33
|
+
cmd_13dg.build(_g_13dg)
|
|
34
|
+
_g_13f = typer.Typer(no_args_is_help=True)
|
|
35
|
+
app.add_typer(_g_13f, name='13f')
|
|
36
|
+
cmd_13f.build(_g_13f)
|
|
37
|
+
_g_billing = typer.Typer(no_args_is_help=True)
|
|
38
|
+
app.add_typer(_g_billing, name='billing')
|
|
39
|
+
cmd_billing.build(_g_billing)
|
|
40
|
+
_g_core = typer.Typer(no_args_is_help=True)
|
|
41
|
+
app.add_typer(_g_core, name='core')
|
|
42
|
+
cmd_core.build(_g_core)
|
|
43
|
+
_g_cusip = typer.Typer(no_args_is_help=True)
|
|
44
|
+
app.add_typer(_g_cusip, name='cusip')
|
|
45
|
+
cmd_cusip.build(_g_cusip)
|
|
46
|
+
_g_download = typer.Typer(no_args_is_help=True)
|
|
47
|
+
app.add_typer(_g_download, name='download')
|
|
48
|
+
cmd_download.build(_g_download)
|
|
49
|
+
_g_facts = typer.Typer(no_args_is_help=True)
|
|
50
|
+
app.add_typer(_g_facts, name='facts')
|
|
51
|
+
cmd_facts.build(_g_facts)
|
|
52
|
+
_g_financials = typer.Typer(no_args_is_help=True)
|
|
53
|
+
app.add_typer(_g_financials, name='financials')
|
|
54
|
+
cmd_financials.build(_g_financials)
|
|
55
|
+
_g_holders = typer.Typer(no_args_is_help=True)
|
|
56
|
+
app.add_typer(_g_holders, name='holders')
|
|
57
|
+
cmd_holders.build(_g_holders)
|
|
58
|
+
_g_index = typer.Typer(no_args_is_help=True)
|
|
59
|
+
app.add_typer(_g_index, name='index')
|
|
60
|
+
cmd_index.build(_g_index)
|
|
61
|
+
_g_insider = typer.Typer(no_args_is_help=True)
|
|
62
|
+
app.add_typer(_g_insider, name='insider')
|
|
63
|
+
cmd_insider.build(_g_insider)
|
|
64
|
+
_g_market = typer.Typer(no_args_is_help=True)
|
|
65
|
+
app.add_typer(_g_market, name='market')
|
|
66
|
+
cmd_market.build(_g_market)
|
|
67
|
+
_g_ownership = typer.Typer(no_args_is_help=True)
|
|
68
|
+
app.add_typer(_g_ownership, name='ownership')
|
|
69
|
+
cmd_ownership.build(_g_ownership)
|
|
70
|
+
_g_ownership_analytics = typer.Typer(no_args_is_help=True)
|
|
71
|
+
app.add_typer(_g_ownership_analytics, name='ownership-analytics')
|
|
72
|
+
cmd_ownership_analytics.build(_g_ownership_analytics)
|
|
73
|
+
_g_positions = typer.Typer(no_args_is_help=True)
|
|
74
|
+
app.add_typer(_g_positions, name='positions')
|
|
75
|
+
cmd_positions.build(_g_positions)
|
|
76
|
+
_g_pricing = typer.Typer(no_args_is_help=True)
|
|
77
|
+
app.add_typer(_g_pricing, name='pricing')
|
|
78
|
+
cmd_pricing.build(_g_pricing)
|
|
79
|
+
_g_progress = typer.Typer(no_args_is_help=True)
|
|
80
|
+
app.add_typer(_g_progress, name='progress')
|
|
81
|
+
cmd_progress.build(_g_progress)
|
|
82
|
+
_g_search = typer.Typer(no_args_is_help=True)
|
|
83
|
+
app.add_typer(_g_search, name='search')
|
|
84
|
+
cmd_search.build(_g_search)
|
|
85
|
+
_g_sec = typer.Typer(no_args_is_help=True)
|
|
86
|
+
app.add_typer(_g_sec, name='sec')
|
|
87
|
+
cmd_sec.build(_g_sec)
|
|
88
|
+
_g_status = typer.Typer(no_args_is_help=True)
|
|
89
|
+
app.add_typer(_g_status, name='status')
|
|
90
|
+
cmd_status.build(_g_status)
|
|
91
|
+
_g_ticker = typer.Typer(no_args_is_help=True)
|
|
92
|
+
app.add_typer(_g_ticker, name='ticker')
|
|
93
|
+
cmd_ticker.build(_g_ticker)
|
|
94
|
+
_g_user = typer.Typer(no_args_is_help=True)
|
|
95
|
+
app.add_typer(_g_user, name='user')
|
|
96
|
+
cmd_user.build(_g_user)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""GENERATED by generate.py — do not edit. Run `make generate`."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from finradar_cli.commands.api import run_request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build(parent: typer.Typer) -> None:
|
|
9
|
+
_sub_form_13d = typer.Typer(no_args_is_help=True)
|
|
10
|
+
parent.add_typer(_sub_form_13d, name='form-13d')
|
|
11
|
+
|
|
12
|
+
@_sub_form_13d.command('create', help='Form 13D [cost 0]')
|
|
13
|
+
def cmd_post_api_v1_form_13d(ctx: typer.Context, query_ticker: str = typer.Option(None, '--query.ticker', help="Issuer trading symbol (e.g. 'TWTR'). Case-insensitive."), query_issuerCik: str = typer.Option(None, '--query.issuerCik', help='Issuer CIK number. Leading zeros are stripped automatically.'), query_filerCik: str = typer.Option(None, '--query.filerCik', help='Filer (reporting person) CIK. Leading zeros are stripped automatically.'), query_filerName: str = typer.Option(None, '--query.filerName', help="Partial match on filer name (case-insensitive). E.g. 'Elon Musk'."), query_minOwnership: str = typer.Option(None, '--query.minOwnership', help='Minimum percent of class owned. E.g. 5.0 for >= 5%.'), query_formType: str = typer.Option(None, '--query.formType', help="Filter by form type: 'SC 13D', 'SC 13D/A', 'SC 13G', 'SC 13G/A'. Both SC and SCHEDULE variants accepted."), query_dateRange_from: str = typer.Option(None, '--query.dateRange.from', help="Start date filter (ISO format, e.g. '2025-01-01')."), query_dateRange_to: str = typer.Option(None, '--query.dateRange.to', help="End date filter (ISO format, e.g. '2026-03-14')."), from_: str = typer.Option(None, '--from', help='Pagination offset.'), size: str = typer.Option(None, '--size', help='Page size (max 50).'), sort: str = typer.Option(None, '--sort', help='Sort specification. Supported fields: filedAt, percentOfClass, sharesOwned.'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
|
|
14
|
+
url = '/api/v1/form-13d'
|
|
15
|
+
_pp: dict[str, str] = {}
|
|
16
|
+
for _k, _v in _pp.items():
|
|
17
|
+
url = url.replace("{" + _k + "}", str(_v))
|
|
18
|
+
_kv: list[str] = []
|
|
19
|
+
_body: dict[str, str | None] = {'query.ticker': query_ticker, 'query.issuerCik': query_issuerCik, 'query.filerCik': query_filerCik, 'query.filerName': query_filerName, 'query.minOwnership': query_minOwnership, 'query.formType': query_formType, 'query.dateRange.from': query_dateRange_from, 'query.dateRange.to': query_dateRange_to, 'from': from_, 'size': size, 'sort': sort}
|
|
20
|
+
for _name, _val in _body.items():
|
|
21
|
+
if _val is not None:
|
|
22
|
+
_kv.append(f"{_name}={_val}")
|
|
23
|
+
run_request(ctx.obj, 'POST', url, _kv, data=data)
|