selectel-sm 0.1.0__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.
- selectel_sm-0.1.0/.claude/settings.local.json +13 -0
- selectel_sm-0.1.0/.gitignore +223 -0
- selectel_sm-0.1.0/LICENSE +21 -0
- selectel_sm-0.1.0/PKG-INFO +204 -0
- selectel_sm-0.1.0/README.md +160 -0
- selectel_sm-0.1.0/pyproject.toml +69 -0
- selectel_sm-0.1.0/selectel_sm/__init__.py +60 -0
- selectel_sm-0.1.0/selectel_sm/_core/__init__.py +1 -0
- selectel_sm-0.1.0/selectel_sm/_core/encoding.py +31 -0
- selectel_sm-0.1.0/selectel_sm/_core/errors.py +57 -0
- selectel_sm-0.1.0/selectel_sm/_core/keystone.py +77 -0
- selectel_sm-0.1.0/selectel_sm/_core/request.py +39 -0
- selectel_sm-0.1.0/selectel_sm/_core/timestamps.py +23 -0
- selectel_sm-0.1.0/selectel_sm/_core/urls.py +50 -0
- selectel_sm-0.1.0/selectel_sm/_transport/__init__.py +1 -0
- selectel_sm-0.1.0/selectel_sm/_transport/_common.py +48 -0
- selectel_sm-0.1.0/selectel_sm/_transport/async_.py +68 -0
- selectel_sm-0.1.0/selectel_sm/_transport/sync.py +72 -0
- selectel_sm-0.1.0/selectel_sm/aclient.py +115 -0
- selectel_sm-0.1.0/selectel_sm/auth/__init__.py +9 -0
- selectel_sm-0.1.0/selectel_sm/auth/_cache.py +39 -0
- selectel_sm-0.1.0/selectel_sm/auth/base.py +62 -0
- selectel_sm-0.1.0/selectel_sm/auth/password.py +75 -0
- selectel_sm-0.1.0/selectel_sm/auth/static.py +95 -0
- selectel_sm-0.1.0/selectel_sm/cli/__init__.py +1 -0
- selectel_sm-0.1.0/selectel_sm/cli/app.py +85 -0
- selectel_sm-0.1.0/selectel_sm/client.py +142 -0
- selectel_sm-0.1.0/selectel_sm/config.py +49 -0
- selectel_sm-0.1.0/selectel_sm/exceptions.py +111 -0
- selectel_sm-0.1.0/selectel_sm/models.py +182 -0
- selectel_sm-0.1.0/selectel_sm/py.typed +0 -0
- selectel_sm-0.1.0/selectel_sm/resources/__init__.py +22 -0
- selectel_sm-0.1.0/selectel_sm/resources/models.py +157 -0
- selectel_sm-0.1.0/selectel_sm/resources/secrets.py +246 -0
- selectel_sm-0.1.0/tests/conftest.py +79 -0
- selectel_sm-0.1.0/tests/fixtures/keystone_token.json +8325 -0
- selectel_sm-0.1.0/tests/test_auth.py +96 -0
- selectel_sm-0.1.0/tests/test_catalog.py +39 -0
- selectel_sm-0.1.0/tests/test_cli.py +21 -0
- selectel_sm-0.1.0/tests/test_config.py +26 -0
- selectel_sm-0.1.0/tests/test_encoding.py +20 -0
- selectel_sm-0.1.0/tests/test_exceptions.py +33 -0
- selectel_sm-0.1.0/tests/test_keystone.py +59 -0
- selectel_sm-0.1.0/tests/test_secrets.py +617 -0
- selectel_sm-0.1.0/tests/test_transport.py +107 -0
- selectel_sm-0.1.0/tests/test_urls.py +40 -0
- selectel_sm-0.1.0/uv.lock +462 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(uv --version)",
|
|
5
|
+
"Bash(echo \"uv: $\\(uv --version 2>&1\\)\")",
|
|
6
|
+
"Bash(git -C /home/flacy/prj/selectel-sm remote -v)",
|
|
7
|
+
"Bash(uv sync *)",
|
|
8
|
+
"Bash(uv run *)",
|
|
9
|
+
"Bash(uv build *)",
|
|
10
|
+
"Bash(python3 -c \"import zipfile,glob; w=sorted\\(glob.glob\\('dist/*.whl'\\)\\)[-1]; print\\(w\\); [print\\(' ',n\\) for n in zipfile.ZipFile\\(w\\).namelist\\(\\) if n.endswith\\(\\('.py','py.typed'\\)\\)]\")"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
219
|
+
|
|
220
|
+
# Local-only real API responses (contain real account/project IDs and secret names).
|
|
221
|
+
# Sanitized/representative copies live under tests/fixtures/.
|
|
222
|
+
/response.json
|
|
223
|
+
/response-list.json
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Krylov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: selectel-sm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for Selectel Secrets Manager (sync + async)
|
|
5
|
+
Project-URL: Homepage, https://github.com/Flacy/selectel-sm
|
|
6
|
+
Project-URL: Repository, https://github.com/Flacy/selectel-sm
|
|
7
|
+
Author-email: Andrew Krylov <luntiqes@gmail.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Andrew Krylov
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: httpx,secrets,secrets-manager,selectel,vault
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
36
|
+
Classifier: Topic :: Security
|
|
37
|
+
Classifier: Typing :: Typed
|
|
38
|
+
Requires-Python: >=3.12
|
|
39
|
+
Requires-Dist: httpx>=0.27
|
|
40
|
+
Provides-Extra: cli
|
|
41
|
+
Requires-Dist: rich>=13.0; extra == 'cli'
|
|
42
|
+
Requires-Dist: typer>=0.12; extra == 'cli'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# selectel-sm
|
|
46
|
+
|
|
47
|
+
A typed Python client for [Selectel Secrets Manager](https://docs.selectel.ru/api/secrets-manager/),
|
|
48
|
+
with both **synchronous and asynchronous** clients sharing a single transport-agnostic core.
|
|
49
|
+
|
|
50
|
+
> ## ⚠️ Disclaimer
|
|
51
|
+
>
|
|
52
|
+
> This is a **non-commercial, community project built out of pure enthusiasm**. I am **not** an
|
|
53
|
+
> employee of Selectel and have no affiliation with them whatsoever. I built this simply because
|
|
54
|
+
> I couldn't find a maintained library for working with Selectel's Secrets Manager.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install selectel-sm # library
|
|
60
|
+
pip install "selectel-sm[cli]" # + CLI (typer + rich)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires Python 3.12+.
|
|
64
|
+
|
|
65
|
+
## Authentication
|
|
66
|
+
|
|
67
|
+
Secrets Manager requires a **project-scoped** IAM token. (The public docs mention an
|
|
68
|
+
account-scoped token, but in practice SM rejects it — a project-scoped token is required.) The
|
|
69
|
+
library obtains one for you from service-user credentials, caches it, and refreshes it before
|
|
70
|
+
expiry:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from selectel_sm import SecretsManagerClient
|
|
74
|
+
|
|
75
|
+
with SecretsManagerClient.from_credentials(
|
|
76
|
+
region="ru-7",
|
|
77
|
+
account_id="123456",
|
|
78
|
+
username="my-service-user",
|
|
79
|
+
password="...",
|
|
80
|
+
project_name="my-project",
|
|
81
|
+
) as client:
|
|
82
|
+
secret = client.secrets.get("database_password")
|
|
83
|
+
print(secret.value) # -> b"..."
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or bring your own project-scoped token (the client introspects it to discover the service
|
|
87
|
+
catalog):
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
client = SecretsManagerClient.from_token(region="ru-7", token="gAAAAAB...")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The async client mirrors the same API:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from selectel_sm import AsyncSecretsManagerClient
|
|
97
|
+
|
|
98
|
+
async with AsyncSecretsManagerClient.from_credentials(region="ru-7", ...) as client:
|
|
99
|
+
secret = await client.secrets.get("database_password")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Endpoint resolution
|
|
103
|
+
|
|
104
|
+
The Secrets Manager URL is **not hardcoded**. After authenticating, the library reads the
|
|
105
|
+
service catalog returned in the Keystone token and resolves the `secrets-manager` endpoint for
|
|
106
|
+
the configured `region` and `interface` (default `public`). Set `sm_base_url` on the client to
|
|
107
|
+
bypass catalog resolution (e.g. for testing).
|
|
108
|
+
|
|
109
|
+
## Usage
|
|
110
|
+
|
|
111
|
+
All operations live under `client.secrets` (and identically on the async client with `await`).
|
|
112
|
+
|
|
113
|
+
### Secrets
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# Create a secret with its first version. `value` is plain data (bytes or str);
|
|
117
|
+
# it is base64-encoded for you before being sent.
|
|
118
|
+
client.secrets.create("api_key", "s3cr3t", description="Third-party API key")
|
|
119
|
+
|
|
120
|
+
# Read the current value.
|
|
121
|
+
secret = client.secrets.get("api_key")
|
|
122
|
+
secret.value # b"s3cr3t"
|
|
123
|
+
secret.description # "Third-party API key" ("" from the API becomes None)
|
|
124
|
+
secret.version # the current SecretVersion
|
|
125
|
+
|
|
126
|
+
# Update / clear the description (None clears it).
|
|
127
|
+
client.secrets.update_description("api_key", "Rotated key")
|
|
128
|
+
|
|
129
|
+
# List all secrets (metadata only — no values).
|
|
130
|
+
for summary in client.secrets.list():
|
|
131
|
+
print(summary.name, summary.type, summary.description, summary.created_at)
|
|
132
|
+
|
|
133
|
+
# Delete a secret and all of its versions.
|
|
134
|
+
client.secrets.delete("api_key")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Versions
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# Add a new version. Pass activate=True to make it the current version.
|
|
141
|
+
client.secrets.create_version("api_key", "rotated-secret", activate=True)
|
|
142
|
+
|
|
143
|
+
# The secret plus metadata for all of its versions (no values).
|
|
144
|
+
sv = client.secrets.get_versions("api_key")
|
|
145
|
+
sv.versions # tuple[SecretVersion, ...]
|
|
146
|
+
sv.current # the version flagged is_current, if any
|
|
147
|
+
|
|
148
|
+
# A single version, including its value.
|
|
149
|
+
version = client.secrets.get_version("api_key", version_id=1)
|
|
150
|
+
version.value # b"..."
|
|
151
|
+
|
|
152
|
+
# Make a specific version current.
|
|
153
|
+
client.secrets.activate_version("api_key", version_id=1)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Error handling
|
|
157
|
+
|
|
158
|
+
Every error derives from `SelectelSMError`, so you can catch broadly or narrowly:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from selectel_sm import NotFoundError, SelectelSMError
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
client.secrets.get("does-not-exist")
|
|
165
|
+
except NotFoundError:
|
|
166
|
+
...
|
|
167
|
+
except SelectelSMError:
|
|
168
|
+
...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
HTTP statuses map to `BadRequestError` (400), `ForbiddenError` (403), `NotFoundError` (404),
|
|
172
|
+
`ConflictError` (409), and `ServerError` (5xx). Authentication and endpoint-resolution problems
|
|
173
|
+
raise `AuthenticationError` and `EndpointNotFoundError` respectively.
|
|
174
|
+
|
|
175
|
+
## A note on quirks
|
|
176
|
+
|
|
177
|
+
Selectel's Secrets Manager API has a few undocumented behaviors this client handles for you,
|
|
178
|
+
for example:
|
|
179
|
+
|
|
180
|
+
- Listing requires a `?list=<any value>` flag **and** a trailing slash (`/v1/?list=true`),
|
|
181
|
+
otherwise it returns `404 page not found`.
|
|
182
|
+
- Secret values must be valid base64 — the client always encodes plain input for you.
|
|
183
|
+
- `activate version` is documented as `204 No Content` but actually returns `200` with the
|
|
184
|
+
version's metadata.
|
|
185
|
+
|
|
186
|
+
## Support & maintenance
|
|
187
|
+
|
|
188
|
+
Because I don't work with Selectel, **I may not be aware of the latest changes to their API, and
|
|
189
|
+
something could break unexpectedly.** I do my best to keep this library up to date, but that
|
|
190
|
+
isn't always possible. Contributions, bug reports, and help with its development are very
|
|
191
|
+
welcome — please open an issue or a pull request.
|
|
192
|
+
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
uv sync
|
|
197
|
+
uv run ruff check . && uv run ruff format --check .
|
|
198
|
+
uv run mypy selectel_sm
|
|
199
|
+
uv run pytest
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
[MIT](LICENSE) © Andrew Krylov
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# selectel-sm
|
|
2
|
+
|
|
3
|
+
A typed Python client for [Selectel Secrets Manager](https://docs.selectel.ru/api/secrets-manager/),
|
|
4
|
+
with both **synchronous and asynchronous** clients sharing a single transport-agnostic core.
|
|
5
|
+
|
|
6
|
+
> ## ⚠️ Disclaimer
|
|
7
|
+
>
|
|
8
|
+
> This is a **non-commercial, community project built out of pure enthusiasm**. I am **not** an
|
|
9
|
+
> employee of Selectel and have no affiliation with them whatsoever. I built this simply because
|
|
10
|
+
> I couldn't find a maintained library for working with Selectel's Secrets Manager.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install selectel-sm # library
|
|
16
|
+
pip install "selectel-sm[cli]" # + CLI (typer + rich)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Python 3.12+.
|
|
20
|
+
|
|
21
|
+
## Authentication
|
|
22
|
+
|
|
23
|
+
Secrets Manager requires a **project-scoped** IAM token. (The public docs mention an
|
|
24
|
+
account-scoped token, but in practice SM rejects it — a project-scoped token is required.) The
|
|
25
|
+
library obtains one for you from service-user credentials, caches it, and refreshes it before
|
|
26
|
+
expiry:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from selectel_sm import SecretsManagerClient
|
|
30
|
+
|
|
31
|
+
with SecretsManagerClient.from_credentials(
|
|
32
|
+
region="ru-7",
|
|
33
|
+
account_id="123456",
|
|
34
|
+
username="my-service-user",
|
|
35
|
+
password="...",
|
|
36
|
+
project_name="my-project",
|
|
37
|
+
) as client:
|
|
38
|
+
secret = client.secrets.get("database_password")
|
|
39
|
+
print(secret.value) # -> b"..."
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or bring your own project-scoped token (the client introspects it to discover the service
|
|
43
|
+
catalog):
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
client = SecretsManagerClient.from_token(region="ru-7", token="gAAAAAB...")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The async client mirrors the same API:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from selectel_sm import AsyncSecretsManagerClient
|
|
53
|
+
|
|
54
|
+
async with AsyncSecretsManagerClient.from_credentials(region="ru-7", ...) as client:
|
|
55
|
+
secret = await client.secrets.get("database_password")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Endpoint resolution
|
|
59
|
+
|
|
60
|
+
The Secrets Manager URL is **not hardcoded**. After authenticating, the library reads the
|
|
61
|
+
service catalog returned in the Keystone token and resolves the `secrets-manager` endpoint for
|
|
62
|
+
the configured `region` and `interface` (default `public`). Set `sm_base_url` on the client to
|
|
63
|
+
bypass catalog resolution (e.g. for testing).
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
All operations live under `client.secrets` (and identically on the async client with `await`).
|
|
68
|
+
|
|
69
|
+
### Secrets
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# Create a secret with its first version. `value` is plain data (bytes or str);
|
|
73
|
+
# it is base64-encoded for you before being sent.
|
|
74
|
+
client.secrets.create("api_key", "s3cr3t", description="Third-party API key")
|
|
75
|
+
|
|
76
|
+
# Read the current value.
|
|
77
|
+
secret = client.secrets.get("api_key")
|
|
78
|
+
secret.value # b"s3cr3t"
|
|
79
|
+
secret.description # "Third-party API key" ("" from the API becomes None)
|
|
80
|
+
secret.version # the current SecretVersion
|
|
81
|
+
|
|
82
|
+
# Update / clear the description (None clears it).
|
|
83
|
+
client.secrets.update_description("api_key", "Rotated key")
|
|
84
|
+
|
|
85
|
+
# List all secrets (metadata only — no values).
|
|
86
|
+
for summary in client.secrets.list():
|
|
87
|
+
print(summary.name, summary.type, summary.description, summary.created_at)
|
|
88
|
+
|
|
89
|
+
# Delete a secret and all of its versions.
|
|
90
|
+
client.secrets.delete("api_key")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Versions
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Add a new version. Pass activate=True to make it the current version.
|
|
97
|
+
client.secrets.create_version("api_key", "rotated-secret", activate=True)
|
|
98
|
+
|
|
99
|
+
# The secret plus metadata for all of its versions (no values).
|
|
100
|
+
sv = client.secrets.get_versions("api_key")
|
|
101
|
+
sv.versions # tuple[SecretVersion, ...]
|
|
102
|
+
sv.current # the version flagged is_current, if any
|
|
103
|
+
|
|
104
|
+
# A single version, including its value.
|
|
105
|
+
version = client.secrets.get_version("api_key", version_id=1)
|
|
106
|
+
version.value # b"..."
|
|
107
|
+
|
|
108
|
+
# Make a specific version current.
|
|
109
|
+
client.secrets.activate_version("api_key", version_id=1)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Error handling
|
|
113
|
+
|
|
114
|
+
Every error derives from `SelectelSMError`, so you can catch broadly or narrowly:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from selectel_sm import NotFoundError, SelectelSMError
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
client.secrets.get("does-not-exist")
|
|
121
|
+
except NotFoundError:
|
|
122
|
+
...
|
|
123
|
+
except SelectelSMError:
|
|
124
|
+
...
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
HTTP statuses map to `BadRequestError` (400), `ForbiddenError` (403), `NotFoundError` (404),
|
|
128
|
+
`ConflictError` (409), and `ServerError` (5xx). Authentication and endpoint-resolution problems
|
|
129
|
+
raise `AuthenticationError` and `EndpointNotFoundError` respectively.
|
|
130
|
+
|
|
131
|
+
## A note on quirks
|
|
132
|
+
|
|
133
|
+
Selectel's Secrets Manager API has a few undocumented behaviors this client handles for you,
|
|
134
|
+
for example:
|
|
135
|
+
|
|
136
|
+
- Listing requires a `?list=<any value>` flag **and** a trailing slash (`/v1/?list=true`),
|
|
137
|
+
otherwise it returns `404 page not found`.
|
|
138
|
+
- Secret values must be valid base64 — the client always encodes plain input for you.
|
|
139
|
+
- `activate version` is documented as `204 No Content` but actually returns `200` with the
|
|
140
|
+
version's metadata.
|
|
141
|
+
|
|
142
|
+
## Support & maintenance
|
|
143
|
+
|
|
144
|
+
Because I don't work with Selectel, **I may not be aware of the latest changes to their API, and
|
|
145
|
+
something could break unexpectedly.** I do my best to keep this library up to date, but that
|
|
146
|
+
isn't always possible. Contributions, bug reports, and help with its development are very
|
|
147
|
+
welcome — please open an issue or a pull request.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
uv sync
|
|
153
|
+
uv run ruff check . && uv run ruff format --check .
|
|
154
|
+
uv run mypy selectel_sm
|
|
155
|
+
uv run pytest
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
[MIT](LICENSE) © Andrew Krylov
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "selectel-sm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for Selectel Secrets Manager (sync + async)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Andrew Krylov", email = "luntiqes@gmail.com" }]
|
|
13
|
+
keywords = ["selectel", "secrets-manager", "secrets", "vault", "httpx"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Security",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["httpx>=0.27"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
cli = ["typer>=0.12", "rich>=13.0"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/Flacy/selectel-sm"
|
|
30
|
+
Repository = "https://github.com/Flacy/selectel-sm"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
selectel-sm = "selectel_sm.cli.app:app"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"mypy>=1.11",
|
|
38
|
+
"ruff>=0.6",
|
|
39
|
+
"pytest>=8.0",
|
|
40
|
+
"pytest-asyncio>=0.24",
|
|
41
|
+
"assertpy>=1.1",
|
|
42
|
+
# CLI deps are needed to type-check and test the cli package.
|
|
43
|
+
"typer>=0.12",
|
|
44
|
+
"rich>=13.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["selectel_sm"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 99
|
|
52
|
+
target-version = "py312"
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "TCH", "RUF"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint.per-file-ignores]
|
|
58
|
+
"tests/*" = ["B011"]
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.12"
|
|
62
|
+
strict = true
|
|
63
|
+
warn_unused_ignores = true
|
|
64
|
+
warn_redundant_casts = true
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
asyncio_mode = "auto"
|
|
69
|
+
addopts = "-ra"
|