nextmv 0.20.1__py3-none-any.whl → 0.21.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/cloud/__init__.py +2 -0
- nextmv/cloud/application.py +109 -6
- nextmv/cloud/run.py +116 -2
- nextmv/input.py +20 -0
- nextmv/output.py +23 -0
- {nextmv-0.20.1.dist-info → nextmv-0.21.0.dist-info}/METADATA +1 -1
- {nextmv-0.20.1.dist-info → nextmv-0.21.0.dist-info}/RECORD +10 -10
- {nextmv-0.20.1.dist-info → nextmv-0.21.0.dist-info}/WHEEL +0 -0
- {nextmv-0.20.1.dist-info → nextmv-0.21.0.dist-info}/licenses/LICENSE +0 -0
nextmv/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "v0.
|
|
1
|
+
__version__ = "v0.21.0"
|
nextmv/cloud/__init__.py
CHANGED
|
@@ -48,6 +48,8 @@ from .run import RunLog as RunLog
|
|
|
48
48
|
from .run import RunResult as RunResult
|
|
49
49
|
from .run import RunType as RunType
|
|
50
50
|
from .run import RunTypeConfiguration as RunTypeConfiguration
|
|
51
|
+
from .run import TrackedRun as TrackedRun
|
|
52
|
+
from .run import TrackedRunStatus as TrackedRunStatus
|
|
51
53
|
from .secrets import Secret as Secret
|
|
52
54
|
from .secrets import SecretsCollection as SecretsCollection
|
|
53
55
|
from .secrets import SecretsCollectionSummary as SecretsCollectionSummary
|
nextmv/cloud/application.py
CHANGED
|
@@ -18,12 +18,14 @@ from nextmv.cloud.client import Client, get_size
|
|
|
18
18
|
from nextmv.cloud.input_set import InputSet
|
|
19
19
|
from nextmv.cloud.instance import Instance, InstanceConfiguration
|
|
20
20
|
from nextmv.cloud.manifest import Manifest
|
|
21
|
-
from nextmv.cloud.run import ExternalRunResult, RunConfiguration, RunInformation, RunLog, RunResult
|
|
21
|
+
from nextmv.cloud.run import ExternalRunResult, RunConfiguration, RunInformation, RunLog, RunResult, TrackedRun
|
|
22
22
|
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
23
23
|
from nextmv.cloud.status import StatusV2
|
|
24
24
|
from nextmv.cloud.version import Version
|
|
25
|
+
from nextmv.input import Input
|
|
25
26
|
from nextmv.logger import log
|
|
26
27
|
from nextmv.model import Model, ModelConfiguration
|
|
28
|
+
from nextmv.output import Output
|
|
27
29
|
|
|
28
30
|
_MAX_RUN_SIZE: int = 5 * 1024 * 1024
|
|
29
31
|
"""Maximum size of the run input/output. This value is used to determine
|
|
@@ -735,8 +737,8 @@ class Application:
|
|
|
735
737
|
def new_instance(
|
|
736
738
|
self,
|
|
737
739
|
version_id: str,
|
|
738
|
-
id:
|
|
739
|
-
name:
|
|
740
|
+
id: str,
|
|
741
|
+
name: str,
|
|
740
742
|
description: Optional[str] = None,
|
|
741
743
|
configuration: Optional[InstanceConfiguration] = None,
|
|
742
744
|
) -> Instance:
|
|
@@ -934,8 +936,8 @@ class Application:
|
|
|
934
936
|
def new_secrets_collection(
|
|
935
937
|
self,
|
|
936
938
|
secrets: list[Secret],
|
|
937
|
-
id:
|
|
938
|
-
name:
|
|
939
|
+
id: str,
|
|
940
|
+
name: str,
|
|
939
941
|
description: Optional[str] = None,
|
|
940
942
|
) -> SecretsCollectionSummary:
|
|
941
943
|
"""
|
|
@@ -1284,6 +1286,107 @@ class Application:
|
|
|
1284
1286
|
|
|
1285
1287
|
return self.__run_result(run_id=run_id, run_information=run_information)
|
|
1286
1288
|
|
|
1289
|
+
def track_run(self, tracked_run: TrackedRun) -> str:
|
|
1290
|
+
"""
|
|
1291
|
+
Track an external run.
|
|
1292
|
+
|
|
1293
|
+
This method allows you to register in Nextmv a run that happened
|
|
1294
|
+
elsewhere, as though it were executed in the Nextmv platform. Having
|
|
1295
|
+
information about a run in Nextmv is useful for things like
|
|
1296
|
+
experimenting and testing.
|
|
1297
|
+
|
|
1298
|
+
Parameters
|
|
1299
|
+
----------
|
|
1300
|
+
tracked_run : TrackedRun
|
|
1301
|
+
The run to track.
|
|
1302
|
+
|
|
1303
|
+
Returns
|
|
1304
|
+
-------
|
|
1305
|
+
str
|
|
1306
|
+
The ID of the run that was tracked.
|
|
1307
|
+
|
|
1308
|
+
Raises
|
|
1309
|
+
------
|
|
1310
|
+
requests.HTTPError
|
|
1311
|
+
If the response status code is not 2xx.
|
|
1312
|
+
ValueError
|
|
1313
|
+
If the tracked run does not have an input or output.
|
|
1314
|
+
"""
|
|
1315
|
+
url_input = self.upload_url()
|
|
1316
|
+
|
|
1317
|
+
upload_input = tracked_run.input
|
|
1318
|
+
if isinstance(tracked_run.input, Input):
|
|
1319
|
+
upload_input = tracked_run.input.to_dict()
|
|
1320
|
+
|
|
1321
|
+
self.upload_large_input(input=upload_input, upload_url=url_input)
|
|
1322
|
+
|
|
1323
|
+
url_output = self.upload_url()
|
|
1324
|
+
|
|
1325
|
+
upload_output = tracked_run.output
|
|
1326
|
+
if isinstance(tracked_run.output, Output):
|
|
1327
|
+
upload_output = tracked_run.output.to_dict()
|
|
1328
|
+
|
|
1329
|
+
self.upload_large_input(input=upload_output, upload_url=url_output)
|
|
1330
|
+
|
|
1331
|
+
external_result = ExternalRunResult(
|
|
1332
|
+
output_upload_id=url_output.upload_id,
|
|
1333
|
+
status=tracked_run.status.value,
|
|
1334
|
+
execution_duration=tracked_run.duration,
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
if tracked_run.logs is not None:
|
|
1338
|
+
url_stderr = self.upload_url()
|
|
1339
|
+
self.upload_large_input(input=tracked_run.logs_text(), upload_url=url_stderr)
|
|
1340
|
+
external_result.error_upload_id = url_stderr.upload_id
|
|
1341
|
+
|
|
1342
|
+
if tracked_run.error is not None and tracked_run.error != "":
|
|
1343
|
+
external_result.error_message = tracked_run.error
|
|
1344
|
+
|
|
1345
|
+
return self.new_run(upload_id=url_input.upload_id, external_result=external_result)
|
|
1346
|
+
|
|
1347
|
+
def track_run_with_result(
|
|
1348
|
+
self,
|
|
1349
|
+
tracked_run: TrackedRun,
|
|
1350
|
+
polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
|
|
1351
|
+
) -> RunResult:
|
|
1352
|
+
"""
|
|
1353
|
+
Track an external run and poll for the result. This is a convenience
|
|
1354
|
+
method that combines the `track_run` and `run_result_with_polling`
|
|
1355
|
+
methods. It applies polling logic to check when the run was
|
|
1356
|
+
successfully registered.
|
|
1357
|
+
|
|
1358
|
+
Parameters
|
|
1359
|
+
----------
|
|
1360
|
+
tracked_run : TrackedRun
|
|
1361
|
+
The run to track.
|
|
1362
|
+
polling_options : PollingOptions
|
|
1363
|
+
Options to use when polling for the run result.
|
|
1364
|
+
|
|
1365
|
+
Returns
|
|
1366
|
+
-------
|
|
1367
|
+
RunResult
|
|
1368
|
+
Result of the run.
|
|
1369
|
+
|
|
1370
|
+
Raises
|
|
1371
|
+
------
|
|
1372
|
+
requests.HTTPError
|
|
1373
|
+
If the response status code is not 2xx.
|
|
1374
|
+
ValueError
|
|
1375
|
+
If the tracked run does not have an input or output.
|
|
1376
|
+
TimeoutError
|
|
1377
|
+
If the run does not succeed after the polling strategy is
|
|
1378
|
+
exhausted based on time duration.
|
|
1379
|
+
RuntimeError
|
|
1380
|
+
If the run does not succeed after the polling strategy is
|
|
1381
|
+
exhausted based on number of tries.
|
|
1382
|
+
"""
|
|
1383
|
+
run_id = self.track_run(tracked_run=tracked_run)
|
|
1384
|
+
|
|
1385
|
+
return self.run_result_with_polling(
|
|
1386
|
+
run_id=run_id,
|
|
1387
|
+
polling_options=polling_options,
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1287
1390
|
def update_instance(
|
|
1288
1391
|
self,
|
|
1289
1392
|
id: str,
|
|
@@ -1334,7 +1437,7 @@ class Application:
|
|
|
1334
1437
|
name: str,
|
|
1335
1438
|
description: str,
|
|
1336
1439
|
secrets: list[Secret],
|
|
1337
|
-
) ->
|
|
1440
|
+
) -> SecretsCollectionSummary:
|
|
1338
1441
|
"""
|
|
1339
1442
|
Update a secrets collection.
|
|
1340
1443
|
|
nextmv/cloud/run.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
"""This module contains definitions for an app run."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
3
5
|
from datetime import datetime
|
|
4
6
|
from enum import Enum
|
|
5
|
-
from typing import Any, Optional
|
|
7
|
+
from typing import Any, Optional, Union
|
|
6
8
|
|
|
7
9
|
from pydantic import AliasChoices, Field
|
|
8
10
|
|
|
9
11
|
from nextmv.base_model import BaseModel
|
|
10
12
|
from nextmv.cloud.status import Status, StatusV2
|
|
11
|
-
from nextmv.input import InputFormat
|
|
13
|
+
from nextmv.input import Input, InputFormat
|
|
14
|
+
from nextmv.output import Output, OutputFormat
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class Metadata(BaseModel):
|
|
@@ -159,3 +162,114 @@ class ExternalRunResult(BaseModel):
|
|
|
159
162
|
valid_statuses = {"succeeded", "failed"}
|
|
160
163
|
if self.status is not None and self.status not in valid_statuses:
|
|
161
164
|
raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TrackedRunStatus(str, Enum):
|
|
168
|
+
"""
|
|
169
|
+
The status of a tracked run.
|
|
170
|
+
|
|
171
|
+
Attributes
|
|
172
|
+
----------
|
|
173
|
+
SUCCEEDED : str
|
|
174
|
+
The run succeeded.
|
|
175
|
+
FAILED : str
|
|
176
|
+
The run failed.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
SUCCEEDED = "succeeded"
|
|
180
|
+
"""The run succeeded."""
|
|
181
|
+
FAILED = "failed"
|
|
182
|
+
"""The run failed."""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class TrackedRun:
|
|
187
|
+
"""
|
|
188
|
+
An external run that is tracked in the Nextmv platform.
|
|
189
|
+
|
|
190
|
+
Attributes
|
|
191
|
+
----------
|
|
192
|
+
input : Union[Input, dict[str, any], str]
|
|
193
|
+
The input of the run being tracked. Please note that if the input
|
|
194
|
+
format is JSON, then the input data must be JSON serializable. This
|
|
195
|
+
field is required.
|
|
196
|
+
output : Union[Output, dict[str, any], str]
|
|
197
|
+
The output of the run being tracked. Please note that if the output
|
|
198
|
+
format is JSON, then the output data must be JSON serializable. This
|
|
199
|
+
field is required.
|
|
200
|
+
status : TrackedRunStatus
|
|
201
|
+
The status of the run being tracked. This field is required.
|
|
202
|
+
duration : Optional[int]
|
|
203
|
+
The duration of the run being tracked, in seconds. This field is
|
|
204
|
+
optional.
|
|
205
|
+
error : Optional[str]
|
|
206
|
+
An error message if the run failed. You should only specify this if the
|
|
207
|
+
run failed (the `status` is `TrackedRunStatus.FAILED`), otherwise an
|
|
208
|
+
exception will be raised. This field is optional.
|
|
209
|
+
logs : Optional[list[str]]
|
|
210
|
+
The logs of the run being tracked. Each element of the list is a line in
|
|
211
|
+
the log. This field is optional.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
input: Union[Input, dict[str, any], str]
|
|
215
|
+
"""The input of the run being tracked."""
|
|
216
|
+
output: Union[Output, dict[str, any], str]
|
|
217
|
+
"""The output of the run being tracked. Only JSON output_format is supported."""
|
|
218
|
+
status: TrackedRunStatus
|
|
219
|
+
"""The status of the run being tracked"""
|
|
220
|
+
|
|
221
|
+
duration: Optional[int] = None
|
|
222
|
+
"""The duration of the run being tracked, in seconds."""
|
|
223
|
+
error: Optional[str] = None
|
|
224
|
+
"""An error message if the run failed. You should only specify this if the
|
|
225
|
+
run failed, otherwise an exception will be raised."""
|
|
226
|
+
logs: Optional[list[str]] = None
|
|
227
|
+
"""The logs of the run being tracked. Each element of the list is a line in
|
|
228
|
+
the log."""
|
|
229
|
+
|
|
230
|
+
def __post_init__(self): # noqa: C901
|
|
231
|
+
"""Validations done after parsing the model."""
|
|
232
|
+
|
|
233
|
+
valid_statuses = {TrackedRunStatus.SUCCEEDED, TrackedRunStatus.FAILED}
|
|
234
|
+
if self.status not in valid_statuses:
|
|
235
|
+
raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
|
|
236
|
+
|
|
237
|
+
if self.error is not None and self.error != "" and self.status != TrackedRunStatus.FAILED:
|
|
238
|
+
raise ValueError("Error message must be empty if the run succeeded.")
|
|
239
|
+
|
|
240
|
+
if isinstance(self.input, Input):
|
|
241
|
+
if self.input.input_format != InputFormat.JSON:
|
|
242
|
+
raise ValueError("Input.input_format must be JSON.")
|
|
243
|
+
elif isinstance(self.input, dict):
|
|
244
|
+
try:
|
|
245
|
+
_ = json.dumps(self.input)
|
|
246
|
+
except (TypeError, OverflowError) as e:
|
|
247
|
+
raise ValueError("Input is dict[str, any] but it is not JSON serializable") from e
|
|
248
|
+
|
|
249
|
+
if isinstance(self.output, Output):
|
|
250
|
+
if self.output.output_format != OutputFormat.JSON:
|
|
251
|
+
raise ValueError("Output.output_format must be JSON.")
|
|
252
|
+
elif isinstance(self.output, dict):
|
|
253
|
+
try:
|
|
254
|
+
_ = json.dumps(self.output)
|
|
255
|
+
except (TypeError, OverflowError) as e:
|
|
256
|
+
raise ValueError("Output is dict[str, any] but it is not JSON serializable") from e
|
|
257
|
+
|
|
258
|
+
def logs_text(self) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Returns the logs as a single string.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
None
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
str
|
|
269
|
+
The logs as a single string.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
if self.logs is None:
|
|
273
|
+
return ""
|
|
274
|
+
|
|
275
|
+
return "\n".join(self.logs)
|
nextmv/input.py
CHANGED
|
@@ -91,6 +91,26 @@ class Input:
|
|
|
91
91
|
new_options = copy.deepcopy(init_options)
|
|
92
92
|
self.options = new_options
|
|
93
93
|
|
|
94
|
+
def to_dict(self) -> dict[str, any]:
|
|
95
|
+
"""
|
|
96
|
+
Convert the input to a dictionary.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
None
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
dict[str, any]
|
|
105
|
+
The input as a dictionary.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"data": self.data,
|
|
110
|
+
"input_format": self.input_format.value,
|
|
111
|
+
"options": self.options.to_dict() if self.options is not None else None,
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
|
|
95
115
|
class InputLoader:
|
|
96
116
|
"""Base class for loading inputs."""
|
nextmv/output.py
CHANGED
|
@@ -322,6 +322,29 @@ class Output:
|
|
|
322
322
|
"output_format OutputFormat.CSV_ARCHIVE, supported type is `dict`"
|
|
323
323
|
)
|
|
324
324
|
|
|
325
|
+
def to_dict(self) -> dict[str, any]:
|
|
326
|
+
"""
|
|
327
|
+
Convert the `Output` object to a dictionary.
|
|
328
|
+
|
|
329
|
+
Returns
|
|
330
|
+
-------
|
|
331
|
+
dict[str, any]
|
|
332
|
+
The dictionary representation of the `Output` object.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
output_dict = {
|
|
336
|
+
"options": self.options.to_dict() if self.options is not None else None,
|
|
337
|
+
"output_format": self.output_format,
|
|
338
|
+
"solution": self.solution,
|
|
339
|
+
"statistics": self.statistics.to_dict() if self.statistics is not None else None,
|
|
340
|
+
"assets": [asset.to_dict() for asset in self.assets] if self.assets is not None else None,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if self.output_format == OutputFormat.CSV_ARCHIVE:
|
|
344
|
+
output_dict["csv_configurations"] = self.csv_configurations
|
|
345
|
+
|
|
346
|
+
return output_dict
|
|
347
|
+
|
|
325
348
|
|
|
326
349
|
class OutputWriter:
|
|
327
350
|
"""Base class for writing outputs."""
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
nextmv/__about__.py,sha256=
|
|
1
|
+
nextmv/__about__.py,sha256=cVvubA2yQRXFVwQqlSnbiwiGxtKCAPt8AmgS0WPjXVw,24
|
|
2
2
|
nextmv/__entrypoint__.py,sha256=o6xYGBBUgCY_htteW-qNJfp-S3Th8dzS4RreJcDCnzM,1333
|
|
3
3
|
nextmv/__init__.py,sha256=rz9oP7JGyFQJdUtYX4j9xx4Gv4LMGfNvXoEID5qUqig,1354
|
|
4
4
|
nextmv/base_model.py,sha256=mdaBe-epNK1cFgP4TxbOtn3So4pCi1vMTOrIBkCBp7A,1050
|
|
5
|
-
nextmv/input.py,sha256=
|
|
5
|
+
nextmv/input.py,sha256=tYjiD9KM37Phqzm4faO4QLem8IXnU6HS160c1E-Yhc8,12732
|
|
6
6
|
nextmv/logger.py,sha256=5qQ7E3Aaw3zzkIeiUuwYGzTcB7VhVqIzNZm5PHmdpUI,852
|
|
7
7
|
nextmv/model.py,sha256=rwBdgmKSEp1DPv43zF0azj3QnbHO6O6wKs0PIGvVS40,9861
|
|
8
8
|
nextmv/options.py,sha256=DZmjNxZyaTwnG2MEEKbIiBx3hI5F2DG70rY7DdeX44M,17336
|
|
9
|
-
nextmv/output.py,sha256=
|
|
10
|
-
nextmv/cloud/__init__.py,sha256=
|
|
9
|
+
nextmv/output.py,sha256=cxw7Z0dTj5u42w2MQLOz2gesj046tvYFSWmhSZtRSXU,20082
|
|
10
|
+
nextmv/cloud/__init__.py,sha256=ojgA2vQRQnR8QO2tJme1TbGrjhr9yq9LIauxKJ5F40U,3305
|
|
11
11
|
nextmv/cloud/acceptance_test.py,sha256=NtqGhj-UYibxGBbU2kfjr-lYcngojb_5VMvK2WZwibI,6620
|
|
12
12
|
nextmv/cloud/account.py,sha256=mZUGzV-uMGBA5BC_FPtsiCMFuz5jxEZ3O1BbELZIm18,1841
|
|
13
|
-
nextmv/cloud/application.py,sha256=
|
|
13
|
+
nextmv/cloud/application.py,sha256=cmgXz9J9pdugPVqTioqzXh_1iidY0Gcbr11e_JLat3E,55765
|
|
14
14
|
nextmv/cloud/batch_experiment.py,sha256=UxrMNAm2c7-RZ9TWRhZBtiVyEeUG1pjsLNhYkoT5Jog,2236
|
|
15
15
|
nextmv/cloud/client.py,sha256=E4C8wOGOdgyMf-DCDt4YfI7ib8HwmUhAtKMdrSBJc2M,9004
|
|
16
16
|
nextmv/cloud/input_set.py,sha256=ovkP17-jYs0yWrbqTM6Nl5ubWQabD_UrDqAHNo8aE2s,672
|
|
17
17
|
nextmv/cloud/instance.py,sha256=UfyfZXfL1ugCGAB6zwZJIAi8qxI1JCUGsluwaGdjfN4,1223
|
|
18
18
|
nextmv/cloud/manifest.py,sha256=Fdjw4c14_ph20ASOzBs2mLslyLs1jGSxWThipGFp8rk,7458
|
|
19
19
|
nextmv/cloud/package.py,sha256=Y3RethLiXXW7Y6_QBJDJdd7mFNaYaSBMyasIeGLav8o,12287
|
|
20
|
-
nextmv/cloud/run.py,sha256=
|
|
20
|
+
nextmv/cloud/run.py,sha256=4hEANGXysDiOI1SQtfs7t_IVk-bxCldIs2TOvCI7a3E,8689
|
|
21
21
|
nextmv/cloud/secrets.py,sha256=kqlN4ceww_L4kVTrAU8BZykRzXINO3zhMT_BLYea6tk,1764
|
|
22
22
|
nextmv/cloud/status.py,sha256=C-ax8cLw0jPeh7CPsJkCa0s4ImRyFI4NDJJxI0_1sr4,602
|
|
23
23
|
nextmv/cloud/version.py,sha256=sjVRNRtohHA97j6IuyM33_DSSsXYkZPusYgpb6hlcrc,1244
|
|
24
|
-
nextmv-0.
|
|
25
|
-
nextmv-0.
|
|
26
|
-
nextmv-0.
|
|
27
|
-
nextmv-0.
|
|
24
|
+
nextmv-0.21.0.dist-info/METADATA,sha256=GYySweVUa_akR4revy2w1dgfgBoPJBD7q4AL95EiIeE,14557
|
|
25
|
+
nextmv-0.21.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
+
nextmv-0.21.0.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
|
|
27
|
+
nextmv-0.21.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|