nextmv 0.18.0__py3-none-any.whl → 1.0.0.dev2__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.
Files changed (175) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +8 -13
  3. nextmv/__init__.py +53 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +54 -9
  6. nextmv/cli/CONTRIBUTING.md +511 -0
  7. nextmv/cli/__init__.py +0 -0
  8. nextmv/cli/cloud/__init__.py +47 -0
  9. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  10. nextmv/cli/cloud/acceptance/create.py +393 -0
  11. nextmv/cli/cloud/acceptance/delete.py +68 -0
  12. nextmv/cli/cloud/acceptance/get.py +104 -0
  13. nextmv/cli/cloud/acceptance/list.py +62 -0
  14. nextmv/cli/cloud/acceptance/update.py +95 -0
  15. nextmv/cli/cloud/account/__init__.py +28 -0
  16. nextmv/cli/cloud/account/create.py +83 -0
  17. nextmv/cli/cloud/account/delete.py +60 -0
  18. nextmv/cli/cloud/account/get.py +66 -0
  19. nextmv/cli/cloud/account/update.py +70 -0
  20. nextmv/cli/cloud/app/__init__.py +35 -0
  21. nextmv/cli/cloud/app/create.py +141 -0
  22. nextmv/cli/cloud/app/delete.py +58 -0
  23. nextmv/cli/cloud/app/exists.py +44 -0
  24. nextmv/cli/cloud/app/get.py +66 -0
  25. nextmv/cli/cloud/app/list.py +61 -0
  26. nextmv/cli/cloud/app/push.py +137 -0
  27. nextmv/cli/cloud/app/update.py +124 -0
  28. nextmv/cli/cloud/batch/__init__.py +29 -0
  29. nextmv/cli/cloud/batch/create.py +454 -0
  30. nextmv/cli/cloud/batch/delete.py +68 -0
  31. nextmv/cli/cloud/batch/get.py +104 -0
  32. nextmv/cli/cloud/batch/list.py +63 -0
  33. nextmv/cli/cloud/batch/metadata.py +66 -0
  34. nextmv/cli/cloud/batch/update.py +95 -0
  35. nextmv/cli/cloud/data/__init__.py +26 -0
  36. nextmv/cli/cloud/data/upload.py +162 -0
  37. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  38. nextmv/cli/cloud/ensemble/create.py +414 -0
  39. nextmv/cli/cloud/ensemble/delete.py +67 -0
  40. nextmv/cli/cloud/ensemble/get.py +65 -0
  41. nextmv/cli/cloud/ensemble/update.py +103 -0
  42. nextmv/cli/cloud/input_set/__init__.py +30 -0
  43. nextmv/cli/cloud/input_set/create.py +170 -0
  44. nextmv/cli/cloud/input_set/get.py +63 -0
  45. nextmv/cli/cloud/input_set/list.py +63 -0
  46. nextmv/cli/cloud/input_set/update.py +123 -0
  47. nextmv/cli/cloud/instance/__init__.py +35 -0
  48. nextmv/cli/cloud/instance/create.py +290 -0
  49. nextmv/cli/cloud/instance/delete.py +62 -0
  50. nextmv/cli/cloud/instance/exists.py +39 -0
  51. nextmv/cli/cloud/instance/get.py +62 -0
  52. nextmv/cli/cloud/instance/list.py +60 -0
  53. nextmv/cli/cloud/instance/update.py +216 -0
  54. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  55. nextmv/cli/cloud/managed_input/create.py +146 -0
  56. nextmv/cli/cloud/managed_input/delete.py +65 -0
  57. nextmv/cli/cloud/managed_input/get.py +63 -0
  58. nextmv/cli/cloud/managed_input/list.py +60 -0
  59. nextmv/cli/cloud/managed_input/update.py +97 -0
  60. nextmv/cli/cloud/run/__init__.py +37 -0
  61. nextmv/cli/cloud/run/cancel.py +37 -0
  62. nextmv/cli/cloud/run/create.py +530 -0
  63. nextmv/cli/cloud/run/get.py +199 -0
  64. nextmv/cli/cloud/run/input.py +86 -0
  65. nextmv/cli/cloud/run/list.py +80 -0
  66. nextmv/cli/cloud/run/logs.py +167 -0
  67. nextmv/cli/cloud/run/metadata.py +67 -0
  68. nextmv/cli/cloud/run/track.py +501 -0
  69. nextmv/cli/cloud/scenario/__init__.py +29 -0
  70. nextmv/cli/cloud/scenario/create.py +451 -0
  71. nextmv/cli/cloud/scenario/delete.py +65 -0
  72. nextmv/cli/cloud/scenario/get.py +102 -0
  73. nextmv/cli/cloud/scenario/list.py +63 -0
  74. nextmv/cli/cloud/scenario/metadata.py +67 -0
  75. nextmv/cli/cloud/scenario/update.py +93 -0
  76. nextmv/cli/cloud/secrets/__init__.py +33 -0
  77. nextmv/cli/cloud/secrets/create.py +206 -0
  78. nextmv/cli/cloud/secrets/delete.py +67 -0
  79. nextmv/cli/cloud/secrets/get.py +66 -0
  80. nextmv/cli/cloud/secrets/list.py +60 -0
  81. nextmv/cli/cloud/secrets/update.py +147 -0
  82. nextmv/cli/cloud/shadow/__init__.py +33 -0
  83. nextmv/cli/cloud/shadow/create.py +184 -0
  84. nextmv/cli/cloud/shadow/delete.py +68 -0
  85. nextmv/cli/cloud/shadow/get.py +61 -0
  86. nextmv/cli/cloud/shadow/list.py +63 -0
  87. nextmv/cli/cloud/shadow/metadata.py +66 -0
  88. nextmv/cli/cloud/shadow/start.py +43 -0
  89. nextmv/cli/cloud/shadow/stop.py +43 -0
  90. nextmv/cli/cloud/shadow/update.py +95 -0
  91. nextmv/cli/cloud/upload/__init__.py +22 -0
  92. nextmv/cli/cloud/upload/create.py +39 -0
  93. nextmv/cli/cloud/version/__init__.py +33 -0
  94. nextmv/cli/cloud/version/create.py +97 -0
  95. nextmv/cli/cloud/version/delete.py +62 -0
  96. nextmv/cli/cloud/version/exists.py +39 -0
  97. nextmv/cli/cloud/version/get.py +62 -0
  98. nextmv/cli/cloud/version/list.py +60 -0
  99. nextmv/cli/cloud/version/update.py +92 -0
  100. nextmv/cli/community/__init__.py +24 -0
  101. nextmv/cli/community/clone.py +270 -0
  102. nextmv/cli/community/list.py +265 -0
  103. nextmv/cli/configuration/__init__.py +23 -0
  104. nextmv/cli/configuration/config.py +195 -0
  105. nextmv/cli/configuration/create.py +94 -0
  106. nextmv/cli/configuration/delete.py +67 -0
  107. nextmv/cli/configuration/list.py +77 -0
  108. nextmv/cli/main.py +188 -0
  109. nextmv/cli/message.py +153 -0
  110. nextmv/cli/options.py +206 -0
  111. nextmv/cli/version.py +38 -0
  112. nextmv/cloud/__init__.py +71 -17
  113. nextmv/cloud/acceptance_test.py +757 -51
  114. nextmv/cloud/account.py +406 -17
  115. nextmv/cloud/application/__init__.py +957 -0
  116. nextmv/cloud/application/_acceptance.py +419 -0
  117. nextmv/cloud/application/_batch_scenario.py +860 -0
  118. nextmv/cloud/application/_ensemble.py +251 -0
  119. nextmv/cloud/application/_input_set.py +227 -0
  120. nextmv/cloud/application/_instance.py +289 -0
  121. nextmv/cloud/application/_managed_input.py +227 -0
  122. nextmv/cloud/application/_run.py +1393 -0
  123. nextmv/cloud/application/_secrets.py +294 -0
  124. nextmv/cloud/application/_shadow.py +314 -0
  125. nextmv/cloud/application/_utils.py +54 -0
  126. nextmv/cloud/application/_version.py +303 -0
  127. nextmv/cloud/assets.py +48 -0
  128. nextmv/cloud/batch_experiment.py +294 -33
  129. nextmv/cloud/client.py +307 -66
  130. nextmv/cloud/ensemble.py +247 -0
  131. nextmv/cloud/input_set.py +120 -2
  132. nextmv/cloud/instance.py +133 -8
  133. nextmv/cloud/integration.py +533 -0
  134. nextmv/cloud/package.py +168 -53
  135. nextmv/cloud/scenario.py +410 -0
  136. nextmv/cloud/secrets.py +234 -0
  137. nextmv/cloud/shadow.py +190 -0
  138. nextmv/cloud/url.py +73 -0
  139. nextmv/cloud/version.py +132 -4
  140. nextmv/default_app/.gitignore +1 -0
  141. nextmv/default_app/README.md +32 -0
  142. nextmv/default_app/app.yaml +12 -0
  143. nextmv/default_app/input.json +5 -0
  144. nextmv/default_app/main.py +37 -0
  145. nextmv/default_app/requirements.txt +2 -0
  146. nextmv/default_app/src/__init__.py +0 -0
  147. nextmv/default_app/src/visuals.py +36 -0
  148. nextmv/deprecated.py +47 -0
  149. nextmv/input.py +861 -90
  150. nextmv/local/__init__.py +5 -0
  151. nextmv/local/application.py +1251 -0
  152. nextmv/local/executor.py +1042 -0
  153. nextmv/local/geojson_handler.py +323 -0
  154. nextmv/local/local.py +97 -0
  155. nextmv/local/plotly_handler.py +61 -0
  156. nextmv/local/runner.py +274 -0
  157. nextmv/logger.py +80 -9
  158. nextmv/manifest.py +1466 -0
  159. nextmv/model.py +241 -66
  160. nextmv/options.py +708 -115
  161. nextmv/output.py +1301 -274
  162. nextmv/polling.py +325 -0
  163. nextmv/run.py +1702 -0
  164. nextmv/safe.py +145 -0
  165. nextmv/status.py +122 -0
  166. nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
  167. nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
  168. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
  169. nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
  170. nextmv/cloud/application.py +0 -1405
  171. nextmv/cloud/manifest.py +0 -234
  172. nextmv/cloud/status.py +0 -29
  173. nextmv-0.18.0.dist-info/METADATA +0 -770
  174. nextmv-0.18.0.dist-info/RECORD +0 -25
  175. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -1,1405 +0,0 @@
1
- """This module contains the application class."""
2
-
3
- import json
4
- import shutil
5
- import time
6
- from dataclasses import dataclass
7
- from datetime import datetime
8
- from typing import Any, Optional, Union
9
-
10
- import requests
11
-
12
- from nextmv.base_model import BaseModel
13
- from nextmv.cloud import package
14
- from nextmv.cloud.acceptance_test import AcceptanceTest, ExperimentStatus, Metric
15
- from nextmv.cloud.batch_experiment import BatchExperiment, BatchExperimentMetadata, BatchExperimentRun
16
- from nextmv.cloud.client import Client, get_size
17
- from nextmv.cloud.input_set import InputSet
18
- from nextmv.cloud.instance import Configuration, Instance
19
- from nextmv.cloud.manifest import Manifest
20
- from nextmv.cloud.status import Status, StatusV2
21
- from nextmv.cloud.version import Version
22
- from nextmv.logger import log
23
- from nextmv.model import Model, ModelConfiguration
24
-
25
- _MAX_RUN_SIZE: int = 5 * 1024 * 1024
26
- """Maximum size of the run input/output. This value is used to determine
27
- whether to use the large input upload and/or result download endpoints."""
28
-
29
-
30
- class DownloadURL(BaseModel):
31
- """Result of getting a download URL."""
32
-
33
- url: str
34
- """URL to use for downloading the file."""
35
-
36
-
37
- class ErrorLog(BaseModel):
38
- """Error log of a run, when it was not successful."""
39
-
40
- error: Optional[str] = None
41
- """Error message."""
42
- stdout: Optional[str] = None
43
- """Standard output."""
44
- stderr: Optional[str] = None
45
- """Standard error."""
46
-
47
-
48
- class Metadata(BaseModel):
49
- """Metadata of a run, whether it was successful or not."""
50
-
51
- application_id: str
52
- """ID of the application where the run was submitted to."""
53
- application_instance_id: str
54
- """ID of the instance where the run was submitted to."""
55
- application_version_id: str
56
- """ID of the version of the application where the run was submitted to."""
57
- created_at: datetime
58
- """Date and time when the run was created."""
59
- duration: float
60
- """Duration of the run in milliseconds."""
61
- error: str
62
- """Error message if the run failed."""
63
- input_size: float
64
- """Size of the input in bytes."""
65
- output_size: float
66
- """Size of the output in bytes."""
67
- status: Status
68
- """Deprecated: use status_v2."""
69
- status_v2: StatusV2
70
- """Status of the run."""
71
-
72
-
73
- class PollingOptions(BaseModel):
74
- """Options to use when polling for a run result."""
75
-
76
- backoff: float = 1
77
- """Backoff factor to use between polls. Leave this at 1 to poll at a
78
- constant rate."""
79
- delay: float = 1
80
- """Delay to use between polls, in seconds."""
81
- initial_delay: float = 1
82
- """Initial delay to use before starting the polling strategy, in
83
- seconds."""
84
- max_delay: float = 20
85
- """Maximum delay to use between polls, in seconds. This parameter is
86
- activated when the backoff parameter is greater than 1, such that the delay
87
- is increasing after each poll."""
88
- max_duration: float = 300
89
- """Maximum duration of the polling strategy, in seconds."""
90
- max_tries: int = 20
91
- """Maximum number of tries to use."""
92
-
93
-
94
- _DEFAULT_POLLING_OPTIONS: PollingOptions = PollingOptions()
95
- """Default polling options to use when polling for a run result."""
96
-
97
-
98
- class RunInformation(BaseModel):
99
- """Information of a run."""
100
-
101
- description: str
102
- """Description of the run."""
103
- id: str
104
- """ID of the run."""
105
- metadata: Metadata
106
- """Metadata of the run."""
107
- name: str
108
- """Name of the run."""
109
- user_email: str
110
- """Email of the user who submitted the run."""
111
-
112
-
113
- class RunResult(RunInformation):
114
- """Result of a run, whether it was successful or not."""
115
-
116
- error_log: Optional[ErrorLog] = None
117
- """Error log of the run. Only available if the run failed."""
118
- output: Optional[dict[str, Any]] = None
119
- """Output of the run. Only available if the run succeeded."""
120
-
121
-
122
- class RunLog(BaseModel):
123
- """Log of a run."""
124
-
125
- log: str
126
- """Log of the run."""
127
-
128
-
129
- class UploadURL(BaseModel):
130
- """Result of getting an upload URL."""
131
-
132
- upload_id: str
133
- """ID of the upload."""
134
- upload_url: str
135
- """URL to use for uploading the file."""
136
-
137
-
138
- @dataclass
139
- class Application:
140
- """An application is a published decision model that can be executed."""
141
-
142
- client: Client
143
- """Client to use for interacting with the Nextmv Cloud API."""
144
- id: str
145
- """ID of the application."""
146
-
147
- default_instance_id: str = "devint"
148
- """Default instance ID to use for submitting runs."""
149
- endpoint: str = "v1/applications/{id}"
150
- """Base endpoint for the application."""
151
- experiments_endpoint: str = "{base}/experiments"
152
- """Base endpoint for the experiments in the application."""
153
-
154
- def __post_init__(self):
155
- """Logic to run after the class is initialized."""
156
-
157
- self.endpoint = self.endpoint.format(id=self.id)
158
- self.experiments_endpoint = self.experiments_endpoint.format(base=self.endpoint)
159
-
160
- def acceptance_test(self, acceptance_test_id: str) -> AcceptanceTest:
161
- """
162
- Get an acceptance test.
163
-
164
- Args:
165
- acceptance_test_id: ID of the acceptance test.
166
-
167
- Returns:
168
- Acceptance test.
169
-
170
- Raises:
171
- requests.HTTPError: If the response status code is not 2xx.
172
- """
173
-
174
- response = self.client.request(
175
- method="GET",
176
- endpoint=f"{self.experiments_endpoint}/acceptance/{acceptance_test_id}",
177
- )
178
-
179
- return AcceptanceTest.from_dict(response.json())
180
-
181
- def batch_experiment(self, batch_id: str) -> BatchExperiment:
182
- """
183
- Get a batch experiment.
184
-
185
- Args:
186
- batch_id: ID of the batch experiment.
187
-
188
- Returns:
189
- Batch experiment.
190
-
191
- Raises:
192
- requests.HTTPError: If the response status code is not 2xx.
193
- """
194
-
195
- response = self.client.request(
196
- method="GET",
197
- endpoint=f"{self.experiments_endpoint}/batch/{batch_id}",
198
- )
199
-
200
- return BatchExperiment.from_dict(response.json())
201
-
202
- def cancel_run(self, run_id: str) -> None:
203
- """
204
- Cancel a run.
205
-
206
- Args:
207
- run_id: ID of the run.
208
-
209
- Raises:
210
- requests.HTTPError: If the response status code is not 2xx.
211
- """
212
-
213
- _ = self.client.request(
214
- method="PATCH",
215
- endpoint=f"{self.endpoint}/runs/{run_id}/cancel",
216
- )
217
-
218
- def delete_batch_experiment(self, batch_id: str) -> None:
219
- """
220
- Deletes a batch experiment, along with all the associated information,
221
- such as its runs.
222
-
223
- Args:
224
- batch_id: ID of the batch experiment.
225
-
226
- Raises:
227
- requests.HTTPError: If the response status code is not 2xx.
228
- """
229
-
230
- _ = self.client.request(
231
- method="DELETE",
232
- endpoint=f"{self.experiments_endpoint}/batch/{batch_id}",
233
- )
234
-
235
- def delete_acceptance_test(self, acceptance_test_id: str) -> None:
236
- """
237
- Deletes an acceptance test, along with all the associated information
238
- such as the underlying batch experiment.
239
-
240
- Args:
241
- acceptance_test_id: ID of the acceptance test.
242
-
243
- Raises:
244
- requests.HTTPError: If the response status code is not 2xx.
245
- """
246
-
247
- _ = self.client.request(
248
- method="DELETE",
249
- endpoint=f"{self.experiments_endpoint}/acceptance/{acceptance_test_id}",
250
- )
251
-
252
- def input_set(self, input_set_id: str) -> InputSet:
253
- """
254
- Get an input set.
255
-
256
- Args:
257
- input_set_id: ID of the input set.
258
-
259
- Returns:
260
- Input set.
261
-
262
- Raises:
263
- requests.HTTPError: If the response status code is not 2xx.
264
- """
265
-
266
- response = self.client.request(
267
- method="GET",
268
- endpoint=f"{self.experiments_endpoint}/inputsets/{input_set_id}",
269
- )
270
-
271
- return InputSet.from_dict(response.json())
272
-
273
- def instance(self, instance_id: str) -> Instance:
274
- """
275
- Get an instance.
276
-
277
- Args:
278
- instance_id: ID of the instance.
279
-
280
- Returns:
281
- Instance.
282
-
283
- Raises:
284
- requests.HTTPError: If the response status code is not 2xx.
285
- """
286
-
287
- response = self.client.request(
288
- method="GET",
289
- endpoint=f"{self.endpoint}/instances/{instance_id}",
290
- )
291
-
292
- return Instance.from_dict(response.json())
293
-
294
- def list_acceptance_tests(self) -> list[AcceptanceTest]:
295
- """
296
- List all acceptance tests.
297
-
298
- Returns:
299
- List of acceptance tests.
300
-
301
- Raises:
302
- requests.HTTPError: If the response status code is not 2xx.
303
- """
304
-
305
- response = self.client.request(
306
- method="GET",
307
- endpoint=f"{self.experiments_endpoint}/acceptance",
308
- )
309
-
310
- return [AcceptanceTest.from_dict(acceptance_test) for acceptance_test in response.json()]
311
-
312
- def list_batch_experiments(self) -> list[BatchExperimentMetadata]:
313
- """
314
- List all batch experiments.
315
-
316
- Returns:
317
- List of batch experiments.
318
-
319
- Raises:
320
- requests.HTTPError: If the response status code is not 2xx.
321
- """
322
-
323
- response = self.client.request(
324
- method="GET",
325
- endpoint=f"{self.experiments_endpoint}/batch",
326
- )
327
-
328
- return [BatchExperimentMetadata.from_dict(batch_experiment) for batch_experiment in response.json()]
329
-
330
- def list_input_sets(self) -> list[InputSet]:
331
- """
332
- List all input sets.
333
-
334
- Returns:
335
- List of input sets.
336
-
337
- Raises:
338
- requests.HTTPError: If the response status code is not 2xx.
339
- """
340
-
341
- response = self.client.request(
342
- method="GET",
343
- endpoint=f"{self.experiments_endpoint}/inputsets",
344
- )
345
-
346
- return [InputSet.from_dict(input_set) for input_set in response.json()]
347
-
348
- def list_instances(self) -> list[Instance]:
349
- """
350
- List all instances.
351
-
352
- Returns:
353
- List of instances.
354
-
355
- Raises:
356
- requests.HTTPError: If the response status code is not 2xx.
357
- """
358
-
359
- response = self.client.request(
360
- method="GET",
361
- endpoint=f"{self.endpoint}/instances",
362
- )
363
-
364
- return [Instance.from_dict(instance) for instance in response.json()]
365
-
366
- def list_versions(self) -> list[Version]:
367
- """
368
- List all versions.
369
-
370
- Returns:
371
- List of versions.
372
-
373
- Raises:
374
- requests.HTTPError: If the response status code is not 2xx.
375
- """
376
-
377
- response = self.client.request(
378
- method="GET",
379
- endpoint=f"{self.endpoint}/versions",
380
- )
381
-
382
- return [Version.from_dict(version) for version in response.json()]
383
-
384
- def new_acceptance_test(
385
- self,
386
- candidate_instance_id: str,
387
- baseline_instance_id: str,
388
- id: str,
389
- metrics: list[Union[Metric, dict[str, Any]]],
390
- name: str,
391
- input_set_id: Optional[str] = None,
392
- description: Optional[str] = None,
393
- ) -> AcceptanceTest:
394
- """
395
- Create a new acceptance test. The acceptance test is based on a batch
396
- experiment. If you already started a batch experiment, you don't need
397
- to provide the input_set_id parameter. In that case, the ID of the
398
- acceptance test and the batch experiment must be the same. If the batch
399
- experiment does not exist, you can provide the input_set_id parameter
400
- and a new batch experiment will be created for you.
401
-
402
- Args:
403
- candidate_instance_id: ID of the candidate instance.
404
- baseline_instance_id: ID of the baseline instance.
405
- id: ID of the acceptance test.
406
- metrics: List of metrics to use for the acceptance test.
407
- name: Name of the acceptance test.
408
- input_set_id: ID of the input set to use for the underlying batch
409
- experiment, in case it hasn't been started.
410
- description: Description of the acceptance test.
411
-
412
- Returns:
413
- Acceptance test.
414
-
415
- Raises:
416
- requests.HTTPError: If the response status code is not 2xx.
417
- ValueError: If the batch experiment ID does not match the
418
- acceptance test ID.
419
- """
420
-
421
- if input_set_id is None:
422
- try:
423
- batch_experiment = self.batch_experiment(batch_id=id)
424
- batch_experiment_id = batch_experiment.id
425
- except requests.HTTPError as e:
426
- if e.response.status_code != 404:
427
- raise e
428
-
429
- raise ValueError(
430
- f"batch experiment {id} does not exist, input_set_id must be defined to create a new one"
431
- ) from e
432
- else:
433
- batch_experiment_id = self.new_batch_experiment(
434
- name=name,
435
- input_set_id=input_set_id,
436
- instance_ids=[candidate_instance_id, baseline_instance_id],
437
- description=description,
438
- id=id,
439
- )
440
-
441
- if batch_experiment_id != id:
442
- raise ValueError(f"batch experiment_id ({batch_experiment_id}) does not match acceptance test id ({id})")
443
-
444
- payload_metrics = [{}] * len(metrics)
445
- for i, metric in enumerate(metrics):
446
- payload_metrics[i] = metric.to_dict() if isinstance(metric, Metric) else metric
447
-
448
- payload = {
449
- "candidate": {"instance_id": candidate_instance_id},
450
- "control": {"instance_id": baseline_instance_id},
451
- "metrics": payload_metrics,
452
- "experiment_id": batch_experiment_id,
453
- "name": name,
454
- }
455
- if description is not None:
456
- payload["description"] = description
457
- if id is not None:
458
- payload["id"] = id
459
-
460
- response = self.client.request(
461
- method="POST",
462
- endpoint=f"{self.experiments_endpoint}/acceptance",
463
- payload=payload,
464
- )
465
-
466
- return AcceptanceTest.from_dict(response.json())
467
-
468
- def new_acceptance_test_with_result(
469
- self,
470
- candidate_instance_id: str,
471
- baseline_instance_id: str,
472
- id: str,
473
- metrics: list[Union[Metric, dict[str, Any]]],
474
- name: str,
475
- input_set_id: Optional[str] = None,
476
- description: Optional[str] = None,
477
- polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
478
- ) -> AcceptanceTest:
479
- """
480
- Create a new acceptance test and poll for the result. This is a
481
- convenience method that combines the new_acceptance_test with polling
482
- logic to check when the acceptance test is done.
483
-
484
- Args:
485
- candidate_instance_id: ID of the candidate instance.
486
- baseline_instance_id: ID of the baseline instance.
487
- id: ID of the acceptance test.
488
- metrics: List of metrics to use for the acceptance test.
489
- name: Name of the acceptance test.
490
- input_set_id: ID of the input set to use for the underlying batch
491
- experiment, in case it hasn't been started.
492
- description: Description of the acceptance test.
493
- polling_options: Options to use when polling for the run result.
494
-
495
- Returns:
496
- Result of the acceptance test.
497
-
498
- Raises:
499
- requests.HTTPError: If the response status code is not 2xx.
500
- TimeoutError: If the acceptance test does not succeed after the
501
- polling strategy is exhausted based on time duration.
502
- RuntimeError: If the acceptance test does not succeed after the
503
- polling strategy is exhausted based on number of tries.
504
- """
505
- _ = self.new_acceptance_test(
506
- candidate_instance_id=candidate_instance_id,
507
- baseline_instance_id=baseline_instance_id,
508
- id=id,
509
- metrics=metrics,
510
- name=name,
511
- input_set_id=input_set_id,
512
- description=description,
513
- )
514
-
515
- time.sleep(polling_options.initial_delay)
516
- delay = polling_options.delay
517
- polling_ok = False
518
- for _ in range(polling_options.max_tries):
519
- test_information = self.acceptance_test(acceptance_test_id=id)
520
- if test_information.status in [
521
- ExperimentStatus.completed,
522
- ExperimentStatus.failed,
523
- ExperimentStatus.canceled,
524
- ]:
525
- polling_ok = True
526
- break
527
-
528
- if delay > polling_options.max_duration:
529
- raise TimeoutError(
530
- f"acceptance_test {id} did not succeed after {delay} seconds",
531
- )
532
-
533
- sleep_duration = min(delay, polling_options.max_delay)
534
- time.sleep(sleep_duration)
535
- delay *= polling_options.backoff
536
-
537
- if not polling_ok:
538
- raise RuntimeError(
539
- f"acceptance_test {id} did not succeed after {polling_options.max_tries} tries",
540
- )
541
-
542
- return test_information
543
-
544
- def new_batch_experiment(
545
- self,
546
- name: str,
547
- input_set_id: str,
548
- instance_ids: list[str] = None,
549
- description: Optional[str] = None,
550
- id: Optional[str] = None,
551
- option_sets: Optional[dict[str, dict[str, str]]] = None,
552
- runs: Optional[list[Union[BatchExperimentRun, dict[str, Any]]]] = None,
553
- ) -> str:
554
- """
555
- Create a new batch experiment.
556
-
557
- Args:
558
- name: Name of the batch experiment.
559
- input_set_id: ID of the input set to use for the experiment.
560
- instance_ids: List of instance IDs to use for the experiment.
561
- description: Description of the batch experiment.
562
- id: ID of the batch experiment.
563
- option_sets: Option sets to use for the experiment.
564
- runs: Runs to use for the experiment.
565
-
566
- Returns:
567
- ID of the batch experiment.
568
-
569
- Raises:
570
- requests.HTTPError: If the response status code is not 2xx.
571
- """
572
-
573
- payload = {
574
- "name": name,
575
- "input_set_id": input_set_id,
576
- "instance_ids": instance_ids,
577
- }
578
- if description is not None:
579
- payload["description"] = description
580
- if id is not None:
581
- payload["id"] = id
582
- if option_sets is not None:
583
- payload["option_sets"] = option_sets
584
- if runs is not None:
585
- payload_runs = [{}] * len(runs)
586
- for i, run in enumerate(runs):
587
- payload_runs[i] = run.to_dict() if isinstance(run, BatchExperimentRun) else run
588
- payload["runs"] = payload_runs
589
-
590
- response = self.client.request(
591
- method="POST",
592
- endpoint=f"{self.experiments_endpoint}/batch",
593
- payload=payload,
594
- )
595
-
596
- return response.json()["id"]
597
-
598
- def new_input_set(
599
- self,
600
- id: str,
601
- name: str,
602
- description: Optional[str] = None,
603
- end_time: Optional[datetime] = None,
604
- instance_id: Optional[str] = None,
605
- maximum_runs: Optional[int] = None,
606
- run_ids: Optional[list[str]] = None,
607
- start_time: Optional[datetime] = None,
608
- ) -> InputSet:
609
- """
610
- Create a new input set.
611
-
612
- Args:
613
- id: ID of the input set.
614
- name: Name of the input set.
615
- description: Description of the input set.
616
- end_time: End time of the runs to construct the input set.
617
- instance_id: ID of the instance to use for the input set. If not
618
- provided, the default_instance_id will be used.
619
- maximum_runs: Maximum number of runs to use for the input set.
620
- run_ids: IDs of the runs to use for the input set.
621
- start_time: Start time of the runs to construct the input set.
622
-
623
- Returns:
624
- Input set.
625
-
626
- Raises:
627
- requests.HTTPError: If the response status code is not 2xx.
628
- """
629
-
630
- payload = {
631
- "id": id,
632
- "name": name,
633
- }
634
- if description is not None:
635
- payload["description"] = description
636
- if end_time is not None:
637
- payload["end_time"] = end_time.isoformat()
638
- if instance_id is not None:
639
- payload["instance_id"] = instance_id
640
- if maximum_runs is not None:
641
- payload["maximum_runs"] = maximum_runs
642
- if run_ids is not None:
643
- payload["run_ids"] = run_ids
644
- if start_time is not None:
645
- payload["start_time"] = start_time.isoformat()
646
-
647
- response = self.client.request(
648
- method="POST",
649
- endpoint=f"{self.experiments_endpoint}/inputsets",
650
- payload=payload,
651
- )
652
-
653
- return InputSet.from_dict(response.json())
654
-
655
- def new_run( # noqa: C901 # Lot of if statements, but clear logic.
656
- self,
657
- input: Union[dict[str, Any], BaseModel, str] = None,
658
- instance_id: Optional[str] = None,
659
- name: Optional[str] = None,
660
- description: Optional[str] = None,
661
- upload_id: Optional[str] = None,
662
- options: Optional[dict[str, str]] = None,
663
- configuration: Optional[Configuration] = None,
664
- ) -> str:
665
- """
666
- Submit an input to start a new run of the application. Returns the
667
- run_id of the submitted run.
668
-
669
- Args:
670
- input: Input to use for the run. This can be JSON (given as dict
671
- or BaseModel) or text (given as str).
672
- instance_id: ID of the instance to use for the run. If not
673
- provided, the default_instance_id will be used.
674
- name: Name of the run.
675
- description: Description of the run.
676
- upload_id: ID to use when running a large input.
677
- options: Options to use for the run.
678
- configuration: Configuration to use for the run.
679
-
680
- Returns:
681
- ID of the submitted run.
682
-
683
- Raises:
684
- requests.HTTPError: If the response status code is not 2xx.
685
- """
686
-
687
- input_size = 0
688
- if isinstance(input, BaseModel):
689
- input = input.to_dict()
690
- if input is not None:
691
- input_size = get_size(input)
692
- elif isinstance(input, dict):
693
- input_size = get_size(input)
694
-
695
- upload_url_required = isinstance(input, str) or input_size > _MAX_RUN_SIZE
696
-
697
- upload_id_used = upload_id is not None
698
- if not upload_id_used and upload_url_required:
699
- upload_url = self.upload_url()
700
- self.upload_large_input(input=input, upload_url=upload_url)
701
- upload_id = upload_url.upload_id
702
- upload_id_used = True
703
-
704
- if options is not None:
705
- for key, value in options.items():
706
- if not isinstance(value, str):
707
- options[key] = json.dumps(value)
708
-
709
- payload = {}
710
- if upload_id_used:
711
- payload["upload_id"] = upload_id
712
- else:
713
- payload["input"] = input
714
-
715
- if name is not None:
716
- payload["name"] = name
717
- if description is not None:
718
- payload["description"] = description
719
- if options is not None:
720
- payload["options"] = options
721
- if configuration is not None:
722
- payload["configuration"] = configuration.to_dict()
723
-
724
- query_params = {
725
- "instance_id": instance_id if instance_id is not None else self.default_instance_id,
726
- }
727
- response = self.client.request(
728
- method="POST",
729
- endpoint=f"{self.endpoint}/runs",
730
- payload=payload,
731
- query_params=query_params,
732
- )
733
-
734
- return response.json()["run_id"]
735
-
736
- def new_run_with_result(
737
- self,
738
- input: Union[dict[str, Any], BaseModel] = None,
739
- instance_id: Optional[str] = None,
740
- name: Optional[str] = None,
741
- description: Optional[str] = None,
742
- upload_id: Optional[str] = None,
743
- run_options: Optional[dict[str, str]] = None,
744
- polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
745
- configuration: Optional[Configuration] = None,
746
- ) -> RunResult:
747
- """
748
- Submit an input to start a new run of the application and poll for the
749
- result. This is a convenience method that combines the new_run and
750
- run_result_with_polling methods, applying polling logic to check when
751
- the run succeeded.
752
-
753
- Args:
754
- input: Input to use for the run.
755
- instance_id: ID of the instance to use for the run. If not
756
- provided, the default_instance_id will be used.
757
- name: Name of the run.
758
- description: Description of the run.
759
- upload_id: ID to use when running a large input.
760
- run_options: Options to use for the run.
761
- polling_options: Options to use when polling for the run result.
762
- configuration: Configuration to use for the run.
763
-
764
- Returns:
765
- Result of the run.
766
-
767
- Raises:
768
- requests.HTTPError: If the response status code is not 2xx.
769
- TimeoutError: If the run does not succeed after the polling
770
- strategy is exhausted based on time duration.
771
- RuntimeError: If the run does not succeed after the polling
772
- strategy is exhausted based on number of tries.
773
- """
774
-
775
- run_id = self.new_run(
776
- input=input,
777
- instance_id=instance_id,
778
- name=name,
779
- description=description,
780
- upload_id=upload_id,
781
- options=run_options,
782
- configuration=configuration,
783
- )
784
-
785
- return self.run_result_with_polling(
786
- run_id=run_id,
787
- polling_options=polling_options,
788
- )
789
-
790
- def new_version(
791
- self,
792
- id: Optional[str] = None,
793
- name: Optional[str] = None,
794
- description: Optional[str] = None,
795
- ) -> Version:
796
- """
797
- Create a new version using the current dev binary.
798
-
799
- Args:
800
- id: ID of the version. Will be generated if not provided.
801
- name: Name of the version. Will be generated if not provided.
802
- description: Description of the version. Will be generated if not provided.
803
-
804
- Returns:
805
- Version.
806
-
807
- Raises:
808
- requests.HTTPError: If the response status code is not 2xx.
809
- """
810
-
811
- payload = {}
812
-
813
- if id is not None:
814
- payload["id"] = id
815
- if name is not None:
816
- payload["name"] = name
817
- if description is not None:
818
- payload["description"] = description
819
-
820
- response = self.client.request(
821
- method="POST",
822
- endpoint=f"{self.endpoint}/versions",
823
- payload=payload,
824
- )
825
-
826
- return Version.from_dict(response.json())
827
-
828
- def new_instance(
829
- self,
830
- version_id: str,
831
- id: Optional[str] = None,
832
- name: Optional[str] = None,
833
- description: Optional[str] = None,
834
- configuration: Optional[Configuration] = None,
835
- ) -> Instance:
836
- """
837
- Create a new instance and associate it with a version.
838
-
839
- Args:
840
- version_id: ID of the version to associate the instance with.
841
- id: ID of the instance. Will be generated if not provided.
842
- name: Name of the instance. Will be generated if not provided.
843
- description: Description of the instance. Will be generated if not provided.
844
- configuration: Configuration to use for the instance.
845
-
846
- Returns:
847
- Instance.
848
-
849
- Raises:
850
- requests.HTTPError: If the response status code is not 2xx.
851
- """
852
-
853
- payload = {
854
- "version_id": version_id,
855
- }
856
-
857
- if id is not None:
858
- payload["id"] = id
859
- if name is not None:
860
- payload["name"] = name
861
- if description is not None:
862
- payload["description"] = description
863
- if configuration is not None:
864
- payload["configuration"] = configuration.to_dict()
865
-
866
- response = self.client.request(
867
- method="POST",
868
- endpoint=f"{self.endpoint}/instances",
869
- payload=payload,
870
- )
871
-
872
- return Instance.from_dict(response.json())
873
-
874
- @staticmethod
875
- def new(
876
- client: Client,
877
- name: str,
878
- id: Optional[str] = None,
879
- description: Optional[str] = None,
880
- ) -> "Application":
881
- """
882
- Create a new application.
883
-
884
- Args:
885
- client: Client to use for interacting with the Nextmv Cloud API.
886
- name: Name of the application.
887
- id: ID of the application. Will be generated if not provided.
888
- description: Description of the application.
889
-
890
- Returns:
891
- The new application.
892
- """
893
-
894
- payload = {
895
- "name": name,
896
- }
897
-
898
- if description is not None:
899
- payload["description"] = description
900
- if id is not None:
901
- payload["id"] = id
902
-
903
- response = client.request(
904
- method="POST",
905
- endpoint="v1/applications",
906
- payload=payload,
907
- )
908
-
909
- return Application(client=client, id=response.json()["id"])
910
-
911
- def delete(self) -> None:
912
- """
913
- Delete the application.
914
-
915
- Raises:
916
- requests.HTTPError: If the response status code is not 2xx.
917
- """
918
-
919
- _ = self.client.request(
920
- method="DELETE",
921
- endpoint=self.endpoint,
922
- )
923
-
924
- def push(
925
- self,
926
- manifest: Optional[Manifest] = None,
927
- app_dir: Optional[str] = None,
928
- verbose: bool = False,
929
- model: Optional[Model] = None,
930
- model_configuration: Optional[ModelConfiguration] = None,
931
- ) -> None:
932
- """
933
- Push an app to Nextmv Cloud.
934
-
935
- If the manifest is not provided, an `app.yaml` file will be searched
936
- for in the provided path. If there is no manifest file found, an
937
- exception will be raised.
938
-
939
- There are two ways to push an app to Nextmv Cloud:
940
- 1. Specifying `app_dir`, which is the path to an app’s root directory.
941
- This acts as an external strategy, where the app is composed of files
942
- in a directory and those apps are packaged and pushed to Nextmv Cloud.
943
- 2. Specifying a `model` and `model_configuration`. This acts as an
944
- internal (or Python-native) strategy, where the app is actually a
945
- `nextmv.Model`. The model is encoded, some dependencies and
946
- accompanying files are packaged, and the app is pushed to Nextmv Cloud.
947
-
948
- Examples
949
- -------
950
-
951
- 1. Push an app using an external strategy, i.e., specifying the app’s
952
- directory:
953
- ```python
954
- import os
955
-
956
- from nextmv import cloud
957
-
958
- client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
959
- app = cloud.Application(client=client, id="<YOUR-APP-ID>")
960
- app.push() # Use verbose=True for step-by-step output.
961
- ```
962
-
963
- 2. Push an app using an internal strategy, i.e., specifying the model
964
- and model configuration:
965
- ```python
966
- import os
967
-
968
- import nextroute
969
-
970
- import nextmv
971
- import nextmv.cloud
972
-
973
-
974
- # Define the model that makes decisions. This model uses the Nextroute
975
- # library to solve a vehicle routing problem.
976
- class DecisionModel(nextmv.Model):
977
- def solve(self, input: nextmv.Input) -> nextmv.Output:
978
- nextroute_input = nextroute.schema.Input.from_dict(input.data)
979
- nextroute_options = nextroute.Options.extract_from_dict(input.options.to_dict())
980
- nextroute_output = nextroute.solve(nextroute_input, nextroute_options)
981
-
982
- return nextmv.Output(
983
- options=input.options,
984
- solution=nextroute_output.solutions[0].to_dict(),
985
- statistics=nextroute_output.statistics.to_dict(),
986
- )
987
-
988
-
989
- # Define the options that the model needs.
990
- parameters = []
991
- default_options = nextroute.Options()
992
- for name, default_value in default_options.to_dict().items():
993
- parameters.append(nextmv.Parameter(name.lower(), type(default_value), default_value, name, False))
994
-
995
- options = nextmv.Options(*parameters)
996
-
997
- # Instantiate the model and model configuration.
998
- model = DecisionModel()
999
- model_configuration = nextmv.ModelConfiguration(
1000
- name="python_nextroute_model",
1001
- requirements=[
1002
- "nextroute==1.8.1",
1003
- "nextmv==0.14.0.dev1",
1004
- ],
1005
- options=options,
1006
- )
1007
-
1008
- # Define the Nextmv application and push the model to the cloud.
1009
- client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
1010
- app = cloud.Application(client=client, id="<YOUR-APP-ID>")
1011
- manifest = nextmv.cloud.default_python_manifest()
1012
- app.push(
1013
- manifest=manifest,
1014
- verbose=True,
1015
- model=model,
1016
- model_configuration=model_configuration,
1017
- )
1018
- ```
1019
-
1020
- Parameters
1021
- ----------
1022
- manifest : Optional[Manifest], optional
1023
- The manifest for the app, by default None.
1024
- app_dir : Optional[str], optional
1025
- The path to the app’s directory, by default None.
1026
- verbose : bool, optional
1027
- Whether to print verbose output, by default False.
1028
- """
1029
-
1030
- if verbose:
1031
- log("💽 Starting build for Nextmv application.")
1032
-
1033
- if app_dir is None or app_dir == "":
1034
- app_dir = "."
1035
-
1036
- if manifest is None:
1037
- manifest = Manifest.from_yaml(app_dir)
1038
-
1039
- if model is not None and not isinstance(model, Model):
1040
- raise TypeError("model must be an instance of nextmv.Model")
1041
-
1042
- if model_configuration is not None and not isinstance(model_configuration, ModelConfiguration):
1043
- raise TypeError("model_configuration must be an instance of nextmv.ModelConfiguration")
1044
-
1045
- if (model is None and model_configuration is not None) or (model is not None and model_configuration is None):
1046
- raise ValueError("model and model_configuration must be provided together")
1047
-
1048
- package._run_build_command(app_dir, manifest.build, verbose)
1049
- package._run_pre_push_command(app_dir, manifest.pre_push, verbose)
1050
- tar_file, output_dir = package._package(app_dir, manifest, model, model_configuration, verbose)
1051
- self.__update_app_binary(tar_file, manifest, verbose)
1052
-
1053
- try:
1054
- shutil.rmtree(output_dir)
1055
- except OSError as e:
1056
- raise Exception(f"error deleting output directory: {e}") from e
1057
-
1058
- def run_input(self, run_id: str) -> dict[str, Any]:
1059
- """
1060
- Get the input of a run.
1061
-
1062
- Args:
1063
- run_id: ID of the run.
1064
-
1065
- Returns:
1066
- Input of the run.
1067
-
1068
- Raises:
1069
- requests.HTTPError: If the response status code is not 2xx.
1070
- """
1071
- run_information = self.run_metadata(run_id=run_id)
1072
-
1073
- query_params = None
1074
- large = False
1075
- if run_information.metadata.input_size > _MAX_RUN_SIZE:
1076
- query_params = {"format": "url"}
1077
- large = True
1078
-
1079
- response = self.client.request(
1080
- method="GET",
1081
- endpoint=f"{self.endpoint}/runs/{run_id}/input",
1082
- query_params=query_params,
1083
- )
1084
- if not large:
1085
- return response.json()
1086
-
1087
- download_url = DownloadURL.from_dict(response.json())
1088
- download_response = self.client.request(
1089
- method="GET",
1090
- endpoint=download_url.url,
1091
- headers={"Content-Type": "application/json"},
1092
- )
1093
-
1094
- return download_response.json()
1095
-
1096
- def run_logs(self, run_id: str) -> RunLog:
1097
- """
1098
- Get the logs of a run.
1099
-
1100
- Args:
1101
- run_id: ID of the run.
1102
-
1103
- Returns:
1104
- Logs of the run.
1105
-
1106
- Raises:
1107
- requests.HTTPError: If the response status code is not 2xx.
1108
- """
1109
- response = self.client.request(
1110
- method="GET",
1111
- endpoint=f"{self.endpoint}/runs/{run_id}/logs",
1112
- )
1113
- return RunLog.from_dict(response.json())
1114
-
1115
- def run_metadata(self, run_id: str) -> RunInformation:
1116
- """
1117
- Get the metadata of a run. The result does not include the run output.
1118
-
1119
- Args:
1120
- run_id: ID of the run.
1121
-
1122
- Returns:
1123
- Metadata of the run (Run result with no output).
1124
-
1125
- Raises:
1126
- requests.HTTPError: If the response status code is not 2xx.
1127
- """
1128
-
1129
- response = self.client.request(
1130
- method="GET",
1131
- endpoint=f"{self.endpoint}/runs/{run_id}/metadata",
1132
- )
1133
-
1134
- return RunInformation.from_dict(response.json())
1135
-
1136
- def run_result(self, run_id: str) -> RunResult:
1137
- """
1138
- Get the result of a run. The result includes the run output.
1139
-
1140
- Args:
1141
- run_id: ID of the run.
1142
-
1143
- Returns:
1144
- Result of the run.
1145
-
1146
- Raises:
1147
- requests.HTTPError: If the response status code is not 2xx.
1148
- """
1149
-
1150
- run_information = self.run_metadata(run_id=run_id)
1151
-
1152
- return self.__run_result(run_id=run_id, run_information=run_information)
1153
-
1154
- def run_result_with_polling(
1155
- self,
1156
- run_id: str,
1157
- polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
1158
- ) -> RunResult:
1159
- """
1160
- Get the result of a run. The result includes the run output. This
1161
- method polls for the result until the run finishes executing or the
1162
- polling strategy is exhausted.
1163
-
1164
- Args:
1165
- run_id: ID of the run.
1166
- polling_options: Options to use when polling for the run result.
1167
-
1168
- Returns:
1169
- Result of the run.
1170
-
1171
- Raises:
1172
- requests.HTTPError: If the response status code is not 2xx.
1173
- """
1174
-
1175
- time.sleep(polling_options.initial_delay)
1176
- delay = polling_options.delay
1177
- polling_ok = False
1178
- for _ in range(polling_options.max_tries):
1179
- run_information = self.run_metadata(run_id=run_id)
1180
- if run_information.metadata.status_v2 in [
1181
- StatusV2.succeeded,
1182
- StatusV2.failed,
1183
- StatusV2.canceled,
1184
- ]:
1185
- polling_ok = True
1186
- break
1187
-
1188
- if delay > polling_options.max_duration:
1189
- raise TimeoutError(
1190
- f"run {run_id} did not succeed after {delay} seconds",
1191
- )
1192
-
1193
- sleep_duration = min(delay, polling_options.max_delay)
1194
- time.sleep(sleep_duration)
1195
- delay *= polling_options.backoff
1196
-
1197
- if not polling_ok:
1198
- raise RuntimeError(
1199
- f"run {run_id} did not succeed after {polling_options.max_tries} tries",
1200
- )
1201
-
1202
- return self.__run_result(run_id=run_id, run_information=run_information)
1203
-
1204
- def update_instance(
1205
- self,
1206
- id: str,
1207
- name: str,
1208
- version_id: Optional[str] = None,
1209
- description: Optional[str] = None,
1210
- configuration: Optional[Configuration] = None,
1211
- ) -> Instance:
1212
- """
1213
- Update an instance.
1214
-
1215
- Args:
1216
- id: ID of the instance to update.
1217
- version_id: ID of the version to associate the instance with.
1218
- name: Name of the instance.
1219
- description: Description of the instance.
1220
- configuration: Configuration to use for the instance.
1221
-
1222
- Returns:
1223
- Instance.
1224
-
1225
- Raises:
1226
- requests.HTTPError: If the response status code is not 2xx.
1227
- """
1228
-
1229
- payload = {}
1230
-
1231
- if version_id is not None:
1232
- payload["version_id"] = version_id
1233
- if name is not None:
1234
- payload["name"] = name
1235
- if description is not None:
1236
- payload["description"] = description
1237
- if configuration is not None:
1238
- payload["configuration"] = configuration.to_dict()
1239
-
1240
- response = self.client.request(
1241
- method="PUT",
1242
- endpoint=f"{self.endpoint}/instances/{id}",
1243
- payload=payload,
1244
- )
1245
-
1246
- return Instance.from_dict(response.json())
1247
-
1248
- def upload_large_input(
1249
- self,
1250
- input: Union[dict[str, Any], str],
1251
- upload_url: UploadURL,
1252
- ) -> None:
1253
- """
1254
- Upload the file located at the given path to the provided upload URL.
1255
-
1256
- Args:
1257
- upload_url: Upload URL to use for uploading the file.
1258
- input: Input to use for the run.
1259
-
1260
- Raises:
1261
- requests.HTTPError: If the response status code is not 2xx.
1262
- """
1263
-
1264
- if isinstance(input, dict):
1265
- input = json.dumps(input)
1266
-
1267
- _ = self.client.upload_to_presigned_url(
1268
- url=upload_url.upload_url,
1269
- data=input,
1270
- )
1271
-
1272
- def upload_url(self) -> UploadURL:
1273
- """
1274
- Get an upload URL to use for uploading a file.
1275
-
1276
- Returns:
1277
- Result of getting an upload URL.
1278
-
1279
- Raises:
1280
- requests.HTTPError: If the response status code is not 2xx.
1281
- """
1282
-
1283
- response = self.client.request(
1284
- method="POST",
1285
- endpoint=f"{self.endpoint}/runs/uploadurl",
1286
- )
1287
-
1288
- return UploadURL.from_dict(response.json())
1289
-
1290
- def version(self, version_id: str) -> Version:
1291
- """
1292
- Get a version.
1293
-
1294
- Args:
1295
- version_id: ID of the version.
1296
-
1297
- Returns:
1298
- Version.
1299
-
1300
- Raises:
1301
- requests.HTTPError: If the response status code is not 2xx.
1302
- """
1303
-
1304
- response = self.client.request(
1305
- method="GET",
1306
- endpoint=f"{self.endpoint}/versions/{version_id}",
1307
- )
1308
-
1309
- return Version.from_dict(response.json())
1310
-
1311
- def __run_result(
1312
- self,
1313
- run_id: str,
1314
- run_information: RunInformation,
1315
- ) -> RunResult:
1316
- """
1317
- Get the result of a run. The result includes the run output. This is a
1318
- private method that is the base for retrieving a run result, regardless
1319
- of polling.
1320
-
1321
- Args:
1322
- run_id: ID of the run.
1323
- run_information: Information of the run.
1324
-
1325
- Returns:
1326
- Result of the run.
1327
-
1328
- Raises:
1329
- requests.HTTPError: If the response status code is not 2xx.
1330
- """
1331
- query_params = None
1332
- large_output = False
1333
- if run_information.metadata.output_size > _MAX_RUN_SIZE:
1334
- query_params = {"format": "url"}
1335
- large_output = True
1336
-
1337
- response = self.client.request(
1338
- method="GET",
1339
- endpoint=f"{self.endpoint}/runs/{run_id}",
1340
- query_params=query_params,
1341
- )
1342
- result = RunResult.from_dict(response.json())
1343
- if not large_output:
1344
- return result
1345
-
1346
- download_url = DownloadURL.from_dict(response.json()["output"])
1347
- download_response = self.client.request(
1348
- method="GET",
1349
- endpoint=download_url.url,
1350
- headers={"Content-Type": "application/json"},
1351
- )
1352
- result.output = download_response.json()
1353
-
1354
- return result
1355
-
1356
- def __update_app_binary(
1357
- self,
1358
- tar_file: str,
1359
- manifest: Manifest,
1360
- verbose: bool = False,
1361
- ) -> None:
1362
- """Updates the application binary in Cloud."""
1363
-
1364
- if verbose:
1365
- log(f'🌟 Pushing to application: "{self.id}".')
1366
-
1367
- endpoint = f"{self.endpoint}/binary"
1368
- response = self.client.request(
1369
- method="GET",
1370
- endpoint=endpoint,
1371
- )
1372
- upload_url = response.json()["upload_url"]
1373
-
1374
- with open(tar_file, "rb") as f:
1375
- response = self.client.request(
1376
- method="PUT",
1377
- endpoint=upload_url,
1378
- data=f,
1379
- headers={"Content-Type": "application/octet-stream"},
1380
- )
1381
-
1382
- activation_request = {
1383
- "requirements": {
1384
- "executable_type": manifest.type,
1385
- "runtime": manifest.runtime,
1386
- },
1387
- }
1388
- response = self.client.request(
1389
- method="PUT",
1390
- endpoint=endpoint,
1391
- payload=activation_request,
1392
- )
1393
-
1394
- if verbose:
1395
- log(f'💥️ Successfully pushed to application: "{self.id}".')
1396
- log(
1397
- json.dumps(
1398
- {
1399
- "app_id": self.id,
1400
- "endpoint": self.client.url,
1401
- "instance_url": f"{self.endpoint}/runs?instance_id=devint",
1402
- },
1403
- indent=2,
1404
- )
1405
- )