insightfactory-cli 1.0.2.dev13__tar.gz → 1.0.3.dev17__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 (52) hide show
  1. insightfactory_cli-1.0.3.dev17/PKG-INFO +314 -0
  2. insightfactory_cli-1.0.3.dev17/README.md +296 -0
  3. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/pyproject.toml +1 -1
  4. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/uv.lock +1 -1
  5. insightfactory_cli-1.0.2.dev13/PKG-INFO +0 -317
  6. insightfactory_cli-1.0.2.dev13/README.md +0 -299
  7. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/.github/workflows/ci.yml +0 -0
  8. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/.github/workflows/claude.yml +0 -0
  9. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/.github/workflows/release.yml +0 -0
  10. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/.gitignore +0 -0
  11. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/.python-version +0 -0
  12. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/AGENTS.md +0 -0
  13. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/CLAUDE.md +0 -0
  14. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/LICENSE +0 -0
  15. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/__init__.py +0 -0
  16. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/__main__.py +0 -0
  17. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/cache.py +0 -0
  18. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/cli.py +0 -0
  19. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/colour.py +0 -0
  20. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/commands/__init__.py +0 -0
  21. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/commands/api.py +0 -0
  22. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/commands/config.py +0 -0
  23. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/commands/login.py +0 -0
  24. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/commands/logout.py +0 -0
  25. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/commands/profiles.py +0 -0
  26. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/commands/set_token.py +0 -0
  27. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/commands/token.py +0 -0
  28. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/config.py +0 -0
  29. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/constants.py +0 -0
  30. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/http.py +0 -0
  31. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/main.py +0 -0
  32. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/oauth.py +0 -0
  33. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/src/if_cli/runtime.py +0 -0
  34. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/__init__.py +0 -0
  35. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/cache_writer.py +0 -0
  36. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/conftest.py +0 -0
  37. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/helpers.py +0 -0
  38. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/servers.py +0 -0
  39. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_api.py +0 -0
  40. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_api_command.py +0 -0
  41. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_cache.py +0 -0
  42. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_cli.py +0 -0
  43. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_config.py +0 -0
  44. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_config_command.py +0 -0
  45. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_login.py +0 -0
  46. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_oauth.py +0 -0
  47. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_oauth_flow.py +0 -0
  48. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_profiles.py +0 -0
  49. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_programmatic_api.py +0 -0
  50. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_runtime.py +0 -0
  51. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_set_token.py +0 -0
  52. {insightfactory_cli-1.0.2.dev13 → insightfactory_cli-1.0.3.dev17}/tests/test_token.py +0 -0
@@ -0,0 +1,314 @@
1
+ Metadata-Version: 2.5
2
+ Name: insightfactory-cli
3
+ Version: 1.0.3.dev17
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "insightfactory-cli"
3
- version = "1.0.2.dev13"
3
+ version = "1.0.3.dev17"
4
4
  description = "Profile-based authentication CLI for the InsightFactory Interfaces API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -34,7 +34,7 @@ wheels = [
34
34
 
35
35
  [[package]]
36
36
  name = "insightfactory-cli"
37
- version = "1.0.2"
37
+ version = "1.0.3"
38
38
  source = { editable = "." }
39
39
 
40
40
  [package.dev-dependencies]