nextmv 0.40.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +1 -2
- nextmv/__init__.py +2 -4
- nextmv/cli/CONTRIBUTING.md +583 -0
- nextmv/cli/cloud/__init__.py +49 -0
- nextmv/cli/cloud/acceptance/__init__.py +27 -0
- nextmv/cli/cloud/acceptance/create.py +391 -0
- nextmv/cli/cloud/acceptance/delete.py +64 -0
- nextmv/cli/cloud/acceptance/get.py +103 -0
- nextmv/cli/cloud/acceptance/list.py +62 -0
- nextmv/cli/cloud/acceptance/update.py +95 -0
- nextmv/cli/cloud/account/__init__.py +28 -0
- nextmv/cli/cloud/account/create.py +83 -0
- nextmv/cli/cloud/account/delete.py +59 -0
- nextmv/cli/cloud/account/get.py +66 -0
- nextmv/cli/cloud/account/update.py +70 -0
- nextmv/cli/cloud/app/__init__.py +35 -0
- nextmv/cli/cloud/app/create.py +140 -0
- nextmv/cli/cloud/app/delete.py +57 -0
- nextmv/cli/cloud/app/exists.py +44 -0
- nextmv/cli/cloud/app/get.py +66 -0
- nextmv/cli/cloud/app/list.py +61 -0
- nextmv/cli/cloud/app/push.py +432 -0
- nextmv/cli/cloud/app/update.py +124 -0
- nextmv/cli/cloud/batch/__init__.py +29 -0
- nextmv/cli/cloud/batch/create.py +452 -0
- nextmv/cli/cloud/batch/delete.py +64 -0
- nextmv/cli/cloud/batch/get.py +104 -0
- nextmv/cli/cloud/batch/list.py +63 -0
- nextmv/cli/cloud/batch/metadata.py +66 -0
- nextmv/cli/cloud/batch/update.py +95 -0
- nextmv/cli/cloud/data/__init__.py +26 -0
- nextmv/cli/cloud/data/upload.py +162 -0
- nextmv/cli/cloud/ensemble/__init__.py +33 -0
- nextmv/cli/cloud/ensemble/create.py +413 -0
- nextmv/cli/cloud/ensemble/delete.py +63 -0
- nextmv/cli/cloud/ensemble/get.py +65 -0
- nextmv/cli/cloud/ensemble/list.py +63 -0
- nextmv/cli/cloud/ensemble/update.py +103 -0
- nextmv/cli/cloud/input_set/__init__.py +32 -0
- nextmv/cli/cloud/input_set/create.py +168 -0
- nextmv/cli/cloud/input_set/delete.py +64 -0
- nextmv/cli/cloud/input_set/get.py +63 -0
- nextmv/cli/cloud/input_set/list.py +63 -0
- nextmv/cli/cloud/input_set/update.py +123 -0
- nextmv/cli/cloud/instance/__init__.py +35 -0
- nextmv/cli/cloud/instance/create.py +289 -0
- nextmv/cli/cloud/instance/delete.py +61 -0
- nextmv/cli/cloud/instance/exists.py +39 -0
- nextmv/cli/cloud/instance/get.py +62 -0
- nextmv/cli/cloud/instance/list.py +60 -0
- nextmv/cli/cloud/instance/update.py +216 -0
- nextmv/cli/cloud/managed_input/__init__.py +31 -0
- nextmv/cli/cloud/managed_input/create.py +144 -0
- nextmv/cli/cloud/managed_input/delete.py +64 -0
- nextmv/cli/cloud/managed_input/get.py +63 -0
- nextmv/cli/cloud/managed_input/list.py +60 -0
- nextmv/cli/cloud/managed_input/update.py +97 -0
- nextmv/cli/cloud/run/__init__.py +37 -0
- nextmv/cli/cloud/run/cancel.py +37 -0
- nextmv/cli/cloud/run/create.py +524 -0
- nextmv/cli/cloud/run/get.py +199 -0
- nextmv/cli/cloud/run/input.py +86 -0
- nextmv/cli/cloud/run/list.py +80 -0
- nextmv/cli/cloud/run/logs.py +166 -0
- nextmv/cli/cloud/run/metadata.py +67 -0
- nextmv/cli/cloud/run/track.py +500 -0
- nextmv/cli/cloud/scenario/__init__.py +29 -0
- nextmv/cli/cloud/scenario/create.py +451 -0
- nextmv/cli/cloud/scenario/delete.py +61 -0
- nextmv/cli/cloud/scenario/get.py +102 -0
- nextmv/cli/cloud/scenario/list.py +63 -0
- nextmv/cli/cloud/scenario/metadata.py +67 -0
- nextmv/cli/cloud/scenario/update.py +93 -0
- nextmv/cli/cloud/secrets/__init__.py +33 -0
- nextmv/cli/cloud/secrets/create.py +206 -0
- nextmv/cli/cloud/secrets/delete.py +63 -0
- nextmv/cli/cloud/secrets/get.py +66 -0
- nextmv/cli/cloud/secrets/list.py +60 -0
- nextmv/cli/cloud/secrets/update.py +144 -0
- nextmv/cli/cloud/shadow/__init__.py +33 -0
- nextmv/cli/cloud/shadow/create.py +184 -0
- nextmv/cli/cloud/shadow/delete.py +64 -0
- nextmv/cli/cloud/shadow/get.py +61 -0
- nextmv/cli/cloud/shadow/list.py +63 -0
- nextmv/cli/cloud/shadow/metadata.py +66 -0
- nextmv/cli/cloud/shadow/start.py +43 -0
- nextmv/cli/cloud/shadow/stop.py +53 -0
- nextmv/cli/cloud/shadow/update.py +96 -0
- nextmv/cli/cloud/switchback/__init__.py +33 -0
- nextmv/cli/cloud/switchback/create.py +151 -0
- nextmv/cli/cloud/switchback/delete.py +64 -0
- nextmv/cli/cloud/switchback/get.py +62 -0
- nextmv/cli/cloud/switchback/list.py +63 -0
- nextmv/cli/cloud/switchback/metadata.py +68 -0
- nextmv/cli/cloud/switchback/start.py +43 -0
- nextmv/cli/cloud/switchback/stop.py +53 -0
- nextmv/cli/cloud/switchback/update.py +96 -0
- nextmv/cli/cloud/upload/__init__.py +22 -0
- nextmv/cli/cloud/upload/create.py +39 -0
- nextmv/cli/cloud/version/__init__.py +33 -0
- nextmv/cli/cloud/version/create.py +96 -0
- nextmv/cli/cloud/version/delete.py +61 -0
- nextmv/cli/cloud/version/exists.py +39 -0
- nextmv/cli/cloud/version/get.py +62 -0
- nextmv/cli/cloud/version/list.py +60 -0
- nextmv/cli/cloud/version/update.py +92 -0
- nextmv/cli/community/__init__.py +24 -0
- nextmv/cli/community/clone.py +20 -204
- nextmv/cli/community/list.py +61 -126
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +103 -6
- nextmv/cli/configuration/create.py +17 -18
- nextmv/cli/configuration/delete.py +25 -13
- nextmv/cli/configuration/list.py +4 -4
- nextmv/cli/confirm.py +34 -0
- nextmv/cli/main.py +68 -36
- nextmv/cli/message.py +170 -0
- nextmv/cli/options.py +196 -0
- nextmv/cli/version.py +20 -1
- nextmv/cloud/__init__.py +17 -38
- nextmv/cloud/acceptance_test.py +20 -83
- nextmv/cloud/account.py +269 -30
- nextmv/cloud/application/__init__.py +898 -0
- nextmv/cloud/application/_acceptance.py +424 -0
- nextmv/cloud/application/_batch_scenario.py +845 -0
- nextmv/cloud/application/_ensemble.py +251 -0
- nextmv/cloud/application/_input_set.py +263 -0
- nextmv/cloud/application/_instance.py +289 -0
- nextmv/cloud/application/_managed_input.py +227 -0
- nextmv/cloud/application/_run.py +1393 -0
- nextmv/cloud/application/_secrets.py +294 -0
- nextmv/cloud/application/_shadow.py +320 -0
- nextmv/cloud/application/_switchback.py +332 -0
- nextmv/cloud/application/_utils.py +54 -0
- nextmv/cloud/application/_version.py +304 -0
- nextmv/cloud/batch_experiment.py +6 -2
- nextmv/cloud/community.py +446 -0
- nextmv/cloud/instance.py +11 -1
- nextmv/cloud/integration.py +8 -5
- nextmv/cloud/package.py +50 -9
- nextmv/cloud/shadow.py +254 -0
- nextmv/cloud/switchback.py +228 -0
- nextmv/deprecated.py +5 -3
- nextmv/input.py +20 -88
- nextmv/local/application.py +3 -15
- nextmv/local/runner.py +1 -1
- nextmv/model.py +50 -11
- nextmv/options.py +11 -256
- nextmv/output.py +0 -62
- nextmv/polling.py +54 -16
- nextmv/run.py +84 -37
- nextmv/status.py +1 -51
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/METADATA +37 -11
- nextmv-1.0.0.dist-info/RECORD +185 -0
- nextmv-1.0.0.dist-info/entry_points.txt +2 -0
- nextmv/cli/community/community.py +0 -24
- nextmv/cli/configuration/configuration.py +0 -23
- nextmv/cli/error.py +0 -22
- nextmv/cloud/application.py +0 -4204
- nextmv-0.40.0.dist-info/RECORD +0 -66
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application module for interacting with Nextmv Cloud applications.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to interact with applications in Nextmv Cloud,
|
|
5
|
+
including application management, running applications, and managing experiments
|
|
6
|
+
and inputs.
|
|
7
|
+
|
|
8
|
+
Classes
|
|
9
|
+
-------
|
|
10
|
+
ApplicationType
|
|
11
|
+
Enumeration of application types in Nextmv Cloud.
|
|
12
|
+
Application
|
|
13
|
+
Class for interacting with applications in Nextmv Cloud.
|
|
14
|
+
|
|
15
|
+
Functions
|
|
16
|
+
---------
|
|
17
|
+
list_application
|
|
18
|
+
Function to list applications in Nextmv Cloud.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import shutil
|
|
23
|
+
import sys
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import requests
|
|
29
|
+
import rich
|
|
30
|
+
from pydantic import AliasChoices, Field
|
|
31
|
+
|
|
32
|
+
from nextmv import deprecated
|
|
33
|
+
from nextmv._serialization import deflated_serialize_json
|
|
34
|
+
from nextmv.base_model import BaseModel
|
|
35
|
+
from nextmv.cloud import package
|
|
36
|
+
from nextmv.cloud.application._acceptance import ApplicationAcceptanceMixin
|
|
37
|
+
from nextmv.cloud.application._batch_scenario import ApplicationBatchMixin
|
|
38
|
+
from nextmv.cloud.application._ensemble import ApplicationEnsembleMixin
|
|
39
|
+
from nextmv.cloud.application._input_set import ApplicationInputSetMixin
|
|
40
|
+
from nextmv.cloud.application._instance import ApplicationInstanceMixin
|
|
41
|
+
from nextmv.cloud.application._managed_input import ApplicationManagedInputMixin
|
|
42
|
+
from nextmv.cloud.application._run import ApplicationRunMixin
|
|
43
|
+
from nextmv.cloud.application._secrets import ApplicationSecretsMixin
|
|
44
|
+
from nextmv.cloud.application._shadow import ApplicationShadowMixin
|
|
45
|
+
from nextmv.cloud.application._switchback import ApplicationSwitchbackMixin
|
|
46
|
+
from nextmv.cloud.application._utils import _is_not_exist_error
|
|
47
|
+
from nextmv.cloud.application._version import ApplicationVersionMixin
|
|
48
|
+
from nextmv.cloud.client import Client
|
|
49
|
+
from nextmv.cloud.url import UploadURL
|
|
50
|
+
from nextmv.logger import log
|
|
51
|
+
from nextmv.manifest import Manifest
|
|
52
|
+
from nextmv.model import Model, ModelConfiguration
|
|
53
|
+
from nextmv.safe import safe_id
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ApplicationType(str, Enum):
|
|
57
|
+
"""
|
|
58
|
+
Enumeration of application types in Nextmv Cloud.
|
|
59
|
+
|
|
60
|
+
You can import the `ApplicationType` class directly from `cloud`:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from nextmv.cloud import ApplicationType
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Attributes
|
|
67
|
+
----------
|
|
68
|
+
CUSTOM : str
|
|
69
|
+
Custom application type, which is the most common. Represents a standard
|
|
70
|
+
application that you can push code to.
|
|
71
|
+
SUBSCRIPTION : str
|
|
72
|
+
Subscription application type. You cannot push code to subscription
|
|
73
|
+
applications, but only subscribe to them through the marketplace.
|
|
74
|
+
PIPELINE : str
|
|
75
|
+
Pipeline application type that refers to workflows.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
CUSTOM = "custom"
|
|
79
|
+
"""
|
|
80
|
+
Custom application type, which is the most common. Represents a standard
|
|
81
|
+
application that you can push code to.
|
|
82
|
+
"""
|
|
83
|
+
SUBSCRIPTION = "subscription"
|
|
84
|
+
"""
|
|
85
|
+
Subscription application type. You cannot push code to subscription
|
|
86
|
+
applications, but only subscribe to them through the marketplace.
|
|
87
|
+
"""
|
|
88
|
+
PIPELINE = "pipeline"
|
|
89
|
+
"""
|
|
90
|
+
Pipeline application type that refers to workflows.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Application(
|
|
95
|
+
BaseModel,
|
|
96
|
+
ApplicationAcceptanceMixin,
|
|
97
|
+
ApplicationBatchMixin,
|
|
98
|
+
ApplicationRunMixin,
|
|
99
|
+
ApplicationEnsembleMixin,
|
|
100
|
+
ApplicationInstanceMixin,
|
|
101
|
+
ApplicationSecretsMixin,
|
|
102
|
+
ApplicationVersionMixin,
|
|
103
|
+
ApplicationInputSetMixin,
|
|
104
|
+
ApplicationManagedInputMixin,
|
|
105
|
+
ApplicationShadowMixin,
|
|
106
|
+
ApplicationSwitchbackMixin,
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
A published decision model that can be executed.
|
|
110
|
+
|
|
111
|
+
You can import the `Application` class directly from `cloud`:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from nextmv.cloud import Application
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This class represents an application in Nextmv Cloud, providing methods to
|
|
118
|
+
interact with the application, run it with different inputs, manage versions,
|
|
119
|
+
instances, experiments, and more.
|
|
120
|
+
|
|
121
|
+
Note: It is recommended to use `Application.get()` or `Application.new()`
|
|
122
|
+
instead of direct initialization to ensure proper setup.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
client : Client
|
|
127
|
+
Client to use for interacting with the Nextmv Cloud API.
|
|
128
|
+
id : str
|
|
129
|
+
ID of the application.
|
|
130
|
+
name : str, optional
|
|
131
|
+
Name of the application.
|
|
132
|
+
description : str, optional
|
|
133
|
+
Description of the application.
|
|
134
|
+
type : ApplicationType, optional
|
|
135
|
+
Type of the application (CUSTOM, SUBSCRIPTION, or PIPELINE).
|
|
136
|
+
default_instance_id : str, optional
|
|
137
|
+
Default instance ID to use for submitting runs.
|
|
138
|
+
default_experiment_instance : str, optional
|
|
139
|
+
Default experiment instance ID to use for experiments.
|
|
140
|
+
subscription_id : str, optional
|
|
141
|
+
Subscription ID if the application is a subscription type.
|
|
142
|
+
locked : bool, default=False
|
|
143
|
+
Whether the application is locked.
|
|
144
|
+
created_at : datetime, optional
|
|
145
|
+
Creation timestamp of the application.
|
|
146
|
+
updated_at : datetime, optional
|
|
147
|
+
Last update timestamp of the application.
|
|
148
|
+
endpoint : str, default="v1/applications/{id}"
|
|
149
|
+
Base endpoint for the application (SDK-specific).
|
|
150
|
+
experiments_endpoint : str, default="{base}/experiments"
|
|
151
|
+
Base endpoint for experiments (SDK-specific).
|
|
152
|
+
ensembles_endpoint : str, default="{base}/ensembles"
|
|
153
|
+
Base endpoint for ensembles (SDK-specific).
|
|
154
|
+
|
|
155
|
+
Examples
|
|
156
|
+
--------
|
|
157
|
+
>>> from nextmv.cloud import Client, Application
|
|
158
|
+
>>> client = Client(api_key="your-api-key")
|
|
159
|
+
>>> # Retrieve an existing application
|
|
160
|
+
>>> app = Application.get(client=client, id="your-app-id")
|
|
161
|
+
>>> print(f"Application name: {app.name}")
|
|
162
|
+
Application name: My Application
|
|
163
|
+
>>> # Create a new application
|
|
164
|
+
>>> new_app = Application.new(client=client, name="My New App", id="my-new-app")
|
|
165
|
+
>>> # List application instances
|
|
166
|
+
>>> instances = app.list_instances()
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
# Actual API attributes of an application.
|
|
170
|
+
id: str
|
|
171
|
+
"""ID of the application."""
|
|
172
|
+
name: str | None = None
|
|
173
|
+
"""Name of the application."""
|
|
174
|
+
description: str | None = None
|
|
175
|
+
"""Description of the application."""
|
|
176
|
+
type: ApplicationType | None = None
|
|
177
|
+
"""Type of the application."""
|
|
178
|
+
default_instance_id: str | None = Field(
|
|
179
|
+
serialization_alias="default_instance",
|
|
180
|
+
validation_alias=AliasChoices("default_instance", "default_instance_id"),
|
|
181
|
+
default=None,
|
|
182
|
+
)
|
|
183
|
+
"""Default instance ID to use for submitting runs."""
|
|
184
|
+
default_experiment_instance: str | None = None
|
|
185
|
+
"""Default experiment instance ID to use for experiments."""
|
|
186
|
+
subscription_id: str | None = None
|
|
187
|
+
"""Subscription ID if the application is a subscription type."""
|
|
188
|
+
locked: bool = False
|
|
189
|
+
"""Whether the application is locked."""
|
|
190
|
+
created_at: datetime | None = None
|
|
191
|
+
"""Creation timestamp of the application."""
|
|
192
|
+
updated_at: datetime | None = None
|
|
193
|
+
"""Last update timestamp of the application."""
|
|
194
|
+
|
|
195
|
+
# SDK-specific attributes for convenience when using methods.
|
|
196
|
+
client: Client = Field(exclude=True)
|
|
197
|
+
"""Client to use for interacting with the Nextmv Cloud API."""
|
|
198
|
+
endpoint: str = Field(exclude=True, default="v1/applications/{id}")
|
|
199
|
+
"""Base endpoint for the application."""
|
|
200
|
+
experiments_endpoint: str = Field(exclude=True, default="{base}/experiments")
|
|
201
|
+
"""Base endpoint for the experiments in the application."""
|
|
202
|
+
ensembles_endpoint: str = Field(exclude=True, default="{base}/ensembles")
|
|
203
|
+
"""Base endpoint for managing the ensemble definitions in the
|
|
204
|
+
application"""
|
|
205
|
+
|
|
206
|
+
def model_post_init(self, __context) -> None:
|
|
207
|
+
"""Initialize the endpoint and experiments_endpoint attributes.
|
|
208
|
+
|
|
209
|
+
This method is automatically called after class initialization to
|
|
210
|
+
format the endpoint and experiments_endpoint URLs with the application ID.
|
|
211
|
+
"""
|
|
212
|
+
self.endpoint = self.endpoint.format(id=self.id)
|
|
213
|
+
self.experiments_endpoint = self.experiments_endpoint.format(base=self.endpoint)
|
|
214
|
+
self.ensembles_endpoint = self.ensembles_endpoint.format(base=self.endpoint)
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def get(cls, client: Client, id: str) -> "Application":
|
|
218
|
+
"""
|
|
219
|
+
Retrieve an application directly from Nextmv Cloud.
|
|
220
|
+
|
|
221
|
+
This function is useful if you want to populate an `Application` class
|
|
222
|
+
by fetching the attributes directly from Nextmv Cloud.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
client : Client
|
|
227
|
+
Client to use for interacting with the Nextmv Cloud API.
|
|
228
|
+
id : str
|
|
229
|
+
ID of the application to retrieve.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
Application
|
|
234
|
+
The requested application.
|
|
235
|
+
|
|
236
|
+
Raises
|
|
237
|
+
------
|
|
238
|
+
requests.HTTPError
|
|
239
|
+
If the response status code is not 2xx.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
response = client.request(
|
|
243
|
+
method="GET",
|
|
244
|
+
endpoint=f"v1/applications/{id}",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return cls.from_dict({"client": client} | response.json())
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def new(
|
|
251
|
+
cls,
|
|
252
|
+
client: Client,
|
|
253
|
+
name: str | None = None,
|
|
254
|
+
id: str | None = None,
|
|
255
|
+
description: str | None = None,
|
|
256
|
+
is_workflow: bool | None = None,
|
|
257
|
+
exist_ok: bool = False,
|
|
258
|
+
default_instance_id: str | None = None,
|
|
259
|
+
default_experiment_instance: str | None = None,
|
|
260
|
+
) -> "Application":
|
|
261
|
+
"""
|
|
262
|
+
Create a new application directly in Nextmv Cloud.
|
|
263
|
+
|
|
264
|
+
The application is created as an empty shell, and executable code must
|
|
265
|
+
be pushed to the app before running it remotely.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
client : Client
|
|
270
|
+
Client to use for interacting with the Nextmv Cloud API.
|
|
271
|
+
name : str | None = None
|
|
272
|
+
Name of the application. Uses the ID as the name if not provided.
|
|
273
|
+
id : str | None = None
|
|
274
|
+
ID of the application. Will be generated if not provided.
|
|
275
|
+
description : str | None = None
|
|
276
|
+
Description of the application.
|
|
277
|
+
is_workflow : bool | None = None
|
|
278
|
+
Whether the application is a Decision Workflow.
|
|
279
|
+
exist_ok : bool, default=False
|
|
280
|
+
If True and an application with the same ID already exists,
|
|
281
|
+
return the existing application instead of creating a new one.
|
|
282
|
+
default_instance_id : str, optional
|
|
283
|
+
Default instance ID to use for submitting runs.
|
|
284
|
+
default_experiment_instance : str, optional
|
|
285
|
+
Default experiment instance ID to use for experiments.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
Application
|
|
290
|
+
The newly created (or existing) application.
|
|
291
|
+
|
|
292
|
+
Examples
|
|
293
|
+
--------
|
|
294
|
+
>>> from nextmv.cloud import Client
|
|
295
|
+
>>> client = Client(api_key="your-api-key")
|
|
296
|
+
>>> app = Application.new(client=client, name="My New App", id="my-app")
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
if exist_ok and (id is None or id == ""):
|
|
300
|
+
raise ValueError("If exist_ok is True, id must be provided")
|
|
301
|
+
|
|
302
|
+
if id is None or id == "":
|
|
303
|
+
id = safe_id("app")
|
|
304
|
+
|
|
305
|
+
if exist_ok and cls.exists(client=client, id=id):
|
|
306
|
+
response = client.request(
|
|
307
|
+
method="GET",
|
|
308
|
+
endpoint=f"v1/applications/{id}",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
return cls.from_dict({"client": client} | response.json())
|
|
312
|
+
|
|
313
|
+
if name is None or name == "":
|
|
314
|
+
name = id
|
|
315
|
+
|
|
316
|
+
payload = {
|
|
317
|
+
"name": name,
|
|
318
|
+
"id": id,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if description is not None:
|
|
322
|
+
payload["description"] = description
|
|
323
|
+
|
|
324
|
+
if is_workflow is not None:
|
|
325
|
+
payload["is_pipeline"] = is_workflow
|
|
326
|
+
|
|
327
|
+
if default_instance_id is not None:
|
|
328
|
+
payload["default_instance"] = default_instance_id
|
|
329
|
+
|
|
330
|
+
if default_experiment_instance is not None:
|
|
331
|
+
payload["default_experiment_instance"] = default_experiment_instance
|
|
332
|
+
|
|
333
|
+
response = client.request(
|
|
334
|
+
method="POST",
|
|
335
|
+
endpoint="v1/applications",
|
|
336
|
+
payload=payload,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return cls.from_dict({"client": client} | response.json())
|
|
340
|
+
|
|
341
|
+
def delete(self) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Delete the application.
|
|
344
|
+
|
|
345
|
+
Permanently removes the application from Nextmv Cloud.
|
|
346
|
+
|
|
347
|
+
Raises
|
|
348
|
+
------
|
|
349
|
+
requests.HTTPError
|
|
350
|
+
If the response status code is not 2xx.
|
|
351
|
+
|
|
352
|
+
Examples
|
|
353
|
+
--------
|
|
354
|
+
>>> app.delete() # Permanently deletes the application
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
_ = self.client.request(
|
|
358
|
+
method="DELETE",
|
|
359
|
+
endpoint=self.endpoint,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def exists(client: Client, id: str) -> bool:
|
|
364
|
+
"""
|
|
365
|
+
Check if an application exists.
|
|
366
|
+
|
|
367
|
+
Parameters
|
|
368
|
+
----------
|
|
369
|
+
client : Client
|
|
370
|
+
Client to use for interacting with the Nextmv Cloud API.
|
|
371
|
+
id : str
|
|
372
|
+
ID of the application to check.
|
|
373
|
+
|
|
374
|
+
Returns
|
|
375
|
+
-------
|
|
376
|
+
bool
|
|
377
|
+
True if the application exists, False otherwise.
|
|
378
|
+
|
|
379
|
+
Examples
|
|
380
|
+
--------
|
|
381
|
+
>>> from nextmv.cloud import Client
|
|
382
|
+
>>> client = Client(api_key="your-api-key")
|
|
383
|
+
>>> Application.exists(client, "app-123")
|
|
384
|
+
True
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
_ = client.request(
|
|
389
|
+
method="GET",
|
|
390
|
+
endpoint=f"v1/applications/{id}",
|
|
391
|
+
)
|
|
392
|
+
# If the request was successful, the application exists.
|
|
393
|
+
return True
|
|
394
|
+
except requests.HTTPError as e:
|
|
395
|
+
if _is_not_exist_error(e):
|
|
396
|
+
return False
|
|
397
|
+
# Re-throw the exception if it is not the expected 404 error.
|
|
398
|
+
raise e from None
|
|
399
|
+
|
|
400
|
+
def push( # noqa: C901
|
|
401
|
+
self,
|
|
402
|
+
manifest: Manifest | None = None,
|
|
403
|
+
app_dir: str | None = None,
|
|
404
|
+
verbose: bool = False,
|
|
405
|
+
model: Model | None = None,
|
|
406
|
+
model_configuration: ModelConfiguration | None = None,
|
|
407
|
+
rich_print: bool = False,
|
|
408
|
+
) -> None:
|
|
409
|
+
"""
|
|
410
|
+
Push an app to Nextmv Cloud.
|
|
411
|
+
|
|
412
|
+
If the manifest is not provided, an `app.yaml` file will be searched
|
|
413
|
+
for in the provided path. If there is no manifest file found, an
|
|
414
|
+
exception will be raised.
|
|
415
|
+
|
|
416
|
+
There are two ways to push an app to Nextmv Cloud:
|
|
417
|
+
1. Specifying `app_dir`, which is the path to an app's root directory.
|
|
418
|
+
This acts as an external strategy, where the app is composed of files
|
|
419
|
+
in a directory and those apps are packaged and pushed to Nextmv Cloud.
|
|
420
|
+
2. Specifying a `model` and `model_configuration`. This acts as an
|
|
421
|
+
internal (or Python-native) strategy, where the app is actually a
|
|
422
|
+
`nextmv.Model`. The model is encoded, some dependencies and
|
|
423
|
+
accompanying files are packaged, and the app is pushed to Nextmv Cloud.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
manifest : Optional[Manifest], default=None
|
|
428
|
+
The manifest for the app. If None, an `app.yaml` file in the provided
|
|
429
|
+
app directory will be used.
|
|
430
|
+
app_dir : Optional[str], default=None
|
|
431
|
+
The path to the app's root directory. If None, the current directory
|
|
432
|
+
will be used. This is for the external strategy approach.
|
|
433
|
+
verbose : bool, default=False
|
|
434
|
+
Whether to print verbose output during the push process.
|
|
435
|
+
model : Optional[Model], default=None
|
|
436
|
+
The Python-native model to push. Must be specified together with
|
|
437
|
+
`model_configuration`. This is for the internal strategy approach.
|
|
438
|
+
model_configuration : Optional[ModelConfiguration], default=None
|
|
439
|
+
Configuration for the Python-native model. Must be specified together
|
|
440
|
+
with `model`.
|
|
441
|
+
rich_print : bool, default=False
|
|
442
|
+
Whether to use rich printing when verbose output is enabled.
|
|
443
|
+
|
|
444
|
+
Raises
|
|
445
|
+
------
|
|
446
|
+
ValueError
|
|
447
|
+
If neither app_dir nor model/model_configuration is provided correctly,
|
|
448
|
+
or if only one of model and model_configuration is provided.
|
|
449
|
+
TypeError
|
|
450
|
+
If model is not an instance of nextmv.Model or if model_configuration
|
|
451
|
+
is not an instance of nextmv.ModelConfiguration.
|
|
452
|
+
Exception
|
|
453
|
+
If there's an error in the build, packaging, or cleanup process.
|
|
454
|
+
|
|
455
|
+
Examples
|
|
456
|
+
--------
|
|
457
|
+
1. Push an app using an external strategy (directory-based):
|
|
458
|
+
|
|
459
|
+
>>> import os
|
|
460
|
+
>>> from nextmv import cloud
|
|
461
|
+
>>> client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
|
|
462
|
+
>>> app = cloud.Application(client=client, id="<YOUR-APP-ID>")
|
|
463
|
+
>>> app.push() # Use verbose=True for step-by-step output.
|
|
464
|
+
|
|
465
|
+
2. Push an app using an internal strategy (Python-native model):
|
|
466
|
+
|
|
467
|
+
>>> import os
|
|
468
|
+
>>> import nextroute
|
|
469
|
+
>>> import nextmv
|
|
470
|
+
>>> import nextmv.cloud
|
|
471
|
+
>>>
|
|
472
|
+
>>> # Define the model that makes decisions
|
|
473
|
+
>>> class DecisionModel(nextmv.Model):
|
|
474
|
+
... def solve(self, input: nextmv.Input) -> nextmv.Output:
|
|
475
|
+
... nextroute_input = nextroute.schema.Input.from_dict(input.data)
|
|
476
|
+
... nextroute_options = nextroute.Options.extract_from_dict(input.options.to_dict())
|
|
477
|
+
... nextroute_output = nextroute.solve(nextroute_input, nextroute_options)
|
|
478
|
+
...
|
|
479
|
+
... return nextmv.Output(
|
|
480
|
+
... options=input.options,
|
|
481
|
+
... solution=nextroute_output.solutions[0].to_dict(),
|
|
482
|
+
... statistics=nextroute_output.statistics.to_dict(),
|
|
483
|
+
... )
|
|
484
|
+
>>>
|
|
485
|
+
>>> # Define the options that the model needs
|
|
486
|
+
>>> opt = []
|
|
487
|
+
>>> default_options = nextroute.Options()
|
|
488
|
+
>>> for name, default_value in default_options.to_dict().items():
|
|
489
|
+
... opt.append(nextmv.Option(name.lower(), type(default_value), default_value, name, False))
|
|
490
|
+
>>> options = nextmv.Options(*opt)
|
|
491
|
+
>>>
|
|
492
|
+
>>> # Instantiate the model and model configuration
|
|
493
|
+
>>> model = DecisionModel()
|
|
494
|
+
>>> model_configuration = nextmv.ModelConfiguration(
|
|
495
|
+
... name="python_nextroute_model",
|
|
496
|
+
... requirements=[
|
|
497
|
+
... "nextroute==1.8.1",
|
|
498
|
+
... "nextmv==0.14.0.dev1",
|
|
499
|
+
... ],
|
|
500
|
+
... options=options,
|
|
501
|
+
... )
|
|
502
|
+
>>>
|
|
503
|
+
>>> # Push the model to Nextmv Cloud
|
|
504
|
+
>>> client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
|
|
505
|
+
>>> app = cloud.Application(client=client, id="<YOUR-APP-ID>")
|
|
506
|
+
>>> manifest = nextmv.cloud.default_python_manifest()
|
|
507
|
+
>>> app.push(
|
|
508
|
+
... manifest=manifest,
|
|
509
|
+
... verbose=True,
|
|
510
|
+
... model=model,
|
|
511
|
+
... model_configuration=model_configuration,
|
|
512
|
+
... )
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
if verbose:
|
|
516
|
+
if rich_print:
|
|
517
|
+
rich.print(f":cd: Starting build for Nextmv application [magenta]{self.id}[/magenta].", file=sys.stderr)
|
|
518
|
+
else:
|
|
519
|
+
log("💽 Starting build for Nextmv application.")
|
|
520
|
+
|
|
521
|
+
if app_dir is None or app_dir == "":
|
|
522
|
+
app_dir = "."
|
|
523
|
+
|
|
524
|
+
if manifest is None:
|
|
525
|
+
manifest = Manifest.from_yaml(app_dir)
|
|
526
|
+
|
|
527
|
+
if model is not None and not isinstance(model, Model):
|
|
528
|
+
raise TypeError("model must be an instance of nextmv.Model")
|
|
529
|
+
|
|
530
|
+
if model_configuration is not None and not isinstance(model_configuration, ModelConfiguration):
|
|
531
|
+
raise TypeError("model_configuration must be an instance of nextmv.ModelConfiguration")
|
|
532
|
+
|
|
533
|
+
if (model is None and model_configuration is not None) or (model is not None and model_configuration is None):
|
|
534
|
+
raise ValueError("model and model_configuration must be provided together")
|
|
535
|
+
|
|
536
|
+
package._run_build_command(app_dir, manifest.build, verbose, rich_print)
|
|
537
|
+
package._run_pre_push_command(app_dir, manifest.pre_push, verbose, rich_print)
|
|
538
|
+
tar_file, output_dir = package._package(app_dir, manifest, model, model_configuration, verbose, rich_print)
|
|
539
|
+
self.__update_app_binary(tar_file, manifest, verbose, rich_print)
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
shutil.rmtree(output_dir)
|
|
543
|
+
except OSError as e:
|
|
544
|
+
raise Exception(f"error deleting output directory: {e}") from e
|
|
545
|
+
|
|
546
|
+
def update(
|
|
547
|
+
self,
|
|
548
|
+
name: str | None = None,
|
|
549
|
+
description: str | None = None,
|
|
550
|
+
default_instance_id: str | None = None,
|
|
551
|
+
default_experiment_instance: str | None = None,
|
|
552
|
+
) -> "Application":
|
|
553
|
+
"""
|
|
554
|
+
Update the application.
|
|
555
|
+
|
|
556
|
+
Parameters
|
|
557
|
+
----------
|
|
558
|
+
name : Optional[str], default=None
|
|
559
|
+
Optional name of the application.
|
|
560
|
+
description : Optional[str], default=None
|
|
561
|
+
Optional description of the application.
|
|
562
|
+
default_instance_id : Optional[str], default=None
|
|
563
|
+
Optional default instance ID for the application.
|
|
564
|
+
default_experiment_instance : Optional[str], default=None
|
|
565
|
+
Optional default experiment instance ID for the application.
|
|
566
|
+
|
|
567
|
+
Returns
|
|
568
|
+
-------
|
|
569
|
+
Application
|
|
570
|
+
The updated application.
|
|
571
|
+
|
|
572
|
+
Raises
|
|
573
|
+
------
|
|
574
|
+
requests.HTTPError
|
|
575
|
+
If the response status code is not 2xx.
|
|
576
|
+
"""
|
|
577
|
+
|
|
578
|
+
app = self.get(client=self.client, id=self.id)
|
|
579
|
+
app_dict = app.to_dict()
|
|
580
|
+
payload = app_dict.copy()
|
|
581
|
+
|
|
582
|
+
if name is not None:
|
|
583
|
+
payload["name"] = name
|
|
584
|
+
if description is not None:
|
|
585
|
+
payload["description"] = description
|
|
586
|
+
if default_instance_id is not None:
|
|
587
|
+
payload["default_instance"] = default_instance_id
|
|
588
|
+
if default_experiment_instance is not None:
|
|
589
|
+
payload["default_experiment_instance"] = default_experiment_instance
|
|
590
|
+
|
|
591
|
+
response = self.client.request(
|
|
592
|
+
method="PUT",
|
|
593
|
+
endpoint=self.endpoint,
|
|
594
|
+
payload=payload,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
return Application.from_dict({"client": self.client} | response.json())
|
|
598
|
+
|
|
599
|
+
def upload_data(
|
|
600
|
+
self,
|
|
601
|
+
upload_url: UploadURL | str,
|
|
602
|
+
data: dict[str, Any] | str | None = None,
|
|
603
|
+
json_configurations: dict[str, Any] | None = None,
|
|
604
|
+
tar_file: str | None = None,
|
|
605
|
+
) -> None:
|
|
606
|
+
"""
|
|
607
|
+
Upload data to the provided upload URL.
|
|
608
|
+
|
|
609
|
+
This method allows uploading data (either a dictionary or string)
|
|
610
|
+
to a pre-signed URL. If the data is a dictionary, it will be converted to
|
|
611
|
+
a JSON string before upload.
|
|
612
|
+
|
|
613
|
+
Parameters
|
|
614
|
+
----------
|
|
615
|
+
upload_url : UploadURL | str
|
|
616
|
+
Upload URL object containing the pre-signed URL to use for
|
|
617
|
+
uploading. If it is a string, it will be used directly as the
|
|
618
|
+
pre-signed URL.
|
|
619
|
+
data : Optional[Union[dict[str, Any], str]]
|
|
620
|
+
Data to upload. Can be either a dictionary that will be
|
|
621
|
+
converted to JSON, or a pre-formatted JSON string.
|
|
622
|
+
json_configurations : Optional[dict[str, Any]], default=None
|
|
623
|
+
Optional configurations for JSON serialization. If provided, these
|
|
624
|
+
configurations will be used when serializing the data via
|
|
625
|
+
`json.dumps`.
|
|
626
|
+
tar_file : Optional[str], default=None
|
|
627
|
+
If provided, this will be used to upload a tar file instead of
|
|
628
|
+
a JSON string or dictionary. This is useful for uploading large
|
|
629
|
+
files that are already packaged as a tarball.
|
|
630
|
+
|
|
631
|
+
Returns
|
|
632
|
+
-------
|
|
633
|
+
None
|
|
634
|
+
This method doesn't return anything.
|
|
635
|
+
|
|
636
|
+
Raises
|
|
637
|
+
------
|
|
638
|
+
requests.HTTPError
|
|
639
|
+
If the response status code is not 2xx.
|
|
640
|
+
|
|
641
|
+
Examples
|
|
642
|
+
--------
|
|
643
|
+
>>> # Upload a dictionary as JSON
|
|
644
|
+
>>> data = {"locations": [...], "vehicles": [...]}
|
|
645
|
+
>>> url = app.upload_url()
|
|
646
|
+
>>> app.upload_data(data=data, upload_url=url)
|
|
647
|
+
>>>
|
|
648
|
+
>>> # Upload a pre-formatted JSON string
|
|
649
|
+
>>> json_str = '{"locations": [...], "vehicles": [...]}'
|
|
650
|
+
>>> app.upload_data(data=json_str, upload_url=url)
|
|
651
|
+
"""
|
|
652
|
+
|
|
653
|
+
if data is not None and isinstance(data, dict):
|
|
654
|
+
data = deflated_serialize_json(data, json_configurations=json_configurations)
|
|
655
|
+
|
|
656
|
+
self.client.upload_to_presigned_url(
|
|
657
|
+
url=upload_url.upload_url if isinstance(upload_url, UploadURL) else upload_url,
|
|
658
|
+
data=data,
|
|
659
|
+
tar_file=tar_file,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
def upload_large_input(
|
|
663
|
+
self,
|
|
664
|
+
input: dict[str, Any] | str | None,
|
|
665
|
+
upload_url: UploadURL,
|
|
666
|
+
json_configurations: dict[str, Any] | None = None,
|
|
667
|
+
tar_file: str | None = None,
|
|
668
|
+
) -> None:
|
|
669
|
+
"""
|
|
670
|
+
!!! warning
|
|
671
|
+
`upload_large_input` is deprecated, use `upload_data` instead.
|
|
672
|
+
|
|
673
|
+
Upload large input data to the provided upload URL.
|
|
674
|
+
|
|
675
|
+
This method allows uploading large input data (either a dictionary or string)
|
|
676
|
+
to a pre-signed URL. If the input is a dictionary, it will be converted to
|
|
677
|
+
a JSON string before upload.
|
|
678
|
+
|
|
679
|
+
Parameters
|
|
680
|
+
----------
|
|
681
|
+
input : Optional[Union[dict[str, Any], str]]
|
|
682
|
+
Input data to upload. Can be either a dictionary that will be
|
|
683
|
+
converted to JSON, or a pre-formatted JSON string.
|
|
684
|
+
upload_url : UploadURL
|
|
685
|
+
Upload URL object containing the pre-signed URL to use for uploading.
|
|
686
|
+
json_configurations : Optional[dict[str, Any]], default=None
|
|
687
|
+
Optional configurations for JSON serialization. If provided, these
|
|
688
|
+
configurations will be used when serializing the data via
|
|
689
|
+
`json.dumps`.
|
|
690
|
+
tar_file : Optional[str], default=None
|
|
691
|
+
If provided, this will be used to upload a tar file instead of
|
|
692
|
+
a JSON string or dictionary. This is useful for uploading large
|
|
693
|
+
files that are already packaged as a tarball.
|
|
694
|
+
|
|
695
|
+
Returns
|
|
696
|
+
-------
|
|
697
|
+
None
|
|
698
|
+
This method doesn't return anything.
|
|
699
|
+
|
|
700
|
+
Raises
|
|
701
|
+
------
|
|
702
|
+
requests.HTTPError
|
|
703
|
+
If the response status code is not 2xx.
|
|
704
|
+
|
|
705
|
+
Examples
|
|
706
|
+
--------
|
|
707
|
+
>>> # Upload a dictionary as JSON
|
|
708
|
+
>>> data = {"locations": [...], "vehicles": [...]}
|
|
709
|
+
>>> url = app.upload_url()
|
|
710
|
+
>>> app.upload_large_input(input=data, upload_url=url)
|
|
711
|
+
>>>
|
|
712
|
+
>>> # Upload a pre-formatted JSON string
|
|
713
|
+
>>> json_str = '{"locations": [...], "vehicles": [...]}'
|
|
714
|
+
>>> app.upload_large_input(input=json_str, upload_url=url)
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
deprecated(
|
|
718
|
+
name="Application.upload_large_input",
|
|
719
|
+
reason="`upload_large_input` is deprecated, use `upload_data` instead",
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
self.upload_data(
|
|
723
|
+
data=input,
|
|
724
|
+
upload_url=upload_url,
|
|
725
|
+
json_configurations=json_configurations,
|
|
726
|
+
tar_file=tar_file,
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
def upload_url(self) -> UploadURL:
|
|
730
|
+
"""
|
|
731
|
+
Get an upload URL to use for uploading a file.
|
|
732
|
+
|
|
733
|
+
This method generates a pre-signed URL that can be used to upload large files
|
|
734
|
+
to Nextmv Cloud. It's primarily used for uploading large input data, output
|
|
735
|
+
results, or log files that exceed the size limits for direct API calls.
|
|
736
|
+
|
|
737
|
+
Returns
|
|
738
|
+
-------
|
|
739
|
+
UploadURL
|
|
740
|
+
An object containing both the upload URL and an upload ID for reference.
|
|
741
|
+
The upload URL is a pre-signed URL that allows temporary write access.
|
|
742
|
+
|
|
743
|
+
Raises
|
|
744
|
+
------
|
|
745
|
+
requests.HTTPError
|
|
746
|
+
If the response status code is not 2xx.
|
|
747
|
+
|
|
748
|
+
Examples
|
|
749
|
+
--------
|
|
750
|
+
>>> # Get an upload URL and upload large input data
|
|
751
|
+
>>> upload_url = app.upload_url()
|
|
752
|
+
>>> large_input = {"locations": [...], "vehicles": [...]}
|
|
753
|
+
>>> app.upload_data(data=large_input, upload_url=upload_url)
|
|
754
|
+
"""
|
|
755
|
+
|
|
756
|
+
response = self.client.request(
|
|
757
|
+
method="POST",
|
|
758
|
+
endpoint=f"{self.endpoint}/runs/uploadurl",
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
return UploadURL.from_dict(response.json())
|
|
762
|
+
|
|
763
|
+
@staticmethod
|
|
764
|
+
def __convert_manifest_to_payload(manifest: Manifest) -> dict[str, Any]: # noqa: C901
|
|
765
|
+
"""Converts a manifest to a payload dictionary for the API."""
|
|
766
|
+
|
|
767
|
+
activation_request = {
|
|
768
|
+
"requirements": {
|
|
769
|
+
"executable_type": manifest.type,
|
|
770
|
+
"runtime": manifest.runtime,
|
|
771
|
+
},
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if manifest.configuration is not None and manifest.configuration.content is not None:
|
|
775
|
+
content = manifest.configuration.content
|
|
776
|
+
io_config = {
|
|
777
|
+
"format": content.format,
|
|
778
|
+
}
|
|
779
|
+
if content.multi_file is not None:
|
|
780
|
+
multi_config = io_config["multi_file"] = {}
|
|
781
|
+
if content.multi_file.input is not None:
|
|
782
|
+
multi_config["input_path"] = content.multi_file.input.path
|
|
783
|
+
if content.multi_file.output is not None:
|
|
784
|
+
output_config = multi_config["output_configuration"] = {}
|
|
785
|
+
if content.multi_file.output.statistics:
|
|
786
|
+
output_config["statistics_path"] = content.multi_file.output.statistics
|
|
787
|
+
if content.multi_file.output.assets:
|
|
788
|
+
output_config["assets_path"] = content.multi_file.output.assets
|
|
789
|
+
if content.multi_file.output.solutions:
|
|
790
|
+
output_config["solutions_path"] = content.multi_file.output.solutions
|
|
791
|
+
activation_request["requirements"]["io_configuration"] = io_config
|
|
792
|
+
|
|
793
|
+
if manifest.configuration is not None and manifest.configuration.options is not None:
|
|
794
|
+
options = manifest.configuration.options.to_dict()
|
|
795
|
+
if "format" in options and isinstance(options["format"], list):
|
|
796
|
+
# the endpoint expects a dictionary with a template key having a list of strings
|
|
797
|
+
# the app.yaml however defines format as a list of strings, so we need to convert it here
|
|
798
|
+
options["format"] = {
|
|
799
|
+
"template": options["format"],
|
|
800
|
+
}
|
|
801
|
+
activation_request["requirements"]["options"] = options
|
|
802
|
+
|
|
803
|
+
if manifest.execution is not None:
|
|
804
|
+
if manifest.execution.entrypoint:
|
|
805
|
+
activation_request["requirements"]["entrypoint"] = manifest.execution.entrypoint
|
|
806
|
+
if manifest.execution.cwd:
|
|
807
|
+
activation_request["requirements"]["working_directory"] = manifest.execution.cwd
|
|
808
|
+
|
|
809
|
+
return activation_request
|
|
810
|
+
|
|
811
|
+
def __update_app_binary(
|
|
812
|
+
self,
|
|
813
|
+
tar_file: str,
|
|
814
|
+
manifest: Manifest,
|
|
815
|
+
verbose: bool = False,
|
|
816
|
+
rich_print: bool = False,
|
|
817
|
+
) -> None:
|
|
818
|
+
"""Updates the application binary in Cloud."""
|
|
819
|
+
|
|
820
|
+
if verbose:
|
|
821
|
+
if rich_print:
|
|
822
|
+
rich.print(f":star2: Pushing to application: [magenta]{self.id}[/magenta].", file=sys.stderr)
|
|
823
|
+
else:
|
|
824
|
+
log(f'🌟 Pushing to application: "{self.id}".')
|
|
825
|
+
|
|
826
|
+
endpoint = f"{self.endpoint}/binary"
|
|
827
|
+
response = self.client.request(
|
|
828
|
+
method="GET",
|
|
829
|
+
endpoint=endpoint,
|
|
830
|
+
)
|
|
831
|
+
upload_url = response.json()["upload_url"]
|
|
832
|
+
|
|
833
|
+
with open(tar_file, "rb") as f:
|
|
834
|
+
response = self.client.request(
|
|
835
|
+
method="PUT",
|
|
836
|
+
endpoint=upload_url,
|
|
837
|
+
data=f,
|
|
838
|
+
headers={"Content-Type": "application/octet-stream"},
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
response = self.client.request(
|
|
842
|
+
method="PUT",
|
|
843
|
+
endpoint=endpoint,
|
|
844
|
+
payload=Application.__convert_manifest_to_payload(manifest=manifest),
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
if verbose:
|
|
848
|
+
data = {
|
|
849
|
+
"app_id": self.id,
|
|
850
|
+
"endpoint": self.client.url,
|
|
851
|
+
"instance_url": f"{self.endpoint}/runs?instance_id=latest",
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if rich_print:
|
|
855
|
+
rich.print(f":boom: Successfully pushed to application: [magenta]{self.id}[/magenta].", file=sys.stderr)
|
|
856
|
+
rich.print_json(data=data)
|
|
857
|
+
else:
|
|
858
|
+
log(f'💥️ Successfully pushed to application: "{self.id}".')
|
|
859
|
+
log(json.dumps(data, indent=2))
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def list_applications(client: Client) -> list[Application]:
|
|
863
|
+
"""
|
|
864
|
+
List all Nextmv Cloud applications.
|
|
865
|
+
|
|
866
|
+
You can import the `list_applications` function directly from `cloud`:
|
|
867
|
+
|
|
868
|
+
```python
|
|
869
|
+
from nextmv.cloud import list_applications
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
Parameters
|
|
873
|
+
----------
|
|
874
|
+
client : Client
|
|
875
|
+
The Nextmv Cloud client used to make API requests.
|
|
876
|
+
|
|
877
|
+
Returns
|
|
878
|
+
-------
|
|
879
|
+
list[Application]
|
|
880
|
+
A list of Nextmv Cloud applications.
|
|
881
|
+
|
|
882
|
+
Raises
|
|
883
|
+
-------
|
|
884
|
+
requests.HTTPError
|
|
885
|
+
If the response status code is not 2xx.
|
|
886
|
+
"""
|
|
887
|
+
|
|
888
|
+
response = client.request(
|
|
889
|
+
method="GET",
|
|
890
|
+
endpoint="v1/applications",
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
applications = []
|
|
894
|
+
for app_data in response.json() or []:
|
|
895
|
+
app = Application.from_dict({"client": client} | app_data)
|
|
896
|
+
applications.append(app)
|
|
897
|
+
|
|
898
|
+
return applications
|