insightfactory-cli 1.0.2.dev12__tar.gz → 1.0.2.dev16__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.
- insightfactory_cli-1.0.2.dev16/LICENSE +5 -0
- insightfactory_cli-1.0.2.dev16/PKG-INFO +314 -0
- insightfactory_cli-1.0.2.dev16/README.md +296 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/pyproject.toml +5 -3
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/http.py +5 -1
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/oauth.py +3 -2
- insightfactory_cli-1.0.2.dev12/PKG-INFO +0 -310
- insightfactory_cli-1.0.2.dev12/README.md +0 -294
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/.github/workflows/ci.yml +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/.github/workflows/claude.yml +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/.github/workflows/release.yml +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/.gitignore +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/.python-version +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/AGENTS.md +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/CLAUDE.md +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/__init__.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/__main__.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/cache.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/cli.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/colour.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/commands/__init__.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/commands/api.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/commands/config.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/commands/login.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/commands/logout.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/commands/profiles.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/commands/set_token.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/commands/token.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/config.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/constants.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/main.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/src/if_cli/runtime.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/__init__.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/cache_writer.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/conftest.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/helpers.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/servers.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_api.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_api_command.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_cache.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_cli.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_config.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_config_command.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_login.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_oauth.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_oauth_flow.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_profiles.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_programmatic_api.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_runtime.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_set_token.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/tests/test_token.py +0 -0
- {insightfactory_cli-1.0.2.dev12 → insightfactory_cli-1.0.2.dev16}/uv.lock +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: insightfactory-cli
|
|
3
|
+
Version: 1.0.2.dev16
|
|
4
|
+
Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
|
|
5
|
+
Project-URL: Homepage, https://insightfactory.ai
|
|
6
|
+
Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
|
|
7
|
+
License-Expression: LicenseRef-Proprietary
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: cli,insightfactory,oauth,pkce
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: Other/Proprietary License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# insightfactory-cli
|
|
20
|
+
|
|
21
|
+
Profile-based authentication CLI for the InsightFactory Interfaces API. It
|
|
22
|
+
installs the `if-cli` command, which logs in through the browser with OAuth
|
|
23
|
+
authorization code and PKCE, refreshes tokens, and makes authenticated API
|
|
24
|
+
calls. Profiles live in `~/.insightfactory` in the same layout the Node
|
|
25
|
+
`@insightfactory-ai/if-cli` uses, so both implementations can share one config
|
|
26
|
+
directory.
|
|
27
|
+
|
|
28
|
+
This is the production Python CLI, published to public PyPI. It needs Python
|
|
29
|
+
3.10 or newer.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install insightfactory-cli # persistent install, on PATH
|
|
35
|
+
uvx insightfactory-cli profiles # or zero-install, run-once
|
|
36
|
+
uv tool upgrade insightfactory-cli # updates
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`uv` fetches a managed Python if the machine lacks one. Fallbacks:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pipx install insightfactory-cli
|
|
43
|
+
pip install insightfactory-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Develop-branch builds are published as PEP 440 `.devN` pre-releases. Install
|
|
47
|
+
the latest one with:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install --pre insightfactory-cli
|
|
51
|
+
uv tool install --prerelease allow insightfactory-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
PEP 440 sorts `1.0.0.devN` before `1.0.0`, so a dependency spec of
|
|
55
|
+
`insightfactory-cli>=1.0.0` does not match a develop-channel dev build. An
|
|
56
|
+
in-process consumer that wants those builds needs a spec that admits
|
|
57
|
+
pre-releases, such as `insightfactory-cli>=1.0.0.dev0`. That spec matches both
|
|
58
|
+
the `.devN` builds and the final `1.0.0` release.
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Create a profile and authenticate through the browser.
|
|
64
|
+
if-cli login -p example-dev --host https://factory.example
|
|
65
|
+
|
|
66
|
+
# Inspect profiles and cached-token status. Statuses are colour-coded on a TTY
|
|
67
|
+
# (green valid, yellow refreshable-but-expired, red missing or broken); set
|
|
68
|
+
# NO_COLOR=1 to disable or FORCE_COLOR=1 to keep colour when piping.
|
|
69
|
+
if-cli profiles
|
|
70
|
+
if-cli profiles --json
|
|
71
|
+
|
|
72
|
+
# Read a single non-secret profile value for scripting.
|
|
73
|
+
if-cli config get host -p example-dev
|
|
74
|
+
|
|
75
|
+
# Print a valid token, refreshing it when a refresh token is available.
|
|
76
|
+
if-cli token -p example-dev
|
|
77
|
+
if-cli token -p example-dev --env
|
|
78
|
+
if-cli token -p example-dev --env-name INSIGHTFACTORY_ACCESS_TOKEN_DEV
|
|
79
|
+
|
|
80
|
+
# Make an authenticated API request.
|
|
81
|
+
if-cli api -p example-dev /api/agent-projects
|
|
82
|
+
if-cli api -p example-dev -X POST -d '{"name":"example"}' /api/example
|
|
83
|
+
|
|
84
|
+
# Wait longer than the 30-second default for a long-running endpoint.
|
|
85
|
+
if-cli api -p example-dev --timeout 300 -X PUT -d '{"productionLineCodes":["PL001"]}' \
|
|
86
|
+
/api/orchestration/run
|
|
87
|
+
|
|
88
|
+
# Discover API routes from the factory's OpenAPI document.
|
|
89
|
+
if-cli api routes -p example-dev
|
|
90
|
+
if-cli api routes -p example-dev agent-projects
|
|
91
|
+
if-cli api describe -p example-dev GET /api/agent-projects/{id}
|
|
92
|
+
|
|
93
|
+
# Remove the cached credential for a profile.
|
|
94
|
+
if-cli logout -p example-dev
|
|
95
|
+
|
|
96
|
+
# Fallback for factories where browser OAuth is not available.
|
|
97
|
+
if-cli set-token -p example-dev
|
|
98
|
+
|
|
99
|
+
if-cli --version
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Profile selection tries `-p` first, then `INSIGHTFACTORY_CONFIG_PROFILE`, then
|
|
103
|
+
`[DEFAULT]`. Route discovery reads `{host}/swagger/v1/swagger.json` and does
|
|
104
|
+
not need a login. Authenticated requests only go to the selected factory's
|
|
105
|
+
origin, so a profile token cannot be forwarded to another host. A profile host
|
|
106
|
+
must be a bare origin, not a URL with an application path prefix.
|
|
107
|
+
|
|
108
|
+
Requests time out after 30 seconds by default. Raise the deadline for one
|
|
109
|
+
invocation with `--timeout <seconds>`, or for a whole session with
|
|
110
|
+
`INSIGHTFACTORY_REQUEST_TIMEOUT`. The flag wins over the environment variable,
|
|
111
|
+
and the CLI validates both before it touches the network. There is no upper
|
|
112
|
+
bound, because the right ceiling depends on the endpoint.
|
|
113
|
+
|
|
114
|
+
Both settings apply to the API request itself. OAuth discovery and token
|
|
115
|
+
refresh keep the 30-second default, so `if-cli token` and in-process callers of
|
|
116
|
+
`if_cli.oauth.get_valid_token` always wait a bounded time that no ambient
|
|
117
|
+
environment value can change.
|
|
118
|
+
|
|
119
|
+
A timeout is a client-side deadline, not a rejection. The factory may have
|
|
120
|
+
accepted and completed the request after `if-cli` gave up, so re-issuing a
|
|
121
|
+
`POST`, `PUT`, `PATCH`, or `DELETE` can apply it twice. For those methods the
|
|
122
|
+
timeout message tells you to confirm the current state before retrying rather
|
|
123
|
+
than repeat the call.
|
|
124
|
+
|
|
125
|
+
Override the config directory with `INSIGHTFACTORY_CONFIG_DIR`, which is useful
|
|
126
|
+
in tests and CI. The default is `~/.insightfactory`.
|
|
127
|
+
|
|
128
|
+
The request method goes in `-X`. `if-cli api` takes exactly one positional
|
|
129
|
+
argument and reads it as the path, so the `curl`-shaped
|
|
130
|
+
`if-cli api GET /api/schedules` parses `GET` as the path. Write
|
|
131
|
+
`if-cli api -X GET /api/schedules` instead. The error names the misplaced verb
|
|
132
|
+
rather than reporting a problem with `/api/schedules`.
|
|
133
|
+
|
|
134
|
+
On Git Bash (MSYS2) on Windows, path conversion rewrites a leading-slash
|
|
135
|
+
argument such as `/api/agent-projects` into a Windows path. Prefix the command
|
|
136
|
+
with `MSYS_NO_PATHCONV=1`:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
MSYS_NO_PATHCONV=1 if-cli api -p example-dev /api/agent-projects
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Scripting against profiles
|
|
143
|
+
|
|
144
|
+
`profiles --json` and `config get` are the supported machine-readable
|
|
145
|
+
interfaces. The padded `profiles` columns are for humans and are not a
|
|
146
|
+
contract, and neither is the layout of `~/.insightfactory/config`.
|
|
147
|
+
|
|
148
|
+
`if-cli profiles --json` prints an array with one entry per configured profile,
|
|
149
|
+
in file order, each holding `name`, `host`, and `token`:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
[
|
|
153
|
+
{
|
|
154
|
+
"name": "example-dev",
|
|
155
|
+
"host": "https://factory.example",
|
|
156
|
+
"token": { "status": "valid", "expires_at": 1785918336, "refreshable": true }
|
|
157
|
+
},
|
|
158
|
+
{ "name": "other", "host": "https://other.factory.example", "token": { "status": "none" } }
|
|
159
|
+
]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`token.status` is `valid`, `expired`, `none`, or `unknown`. `expires_at` and
|
|
163
|
+
`refreshable` appear only when a cached token exists. `valid` means
|
|
164
|
+
`if-cli token` will hand back the cached token as-is. A token inside the
|
|
165
|
+
60-second slack window that command refreshes within is therefore reported as
|
|
166
|
+
`expired`, so read the raw `expires_at` if you need the literal expiry instant.
|
|
167
|
+
A profile that cannot be resolved has a `host` of `null`, or its raw configured
|
|
168
|
+
value, plus a top-level `error` describing the problem. It does not stop the
|
|
169
|
+
other profiles from being listed. `token.error` is set only when the token
|
|
170
|
+
cache itself could not be read.
|
|
171
|
+
|
|
172
|
+
`if-cli config get <key> -p <profile>` prints one newline-terminated value and
|
|
173
|
+
exits non-zero if the profile cannot be resolved or the key is not configured
|
|
174
|
+
in it. A profile with no host cannot be resolved at all, so every key on it
|
|
175
|
+
fails, not just `host`. A key that is present but empty, such as `client_id =`,
|
|
176
|
+
counts as not configured:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
curl -H "Authorization: Bearer $(if-cli token -p example-dev)" \
|
|
180
|
+
"$(if-cli config get host -p example-dev)/api/agent-projects"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The readable keys are `host`, `audience`, `callback_port`, `client_id`, and
|
|
184
|
+
`organization`. All are non-secret and resolved the same way the CLI resolves
|
|
185
|
+
them, so `audience` and `callback_port` return their defaults when the profile
|
|
186
|
+
omits them. Tokens are deliberately not readable this way. Use `if-cli token`.
|
|
187
|
+
|
|
188
|
+
## Programmatic API
|
|
189
|
+
|
|
190
|
+
You can import `insightfactory-cli` in-process to resolve a profile to a
|
|
191
|
+
factory host and a fresh access token. These four symbols are supported for
|
|
192
|
+
in-process consumers:
|
|
193
|
+
|
|
194
|
+
- `if_cli.config.load_config`
|
|
195
|
+
- `if_cli.config.get_profile`
|
|
196
|
+
- `if_cli.oauth.get_valid_token`
|
|
197
|
+
- `if_cli.runtime.CliError`
|
|
198
|
+
|
|
199
|
+
They raise `CliError` on failure, never `SystemExit` or `sys.exit`. They never
|
|
200
|
+
write to stdout, never launch a browser or block on interactive input, and only
|
|
201
|
+
use bounded waits. Changing or removing any of them is a breaking change and
|
|
202
|
+
needs a major version bump.
|
|
203
|
+
|
|
204
|
+
## How authentication works
|
|
205
|
+
|
|
206
|
+
- Profiles live in `~/.insightfactory/config`, one per customer and environment.
|
|
207
|
+
- The CLI manages the profile file and regenerates it on updates. Comments and
|
|
208
|
+
empty sections are not preserved.
|
|
209
|
+
- Tokens are cached by host in `~/.insightfactory/token-cache.json` with file
|
|
210
|
+
mode `0600`.
|
|
211
|
+
- The configuration directory is `0700`. Configuration and cache updates are
|
|
212
|
+
atomic.
|
|
213
|
+
- Remote factory, authorization, and token endpoints must use HTTPS. HTTP is
|
|
214
|
+
accepted only for loopback development.
|
|
215
|
+
- Login discovers OAuth metadata from
|
|
216
|
+
`{host}/.well-known/oauth-authorization-server`.
|
|
217
|
+
- Authorization uses PKCE through the factory `/authorize` proxy with a
|
|
218
|
+
loopback-only `127.0.0.1` callback. The CLI is a public OAuth client. It reads
|
|
219
|
+
the client ID from the factory's discovery document at runtime and holds no
|
|
220
|
+
client secret.
|
|
221
|
+
|
|
222
|
+
## Development
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
uv sync
|
|
226
|
+
uv run pytest
|
|
227
|
+
uv run ruff check .
|
|
228
|
+
uv run ty check
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Smoke-test against a throwaway config directory, never `~/.insightfactory`:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
uv run if-cli --help
|
|
235
|
+
uv run if-cli --version
|
|
236
|
+
INSIGHTFACTORY_CONFIG_DIR=$(mktemp -d) uv run if-cli profiles
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Python 3.10 or newer is required. Runtime code is stdlib-only.
|
|
240
|
+
|
|
241
|
+
## Release
|
|
242
|
+
|
|
243
|
+
A release is cut by pushing a tag, not by merging. Merging to `main` runs CI
|
|
244
|
+
and publishes nothing.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
git tag v1.0.1 && git push origin v1.0.1
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Verify and build live in the reusable workflow
|
|
251
|
+
[`if_s_insightfactory_cli.yml`](https://github.com/insightfactory-ai/if_sre_github_actions/blob/main/.github/workflows/if_s_insightfactory_cli.yml)
|
|
252
|
+
in `insightfactory-ai/if_sre_github_actions`. This repository's
|
|
253
|
+
`.github/workflows/release.yml` keeps only the OIDC publish job, because PyPI
|
|
254
|
+
Trusted Publishing is registered for this repo, workflow `release.yml`, and
|
|
255
|
+
environment `pypi`. A reusable workflow cannot be registered.
|
|
256
|
+
|
|
257
|
+
The PyPI project needs a one-time trusted-publisher registration for that
|
|
258
|
+
triple. The publish job downloads the `dist-release` artifact staged by the
|
|
259
|
+
reusable workflow and uploads it with `pypa/gh-action-pypi-publish`.
|
|
260
|
+
|
|
261
|
+
| Trigger | Published version | PyPI role |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| Push / merge to `develop` | `1.0.1.dev{N}` | pre-release (`pip install --pre`) |
|
|
264
|
+
| Push of a `v*` tag | the version in `pyproject.toml`, which the tag must match | latest |
|
|
265
|
+
| Merge to `main` | nothing, CI only | none |
|
|
266
|
+
|
|
267
|
+
`N` is `github.run_number` of the reusable workflow. It is a per-workflow
|
|
268
|
+
integer that goes up on every run and stays the same across re-runs of that
|
|
269
|
+
run, so a failed publish can be retried under the same version. It does not
|
|
270
|
+
depend on git history or clone depth. After a release, bump `version` in
|
|
271
|
+
`pyproject.toml` on `develop` so later `.devN` builds sort after what just
|
|
272
|
+
landed. PEP 440 puts `1.0.1.devN` before `1.0.1`, which is also why a spec of
|
|
273
|
+
`insightfactory-cli>=1.0.0` skips develop-channel builds and an in-process
|
|
274
|
+
consumer that wants them must ask for `insightfactory-cli>=1.0.0.dev0`.
|
|
275
|
+
|
|
276
|
+
A `v*` tag whose name does not match `pyproject.toml` fails before publishing.
|
|
277
|
+
|
|
278
|
+
The two channels treat an already-published version differently, on purpose:
|
|
279
|
+
|
|
280
|
+
- **Release channel, a tag.** Publishing fails and names the fix. A tag is a
|
|
281
|
+
request to release that version. If it cannot be honoured, the run must say
|
|
282
|
+
so rather than report success having published nothing. Git also refuses to
|
|
283
|
+
push a tag that already exists, so this is hard to reach.
|
|
284
|
+
- **Dev channel, `develop`.** Publishing is skipped quietly. `.dev{N}` uses
|
|
285
|
+
`github.run_number`, which is stable across re-runs of one run, so an
|
|
286
|
+
already-present version there can only mean a re-run of a run that already
|
|
287
|
+
published. That is idempotency, not a swallowed mistake.
|
|
288
|
+
|
|
289
|
+
This split is why releasing moved off `main`. While a merge was the trigger, an
|
|
290
|
+
already-published version had to be tolerated as a no-op for merges to stay
|
|
291
|
+
green. A merge that released nothing then looked the same as one that
|
|
292
|
+
released, and a forgotten version bump shipped nothing, silently.
|
|
293
|
+
|
|
294
|
+
## Differences from the Node CLI
|
|
295
|
+
|
|
296
|
+
Behaviour matches `@insightfactory-ai/if-cli` except where the platforms
|
|
297
|
+
differ:
|
|
298
|
+
|
|
299
|
+
- **Unknown-option wording** is aligned to `Unknown option '--flag'`. Other
|
|
300
|
+
argparse messages, such as a missing option value, still differ from Node's
|
|
301
|
+
`util.parseArgs`.
|
|
302
|
+
- **Expiry timestamps** in human-readable output use the C-locale `%c` format
|
|
303
|
+
rather than JavaScript `Date#toLocaleString()`.
|
|
304
|
+
- **`--version`** reads the installed package metadata from `1.0.0` onward
|
|
305
|
+
rather than an npm `package.json`.
|
|
306
|
+
|
|
307
|
+
On-disk profile and token-cache paths, JSON shapes, and file permissions stay
|
|
308
|
+
compatible with the Node CLI, so you can switch implementations without
|
|
309
|
+
re-authenticating.
|
|
310
|
+
|
|
311
|
+
## Licence
|
|
312
|
+
|
|
313
|
+
Copyright 2026 insightfactory.ai. All rights reserved. This package is proprietary
|
|
314
|
+
and may be used only under a separate written agreement with insightfactory.ai.
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# insightfactory-cli
|
|
2
|
+
|
|
3
|
+
Profile-based authentication CLI for the InsightFactory Interfaces API. It
|
|
4
|
+
installs the `if-cli` command, which logs in through the browser with OAuth
|
|
5
|
+
authorization code and PKCE, refreshes tokens, and makes authenticated API
|
|
6
|
+
calls. Profiles live in `~/.insightfactory` in the same layout the Node
|
|
7
|
+
`@insightfactory-ai/if-cli` uses, so both implementations can share one config
|
|
8
|
+
directory.
|
|
9
|
+
|
|
10
|
+
This is the production Python CLI, published to public PyPI. It needs Python
|
|
11
|
+
3.10 or newer.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv tool install insightfactory-cli # persistent install, on PATH
|
|
17
|
+
uvx insightfactory-cli profiles # or zero-install, run-once
|
|
18
|
+
uv tool upgrade insightfactory-cli # updates
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`uv` fetches a managed Python if the machine lacks one. Fallbacks:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pipx install insightfactory-cli
|
|
25
|
+
pip install insightfactory-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Develop-branch builds are published as PEP 440 `.devN` pre-releases. Install
|
|
29
|
+
the latest one with:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install --pre insightfactory-cli
|
|
33
|
+
uv tool install --prerelease allow insightfactory-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
PEP 440 sorts `1.0.0.devN` before `1.0.0`, so a dependency spec of
|
|
37
|
+
`insightfactory-cli>=1.0.0` does not match a develop-channel dev build. An
|
|
38
|
+
in-process consumer that wants those builds needs a spec that admits
|
|
39
|
+
pre-releases, such as `insightfactory-cli>=1.0.0.dev0`. That spec matches both
|
|
40
|
+
the `.devN` builds and the final `1.0.0` release.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Create a profile and authenticate through the browser.
|
|
46
|
+
if-cli login -p example-dev --host https://factory.example
|
|
47
|
+
|
|
48
|
+
# Inspect profiles and cached-token status. Statuses are colour-coded on a TTY
|
|
49
|
+
# (green valid, yellow refreshable-but-expired, red missing or broken); set
|
|
50
|
+
# NO_COLOR=1 to disable or FORCE_COLOR=1 to keep colour when piping.
|
|
51
|
+
if-cli profiles
|
|
52
|
+
if-cli profiles --json
|
|
53
|
+
|
|
54
|
+
# Read a single non-secret profile value for scripting.
|
|
55
|
+
if-cli config get host -p example-dev
|
|
56
|
+
|
|
57
|
+
# Print a valid token, refreshing it when a refresh token is available.
|
|
58
|
+
if-cli token -p example-dev
|
|
59
|
+
if-cli token -p example-dev --env
|
|
60
|
+
if-cli token -p example-dev --env-name INSIGHTFACTORY_ACCESS_TOKEN_DEV
|
|
61
|
+
|
|
62
|
+
# Make an authenticated API request.
|
|
63
|
+
if-cli api -p example-dev /api/agent-projects
|
|
64
|
+
if-cli api -p example-dev -X POST -d '{"name":"example"}' /api/example
|
|
65
|
+
|
|
66
|
+
# Wait longer than the 30-second default for a long-running endpoint.
|
|
67
|
+
if-cli api -p example-dev --timeout 300 -X PUT -d '{"productionLineCodes":["PL001"]}' \
|
|
68
|
+
/api/orchestration/run
|
|
69
|
+
|
|
70
|
+
# Discover API routes from the factory's OpenAPI document.
|
|
71
|
+
if-cli api routes -p example-dev
|
|
72
|
+
if-cli api routes -p example-dev agent-projects
|
|
73
|
+
if-cli api describe -p example-dev GET /api/agent-projects/{id}
|
|
74
|
+
|
|
75
|
+
# Remove the cached credential for a profile.
|
|
76
|
+
if-cli logout -p example-dev
|
|
77
|
+
|
|
78
|
+
# Fallback for factories where browser OAuth is not available.
|
|
79
|
+
if-cli set-token -p example-dev
|
|
80
|
+
|
|
81
|
+
if-cli --version
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Profile selection tries `-p` first, then `INSIGHTFACTORY_CONFIG_PROFILE`, then
|
|
85
|
+
`[DEFAULT]`. Route discovery reads `{host}/swagger/v1/swagger.json` and does
|
|
86
|
+
not need a login. Authenticated requests only go to the selected factory's
|
|
87
|
+
origin, so a profile token cannot be forwarded to another host. A profile host
|
|
88
|
+
must be a bare origin, not a URL with an application path prefix.
|
|
89
|
+
|
|
90
|
+
Requests time out after 30 seconds by default. Raise the deadline for one
|
|
91
|
+
invocation with `--timeout <seconds>`, or for a whole session with
|
|
92
|
+
`INSIGHTFACTORY_REQUEST_TIMEOUT`. The flag wins over the environment variable,
|
|
93
|
+
and the CLI validates both before it touches the network. There is no upper
|
|
94
|
+
bound, because the right ceiling depends on the endpoint.
|
|
95
|
+
|
|
96
|
+
Both settings apply to the API request itself. OAuth discovery and token
|
|
97
|
+
refresh keep the 30-second default, so `if-cli token` and in-process callers of
|
|
98
|
+
`if_cli.oauth.get_valid_token` always wait a bounded time that no ambient
|
|
99
|
+
environment value can change.
|
|
100
|
+
|
|
101
|
+
A timeout is a client-side deadline, not a rejection. The factory may have
|
|
102
|
+
accepted and completed the request after `if-cli` gave up, so re-issuing a
|
|
103
|
+
`POST`, `PUT`, `PATCH`, or `DELETE` can apply it twice. For those methods the
|
|
104
|
+
timeout message tells you to confirm the current state before retrying rather
|
|
105
|
+
than repeat the call.
|
|
106
|
+
|
|
107
|
+
Override the config directory with `INSIGHTFACTORY_CONFIG_DIR`, which is useful
|
|
108
|
+
in tests and CI. The default is `~/.insightfactory`.
|
|
109
|
+
|
|
110
|
+
The request method goes in `-X`. `if-cli api` takes exactly one positional
|
|
111
|
+
argument and reads it as the path, so the `curl`-shaped
|
|
112
|
+
`if-cli api GET /api/schedules` parses `GET` as the path. Write
|
|
113
|
+
`if-cli api -X GET /api/schedules` instead. The error names the misplaced verb
|
|
114
|
+
rather than reporting a problem with `/api/schedules`.
|
|
115
|
+
|
|
116
|
+
On Git Bash (MSYS2) on Windows, path conversion rewrites a leading-slash
|
|
117
|
+
argument such as `/api/agent-projects` into a Windows path. Prefix the command
|
|
118
|
+
with `MSYS_NO_PATHCONV=1`:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
MSYS_NO_PATHCONV=1 if-cli api -p example-dev /api/agent-projects
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Scripting against profiles
|
|
125
|
+
|
|
126
|
+
`profiles --json` and `config get` are the supported machine-readable
|
|
127
|
+
interfaces. The padded `profiles` columns are for humans and are not a
|
|
128
|
+
contract, and neither is the layout of `~/.insightfactory/config`.
|
|
129
|
+
|
|
130
|
+
`if-cli profiles --json` prints an array with one entry per configured profile,
|
|
131
|
+
in file order, each holding `name`, `host`, and `token`:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
[
|
|
135
|
+
{
|
|
136
|
+
"name": "example-dev",
|
|
137
|
+
"host": "https://factory.example",
|
|
138
|
+
"token": { "status": "valid", "expires_at": 1785918336, "refreshable": true }
|
|
139
|
+
},
|
|
140
|
+
{ "name": "other", "host": "https://other.factory.example", "token": { "status": "none" } }
|
|
141
|
+
]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`token.status` is `valid`, `expired`, `none`, or `unknown`. `expires_at` and
|
|
145
|
+
`refreshable` appear only when a cached token exists. `valid` means
|
|
146
|
+
`if-cli token` will hand back the cached token as-is. A token inside the
|
|
147
|
+
60-second slack window that command refreshes within is therefore reported as
|
|
148
|
+
`expired`, so read the raw `expires_at` if you need the literal expiry instant.
|
|
149
|
+
A profile that cannot be resolved has a `host` of `null`, or its raw configured
|
|
150
|
+
value, plus a top-level `error` describing the problem. It does not stop the
|
|
151
|
+
other profiles from being listed. `token.error` is set only when the token
|
|
152
|
+
cache itself could not be read.
|
|
153
|
+
|
|
154
|
+
`if-cli config get <key> -p <profile>` prints one newline-terminated value and
|
|
155
|
+
exits non-zero if the profile cannot be resolved or the key is not configured
|
|
156
|
+
in it. A profile with no host cannot be resolved at all, so every key on it
|
|
157
|
+
fails, not just `host`. A key that is present but empty, such as `client_id =`,
|
|
158
|
+
counts as not configured:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
curl -H "Authorization: Bearer $(if-cli token -p example-dev)" \
|
|
162
|
+
"$(if-cli config get host -p example-dev)/api/agent-projects"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The readable keys are `host`, `audience`, `callback_port`, `client_id`, and
|
|
166
|
+
`organization`. All are non-secret and resolved the same way the CLI resolves
|
|
167
|
+
them, so `audience` and `callback_port` return their defaults when the profile
|
|
168
|
+
omits them. Tokens are deliberately not readable this way. Use `if-cli token`.
|
|
169
|
+
|
|
170
|
+
## Programmatic API
|
|
171
|
+
|
|
172
|
+
You can import `insightfactory-cli` in-process to resolve a profile to a
|
|
173
|
+
factory host and a fresh access token. These four symbols are supported for
|
|
174
|
+
in-process consumers:
|
|
175
|
+
|
|
176
|
+
- `if_cli.config.load_config`
|
|
177
|
+
- `if_cli.config.get_profile`
|
|
178
|
+
- `if_cli.oauth.get_valid_token`
|
|
179
|
+
- `if_cli.runtime.CliError`
|
|
180
|
+
|
|
181
|
+
They raise `CliError` on failure, never `SystemExit` or `sys.exit`. They never
|
|
182
|
+
write to stdout, never launch a browser or block on interactive input, and only
|
|
183
|
+
use bounded waits. Changing or removing any of them is a breaking change and
|
|
184
|
+
needs a major version bump.
|
|
185
|
+
|
|
186
|
+
## How authentication works
|
|
187
|
+
|
|
188
|
+
- Profiles live in `~/.insightfactory/config`, one per customer and environment.
|
|
189
|
+
- The CLI manages the profile file and regenerates it on updates. Comments and
|
|
190
|
+
empty sections are not preserved.
|
|
191
|
+
- Tokens are cached by host in `~/.insightfactory/token-cache.json` with file
|
|
192
|
+
mode `0600`.
|
|
193
|
+
- The configuration directory is `0700`. Configuration and cache updates are
|
|
194
|
+
atomic.
|
|
195
|
+
- Remote factory, authorization, and token endpoints must use HTTPS. HTTP is
|
|
196
|
+
accepted only for loopback development.
|
|
197
|
+
- Login discovers OAuth metadata from
|
|
198
|
+
`{host}/.well-known/oauth-authorization-server`.
|
|
199
|
+
- Authorization uses PKCE through the factory `/authorize` proxy with a
|
|
200
|
+
loopback-only `127.0.0.1` callback. The CLI is a public OAuth client. It reads
|
|
201
|
+
the client ID from the factory's discovery document at runtime and holds no
|
|
202
|
+
client secret.
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
uv sync
|
|
208
|
+
uv run pytest
|
|
209
|
+
uv run ruff check .
|
|
210
|
+
uv run ty check
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Smoke-test against a throwaway config directory, never `~/.insightfactory`:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
uv run if-cli --help
|
|
217
|
+
uv run if-cli --version
|
|
218
|
+
INSIGHTFACTORY_CONFIG_DIR=$(mktemp -d) uv run if-cli profiles
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Python 3.10 or newer is required. Runtime code is stdlib-only.
|
|
222
|
+
|
|
223
|
+
## Release
|
|
224
|
+
|
|
225
|
+
A release is cut by pushing a tag, not by merging. Merging to `main` runs CI
|
|
226
|
+
and publishes nothing.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
git tag v1.0.1 && git push origin v1.0.1
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Verify and build live in the reusable workflow
|
|
233
|
+
[`if_s_insightfactory_cli.yml`](https://github.com/insightfactory-ai/if_sre_github_actions/blob/main/.github/workflows/if_s_insightfactory_cli.yml)
|
|
234
|
+
in `insightfactory-ai/if_sre_github_actions`. This repository's
|
|
235
|
+
`.github/workflows/release.yml` keeps only the OIDC publish job, because PyPI
|
|
236
|
+
Trusted Publishing is registered for this repo, workflow `release.yml`, and
|
|
237
|
+
environment `pypi`. A reusable workflow cannot be registered.
|
|
238
|
+
|
|
239
|
+
The PyPI project needs a one-time trusted-publisher registration for that
|
|
240
|
+
triple. The publish job downloads the `dist-release` artifact staged by the
|
|
241
|
+
reusable workflow and uploads it with `pypa/gh-action-pypi-publish`.
|
|
242
|
+
|
|
243
|
+
| Trigger | Published version | PyPI role |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| Push / merge to `develop` | `1.0.1.dev{N}` | pre-release (`pip install --pre`) |
|
|
246
|
+
| Push of a `v*` tag | the version in `pyproject.toml`, which the tag must match | latest |
|
|
247
|
+
| Merge to `main` | nothing, CI only | none |
|
|
248
|
+
|
|
249
|
+
`N` is `github.run_number` of the reusable workflow. It is a per-workflow
|
|
250
|
+
integer that goes up on every run and stays the same across re-runs of that
|
|
251
|
+
run, so a failed publish can be retried under the same version. It does not
|
|
252
|
+
depend on git history or clone depth. After a release, bump `version` in
|
|
253
|
+
`pyproject.toml` on `develop` so later `.devN` builds sort after what just
|
|
254
|
+
landed. PEP 440 puts `1.0.1.devN` before `1.0.1`, which is also why a spec of
|
|
255
|
+
`insightfactory-cli>=1.0.0` skips develop-channel builds and an in-process
|
|
256
|
+
consumer that wants them must ask for `insightfactory-cli>=1.0.0.dev0`.
|
|
257
|
+
|
|
258
|
+
A `v*` tag whose name does not match `pyproject.toml` fails before publishing.
|
|
259
|
+
|
|
260
|
+
The two channels treat an already-published version differently, on purpose:
|
|
261
|
+
|
|
262
|
+
- **Release channel, a tag.** Publishing fails and names the fix. A tag is a
|
|
263
|
+
request to release that version. If it cannot be honoured, the run must say
|
|
264
|
+
so rather than report success having published nothing. Git also refuses to
|
|
265
|
+
push a tag that already exists, so this is hard to reach.
|
|
266
|
+
- **Dev channel, `develop`.** Publishing is skipped quietly. `.dev{N}` uses
|
|
267
|
+
`github.run_number`, which is stable across re-runs of one run, so an
|
|
268
|
+
already-present version there can only mean a re-run of a run that already
|
|
269
|
+
published. That is idempotency, not a swallowed mistake.
|
|
270
|
+
|
|
271
|
+
This split is why releasing moved off `main`. While a merge was the trigger, an
|
|
272
|
+
already-published version had to be tolerated as a no-op for merges to stay
|
|
273
|
+
green. A merge that released nothing then looked the same as one that
|
|
274
|
+
released, and a forgotten version bump shipped nothing, silently.
|
|
275
|
+
|
|
276
|
+
## Differences from the Node CLI
|
|
277
|
+
|
|
278
|
+
Behaviour matches `@insightfactory-ai/if-cli` except where the platforms
|
|
279
|
+
differ:
|
|
280
|
+
|
|
281
|
+
- **Unknown-option wording** is aligned to `Unknown option '--flag'`. Other
|
|
282
|
+
argparse messages, such as a missing option value, still differ from Node's
|
|
283
|
+
`util.parseArgs`.
|
|
284
|
+
- **Expiry timestamps** in human-readable output use the C-locale `%c` format
|
|
285
|
+
rather than JavaScript `Date#toLocaleString()`.
|
|
286
|
+
- **`--version`** reads the installed package metadata from `1.0.0` onward
|
|
287
|
+
rather than an npm `package.json`.
|
|
288
|
+
|
|
289
|
+
On-disk profile and token-cache paths, JSON shapes, and file permissions stay
|
|
290
|
+
compatible with the Node CLI, so you can switch implementations without
|
|
291
|
+
re-authenticating.
|
|
292
|
+
|
|
293
|
+
## Licence
|
|
294
|
+
|
|
295
|
+
Copyright 2026 insightfactory.ai. All rights reserved. This package is proprietary
|
|
296
|
+
and may be used only under a separate written agreement with insightfactory.ai.
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "insightfactory-cli"
|
|
3
|
-
version = "1.0.2.
|
|
3
|
+
version = "1.0.2.dev16"
|
|
4
4
|
description = "Profile-based authentication CLI for the InsightFactory Interfaces API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
authors = [{ name = "insightfactory.ai Support", email = "support@insightfactory.ai" }]
|
|
8
|
+
license = "LicenseRef-Proprietary"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
8
10
|
keywords = ["insightfactory", "cli", "oauth", "pkce"]
|
|
9
11
|
classifiers = [
|
|
10
12
|
"Development Status :: 5 - Production/Stable",
|
|
11
13
|
"Environment :: Console",
|
|
12
14
|
"Intended Audience :: Developers",
|
|
15
|
+
"License :: Other/Proprietary License",
|
|
13
16
|
"Programming Language :: Python :: 3 :: Only",
|
|
14
17
|
"Typing :: Typed",
|
|
15
18
|
]
|
|
@@ -18,8 +21,7 @@ classifiers = [
|
|
|
18
21
|
if-cli = "if_cli.main:main"
|
|
19
22
|
|
|
20
23
|
[project.urls]
|
|
21
|
-
Homepage = "https://
|
|
22
|
-
Repository = "https://github.com/insightfactory-ai/insightfactory-cli"
|
|
24
|
+
Homepage = "https://insightfactory.ai"
|
|
23
25
|
|
|
24
26
|
[build-system]
|
|
25
27
|
requires = ["hatchling"]
|
|
@@ -188,7 +188,11 @@ def fetch_with_timeout(
|
|
|
188
188
|
request = urllib.request.Request(url, data=data, headers=headers or {}, method=method)
|
|
189
189
|
effective_timeout = resolve_request_timeout(timeout)
|
|
190
190
|
try:
|
|
191
|
-
|
|
191
|
+
# Callers pass endpoints validated by parse_http_url or derived from a
|
|
192
|
+
# validated factory origin. file: and other local schemes are rejected.
|
|
193
|
+
with urllib.request.urlopen( # nosec B310
|
|
194
|
+
request, timeout=effective_timeout
|
|
195
|
+
) as response:
|
|
192
196
|
return HttpResponse(response.status, response.headers, response.read())
|
|
193
197
|
except urllib.error.HTTPError as error:
|
|
194
198
|
return HttpResponse(error.code, error.headers, error.read())
|
|
@@ -4,7 +4,7 @@ import base64
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import json
|
|
6
6
|
import secrets
|
|
7
|
-
import subprocess
|
|
7
|
+
import subprocess # nosec B404
|
|
8
8
|
import sys
|
|
9
9
|
import time
|
|
10
10
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
@@ -51,7 +51,8 @@ def open_browser(url: str, launcher: str | None = None) -> None:
|
|
|
51
51
|
else:
|
|
52
52
|
command, args = "xdg-open", [url]
|
|
53
53
|
try:
|
|
54
|
-
|
|
54
|
+
# The executable is selected by platform and shell remains false.
|
|
55
|
+
subprocess.Popen( # nosec B603
|
|
55
56
|
[command, *args],
|
|
56
57
|
stdout=subprocess.DEVNULL,
|
|
57
58
|
stderr=subprocess.DEVNULL,
|