Python client for Selectel Secrets Manager (sync + async)
Project description
selectel-sm
A typed Python client for Selectel Secrets Manager, with both synchronous and asynchronous clients sharing a single transport-agnostic core.
⚠️ Disclaimer
This is a non-commercial, community project built out of pure enthusiasm. I am not an employee of Selectel and have no affiliation with them whatsoever. I built this simply because I couldn't find a maintained library for working with Selectel's Secrets Manager.
Installation
pip install selectel-sm # library
pip install "selectel-sm[cli]" # + CLI (typer + rich)
Requires Python 3.12+.
Authentication
Secrets Manager requires a project-scoped IAM token. (The public docs mention an account-scoped token, but in practice SM rejects it — a project-scoped token is required.) The library obtains one for you from service-user credentials, caches it, and refreshes it before expiry:
from selectel_sm import SecretsManagerClient
with SecretsManagerClient.from_credentials(
region="ru-7",
account_id="123456",
username="my-service-user",
password="...",
project_name="my-project",
) as client:
secret = client.secrets.get("database_password")
print(secret.value) # -> b"..."
Or bring your own project-scoped token (the client introspects it to discover the service catalog):
client = SecretsManagerClient.from_token(region="ru-7", token="gAAAAAB...")
The async client mirrors the same API:
from selectel_sm import AsyncSecretsManagerClient
async with AsyncSecretsManagerClient.from_credentials(region="ru-7", ...) as client:
secret = await client.secrets.get("database_password")
Endpoint resolution
The Secrets Manager URL is not hardcoded. After authenticating, the library reads the
service catalog returned in the Keystone token and resolves the secrets-manager endpoint for
the configured region and interface (default public). Set sm_base_url on the client to
bypass catalog resolution (e.g. for testing).
Usage
All operations live under client.secrets (and identically on the async client with await).
Secrets
# Create a secret with its first version. `value` is plain data (bytes or str);
# it is base64-encoded for you before being sent.
client.secrets.create("api_key", "s3cr3t", description="Third-party API key")
# Read the current value.
secret = client.secrets.get("api_key")
secret.value # b"s3cr3t"
secret.description # "Third-party API key" ("" from the API becomes None)
secret.version # the current SecretVersion
# Update / clear the description (None clears it).
client.secrets.update_description("api_key", "Rotated key")
# List all secrets (metadata only — no values).
for summary in client.secrets.list():
print(summary.name, summary.type, summary.description, summary.created_at)
# Delete a secret and all of its versions.
client.secrets.delete("api_key")
Versions
# Add a new version. Pass activate=True to make it the current version.
client.secrets.create_version("api_key", "rotated-secret", activate=True)
# The secret plus metadata for all of its versions (no values).
sv = client.secrets.get_versions("api_key")
sv.versions # tuple[SecretVersion, ...]
sv.current # the version flagged is_current, if any
# A single version, including its value.
version = client.secrets.get_version("api_key", version_id=1)
version.value # b"..."
# Make a specific version current.
client.secrets.activate_version("api_key", version_id=1)
Error handling
Every error derives from SelectelSMError, so you can catch broadly or narrowly:
from selectel_sm import NotFoundError, SelectelSMError
try:
client.secrets.get("does-not-exist")
except NotFoundError:
...
except SelectelSMError:
...
HTTP statuses map to BadRequestError (400), ForbiddenError (403), NotFoundError (404),
ConflictError (409), and ServerError (5xx). Authentication and endpoint-resolution problems
raise AuthenticationError and EndpointNotFoundError respectively.
Command-line interface
Installing the [cli] extra adds a selectel-sm command (built on typer + rich). It is
designed for humans on a developer machine, but degrades gracefully into scripted/CI use.
pip install "selectel-sm[cli]"
Authentication & profiles
Log in once to create a profile. Non-secret context (region, project, username, …) is stored
in a TOML config at $XDG_CONFIG_HOME/selectel-sm/config.toml; the password and cached token
live only in your OS keyring — never on disk.
selectel-sm login --region ru-7 --account-id 123456 \
--project my-project --username my-service-user # prompts for the password
selectel-sm whoami # active profile + cached-token status
selectel-sm profile list # all profiles (marks the default)
selectel-sm profile use prod # switch the default profile
selectel-sm logout # clears keyring secrets, keeps profile metadata
Each profile chooses a persistence policy: keyring (credentials + auto-refreshed token in the
OS keyring — convenient, the default for a workstation) or none (nothing persisted; credentials
come from the environment or a prompt — correct for servers/CI). --no-store (or
SELECTEL_SM_NO_STORE=1) forces "persist nothing" for a single run.
For zero-config automation, set SELECTEL_SM_* environment variables and skip login entirely —
an ephemeral, non-persisting profile is synthesized from the environment:
export SELECTEL_SM_REGION=ru-7
export SELECTEL_SM_TOKEN=gAAAAAB... # or USERNAME/PASSWORD/ACCOUNT_ID/PROJECT
selectel-sm secrets list
Working with secrets
selectel-sm secrets list # metadata only — never values [-o table|json]
selectel-sm secrets create api_key --stdin # value via --stdin, --file, or a hidden prompt
selectel-sm secrets set-description api_key "Rotated key"
selectel-sm secrets delete api_key --yes # destructive → confirmation (or --yes)
Reading a value is deliberately guarded so it never lands in your terminal/logs by accident:
selectel-sm secrets get api_key # metadata + a MASK (••••••) — no value
selectel-sm secrets get api_key --reveal # show the value
selectel-sm secrets get api_key --copy # copy to the clipboard, print nothing
export API_KEY=$(selectel-sm secrets get api_key --raw) # raw bytes to stdout, no newline
selectel-sm secrets get api_key -o json --reveal # value as base64 (safe to log w/o --reveal)
The value is never a positional argument (it would leak into shell history). Versions are
managed under a subgroup; reading a specific version's value still goes through get:
selectel-sm secrets version list api_key # all versions (marks the current one)
selectel-sm secrets version add api_key --activate --file ./new-value
selectel-sm secrets version activate api_key 2
selectel-sm secrets get api_key --version 2 --reveal
Scripting
Errors print to stderr; machine output and secret values go to stdout, so pipes stay
clean. The exit code reflects the failure: 3 auth, 4 not found, 5 forbidden, 6 conflict,
7 server, 8 bad request, 9 endpoint resolution, 10 network, 2 usage, 1 other.
Confirmations fail closed: a destructive command in a non-interactive shell errors out unless
--yes is given (it never hangs on a prompt or deletes silently).
A note on quirks
Selectel's Secrets Manager API has a few undocumented behaviors this client handles for you, for example:
- Listing requires a
?list=<any value>flag and a trailing slash (/v1/?list=true), otherwise it returns404 page not found. - Secret values must be valid base64 — the client always encodes plain input for you.
activate versionis documented as204 No Contentbut actually returns200with the version's metadata.
Support & maintenance
Because I don't work with Selectel, I may not be aware of the latest changes to their API, and something could break unexpectedly. I do my best to keep this library up to date, but that isn't always possible. Contributions, bug reports, and help with its development are very welcome — please open an issue or a pull request.
Development
uv sync
uv run ruff check . && uv run ruff format --check .
uv run mypy selectel_sm
uv run pytest
License
MIT © Andrew Krylov
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file selectel_sm-0.2.0.tar.gz.
File metadata
- Download URL: selectel_sm-0.2.0.tar.gz
- Upload date:
- Size: 101.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"x86_64","distro":{"id":"resolute","libc":{"lib":"glibc","version":"2.43"},"name":"Ubuntu","version":"26.04"},"implementation":{"name":"CPython","version":"3.14.4"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.5.5 27 Jan 2026","python":"3.14.4","system":{"name":"Linux","release":"7.0.0-22-generic"}} HTTPX2/2.4.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26b400b33450659cd0bee3d9d05e88c4e05edbfbd19e38359bc20d69841a61ed
|
|
| MD5 |
fcada85f6b91d0ec494ca7b8c33d79bf
|
|
| BLAKE2b-256 |
dcc73a40eaffd0af280da3ad08189d08433c100a057ea7a71282b5d3ee0306d3
|
File details
Details for the file selectel_sm-0.2.0-py3-none-any.whl.
File metadata
- Download URL: selectel_sm-0.2.0-py3-none-any.whl
- Upload date:
- Size: 53.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"x86_64","distro":{"id":"resolute","libc":{"lib":"glibc","version":"2.43"},"name":"Ubuntu","version":"26.04"},"implementation":{"name":"CPython","version":"3.14.4"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.5.5 27 Jan 2026","python":"3.14.4","system":{"name":"Linux","release":"7.0.0-22-generic"}} HTTPX2/2.4.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b106c33b91dd4316ca9a7eb0976762e538ed461c91585b6e494abd755470e4cf
|
|
| MD5 |
fbc6edaca387bbc5547958880e318e9a
|
|
| BLAKE2b-256 |
c98793c71e6b9022d813a427acf184b34df84408916cc0ef7d61f71aa20bc489
|