govinbox 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.
- govinbox-0.1.0/PKG-INFO +110 -0
- govinbox-0.1.0/README.md +101 -0
- govinbox-0.1.0/govinbox/__init__.py +8 -0
- govinbox-0.1.0/govinbox/__main__.py +6 -0
- govinbox-0.1.0/govinbox/auth.py +70 -0
- govinbox-0.1.0/govinbox/cli.py +566 -0
- govinbox-0.1.0/govinbox/client.py +151 -0
- govinbox-0.1.0/govinbox/output.py +51 -0
- govinbox-0.1.0/govinbox.egg-info/PKG-INFO +110 -0
- govinbox-0.1.0/govinbox.egg-info/SOURCES.txt +14 -0
- govinbox-0.1.0/govinbox.egg-info/dependency_links.txt +1 -0
- govinbox-0.1.0/govinbox.egg-info/entry_points.txt +2 -0
- govinbox-0.1.0/govinbox.egg-info/top_level.txt +1 -0
- govinbox-0.1.0/pyproject.toml +26 -0
- govinbox-0.1.0/setup.cfg +4 -0
- govinbox-0.1.0/tests/test_cli.py +716 -0
govinbox-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: govinbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GovInbox CLI: federal set-aside contract opportunities from your terminal or your AI agent
|
|
5
|
+
Author: Artifex Innovations LLC
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# govinbox CLI
|
|
11
|
+
|
|
12
|
+
Federal set-aside contract opportunities from your terminal. Or your AI agent's terminal.
|
|
13
|
+
|
|
14
|
+
The CLI is a thin wrapper over the versioned GovInbox REST API. All the logic lives on the server behind `/v1/`. This tool only builds requests and prints answers. It has zero dependencies beyond Python itself.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Coming soon: `pip install govinbox`. Until the first release, install straight from the repo:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install git+https://github.com/AUsernameForYou/setaside-radar.git#subdirectory=cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Python 3.10 or newer. That is the whole install. No API keys to configure by hand, no virtualenv dance.
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
govinbox login
|
|
30
|
+
# paste your API token (from your trial confirmation email or account page)
|
|
31
|
+
|
|
32
|
+
govinbox search --set-aside sdvosb --naics 5415 --limit 10
|
|
33
|
+
|
|
34
|
+
govinbox opportunity <notice-id>
|
|
35
|
+
|
|
36
|
+
govinbox digest
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
Every command takes `--json` for machine-readable output (agents, pipes, scripts). Without it you get readable tables.
|
|
42
|
+
|
|
43
|
+
| Command | What it does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `govinbox login` | Save your API token on this machine (stored at `~/.config/govinbox/credentials`, readable only by you). The token is verified against the server; if it fails, nothing is kept. |
|
|
46
|
+
| `govinbox logout` | Remove the saved token. |
|
|
47
|
+
| `govinbox whoami` | Check the saved token is valid and show how many watchlists you have. |
|
|
48
|
+
| `govinbox search` | Search opportunities. Filters: `--set-aside` (sdvosb, 8a, hubzone, wosb, veteran, indian, smallbiz), `--naics` (prefix like 5415), `--keyword`, `--agency` (e.g. "veterans affairs"), `--days` (default 7), `--limit` (default 25). |
|
|
49
|
+
| `govinbox opportunity <id>` | Full details for one opportunity, including the SAM.gov link and early signals. |
|
|
50
|
+
| `govinbox watchlists` | List your saved searches. |
|
|
51
|
+
| `govinbox watchlist <id>` | Show one saved search. |
|
|
52
|
+
| `govinbox digest [--watchlist <id>] [--days N]` | New matches for your watchlists, plain-English lines, newest first. |
|
|
53
|
+
| `govinbox version` | CLI version, API endpoint, and server version. |
|
|
54
|
+
|
|
55
|
+
Add `--debug` to any command to see full error details.
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# SDVOSB IT opportunities posted in the last 3 days
|
|
61
|
+
govinbox search --set-aside sdvosb --naics 5415 --days 3
|
|
62
|
+
|
|
63
|
+
# Pipe titles into another tool (this is the agent pattern)
|
|
64
|
+
govinbox search --keyword cybersecurity --json | jq -r '.items[].title'
|
|
65
|
+
|
|
66
|
+
# Everything due within the week, as JSON
|
|
67
|
+
govinbox search --limit 100 --json | jq '[.items[] | select(.days_left <= 7)]'
|
|
68
|
+
|
|
69
|
+
# This morning's digest for one watchlist
|
|
70
|
+
govinbox digest --watchlist abc123 --days 1
|
|
71
|
+
|
|
72
|
+
# Full detail plus raw data for scripting
|
|
73
|
+
govinbox opportunity abc123 --json > opp.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Auth
|
|
77
|
+
|
|
78
|
+
`govinbox login` prompts for your API token and saves it to `~/.config/govinbox/credentials` with owner-only permissions (0600). Nothing is ever printed back.
|
|
79
|
+
|
|
80
|
+
Two overrides, for CI and agents:
|
|
81
|
+
|
|
82
|
+
- `GOVINBOX_API_TOKEN` environment variable: used instead of the saved token.
|
|
83
|
+
- `GOVINBOX_API_URL` environment variable: point at a staging server instead of `https://api.getgovinbox.com`.
|
|
84
|
+
|
|
85
|
+
If your token is rejected you will see:
|
|
86
|
+
|
|
87
|
+
> Your API token was rejected. Run `govinbox login` to refresh your token.
|
|
88
|
+
|
|
89
|
+
That is always the fix. If it keeps failing, check that you pasted the full token.
|
|
90
|
+
|
|
91
|
+
## How versioning works
|
|
92
|
+
|
|
93
|
+
The CLI never contains business logic, so it rarely needs to change. The API is versioned (`/v1/`), and every request sends an `X-GovInbox-CLI-Version` header identifying the client. The server does not act on the header today; it exists so the server can warn outdated clients in the future. `govinbox version` compares your CLI against the server's reported version.
|
|
94
|
+
|
|
95
|
+
## Exit codes
|
|
96
|
+
|
|
97
|
+
Scripts can rely on these:
|
|
98
|
+
|
|
99
|
+
| Code | Meaning |
|
|
100
|
+
|---|---|
|
|
101
|
+
| 0 | Success |
|
|
102
|
+
| 1 | General error (bad arguments from the server, network failure, unexpected error) |
|
|
103
|
+
| 2 | Not logged in, or the token was rejected (`govinbox login` is the fix) |
|
|
104
|
+
| 3 | Subscription lapsed (HTTP 402) |
|
|
105
|
+
| 4 | Rate limited (HTTP 429; the message tells you when to retry) |
|
|
106
|
+
| 5 | Not found (HTTP 404) |
|
|
107
|
+
| 6 | Bad request (HTTP 400) |
|
|
108
|
+
| 130 | Cancelled with Ctrl-C |
|
|
109
|
+
|
|
110
|
+
Errors print one plain-English line to stderr and never a traceback, unless you pass `--debug`.
|
govinbox-0.1.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# govinbox CLI
|
|
2
|
+
|
|
3
|
+
Federal set-aside contract opportunities from your terminal. Or your AI agent's terminal.
|
|
4
|
+
|
|
5
|
+
The CLI is a thin wrapper over the versioned GovInbox REST API. All the logic lives on the server behind `/v1/`. This tool only builds requests and prints answers. It has zero dependencies beyond Python itself.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Coming soon: `pip install govinbox`. Until the first release, install straight from the repo:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install git+https://github.com/AUsernameForYou/setaside-radar.git#subdirectory=cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.10 or newer. That is the whole install. No API keys to configure by hand, no virtualenv dance.
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
govinbox login
|
|
21
|
+
# paste your API token (from your trial confirmation email or account page)
|
|
22
|
+
|
|
23
|
+
govinbox search --set-aside sdvosb --naics 5415 --limit 10
|
|
24
|
+
|
|
25
|
+
govinbox opportunity <notice-id>
|
|
26
|
+
|
|
27
|
+
govinbox digest
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
Every command takes `--json` for machine-readable output (agents, pipes, scripts). Without it you get readable tables.
|
|
33
|
+
|
|
34
|
+
| Command | What it does |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `govinbox login` | Save your API token on this machine (stored at `~/.config/govinbox/credentials`, readable only by you). The token is verified against the server; if it fails, nothing is kept. |
|
|
37
|
+
| `govinbox logout` | Remove the saved token. |
|
|
38
|
+
| `govinbox whoami` | Check the saved token is valid and show how many watchlists you have. |
|
|
39
|
+
| `govinbox search` | Search opportunities. Filters: `--set-aside` (sdvosb, 8a, hubzone, wosb, veteran, indian, smallbiz), `--naics` (prefix like 5415), `--keyword`, `--agency` (e.g. "veterans affairs"), `--days` (default 7), `--limit` (default 25). |
|
|
40
|
+
| `govinbox opportunity <id>` | Full details for one opportunity, including the SAM.gov link and early signals. |
|
|
41
|
+
| `govinbox watchlists` | List your saved searches. |
|
|
42
|
+
| `govinbox watchlist <id>` | Show one saved search. |
|
|
43
|
+
| `govinbox digest [--watchlist <id>] [--days N]` | New matches for your watchlists, plain-English lines, newest first. |
|
|
44
|
+
| `govinbox version` | CLI version, API endpoint, and server version. |
|
|
45
|
+
|
|
46
|
+
Add `--debug` to any command to see full error details.
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# SDVOSB IT opportunities posted in the last 3 days
|
|
52
|
+
govinbox search --set-aside sdvosb --naics 5415 --days 3
|
|
53
|
+
|
|
54
|
+
# Pipe titles into another tool (this is the agent pattern)
|
|
55
|
+
govinbox search --keyword cybersecurity --json | jq -r '.items[].title'
|
|
56
|
+
|
|
57
|
+
# Everything due within the week, as JSON
|
|
58
|
+
govinbox search --limit 100 --json | jq '[.items[] | select(.days_left <= 7)]'
|
|
59
|
+
|
|
60
|
+
# This morning's digest for one watchlist
|
|
61
|
+
govinbox digest --watchlist abc123 --days 1
|
|
62
|
+
|
|
63
|
+
# Full detail plus raw data for scripting
|
|
64
|
+
govinbox opportunity abc123 --json > opp.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Auth
|
|
68
|
+
|
|
69
|
+
`govinbox login` prompts for your API token and saves it to `~/.config/govinbox/credentials` with owner-only permissions (0600). Nothing is ever printed back.
|
|
70
|
+
|
|
71
|
+
Two overrides, for CI and agents:
|
|
72
|
+
|
|
73
|
+
- `GOVINBOX_API_TOKEN` environment variable: used instead of the saved token.
|
|
74
|
+
- `GOVINBOX_API_URL` environment variable: point at a staging server instead of `https://api.getgovinbox.com`.
|
|
75
|
+
|
|
76
|
+
If your token is rejected you will see:
|
|
77
|
+
|
|
78
|
+
> Your API token was rejected. Run `govinbox login` to refresh your token.
|
|
79
|
+
|
|
80
|
+
That is always the fix. If it keeps failing, check that you pasted the full token.
|
|
81
|
+
|
|
82
|
+
## How versioning works
|
|
83
|
+
|
|
84
|
+
The CLI never contains business logic, so it rarely needs to change. The API is versioned (`/v1/`), and every request sends an `X-GovInbox-CLI-Version` header identifying the client. The server does not act on the header today; it exists so the server can warn outdated clients in the future. `govinbox version` compares your CLI against the server's reported version.
|
|
85
|
+
|
|
86
|
+
## Exit codes
|
|
87
|
+
|
|
88
|
+
Scripts can rely on these:
|
|
89
|
+
|
|
90
|
+
| Code | Meaning |
|
|
91
|
+
|---|---|
|
|
92
|
+
| 0 | Success |
|
|
93
|
+
| 1 | General error (bad arguments from the server, network failure, unexpected error) |
|
|
94
|
+
| 2 | Not logged in, or the token was rejected (`govinbox login` is the fix) |
|
|
95
|
+
| 3 | Subscription lapsed (HTTP 402) |
|
|
96
|
+
| 4 | Rate limited (HTTP 429; the message tells you when to retry) |
|
|
97
|
+
| 5 | Not found (HTTP 404) |
|
|
98
|
+
| 6 | Bad request (HTTP 400) |
|
|
99
|
+
| 130 | Cancelled with Ctrl-C |
|
|
100
|
+
|
|
101
|
+
Errors print one plain-English line to stderr and never a traceback, unless you pass `--debug`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""GovInbox public CLI: thin wrapper over the versioned GovInbox REST API.
|
|
2
|
+
|
|
3
|
+
All business logic lives server-side behind /v1/. This package only builds
|
|
4
|
+
requests, sends them with the caller's API token, and pretty-prints responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Token storage for the GovInbox CLI.
|
|
2
|
+
|
|
3
|
+
The token lives in ~/.config/govinbox/credentials (mode 0600). A
|
|
4
|
+
GOVINBOX_API_TOKEN environment variable overrides the file, which is handy
|
|
5
|
+
for CI and for agents that inject secrets via the environment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "govinbox"
|
|
16
|
+
CREDENTIALS_FILE = CONFIG_DIR / "credentials"
|
|
17
|
+
ENV_TOKEN = "GOVINBOX_API_TOKEN"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_token() -> str | None:
|
|
21
|
+
"""Return the stored token, or None when the user has not logged in."""
|
|
22
|
+
env = os.environ.get(ENV_TOKEN)
|
|
23
|
+
if env:
|
|
24
|
+
return env.strip() or None
|
|
25
|
+
try:
|
|
26
|
+
data = json.loads(CREDENTIALS_FILE.read_text())
|
|
27
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
28
|
+
return None
|
|
29
|
+
token = data.get("token")
|
|
30
|
+
return token.strip() if token else None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def token_source() -> str:
|
|
34
|
+
"""Where the current token came from, for user-facing messages."""
|
|
35
|
+
if os.environ.get(ENV_TOKEN):
|
|
36
|
+
return f"environment variable {ENV_TOKEN}"
|
|
37
|
+
return str(CREDENTIALS_FILE)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def save_token(token: str) -> Path:
|
|
41
|
+
"""Persist the token with owner-only permissions. Returns the file path.
|
|
42
|
+
|
|
43
|
+
Uses os.open with mode 0600 so the file is never briefly world-readable
|
|
44
|
+
between creation and chmod.
|
|
45
|
+
"""
|
|
46
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
fd = os.open(CREDENTIALS_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
48
|
+
try:
|
|
49
|
+
os.write(fd, (json.dumps({"token": token.strip()}) + "\n").encode("utf-8"))
|
|
50
|
+
finally:
|
|
51
|
+
os.close(fd)
|
|
52
|
+
# Belt and suspenders: enforce 0600 even if the file already existed with
|
|
53
|
+
# wider permissions.
|
|
54
|
+
os.chmod(CREDENTIALS_FILE, stat.S_IRUSR | stat.S_IWUSR)
|
|
55
|
+
return CREDENTIALS_FILE
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def clear_token() -> bool:
|
|
59
|
+
"""Remove the stored token. True when something was removed."""
|
|
60
|
+
try:
|
|
61
|
+
CREDENTIALS_FILE.unlink()
|
|
62
|
+
return True
|
|
63
|
+
except FileNotFoundError:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def mask(token: str) -> str:
|
|
68
|
+
"""Last four characters, for confirming which token is in use."""
|
|
69
|
+
t = token.strip()
|
|
70
|
+
return "…" + t[-4:] if len(t) > 4 else "…"
|