nextmv 0.40.0__py3-none-any.whl → 1.0.0.dev0__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 (129) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__init__.py +2 -0
  3. nextmv/cli/CONTRIBUTING.md +511 -0
  4. nextmv/cli/cloud/__init__.py +45 -0
  5. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  6. nextmv/cli/cloud/acceptance/create.py +393 -0
  7. nextmv/cli/cloud/acceptance/delete.py +68 -0
  8. nextmv/cli/cloud/acceptance/get.py +104 -0
  9. nextmv/cli/cloud/acceptance/list.py +62 -0
  10. nextmv/cli/cloud/acceptance/update.py +95 -0
  11. nextmv/cli/cloud/account/__init__.py +28 -0
  12. nextmv/cli/cloud/account/create.py +83 -0
  13. nextmv/cli/cloud/account/delete.py +60 -0
  14. nextmv/cli/cloud/account/get.py +66 -0
  15. nextmv/cli/cloud/account/update.py +70 -0
  16. nextmv/cli/cloud/app/__init__.py +35 -0
  17. nextmv/cli/cloud/app/create.py +141 -0
  18. nextmv/cli/cloud/app/delete.py +58 -0
  19. nextmv/cli/cloud/app/exists.py +44 -0
  20. nextmv/cli/cloud/app/get.py +66 -0
  21. nextmv/cli/cloud/app/list.py +61 -0
  22. nextmv/cli/cloud/app/push.py +137 -0
  23. nextmv/cli/cloud/app/update.py +124 -0
  24. nextmv/cli/cloud/batch/__init__.py +29 -0
  25. nextmv/cli/cloud/batch/create.py +454 -0
  26. nextmv/cli/cloud/batch/delete.py +68 -0
  27. nextmv/cli/cloud/batch/get.py +104 -0
  28. nextmv/cli/cloud/batch/list.py +63 -0
  29. nextmv/cli/cloud/batch/metadata.py +66 -0
  30. nextmv/cli/cloud/batch/update.py +95 -0
  31. nextmv/cli/cloud/data/__init__.py +26 -0
  32. nextmv/cli/cloud/data/upload.py +162 -0
  33. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  34. nextmv/cli/cloud/ensemble/create.py +414 -0
  35. nextmv/cli/cloud/ensemble/delete.py +67 -0
  36. nextmv/cli/cloud/ensemble/get.py +65 -0
  37. nextmv/cli/cloud/ensemble/update.py +103 -0
  38. nextmv/cli/cloud/input_set/__init__.py +30 -0
  39. nextmv/cli/cloud/input_set/create.py +168 -0
  40. nextmv/cli/cloud/input_set/get.py +63 -0
  41. nextmv/cli/cloud/input_set/list.py +63 -0
  42. nextmv/cli/cloud/input_set/update.py +123 -0
  43. nextmv/cli/cloud/instance/__init__.py +35 -0
  44. nextmv/cli/cloud/instance/create.py +290 -0
  45. nextmv/cli/cloud/instance/delete.py +62 -0
  46. nextmv/cli/cloud/instance/exists.py +39 -0
  47. nextmv/cli/cloud/instance/get.py +62 -0
  48. nextmv/cli/cloud/instance/list.py +60 -0
  49. nextmv/cli/cloud/instance/update.py +216 -0
  50. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  51. nextmv/cli/cloud/managed_input/create.py +146 -0
  52. nextmv/cli/cloud/managed_input/delete.py +65 -0
  53. nextmv/cli/cloud/managed_input/get.py +63 -0
  54. nextmv/cli/cloud/managed_input/list.py +60 -0
  55. nextmv/cli/cloud/managed_input/update.py +97 -0
  56. nextmv/cli/cloud/run/__init__.py +37 -0
  57. nextmv/cli/cloud/run/cancel.py +37 -0
  58. nextmv/cli/cloud/run/create.py +530 -0
  59. nextmv/cli/cloud/run/get.py +199 -0
  60. nextmv/cli/cloud/run/input.py +86 -0
  61. nextmv/cli/cloud/run/list.py +80 -0
  62. nextmv/cli/cloud/run/logs.py +167 -0
  63. nextmv/cli/cloud/run/metadata.py +67 -0
  64. nextmv/cli/cloud/run/track.py +501 -0
  65. nextmv/cli/cloud/scenario/__init__.py +29 -0
  66. nextmv/cli/cloud/scenario/create.py +451 -0
  67. nextmv/cli/cloud/scenario/delete.py +65 -0
  68. nextmv/cli/cloud/scenario/get.py +102 -0
  69. nextmv/cli/cloud/scenario/list.py +63 -0
  70. nextmv/cli/cloud/scenario/metadata.py +67 -0
  71. nextmv/cli/cloud/scenario/update.py +93 -0
  72. nextmv/cli/cloud/secrets/__init__.py +33 -0
  73. nextmv/cli/cloud/secrets/create.py +206 -0
  74. nextmv/cli/cloud/secrets/delete.py +67 -0
  75. nextmv/cli/cloud/secrets/get.py +66 -0
  76. nextmv/cli/cloud/secrets/list.py +60 -0
  77. nextmv/cli/cloud/secrets/update.py +147 -0
  78. nextmv/cli/cloud/upload/__init__.py +22 -0
  79. nextmv/cli/cloud/upload/create.py +39 -0
  80. nextmv/cli/cloud/version/__init__.py +33 -0
  81. nextmv/cli/cloud/version/create.py +97 -0
  82. nextmv/cli/cloud/version/delete.py +62 -0
  83. nextmv/cli/cloud/version/exists.py +39 -0
  84. nextmv/cli/cloud/version/get.py +62 -0
  85. nextmv/cli/cloud/version/list.py +60 -0
  86. nextmv/cli/cloud/version/update.py +92 -0
  87. nextmv/cli/community/__init__.py +24 -0
  88. nextmv/cli/community/clone.py +3 -3
  89. nextmv/cli/community/list.py +1 -1
  90. nextmv/cli/configuration/__init__.py +23 -0
  91. nextmv/cli/configuration/config.py +68 -4
  92. nextmv/cli/configuration/create.py +14 -15
  93. nextmv/cli/configuration/delete.py +24 -12
  94. nextmv/cli/configuration/list.py +1 -1
  95. nextmv/cli/main.py +58 -16
  96. nextmv/cli/message.py +153 -0
  97. nextmv/cli/options.py +168 -0
  98. nextmv/cli/version.py +20 -1
  99. nextmv/cloud/__init__.py +4 -1
  100. nextmv/cloud/acceptance_test.py +19 -18
  101. nextmv/cloud/account.py +268 -24
  102. nextmv/cloud/application/__init__.py +955 -0
  103. nextmv/cloud/application/_acceptance.py +419 -0
  104. nextmv/cloud/application/_batch_scenario.py +860 -0
  105. nextmv/cloud/application/_ensemble.py +251 -0
  106. nextmv/cloud/application/_input_set.py +227 -0
  107. nextmv/cloud/application/_instance.py +289 -0
  108. nextmv/cloud/application/_managed_input.py +227 -0
  109. nextmv/cloud/application/_run.py +1393 -0
  110. nextmv/cloud/application/_secrets.py +294 -0
  111. nextmv/cloud/application/_utils.py +54 -0
  112. nextmv/cloud/application/_version.py +303 -0
  113. nextmv/cloud/batch_experiment.py +3 -1
  114. nextmv/cloud/instance.py +11 -1
  115. nextmv/cloud/integration.py +1 -1
  116. nextmv/cloud/package.py +50 -9
  117. nextmv/input.py +20 -36
  118. nextmv/local/application.py +3 -15
  119. nextmv/polling.py +54 -16
  120. nextmv/run.py +83 -27
  121. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dev0.dist-info}/METADATA +33 -8
  122. nextmv-1.0.0.dev0.dist-info/RECORD +158 -0
  123. nextmv/cli/community/community.py +0 -24
  124. nextmv/cli/configuration/configuration.py +0 -23
  125. nextmv/cli/error.py +0 -22
  126. nextmv/cloud/application.py +0 -4204
  127. nextmv-0.40.0.dist-info/RECORD +0 -66
  128. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dev0.dist-info}/WHEEL +0 -0
  129. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dev0.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,54 @@
1
+ """
2
+ Module for general utility methods for Application.
3
+
4
+ Avoid abusing this module, as you should try to keep domain-specific
5
+ functionality together and not rely on generic utilities.
6
+ """
7
+
8
+ import requests
9
+
10
+
11
+ def _is_not_exist_error(e: requests.HTTPError) -> bool:
12
+ """
13
+ Check if the error is a known 404 Not Found error.
14
+
15
+ This is an internal helper function that examines HTTPError objects to determine
16
+ if they represent a "Not Found" (404) condition, either directly or through a
17
+ nested exception.
18
+
19
+ Parameters
20
+ ----------
21
+ e : requests.HTTPError
22
+ The HTTP error to check.
23
+
24
+ Returns
25
+ -------
26
+ bool
27
+ True if the error is a 404 Not Found error, False otherwise.
28
+
29
+ Examples
30
+ --------
31
+ >>> try:
32
+ ... response = requests.get('https://api.example.com/nonexistent')
33
+ ... response.raise_for_status()
34
+ ... except requests.HTTPError as err:
35
+ ... if _is_not_exist_error(err):
36
+ ... print("Resource does not exist")
37
+ ... else:
38
+ ... print("Another error occurred")
39
+ Resource does not exist
40
+ """
41
+ if (
42
+ # Check whether the error is caused by a 404 status code - meaning the app does not exist.
43
+ (hasattr(e, "response") and hasattr(e.response, "status_code") and e.response.status_code == 404)
44
+ or
45
+ # Check a possibly nested exception as well.
46
+ (
47
+ hasattr(e, "__cause__")
48
+ and hasattr(e.__cause__, "response")
49
+ and hasattr(e.__cause__.response, "status_code")
50
+ and e.__cause__.response.status_code == 404
51
+ )
52
+ ):
53
+ return True
54
+ return False
@@ -0,0 +1,303 @@
1
+ """
2
+ Application mixin for managing app versions.
3
+ """
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import requests
8
+
9
+ from nextmv.cloud.application._utils import _is_not_exist_error
10
+ from nextmv.cloud.version import Version
11
+ from nextmv.safe import safe_id
12
+
13
+ if TYPE_CHECKING:
14
+ from . import Application
15
+
16
+
17
+ class ApplicationVersionMixin:
18
+ """
19
+ Mixin class for managing app versions within an application.
20
+ """
21
+
22
+ def delete_version(self: "Application", version_id: str) -> None:
23
+ """
24
+ Delete a version.
25
+
26
+ Permanently removes the specified version from the application.
27
+
28
+ Parameters
29
+ ----------
30
+ version_id : str
31
+ ID of the version to delete.
32
+
33
+ Raises
34
+ ------
35
+ requests.HTTPError
36
+ If the response status code is not 2xx.
37
+
38
+ Examples
39
+ --------
40
+ >>> app.delete_version("v1.0.0") # Permanently deletes the version
41
+ """
42
+
43
+ _ = self.client.request(
44
+ method="DELETE",
45
+ endpoint=f"{self.endpoint}/versions/{version_id}",
46
+ )
47
+
48
+ def list_versions(self: "Application") -> list[Version]:
49
+ """
50
+ List all versions.
51
+
52
+ Returns
53
+ -------
54
+ list[Version]
55
+ List of all versions associated with this application.
56
+
57
+ Raises
58
+ ------
59
+ requests.HTTPError
60
+ If the response status code is not 2xx.
61
+
62
+ Examples
63
+ --------
64
+ >>> versions = app.list_versions()
65
+ >>> for version in versions:
66
+ ... print(version.name)
67
+ 'v1.0.0'
68
+ 'v1.1.0'
69
+ """
70
+
71
+ response = self.client.request(
72
+ method="GET",
73
+ endpoint=f"{self.endpoint}/versions",
74
+ )
75
+
76
+ return [Version.from_dict(version) for version in response.json()]
77
+
78
+ def new_version(
79
+ self: "Application",
80
+ id: str | None = None,
81
+ name: str | None = None,
82
+ description: str | None = None,
83
+ exist_ok: bool = False,
84
+ ) -> Version:
85
+ """
86
+ Create a new version using the latest pushed executable.
87
+
88
+ This method creates a new version of the application using the current development
89
+ binary. Application versions represent different iterations of your application's
90
+ code and configuration that can be deployed.
91
+
92
+ Parameters
93
+ ----------
94
+ id : Optional[str], default=None
95
+ ID of the version. If not provided, a unique ID will be generated.
96
+ name : Optional[str], default=None
97
+ Name of the version. If not provided, a name will be generated.
98
+ description : Optional[str], default=None
99
+ Description of the version. If not provided, a description will be generated.
100
+ exist_ok : bool, default=False
101
+ If True and a version with the same ID already exists,
102
+ return the existing version instead of creating a new one.
103
+ If True, the 'id' parameter must be provided.
104
+
105
+ Returns
106
+ -------
107
+ Version
108
+ The newly created (or existing) version.
109
+
110
+ Raises
111
+ ------
112
+ ValueError
113
+ If exist_ok is True and id is None.
114
+ requests.HTTPError
115
+ If the response status code is not 2xx.
116
+
117
+ Examples
118
+ --------
119
+ >>> # Create a new version
120
+ >>> version = app.new_version(
121
+ ... id="v1.0.0",
122
+ ... name="Initial Release",
123
+ ... description="First stable version"
124
+ ... )
125
+ >>> print(version.id)
126
+ 'v1.0.0'
127
+
128
+ >>> # Get or create a version with exist_ok
129
+ >>> version = app.new_version(
130
+ ... id="v1.0.0",
131
+ ... exist_ok=True
132
+ ... )
133
+ """
134
+
135
+ if exist_ok and id is None:
136
+ raise ValueError("If exist_ok is True, id must be provided")
137
+
138
+ if exist_ok and self.version_exists(version_id=id):
139
+ return self.version(version_id=id)
140
+
141
+ if id is None:
142
+ id = safe_id(prefix="version")
143
+ if name is None:
144
+ name = id
145
+
146
+ payload = {
147
+ "id": id,
148
+ "name": name,
149
+ }
150
+
151
+ if description is not None:
152
+ payload["description"] = description
153
+
154
+ response = self.client.request(
155
+ method="POST",
156
+ endpoint=f"{self.endpoint}/versions",
157
+ payload=payload,
158
+ )
159
+
160
+ return Version.from_dict(response.json())
161
+
162
+ def update_version(
163
+ self: "Application",
164
+ version_id: str,
165
+ name: str | None = None,
166
+ description: str | None = None,
167
+ ) -> Version:
168
+ """
169
+ Update a version.
170
+
171
+ This method updates a specific version of the application. It mimics a
172
+ PATCH operation by allowing you to update only the name and/or description
173
+ fields while preserving all other fields.
174
+
175
+ Parameters
176
+ ----------
177
+ version_id : str
178
+ ID of the version to update.
179
+ name : Optional[str], default=None
180
+ Optional new name for the version.
181
+ description : Optional[str], default=None
182
+ Optional new description for the version.
183
+
184
+ Returns
185
+ -------
186
+ Version
187
+ The updated version object.
188
+
189
+ Raises
190
+ ------
191
+ requests.HTTPError
192
+ If the response status code is not 2xx.
193
+
194
+ Examples
195
+ --------
196
+ >>> # Update a version's name
197
+ >>> updated = app.update_version("v1.0.0", name="Version 1.0")
198
+ >>> print(updated.name)
199
+ 'Version 1.0'
200
+
201
+ >>> # Update a version's description
202
+ >>> updated = app.update_version("v1.0.0", description="Initial release")
203
+ >>> print(updated.description)
204
+ 'Initial release'
205
+ """
206
+
207
+ version = self.version(version_id=version_id)
208
+ version_dict = version.to_dict()
209
+ payload = version_dict.copy()
210
+
211
+ if name is not None:
212
+ payload["name"] = name
213
+ if description is not None:
214
+ payload["description"] = description
215
+
216
+ response = self.client.request(
217
+ method="PUT",
218
+ endpoint=f"{self.endpoint}/versions/{version_id}",
219
+ payload=payload,
220
+ )
221
+
222
+ return Version.from_dict(response.json())
223
+
224
+ def version(self: "Application", version_id: str) -> Version:
225
+ """
226
+ Get a version.
227
+
228
+ Retrieves a specific version of the application by its ID. Application versions
229
+ represent different iterations of your application's code and configuration.
230
+
231
+ Parameters
232
+ ----------
233
+ version_id : str
234
+ ID of the version to retrieve.
235
+
236
+ Returns
237
+ -------
238
+ Version
239
+ The version object containing details about the requested application version.
240
+
241
+ Raises
242
+ ------
243
+ requests.HTTPError
244
+ If the response status code is not 2xx.
245
+
246
+ Examples
247
+ --------
248
+ >>> # Retrieve a specific version
249
+ >>> version = app.version("v1.0.0")
250
+ >>> print(version.id)
251
+ 'v1.0.0'
252
+ >>> print(version.name)
253
+ 'Initial Release'
254
+ """
255
+
256
+ response = self.client.request(
257
+ method="GET",
258
+ endpoint=f"{self.endpoint}/versions/{version_id}",
259
+ )
260
+
261
+ return Version.from_dict(response.json())
262
+
263
+ def version_exists(self: "Application", version_id: str) -> bool:
264
+ """
265
+ Check if a version exists.
266
+
267
+ This method checks if a specific version of the application exists by
268
+ attempting to retrieve it. It handles HTTP errors for non-existent versions
269
+ and returns a boolean indicating existence.
270
+
271
+ Parameters
272
+ ----------
273
+ version_id : str
274
+ ID of the version to check for existence.
275
+
276
+ Returns
277
+ -------
278
+ bool
279
+ True if the version exists, False otherwise.
280
+
281
+ Raises
282
+ ------
283
+ requests.HTTPError
284
+ If an HTTP error occurs that is not related to the non-existence
285
+ of the version.
286
+
287
+ Examples
288
+ --------
289
+ >>> # Check if a version exists
290
+ >>> exists = app.version_exists("v1.0.0")
291
+ >>> if exists:
292
+ ... print("Version exists!")
293
+ ... else:
294
+ ... print("Version does not exist.")
295
+ """
296
+
297
+ try:
298
+ self.version(version_id=version_id)
299
+ return True
300
+ except requests.HTTPError as e:
301
+ if _is_not_exist_error(e):
302
+ return False
303
+ raise e
@@ -223,7 +223,9 @@ class BatchExperimentRun(BaseModel):
223
223
  Parameters
224
224
  ----------
225
225
  input_id : str
226
- ID of the input used for the experiment.
226
+ ID of the input used for the experiment. If a managed input is used,
227
+ this should be the ID of the managed input. If `input_set_id` is provided
228
+ for the run, this should be the ID of an input within that input set.
227
229
  option_set : str
228
230
  Option set used for the experiment. Defaults to None.
229
231
  instance_id : str, optional
nextmv/cloud/instance.py CHANGED
@@ -15,7 +15,7 @@ Instance
15
15
  from datetime import datetime
16
16
 
17
17
  from nextmv.base_model import BaseModel
18
- from nextmv.run import RunQueuing
18
+ from nextmv.run import Format, RunQueuing
19
19
 
20
20
 
21
21
  class InstanceConfiguration(BaseModel):
@@ -54,6 +54,9 @@ class InstanceConfiguration(BaseModel):
54
54
 
55
55
  execution_class: str | None = None
56
56
  """Execution class for the instance."""
57
+ format: Format | None = None
58
+ """Input format for the instance, if applicable. When configuring an
59
+ instance, only the `format.format_input` attribute is used."""
57
60
  options: dict | None = None
58
61
  """Options of the app that the instance uses."""
59
62
  secrets_collection_id: str | None = None
@@ -82,6 +85,13 @@ class InstanceConfiguration(BaseModel):
82
85
 
83
86
  self.execution_class = integration_val
84
87
 
88
+ # Processes the format to ensure only format_input is set.
89
+ if self.format is not None and self.format.format_input is not None:
90
+ final_format = Format(format_input=self.format.format_input)
91
+ else:
92
+ final_format = None
93
+ self.format = final_format
94
+
85
95
 
86
96
  class Instance(BaseModel):
87
97
  """An instance of an application tied to a version with configuration.
@@ -429,7 +429,7 @@ class Integration(BaseModel):
429
429
 
430
430
  integration = self.get(client=self.client, integration_id=self.integration_id)
431
431
  integration_dict = integration.to_dict()
432
- payload = integration_dict
432
+ payload = integration_dict.copy()
433
433
 
434
434
  if name is not None:
435
435
  payload["name"] = name