Feat: Genericize the sift-cli config to be used by Python - #781
Open
alexluck-sift wants to merge 4 commits into
Open
Feat: Genericize the sift-cli config to be used by Python#781alexluck-sift wants to merge 4 commits into
alexluck-sift wants to merge 4 commits into
Conversation
sift-cli keeps per-environment credentials in a sift.toml and selects between them with --profile. Nothing else could read that file, so every SDK, script, and service re-derived credentials its own way and switching environments meant switching it in several places. Add a resolver that reads the same file and wire it into the two Python consumers. SiftClient() now resolves credentials when no connection_config is given, so it connects where sift-cli does with no arguments. SiftClient(profile=) and SiftClient.from_profile() select a named profile. The explicit-argument and connection_config paths are unchanged. credential_sources and profile report which layer supplied each value. The pytest plugin gains --sift-profile, the sift_profile ini key, and SIFT_PROFILE, as one PLUGIN_OPTIONS entry. The plugin's existing surfaces still outrank the profile, which fills only what they leave unset, so a key injected by CI is never overridden by a profile on the runner. This is the one place the precedence differs from SiftClient, and both are documented. Resolution order for SiftClient, highest first: inline arguments, the fields of a profile named in code, the per-field environment variables, the fields of the profile named by SIFT_PROFILE, then the config file's default table. Naming a profile is explicit so it beats ambient environment variables; SIFT_PROFILE is ambient so it does not, which keeps the CI case of profile endpoints plus an injected key working. At most one profile table is ever read, and a named profile does not inherit from the default table, matching sift-cli. Transport security now follows the gRPC URL's scheme. sift_py strips the scheme and decides plaintext vs TLS from use_ssl alone, so a profile's http://localhost:50051 would otherwise be dialed over TLS. This changes behavior only for http:// URLs, which could not work before. The config directory is hand-rolled to match Rust's dirs::config_dir(), which is what sift-cli uses. The current working directory is not searched, so a sift.toml in a cloned repository cannot supply an API key. Deferred deliberately: apikey_env and apikey_command belong in the shared schema alongside the Rust reader, and adding them here alone would recreate the drift this change exists to remove. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0151sUrwsupXb4c2vdAQBcuV
Registry: Option gains a `surfaces` field declaring per-option precedence, and resolve_with_source walks it instead of a hardcoded env > cli > ini > toml. PROFILE_OPTION declares cli first, so the _resolve_profile workaround in pytest_plugin.py deletes. This also fixes a real disagreement: the audit log's settings snapshot goes through resolve_with_source, so with SIFT_PROFILE=staging in the shell and --sift-profile other on the command line, the run used `other` while the snapshot recorded `staging (env)`. The audit log is what people read when a run hits the wrong environment. Env var names now live once, in _internal/credentials.py, and options.py imports them. warn_on_unknown_env_vars unions CREDENTIAL_ENV_VARS with the registry, so SIFT_CONFIG_FILE no longer warns that a variable the run obeyed was "ignored" — it was reported as a typo on every plugin test, which is most of the drop in the suite's warning count. Resolver: Layers are normalized to field-keyed dicts before the precedence walk, so the loop no longer re-derives which key spelling to read from a string tag. The two possible layer orders are stated directly rather than built by conditional appends. _select_profile returns a bool instead of a string tag that encoded that bool. Dropped ResolvedCredentials.config_path, which nothing read. Reused the tomllib/tomli shim from pyproject_config rather than copying it. Client: The resolved branch now builds a SiftConnectionConfig and falls through to the shared GrpcClient/RestClient construction, instead of hand-building GrpcConfig and RestConfig a second time. Public surface: sift_client/credentials.py is trimmed to the two documented names, and client.py and pytest_plugin.py import through it, so there is one import path rather than two. Tests: The config-file isolation fixture moves to the package-wide conftest; any test constructing SiftClient without a connection_config would otherwise read the developer's real ~/.config/sift.toml. Four near-identical pytester tests collapse into one parametrized case. Added a regression test that SIFT_CONFIG_FILE is not reported as an unknown variable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0151sUrwsupXb4c2vdAQBcuV
The Sift API spells it api_key throughout: api_keys.proto, api_key_id, ApiKey, /api/v2/api-keys. The one-word apikey exists only in the hand-written channel-config structs of the three first-generation SDKs (sift_py, Go, sift_connect), and sift.toml inherited it from sift_connect, which defined the file format. sift_client and the C++ SDK went back to the proto spelling, which is what made the inconsistency visible. Reconcile on api_key, and accept apikey permanently: real users have files on disk with the old key and should never have to migrate. sift_cli reads either spelling, canonical first, and reports the canonical one when the key is missing. `config update` writes api_key and removes a legacy apikey from the profile it touches, so no profile ends up carrying both. Other profiles are left alone. sift_connect accepts either spelling. Its two profile branches, which duplicated the same lookups, collapse into one table lookup plus a shared helper, so the alias is expressed once rather than four times. The Python resolver's TOML-key map already existed, so each field now carries its accepted spellings with the canonical one first. One behavior change in sift_connect: an empty-string uri or api_key is now treated as absent and reported as missing, rather than accepted and failing later at connect time. This matches what sift_cli already did. Not changed: the `uri` key, which needs restructuring into grpc_uri and rest_uri rather than a rename, and the Rust `Credentials::Config` struct field, which is public API. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0151sUrwsupXb4c2vdAQBcuV
Contributor
|
Python docs preview: https://sift-stack.github.io/sift/python/pr-781/ Deployed from |
Rewrite the user-facing prose added on this branch to ASD-STE100 style: the credentials guide, the changelog entry, the docstrings and comments in the resolver and its two consumers, and the Rust doc comments on the config keys. What changed, by rule: no semicolons, no contractions, and no em dashes. Active voice, so a sentence names the actor that reads the file or reports the error. Simple verb forms in place of gerunds and the present perfect. `can` and `must` in place of `may` and `would`. Descriptive sentences under 25 words, with the precedence enumeration in the changelog moved to a vertical list. One term per concept: the older key spelling is "older", not "legacy", and a precedence winner "outranks" rather than "wins" or "beats". Conditions now come before their commands, and the two rationale notes that interrupted procedures are marked `Note:`. The profile option's help text is reworded, so the generated settings table in the pytest plugin docs is regenerated to match. Pre-existing prose in these files is left alone, since rewriting it is a separate pass with its own review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0151sUrwsupXb4c2vdAQBcuV
alexluck-sift
marked this pull request as ready for review
September 4, 2026 20:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SiftClientnow reads the samesift.tomlprofiles thatsift-cli --profileuses. If you configure an environment once for the CLI, the Python client uses
it with no arguments.
The pytest plugin gains
--sift-profile, thesift_profileini key, andSIFT_PROFILE.Precedence
Highest first:
SIFT_*environment variables.SIFT_PROFILEnames.client.credential_sourcesreports the layer that supplied each value.The pytest plugin reverses this. There the profile ranks below the plugin's own
surfaces, so a profile on the runner cannot replace a key that CI injects.
Behavior changes
http://URL now connects without TLS instead of failing, becausetransport security follows the scheme of the gRPC URL. This also affects the
explicit-argument path, but only for
http://, which never worked before.api_keyis now the canonical config key.apikeystays valid, so you donot need to migrate.