better-python-doppler 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,10 @@
1
+ # src\better_python_doppler\__init__.py
2
+
3
+ from better_python_doppler.doppler_sdk import Doppler
4
+ from better_python_doppler.secret import Secrets
5
+
6
+
7
+ __all__ = [
8
+ "Doppler",
9
+ "Secrets",
10
+ ]
@@ -0,0 +1,11 @@
1
+ from .environment_apis import EnvironmentAPI
2
+ from .config_apis import ConfigAPI
3
+ from .secret_apis import SecretAPI
4
+ from .project_apis import ProjectAPI
5
+
6
+ __all__ = [
7
+ "EnvironmentAPI",
8
+ "ConfigAPI",
9
+ "SecretAPI",
10
+ "ProjectAPI"
11
+ ]
@@ -0,0 +1,51 @@
1
+ # src\better_python_doppler\apis\config_apis.py
2
+
3
+ import requests
4
+ from requests import Response
5
+ from urllib.parse import quote as url_encode
6
+
7
+ class ConfigAPI:
8
+
9
+ @staticmethod
10
+ def list_configs(
11
+ auth: str,
12
+ project_name: str,
13
+ environment_slug: str = "Environment slug",
14
+ page: int = 1,
15
+ per_page: int = 20
16
+ ) -> Response:
17
+
18
+ base_url = "https://api.doppler.com/v3/configs"
19
+ params = {
20
+ "project": project_name,
21
+ "environment": url_encode(environment_slug),
22
+ "page": page,
23
+ "per_page": per_page
24
+ }
25
+
26
+ headers = {
27
+ "accept": "application/json",
28
+ "authorization": f"Bearer {auth}"
29
+ }
30
+
31
+ return requests.get(base_url, params, headers=headers)
32
+
33
+ @staticmethod
34
+ def get_config(
35
+ auth: str,
36
+ project_name: str,
37
+ config_name: str
38
+ ) -> Response:
39
+
40
+ base_url = "https://api.doppler.com/v3/configs/config"
41
+ params = {
42
+ "project": project_name,
43
+ "config": config_name
44
+ }
45
+
46
+ headers = {
47
+ "accept": "application/json",
48
+ "authorization": f"Bearer {auth}"
49
+ }
50
+
51
+ return requests.get(base_url, params, headers=headers)
@@ -0,0 +1,44 @@
1
+ # src\better_python_doppler\apis\environment_apis.py
2
+
3
+ import requests
4
+ from requests import Response
5
+
6
+ class EnvironmentAPI:
7
+
8
+ @staticmethod
9
+ def list_environments(
10
+ auth: str,
11
+ project_name: str
12
+ ) -> Response:
13
+
14
+ base_url = "https://api.doppler.com/v3/environments"
15
+ params = {
16
+ "project": project_name
17
+ }
18
+
19
+ headers = {
20
+ "accept": "application/json",
21
+ "authorization": f"Bearer {auth}"
22
+ }
23
+
24
+ return requests.get(base_url, params, headers=headers)
25
+
26
+ @staticmethod
27
+ def get_environment(
28
+ auth: str,
29
+ project_name: str,
30
+ environment_name: str
31
+ ) -> Response:
32
+
33
+ base_url = "https://api.doppler.com/v3/environments/environment"
34
+ params = {
35
+ "project": project_name,
36
+ "environment": environment_name
37
+ }
38
+
39
+ headers = {
40
+ "accept": "application/json",
41
+ "authorization": f"Bearer {auth}"
42
+ }
43
+
44
+ return requests.get(base_url, params, headers=headers)
@@ -0,0 +1,42 @@
1
+ # src\better_python_doppler\apis\project_apis.py
2
+
3
+ import requests
4
+ from requests import Response
5
+
6
+ class ProjectAPI:
7
+
8
+ @staticmethod
9
+ def list_projects(
10
+ auth: str,
11
+ page: int = 1,
12
+ per_page: int = 20
13
+ ) -> Response:
14
+
15
+ base_url = "https://api.doppler.com/v3/projects"
16
+ params = {
17
+ "page": page,
18
+ "per_page": per_page
19
+ }
20
+ headers = {
21
+ "accept": "application/json",
22
+ "authorization": f"Bearer {auth}"
23
+ }
24
+
25
+ return requests.get(base_url, params, headers=headers)
26
+ @staticmethod
27
+ def get_project(
28
+ auth: str,
29
+ project_name: str
30
+ ) -> Response:
31
+
32
+ base_url = "https://api.doppler.com/v3/projects/project"
33
+ params = {
34
+ "project": project_name
35
+ }
36
+
37
+ headers = {
38
+ "accept": "application/json",
39
+ "authorization": f"Bearer {auth}"
40
+ }
41
+
42
+ return requests.get(base_url, params, headers=headers)
@@ -0,0 +1,187 @@
1
+ # src\better_python_doppler\apis\secret_apis.py
2
+
3
+ import requests
4
+ from requests import Response
5
+ from typing import Literal
6
+
7
+ class SecretAPI:
8
+
9
+ @staticmethod
10
+ def list_secrets(
11
+ auth: str,
12
+ project_name: str,
13
+ config_name: str,
14
+ include_dynamic_secrets: bool = True,
15
+ dynamic_secrets_ttl_sec: int = 1800,
16
+ secrets: list[str] | None = None,
17
+ include_managed_secrets: bool = True
18
+ ) -> Response:
19
+ base_url = "https://api.doppler.com/v3/configs/config/secrets"
20
+ params = {
21
+ "project": project_name,
22
+ "config": config_name,
23
+ "include_dynamic_secrets": str(include_dynamic_secrets).lower(),
24
+ "dynamic_secrets_ttl_sec": dynamic_secrets_ttl_sec,
25
+ "include_managed_secrets": str(include_managed_secrets).lower(),
26
+ }
27
+
28
+ headers = {
29
+ "Accept": "application/json",
30
+ "Authorization": f"Bearer {auth}",
31
+ }
32
+
33
+ if secrets:
34
+ params["secrets"] = ",".join(secrets)
35
+
36
+ return requests.get(base_url, params, headers=headers)
37
+
38
+ @staticmethod
39
+ def list_secret_names(
40
+ auth: str,
41
+ project_name: str,
42
+ config_name: str,
43
+ include_dynamic_secrets: bool = False,
44
+ include_managed_secrets: bool = True
45
+ ) -> Response:
46
+
47
+ base_url = "https://api.doppler.com/v3/configs/config/secrets/names"
48
+ params = {
49
+ "project": project_name,
50
+ "config": config_name,
51
+ "include_dynamic_secrets": str(include_dynamic_secrets).lower(),
52
+ "include_managed_secrets": str(include_managed_secrets).lower(),
53
+ }
54
+
55
+ headers = {
56
+ "accept": "application/json",
57
+ "authorization": f"Bearer {auth}"
58
+ }
59
+
60
+ return requests.get(base_url, params, headers=headers)
61
+
62
+ @staticmethod
63
+ def get_secret(
64
+ auth: str,
65
+ project_name: str,
66
+ config_name: str,
67
+ secret_name: str
68
+ ) -> Response:
69
+ base_url = "https://api.doppler.com/v3/configs/config/secret"
70
+ params = {
71
+ "project": project_name,
72
+ "config": config_name,
73
+ "name": secret_name
74
+ }
75
+
76
+ headers = {
77
+ "accept": "application/json",
78
+ "authorization": f"Bearer {auth}"
79
+ }
80
+
81
+ return requests.get(base_url, params, headers=headers)
82
+
83
+ @staticmethod
84
+ def update_secrets(
85
+ auth: str,
86
+ project_name: str,
87
+ config_name: str,
88
+ secrets: dict[str, str]
89
+ ) -> Response:
90
+ base_url = "https://api.doppler.com/v3/configs/config/secrets"
91
+
92
+ payload = {
93
+ "project": project_name,
94
+ "config": config_name,
95
+ "secrets": secrets
96
+ }
97
+
98
+ headers = {
99
+ "accept": "application/json",
100
+ "content-type": "application/json",
101
+ "authorization": f"Bearer {auth}"
102
+ }
103
+
104
+ return requests.post(base_url, headers=headers, json=payload)
105
+
106
+ @staticmethod
107
+ def download_secrets(
108
+ auth: str,
109
+ project_name: str,
110
+ config_name: str,
111
+ format: Literal["json", "dotnet-json", "env", "yaml" , "docker", "env-no-quotes"] = "json",
112
+ name_transformer: Literal["camel", "upper-camel", "lower-snake", "tf-var", "dotnet", "dotnet-env", "lower-kebab"] | None = None,
113
+ include_dynamic_secrets: bool = False,
114
+ dynamic_secrets_ttl_sec: int = 1800,
115
+ secrets: list[str] = []
116
+ ) -> Response:
117
+
118
+ base_url = "https://api.doppler.com/v3/configs/config/secrets/download"
119
+ params = {
120
+ "project": project_name,
121
+ "config": config_name,
122
+ "format": format,
123
+ "include_dynamic_secrets" : str(include_dynamic_secrets).lower(),
124
+ "dynamic_secrets_ttl_sec" : str(dynamic_secrets_ttl_sec).lower(),
125
+ }
126
+
127
+ if name_transformer:
128
+ params["name_transformer"] = name_transformer
129
+ if secrets:
130
+ params["secrets"] = ",".join(secrets)
131
+
132
+ accept_header = "application/json"
133
+ if format not in ["json", "dotnet-json"]:
134
+ accept_header = "text/plain"
135
+
136
+ headers = {
137
+ "accept": accept_header,
138
+ "authorization": f"Bearer {auth}"
139
+ }
140
+
141
+ return requests.get(base_url, params, headers=headers)
142
+
143
+ @staticmethod
144
+ def delete_secret(
145
+ auth: str,
146
+ project_name: str,
147
+ config_name: str,
148
+ secret_name: str
149
+ ) -> Response:
150
+ base_url = "https://api.doppler.com/v3/configs/config/secret"
151
+ params = {
152
+ "project": project_name,
153
+ "config": config_name,
154
+ "name": secret_name
155
+ }
156
+
157
+ headers = {
158
+ "accept": "application/json",
159
+ "authorization": f"Bearer {auth}"
160
+ }
161
+
162
+ return requests.delete(base_url, params=params, headers=headers)
163
+
164
+ @staticmethod
165
+ def update_note(
166
+ auth: str,
167
+ project_name: str,
168
+ secret_name: str,
169
+ note: str
170
+ ) -> Response:
171
+
172
+ base_url = "https://api.doppler.com/v3/projects/project/note"
173
+ params = {
174
+ "project": project_name
175
+ }
176
+
177
+ payload = {
178
+ "secret": secret_name,
179
+ "note": note
180
+ }
181
+ headers = {
182
+ "accept": "application/json",
183
+ "content-type": "application/json",
184
+ "authorization": f"Bearer {auth}"
185
+ }
186
+
187
+ return requests.post(base_url, params=params, headers=headers, json=payload)
@@ -0,0 +1,54 @@
1
+ # src\better_python_doppler\doppler_sdk.py
2
+
3
+ from better_python_doppler.secret import Secrets
4
+
5
+ class Doppler:
6
+
7
+ def __init__(
8
+ self,
9
+ service_token: str | None = None,
10
+ *,
11
+ service_token_environ_name: str | None = None
12
+ ) -> None:
13
+
14
+ self._service_token = self._get_service_token(service_token, service_token_environ_name)
15
+
16
+ self._secrets: Secrets | None = None
17
+
18
+
19
+ def _get_service_token(
20
+ self,
21
+ service_token: str | None = None,
22
+ service_token_environ_name: str | None = None
23
+ ) -> str:
24
+
25
+ if (service_token is None) == (service_token_environ_name is None):
26
+ raise ValueError("Either `service_token` OR `service_token_environ_name` must be provided upon init. NOT both or neither.")
27
+
28
+ if service_token is not None:
29
+ return service_token
30
+ else:
31
+ import os
32
+ from dotenv import load_dotenv
33
+ load_dotenv()
34
+
35
+ pulled_token = os.getenv(service_token_environ_name) # type: ignore
36
+
37
+ if pulled_token is None:
38
+ raise ValueError("Attempting to retrieve the environmental variable named `%s` returns `None`.", service_token_environ_name)
39
+
40
+ return pulled_token
41
+
42
+ @property
43
+ def service_token(self) -> str:
44
+ return self._service_token
45
+
46
+ @property
47
+ def Secrets(self) -> Secrets:
48
+ if self._secrets is None:
49
+ self._secrets = Secrets(self._service_token)
50
+
51
+ return self._secrets
52
+
53
+
54
+
@@ -0,0 +1,61 @@
1
+ # src\better_python_doppler\models\config.py
2
+
3
+ from datetime import datetime as DateTime
4
+
5
+ from .project import ProjectModel
6
+ from .environment import EnvironmentModel
7
+
8
+ class ConfigModel:
9
+
10
+ def __init__(
11
+ self,
12
+ name: str | None = None,
13
+ project: ProjectModel = ProjectModel(),
14
+ environment: EnvironmentModel = EnvironmentModel(),
15
+ created_at: DateTime | None = None,
16
+ initial_fetch_at: DateTime | None = None,
17
+ last_fetch_at: DateTime | None = None,
18
+ root: bool | None = None,
19
+ locked: bool | None = None
20
+ ) -> None:
21
+
22
+ self._name: str | None = name
23
+ self._project: ProjectModel = project
24
+ self._environment: EnvironmentModel = environment
25
+ self._created_at: DateTime | None = created_at
26
+ self._initial_fetch_at: DateTime | None = initial_fetch_at
27
+ self._last_fetch_at: DateTime | None = last_fetch_at
28
+ self._root: bool | None = root
29
+ self._locked: bool | None = locked
30
+
31
+ @property
32
+ def name(self) -> str | None:
33
+ return self._name
34
+
35
+ @property
36
+ def project(self) -> ProjectModel:
37
+ return self._project
38
+
39
+ @property
40
+ def environment(self) -> EnvironmentModel:
41
+ return self._environment
42
+
43
+ @property
44
+ def created_at(self) -> DateTime | None:
45
+ return self._created_at
46
+
47
+ @property
48
+ def initial_fetch_at(self) -> DateTime | None:
49
+ return self._initial_fetch_at
50
+
51
+ @property
52
+ def last_fetch_at(self) -> DateTime | None:
53
+ return self._last_fetch_at
54
+
55
+ @property
56
+ def root(self) -> bool | None:
57
+ return self._root
58
+
59
+ @property
60
+ def locked(self) -> bool | None:
61
+ return self._locked
@@ -0,0 +1,44 @@
1
+ # src\better_python_doppler\models\environment.py
2
+
3
+ from datetime import datetime as DateTime
4
+ from typing import Any
5
+
6
+ from .project import ProjectModel
7
+
8
+ class EnvironmentModel:
9
+
10
+ def __init__(
11
+ self,
12
+ id: str | None = None,
13
+ name: str | None = None,
14
+ project: ProjectModel = ProjectModel(),
15
+ created_at: DateTime | None = None,
16
+ initial_fetch_at: DateTime | None = None,
17
+ ) -> None:
18
+
19
+ self._id: str | None = id
20
+ self._name: str | None = name
21
+ self._project: ProjectModel = project
22
+ self._created_at: DateTime | None = created_at
23
+ self._initial_fetch_at: DateTime | None = initial_fetch_at
24
+
25
+
26
+ @property
27
+ def id(self) -> str | None:
28
+ return self._id
29
+
30
+ @property
31
+ def name(self) -> str | None:
32
+ return self._name
33
+
34
+ @property
35
+ def project(self) -> ProjectModel:
36
+ return self._project
37
+
38
+ @property
39
+ def created_at(self) -> DateTime | None:
40
+ return self._created_at
41
+
42
+ @property
43
+ def initial_fetch_at(self) -> DateTime | None:
44
+ return self._initial_fetch_at
@@ -0,0 +1,34 @@
1
+ # src\better_python_doppler\models\project.py
2
+
3
+ from datetime import datetime as DateTime
4
+
5
+ class ProjectModel:
6
+
7
+ def __init__(
8
+ self,
9
+ id: str | None = None,
10
+ name: str | None = None,
11
+ description: str | None = None,
12
+ created_at: DateTime | None = None,
13
+ ) -> None:
14
+
15
+ self._id: str | None = id
16
+ self._name: str | None = name
17
+ self._description: str | None = description
18
+ self._created_at: DateTime | None = created_at
19
+
20
+ @property
21
+ def id(self) -> str | None:
22
+ return self._id
23
+
24
+ @property
25
+ def name(self) -> str | None:
26
+ return self._name
27
+
28
+ @property
29
+ def description(self) -> str | None:
30
+ return self._description
31
+
32
+ @property
33
+ def created_at(self) -> DateTime | None:
34
+ return self._created_at
@@ -0,0 +1,12 @@
1
+ # from .config import ConfigModel
2
+ # from .environment import EnvironmentModel
3
+ # from .project import ProjectModel
4
+ from .secret import SecretModel, SecretValue
5
+
6
+ __all__ = [
7
+ # "ConfigModel",
8
+ # "EnvironmentModel",
9
+ # "ProjectModel",
10
+ "SecretModel",
11
+ "SecretValue",
12
+ ]
@@ -0,0 +1,70 @@
1
+ # src\better_python_doppler\models\secret.py
2
+
3
+ class SecretValue():
4
+
5
+ def __init__(
6
+ self,
7
+ raw: str | None = None,
8
+ computed: str | None = None,
9
+ note: str | None = None
10
+ ) -> None:
11
+
12
+ self._raw: str | None = raw
13
+ self._computed: str | None = computed
14
+ self._note: str | None = note
15
+
16
+ @property
17
+ def raw(self) -> str | None:
18
+ return self._raw
19
+
20
+ @property
21
+ def computed(self) -> str | None:
22
+ return self._computed
23
+
24
+ @property
25
+ def note(self) -> str | None:
26
+ return self._note
27
+
28
+ @raw.setter
29
+ def raw(self, raw: str) -> None:
30
+ self._raw = raw
31
+
32
+ @note.setter
33
+ def note(self, note: str) -> None:
34
+ self._note = note
35
+
36
+ def __str__(self) -> str:
37
+ temp = {'raw': self._raw, 'computed': self._computed, 'note': self._note}
38
+ return str(temp)
39
+
40
+ def dict(self)-> dict:
41
+ return {'raw': self._raw, 'computed': self._computed, 'note': self._note}
42
+
43
+
44
+
45
+
46
+ class SecretModel():
47
+
48
+ def __init__(
49
+ self,
50
+ name: str | None = None,
51
+ value: SecretValue = SecretValue()
52
+ ) -> None:
53
+
54
+ self._name: str | None = name
55
+ self._value: SecretValue = value
56
+
57
+ @property
58
+ def name(self) -> str | None:
59
+ return self._name
60
+
61
+ @property
62
+ def value(self) -> SecretValue:
63
+ return self._value
64
+
65
+ @name.setter
66
+ def name(self, name: str) -> None:
67
+ self._name = name
68
+
69
+ def key_value(self) -> str:
70
+ return f"'{self._name}': '{self._value.raw}'"
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+ from typing import Literal
3
+
4
+ from requests import Response
5
+
6
+ from better_python_doppler.models import SecretModel, SecretValue
7
+ from better_python_doppler.apis import SecretAPI
8
+
9
+ class Secrets:
10
+
11
+ def __init__(
12
+ self,
13
+ service_token: str
14
+ ) -> None:
15
+
16
+ self._service_token = service_token
17
+
18
+ def list(
19
+ self,
20
+ project_name: str,
21
+ config_name: str,
22
+ include_dynamic_secrets: bool = True,
23
+ dynamic_secrets_ttl_sec: int = 1800,
24
+ secrets: list[str] | None = None,
25
+ include_managed_secrets: bool = True,
26
+ ) -> list[SecretModel]:
27
+ response = SecretAPI.list_secrets(
28
+ auth=self._service_token,
29
+ project_name=project_name,
30
+ config_name=config_name,
31
+ include_dynamic_secrets=include_dynamic_secrets,
32
+ dynamic_secrets_ttl_sec=dynamic_secrets_ttl_sec,
33
+ secrets=secrets,
34
+ include_managed_secrets=include_managed_secrets,
35
+ )
36
+ response.raise_for_status()
37
+ data = response.json()
38
+
39
+ secrets_dict = data.get("secrets", {})
40
+ secret_models = []
41
+ for name, value_dict in secrets_dict.items():
42
+ secret_value = SecretValue(
43
+ raw=value_dict.get("raw"),
44
+ computed=value_dict.get("computed"),
45
+ note=value_dict.get("note"),
46
+ )
47
+ secret_model = SecretModel(name=name, value=secret_value)
48
+ secret_models.append(secret_model)
49
+ return secret_models
50
+
51
+ def list_names(
52
+ self,
53
+ project_name: str,
54
+ config_name: str,
55
+ include_dynamic_secrets: bool = False,
56
+ include_managed_secrets: bool = True,
57
+ ) -> list[str]:
58
+ response = SecretAPI.list_secret_names(
59
+ auth=self._service_token,
60
+ project_name=project_name,
61
+ config_name=config_name,
62
+ include_dynamic_secrets=include_dynamic_secrets,
63
+ include_managed_secrets=include_managed_secrets,
64
+ )
65
+ response.raise_for_status()
66
+ data = response.json()
67
+ return data.get("names", [])
68
+
69
+ def get(self, project_name: str, config_name: str, secret_name: str) -> SecretModel:
70
+ response = SecretAPI.get_secret(
71
+ auth=self._service_token,
72
+ project_name=project_name,
73
+ config_name=config_name,
74
+ secret_name=secret_name,
75
+ )
76
+ return response_to_model(response)
77
+
78
+ def update(
79
+ self, project_name: str, config_name: str, secrets: dict[str, str]
80
+ ) -> list[SecretModel]:
81
+ response = SecretAPI.update_secrets(
82
+ auth=self._service_token,
83
+ project_name=project_name,
84
+ config_name=config_name,
85
+ secrets=secrets,
86
+ )
87
+ response.raise_for_status()
88
+ data = response.json()
89
+
90
+ secrets_dict = data.get("secrets", {})
91
+ secret_models = []
92
+ for name, value_dict in secrets_dict.items():
93
+ secret_value = SecretValue(
94
+ raw=value_dict.get("raw"),
95
+ computed=value_dict.get("computed"),
96
+ note=value_dict.get("note"),
97
+ )
98
+ secret_model = SecretModel(name=name, value=secret_value)
99
+ secret_models.append(secret_model)
100
+ return secret_models
101
+
102
+ def download(
103
+ self,
104
+ project_name: str,
105
+ config_name: str,
106
+ format: Literal["json", "dotnet-json", "env", "yaml", "docker", "env-no-quotes"] = "json",
107
+ name_transformer: Literal["camel", "upper-camel", "lower-snake", "tf-var", "dotnet", "dotnet-env", "lower-kebab"] | None = None,
108
+ include_dynamic_secrets: bool = False,
109
+ dynamic_secrets_ttl_sec: int = 1800,
110
+ secrets: list[str] | None = None,
111
+ ) -> dict[str, str] | str:
112
+ response = SecretAPI.download_secrets(
113
+ auth=self._service_token,
114
+ project_name=project_name,
115
+ config_name=config_name,
116
+ format=format,
117
+ name_transformer=name_transformer,
118
+ include_dynamic_secrets=include_dynamic_secrets,
119
+ dynamic_secrets_ttl_sec=dynamic_secrets_ttl_sec,
120
+ secrets=secrets or [],
121
+ )
122
+ response.raise_for_status()
123
+ if format in ["json", "dotnet-json"]:
124
+ return response.json()
125
+ return response.text
126
+
127
+ def delete(self, project_name: str, config_name: str, secret_name: str) -> None:
128
+ response = SecretAPI.delete_secret(
129
+ auth=self._service_token,
130
+ project_name=project_name,
131
+ config_name=config_name,
132
+ secret_name=secret_name,
133
+ )
134
+ response.raise_for_status()
135
+
136
+ def update_note(self, project_name: str, secret_name: str, note: str) -> dict:
137
+ response = SecretAPI.update_note(
138
+ auth=self._service_token,
139
+ project_name=project_name,
140
+ secret_name=secret_name,
141
+ note=note,
142
+ )
143
+ response.raise_for_status()
144
+ return response.json()
145
+
146
+ def response_to_model(response: Response) -> SecretModel:
147
+ response.raise_for_status()
148
+ data = response.json()
149
+
150
+ value_dict = data.get("value", {})
151
+ secret_value = SecretValue(
152
+ raw=value_dict.get("raw"),
153
+ computed=value_dict.get("computed"),
154
+ note=value_dict.get("note"),
155
+ )
156
+
157
+ return SecretModel(name=data.get("name"), value=secret_value)
@@ -0,0 +1,5 @@
1
+ from .utilities import list_to_comma_string
2
+
3
+ __all__ = [
4
+ "list_to_comma_string"
5
+ ]
@@ -0,0 +1,3 @@
1
+ def list_to_comma_string(input_list: list[str]) -> str:
2
+ """Joins a list of strings into a single comma-separated string."""
3
+ return ",".join(input_list)
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: better-python-doppler
3
+ Version: 0.1.0
4
+ Summary: A simplified and up to date Python SDK for Doppler. Because their current one has stale docs and doesn't provide much IDE support.
5
+ Author-email: Derek Banker <dbb2002@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Keywords: Doppler,SKD
8
+ Classifier: Programming Language :: Python
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # Better Python Doppler
15
+
16
+ Better Python Doppler is a lightweight SDK around the [Doppler API](https://docs.doppler.com/reference). It wraps common API endpoints in simple Python classes and includes typed models for working with secrets.
17
+
18
+ ## Installation
19
+
20
+ This project currently requires Python 3.12 or newer. Once published to PyPI it can be installed with `pip`:
21
+
22
+ ```bash
23
+ pip install better-python-doppler
24
+ ```
25
+
26
+ For local development clone the repository and install the dependencies:
27
+
28
+ ```bash
29
+ pip install -e .
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ The example below mirrors `examples/example_secrets.py` and demonstrates basic usage of the SDK. It assumes the environment variables `SERVICE_TOKEN`, `PROJECT_NAME`, `CONFIG_NAME` and `SECRET_NAME` are available.
35
+
36
+ ```python
37
+ from better_python_doppler import Doppler
38
+
39
+ # Create the SDK instance
40
+ sdk = Doppler(service_token=os.getenv("SERVICE_TOKEN"))
41
+
42
+ # List secret names
43
+ names = sdk.Secrets.list_names(
44
+ project_name=os.getenv("PROJECT_NAME"),
45
+ config_name=os.getenv("CONFIG_NAME")
46
+ )
47
+
48
+ # Fetch all secrets
49
+ secrets = sdk.Secrets.list(
50
+ project_name=os.getenv("PROJECT_NAME"),
51
+ config_name=os.getenv("CONFIG_NAME")
52
+ )
53
+
54
+ # Retrieve a specific secret value
55
+ secret_value = sdk.Secrets.get(
56
+ os.getenv("PROJECT_NAME"),
57
+ os.getenv("CONFIG_NAME"),
58
+ os.getenv("SECRET_NAME")
59
+ )
60
+ print(secret_value.value.raw)
61
+ ```
62
+
63
+ ## Functionality
64
+
65
+ The SDK focuses on secrets management via the `Doppler.Secrets` interface defined in `secret.py`. Supported operations include:
66
+
67
+ - `list` – return detailed information for all secrets in a config.
68
+ - `list_names` – fetch only the secret names.
69
+ - `get` – retrieve a single secret as a `SecretModel`.
70
+ - `update` – update one or more secret values.
71
+ - `download` – download secrets in various formats (json, env, yaml, etc.).
72
+ - `delete` – delete a secret.
73
+ - `update_note` – modify the note on an existing secret.
74
+
75
+ Under the hood these call lightweight API wrappers located in `src/better_python_doppler/apis`. The project also exposes minimal data models in `src/better_python_doppler/models` such as `SecretModel` and `SecretValue` for convenient type checking.
76
+
77
+ Authentication is performed with a Doppler service token. You can pass the token directly when creating `Doppler` or load it from an environment variable using the `service_token_environ_name` parameter.
78
+
79
+ ## License
80
+
81
+ This project is licensed under the Apache 2.0 License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,20 @@
1
+ better_python_doppler/__init__.py,sha256=4lM6l9uwOkmsvPg6-_4L16JP-zRquxOZIWWkNlom2UM,189
2
+ better_python_doppler/doppler_sdk.py,sha256=HAYcPd5rn50jWbyaEZ2oVPcKUAhocLNFHjskmjJvBdw,1586
3
+ better_python_doppler/secret.py,sha256=7cKO2tICBsBfNFhXue6Szwu7Azx4R0-zbbJbP7QnYKE,5461
4
+ better_python_doppler/apis/__init__.py,sha256=Pt0fc70zmLNdDoFpUm7I6JR36NO_1hgo0TJTKKTwy80,264
5
+ better_python_doppler/apis/config_apis.py,sha256=HMVlbAyfDW9QEVz-ybHBAx8Bx8CLC4DycS3k6UfQDgQ,1378
6
+ better_python_doppler/apis/environment_apis.py,sha256=U7KkYGBOr5JoEiykhcDstc_mAbOODZD8QoSz1PHn2U4,1127
7
+ better_python_doppler/apis/project_apis.py,sha256=sIjCoIf4xemVScFN0Luhp_aJWaxqF3Q3z8JEvraTiWE,1070
8
+ better_python_doppler/apis/secret_apis.py,sha256=IvMQx2g6QoAEEFqrAWTeWVoa2HeiCmfAQnL__V4MbE8,5808
9
+ better_python_doppler/models/DEPRECATED - config.py,sha256=FZEdUOEREa3RE-pr1JajSejaJBa1_TB49FHQ4pVIXO0,1857
10
+ better_python_doppler/models/DEPRECATED - environment.py,sha256=riENHklCpOFU-qMnSSAcKUiU1ppL7BkH-ls9mDdvOjg,1208
11
+ better_python_doppler/models/DEPRECATED - project.py,sha256=mxDASyOr2G8nxhYFoVkc7oc1jGw_WupAj3dNXfT4tUk,904
12
+ better_python_doppler/models/__init__.py,sha256=I8T7L-fQu3gKFcCtLWFGxhTUwgvpy_J1liehO3vwI80,280
13
+ better_python_doppler/models/secret.py,sha256=mBbLgVYJoicWG9Toj6LE5psQnsCglgtCwemJI2NXdVU,1620
14
+ better_python_doppler/utilities/__init__.py,sha256=HEzuOvkIGwZrRbzMAUjuFLblmjlRvJGGDh0J4tnJsKo,102
15
+ better_python_doppler/utilities/utilities.py,sha256=yQ38tyBogLzHejd7ChM57m0qGGFN0SxAcTAvqWkSl40,160
16
+ better_python_doppler-0.1.0.dist-info/licenses/LICENSE,sha256=UOZ1F5fFDe3XXvG4oNnkL1-Ecun7zpHzRxjp-XsMeAo,11324
17
+ better_python_doppler-0.1.0.dist-info/METADATA,sha256=gFabCeTSTBwH646aN_EAOUS8uwXzlvghuUaJFCETXTc,2918
18
+ better_python_doppler-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ better_python_doppler-0.1.0.dist-info/top_level.txt,sha256=CDeMMzbvM6DRK-cPrljVX0nbYYTVOCIb_pP62UIFFx8,22
20
+ better_python_doppler-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1 @@
1
+ better_python_doppler