better-python-doppler 2.0.0__tar.gz → 2.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.
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/PKG-INFO +21 -1
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/README.md +20 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/pyproject.toml +1 -1
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/doppler_sdk.py +22 -0
- better_python_doppler-2.1.0/src/better_python_doppler/resources/secrets.py +606 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler.egg-info/PKG-INFO +21 -1
- better_python_doppler-2.0.0/src/better_python_doppler/resources/secrets.py +0 -306
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/LICENSE +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/setup.cfg +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/__init__.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/apis/__init__.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/apis/secret_apis.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/exceptions.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/models/__init__.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/models/secret.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/resources/__init__.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/secret.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/transport.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler.egg-info/SOURCES.txt +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler.egg-info/dependency_links.txt +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler.egg-info/requires.txt +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler.egg-info/top_level.txt +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/tests/test_client.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/tests/test_exceptions.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/tests/test_secrets.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/tests/test_secrets_client_api.py +0 -0
- {better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/tests/test_transport.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: better-python-doppler
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Lightweight, secrets-first Python SDK for the Doppler API.
|
|
5
5
|
Author-email: Derek Banker <dbb2002@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -61,6 +61,7 @@ Supported and documented today:
|
|
|
61
61
|
- direct token auth with `Doppler(service_token="...")`
|
|
62
62
|
- explicit environment lookup with `Doppler(service_token_environ_name="...")`
|
|
63
63
|
- explicit `.env` loading with `Doppler.from_env(...)`
|
|
64
|
+
- client-level scoped defaults with `client.set_scope(project_name, config_name)`
|
|
64
65
|
- secrets access through `client.secrets`
|
|
65
66
|
- compatibility access through `client.Secrets`
|
|
66
67
|
- top-level compatibility exports: `Secrets` and `SecretsClient`
|
|
@@ -137,6 +138,25 @@ raw_value = client.secrets.get_raw("my-project", "dev", "API_KEY")
|
|
|
137
138
|
print(raw_value)
|
|
138
139
|
```
|
|
139
140
|
|
|
141
|
+
### Scoped Defaults
|
|
142
|
+
|
|
143
|
+
If you are repeatedly targeting the same project and config, set a client scope once and omit them from later secrets calls.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from better_python_doppler import Doppler
|
|
147
|
+
|
|
148
|
+
client = Doppler(service_token="dp.st.example-token").set_scope(
|
|
149
|
+
"my-project",
|
|
150
|
+
"dev",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
print(client.secrets.list_names())
|
|
154
|
+
print(client.secrets.get_raw("API_KEY"))
|
|
155
|
+
client.secrets.set("API_KEY", "next-value")
|
|
156
|
+
|
|
157
|
+
client.clear_scope()
|
|
158
|
+
```
|
|
159
|
+
|
|
140
160
|
### Single Secret Set/Get
|
|
141
161
|
|
|
142
162
|
```python
|
|
@@ -34,6 +34,7 @@ Supported and documented today:
|
|
|
34
34
|
- direct token auth with `Doppler(service_token="...")`
|
|
35
35
|
- explicit environment lookup with `Doppler(service_token_environ_name="...")`
|
|
36
36
|
- explicit `.env` loading with `Doppler.from_env(...)`
|
|
37
|
+
- client-level scoped defaults with `client.set_scope(project_name, config_name)`
|
|
37
38
|
- secrets access through `client.secrets`
|
|
38
39
|
- compatibility access through `client.Secrets`
|
|
39
40
|
- top-level compatibility exports: `Secrets` and `SecretsClient`
|
|
@@ -110,6 +111,25 @@ raw_value = client.secrets.get_raw("my-project", "dev", "API_KEY")
|
|
|
110
111
|
print(raw_value)
|
|
111
112
|
```
|
|
112
113
|
|
|
114
|
+
### Scoped Defaults
|
|
115
|
+
|
|
116
|
+
If you are repeatedly targeting the same project and config, set a client scope once and omit them from later secrets calls.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from better_python_doppler import Doppler
|
|
120
|
+
|
|
121
|
+
client = Doppler(service_token="dp.st.example-token").set_scope(
|
|
122
|
+
"my-project",
|
|
123
|
+
"dev",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
print(client.secrets.list_names())
|
|
127
|
+
print(client.secrets.get_raw("API_KEY"))
|
|
128
|
+
client.secrets.set("API_KEY", "next-value")
|
|
129
|
+
|
|
130
|
+
client.clear_scope()
|
|
131
|
+
```
|
|
132
|
+
|
|
113
133
|
### Single Secret Set/Get
|
|
114
134
|
|
|
115
135
|
```python
|
{better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/doppler_sdk.py
RENAMED
|
@@ -20,6 +20,8 @@ class Doppler:
|
|
|
20
20
|
service_token_environ_name,
|
|
21
21
|
)
|
|
22
22
|
self._transport = RequestsTransport(self._service_token)
|
|
23
|
+
self._project_name: str | None = None
|
|
24
|
+
self._config_name: str | None = None
|
|
23
25
|
self._secrets: SecretsClient | None = None
|
|
24
26
|
|
|
25
27
|
@classmethod
|
|
@@ -67,12 +69,32 @@ class Doppler:
|
|
|
67
69
|
def service_token(self) -> str:
|
|
68
70
|
return self._service_token
|
|
69
71
|
|
|
72
|
+
def set_scope(self, project_name: str, config_name: str) -> "Doppler":
|
|
73
|
+
self._project_name = project_name
|
|
74
|
+
self._config_name = config_name
|
|
75
|
+
|
|
76
|
+
if self._secrets is not None:
|
|
77
|
+
self._secrets.set_scope(project_name, config_name)
|
|
78
|
+
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def clear_scope(self) -> "Doppler":
|
|
82
|
+
self._project_name = None
|
|
83
|
+
self._config_name = None
|
|
84
|
+
|
|
85
|
+
if self._secrets is not None:
|
|
86
|
+
self._secrets.clear_scope()
|
|
87
|
+
|
|
88
|
+
return self
|
|
89
|
+
|
|
70
90
|
@property
|
|
71
91
|
def secrets(self) -> SecretsClient:
|
|
72
92
|
if self._secrets is None:
|
|
73
93
|
self._secrets = SecretsClient(
|
|
74
94
|
self._service_token,
|
|
75
95
|
transport=self._transport,
|
|
96
|
+
project_name=self._project_name,
|
|
97
|
+
config_name=self._config_name,
|
|
76
98
|
)
|
|
77
99
|
|
|
78
100
|
return self._secrets
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from requests import Response
|
|
6
|
+
|
|
7
|
+
from better_python_doppler.apis import SecretAPI
|
|
8
|
+
from better_python_doppler.exceptions import DopplerResponseError
|
|
9
|
+
from better_python_doppler.models import SecretModel, SecretValue
|
|
10
|
+
from better_python_doppler.transport import RequestsTransport, SyncTransport
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DownloadFormat = Literal[
|
|
14
|
+
"json",
|
|
15
|
+
"dotnet-json",
|
|
16
|
+
"env",
|
|
17
|
+
"yaml",
|
|
18
|
+
"docker",
|
|
19
|
+
"env-no-quotes",
|
|
20
|
+
]
|
|
21
|
+
NameTransformer = Literal[
|
|
22
|
+
"camel",
|
|
23
|
+
"upper-camel",
|
|
24
|
+
"lower-snake",
|
|
25
|
+
"tf-var",
|
|
26
|
+
"dotnet",
|
|
27
|
+
"dotnet-env",
|
|
28
|
+
"lower-kebab",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SecretsClient:
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
service_token: str,
|
|
36
|
+
*,
|
|
37
|
+
transport: SyncTransport | None = None,
|
|
38
|
+
project_name: str | None = None,
|
|
39
|
+
config_name: str | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._service_token = service_token
|
|
42
|
+
self._transport = transport or RequestsTransport(service_token)
|
|
43
|
+
self._project_name = project_name
|
|
44
|
+
self._config_name = config_name
|
|
45
|
+
|
|
46
|
+
def set_scope(self, project_name: str, config_name: str) -> "SecretsClient":
|
|
47
|
+
self._project_name = project_name
|
|
48
|
+
self._config_name = config_name
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def clear_scope(self) -> "SecretsClient":
|
|
52
|
+
self._project_name = None
|
|
53
|
+
self._config_name = None
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def list(
|
|
57
|
+
self,
|
|
58
|
+
project_name: str | None = None,
|
|
59
|
+
config_name: str | None = None,
|
|
60
|
+
include_dynamic_secrets: bool = True,
|
|
61
|
+
dynamic_secrets_ttl_sec: int = 1800,
|
|
62
|
+
secrets: list[str] | None = None,
|
|
63
|
+
include_managed_secrets: bool = True,
|
|
64
|
+
) -> list[SecretModel]:
|
|
65
|
+
project_name, config_name = self._resolve_project_and_config(
|
|
66
|
+
project_name,
|
|
67
|
+
config_name,
|
|
68
|
+
method_name="list",
|
|
69
|
+
)
|
|
70
|
+
response = SecretAPI.list_secrets(
|
|
71
|
+
transport=self._transport,
|
|
72
|
+
project_name=project_name,
|
|
73
|
+
config_name=config_name,
|
|
74
|
+
include_dynamic_secrets=include_dynamic_secrets,
|
|
75
|
+
dynamic_secrets_ttl_sec=dynamic_secrets_ttl_sec,
|
|
76
|
+
secrets=secrets,
|
|
77
|
+
include_managed_secrets=include_managed_secrets,
|
|
78
|
+
)
|
|
79
|
+
return response_to_models(response)
|
|
80
|
+
|
|
81
|
+
def list_names(
|
|
82
|
+
self,
|
|
83
|
+
project_name: str | None = None,
|
|
84
|
+
config_name: str | None = None,
|
|
85
|
+
include_dynamic_secrets: bool = False,
|
|
86
|
+
include_managed_secrets: bool = True,
|
|
87
|
+
) -> list[str]:
|
|
88
|
+
project_name, config_name = self._resolve_project_and_config(
|
|
89
|
+
project_name,
|
|
90
|
+
config_name,
|
|
91
|
+
method_name="list_names",
|
|
92
|
+
)
|
|
93
|
+
response = SecretAPI.list_secret_names(
|
|
94
|
+
transport=self._transport,
|
|
95
|
+
project_name=project_name,
|
|
96
|
+
config_name=config_name,
|
|
97
|
+
include_dynamic_secrets=include_dynamic_secrets,
|
|
98
|
+
include_managed_secrets=include_managed_secrets,
|
|
99
|
+
)
|
|
100
|
+
data = _json_mapping(response, context="secret names")
|
|
101
|
+
names = data.get("names")
|
|
102
|
+
if names is None:
|
|
103
|
+
return []
|
|
104
|
+
if not isinstance(names, list) or not all(
|
|
105
|
+
isinstance(name, str) for name in names
|
|
106
|
+
):
|
|
107
|
+
raise DopplerResponseError(
|
|
108
|
+
"Doppler secret names response contained an invalid `names` payload."
|
|
109
|
+
)
|
|
110
|
+
return names
|
|
111
|
+
|
|
112
|
+
def get(
|
|
113
|
+
self,
|
|
114
|
+
project_name: str | None = None,
|
|
115
|
+
config_name: str | None = None,
|
|
116
|
+
secret_name: str | None = None,
|
|
117
|
+
) -> SecretModel:
|
|
118
|
+
project_name, config_name, secret_name = (
|
|
119
|
+
self._resolve_project_config_secret_name(
|
|
120
|
+
project_name,
|
|
121
|
+
config_name,
|
|
122
|
+
secret_name,
|
|
123
|
+
method_name="get",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
response = SecretAPI.get_secret(
|
|
127
|
+
transport=self._transport,
|
|
128
|
+
project_name=project_name,
|
|
129
|
+
config_name=config_name,
|
|
130
|
+
secret_name=secret_name,
|
|
131
|
+
)
|
|
132
|
+
return response_to_model(response)
|
|
133
|
+
|
|
134
|
+
def get_raw(
|
|
135
|
+
self,
|
|
136
|
+
project_name: str | None = None,
|
|
137
|
+
config_name: str | None = None,
|
|
138
|
+
secret_name: str | None = None,
|
|
139
|
+
) -> str | None:
|
|
140
|
+
project_name, config_name, secret_name = (
|
|
141
|
+
self._resolve_project_config_secret_name(
|
|
142
|
+
project_name,
|
|
143
|
+
config_name,
|
|
144
|
+
secret_name,
|
|
145
|
+
method_name="get_raw",
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
return self.get(project_name, config_name, secret_name).value.raw
|
|
149
|
+
|
|
150
|
+
def as_dict(
|
|
151
|
+
self,
|
|
152
|
+
project_name: str | None = None,
|
|
153
|
+
config_name: str | None = None,
|
|
154
|
+
include_dynamic_secrets: bool = True,
|
|
155
|
+
dynamic_secrets_ttl_sec: int = 1800,
|
|
156
|
+
secrets: list[str] | None = None,
|
|
157
|
+
include_managed_secrets: bool = True,
|
|
158
|
+
) -> dict[str, str | None]:
|
|
159
|
+
project_name, config_name = self._resolve_project_and_config(
|
|
160
|
+
project_name,
|
|
161
|
+
config_name,
|
|
162
|
+
method_name="as_dict",
|
|
163
|
+
)
|
|
164
|
+
return {
|
|
165
|
+
secret.name: secret.value.raw
|
|
166
|
+
for secret in self.list(
|
|
167
|
+
project_name,
|
|
168
|
+
config_name,
|
|
169
|
+
include_dynamic_secrets=include_dynamic_secrets,
|
|
170
|
+
dynamic_secrets_ttl_sec=dynamic_secrets_ttl_sec,
|
|
171
|
+
secrets=secrets,
|
|
172
|
+
include_managed_secrets=include_managed_secrets,
|
|
173
|
+
)
|
|
174
|
+
if secret.name is not None
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def set(
|
|
178
|
+
self,
|
|
179
|
+
project_name: str | None = None,
|
|
180
|
+
config_name: str | None = None,
|
|
181
|
+
secret_name: str | None = None,
|
|
182
|
+
secret_value: str | None = None,
|
|
183
|
+
) -> SecretModel:
|
|
184
|
+
project_name, config_name, secret_name, secret_value = (
|
|
185
|
+
self._resolve_project_config_secret_value(
|
|
186
|
+
project_name,
|
|
187
|
+
config_name,
|
|
188
|
+
secret_name,
|
|
189
|
+
secret_value,
|
|
190
|
+
method_name="set",
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
return self.set_many(
|
|
194
|
+
project_name,
|
|
195
|
+
config_name,
|
|
196
|
+
{secret_name: secret_value},
|
|
197
|
+
)[0]
|
|
198
|
+
|
|
199
|
+
def set_many(
|
|
200
|
+
self,
|
|
201
|
+
project_name: str | dict[str, str] | None = None,
|
|
202
|
+
config_name: str | None = None,
|
|
203
|
+
secrets: dict[str, str] | None = None,
|
|
204
|
+
) -> list[SecretModel]:
|
|
205
|
+
project_name, config_name, secrets = self._resolve_project_config_secrets(
|
|
206
|
+
project_name,
|
|
207
|
+
config_name,
|
|
208
|
+
secrets,
|
|
209
|
+
method_name="set_many",
|
|
210
|
+
)
|
|
211
|
+
response = SecretAPI.update_secrets(
|
|
212
|
+
transport=self._transport,
|
|
213
|
+
project_name=project_name,
|
|
214
|
+
config_name=config_name,
|
|
215
|
+
secrets=secrets,
|
|
216
|
+
)
|
|
217
|
+
return response_to_models(response)
|
|
218
|
+
|
|
219
|
+
def update(
|
|
220
|
+
self,
|
|
221
|
+
project_name: str | dict[str, str] | None = None,
|
|
222
|
+
config_name: str | None = None,
|
|
223
|
+
secret_name: str | None = None,
|
|
224
|
+
secret_value: str | None = None,
|
|
225
|
+
*,
|
|
226
|
+
secrets: dict[str, str] | None = None,
|
|
227
|
+
) -> list[SecretModel]:
|
|
228
|
+
if (
|
|
229
|
+
isinstance(project_name, dict)
|
|
230
|
+
and config_name is None
|
|
231
|
+
and secret_name is None
|
|
232
|
+
and secret_value is None
|
|
233
|
+
and secrets is None
|
|
234
|
+
and self._project_name is not None
|
|
235
|
+
and self._config_name is not None
|
|
236
|
+
):
|
|
237
|
+
secrets = project_name
|
|
238
|
+
project_name = None
|
|
239
|
+
|
|
240
|
+
project_name_input: str | None
|
|
241
|
+
if isinstance(project_name, dict):
|
|
242
|
+
project_name_input = None
|
|
243
|
+
else:
|
|
244
|
+
project_name_input = project_name
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
isinstance(project_name_input, str)
|
|
248
|
+
and isinstance(config_name, str)
|
|
249
|
+
and secret_name is None
|
|
250
|
+
and secret_value is None
|
|
251
|
+
and secrets is None
|
|
252
|
+
and self._project_name is not None
|
|
253
|
+
and self._config_name is not None
|
|
254
|
+
):
|
|
255
|
+
secret_name = project_name_input
|
|
256
|
+
secret_value = config_name
|
|
257
|
+
project_name_input = None
|
|
258
|
+
config_name = None
|
|
259
|
+
|
|
260
|
+
project_name, config_name = self._resolve_project_and_config(
|
|
261
|
+
project_name_input,
|
|
262
|
+
config_name,
|
|
263
|
+
method_name="update",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if secret_name is not None and secret_value is not None:
|
|
267
|
+
return self.set_many(
|
|
268
|
+
project_name,
|
|
269
|
+
config_name,
|
|
270
|
+
{secret_name: secret_value},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if secrets is not None:
|
|
274
|
+
return self.set_many(project_name, config_name, secrets)
|
|
275
|
+
|
|
276
|
+
raise ValueError(
|
|
277
|
+
"Invalid Parameter: Must provide `secret_name` and `secret_value` or `secrets`."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def download(
|
|
281
|
+
self,
|
|
282
|
+
project_name: str | None = None,
|
|
283
|
+
config_name: str | None = None,
|
|
284
|
+
format: DownloadFormat = "json",
|
|
285
|
+
name_transformer: NameTransformer | None = None,
|
|
286
|
+
include_dynamic_secrets: bool = False,
|
|
287
|
+
dynamic_secrets_ttl_sec: int = 1800,
|
|
288
|
+
secrets: list[str] | None = None,
|
|
289
|
+
) -> dict[str, str] | str:
|
|
290
|
+
project_name, config_name = self._resolve_project_and_config(
|
|
291
|
+
project_name,
|
|
292
|
+
config_name,
|
|
293
|
+
method_name="download",
|
|
294
|
+
)
|
|
295
|
+
response = SecretAPI.download_secrets(
|
|
296
|
+
transport=self._transport,
|
|
297
|
+
project_name=project_name,
|
|
298
|
+
config_name=config_name,
|
|
299
|
+
format=format,
|
|
300
|
+
name_transformer=name_transformer,
|
|
301
|
+
include_dynamic_secrets=include_dynamic_secrets,
|
|
302
|
+
dynamic_secrets_ttl_sec=dynamic_secrets_ttl_sec,
|
|
303
|
+
secrets=secrets or [],
|
|
304
|
+
)
|
|
305
|
+
if format in ["json", "dotnet-json"]:
|
|
306
|
+
return _json_mapping(response, context="downloaded secrets")
|
|
307
|
+
return response.text
|
|
308
|
+
|
|
309
|
+
def delete(
|
|
310
|
+
self,
|
|
311
|
+
project_name: str | None = None,
|
|
312
|
+
config_name: str | None = None,
|
|
313
|
+
secret_name: str | None = None,
|
|
314
|
+
) -> None:
|
|
315
|
+
project_name, config_name, secret_name = (
|
|
316
|
+
self._resolve_project_config_secret_name(
|
|
317
|
+
project_name,
|
|
318
|
+
config_name,
|
|
319
|
+
secret_name,
|
|
320
|
+
method_name="delete",
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
SecretAPI.delete_secret(
|
|
324
|
+
transport=self._transport,
|
|
325
|
+
project_name=project_name,
|
|
326
|
+
config_name=config_name,
|
|
327
|
+
secret_name=secret_name,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def update_note(
|
|
331
|
+
self,
|
|
332
|
+
project_name: str | None = None,
|
|
333
|
+
secret_name: str | None = None,
|
|
334
|
+
note: str | None = None,
|
|
335
|
+
) -> dict[str, object]:
|
|
336
|
+
project_name, secret_name, note = self._resolve_project_secret_note(
|
|
337
|
+
project_name,
|
|
338
|
+
secret_name,
|
|
339
|
+
note,
|
|
340
|
+
method_name="update_note",
|
|
341
|
+
)
|
|
342
|
+
response = SecretAPI.update_note(
|
|
343
|
+
transport=self._transport,
|
|
344
|
+
project_name=project_name,
|
|
345
|
+
secret_name=secret_name,
|
|
346
|
+
note=note,
|
|
347
|
+
)
|
|
348
|
+
return _json_mapping(response, context="secret note update")
|
|
349
|
+
|
|
350
|
+
def _resolve_project_and_config(
|
|
351
|
+
self,
|
|
352
|
+
project_name: str | None,
|
|
353
|
+
config_name: str | None,
|
|
354
|
+
*,
|
|
355
|
+
method_name: str,
|
|
356
|
+
) -> tuple[str, str]:
|
|
357
|
+
resolved_project_name = (
|
|
358
|
+
self._project_name if project_name is None else project_name
|
|
359
|
+
)
|
|
360
|
+
resolved_config_name = (
|
|
361
|
+
self._config_name if config_name is None else config_name
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if resolved_project_name is None or resolved_config_name is None:
|
|
365
|
+
raise TypeError(
|
|
366
|
+
f"{method_name}() requires `project_name` and `config_name` unless a client scope is set."
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return resolved_project_name, resolved_config_name
|
|
370
|
+
|
|
371
|
+
def _resolve_project(
|
|
372
|
+
self,
|
|
373
|
+
project_name: str | None,
|
|
374
|
+
*,
|
|
375
|
+
method_name: str,
|
|
376
|
+
) -> str:
|
|
377
|
+
resolved_project_name = (
|
|
378
|
+
self._project_name if project_name is None else project_name
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if resolved_project_name is None:
|
|
382
|
+
raise TypeError(
|
|
383
|
+
f"{method_name}() requires `project_name` unless a client scope is set."
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return resolved_project_name
|
|
387
|
+
|
|
388
|
+
def _resolve_project_config_secret_name(
|
|
389
|
+
self,
|
|
390
|
+
project_name: str | None,
|
|
391
|
+
config_name: str | None,
|
|
392
|
+
secret_name: str | None,
|
|
393
|
+
*,
|
|
394
|
+
method_name: str,
|
|
395
|
+
) -> tuple[str, str, str]:
|
|
396
|
+
if (
|
|
397
|
+
secret_name is None
|
|
398
|
+
and config_name is None
|
|
399
|
+
and project_name is not None
|
|
400
|
+
and self._project_name is not None
|
|
401
|
+
and self._config_name is not None
|
|
402
|
+
):
|
|
403
|
+
secret_name = project_name
|
|
404
|
+
project_name = None
|
|
405
|
+
|
|
406
|
+
resolved_project_name, resolved_config_name = (
|
|
407
|
+
self._resolve_project_and_config(
|
|
408
|
+
project_name,
|
|
409
|
+
config_name,
|
|
410
|
+
method_name=method_name,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
if secret_name is None:
|
|
415
|
+
raise TypeError(f"{method_name}() requires `secret_name`.")
|
|
416
|
+
|
|
417
|
+
return resolved_project_name, resolved_config_name, secret_name
|
|
418
|
+
|
|
419
|
+
def _resolve_project_config_secret_value(
|
|
420
|
+
self,
|
|
421
|
+
project_name: str | None,
|
|
422
|
+
config_name: str | None,
|
|
423
|
+
secret_name: str | None,
|
|
424
|
+
secret_value: str | None,
|
|
425
|
+
*,
|
|
426
|
+
method_name: str,
|
|
427
|
+
) -> tuple[str, str, str, str]:
|
|
428
|
+
if (
|
|
429
|
+
secret_name is None
|
|
430
|
+
and secret_value is None
|
|
431
|
+
and project_name is not None
|
|
432
|
+
and config_name is not None
|
|
433
|
+
and self._project_name is not None
|
|
434
|
+
and self._config_name is not None
|
|
435
|
+
):
|
|
436
|
+
secret_name = project_name
|
|
437
|
+
secret_value = config_name
|
|
438
|
+
project_name = None
|
|
439
|
+
config_name = None
|
|
440
|
+
|
|
441
|
+
resolved_project_name, resolved_config_name = (
|
|
442
|
+
self._resolve_project_and_config(
|
|
443
|
+
project_name,
|
|
444
|
+
config_name,
|
|
445
|
+
method_name=method_name,
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if secret_name is None or secret_value is None:
|
|
450
|
+
raise TypeError(
|
|
451
|
+
f"{method_name}() requires `secret_name` and `secret_value`."
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
resolved_project_name,
|
|
456
|
+
resolved_config_name,
|
|
457
|
+
secret_name,
|
|
458
|
+
secret_value,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def _resolve_project_config_secrets(
|
|
462
|
+
self,
|
|
463
|
+
project_name: str | dict[str, str] | None,
|
|
464
|
+
config_name: str | None,
|
|
465
|
+
secrets: dict[str, str] | None,
|
|
466
|
+
*,
|
|
467
|
+
method_name: str,
|
|
468
|
+
) -> tuple[str, str, dict[str, str]]:
|
|
469
|
+
if (
|
|
470
|
+
isinstance(project_name, dict)
|
|
471
|
+
and config_name is None
|
|
472
|
+
and secrets is None
|
|
473
|
+
and self._project_name is not None
|
|
474
|
+
and self._config_name is not None
|
|
475
|
+
):
|
|
476
|
+
secrets = project_name
|
|
477
|
+
project_name = None
|
|
478
|
+
|
|
479
|
+
project_name_input: str | None
|
|
480
|
+
if isinstance(project_name, dict):
|
|
481
|
+
project_name_input = None
|
|
482
|
+
else:
|
|
483
|
+
project_name_input = project_name
|
|
484
|
+
|
|
485
|
+
resolved_project_name, resolved_config_name = (
|
|
486
|
+
self._resolve_project_and_config(
|
|
487
|
+
project_name_input,
|
|
488
|
+
config_name,
|
|
489
|
+
method_name=method_name,
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
if secrets is None:
|
|
494
|
+
raise TypeError(f"{method_name}() requires `secrets`.")
|
|
495
|
+
|
|
496
|
+
return resolved_project_name, resolved_config_name, secrets
|
|
497
|
+
|
|
498
|
+
def _resolve_project_secret_note(
|
|
499
|
+
self,
|
|
500
|
+
project_name: str | None,
|
|
501
|
+
secret_name: str | None,
|
|
502
|
+
note: str | None,
|
|
503
|
+
*,
|
|
504
|
+
method_name: str,
|
|
505
|
+
) -> tuple[str, str, str]:
|
|
506
|
+
if (
|
|
507
|
+
note is None
|
|
508
|
+
and project_name is not None
|
|
509
|
+
and secret_name is not None
|
|
510
|
+
and self._project_name is not None
|
|
511
|
+
):
|
|
512
|
+
note = secret_name
|
|
513
|
+
secret_name = project_name
|
|
514
|
+
project_name = None
|
|
515
|
+
|
|
516
|
+
resolved_project_name = self._resolve_project(
|
|
517
|
+
project_name,
|
|
518
|
+
method_name=method_name,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if secret_name is None or note is None:
|
|
522
|
+
raise TypeError(
|
|
523
|
+
f"{method_name}() requires `secret_name` and `note`."
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return resolved_project_name, secret_name, note
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
Secrets = SecretsClient
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def response_to_model(response: Response) -> SecretModel:
|
|
533
|
+
data = _json_mapping(response, context="secret")
|
|
534
|
+
name = data.get("name")
|
|
535
|
+
return _secret_from_payload(
|
|
536
|
+
name=name if isinstance(name, str) else None,
|
|
537
|
+
value_payload=data.get("value"),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def response_to_models(response: Response) -> list[SecretModel]:
|
|
542
|
+
data = _json_mapping(response, context="secrets list")
|
|
543
|
+
secrets_payload = data.get("secrets")
|
|
544
|
+
|
|
545
|
+
if secrets_payload is None:
|
|
546
|
+
return []
|
|
547
|
+
|
|
548
|
+
if not isinstance(secrets_payload, dict):
|
|
549
|
+
raise DopplerResponseError(
|
|
550
|
+
"Doppler secrets list response contained an invalid `secrets` payload."
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
return [
|
|
554
|
+
_secret_from_payload(name=name, value_payload=value_payload)
|
|
555
|
+
for name, value_payload in secrets_payload.items()
|
|
556
|
+
]
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _secret_from_payload(
|
|
560
|
+
name: str | None,
|
|
561
|
+
value_payload: object,
|
|
562
|
+
) -> SecretModel:
|
|
563
|
+
value_dict = _secret_value_mapping(value_payload)
|
|
564
|
+
secret_value = SecretValue(
|
|
565
|
+
raw=value_dict.get("raw"),
|
|
566
|
+
computed=value_dict.get("computed"),
|
|
567
|
+
note=value_dict.get("note"),
|
|
568
|
+
)
|
|
569
|
+
return SecretModel(name=name, value=secret_value)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _json_mapping(response: Response, *, context: str) -> dict[str, Any]:
|
|
573
|
+
try:
|
|
574
|
+
data = response.json()
|
|
575
|
+
except ValueError as exc:
|
|
576
|
+
raise DopplerResponseError(
|
|
577
|
+
f"Doppler returned invalid JSON for {context}."
|
|
578
|
+
) from exc
|
|
579
|
+
|
|
580
|
+
if data is None:
|
|
581
|
+
return {}
|
|
582
|
+
|
|
583
|
+
if not isinstance(data, dict):
|
|
584
|
+
raise DopplerResponseError(
|
|
585
|
+
f"Doppler returned an unexpected JSON payload for {context}."
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
return data
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _secret_value_mapping(value_payload: object) -> dict[str, Any]:
|
|
592
|
+
if value_payload is None:
|
|
593
|
+
return {}
|
|
594
|
+
|
|
595
|
+
if not isinstance(value_payload, dict):
|
|
596
|
+
raise DopplerResponseError(
|
|
597
|
+
"Doppler secret response contained an invalid `value` payload."
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
return value_payload
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
__all__ = [
|
|
604
|
+
"Secrets",
|
|
605
|
+
"SecretsClient",
|
|
606
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: better-python-doppler
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Lightweight, secrets-first Python SDK for the Doppler API.
|
|
5
5
|
Author-email: Derek Banker <dbb2002@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -61,6 +61,7 @@ Supported and documented today:
|
|
|
61
61
|
- direct token auth with `Doppler(service_token="...")`
|
|
62
62
|
- explicit environment lookup with `Doppler(service_token_environ_name="...")`
|
|
63
63
|
- explicit `.env` loading with `Doppler.from_env(...)`
|
|
64
|
+
- client-level scoped defaults with `client.set_scope(project_name, config_name)`
|
|
64
65
|
- secrets access through `client.secrets`
|
|
65
66
|
- compatibility access through `client.Secrets`
|
|
66
67
|
- top-level compatibility exports: `Secrets` and `SecretsClient`
|
|
@@ -137,6 +138,25 @@ raw_value = client.secrets.get_raw("my-project", "dev", "API_KEY")
|
|
|
137
138
|
print(raw_value)
|
|
138
139
|
```
|
|
139
140
|
|
|
141
|
+
### Scoped Defaults
|
|
142
|
+
|
|
143
|
+
If you are repeatedly targeting the same project and config, set a client scope once and omit them from later secrets calls.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from better_python_doppler import Doppler
|
|
147
|
+
|
|
148
|
+
client = Doppler(service_token="dp.st.example-token").set_scope(
|
|
149
|
+
"my-project",
|
|
150
|
+
"dev",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
print(client.secrets.list_names())
|
|
154
|
+
print(client.secrets.get_raw("API_KEY"))
|
|
155
|
+
client.secrets.set("API_KEY", "next-value")
|
|
156
|
+
|
|
157
|
+
client.clear_scope()
|
|
158
|
+
```
|
|
159
|
+
|
|
140
160
|
### Single Secret Set/Get
|
|
141
161
|
|
|
142
162
|
```python
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any, Literal
|
|
4
|
-
|
|
5
|
-
from requests import Response
|
|
6
|
-
|
|
7
|
-
from better_python_doppler.apis import SecretAPI
|
|
8
|
-
from better_python_doppler.exceptions import DopplerResponseError
|
|
9
|
-
from better_python_doppler.models import SecretModel, SecretValue
|
|
10
|
-
from better_python_doppler.transport import RequestsTransport, SyncTransport
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
DownloadFormat = Literal[
|
|
14
|
-
"json",
|
|
15
|
-
"dotnet-json",
|
|
16
|
-
"env",
|
|
17
|
-
"yaml",
|
|
18
|
-
"docker",
|
|
19
|
-
"env-no-quotes",
|
|
20
|
-
]
|
|
21
|
-
NameTransformer = Literal[
|
|
22
|
-
"camel",
|
|
23
|
-
"upper-camel",
|
|
24
|
-
"lower-snake",
|
|
25
|
-
"tf-var",
|
|
26
|
-
"dotnet",
|
|
27
|
-
"dotnet-env",
|
|
28
|
-
"lower-kebab",
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class SecretsClient:
|
|
33
|
-
def __init__(
|
|
34
|
-
self,
|
|
35
|
-
service_token: str,
|
|
36
|
-
*,
|
|
37
|
-
transport: SyncTransport | None = None,
|
|
38
|
-
) -> None:
|
|
39
|
-
self._service_token = service_token
|
|
40
|
-
self._transport = transport or RequestsTransport(service_token)
|
|
41
|
-
|
|
42
|
-
def list(
|
|
43
|
-
self,
|
|
44
|
-
project_name: str,
|
|
45
|
-
config_name: str,
|
|
46
|
-
include_dynamic_secrets: bool = True,
|
|
47
|
-
dynamic_secrets_ttl_sec: int = 1800,
|
|
48
|
-
secrets: list[str] | None = None,
|
|
49
|
-
include_managed_secrets: bool = True,
|
|
50
|
-
) -> list[SecretModel]:
|
|
51
|
-
response = SecretAPI.list_secrets(
|
|
52
|
-
transport=self._transport,
|
|
53
|
-
project_name=project_name,
|
|
54
|
-
config_name=config_name,
|
|
55
|
-
include_dynamic_secrets=include_dynamic_secrets,
|
|
56
|
-
dynamic_secrets_ttl_sec=dynamic_secrets_ttl_sec,
|
|
57
|
-
secrets=secrets,
|
|
58
|
-
include_managed_secrets=include_managed_secrets,
|
|
59
|
-
)
|
|
60
|
-
return response_to_models(response)
|
|
61
|
-
|
|
62
|
-
def list_names(
|
|
63
|
-
self,
|
|
64
|
-
project_name: str,
|
|
65
|
-
config_name: str,
|
|
66
|
-
include_dynamic_secrets: bool = False,
|
|
67
|
-
include_managed_secrets: bool = True,
|
|
68
|
-
) -> list[str]:
|
|
69
|
-
response = SecretAPI.list_secret_names(
|
|
70
|
-
transport=self._transport,
|
|
71
|
-
project_name=project_name,
|
|
72
|
-
config_name=config_name,
|
|
73
|
-
include_dynamic_secrets=include_dynamic_secrets,
|
|
74
|
-
include_managed_secrets=include_managed_secrets,
|
|
75
|
-
)
|
|
76
|
-
data = _json_mapping(response, context="secret names")
|
|
77
|
-
names = data.get("names")
|
|
78
|
-
if names is None:
|
|
79
|
-
return []
|
|
80
|
-
if not isinstance(names, list) or not all(
|
|
81
|
-
isinstance(name, str) for name in names
|
|
82
|
-
):
|
|
83
|
-
raise DopplerResponseError(
|
|
84
|
-
"Doppler secret names response contained an invalid `names` payload."
|
|
85
|
-
)
|
|
86
|
-
return names
|
|
87
|
-
|
|
88
|
-
def get(
|
|
89
|
-
self,
|
|
90
|
-
project_name: str,
|
|
91
|
-
config_name: str,
|
|
92
|
-
secret_name: str,
|
|
93
|
-
) -> SecretModel:
|
|
94
|
-
response = SecretAPI.get_secret(
|
|
95
|
-
transport=self._transport,
|
|
96
|
-
project_name=project_name,
|
|
97
|
-
config_name=config_name,
|
|
98
|
-
secret_name=secret_name,
|
|
99
|
-
)
|
|
100
|
-
return response_to_model(response)
|
|
101
|
-
|
|
102
|
-
def get_raw(
|
|
103
|
-
self,
|
|
104
|
-
project_name: str,
|
|
105
|
-
config_name: str,
|
|
106
|
-
secret_name: str,
|
|
107
|
-
) -> str | None:
|
|
108
|
-
return self.get(project_name, config_name, secret_name).value.raw
|
|
109
|
-
|
|
110
|
-
def as_dict(
|
|
111
|
-
self,
|
|
112
|
-
project_name: str,
|
|
113
|
-
config_name: str,
|
|
114
|
-
include_dynamic_secrets: bool = True,
|
|
115
|
-
dynamic_secrets_ttl_sec: int = 1800,
|
|
116
|
-
secrets: list[str] | None = None,
|
|
117
|
-
include_managed_secrets: bool = True,
|
|
118
|
-
) -> dict[str, str | None]:
|
|
119
|
-
return {
|
|
120
|
-
secret.name: secret.value.raw
|
|
121
|
-
for secret in self.list(
|
|
122
|
-
project_name,
|
|
123
|
-
config_name,
|
|
124
|
-
include_dynamic_secrets=include_dynamic_secrets,
|
|
125
|
-
dynamic_secrets_ttl_sec=dynamic_secrets_ttl_sec,
|
|
126
|
-
secrets=secrets,
|
|
127
|
-
include_managed_secrets=include_managed_secrets,
|
|
128
|
-
)
|
|
129
|
-
if secret.name is not None
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
def set(
|
|
133
|
-
self,
|
|
134
|
-
project_name: str,
|
|
135
|
-
config_name: str,
|
|
136
|
-
secret_name: str,
|
|
137
|
-
secret_value: str,
|
|
138
|
-
) -> SecretModel:
|
|
139
|
-
return self.set_many(
|
|
140
|
-
project_name,
|
|
141
|
-
config_name,
|
|
142
|
-
{secret_name: secret_value},
|
|
143
|
-
)[0]
|
|
144
|
-
|
|
145
|
-
def set_many(
|
|
146
|
-
self,
|
|
147
|
-
project_name: str,
|
|
148
|
-
config_name: str,
|
|
149
|
-
secrets: dict[str, str],
|
|
150
|
-
) -> list[SecretModel]:
|
|
151
|
-
response = SecretAPI.update_secrets(
|
|
152
|
-
transport=self._transport,
|
|
153
|
-
project_name=project_name,
|
|
154
|
-
config_name=config_name,
|
|
155
|
-
secrets=secrets,
|
|
156
|
-
)
|
|
157
|
-
return response_to_models(response)
|
|
158
|
-
|
|
159
|
-
def update(
|
|
160
|
-
self,
|
|
161
|
-
project_name: str,
|
|
162
|
-
config_name: str,
|
|
163
|
-
secret_name: str | None = None,
|
|
164
|
-
secret_value: str | None = None,
|
|
165
|
-
*,
|
|
166
|
-
secrets: dict[str, str] | None = None,
|
|
167
|
-
) -> list[SecretModel]:
|
|
168
|
-
if secret_name is not None and secret_value is not None:
|
|
169
|
-
return self.set_many(
|
|
170
|
-
project_name,
|
|
171
|
-
config_name,
|
|
172
|
-
{secret_name: secret_value},
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
if secrets is not None:
|
|
176
|
-
return self.set_many(project_name, config_name, secrets)
|
|
177
|
-
|
|
178
|
-
raise ValueError(
|
|
179
|
-
"Invalid Parameter: Must provide `secret_name` and `secret_value` or `secrets`."
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
def download(
|
|
183
|
-
self,
|
|
184
|
-
project_name: str,
|
|
185
|
-
config_name: str,
|
|
186
|
-
format: DownloadFormat = "json",
|
|
187
|
-
name_transformer: NameTransformer | None = None,
|
|
188
|
-
include_dynamic_secrets: bool = False,
|
|
189
|
-
dynamic_secrets_ttl_sec: int = 1800,
|
|
190
|
-
secrets: list[str] | None = None,
|
|
191
|
-
) -> dict[str, str] | str:
|
|
192
|
-
response = SecretAPI.download_secrets(
|
|
193
|
-
transport=self._transport,
|
|
194
|
-
project_name=project_name,
|
|
195
|
-
config_name=config_name,
|
|
196
|
-
format=format,
|
|
197
|
-
name_transformer=name_transformer,
|
|
198
|
-
include_dynamic_secrets=include_dynamic_secrets,
|
|
199
|
-
dynamic_secrets_ttl_sec=dynamic_secrets_ttl_sec,
|
|
200
|
-
secrets=secrets or [],
|
|
201
|
-
)
|
|
202
|
-
if format in ["json", "dotnet-json"]:
|
|
203
|
-
return _json_mapping(response, context="downloaded secrets")
|
|
204
|
-
return response.text
|
|
205
|
-
|
|
206
|
-
def delete(self, project_name: str, config_name: str, secret_name: str) -> None:
|
|
207
|
-
SecretAPI.delete_secret(
|
|
208
|
-
transport=self._transport,
|
|
209
|
-
project_name=project_name,
|
|
210
|
-
config_name=config_name,
|
|
211
|
-
secret_name=secret_name,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
def update_note(
|
|
215
|
-
self,
|
|
216
|
-
project_name: str,
|
|
217
|
-
secret_name: str,
|
|
218
|
-
note: str,
|
|
219
|
-
) -> dict[str, object]:
|
|
220
|
-
response = SecretAPI.update_note(
|
|
221
|
-
transport=self._transport,
|
|
222
|
-
project_name=project_name,
|
|
223
|
-
secret_name=secret_name,
|
|
224
|
-
note=note,
|
|
225
|
-
)
|
|
226
|
-
return _json_mapping(response, context="secret note update")
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Secrets = SecretsClient
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def response_to_model(response: Response) -> SecretModel:
|
|
233
|
-
data = _json_mapping(response, context="secret")
|
|
234
|
-
name = data.get("name")
|
|
235
|
-
return _secret_from_payload(
|
|
236
|
-
name=name if isinstance(name, str) else None,
|
|
237
|
-
value_payload=data.get("value"),
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def response_to_models(response: Response) -> list[SecretModel]:
|
|
242
|
-
data = _json_mapping(response, context="secrets list")
|
|
243
|
-
secrets_payload = data.get("secrets")
|
|
244
|
-
|
|
245
|
-
if secrets_payload is None:
|
|
246
|
-
return []
|
|
247
|
-
|
|
248
|
-
if not isinstance(secrets_payload, dict):
|
|
249
|
-
raise DopplerResponseError(
|
|
250
|
-
"Doppler secrets list response contained an invalid `secrets` payload."
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
return [
|
|
254
|
-
_secret_from_payload(name=name, value_payload=value_payload)
|
|
255
|
-
for name, value_payload in secrets_payload.items()
|
|
256
|
-
]
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def _secret_from_payload(
|
|
260
|
-
name: str | None,
|
|
261
|
-
value_payload: object,
|
|
262
|
-
) -> SecretModel:
|
|
263
|
-
value_dict = _secret_value_mapping(value_payload)
|
|
264
|
-
secret_value = SecretValue(
|
|
265
|
-
raw=value_dict.get("raw"),
|
|
266
|
-
computed=value_dict.get("computed"),
|
|
267
|
-
note=value_dict.get("note"),
|
|
268
|
-
)
|
|
269
|
-
return SecretModel(name=name, value=secret_value)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def _json_mapping(response: Response, *, context: str) -> dict[str, Any]:
|
|
273
|
-
try:
|
|
274
|
-
data = response.json()
|
|
275
|
-
except ValueError as exc:
|
|
276
|
-
raise DopplerResponseError(
|
|
277
|
-
f"Doppler returned invalid JSON for {context}."
|
|
278
|
-
) from exc
|
|
279
|
-
|
|
280
|
-
if data is None:
|
|
281
|
-
return {}
|
|
282
|
-
|
|
283
|
-
if not isinstance(data, dict):
|
|
284
|
-
raise DopplerResponseError(
|
|
285
|
-
f"Doppler returned an unexpected JSON payload for {context}."
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
return data
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def _secret_value_mapping(value_payload: object) -> dict[str, Any]:
|
|
292
|
-
if value_payload is None:
|
|
293
|
-
return {}
|
|
294
|
-
|
|
295
|
-
if not isinstance(value_payload, dict):
|
|
296
|
-
raise DopplerResponseError(
|
|
297
|
-
"Doppler secret response contained an invalid `value` payload."
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
return value_payload
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
__all__ = [
|
|
304
|
-
"Secrets",
|
|
305
|
-
"SecretsClient",
|
|
306
|
-
]
|
|
File without changes
|
|
File without changes
|
{better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/secret.py
RENAMED
|
File without changes
|
{better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/src/better_python_doppler/transport.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{better_python_doppler-2.0.0 → better_python_doppler-2.1.0}/tests/test_secrets_client_api.py
RENAMED
|
File without changes
|
|
File without changes
|