nextmv 0.39.0.dev1__py3-none-any.whl → 1.0.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.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +1 -2
- nextmv/__init__.py +2 -4
- nextmv/cli/CONTRIBUTING.md +583 -0
- nextmv/cli/cloud/__init__.py +49 -0
- nextmv/cli/cloud/acceptance/__init__.py +27 -0
- nextmv/cli/cloud/acceptance/create.py +391 -0
- nextmv/cli/cloud/acceptance/delete.py +64 -0
- nextmv/cli/cloud/acceptance/get.py +103 -0
- nextmv/cli/cloud/acceptance/list.py +62 -0
- nextmv/cli/cloud/acceptance/update.py +95 -0
- nextmv/cli/cloud/account/__init__.py +28 -0
- nextmv/cli/cloud/account/create.py +83 -0
- nextmv/cli/cloud/account/delete.py +59 -0
- nextmv/cli/cloud/account/get.py +66 -0
- nextmv/cli/cloud/account/update.py +70 -0
- nextmv/cli/cloud/app/__init__.py +35 -0
- nextmv/cli/cloud/app/create.py +140 -0
- nextmv/cli/cloud/app/delete.py +57 -0
- nextmv/cli/cloud/app/exists.py +44 -0
- nextmv/cli/cloud/app/get.py +66 -0
- nextmv/cli/cloud/app/list.py +61 -0
- nextmv/cli/cloud/app/push.py +432 -0
- nextmv/cli/cloud/app/update.py +124 -0
- nextmv/cli/cloud/batch/__init__.py +29 -0
- nextmv/cli/cloud/batch/create.py +452 -0
- nextmv/cli/cloud/batch/delete.py +64 -0
- nextmv/cli/cloud/batch/get.py +104 -0
- nextmv/cli/cloud/batch/list.py +63 -0
- nextmv/cli/cloud/batch/metadata.py +66 -0
- nextmv/cli/cloud/batch/update.py +95 -0
- nextmv/cli/cloud/data/__init__.py +26 -0
- nextmv/cli/cloud/data/upload.py +162 -0
- nextmv/cli/cloud/ensemble/__init__.py +33 -0
- nextmv/cli/cloud/ensemble/create.py +413 -0
- nextmv/cli/cloud/ensemble/delete.py +63 -0
- nextmv/cli/cloud/ensemble/get.py +65 -0
- nextmv/cli/cloud/ensemble/list.py +63 -0
- nextmv/cli/cloud/ensemble/update.py +103 -0
- nextmv/cli/cloud/input_set/__init__.py +32 -0
- nextmv/cli/cloud/input_set/create.py +168 -0
- nextmv/cli/cloud/input_set/delete.py +64 -0
- nextmv/cli/cloud/input_set/get.py +63 -0
- nextmv/cli/cloud/input_set/list.py +63 -0
- nextmv/cli/cloud/input_set/update.py +123 -0
- nextmv/cli/cloud/instance/__init__.py +35 -0
- nextmv/cli/cloud/instance/create.py +289 -0
- nextmv/cli/cloud/instance/delete.py +61 -0
- nextmv/cli/cloud/instance/exists.py +39 -0
- nextmv/cli/cloud/instance/get.py +62 -0
- nextmv/cli/cloud/instance/list.py +60 -0
- nextmv/cli/cloud/instance/update.py +216 -0
- nextmv/cli/cloud/managed_input/__init__.py +31 -0
- nextmv/cli/cloud/managed_input/create.py +144 -0
- nextmv/cli/cloud/managed_input/delete.py +64 -0
- nextmv/cli/cloud/managed_input/get.py +63 -0
- nextmv/cli/cloud/managed_input/list.py +60 -0
- nextmv/cli/cloud/managed_input/update.py +97 -0
- nextmv/cli/cloud/run/__init__.py +37 -0
- nextmv/cli/cloud/run/cancel.py +37 -0
- nextmv/cli/cloud/run/create.py +524 -0
- nextmv/cli/cloud/run/get.py +199 -0
- nextmv/cli/cloud/run/input.py +86 -0
- nextmv/cli/cloud/run/list.py +80 -0
- nextmv/cli/cloud/run/logs.py +166 -0
- nextmv/cli/cloud/run/metadata.py +67 -0
- nextmv/cli/cloud/run/track.py +500 -0
- nextmv/cli/cloud/scenario/__init__.py +29 -0
- nextmv/cli/cloud/scenario/create.py +451 -0
- nextmv/cli/cloud/scenario/delete.py +61 -0
- nextmv/cli/cloud/scenario/get.py +102 -0
- nextmv/cli/cloud/scenario/list.py +63 -0
- nextmv/cli/cloud/scenario/metadata.py +67 -0
- nextmv/cli/cloud/scenario/update.py +93 -0
- nextmv/cli/cloud/secrets/__init__.py +33 -0
- nextmv/cli/cloud/secrets/create.py +206 -0
- nextmv/cli/cloud/secrets/delete.py +63 -0
- nextmv/cli/cloud/secrets/get.py +66 -0
- nextmv/cli/cloud/secrets/list.py +60 -0
- nextmv/cli/cloud/secrets/update.py +144 -0
- nextmv/cli/cloud/shadow/__init__.py +33 -0
- nextmv/cli/cloud/shadow/create.py +184 -0
- nextmv/cli/cloud/shadow/delete.py +64 -0
- nextmv/cli/cloud/shadow/get.py +61 -0
- nextmv/cli/cloud/shadow/list.py +63 -0
- nextmv/cli/cloud/shadow/metadata.py +66 -0
- nextmv/cli/cloud/shadow/start.py +43 -0
- nextmv/cli/cloud/shadow/stop.py +53 -0
- nextmv/cli/cloud/shadow/update.py +96 -0
- nextmv/cli/cloud/switchback/__init__.py +33 -0
- nextmv/cli/cloud/switchback/create.py +151 -0
- nextmv/cli/cloud/switchback/delete.py +64 -0
- nextmv/cli/cloud/switchback/get.py +62 -0
- nextmv/cli/cloud/switchback/list.py +63 -0
- nextmv/cli/cloud/switchback/metadata.py +68 -0
- nextmv/cli/cloud/switchback/start.py +43 -0
- nextmv/cli/cloud/switchback/stop.py +53 -0
- nextmv/cli/cloud/switchback/update.py +96 -0
- nextmv/cli/cloud/upload/__init__.py +22 -0
- nextmv/cli/cloud/upload/create.py +39 -0
- nextmv/cli/cloud/version/__init__.py +33 -0
- nextmv/cli/cloud/version/create.py +96 -0
- nextmv/cli/cloud/version/delete.py +61 -0
- nextmv/cli/cloud/version/exists.py +39 -0
- nextmv/cli/cloud/version/get.py +62 -0
- nextmv/cli/cloud/version/list.py +60 -0
- nextmv/cli/cloud/version/update.py +92 -0
- nextmv/cli/community/__init__.py +24 -0
- nextmv/cli/community/clone.py +86 -0
- nextmv/cli/community/list.py +200 -0
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +228 -0
- nextmv/cli/configuration/create.py +94 -0
- nextmv/cli/configuration/delete.py +67 -0
- nextmv/cli/configuration/list.py +77 -0
- nextmv/cli/confirm.py +34 -0
- nextmv/cli/main.py +161 -3
- nextmv/cli/message.py +170 -0
- nextmv/cli/options.py +220 -0
- nextmv/cli/version.py +22 -2
- nextmv/cloud/__init__.py +17 -38
- nextmv/cloud/acceptance_test.py +20 -83
- nextmv/cloud/account.py +269 -30
- nextmv/cloud/application/__init__.py +898 -0
- nextmv/cloud/application/_acceptance.py +424 -0
- nextmv/cloud/application/_batch_scenario.py +845 -0
- nextmv/cloud/application/_ensemble.py +251 -0
- nextmv/cloud/application/_input_set.py +263 -0
- nextmv/cloud/application/_instance.py +289 -0
- nextmv/cloud/application/_managed_input.py +227 -0
- nextmv/cloud/application/_run.py +1393 -0
- nextmv/cloud/application/_secrets.py +294 -0
- nextmv/cloud/application/_shadow.py +320 -0
- nextmv/cloud/application/_switchback.py +332 -0
- nextmv/cloud/application/_utils.py +54 -0
- nextmv/cloud/application/_version.py +304 -0
- nextmv/cloud/batch_experiment.py +6 -2
- nextmv/cloud/community.py +446 -0
- nextmv/cloud/instance.py +11 -1
- nextmv/cloud/integration.py +8 -5
- nextmv/cloud/package.py +50 -9
- nextmv/cloud/shadow.py +254 -0
- nextmv/cloud/switchback.py +228 -0
- nextmv/deprecated.py +5 -3
- nextmv/input.py +20 -88
- nextmv/local/application.py +3 -15
- nextmv/local/runner.py +1 -1
- nextmv/model.py +50 -11
- nextmv/options.py +11 -256
- nextmv/output.py +0 -62
- nextmv/polling.py +54 -16
- nextmv/run.py +84 -37
- nextmv/status.py +1 -51
- {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/METADATA +37 -11
- nextmv-1.0.0.dist-info/RECORD +185 -0
- nextmv-1.0.0.dist-info/entry_points.txt +2 -0
- nextmv/cloud/application.py +0 -4204
- nextmv-0.39.0.dev1.dist-info/RECORD +0 -55
- nextmv-0.39.0.dev1.dist-info/entry_points.txt +0 -2
- {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
- {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application mixin for managing app secrets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
8
|
+
from nextmv.safe import safe_id
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from . import Application
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApplicationSecretsMixin:
|
|
15
|
+
"""
|
|
16
|
+
Mixin class for managing app secrets within an application.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def delete_secrets_collection(self: "Application", secrets_collection_id: str) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Delete a secrets collection.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
secrets_collection_id : str
|
|
26
|
+
ID of the secrets collection to delete.
|
|
27
|
+
|
|
28
|
+
Raises
|
|
29
|
+
------
|
|
30
|
+
requests.HTTPError
|
|
31
|
+
If the response status code is not 2xx.
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> app.delete_secrets_collection("secrets-123")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_ = self.client.request(
|
|
39
|
+
method="DELETE",
|
|
40
|
+
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def list_secrets_collections(self: "Application") -> list[SecretsCollectionSummary]:
|
|
44
|
+
"""
|
|
45
|
+
List all secrets collections.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
list[SecretsCollectionSummary]
|
|
50
|
+
List of all secrets collections associated with this application.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
requests.HTTPError
|
|
55
|
+
If the response status code is not 2xx.
|
|
56
|
+
|
|
57
|
+
Examples
|
|
58
|
+
--------
|
|
59
|
+
>>> collections = app.list_secrets_collections()
|
|
60
|
+
>>> for collection in collections:
|
|
61
|
+
... print(collection.name)
|
|
62
|
+
'API Keys'
|
|
63
|
+
'Database Credentials'
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
response = self.client.request(
|
|
67
|
+
method="GET",
|
|
68
|
+
endpoint=f"{self.endpoint}/secrets",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return [SecretsCollectionSummary.from_dict(secrets) for secrets in response.json()["items"]]
|
|
72
|
+
|
|
73
|
+
def new_secrets_collection(
|
|
74
|
+
self: "Application",
|
|
75
|
+
secrets: list[Secret],
|
|
76
|
+
id: str | None = None,
|
|
77
|
+
name: str | None = None,
|
|
78
|
+
description: str | None = None,
|
|
79
|
+
) -> SecretsCollectionSummary:
|
|
80
|
+
"""
|
|
81
|
+
Create a new secrets collection.
|
|
82
|
+
|
|
83
|
+
This method creates a new secrets collection with the provided secrets.
|
|
84
|
+
A secrets collection is a group of key-value pairs that can be used by
|
|
85
|
+
your application instances during execution. If no secrets are
|
|
86
|
+
provided, a ValueError is raised. If the `id` or `name` parameters are
|
|
87
|
+
not provided, they will be generated based on a unique ID.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
secrets : list[Secret]
|
|
92
|
+
List of secrets to use for the secrets collection. Each secret
|
|
93
|
+
should be an instance of the Secret class containing a key and
|
|
94
|
+
value.
|
|
95
|
+
id : str | None, default=None
|
|
96
|
+
ID of the secrets collection. If not provided, a unique ID will be
|
|
97
|
+
generated.
|
|
98
|
+
name : str | None, default=None
|
|
99
|
+
Name of the secrets collection. If not provided, the ID will be
|
|
100
|
+
used.
|
|
101
|
+
description : Optional[str], default=None
|
|
102
|
+
Description of the secrets collection.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
SecretsCollectionSummary
|
|
107
|
+
Summary of the secrets collection including its metadata.
|
|
108
|
+
|
|
109
|
+
Raises
|
|
110
|
+
------
|
|
111
|
+
ValueError
|
|
112
|
+
If no secrets are provided.
|
|
113
|
+
requests.HTTPError
|
|
114
|
+
If the response status code is not 2xx.
|
|
115
|
+
|
|
116
|
+
Examples
|
|
117
|
+
--------
|
|
118
|
+
>>> # Create a new secrets collection with API keys
|
|
119
|
+
>>> from nextmv.cloud import Secret
|
|
120
|
+
>>> secrets = [
|
|
121
|
+
... Secret(
|
|
122
|
+
... location="API_KEY",
|
|
123
|
+
... value="your-api-key",
|
|
124
|
+
... secret_type=SecretType.ENV,
|
|
125
|
+
... ),
|
|
126
|
+
... Secret(
|
|
127
|
+
... location="DATABASE_URL",
|
|
128
|
+
... value="your-database-url",
|
|
129
|
+
... secret_type=SecretType.ENV,
|
|
130
|
+
... ),
|
|
131
|
+
... ]
|
|
132
|
+
>>> collection = app.new_secrets_collection(
|
|
133
|
+
... secrets=secrets,
|
|
134
|
+
... id="api-secrets",
|
|
135
|
+
... name="API Secrets",
|
|
136
|
+
... description="Collection of API secrets for external services"
|
|
137
|
+
... )
|
|
138
|
+
>>> print(collection.id)
|
|
139
|
+
'api-secrets'
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
if len(secrets) == 0:
|
|
143
|
+
raise ValueError("secrets must be provided")
|
|
144
|
+
|
|
145
|
+
if id is None or id == "":
|
|
146
|
+
id = safe_id(prefix="secrets")
|
|
147
|
+
if name is None or name == "":
|
|
148
|
+
name = id
|
|
149
|
+
|
|
150
|
+
payload = {
|
|
151
|
+
"id": id,
|
|
152
|
+
"name": name,
|
|
153
|
+
"secrets": [secret.to_dict() for secret in secrets],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if description is not None:
|
|
157
|
+
payload["description"] = description
|
|
158
|
+
|
|
159
|
+
response = self.client.request(
|
|
160
|
+
method="POST",
|
|
161
|
+
endpoint=f"{self.endpoint}/secrets",
|
|
162
|
+
payload=payload,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return SecretsCollectionSummary.from_dict(response.json())
|
|
166
|
+
|
|
167
|
+
def secrets_collection(self: "Application", secrets_collection_id: str) -> SecretsCollection:
|
|
168
|
+
"""
|
|
169
|
+
Get a secrets collection.
|
|
170
|
+
|
|
171
|
+
This method retrieves a secrets collection by its ID. A secrets collection
|
|
172
|
+
is a group of key-value pairs that can be used by your application
|
|
173
|
+
instances during execution.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
secrets_collection_id : str
|
|
178
|
+
ID of the secrets collection to retrieve.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
SecretsCollection
|
|
183
|
+
The requested secrets collection, including all secret values
|
|
184
|
+
and metadata.
|
|
185
|
+
|
|
186
|
+
Raises
|
|
187
|
+
------
|
|
188
|
+
requests.HTTPError
|
|
189
|
+
If the response status code is not 2xx.
|
|
190
|
+
|
|
191
|
+
Examples
|
|
192
|
+
--------
|
|
193
|
+
>>> # Retrieve a secrets collection
|
|
194
|
+
>>> collection = app.secrets_collection("api-secrets")
|
|
195
|
+
>>> print(collection.name)
|
|
196
|
+
'API Secrets'
|
|
197
|
+
>>> print(len(collection.secrets))
|
|
198
|
+
2
|
|
199
|
+
>>> for secret in collection.secrets:
|
|
200
|
+
... print(secret.location)
|
|
201
|
+
'API_KEY'
|
|
202
|
+
'DATABASE_URL'
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
response = self.client.request(
|
|
206
|
+
method="GET",
|
|
207
|
+
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return SecretsCollection.from_dict(response.json())
|
|
211
|
+
|
|
212
|
+
def update_secrets_collection(
|
|
213
|
+
self: "Application",
|
|
214
|
+
secrets_collection_id: str,
|
|
215
|
+
name: str | None = None,
|
|
216
|
+
description: str | None = None,
|
|
217
|
+
secrets: list[Secret | dict[str, Any]] | None = None,
|
|
218
|
+
) -> SecretsCollectionSummary:
|
|
219
|
+
"""
|
|
220
|
+
Update a secrets collection.
|
|
221
|
+
|
|
222
|
+
This method updates an existing secrets collection with new values for name,
|
|
223
|
+
description, and secrets. A secrets collection is a group of key-value pairs
|
|
224
|
+
that can be used by your application instances during execution.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
secrets_collection_id : str
|
|
229
|
+
ID of the secrets collection to update.
|
|
230
|
+
name : Optional[str], default=None
|
|
231
|
+
Optional new name for the secrets collection.
|
|
232
|
+
description : Optional[str], default=None
|
|
233
|
+
Optional new description for the secrets collection.
|
|
234
|
+
secrets : Optional[list[Secret | dict[str, Any]]], default=None
|
|
235
|
+
Optional list of secrets to update. Each secret should be an
|
|
236
|
+
instance of the Secret class containing a key and value.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
SecretsCollectionSummary
|
|
241
|
+
Summary of the updated secrets collection including its metadata.
|
|
242
|
+
|
|
243
|
+
Raises
|
|
244
|
+
------
|
|
245
|
+
ValueError
|
|
246
|
+
If no secrets are provided.
|
|
247
|
+
requests.HTTPError
|
|
248
|
+
If the response status code is not 2xx.
|
|
249
|
+
|
|
250
|
+
Examples
|
|
251
|
+
--------
|
|
252
|
+
>>> # Update an existing secrets collection
|
|
253
|
+
>>> from nextmv.cloud import Secret
|
|
254
|
+
>>> updated_secrets = [
|
|
255
|
+
... Secret(key="API_KEY", value="new-api-key"),
|
|
256
|
+
... Secret(key="DATABASE_URL", value="new-database-url")
|
|
257
|
+
... ]
|
|
258
|
+
>>> updated_collection = app.update_secrets_collection(
|
|
259
|
+
... secrets_collection_id="api-secrets",
|
|
260
|
+
... name="Updated API Secrets",
|
|
261
|
+
... description="Updated collection of API secrets",
|
|
262
|
+
... secrets=updated_secrets
|
|
263
|
+
... )
|
|
264
|
+
>>> print(updated_collection.id)
|
|
265
|
+
'api-secrets'
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
collection = self.secrets_collection(secrets_collection_id)
|
|
269
|
+
collection_dict = collection.to_dict()
|
|
270
|
+
payload = collection_dict.copy()
|
|
271
|
+
|
|
272
|
+
if name is not None:
|
|
273
|
+
payload["name"] = name
|
|
274
|
+
if description is not None:
|
|
275
|
+
payload["description"] = description
|
|
276
|
+
if secrets is not None and len(secrets) > 0:
|
|
277
|
+
secrets_dicts = []
|
|
278
|
+
for ix, secret in enumerate(secrets):
|
|
279
|
+
if isinstance(secret, dict):
|
|
280
|
+
secrets_dicts.append(secret)
|
|
281
|
+
elif isinstance(secret, Secret):
|
|
282
|
+
secrets_dicts.append(secret.to_dict())
|
|
283
|
+
else:
|
|
284
|
+
raise ValueError(f"secret at index {ix} must be either a Secret or dict object")
|
|
285
|
+
|
|
286
|
+
payload["secrets"] = secrets_dicts
|
|
287
|
+
|
|
288
|
+
response = self.client.request(
|
|
289
|
+
method="PUT",
|
|
290
|
+
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
|
|
291
|
+
payload=payload,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return SecretsCollectionSummary.from_dict(response.json())
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application mixin for managing shadow tests.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from nextmv.cloud.shadow import ShadowTest, ShadowTestMetadata, StartEvents, StopIntent, TerminationEvents
|
|
8
|
+
from nextmv.run import Run
|
|
9
|
+
from nextmv.safe import safe_id
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from . import Application
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApplicationShadowMixin:
|
|
16
|
+
"""
|
|
17
|
+
Mixin class for managing shadow tests within an application.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def shadow_test(self: "Application", shadow_test_id: str) -> ShadowTest:
|
|
21
|
+
"""
|
|
22
|
+
Get a shadow test. This method also returns the runs of the shadow
|
|
23
|
+
test under the `.runs` attribute.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
shadow_test_id : str
|
|
28
|
+
ID of the shadow test.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
ShadowTest
|
|
33
|
+
The requested shadow test details.
|
|
34
|
+
|
|
35
|
+
Raises
|
|
36
|
+
------
|
|
37
|
+
requests.HTTPError
|
|
38
|
+
If the response status code is not 2xx.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> shadow_test = app.shadow_test("shadow-123")
|
|
43
|
+
>>> print(shadow_test.name)
|
|
44
|
+
'My Shadow Test'
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
response = self.client.request(
|
|
48
|
+
method="GET",
|
|
49
|
+
endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
exp = ShadowTest.from_dict(response.json())
|
|
53
|
+
|
|
54
|
+
runs_response = self.client.request(
|
|
55
|
+
method="GET",
|
|
56
|
+
endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/runs",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
runs = [Run.from_dict(run) for run in runs_response.json().get("runs", [])]
|
|
60
|
+
exp.runs = runs
|
|
61
|
+
|
|
62
|
+
return exp
|
|
63
|
+
|
|
64
|
+
def shadow_test_metadata(self: "Application", shadow_test_id: str) -> ShadowTestMetadata:
|
|
65
|
+
"""
|
|
66
|
+
Get metadata for a shadow test.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
shadow_test_id : str
|
|
71
|
+
ID of the shadow test.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
ShadowTestMetadata
|
|
76
|
+
The requested shadow test metadata.
|
|
77
|
+
|
|
78
|
+
Raises
|
|
79
|
+
------
|
|
80
|
+
requests.HTTPError
|
|
81
|
+
If the response status code is not 2xx.
|
|
82
|
+
|
|
83
|
+
Examples
|
|
84
|
+
--------
|
|
85
|
+
>>> metadata = app.shadow_test_metadata("shadow-123")
|
|
86
|
+
>>> print(metadata.name)
|
|
87
|
+
'My Shadow Test'
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
response = self.client.request(
|
|
91
|
+
method="GET",
|
|
92
|
+
endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/metadata",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return ShadowTestMetadata.from_dict(response.json())
|
|
96
|
+
|
|
97
|
+
def delete_shadow_test(self: "Application", shadow_test_id: str) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Delete a shadow test.
|
|
100
|
+
|
|
101
|
+
Deletes a shadow test along with all the associated information,
|
|
102
|
+
such as its runs.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
shadow_test_id : str
|
|
107
|
+
ID of the shadow test to delete.
|
|
108
|
+
|
|
109
|
+
Raises
|
|
110
|
+
------
|
|
111
|
+
requests.HTTPError
|
|
112
|
+
If the response status code is not 2xx.
|
|
113
|
+
|
|
114
|
+
Examples
|
|
115
|
+
--------
|
|
116
|
+
>>> app.delete_shadow_test("shadow-123")
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
_ = self.client.request(
|
|
120
|
+
method="DELETE",
|
|
121
|
+
endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def list_shadow_tests(self: "Application") -> list[ShadowTest]:
|
|
125
|
+
"""
|
|
126
|
+
List all shadow tests.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
list[ShadowTest]
|
|
131
|
+
List of shadow tests.
|
|
132
|
+
|
|
133
|
+
Raises
|
|
134
|
+
------
|
|
135
|
+
requests.HTTPError
|
|
136
|
+
If the response status code is not 2xx.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
response = self.client.request(
|
|
140
|
+
method="GET",
|
|
141
|
+
endpoint=f"{self.experiments_endpoint}/shadow",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return [ShadowTest.from_dict(shadow_test) for shadow_test in response.json()]
|
|
145
|
+
|
|
146
|
+
def new_shadow_test(
|
|
147
|
+
self: "Application",
|
|
148
|
+
comparisons: dict[str, list[str]],
|
|
149
|
+
termination_events: TerminationEvents,
|
|
150
|
+
shadow_test_id: str | None = None,
|
|
151
|
+
name: str | None = None,
|
|
152
|
+
description: str | None = None,
|
|
153
|
+
start_events: StartEvents | None = None,
|
|
154
|
+
) -> ShadowTest:
|
|
155
|
+
"""
|
|
156
|
+
Create a new shadow test in draft mode. Shadow tests are experiments
|
|
157
|
+
that run instances in parallel to compare their results.
|
|
158
|
+
|
|
159
|
+
Use the `comparisons` parameter to define how to set up instance
|
|
160
|
+
comparisons. The keys of the `comparisons` dictionary are the baseline
|
|
161
|
+
instance IDs, and the values are the candidate lists of instance IDs to
|
|
162
|
+
compare against the respective baseline.
|
|
163
|
+
|
|
164
|
+
You may specify `start_events` to make the shadow test start at a
|
|
165
|
+
specific time. Alternatively, you may use the `start_shadow_test`
|
|
166
|
+
method to start the test.
|
|
167
|
+
|
|
168
|
+
The `termination_events` parameter is required and provides control
|
|
169
|
+
over when the shadow test should terminate. Alternatively, you may use
|
|
170
|
+
the `stop_shadow_test` method to stop the test.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
comparisons : dict[str, list[str]]
|
|
175
|
+
Dictionary defining the baseline and candidate instance IDs for
|
|
176
|
+
comparison. The keys are baseline instance IDs, and the values are
|
|
177
|
+
lists of candidate instance IDs to compare against the respective
|
|
178
|
+
baseline.
|
|
179
|
+
termination_events : TerminationEvents
|
|
180
|
+
Termination events for the shadow test.
|
|
181
|
+
shadow_test_id : Optional[str]
|
|
182
|
+
ID of the shadow test. Will be generated if not provided.
|
|
183
|
+
name : Optional[str]
|
|
184
|
+
Name of the shadow test. If not provided, the ID will be used as
|
|
185
|
+
the name.
|
|
186
|
+
description : Optional[str]
|
|
187
|
+
Optional description of the shadow test.
|
|
188
|
+
start_events : Optional[StartEvents]
|
|
189
|
+
Start events for the shadow test.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
ShadowTest
|
|
194
|
+
The created shadow test.
|
|
195
|
+
|
|
196
|
+
Raises
|
|
197
|
+
------
|
|
198
|
+
requests.HTTPError
|
|
199
|
+
If the response status code is not 2xx.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
# Generate ID if not provided
|
|
203
|
+
if shadow_test_id is None:
|
|
204
|
+
shadow_test_id = safe_id("shadow")
|
|
205
|
+
|
|
206
|
+
# Use ID as name if name not provided
|
|
207
|
+
if name is None or name == "":
|
|
208
|
+
name = shadow_test_id
|
|
209
|
+
|
|
210
|
+
payload = {
|
|
211
|
+
"id": shadow_test_id,
|
|
212
|
+
"name": name,
|
|
213
|
+
"comparisons": comparisons,
|
|
214
|
+
"termination_events": termination_events.to_dict(),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if description is not None:
|
|
218
|
+
payload["description"] = description
|
|
219
|
+
if start_events is not None:
|
|
220
|
+
payload["start_events"] = start_events.to_dict()
|
|
221
|
+
|
|
222
|
+
response = self.client.request(
|
|
223
|
+
method="POST",
|
|
224
|
+
endpoint=f"{self.experiments_endpoint}/shadow",
|
|
225
|
+
payload=payload,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return ShadowTest.from_dict(response.json())
|
|
229
|
+
|
|
230
|
+
def start_shadow_test(self: "Application", shadow_test_id: str) -> None:
|
|
231
|
+
"""
|
|
232
|
+
Start a shadow test. Create a shadow test in draft mode using the
|
|
233
|
+
`new_shadow_test` method, then use this method to start the test.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
shadow_test_id : str
|
|
238
|
+
ID of the shadow test to start.
|
|
239
|
+
|
|
240
|
+
Raises
|
|
241
|
+
------
|
|
242
|
+
requests.HTTPError
|
|
243
|
+
If the response status code is not 2xx.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
_ = self.client.request(
|
|
247
|
+
method="PUT",
|
|
248
|
+
endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/start",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def stop_shadow_test(self: "Application", shadow_test_id: str, intent: StopIntent) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Stop a shadow test. The test should already have started before using
|
|
254
|
+
this method.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
shadow_test_id : str
|
|
259
|
+
ID of the shadow test to stop.
|
|
260
|
+
intent : StopIntent
|
|
261
|
+
Intent for stopping the shadow test.
|
|
262
|
+
Raises
|
|
263
|
+
------
|
|
264
|
+
requests.HTTPError
|
|
265
|
+
If the response status code is not 2xx.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
payload = {
|
|
269
|
+
"intent": intent.value,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_ = self.client.request(
|
|
273
|
+
method="PUT",
|
|
274
|
+
endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/stop",
|
|
275
|
+
payload=payload,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def update_shadow_test(
|
|
279
|
+
self: "Application",
|
|
280
|
+
shadow_test_id: str,
|
|
281
|
+
name: str | None = None,
|
|
282
|
+
description: str | None = None,
|
|
283
|
+
) -> ShadowTest:
|
|
284
|
+
"""
|
|
285
|
+
Update a shadow test.
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
shadow_test_id : str
|
|
290
|
+
ID of the shadow test to update.
|
|
291
|
+
name : Optional[str], default=None
|
|
292
|
+
Optional name of the shadow test.
|
|
293
|
+
description : Optional[str], default=None
|
|
294
|
+
Optional description of the shadow test.
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
ShadowTest
|
|
299
|
+
The information with the updated shadow test.
|
|
300
|
+
|
|
301
|
+
Raises
|
|
302
|
+
------
|
|
303
|
+
requests.HTTPError
|
|
304
|
+
If the response status code is not 2xx.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
payload = {}
|
|
308
|
+
|
|
309
|
+
if name is not None:
|
|
310
|
+
payload["name"] = name
|
|
311
|
+
if description is not None:
|
|
312
|
+
payload["description"] = description
|
|
313
|
+
|
|
314
|
+
response = self.client.request(
|
|
315
|
+
method="PATCH",
|
|
316
|
+
endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}",
|
|
317
|
+
payload=payload,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return ShadowTest.from_dict(response.json())
|