Skip to content

Feat: Genericize the sift-cli config to be used by Python - #781

Open
alexluck-sift wants to merge 4 commits into
mainfrom
claude/sift-cli-profile-config-generic-xdh41t
Open

Feat: Genericize the sift-cli config to be used by Python#781
alexluck-sift wants to merge 4 commits into
mainfrom
claude/sift-cli-profile-config-generic-xdh41t

Conversation

@alexluck-sift

@alexluck-sift alexluck-sift commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

SiftClient now reads the same sift.toml profiles that sift-cli --profile
uses. If you configure an environment once for the CLI, the Python client uses
it with no arguments.

client = SiftClient()                    # the CLI's default profile
client = SiftClient(profile="staging")   # a named profile

The pytest plugin gains --sift-profile, the sift_profile ini key, and
SIFT_PROFILE.

Precedence

Highest first:

  1. Arguments.
  2. A profile that you name in code.
  3. The SIFT_* environment variables.
  4. The profile that SIFT_PROFILE names.
  5. The config file's default table.

client.credential_sources reports 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

  • An http:// URL now connects without TLS instead of failing, because
    transport security follows the scheme of the gRPC URL. This also affects the
    explicit-argument path, but only for http://, which never worked before.
  • api_key is now the canonical config key. apikey stays valid, so you do
    not need to migrate.

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
@alexluck-sift alexluck-sift changed the title Feat: Genericizie the sift-cli config to be used by Python Feat: Genericize the sift-cli config to be used by Python Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Python docs preview: https://sift-stack.github.io/sift/python/pr-781/

Deployed from d6bfab7. The link may take up to a minute to become live as GitHub Pages propagates.

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
alexluck-sift marked this pull request as ready for review September 4, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants