{"id":50848997,"url":"https://github.com/flacy/selectel-sm","last_synced_at":"2026-06-17T15:01:13.856Z","repository":{"id":364263238,"uuid":"1266831298","full_name":"Flacy/selectel-sm","owner":"Flacy","description":"Selectel Secrets Manager for Python","archived":false,"fork":false,"pushed_at":"2026-06-14T10:55:16.000Z","size":143,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-15T13:25:04.846Z","etag":null,"topics":["secrets","selectel"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Flacy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-12T01:52:52.000Z","updated_at":"2026-06-14T10:55:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Flacy/selectel-sm","commit_stats":null,"previous_names":["flacy/selectel-sm"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/Flacy/selectel-sm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flacy%2Fselectel-sm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flacy%2Fselectel-sm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flacy%2Fselectel-sm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flacy%2Fselectel-sm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Flacy","download_url":"https://codeload.github.com/Flacy/selectel-sm/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flacy%2Fselectel-sm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34408788,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["secrets","selectel"],"created_at":"2026-06-14T12:01:05.108Z","updated_at":"2026-06-16T14:00:40.827Z","avatar_url":"https://github.com/Flacy.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# selectel-sm\n\n[![Tests](https://github.com/Flacy/selectel-sm/actions/workflows/tests.yml/badge.svg)](https://github.com/Flacy/selectel-sm/actions/workflows/tests.yml)\n[![Lint](https://github.com/Flacy/selectel-sm/actions/workflows/lint.yml/badge.svg)](https://github.com/Flacy/selectel-sm/actions/workflows/lint.yml)\n[![codecov](https://codecov.io/gh/Flacy/selectel-sm/branch/main/graph/badge.svg)](https://codecov.io/gh/Flacy/selectel-sm)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/selectel-sm)\n\nA typed Python client for [Selectel Secrets Manager](https://docs.selectel.ru/api/secrets-manager/),\nwith both **synchronous and asynchronous** clients sharing a single transport-agnostic core.\n\n\u003e ## ⚠️ Disclaimer\n\u003e\n\u003e This is a **non-commercial, community project built out of pure enthusiasm**. I am **not** an\n\u003e employee of Selectel and have no affiliation with them whatsoever. I built this simply because\n\u003e I couldn't find a maintained library for working with Selectel's Secrets Manager.\n\n## Installation\n\n```bash\npip install selectel-sm          # library\npip install \"selectel-sm[cli]\"   # + CLI (typer + rich)\n```\n\nRequires Python 3.12+.\n\n## Authentication\n\nSecrets Manager requires a **project-scoped** IAM token. (The public docs mention an\naccount-scoped token, but in practice SM rejects it — a project-scoped token is required.) The\nlibrary obtains one for you from service-user credentials, caches it, and refreshes it before\nexpiry:\n\n```python\nfrom selectel_sm import SecretsManagerClient\n\nwith SecretsManagerClient.from_credentials(\n    region=\"ru-7\",\n    account_id=\"123456\",\n    username=\"my-service-user\",\n    password=\"...\",\n    project_name=\"my-project\",\n) as client:\n    secret = client.secrets.get(\"database_password\")\n    print(secret.value)  # -\u003e b\"...\"\n```\n\nOr bring your own project-scoped token (the client introspects it to discover the service\ncatalog):\n\n```python\nclient = SecretsManagerClient.from_token(region=\"ru-7\", token=\"gAAAAAB...\")\n```\n\nThe async client mirrors the same API:\n\n```python\nfrom selectel_sm import AsyncSecretsManagerClient\n\nasync with AsyncSecretsManagerClient.from_credentials(region=\"ru-7\", ...) as client:\n    secret = await client.secrets.get(\"database_password\")\n```\n\n## Endpoint resolution\n\nThe Secrets Manager URL is **not hardcoded**. After authenticating, the library reads the\nservice catalog returned in the Keystone token and resolves the `secrets-manager` endpoint for\nthe configured `region` and `interface` (default `public`). Set `sm_base_url` on the client to\nbypass catalog resolution (e.g. for testing).\n\n## Usage\n\nAll operations live under `client.secrets` (and identically on the async client with `await`).\n\n### Secrets\n\n```python\n# Create a secret with its first version. `value` is plain data (bytes or str);\n# it is base64-encoded for you before being sent.\nclient.secrets.create(\"api_key\", \"s3cr3t\", description=\"Third-party API key\")\n\n# Read the current value.\nsecret = client.secrets.get(\"api_key\")\nsecret.value          # b\"s3cr3t\"\nsecret.description    # \"Third-party API key\"  (\"\" from the API becomes None)\nsecret.version        # the current SecretVersion\n\n# Update / clear the description (None clears it).\nclient.secrets.update_description(\"api_key\", \"Rotated key\")\n\n# List all secrets (metadata only — no values).\nfor summary in client.secrets.list():\n    print(summary.name, summary.type, summary.description, summary.created_at)\n\n# Delete a secret and all of its versions.\nclient.secrets.delete(\"api_key\")\n```\n\n### Versions\n\n```python\n# Add a new version. Pass activate=True to make it the current version.\nclient.secrets.create_version(\"api_key\", \"rotated-secret\", activate=True)\n\n# The secret plus metadata for all of its versions (no values).\nsv = client.secrets.get_versions(\"api_key\")\nsv.versions           # tuple[SecretVersion, ...]\nsv.current            # the version flagged is_current, if any\n\n# A single version, including its value.\nversion = client.secrets.get_version(\"api_key\", version_id=1)\nversion.value         # b\"...\"\n\n# Make a specific version current.\nclient.secrets.activate_version(\"api_key\", version_id=1)\n```\n\n### Error handling\n\nEvery error derives from `SelectelSMError`, so you can catch broadly or narrowly:\n\n```python\nfrom selectel_sm import NotFoundError, SelectelSMError\n\ntry:\n    client.secrets.get(\"does-not-exist\")\nexcept NotFoundError:\n    ...\nexcept SelectelSMError:\n    ...\n```\n\nHTTP statuses map to `BadRequestError` (400), `ForbiddenError` (403), `NotFoundError` (404),\n`ConflictError` (409), and `ServerError` (5xx). Authentication and endpoint-resolution problems\nraise `AuthenticationError` and `EndpointNotFoundError` respectively.\n\n## Command-line interface\n\nInstalling the `[cli]` extra adds a `selectel-sm` command (built on `typer` + `rich`). It is\ndesigned for humans on a developer machine, but degrades gracefully into scripted/CI use.\n\n```bash\npip install \"selectel-sm[cli]\"\n```\n\n### Authentication \u0026 profiles\n\nLog in once to create a **profile**. Non-secret context (region, project, username, …) is stored\nin a TOML config at `$XDG_CONFIG_HOME/selectel-sm/config.toml`; the **password and cached token\nlive only in your OS keyring** — never on disk.\n\n```bash\nselectel-sm login --region ru-7 --account-id 123456 \\\n    --project my-project --username my-service-user      # prompts for the password\n\nselectel-sm whoami                  # active profile + cached-token status\nselectel-sm profile list            # all profiles (marks the default)\nselectel-sm profile use prod        # switch the default profile\nselectel-sm logout                  # clears keyring secrets, keeps profile metadata\n```\n\nEach profile chooses a persistence policy: `keyring` (credentials + auto-refreshed token in the\nOS keyring — convenient, the default for a workstation) or `none` (nothing persisted; credentials\ncome from the environment or a prompt — correct for servers/CI). `--no-store` (or\n`SELECTEL_SM_NO_STORE=1`) forces \"persist nothing\" for a single run.\n\nFor zero-config automation, set `SELECTEL_SM_*` environment variables and skip `login` entirely —\nan ephemeral, non-persisting profile is synthesized from the environment:\n\n```bash\nexport SELECTEL_SM_REGION=ru-7\nexport SELECTEL_SM_TOKEN=gAAAAAB...            # or USERNAME/PASSWORD/ACCOUNT_ID/PROJECT\nselectel-sm secrets list\n```\n\n### Working with secrets\n\n```bash\nselectel-sm secrets list                       # metadata only — never values   [-o table|json]\nselectel-sm secrets create api_key --stdin     # value via --stdin, --file, or a hidden prompt\nselectel-sm secrets set-description api_key \"Rotated key\"\nselectel-sm secrets delete api_key --yes       # destructive → confirmation (or --yes)\n```\n\nReading a value is deliberately guarded so it never lands in your terminal/logs by accident:\n\n```bash\nselectel-sm secrets get api_key                # metadata + a MASK (••••••) — no value\nselectel-sm secrets get api_key --reveal       # show the value\nselectel-sm secrets get api_key --copy         # copy to the clipboard, print nothing\nexport API_KEY=$(selectel-sm secrets get api_key --raw)   # raw bytes to stdout, no newline\nselectel-sm secrets get api_key -o json --reveal          # value as base64 (safe to log w/o --reveal)\n```\n\nThe value is **never** a positional argument (it would leak into shell history). Versions are\nmanaged under a subgroup; reading a specific version's value still goes through `get`:\n\n```bash\nselectel-sm secrets version list api_key       # all versions (marks the current one)\nselectel-sm secrets version add api_key --activate --file ./new-value\nselectel-sm secrets version activate api_key 2\nselectel-sm secrets get api_key --version 2 --reveal\n```\n\n### Scripting\n\nErrors print to **stderr**; machine output and secret values go to **stdout**, so pipes stay\nclean. The exit code reflects the failure: `3` auth, `4` not found, `5` forbidden, `6` conflict,\n`7` server, `8` bad request, `9` endpoint resolution, `10` network, `2` usage, `1` other.\nConfirmations **fail closed**: a destructive command in a non-interactive shell errors out unless\n`--yes` is given (it never hangs on a prompt or deletes silently).\n\n## A note on quirks\n\nSelectel's Secrets Manager API has a few undocumented behaviors this client handles for you,\nfor example:\n\n- Listing requires a `?list=\u003cany value\u003e` flag **and** a trailing slash (`/v1/?list=true`),\n  otherwise it returns `404 page not found`.\n- Secret values must be valid base64 — the client always encodes plain input for you.\n- `activate version` is documented as `204 No Content` but actually returns `200` with the\n  version's metadata.\n\n## Support \u0026 maintenance\n\nBecause I don't work with Selectel, **I may not be aware of the latest changes to their API, and\nsomething could break unexpectedly.** I do my best to keep this library up to date, but that\nisn't always possible. Contributions, bug reports, and help with its development are very\nwelcome — please open an issue or a pull request.\n\n## Development\n\n```bash\nuv sync\nuv run ruff check . \u0026\u0026 uv run ruff format --check .\nuv run mypy selectel_sm\nuv run pytest\n```\n\n## License\n\n[MIT](LICENSE) © Andrew Krylov\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflacy%2Fselectel-sm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflacy%2Fselectel-sm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflacy%2Fselectel-sm/lists"}