insightfactory-cli 1.2.0.dev34__tar.gz → 1.3.0.dev39__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.3.0.dev39/.github/workflows/claude.yml +144 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/CHANGELOG.md +34 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/CLAUDE.md +3 -2
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/PKG-INFO +24 -9
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/README.md +23 -8
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/pyproject.toml +1 -1
- insightfactory_cli-1.3.0.dev39/src/if_cli/cache.py +318 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/logout.py +3 -4
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/profiles.py +3 -3
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/set_token.py +1 -1
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/oauth.py +13 -14
- insightfactory_cli-1.3.0.dev39/tests/cache_writer.py +16 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_attribution.py +1 -1
- insightfactory_cli-1.3.0.dev39/tests/test_cache.py +290 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_cli.py +2 -2
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_mcp_command.py +6 -2
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_oauth_flow.py +113 -21
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_oauth_force_refresh.py +3 -3
- insightfactory_cli-1.3.0.dev39/tests/test_profile_tokens.py +170 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_router_upstream.py +26 -2
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_set_token.py +1 -3
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/uv.lock +1 -1
- insightfactory_cli-1.2.0.dev34/.github/workflows/claude.yml +0 -78
- insightfactory_cli-1.2.0.dev34/src/if_cli/cache.py +0 -206
- insightfactory_cli-1.2.0.dev34/tests/cache_writer.py +0 -14
- insightfactory_cli-1.2.0.dev34/tests/test_cache.py +0 -146
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/.github/workflows/ci.yml +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/.github/workflows/release.yml +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/.gitignore +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/.python-version +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/AGENTS.md +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/LICENSE +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/__init__.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/__main__.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/assets/__init__.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/assets/insightfactoryai-logo.svg +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/attribution.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/callback_page.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/cli.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/colour.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/__init__.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/api.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/config.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/login.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/mcp.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/token.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/config.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/constants.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/http.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/main.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/router/__init__.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/router/catalog.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/router/content.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/router/log.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/router/policy.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/router/server.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/router/upstream.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/runtime.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/__init__.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/conftest.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/helpers.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/servers.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_api.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_api_command.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_config.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_config_command.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_login.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_oauth.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_profiles.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_programmatic_api.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_release_tools.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_router_catalog.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_router_content.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_router_log.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_router_policy.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_router_resources.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_router_server.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_runtime.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tests/test_token.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tools/check_release.py +0 -0
- {insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/tools/release_notes.py +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# =======================================================
|
|
2
|
+
# GitHub Actions Workflow: Claude Code Assistant
|
|
3
|
+
#
|
|
4
|
+
# Purpose: AI-powered code review and assistance, on demand only.
|
|
5
|
+
# Triggered by mentioning @claude in a PR comment, an issue
|
|
6
|
+
# comment, or an inline PR review comment (e.g. `@claude review`).
|
|
7
|
+
# @agent-review on a PR runs Claude and Codex together through the
|
|
8
|
+
# shared agent review instead. There is no automatic review on PR open.
|
|
9
|
+
#
|
|
10
|
+
# =======================================================
|
|
11
|
+
|
|
12
|
+
name: Claude Workflow
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
issue_comment:
|
|
16
|
+
types: [created]
|
|
17
|
+
pull_request_review_comment:
|
|
18
|
+
types: [created]
|
|
19
|
+
|
|
20
|
+
# No `permissions:` block here on purpose. A called workflow may only reduce the
|
|
21
|
+
# caller's grant, never raise it, and GitHub validates that at startup -- so
|
|
22
|
+
# capping this to `contents: read` made every run fail with `startup_failure`
|
|
23
|
+
# before any job ran. if_s_claude_review.yml declares the scopes it needs
|
|
24
|
+
# (id-token/pull-requests/issues write, actions read); leaving the caller
|
|
25
|
+
# unrestricted is what every other repo in the org that calls it does.
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
claude:
|
|
29
|
+
# Author gating. The comment body is attacker-controlled and the call below
|
|
30
|
+
# inherits every secret, so restrict the trigger to people with write-ish
|
|
31
|
+
# access rather than anyone who can leave a comment.
|
|
32
|
+
if: |
|
|
33
|
+
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
|
|
34
|
+
contains(github.event.comment.body, '@claude')
|
|
35
|
+
uses: insightfactory-ai/if_sre_github_actions/.github/workflows/if_s_claude_review.yml@main
|
|
36
|
+
secrets: inherit
|
|
37
|
+
with:
|
|
38
|
+
# CONSTRAINT: the value below is interpolated inside a double-quoted CLI argument with no
|
|
39
|
+
# escaping. Do NOT include ASCII double-quote characters. Use single quotes or backticks
|
|
40
|
+
# if you need to quote something. Newlines are fine.
|
|
41
|
+
append_system_prompt: |
|
|
42
|
+
This repository (insightfactory-cli) is the production Python `if-cli`, published to public
|
|
43
|
+
PyPI. Read CLAUDE.md and README.md before reviewing; the rules below are the ones that get
|
|
44
|
+
violated most often.
|
|
45
|
+
|
|
46
|
+
Runtime and layout:
|
|
47
|
+
- Runtime code is stdlib-only. Flag any new runtime dependency in pyproject.toml or any
|
|
48
|
+
import outside the standard library.
|
|
49
|
+
- Keep one module per CLI command under `src/if_cli/commands/`.
|
|
50
|
+
- Do not suggest third-party HTTP, OAuth, or CLI frameworks; the codebase uses urllib and
|
|
51
|
+
argparse deliberately.
|
|
52
|
+
|
|
53
|
+
Security and auth:
|
|
54
|
+
- A profile's bearer token must never be sent outside that profile's factory origin. Flag
|
|
55
|
+
any code path that could forward a token to another host.
|
|
56
|
+
- OAuth uses authorization-code flow with PKCE and a loopback-only (`127.0.0.1`) callback.
|
|
57
|
+
The CLI is a public client with no client secret.
|
|
58
|
+
- Do not commit credentials, access tokens, refresh tokens, or client secrets in code,
|
|
59
|
+
tests, or fixtures. Fixtures must use neutral hostnames such as `factory.example`.
|
|
60
|
+
|
|
61
|
+
On-disk compatibility:
|
|
62
|
+
- Profiles and tokens must be written only through the atomic private-file helpers
|
|
63
|
+
(`write_private_file` in config.py): `0o600` files, `0o700` directories, atomic replace
|
|
64
|
+
via temp file.
|
|
65
|
+
- The Node CLI is retired. Profile sections keep the Node layout (`~/.insightfactory`,
|
|
66
|
+
`INSIGHTFACTORY_CONFIG_DIR` override). `token-cache.json` is version 2: keyed by
|
|
67
|
+
profile name, with the host in each entry. Older `if-cli` releases and the Node CLI
|
|
68
|
+
cannot read it, and that is accepted. Reading a version 1 cache must keep working.
|
|
69
|
+
|
|
70
|
+
Programmatic API (breaking-change surface):
|
|
71
|
+
- These four symbols are a supported in-process surface documented in README.md:
|
|
72
|
+
`if_cli.config.load_config`, `if_cli.config.get_profile`, `if_cli.oauth.get_valid_token`,
|
|
73
|
+
`if_cli.runtime.CliError`.
|
|
74
|
+
- They must raise `CliError` on failure (never `SystemExit`), never write to stdout, never
|
|
75
|
+
launch a browser or block on interactive input, and use only bounded waits.
|
|
76
|
+
- Changing signatures, behaviour, or removing any of them requires a major version bump.
|
|
77
|
+
|
|
78
|
+
Tests:
|
|
79
|
+
- `tests/` uses isolated config dirs via `INSIGHTFACTORY_CONFIG_DIR`; never smoke-test
|
|
80
|
+
against `~/.insightfactory`.
|
|
81
|
+
|
|
82
|
+
agent-review:
|
|
83
|
+
# Claude and Codex review the PR together, one matrix job each. The shared
|
|
84
|
+
# workflow's request check refuses anything but a PR comment from someone with
|
|
85
|
+
# write access that names @agent-review as a whole word.
|
|
86
|
+
if: >-
|
|
87
|
+
contains(github.event.comment.body, '@agent-review')
|
|
88
|
+
&& (github.event_name == 'pull_request_review_comment' || github.event.issue.pull_request)
|
|
89
|
+
permissions:
|
|
90
|
+
id-token: write
|
|
91
|
+
contents: read
|
|
92
|
+
pull-requests: write
|
|
93
|
+
issues: write
|
|
94
|
+
actions: read
|
|
95
|
+
checks: read
|
|
96
|
+
statuses: read
|
|
97
|
+
uses: insightfactory-ai/if_sre_github_actions/.github/workflows/if_s_agent_review.yml@main
|
|
98
|
+
secrets: inherit
|
|
99
|
+
with:
|
|
100
|
+
# A reviewer approves only an approved verdict with no P0, P1 or P2, and a
|
|
101
|
+
# person from the required team still approves every PR (the Approvers
|
|
102
|
+
# ruleset on develop, test and main).
|
|
103
|
+
allow_agent_approval: true
|
|
104
|
+
# The same instructions as the @claude job above; keep the two in step.
|
|
105
|
+
append_system_prompt: |
|
|
106
|
+
This repository (insightfactory-cli) is the production Python `if-cli`, published to public
|
|
107
|
+
PyPI. Read CLAUDE.md and README.md before reviewing; the rules below are the ones that get
|
|
108
|
+
violated most often.
|
|
109
|
+
|
|
110
|
+
Runtime and layout:
|
|
111
|
+
- Runtime code is stdlib-only. Flag any new runtime dependency in pyproject.toml or any
|
|
112
|
+
import outside the standard library.
|
|
113
|
+
- Keep one module per CLI command under `src/if_cli/commands/`.
|
|
114
|
+
- Do not suggest third-party HTTP, OAuth, or CLI frameworks; the codebase uses urllib and
|
|
115
|
+
argparse deliberately.
|
|
116
|
+
|
|
117
|
+
Security and auth:
|
|
118
|
+
- A profile's bearer token must never be sent outside that profile's factory origin. Flag
|
|
119
|
+
any code path that could forward a token to another host.
|
|
120
|
+
- OAuth uses authorization-code flow with PKCE and a loopback-only (`127.0.0.1`) callback.
|
|
121
|
+
The CLI is a public client with no client secret.
|
|
122
|
+
- Do not commit credentials, access tokens, refresh tokens, or client secrets in code,
|
|
123
|
+
tests, or fixtures. Fixtures must use neutral hostnames such as `factory.example`.
|
|
124
|
+
|
|
125
|
+
On-disk compatibility:
|
|
126
|
+
- Profiles and tokens must be written only through the atomic private-file helpers
|
|
127
|
+
(`write_private_file` in config.py): `0o600` files, `0o700` directories, atomic replace
|
|
128
|
+
via temp file.
|
|
129
|
+
- The Node CLI is retired. Profile sections keep the Node layout (`~/.insightfactory`,
|
|
130
|
+
`INSIGHTFACTORY_CONFIG_DIR` override). `token-cache.json` is version 2: keyed by
|
|
131
|
+
profile name, with the host in each entry. Older `if-cli` releases and the Node CLI
|
|
132
|
+
cannot read it, and that is accepted. Reading a version 1 cache must keep working.
|
|
133
|
+
|
|
134
|
+
Programmatic API (breaking-change surface):
|
|
135
|
+
- These four symbols are a supported in-process surface documented in README.md:
|
|
136
|
+
`if_cli.config.load_config`, `if_cli.config.get_profile`, `if_cli.oauth.get_valid_token`,
|
|
137
|
+
`if_cli.runtime.CliError`.
|
|
138
|
+
- They must raise `CliError` on failure (never `SystemExit`), never write to stdout, never
|
|
139
|
+
launch a browser or block on interactive input, and use only bounded waits.
|
|
140
|
+
- Changing signatures, behaviour, or removing any of them requires a major version bump.
|
|
141
|
+
|
|
142
|
+
Tests:
|
|
143
|
+
- `tests/` uses isolated config dirs via `INSIGHTFACTORY_CONFIG_DIR`; never smoke-test
|
|
144
|
+
against `~/.insightfactory`.
|
|
@@ -11,6 +11,40 @@ that: every PR into `develop` must add an entry here, and a tag whose version ha
|
|
|
11
11
|
section fails rather than releasing an empty page. The version only moves when the
|
|
12
12
|
last one has been released, so a cycle's PRs share a section.
|
|
13
13
|
|
|
14
|
+
## 1.3.0
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Internal only: a PR comment naming `@agent-review` runs the shared Claude and
|
|
19
|
+
Codex review, which can approve a PR that a person from the required team
|
|
20
|
+
still approves. The CLI itself is unchanged.
|
|
21
|
+
- `logout` names the profile as well as the host in its output, for example
|
|
22
|
+
`✓ cleared cached token for profile 'dev' (https://factory.example)`, because
|
|
23
|
+
two profiles can now share a host.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- The token cache is keyed by profile, not by host. Before, two profiles on the
|
|
28
|
+
same factory host shared one token, so signing in as a second account on one
|
|
29
|
+
profile silently replaced the other profile's token, and that profile then
|
|
30
|
+
acted as the second account. Each profile now keeps its own token through
|
|
31
|
+
`login`, refresh, `token`, `api`, `mcp`, `profiles`, `set-token` and `logout`.
|
|
32
|
+
Each entry records the host that issued it, and a profile whose `host` changes
|
|
33
|
+
stops using it, so the token is not sent to the new origin.
|
|
34
|
+
- `token-cache.json` moves to version 2. The CLI reads a version 1 cache and
|
|
35
|
+
gives each host's token to every profile on that host, so upgrading needs no
|
|
36
|
+
new login. The first write saves version 2. Older `if-cli` releases cannot
|
|
37
|
+
read version 2 and stop on every command, `login` included. To downgrade,
|
|
38
|
+
move `token-cache.json` aside, then sign in to each profile again.
|
|
39
|
+
- A version 1 cache cannot say which account its host entry holds. If two
|
|
40
|
+
profiles on one host signed in as different accounts before this release, both
|
|
41
|
+
start out holding whichever account signed in last. Run
|
|
42
|
+
`if-cli login -p <profile>` once for each such profile to give it its own
|
|
43
|
+
account.
|
|
44
|
+
- Internal only: the test that `mcp` answers `initialize` before the catalogue
|
|
45
|
+
build finishes allows 3 seconds instead of 1, because starting the subprocess
|
|
46
|
+
on a CI runner took just over a second and failed the test at random.
|
|
47
|
+
|
|
14
48
|
## 1.2.0
|
|
15
49
|
|
|
16
50
|
### Added
|
|
@@ -22,8 +22,9 @@ uv run ty check
|
|
|
22
22
|
- OAuth uses authorization-code flow with PKCE and a loopback-only callback.
|
|
23
23
|
- Store profiles and tokens through the atomic private-file helpers.
|
|
24
24
|
- The Node CLI is retired; `[factory <name>]` sections are Python-only. Profile
|
|
25
|
-
sections
|
|
26
|
-
`
|
|
25
|
+
sections keep the Node layout (`~/.insightfactory`, `INSIGHTFACTORY_CONFIG_DIR`
|
|
26
|
+
override). `token-cache.json` is version 2: keyed by profile name, with the
|
|
27
|
+
host in each entry. An older `if-cli` cannot read it.
|
|
27
28
|
- Do not commit credentials, access tokens, refresh tokens, or client secrets.
|
|
28
29
|
- Test fixtures use only neutral hostnames (`factory.example`).
|
|
29
30
|
- Do not publish to PyPI from a developer machine. Publishing happens in CI: the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: insightfactory-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0.dev39
|
|
4
4
|
Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
|
|
5
5
|
Project-URL: Homepage, https://insightfactory.ai
|
|
6
6
|
Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
|
|
@@ -27,9 +27,9 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
Profile-based authentication CLI for the InsightFactory Interfaces API. It
|
|
28
28
|
installs the `if-cli` command, which logs in through the browser with OAuth
|
|
29
29
|
authorization code and PKCE, refreshes tokens, and makes authenticated API
|
|
30
|
-
calls. Profiles live in `~/.insightfactory` in the same layout the Node
|
|
31
|
-
`@insightfactory-ai/if-cli`
|
|
32
|
-
|
|
30
|
+
calls. Profiles live in `~/.insightfactory` in the same layout the retired Node
|
|
31
|
+
`@insightfactory-ai/if-cli` used. The token cache uses its own format, described
|
|
32
|
+
under "How authentication works".
|
|
33
33
|
|
|
34
34
|
This is the production Python CLI, published to public PyPI. It needs Python
|
|
35
35
|
3.10 or newer.
|
|
@@ -229,8 +229,20 @@ rejected.
|
|
|
229
229
|
- Profiles live in `~/.insightfactory/config`, one per customer and environment.
|
|
230
230
|
- The CLI manages the profile file and regenerates it on updates. Comments and
|
|
231
231
|
empty sections are not preserved.
|
|
232
|
-
- Tokens are cached
|
|
233
|
-
mode `0600`.
|
|
232
|
+
- Tokens are cached per profile in `~/.insightfactory/token-cache.json` with
|
|
233
|
+
file mode `0600`. Two profiles on the same host can sign in as different
|
|
234
|
+
accounts, and each keeps its own token. Each entry records the host that
|
|
235
|
+
issued it, and a profile uses it only while its `host` still matches, so a
|
|
236
|
+
token never goes to another origin after a host change.
|
|
237
|
+
- `if-cli` 1.3.0 and later write version 2 of the cache, keyed by profile
|
|
238
|
+
name. They read the older version 1 cache, keyed by host, and give its token
|
|
239
|
+
to every profile on that host, so an upgrade needs no new login. Older
|
|
240
|
+
`if-cli` releases and the Node CLI cannot read version 2 and stop on every
|
|
241
|
+
command, `login` included. To downgrade, move `token-cache.json` aside, then
|
|
242
|
+
sign in to each profile again.
|
|
243
|
+
- A version 1 cache holds one account per host. If two profiles on one host
|
|
244
|
+
signed in as different accounts before 1.3.0, both start out as whichever
|
|
245
|
+
account signed in last. Run `if-cli login -p <profile>` once for each of them.
|
|
234
246
|
- The configuration directory is `0700`. Configuration and cache updates are
|
|
235
247
|
atomic.
|
|
236
248
|
- Remote factory, authorization, and token endpoints must use HTTPS. HTTP is
|
|
@@ -512,9 +524,12 @@ differ:
|
|
|
512
524
|
- **`--version`** reads the installed package metadata from `1.0.0` onward
|
|
513
525
|
rather than an npm `package.json`.
|
|
514
526
|
|
|
515
|
-
On-disk profile and token-cache paths
|
|
516
|
-
|
|
517
|
-
|
|
527
|
+
On-disk profile and token-cache paths and file permissions match the Node CLI.
|
|
528
|
+
The token cache's JSON shape does not: `if-cli` keys it by profile (version 2),
|
|
529
|
+
which the Node CLI and older `if-cli` releases cannot read. The Python CLI
|
|
530
|
+
still reads a Node-written version 1 cache, so moving from Node to Python needs
|
|
531
|
+
no new login. To move back, move `token-cache.json` aside and sign in to each
|
|
532
|
+
profile again.
|
|
518
533
|
|
|
519
534
|
## Licence
|
|
520
535
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
Profile-based authentication CLI for the InsightFactory Interfaces API. It
|
|
4
4
|
installs the `if-cli` command, which logs in through the browser with OAuth
|
|
5
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`
|
|
8
|
-
|
|
6
|
+
calls. Profiles live in `~/.insightfactory` in the same layout the retired Node
|
|
7
|
+
`@insightfactory-ai/if-cli` used. The token cache uses its own format, described
|
|
8
|
+
under "How authentication works".
|
|
9
9
|
|
|
10
10
|
This is the production Python CLI, published to public PyPI. It needs Python
|
|
11
11
|
3.10 or newer.
|
|
@@ -205,8 +205,20 @@ rejected.
|
|
|
205
205
|
- Profiles live in `~/.insightfactory/config`, one per customer and environment.
|
|
206
206
|
- The CLI manages the profile file and regenerates it on updates. Comments and
|
|
207
207
|
empty sections are not preserved.
|
|
208
|
-
- Tokens are cached
|
|
209
|
-
mode `0600`.
|
|
208
|
+
- Tokens are cached per profile in `~/.insightfactory/token-cache.json` with
|
|
209
|
+
file mode `0600`. Two profiles on the same host can sign in as different
|
|
210
|
+
accounts, and each keeps its own token. Each entry records the host that
|
|
211
|
+
issued it, and a profile uses it only while its `host` still matches, so a
|
|
212
|
+
token never goes to another origin after a host change.
|
|
213
|
+
- `if-cli` 1.3.0 and later write version 2 of the cache, keyed by profile
|
|
214
|
+
name. They read the older version 1 cache, keyed by host, and give its token
|
|
215
|
+
to every profile on that host, so an upgrade needs no new login. Older
|
|
216
|
+
`if-cli` releases and the Node CLI cannot read version 2 and stop on every
|
|
217
|
+
command, `login` included. To downgrade, move `token-cache.json` aside, then
|
|
218
|
+
sign in to each profile again.
|
|
219
|
+
- A version 1 cache holds one account per host. If two profiles on one host
|
|
220
|
+
signed in as different accounts before 1.3.0, both start out as whichever
|
|
221
|
+
account signed in last. Run `if-cli login -p <profile>` once for each of them.
|
|
210
222
|
- The configuration directory is `0700`. Configuration and cache updates are
|
|
211
223
|
atomic.
|
|
212
224
|
- Remote factory, authorization, and token endpoints must use HTTPS. HTTP is
|
|
@@ -488,9 +500,12 @@ differ:
|
|
|
488
500
|
- **`--version`** reads the installed package metadata from `1.0.0` onward
|
|
489
501
|
rather than an npm `package.json`.
|
|
490
502
|
|
|
491
|
-
On-disk profile and token-cache paths
|
|
492
|
-
|
|
493
|
-
|
|
503
|
+
On-disk profile and token-cache paths and file permissions match the Node CLI.
|
|
504
|
+
The token cache's JSON shape does not: `if-cli` keys it by profile (version 2),
|
|
505
|
+
which the Node CLI and older `if-cli` releases cannot read. The Python CLI
|
|
506
|
+
still reads a Node-written version 1 cache, so moving from Node to Python needs
|
|
507
|
+
no new login. To move back, move `token-cache.json` aside and sign in to each
|
|
508
|
+
profile again.
|
|
494
509
|
|
|
495
510
|
## Licence
|
|
496
511
|
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import NoReturn, TypedDict, TypeGuard, TypeVar
|
|
10
|
+
|
|
11
|
+
from if_cli.config import cache_file, get_profile, is_factory_section, load_config, write_private_file
|
|
12
|
+
from if_cli.constants import EXPIRY_SLACK_SECONDS
|
|
13
|
+
from if_cli.runtime import die, is_record
|
|
14
|
+
|
|
15
|
+
CACHE_LOCK_STALE_MS = 30_000
|
|
16
|
+
CACHE_VERSION = 2
|
|
17
|
+
HOST_KEYED_CACHE_VERSION = 1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CacheEntry(TypedDict, total=False):
|
|
21
|
+
host: str
|
|
22
|
+
grant: str
|
|
23
|
+
access_token: str
|
|
24
|
+
refresh_token: str
|
|
25
|
+
expires_at: float
|
|
26
|
+
token_endpoint: str
|
|
27
|
+
client_id: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TokenCache(TypedDict):
|
|
31
|
+
"""Version 2 keys `tokens` by profile name. Each entry records the host it was
|
|
32
|
+
issued for, and is used only while the profile still points at that host."""
|
|
33
|
+
|
|
34
|
+
version: int
|
|
35
|
+
tokens: dict[str, CacheEntry]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TokenOwner(TypedDict):
|
|
39
|
+
"""The part of a `Profile` that owns a cache entry."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
host: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def empty_cache() -> TokenCache:
|
|
46
|
+
return {"version": CACHE_VERSION, "tokens": {}}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def entry_for(cache: TokenCache, owner: TokenOwner) -> CacheEntry | None:
|
|
50
|
+
"""The profile's entry, unless it was issued for a host the profile no longer
|
|
51
|
+
names: a bearer token never goes to an origin other than the one it came from."""
|
|
52
|
+
entry = cache["tokens"].get(owner["name"])
|
|
53
|
+
if entry is None or entry.get("host") != owner["host"]:
|
|
54
|
+
return None
|
|
55
|
+
return entry
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _siblings(cache: TokenCache, owner: TokenOwner, grant: str | None) -> list[str]:
|
|
59
|
+
"""Other profiles on the same host that share the owner's `grant`.
|
|
60
|
+
|
|
61
|
+
Only a version 1 cache produces these: it held one entry per host, which the
|
|
62
|
+
upgrade copies to every profile on that host, marking the copies with one
|
|
63
|
+
`grant` id. They are one sign-in, so a rotation or rejection on one applies to
|
|
64
|
+
all of them; leaving a sibling on the rotated-out refresh token would replay it
|
|
65
|
+
on its next refresh. The id outlives a cleared refresh token, so a refresh that
|
|
66
|
+
lands after a peer's invalid_grant still reaches every sibling. A login or a
|
|
67
|
+
pasted token starts a fresh entry without it, which ends the sharing.
|
|
68
|
+
"""
|
|
69
|
+
if not grant:
|
|
70
|
+
return []
|
|
71
|
+
return [
|
|
72
|
+
name
|
|
73
|
+
for name, entry in cache["tokens"].items()
|
|
74
|
+
if name != owner["name"] and entry.get("host") == owner["host"] and entry.get("grant") == grant
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _positive_integer_env(name: str, fallback: int) -> int:
|
|
79
|
+
raw = os.environ.get(name)
|
|
80
|
+
if raw is None:
|
|
81
|
+
return fallback
|
|
82
|
+
try:
|
|
83
|
+
value = float(raw)
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
return fallback
|
|
86
|
+
if math.isfinite(value) and value == int(value) and value > 0:
|
|
87
|
+
return int(value)
|
|
88
|
+
return fallback
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _cache_lock_file() -> str:
|
|
92
|
+
return f"{cache_file()}.lock"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _cache_lock_attempts() -> int:
|
|
96
|
+
return _positive_integer_env("INSIGHTFACTORY_CACHE_LOCK_ATTEMPTS", 200)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _cache_lock_retry_ms() -> int:
|
|
100
|
+
return _positive_integer_env("INSIGHTFACTORY_CACHE_LOCK_RETRY_MS", 25)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def is_fresh(entry: CacheEntry, now_seconds: float | None = None) -> bool:
|
|
104
|
+
if now_seconds is None:
|
|
105
|
+
now_seconds = time.time()
|
|
106
|
+
return entry["expires_at"] - EXPIRY_SLACK_SECONDS > now_seconds
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _is_cache_entry(value: object, *, with_host: bool) -> TypeGuard[CacheEntry]:
|
|
110
|
+
if not is_record(value):
|
|
111
|
+
return False
|
|
112
|
+
if with_host and not isinstance(value.get("host"), str):
|
|
113
|
+
return False
|
|
114
|
+
if "grant" in value and not isinstance(value.get("grant"), str):
|
|
115
|
+
return False
|
|
116
|
+
access_token = value.get("access_token")
|
|
117
|
+
expires_at = value.get("expires_at")
|
|
118
|
+
token_endpoint = value.get("token_endpoint")
|
|
119
|
+
refresh_token = value.get("refresh_token")
|
|
120
|
+
client_id = value.get("client_id")
|
|
121
|
+
return (
|
|
122
|
+
isinstance(access_token, str)
|
|
123
|
+
and isinstance(expires_at, (int, float))
|
|
124
|
+
and not isinstance(expires_at, bool)
|
|
125
|
+
and math.isfinite(float(expires_at))
|
|
126
|
+
and isinstance(token_endpoint, str)
|
|
127
|
+
and ("refresh_token" not in value or isinstance(refresh_token, str))
|
|
128
|
+
and ("client_id" not in value or isinstance(client_id, str))
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _invalid_structure() -> NoReturn:
|
|
133
|
+
die(f"token cache {cache_file()} has an invalid structure; move it aside and log in again")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _parse_entries(tokens_value: dict[str, object], *, with_host: bool) -> dict[str, CacheEntry]:
|
|
137
|
+
tokens: dict[str, CacheEntry] = {}
|
|
138
|
+
for key, entry in tokens_value.items():
|
|
139
|
+
if not _is_cache_entry(entry, with_host=with_host):
|
|
140
|
+
die(f"token cache entry for {key} is invalid; move {cache_file()} aside and log in again")
|
|
141
|
+
tokens[key] = entry
|
|
142
|
+
return tokens
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _from_host_keyed(tokens_by_host: dict[str, CacheEntry]) -> dict[str, CacheEntry]:
|
|
146
|
+
"""Give each configured profile the version 1 entry for its host.
|
|
147
|
+
|
|
148
|
+
A host entry with no profile left on that host is unreachable, so it is
|
|
149
|
+
dropped. A profile that does not resolve gets nothing, as it could not have
|
|
150
|
+
used a token before either.
|
|
151
|
+
"""
|
|
152
|
+
config = load_config()
|
|
153
|
+
tokens: dict[str, CacheEntry] = {}
|
|
154
|
+
for section in config:
|
|
155
|
+
if is_factory_section(section):
|
|
156
|
+
continue
|
|
157
|
+
name = "DEFAULT" if section == "" else section
|
|
158
|
+
try:
|
|
159
|
+
host = get_profile(config, name)["host"]
|
|
160
|
+
except Exception:
|
|
161
|
+
# Not only CliError: a bad port raises ValueError or OverflowError. One
|
|
162
|
+
# broken profile must not stop every other profile's cache read.
|
|
163
|
+
continue
|
|
164
|
+
entry = tokens_by_host.get(host)
|
|
165
|
+
if entry is not None:
|
|
166
|
+
migrated = entry.copy()
|
|
167
|
+
migrated["host"] = host
|
|
168
|
+
tokens[name] = migrated
|
|
169
|
+
hosts = [entry["host"] for entry in tokens.values()]
|
|
170
|
+
for entry in tokens.values():
|
|
171
|
+
refresh_token = entry.get("refresh_token")
|
|
172
|
+
if refresh_token and hosts.count(entry["host"]) > 1:
|
|
173
|
+
# Derived, not random: every process reading the same version 1 file
|
|
174
|
+
# agrees on it until the first write saves version 2.
|
|
175
|
+
entry["grant"] = hashlib.sha256(f"{entry['host']}\n{refresh_token}".encode()).hexdigest()[:32]
|
|
176
|
+
return tokens
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _parse_cache(raw: str) -> TokenCache:
|
|
180
|
+
value: object = json.loads(raw)
|
|
181
|
+
if not is_record(value):
|
|
182
|
+
_invalid_structure()
|
|
183
|
+
tokens_value = value.get("tokens")
|
|
184
|
+
if not is_record(tokens_value):
|
|
185
|
+
_invalid_structure()
|
|
186
|
+
version = value.get("version")
|
|
187
|
+
if version == CACHE_VERSION:
|
|
188
|
+
return {"version": CACHE_VERSION, "tokens": _parse_entries(tokens_value, with_host=True)}
|
|
189
|
+
if version == HOST_KEYED_CACHE_VERSION:
|
|
190
|
+
tokens_by_host = _parse_entries(tokens_value, with_host=False)
|
|
191
|
+
return {"version": CACHE_VERSION, "tokens": _from_host_keyed(tokens_by_host)}
|
|
192
|
+
_invalid_structure()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def load_cache() -> TokenCache:
|
|
196
|
+
try:
|
|
197
|
+
with open(cache_file(), encoding="utf-8") as handle:
|
|
198
|
+
return _parse_cache(handle.read())
|
|
199
|
+
except FileNotFoundError:
|
|
200
|
+
return empty_cache()
|
|
201
|
+
except json.JSONDecodeError:
|
|
202
|
+
die(f"token cache {cache_file()} is not valid JSON; move it aside and log in again")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _save_cache(cache: TokenCache) -> None:
|
|
206
|
+
write_private_file(cache_file(), f"{json.dumps(cache, indent=2)}\n")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
T = TypeVar("T")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def with_cache_lock(operation: Callable[[], T]) -> T:
|
|
213
|
+
lock_file = _cache_lock_file()
|
|
214
|
+
os.makedirs(os.path.dirname(lock_file), mode=0o700, exist_ok=True)
|
|
215
|
+
attempts = _cache_lock_attempts()
|
|
216
|
+
retry_ms = _cache_lock_retry_ms()
|
|
217
|
+
for _attempt in range(attempts):
|
|
218
|
+
try:
|
|
219
|
+
descriptor = os.open(lock_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
220
|
+
except FileExistsError:
|
|
221
|
+
try:
|
|
222
|
+
mtime_ms = os.stat(lock_file).st_mtime * 1000
|
|
223
|
+
if time.time() * 1000 - mtime_ms > CACHE_LOCK_STALE_MS:
|
|
224
|
+
os.unlink(lock_file)
|
|
225
|
+
continue
|
|
226
|
+
except OSError:
|
|
227
|
+
pass
|
|
228
|
+
time.sleep(retry_ms / 1000)
|
|
229
|
+
continue
|
|
230
|
+
try:
|
|
231
|
+
return operation()
|
|
232
|
+
finally:
|
|
233
|
+
os.close(descriptor)
|
|
234
|
+
try:
|
|
235
|
+
os.unlink(lock_file)
|
|
236
|
+
except OSError:
|
|
237
|
+
pass
|
|
238
|
+
die(f"timed out waiting for token cache lock {lock_file}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def clear_token(owner: TokenOwner) -> bool:
|
|
242
|
+
"""Remove the profile's entry, whichever host it was issued for."""
|
|
243
|
+
|
|
244
|
+
def operation() -> bool:
|
|
245
|
+
cache = load_cache()
|
|
246
|
+
if owner["name"] not in cache["tokens"]:
|
|
247
|
+
return False
|
|
248
|
+
del cache["tokens"][owner["name"]]
|
|
249
|
+
_save_cache(cache)
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
return with_cache_lock(operation)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def clear_refresh_token_if_matches(owner: TokenOwner, rejected_token: str) -> bool:
|
|
256
|
+
def operation() -> bool:
|
|
257
|
+
cache = load_cache()
|
|
258
|
+
entry = entry_for(cache, owner)
|
|
259
|
+
if not entry or entry.get("refresh_token") != rejected_token:
|
|
260
|
+
return False
|
|
261
|
+
for name in _siblings(cache, owner, entry.get("grant")):
|
|
262
|
+
if cache["tokens"][name].get("refresh_token") == rejected_token:
|
|
263
|
+
cache["tokens"][name].pop("refresh_token", None)
|
|
264
|
+
entry.pop("refresh_token", None)
|
|
265
|
+
_save_cache(cache)
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
return with_cache_lock(operation)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def store_tokens(
|
|
272
|
+
owner: TokenOwner,
|
|
273
|
+
token_response: dict[str, object],
|
|
274
|
+
token_endpoint: str,
|
|
275
|
+
client_id: str | None = None,
|
|
276
|
+
*,
|
|
277
|
+
replace: bool = False,
|
|
278
|
+
) -> CacheEntry:
|
|
279
|
+
"""Write the profile's entry. `replace` starts it afresh (a login or a pasted
|
|
280
|
+
token); otherwise it updates the entry in place (a refresh)."""
|
|
281
|
+
|
|
282
|
+
def operation() -> CacheEntry:
|
|
283
|
+
cache = load_cache()
|
|
284
|
+
previous = None if replace else entry_for(cache, owner)
|
|
285
|
+
expires_in_raw = token_response.get("expires_in")
|
|
286
|
+
if (
|
|
287
|
+
isinstance(expires_in_raw, bool)
|
|
288
|
+
or not isinstance(expires_in_raw, (int, float))
|
|
289
|
+
or not math.isfinite(float(expires_in_raw))
|
|
290
|
+
):
|
|
291
|
+
expires_in: int | float = 3600
|
|
292
|
+
else:
|
|
293
|
+
expires_in = expires_in_raw
|
|
294
|
+
entry: CacheEntry = previous.copy() if previous is not None else {}
|
|
295
|
+
access_token = token_response.get("access_token")
|
|
296
|
+
if not isinstance(access_token, str):
|
|
297
|
+
die("token response did not include a valid access token")
|
|
298
|
+
entry["host"] = owner["host"]
|
|
299
|
+
entry["access_token"] = access_token
|
|
300
|
+
entry["expires_at"] = int(math.floor(time.time()) + expires_in)
|
|
301
|
+
entry["token_endpoint"] = token_endpoint
|
|
302
|
+
if client_id:
|
|
303
|
+
entry["client_id"] = client_id
|
|
304
|
+
else:
|
|
305
|
+
entry.pop("client_id", None)
|
|
306
|
+
refresh_token = token_response.get("refresh_token")
|
|
307
|
+
if isinstance(refresh_token, str) and refresh_token:
|
|
308
|
+
entry["refresh_token"] = refresh_token
|
|
309
|
+
elif replace:
|
|
310
|
+
entry.pop("refresh_token", None)
|
|
311
|
+
if previous is not None:
|
|
312
|
+
for name in _siblings(cache, owner, previous.get("grant")):
|
|
313
|
+
cache["tokens"][name] = entry.copy()
|
|
314
|
+
cache["tokens"][owner["name"]] = entry
|
|
315
|
+
_save_cache(cache)
|
|
316
|
+
return entry
|
|
317
|
+
|
|
318
|
+
return with_cache_lock(operation)
|
{insightfactory_cli-1.2.0.dev34 → insightfactory_cli-1.3.0.dev39}/src/if_cli/commands/logout.py
RENAMED
|
@@ -14,7 +14,6 @@ LOGOUT_OPTIONS: Options = {
|
|
|
14
14
|
def logout_command(argv: list[str]) -> None:
|
|
15
15
|
values, _positionals = parse_args(argv, options=LOGOUT_OPTIONS)
|
|
16
16
|
profile = get_profile(load_config(), resolve_profile_name(values["profile"]))
|
|
17
|
-
removed = clear_token(profile
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
17
|
+
removed = clear_token(profile)
|
|
18
|
+
target = f"profile '{profile['name']}' ({profile['host']})"
|
|
19
|
+
sys.stdout.write(f"✓ cleared cached token for {target}\n" if removed else f"no cached token for {target}\n")
|