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
nextmv/manifest.py ADDED
@@ -0,0 +1,1466 @@
1
+ """
2
+ Module with the logic for handling an app manifest.
3
+
4
+ This module provides classes and functions for managing Nextmv app manifests.
5
+ Manifest files (app.yaml) define how an application is built, run, and deployed
6
+ on the Nextmv platform.
7
+
8
+ Classes
9
+ -------
10
+ ManifestType
11
+ Enum for application types based on programming language.
12
+ ManifestRuntime
13
+ Enum for runtime environments where apps run on Nextmv.
14
+ ManifestPythonArch
15
+ Enum for target architecture for bundling Python apps.
16
+ ManifestBuild
17
+ Class for build-specific attributes in the manifest.
18
+ ManifestPythonModel
19
+ Class for model-specific instructions for Python apps.
20
+ ManifestPython
21
+ Class for Python-specific instructions in the manifest.
22
+ ManifestOptionUI
23
+ Class for UI attributes of options in the manifest.
24
+ ManifestOption
25
+ Class representing an option for the decision model in the manifest.
26
+ ManifestOptions
27
+ Class containing a list of options for the decision model.
28
+ ManifestValidation
29
+ Class for validation rules for options in the manifest.
30
+ ManifestContentMultiFileInput
31
+ Class for multi-file content format input configuration.
32
+ ManifestContentMultiFileOutput
33
+ Class for multi-file content format output configuration.
34
+ ManifestContentMultiFile
35
+ Class for multi-file content format configuration.
36
+ ManifestContent
37
+ Class for content configuration specifying how app input/output is handled.
38
+ ManifestConfiguration
39
+ Class for configuration settings for the decision model.
40
+ Manifest
41
+ Main class representing an app manifest for Nextmv.
42
+
43
+ Functions
44
+ ---------
45
+ default_python_manifest
46
+ Creates a default Python manifest as a starting point for applications.
47
+
48
+ Constants
49
+ --------
50
+ MANIFEST_FILE_NAME
51
+ Name of the app manifest file.
52
+ """
53
+
54
+ import os
55
+ from enum import Enum
56
+ from typing import Any
57
+
58
+ import yaml
59
+ from pydantic import AliasChoices, Field, field_validator
60
+
61
+ from nextmv.base_model import BaseModel
62
+ from nextmv.input import InputFormat
63
+ from nextmv.model import _REQUIREMENTS_FILE, ModelConfiguration
64
+ from nextmv.options import Option, Options, OptionsEnforcement
65
+
66
+ MANIFEST_FILE_NAME = "app.yaml"
67
+ """Name of the app manifest file.
68
+
69
+ This constant defines the standard filename for Nextmv app manifest files.
70
+
71
+ You can import the `MANIFEST_FILE_NAME` constant directly from `nextmv`:
72
+
73
+ ```python
74
+ from nextmv import MANIFEST_FILE_NAME
75
+ ```
76
+
77
+ Notes
78
+ -----
79
+ All Nextmv applications must include an app.yaml file for proper deployment.
80
+ """
81
+
82
+
83
+ class ManifestType(str, Enum):
84
+ """
85
+ Type of application in the manifest, based on the programming language.
86
+
87
+ You can import the `ManifestType` class directly from `nextmv`:
88
+
89
+ ```python
90
+ from nextmv import ManifestType
91
+ ```
92
+
93
+ This enum defines the supported programming languages for applications
94
+ that can be deployed on Nextmv Cloud.
95
+
96
+ Attributes
97
+ ----------
98
+ PYTHON : str
99
+ Python format, used for Python applications.
100
+ GO : str
101
+ Go format, used for Go applications.
102
+ JAVA : str
103
+ Java format, used for Java applications.
104
+
105
+ Examples
106
+ --------
107
+ >>> from nextmv import ManifestType
108
+ >>> manifest_type = ManifestType.PYTHON
109
+ >>> manifest_type
110
+ <ManifestType.PYTHON: 'python'>
111
+ >>> str(manifest_type)
112
+ 'python'
113
+ """
114
+
115
+ PYTHON = "python"
116
+ """Python format"""
117
+ GO = "go"
118
+ """Go format"""
119
+ JAVA = "java"
120
+ """Java format"""
121
+ BINARY = "binary"
122
+ """Binary format"""
123
+
124
+
125
+ class ManifestRuntime(str, Enum):
126
+ """
127
+ Runtime (environment) where the app will be run on Nextmv Cloud.
128
+
129
+ You can import the `ManifestRuntime` class directly from `nextmv`:
130
+
131
+ ```python
132
+ from nextmv import ManifestRuntime
133
+ ```
134
+
135
+ This enum defines the supported runtime environments for applications
136
+ that can be deployed on Nextmv Cloud.
137
+
138
+ Attributes
139
+ ----------
140
+ DEFAULT : str
141
+ This runtime is used to run compiled applications such as Go binaries.
142
+ PYTHON : str
143
+ This runtime is used as the basis for all other Python runtimes and
144
+ Python applications.
145
+ JAVA : str
146
+ This runtime is used to run Java applications.
147
+ PYOMO : str
148
+ This runtime provisions Python packages to run Pyomo applications.
149
+ HEXALY : str
150
+ This runtime provisions Python packages to run Hexaly applications.
151
+
152
+ Examples
153
+ --------
154
+ >>> from nextmv import ManifestRuntime
155
+ >>> runtime = ManifestRuntime.PYTHON
156
+ >>> runtime
157
+ <ManifestRuntime.PYTHON: 'ghcr.io/nextmv-io/runtime/python:3.11'>
158
+ >>> str(runtime)
159
+ 'ghcr.io/nextmv-io/runtime/python:3.11'
160
+ """
161
+
162
+ DEFAULT = "ghcr.io/nextmv-io/runtime/default:latest"
163
+ """A runtime used to run compiled applications such as Go binaries."""
164
+ PYTHON = "ghcr.io/nextmv-io/runtime/python:3.11"
165
+ """A runtime used to run standard Python applications."""
166
+ JAVA = "ghcr.io/nextmv-io/runtime/java:latest"
167
+ """A runtime used to run Java applications."""
168
+ PYOMO = "ghcr.io/nextmv-io/runtime/pyomo:latest"
169
+ """A runtime provisioning Python packages to run Pyomo applications."""
170
+ CUOPT = "ghcr.io/nextmv-io/runtime/cuopt:latest"
171
+ """A runtime providing the NVIDIA cuOpt solver."""
172
+ GAMSPY = "ghcr.io/nextmv-io/runtime/gamspy:latest"
173
+ """A runtime provisioning the GAMS Python API and supported solvers."""
174
+
175
+
176
+ class ManifestPythonArch(str, Enum):
177
+ """
178
+ Target architecture for bundling Python apps.
179
+
180
+ You can import the `ManifestPythonArch` class directly from `nextmv`:
181
+
182
+ ```python
183
+ from nextmv import ManifestPythonArch
184
+ ```
185
+
186
+ Attributes
187
+ ----------
188
+ ARM64 : str
189
+ ARM 64-bit architecture.
190
+ AMD64 : str
191
+ AMD 64-bit architecture.
192
+
193
+ Examples
194
+ --------
195
+ >>> from nextmv import ManifestPythonArch
196
+ >>> arch = ManifestPythonArch.ARM64
197
+ >>> arch
198
+ <ManifestPythonArch.ARM64: 'arm64'>
199
+ >>> str(arch)
200
+ 'arm64'
201
+ """
202
+
203
+ ARM64 = "arm64"
204
+ """ARM 64-bit architecture."""
205
+ AMD64 = "amd64"
206
+ """AMD 64-bit architecture."""
207
+
208
+
209
+ class ManifestBuild(BaseModel):
210
+ """
211
+ Build-specific attributes.
212
+
213
+ You can import the `ManifestBuild` class directly from `nextmv`:
214
+
215
+ ```python
216
+ from nextmv import ManifestBuild
217
+ ```
218
+
219
+ Parameters
220
+ ----------
221
+ command : Optional[str], default=None
222
+ The command to run to build the app. This command will be executed
223
+ without a shell, i.e., directly. The command must exit with a status of
224
+ 0 to continue the push process of the app to Nextmv Cloud. This command
225
+ is executed prior to the pre-push command.
226
+ environment : Optional[dict[str, Any]], default=None
227
+ Environment variables to set when running the build command given as
228
+ key-value pairs.
229
+
230
+ Examples
231
+ --------
232
+ >>> from nextmv import ManifestBuild
233
+ >>> build_config = ManifestBuild(
234
+ ... command="make build",
235
+ ... environment={"DEBUG": "true"}
236
+ ... )
237
+ >>> build_config.command
238
+ 'make build'
239
+ """
240
+
241
+ command: str | None = None
242
+ """The command to run to build the app.
243
+
244
+ This command will be executed without a shell, i.e., directly. The command
245
+ must exit with a status of 0 to continue the push process of the app to
246
+ Nextmv Cloud. This command is executed prior to the pre-push command.
247
+ """
248
+ environment: dict[str, Any] | None = None
249
+ """Environment variables to set when running the build command.
250
+
251
+ Given as key-value pairs.
252
+ """
253
+
254
+ def environment_to_dict(self) -> dict[str, str]:
255
+ """
256
+ Convert the environment variables to a dictionary.
257
+
258
+ Returns
259
+ -------
260
+ dict[str, str]
261
+ The environment variables as a dictionary of string key-value pairs.
262
+ Returns an empty dictionary if no environment variables are set.
263
+
264
+ Examples
265
+ --------
266
+ >>> from nextmv import ManifestBuild
267
+ >>> build_config = ManifestBuild(environment={"COUNT": 1, "NAME": "test"})
268
+ >>> build_config.environment_to_dict()
269
+ {'COUNT': '1', 'NAME': 'test'}
270
+ >>> build_config_empty = ManifestBuild()
271
+ >>> build_config_empty.environment_to_dict()
272
+ {}
273
+ """
274
+
275
+ if self.environment is None:
276
+ return {}
277
+
278
+ return {key: str(value) for key, value in self.environment.items()}
279
+
280
+
281
+ class ManifestPythonModel(BaseModel):
282
+ """
283
+ Model-specific instructions for a Python app.
284
+
285
+ You can import the `ManifestPythonModel` class directly from `nextmv`:
286
+
287
+ ```python
288
+ from nextmv import ManifestPythonModel
289
+ ```
290
+
291
+ Parameters
292
+ ----------
293
+ name : str
294
+ The name of the decision model.
295
+ options : Optional[list[dict[str, Any]]], default=None
296
+ Options for the decision model. This is a data representation of the
297
+ `nextmv.Options` class. It consists of a list of dicts. Each dict
298
+ represents the `nextmv.Option` class. It is used to be able to
299
+ reconstruct an Options object from data when loading a decision model.
300
+
301
+ Examples
302
+ --------
303
+ >>> from nextmv import ManifestPythonModel
304
+ >>> python_model_config = ManifestPythonModel(
305
+ ... name="routing_model",
306
+ ... options=[{"name": "max_vehicles", "type": "int", "default": 10}]
307
+ ... )
308
+ >>> python_model_config.name
309
+ 'routing_model'
310
+ """
311
+
312
+ name: str
313
+ """The name of the decision model."""
314
+ options: list[dict[str, Any]] | None = None
315
+ """
316
+ Options for the decision model. This is a data representation of the
317
+ `nextmv.Options` class. It consists of a list of dicts. Each dict
318
+ represents the `nextmv.Option` class. It is used to be able to
319
+ reconstruct an Options object from data when loading a decision model.
320
+ """
321
+
322
+
323
+ class ManifestPython(BaseModel):
324
+ """
325
+ Python-specific instructions.
326
+
327
+ You can import the `ManifestPython` class directly from `nextmv`:
328
+
329
+ ```python
330
+ from nextmv import ManifestPython
331
+ ```
332
+
333
+ Parameters
334
+ ----------
335
+ pip_requirements : Optional[Union[str, list[str]]], default=None
336
+ Path to a requirements.txt file containing (additional) Python
337
+ dependencies that will be bundled with the app. Alternatively, you can provide a
338
+ list of strings, each representing a package to install, e.g.,
339
+ `["nextmv==0.28.2", "ortools==9.12.4544"]`.
340
+ Aliases: `pip-requirements`.
341
+ model : Optional[ManifestPythonModel], default=None
342
+ Information about an encoded decision model as handled via mlflow. This
343
+ information is used to load the decision model from the app bundle.
344
+
345
+ Examples
346
+ --------
347
+ >>> from nextmv import ManifestPython, ManifestPythonModel
348
+ >>> python_config = ManifestPython(
349
+ ... pip_requirements="requirements.txt",
350
+ ... model=ManifestPythonModel(name="my_model")
351
+ ... )
352
+ >>> python_config.pip_requirements
353
+ 'requirements.txt'
354
+ """
355
+
356
+ pip_requirements: str | list[str] | None = Field(
357
+ serialization_alias="pip-requirements",
358
+ validation_alias=AliasChoices("pip-requirements", "pip_requirements"),
359
+ default=None,
360
+ )
361
+ """
362
+ Path to a requirements.txt file or list of packages.
363
+
364
+ Contains (additional) Python dependencies that will be bundled with the
365
+ app. Can be either a string path to a requirements.txt file or a list
366
+ of package specifications.
367
+ """
368
+ arch: ManifestPythonArch | None = None
369
+ """
370
+ The architecture this model is meant to run on. One of "arm64" or "amd64". Uses
371
+ "arm64" if not specified.
372
+ """
373
+ version: str | float | None = None
374
+ """
375
+ The Python version this model is meant to run with. Uses "3.11" if not specified.
376
+ """
377
+ model: ManifestPythonModel | None = None
378
+ """
379
+ Information about an encoded decision model.
380
+
381
+ As handled via mlflow. This information is used to load the decision model
382
+ from the app bundle.
383
+ """
384
+
385
+ @field_validator("version", mode="before")
386
+ @classmethod
387
+ def validate_version(cls, v: str | float | None) -> str | None:
388
+ """
389
+ Validate and convert the Python version field to a string.
390
+
391
+ This validator allows the version to be specified as either a float or string
392
+ in the manifest for convenience, but ensures it's stored internally as a string.
393
+
394
+ Parameters
395
+ ----------
396
+ v : Optional[Union[str, float]]
397
+ The version value to validate. Can be None, a string, or a float.
398
+
399
+ Returns
400
+ -------
401
+ Optional[str]
402
+ The version as a string, or None if the input was None.
403
+
404
+ Examples
405
+ --------
406
+ >>> ManifestPython.validate_version(3.11)
407
+ '3.11'
408
+ >>> ManifestPython.validate_version("3.11")
409
+ '3.11'
410
+ >>> ManifestPython.validate_version(None) is None
411
+ True
412
+ """
413
+ # We allow the version to be a float in the manifest for convenience, but we want
414
+ # to store it as a string internally.
415
+ if v is None:
416
+ return None
417
+ if isinstance(v, float):
418
+ return str(v)
419
+ return v
420
+
421
+
422
+ class ManifestOptionUI(BaseModel):
423
+ """
424
+ UI attributes for an option in the manifest.
425
+
426
+ You can import the `ManifestOptionUI` class directly from `nextmv`:
427
+
428
+ ```python
429
+ from nextmv import ManifestOptionUI
430
+ ```
431
+
432
+ Parameters
433
+ ----------
434
+ control_type : str, optional
435
+ The type of control to use for the option in the Nextmv Cloud UI. This is
436
+ useful for defining how the option should be presented in the Nextmv
437
+ Cloud UI. Current control types include "input", "select", "slider", and
438
+ "toggle". This attribute is not used in the local `Options` class, but
439
+ it is used in the Nextmv Cloud UI to define the type of control to use for
440
+ the option. This will be validated by the Nextmv Cloud, and availability
441
+ is based on option_type.
442
+ hidden_from : list[str], optional
443
+ A list of team roles to which this option will be hidden in the UI. For
444
+ example, if you want to hide an option from the "operator" role, you can
445
+ pass `hidden_from=["operator"]`.
446
+ display_name : str, optional
447
+ An optional display name for the option. This is useful for making
448
+ the option more user-friendly in the UI.
449
+
450
+ Examples
451
+ --------
452
+ >>> from nextmv import ManifestOptionUI
453
+ >>> ui_config = ManifestOptionUI(control_type="input")
454
+ >>> ui_config.control_type
455
+ 'input'
456
+ """
457
+
458
+ control_type: str | None = None
459
+ """The type of control to use for the option in the Nextmv Cloud UI."""
460
+ hidden_from: list[str] | None = None
461
+ """A list of team roles for which this option will be hidden in the UI."""
462
+ display_name: str | None = None
463
+ """An optional display name for the option. This is useful for making
464
+ the option more user-friendly in the UI.
465
+ """
466
+
467
+
468
+ class ManifestOption(BaseModel):
469
+ """
470
+ An option for the decision model that is recorded in the manifest.
471
+
472
+ You can import the `ManifestOption` class directly from `nextmv`:
473
+
474
+ ```python
475
+ from nextmv import ManifestOption
476
+ ```
477
+
478
+ Parameters
479
+ ----------
480
+ name : str
481
+ The name of the option.
482
+ option_type : str
483
+ The type of the option. This is a string representation of the
484
+ `nextmv.Option` class (e.g., "string", "int", "bool", "float").
485
+ Aliases: `type`.
486
+ default : Optional[Any], default=None
487
+ The default value of the option.
488
+ description : Optional[str], default=""
489
+ The description of the option.
490
+ required : bool, default=False
491
+ Whether the option is required or not.
492
+ additional_attributes : Optional[dict[str, Any]], default=None
493
+ Optional additional attributes for the option. The Nextmv Cloud may
494
+ perform validation on these attributes. For example, the maximum
495
+ length of a string or the maximum value of an integer. These
496
+ additional attributes will be shown in the help message of the
497
+ `Options`.
498
+ ui : Optional[ManifestOptionUI], default=None
499
+ Optional UI attributes for the option. This is a dictionary that can
500
+ contain additional information about how the option should be displayed
501
+ in the Nextmv Cloud UI. This is not used in the local `Options` class,
502
+ but it is used in the Nextmv Cloud UI to define how the option should be
503
+ presented.
504
+
505
+ Examples
506
+ --------
507
+ >>> from nextmv import ManifestOption
508
+ >>> option = ManifestOption(
509
+ ... name="solve.duration",
510
+ ... option_type="string",
511
+ ... default="30s",
512
+ ... description="Maximum duration for the solver."
513
+ ... )
514
+ >>> option.name
515
+ 'solve.duration'
516
+ """
517
+
518
+ name: str
519
+ """The name of the option"""
520
+ option_type: str = Field(
521
+ serialization_alias="option_type",
522
+ validation_alias=AliasChoices("type", "option_type"),
523
+ )
524
+ """The type of the option (e.g., "string", "int", "bool", "float)."""
525
+
526
+ default: Any | None = None
527
+ """The default value of the option"""
528
+ description: str | None = ""
529
+ """The description of the option"""
530
+ required: bool = False
531
+ """Whether the option is required or not"""
532
+ additional_attributes: dict[str, Any] | None = None
533
+ """Optional additional attributes for the option."""
534
+ ui: ManifestOptionUI | None = None
535
+ """Optional UI attributes for the option."""
536
+
537
+ @classmethod
538
+ def from_option(cls, option: Option) -> "ManifestOption":
539
+ """
540
+ Create a `ManifestOption` from an `Option`.
541
+
542
+ Parameters
543
+ ----------
544
+ option : nextmv.options.Option
545
+ The option to convert.
546
+
547
+ Returns
548
+ -------
549
+ ManifestOption
550
+ The converted option.
551
+
552
+ Raises
553
+ ------
554
+ ValueError
555
+ If the `option.option_type` is unknown.
556
+
557
+ Examples
558
+ --------
559
+ >>> from nextmv.options import Option
560
+ >>> from nextmv import ManifestOption
561
+ >>> sdk_option = Option(name="max_stops", option_type=int, default=100)
562
+ >>> manifest_opt = ManifestOption.from_option(sdk_option)
563
+ >>> manifest_opt.name
564
+ 'max_stops'
565
+ >>> manifest_opt.option_type
566
+ 'int'
567
+ """
568
+ option_type = option.option_type
569
+ if option_type is str:
570
+ option_type = "string"
571
+ elif option_type is bool:
572
+ option_type = "bool"
573
+ elif option_type is int:
574
+ option_type = "int"
575
+ elif option_type is float:
576
+ option_type = "float"
577
+ else:
578
+ raise ValueError(f"unknown option type: {option_type}")
579
+
580
+ return cls(
581
+ name=option.name,
582
+ option_type=option_type,
583
+ default=option.default,
584
+ description=option.description,
585
+ required=option.required,
586
+ additional_attributes=option.additional_attributes,
587
+ ui=ManifestOptionUI(
588
+ control_type=option.control_type,
589
+ hidden_from=option.hidden_from,
590
+ display_name=option.display_name,
591
+ )
592
+ if option.control_type or option.hidden_from or option.display_name
593
+ else None,
594
+ )
595
+
596
+ def to_option(self) -> Option:
597
+ """
598
+ Convert the `ManifestOption` to an `Option`.
599
+
600
+ Returns
601
+ -------
602
+ nextmv.options.Option
603
+ The converted option.
604
+
605
+ Raises
606
+ ------
607
+ ValueError
608
+ If the `self.option_type` is unknown.
609
+
610
+ Examples
611
+ --------
612
+ >>> from nextmv import ManifestOption
613
+ >>> manifest_opt = ManifestOption(name="max_stops", option_type="int", default=100)
614
+ >>> sdk_option = manifest_opt.to_option()
615
+ >>> sdk_option.name
616
+ 'max_stops'
617
+ >>> sdk_option.option_type
618
+ <class 'int'>
619
+ """
620
+
621
+ option_type_string = self.option_type
622
+ if option_type_string == "string":
623
+ option_type = str
624
+ elif option_type_string == "bool":
625
+ option_type = bool
626
+ elif option_type_string == "int":
627
+ option_type = int
628
+ elif option_type_string == "float":
629
+ option_type = float
630
+ else:
631
+ raise ValueError(f"unknown option type: {option_type_string}")
632
+
633
+ return Option(
634
+ name=self.name,
635
+ option_type=option_type,
636
+ default=self.default,
637
+ description=self.description,
638
+ required=self.required,
639
+ additional_attributes=self.additional_attributes,
640
+ control_type=self.ui.control_type if self.ui else None,
641
+ hidden_from=self.ui.hidden_from if self.ui else None,
642
+ display_name=self.ui.display_name if self.ui else None,
643
+ )
644
+
645
+
646
+ class ManifestValidation(BaseModel):
647
+ """
648
+ Validation rules for options in the manifest.
649
+
650
+ You can import the `ManifestValidation` class directly from `nextmv`:
651
+
652
+ ```python
653
+ from nextmv import ManifestValidation
654
+ ```
655
+
656
+ Parameters
657
+ ----------
658
+ enforce : str, default="none"
659
+ The enforcement level for the validation rules. This can be set to
660
+ "none" or "all". If set to "none", no validation will be performed
661
+ on the options prior to creating a run. If set to "all", all validation
662
+ rules will be enforced on the options, and runs will not be created
663
+ if any of the rules of the options are violated.
664
+
665
+ Examples
666
+ --------
667
+ >>> from nextmv import ManifestValidation
668
+ >>> validation = ManifestValidation(enforce="all")
669
+ >>> validation.enforce
670
+ 'all'
671
+ """
672
+
673
+ enforce: str = "none"
674
+ """The enforcement level for the validation rules.
675
+ This can be set to "none" or "all". If set to "none", no validation will
676
+ be performed on the options prior to creating a run. If set to "all", all
677
+ validation rules will be enforced on the options, and runs will not be
678
+ created if any of the rules of the options are violated.
679
+ """
680
+
681
+
682
+ class ManifestOptions(BaseModel):
683
+ """
684
+ Options for the decision model.
685
+
686
+ You can import the `ManifestOptions` class directly from `nextmv`:
687
+
688
+ ```python
689
+ from nextmv import ManifestOptions
690
+ ```
691
+
692
+ Parameters
693
+ ----------
694
+ strict : Optional[bool], default=False
695
+ If strict is set to `True`, only the listed options will be allowed.
696
+ items : Optional[list[ManifestOption]], default=None
697
+ The actual list of options for the decision model. An option
698
+ is a parameter that configures the decision model.
699
+ validation: Optional[ManifestValidation], default=None
700
+ Optional validation rules for all options.
701
+ format : Optional[list[str]], default=None
702
+ A list of strings that define how options are transformed into command
703
+ line arguments. Use `{{name}}` to refer to the option name and
704
+ `{{value}}` to refer to the option value.
705
+
706
+
707
+ Examples
708
+ --------
709
+ >>> from nextmv import ManifestOptions, ManifestOption
710
+ >>> options_config = ManifestOptions(
711
+ ... strict=True,
712
+ ... validation=ManifestValidation(enforce="all"),
713
+ ... items=[
714
+ ... ManifestOption(name="timeout", option_type="int", default=60),
715
+ ... ManifestOption(name="vehicle_capacity", option_type="float", default=100.0)
716
+ ... ]
717
+ ... )
718
+ >>> options_config.strict
719
+ True
720
+ >>> len(options_config.items)
721
+ 2
722
+ """
723
+
724
+ strict: bool | None = False
725
+ """If strict is set to `True`, only the listed options will be allowed."""
726
+ validation: ManifestValidation | None = None
727
+ """Optional validation rules for all options."""
728
+ items: list[ManifestOption] | None = None
729
+ """The actual list of options for the decision model.
730
+
731
+ An option is a parameter that configures the decision model.
732
+ """
733
+ format: list[str] | None = None
734
+ """A list of strings that define how options are transformed into command line arguments.
735
+
736
+ Use `{{name}}` to refer to the option name and `{{value}}` to refer to the option value.
737
+ For example, `["-{{name}}", "{{value}}"]` will transform an option named `max_vehicles`
738
+ with a value of `10` into the command line argument `-max_vehicles 10`.
739
+ """
740
+
741
+ @classmethod
742
+ def from_options(
743
+ cls,
744
+ options: Options,
745
+ validation: OptionsEnforcement = None,
746
+ format: list[str] | None = None,
747
+ ) -> "ManifestOptions":
748
+ """
749
+ Create a `ManifestOptions` from a `nextmv.Options`.
750
+
751
+ Parameters
752
+ ----------
753
+ options : nextmv.options.Options
754
+ The options to convert.
755
+ validation : Optional[OptionsEnforcement], default=None
756
+ Optional validation rules for the options. If provided, it will be
757
+ used to set the `validation` attribute of the `ManifestOptions`.
758
+ format : Optional[list[str]], default=None
759
+ A list of strings that define how options are transformed into
760
+ command line arguments. Use `{{name}}` to refer to the option name
761
+ and `{{value}}` to refer to the option value.
762
+
763
+ For example, `["-{{name}}", "{{value}}"]` will transform an option
764
+ named `max_vehicles` with a value of `10` into the command line
765
+ argument `-max_vehicles 10`.
766
+
767
+ Returns
768
+ -------
769
+ ManifestOptions
770
+ The converted options.
771
+
772
+ Examples
773
+ --------
774
+ >>> from nextmv.options import Options, Option
775
+ >>> from nextmv import ManifestOptions
776
+ >>> sdk_options = Options(Option("max_vehicles", int, 5))
777
+ >>> manifest_options = ManifestOptions.from_options(sdk_options)
778
+ >>> manifest_options.items[0].name
779
+ 'max_vehicles'
780
+ """
781
+
782
+ items = [ManifestOption.from_option(option) for option in options.options]
783
+ return cls(
784
+ strict=validation.strict if validation else False,
785
+ validation=ManifestValidation(enforce="all" if validation and validation.validation_enforce else "none"),
786
+ items=items,
787
+ format=format,
788
+ )
789
+
790
+
791
+ class ManifestContentMultiFileInput(BaseModel):
792
+ """
793
+ Configuration for multi-file content format input.
794
+
795
+ You can import the `ManifestContentMultiFileInput` class directly from `nextmv`:
796
+
797
+ ```python
798
+ from nextmv import ManifestContentMultiFileInput
799
+ ```
800
+
801
+ Parameters
802
+ ----------
803
+ path : str
804
+ The path to the input file or directory.
805
+
806
+
807
+ Examples
808
+ --------
809
+ >>> from nextmv import ManifestContentMultiFileInput
810
+ >>> input_config = ManifestContentMultiFileInput(path="data/input/")
811
+ >>> input_config.path
812
+ 'data/input/'
813
+ """
814
+
815
+ path: str
816
+ """The path to the input file or directory."""
817
+
818
+
819
+ class ManifestContentMultiFileOutput(BaseModel):
820
+ """
821
+ Configuration for multi-file content format output.
822
+
823
+ You can import the `ManifestContentMultiFileOutput` class directly from `nextmv`:
824
+
825
+ ```python
826
+ from nextmv import ManifestContentMultiFileOutput
827
+ ```
828
+
829
+ Parameters
830
+ ----------
831
+ statistics : Optional[str], default=""
832
+ The path to the statistics file.
833
+ assets : Optional[str], default=""
834
+ The path to the assets file.
835
+ solutions : Optional[str], default=""
836
+ The path to the solutions directory.
837
+
838
+ Examples
839
+ --------
840
+ >>> from nextmv import ManifestContentMultiFileOutput
841
+ >>> output_config = ManifestContentMultiFileOutput(
842
+ ... statistics="my-outputs/statistics.json",
843
+ ... assets="my-outputs/assets.json",
844
+ ... solutions="my-outputs/solutions/"
845
+ ... )
846
+ >>> output_config.statistics
847
+ 'my-outputs/statistics.json'
848
+ """
849
+
850
+ statistics: str | None = ""
851
+ """The path to the statistics file."""
852
+ assets: str | None = ""
853
+ """The path to the assets file."""
854
+ solutions: str | None = ""
855
+ """The path to the solutions directory."""
856
+
857
+
858
+ class ManifestContentMultiFile(BaseModel):
859
+ """
860
+ Configuration for multi-file content format.
861
+
862
+ You can import the `ManifestContentMultiFile` class directly from `nextmv`:
863
+
864
+ ```python
865
+ from nextmv import ManifestContentMultiFile
866
+ ```
867
+
868
+ Parameters
869
+ ----------
870
+ input : ManifestContentMultiFileInput
871
+ Configuration for multi-file content format input.
872
+ output : ManifestContentMultiFileOutput
873
+ Configuration for multi-file content format output.
874
+
875
+ Examples
876
+ --------
877
+ >>> from nextmv import ManifestContentMultiFile, ManifestContentMultiFileInput, ManifestContentMultiFileOutput
878
+ >>> multi_file_config = ManifestContentMultiFile(
879
+ ... input=ManifestContentMultiFileInput(path="data/input/"),
880
+ ... output=ManifestContentMultiFileOutput(
881
+ ... statistics="my-outputs/statistics.json",
882
+ ... assets="my-outputs/assets.json",
883
+ ... solutions="my-outputs/solutions/"
884
+ ... )
885
+ ... )
886
+ >>> multi_file_config.input.path
887
+ 'data/input/'
888
+
889
+ """
890
+
891
+ input: ManifestContentMultiFileInput
892
+ """Configuration for multi-file content format input."""
893
+ output: ManifestContentMultiFileOutput
894
+ """Configuration for multi-file content format output."""
895
+
896
+
897
+ class ManifestContent(BaseModel):
898
+ """
899
+ Content configuration for specifying how the app input/output is handled.
900
+
901
+ You can import the `ManifestContent` class directly from `nextmv`:
902
+
903
+ ```python
904
+ from nextmv import ManifestContent
905
+ ```
906
+
907
+ Parameters
908
+ ----------
909
+ format : str
910
+ The format of the content. Must be one of "json", "multi-file", or "csv-archive".
911
+ multi_file : Optional[ManifestContentMultiFile], default=None
912
+ Configuration for multi-file content format.
913
+
914
+ Examples
915
+ --------
916
+ >>> from nextmv import ManifestContent
917
+ >>> content_config = ManifestContent(
918
+ ... format="multi-file",
919
+ ... multi_file=ManifestContentMultiFile(
920
+ ... input=ManifestContentMultiFileInput(path="data/input/"),
921
+ ... output=ManifestContentMultiFileOutput(
922
+ ... statistics="my-outputs/statistics.json",
923
+ ... assets="my-outputs/assets.json",
924
+ ... solutions="my-outputs/solutions/"
925
+ ... )
926
+ ... )
927
+ ... )
928
+ >>> content_config.format
929
+ 'multi-file'
930
+ >>> content_config.multi_file.input.path
931
+ 'data/input/'
932
+ """
933
+
934
+ format: InputFormat
935
+ """
936
+ The format of the content. Can only be `InputFormat.JSON`,
937
+ `InputFormat.MULTI_FILE`, or `InputFormat.CSV_ARCHIVE`.
938
+ """
939
+ multi_file: ManifestContentMultiFile | None = Field(
940
+ serialization_alias="multi-file",
941
+ validation_alias=AliasChoices("multi-file", "multi_file"),
942
+ default=None,
943
+ )
944
+ """Configuration for multi-file content format."""
945
+
946
+ def model_post_init(self, __context) -> None:
947
+ """
948
+ Post-initialization validation to ensure format field contains valid values.
949
+
950
+ This method is automatically called by Pydantic after the model is initialized
951
+ to validate that the format field contains one of the acceptable values.
952
+
953
+ Parameters
954
+ ----------
955
+ __context : Any
956
+ Pydantic context (unused in this implementation).
957
+
958
+ Raises
959
+ ------
960
+ ValueError
961
+ If the format field contains an invalid value that is not one of the
962
+ acceptable formats (JSON, MULTI_FILE, or CSV_ARCHIVE).
963
+ """
964
+ acceptable_formats = [InputFormat.JSON, InputFormat.MULTI_FILE, InputFormat.CSV_ARCHIVE]
965
+ if self.format not in acceptable_formats:
966
+ raise ValueError(f"Invalid format: {self.format}. Must be one of {acceptable_formats}.")
967
+
968
+
969
+ class ManifestConfiguration(BaseModel):
970
+ """
971
+ Configuration for the decision model.
972
+
973
+ You can import the `ManifestConfiguration` class directly from `nextmv`:
974
+
975
+ ```python
976
+ from nextmv import ManifestConfiguration
977
+ ```
978
+
979
+ Parameters
980
+ ----------
981
+ options : Optional[ManifestOptions], default=None
982
+ Options for the decision model.
983
+ content : Optional[ManifestContent], default=None
984
+ Content configuration for specifying how the app input/output is handled.
985
+
986
+ Examples
987
+ --------
988
+ >>> from nextmv import ManifestConfiguration, ManifestOptions, ManifestOption
989
+ >>> model_config = ManifestConfiguration(
990
+ ... options=ManifestOptions(
991
+ ... items=[ManifestOption(name="debug_mode", option_type="bool", default=False)]
992
+ ... )
993
+ ... )
994
+ >>> model_config.options.items[0].name
995
+ 'debug_mode'
996
+ """
997
+
998
+ options: ManifestOptions | None = None
999
+ """Options for the decision model."""
1000
+ content: ManifestContent | None = None
1001
+ """Content configuration for specifying how the app input/output is handled."""
1002
+
1003
+
1004
+ class ManifestExecution(BaseModel):
1005
+ """
1006
+ Execution configuration for the decision model.
1007
+
1008
+ You can import the `ManifestExecution` class directly from `nextmv`:
1009
+
1010
+ ```python
1011
+ from nextmv import ManifestExecution
1012
+ ```
1013
+
1014
+ Parameters
1015
+ ----------
1016
+ entrypoint : Optional[str], default=None
1017
+ The entrypoint for the decision model, e.g.: `./app.py`.
1018
+ cwd : Optional[str], default=None
1019
+ The working directory to set when running the app, e.g.: `./src/`.
1020
+
1021
+ Examples
1022
+ --------
1023
+ >>> from nextmv import ManifestExecution
1024
+ >>> exec_config = ManifestExecution(
1025
+ ... entrypoint="./app.py",
1026
+ ... cwd="./src/"
1027
+ ... )
1028
+ >>> exec_config.entrypoint
1029
+ './app.py'
1030
+ """
1031
+
1032
+ entrypoint: str | None = None
1033
+ """The entrypoint for the decision model, e.g.: `./app.py`."""
1034
+ cwd: str | None = None
1035
+ """The working directory to set when running the app, e.g.: `./src/`."""
1036
+
1037
+
1038
+ class Manifest(BaseModel):
1039
+ """
1040
+ Represents an app manifest (`app.yaml`) for Nextmv Cloud.
1041
+
1042
+ You can import the `Manifest` class directly from `nextmv`:
1043
+
1044
+ ```python
1045
+ from nextmv import Manifest
1046
+ ```
1047
+
1048
+ An application that runs on the Nextmv Platform must contain a file named
1049
+ `app.yaml` which is known as the app manifest. This file is used to specify
1050
+ the execution environment for the app.
1051
+
1052
+ This class represents the app manifest and allows you to load it from a
1053
+ file or create it programmatically.
1054
+
1055
+ Parameters
1056
+ ----------
1057
+ files : list[str]
1058
+ The files to include (or exclude) in the app. This is mandatory.
1059
+ runtime : ManifestRuntime, default=ManifestRuntime.PYTHON
1060
+ The runtime to use for the app, it provides the environment
1061
+ in which the app runs. This is mandatory.
1062
+ type : ManifestType, default=ManifestType.PYTHON
1063
+ Type of application, based on the programming language. This is
1064
+ mandatory.
1065
+ build : Optional[ManifestBuild], default=None
1066
+ Build-specific attributes. The `build.command` to run to build
1067
+ the app. This command will be executed without a shell, i.e., directly.
1068
+ The command must exit with a status of 0 to continue the push process of
1069
+ the app to Nextmv Cloud. This command is executed prior to the pre-push
1070
+ command. The `build.environment` is used to set environment variables when
1071
+ running the build command given as key-value pairs.
1072
+ pre_push : Optional[str], default=None
1073
+ A command to run before the app is pushed to the Nextmv Cloud.
1074
+ This command can be used to compile a binary, run tests or similar tasks.
1075
+ One difference with what is specified under build, is that the command
1076
+ will be executed via the shell (i.e., `bash -c` on Linux & macOS or
1077
+ `cmd /c` on Windows). The command must exit with a status of 0 to
1078
+ continue the push process. This command is executed just before the app
1079
+ gets bundled and pushed (after the build command).
1080
+ Aliases: `pre-push`.
1081
+ python : Optional[ManifestPython], default=None
1082
+ Only for Python apps. Contains further Python-specific
1083
+ attributes.
1084
+ configuration : Optional[ManifestConfiguration], default=None
1085
+ A list of options for the decision model. An option is a
1086
+ parameter that configures the decision model.
1087
+ entrypoint : Optional[str], default=None
1088
+ Optional entrypoint for the decision model. When not specified, the
1089
+ following default entrypoints are used, according to the `.runtime`:
1090
+ - `ManifestRuntime.PYTHON`, `ManifestRuntime.HEXALY`, `ManifestRuntime.PYOMO`: `./main.py`
1091
+ - `ManifestRuntime.DEFAULT`: `./main`
1092
+ - Java: `./main.jar`
1093
+
1094
+ Examples
1095
+ --------
1096
+ >>> from nextmv import Manifest, ManifestRuntime, ManifestType
1097
+ >>> manifest = Manifest(
1098
+ ... files=["main.py", "model_logic/"],
1099
+ ... runtime=ManifestRuntime.PYTHON,
1100
+ ... type=ManifestType.PYTHON,
1101
+ ... )
1102
+ >>> manifest.files
1103
+ ['main.py', 'model_logic/']
1104
+ """
1105
+
1106
+ type: ManifestType = ManifestType.PYTHON
1107
+ """
1108
+ Type of application, based on the programming language. This is mandatory.
1109
+ """
1110
+ runtime: ManifestRuntime = ManifestRuntime.PYTHON
1111
+ """
1112
+ The runtime to use for the app. It provides the environment in which the
1113
+ app runs. This is mandatory.
1114
+ """
1115
+ python: ManifestPython | None = None
1116
+ """
1117
+ Python-specific attributes. Only for Python apps. Contains further
1118
+ Python-specific attributes.
1119
+ """
1120
+ files: list[str] = Field(
1121
+ default_factory=list,
1122
+ )
1123
+ """The files to include (or exclude) in the app. This is mandatory."""
1124
+ configuration: ManifestConfiguration | None = None
1125
+ """
1126
+ Configuration for the decision model. A list of options for the decision
1127
+ model. An option is a parameter that configures the decision model.
1128
+ """
1129
+ build: ManifestBuild | None = None
1130
+ """
1131
+ Build-specific attributes.
1132
+
1133
+ The `build.command` to run to build the app. This command will be executed
1134
+ without a shell, i.e., directly. The command must exit with a status of 0
1135
+ to continue the push process of the app to Nextmv Cloud. This command is
1136
+ executed prior to the pre-push command. The `build.environment` is used to
1137
+ set environment variables when running the build command given as key-value
1138
+ pairs.
1139
+ """
1140
+ pre_push: str | None = Field(
1141
+ serialization_alias="pre-push",
1142
+ validation_alias=AliasChoices("pre-push", "pre_push"),
1143
+ default=None,
1144
+ )
1145
+ """
1146
+ A command to run before the app is pushed to the Nextmv Cloud.
1147
+
1148
+ This command can be used to compile a binary, run tests or similar tasks.
1149
+ One difference with what is specified under build, is that the command will
1150
+ be executed via the shell (i.e., `bash -c` on Linux & macOS or `cmd /c` on
1151
+ Windows). The command must exit with a status of 0 to continue the push
1152
+ process. This command is executed just before the app gets bundled and
1153
+ pushed (after the build command).
1154
+ """
1155
+ execution: ManifestExecution | None = None
1156
+ """
1157
+ Optional execution configuration for the decision model. Allows configuration of
1158
+ entrypoint and more.
1159
+ """
1160
+
1161
+ @classmethod
1162
+ def from_yaml(cls, dirpath: str) -> "Manifest":
1163
+ """
1164
+ Load a manifest from a YAML file.
1165
+
1166
+ The YAML file is expected to be named `app.yaml` and located in the
1167
+ specified directory.
1168
+
1169
+ Parameters
1170
+ ----------
1171
+ dirpath : str
1172
+ Path to the directory containing the `app.yaml` file.
1173
+
1174
+ Returns
1175
+ -------
1176
+ Manifest
1177
+ The loaded manifest.
1178
+
1179
+ Raises
1180
+ ------
1181
+ FileNotFoundError
1182
+ If the `app.yaml` file is not found in `dirpath`.
1183
+ yaml.YAMLError
1184
+ If there is an error parsing the YAML file.
1185
+
1186
+ Examples
1187
+ --------
1188
+ Assuming an `app.yaml` file exists in `./my_app_dir`:
1189
+
1190
+ ```yaml
1191
+ # ./my_app_dir/app.yaml
1192
+ files:
1193
+ - main.py
1194
+ runtime: ghcr.io/nextmv-io/runtime/python:3.11
1195
+ type: python
1196
+ ```
1197
+
1198
+ >>> from nextmv import Manifest
1199
+ >>> # manifest = Manifest.from_yaml("./my_app_dir") # This would be run
1200
+ >>> # assert manifest.type == "python"
1201
+ """
1202
+
1203
+ with open(os.path.join(dirpath, MANIFEST_FILE_NAME)) as file:
1204
+ raw_manifest = yaml.safe_load(file)
1205
+
1206
+ return cls.from_dict(raw_manifest)
1207
+
1208
+ def to_yaml(self, dirpath: str) -> None:
1209
+ """
1210
+ Write the manifest to a YAML file.
1211
+
1212
+ The manifest will be written to a file named `app.yaml` in the
1213
+ specified directory.
1214
+
1215
+ Parameters
1216
+ ----------
1217
+ dirpath : str
1218
+ Path to the directory where the `app.yaml` file will be written.
1219
+
1220
+ Raises
1221
+ ------
1222
+ IOError
1223
+ If there is an error writing the file.
1224
+ yaml.YAMLError
1225
+ If there is an error serializing the manifest to YAML.
1226
+
1227
+ Examples
1228
+ --------
1229
+ >>> from nextmv import Manifest
1230
+ >>> manifest = Manifest(files=["solver.py"], type="python")
1231
+ >>> # manifest.to_yaml("./output_dir") # This would create ./output_dir/app.yaml
1232
+ """
1233
+
1234
+ with open(os.path.join(dirpath, MANIFEST_FILE_NAME), "w") as file:
1235
+ yaml.dump(
1236
+ self.to_dict(),
1237
+ file,
1238
+ sort_keys=False,
1239
+ default_flow_style=False,
1240
+ indent=2,
1241
+ width=120,
1242
+ )
1243
+
1244
+ def extract_options(self, should_parse: bool = True) -> Options | None:
1245
+ """
1246
+ Convert the manifest options to a `nextmv.Options` object.
1247
+
1248
+ If the manifest does not have valid options defined in
1249
+ `.configuration.options.items`, this method returns `None`.
1250
+
1251
+ Use the `should_parse` argument to decide if you want the options
1252
+ parsed, or not. For more information on option parsing, please read the
1253
+ docstrings on the `.parse()` method of the `nextmv.Options` object.
1254
+
1255
+ Parameters
1256
+ ----------
1257
+ should_parse : bool, default=True
1258
+ Whether to parse the options, or not. By default, options are
1259
+ parsed. When command-line arguments are parsed, the help menu is
1260
+ created, thus parsing Options more than once may result in
1261
+ unexpected behavior.
1262
+
1263
+ Returns
1264
+ -------
1265
+ Optional[nextmv.options.Options]
1266
+ The options extracted from the manifest. If no options are found,
1267
+ `None` is returned.
1268
+
1269
+ Examples
1270
+ --------
1271
+ >>> from nextmv import Manifest, ManifestConfiguration, ManifestOptions, ManifestOption
1272
+ >>> manifest = Manifest(
1273
+ ... files=["main.py"],
1274
+ ... configuration=ManifestConfiguration(
1275
+ ... options=ManifestOptions(
1276
+ ... items=[
1277
+ ... ManifestOption(name="duration", option_type="string", default="10s")
1278
+ ... ]
1279
+ ... )
1280
+ ... )
1281
+ ... )
1282
+ >>> sdk_options = manifest.extract_options()
1283
+ >>> sdk_options.get_option("duration").default
1284
+ '10s'
1285
+ >>> empty_manifest = Manifest(files=["main.py"])
1286
+ >>> empty_manifest.extract_options() is None
1287
+ True
1288
+ """
1289
+
1290
+ if self.configuration is None or self.configuration.options is None or self.configuration.options.items is None:
1291
+ return None
1292
+
1293
+ options = [option.to_option() for option in self.configuration.options.items]
1294
+
1295
+ opt = Options(*options)
1296
+ if should_parse:
1297
+ opt.parse()
1298
+
1299
+ return opt
1300
+
1301
+ @classmethod
1302
+ def from_model_configuration(
1303
+ cls,
1304
+ model_configuration: ModelConfiguration,
1305
+ ) -> "Manifest":
1306
+ """
1307
+ Create a Python manifest from a `nextmv.model.ModelConfiguration`.
1308
+
1309
+ Note that the `ModelConfiguration` is almost always used in
1310
+ conjunction with the `nextmv.Model` class. If you are not
1311
+ implementing an instance of `nextmv.Model`, consider using the
1312
+ `from_options` method instead to initialize the manifest with the
1313
+ options of the model.
1314
+
1315
+ The resulting manifest will have:
1316
+
1317
+ - `files` set to `["main.py", f"{model_configuration.name}/**"]`
1318
+ - `runtime` set to `ManifestRuntime.PYTHON`
1319
+ - `type` set to `ManifestType.PYTHON`
1320
+ - `python.pip_requirements` set to the default requirements file name.
1321
+ - `python.model.name` set to `model_configuration.name`.
1322
+ - `python.model.options` populated from `model_configuration.options`.
1323
+ - `configuration.options` populated from `model_configuration.options`.
1324
+
1325
+ Parameters
1326
+ ----------
1327
+ model_configuration : nextmv.model.ModelConfiguration
1328
+ The model configuration.
1329
+
1330
+ Returns
1331
+ -------
1332
+ Manifest
1333
+ The Python manifest.
1334
+
1335
+ Examples
1336
+ --------
1337
+ >>> from nextmv.model import ModelConfiguration
1338
+ >>> from nextmv.options import Options, Option
1339
+ >>> from nextmv import Manifest
1340
+ >>> opts = Options(Option(name="vehicle_count", option_type=int, default=5))
1341
+ >>> mc = ModelConfiguration(name="vehicle_router", options=opts)
1342
+ >>> manifest = Manifest.from_model_configuration(mc)
1343
+ >>> manifest.python.model.name
1344
+ 'vehicle_router'
1345
+ >>> manifest.files
1346
+ ['main.py', 'vehicle_router/**']
1347
+ >>> manifest.configuration.options.items[0].name
1348
+ 'vehicle_count'
1349
+ """
1350
+
1351
+ manifest_python_dict = {
1352
+ "pip-requirements": _REQUIREMENTS_FILE,
1353
+ "model": {
1354
+ "name": model_configuration.name,
1355
+ },
1356
+ }
1357
+
1358
+ if model_configuration.options is not None:
1359
+ manifest_python_dict["model"]["options"] = model_configuration.options.options_dict()
1360
+
1361
+ manifest_python = ManifestPython.from_dict(manifest_python_dict)
1362
+ manifest = cls(
1363
+ files=["main.py", f"{model_configuration.name}/**"],
1364
+ runtime=ManifestRuntime.PYTHON,
1365
+ type=ManifestType.PYTHON,
1366
+ python=manifest_python,
1367
+ )
1368
+
1369
+ if model_configuration.options is not None:
1370
+ manifest.configuration = ManifestConfiguration(
1371
+ options=ManifestOptions.from_options(
1372
+ options=model_configuration.options,
1373
+ validation=model_configuration.options_enforcement,
1374
+ ),
1375
+ )
1376
+
1377
+ return manifest
1378
+
1379
+ @classmethod
1380
+ def from_options(cls, options: Options, validation: OptionsEnforcement = None) -> "Manifest":
1381
+ """
1382
+ Create a basic Python manifest from `nextmv.options.Options`.
1383
+
1384
+ If you have more files than just a `main.py`, make sure you modify
1385
+ the `.files` attribute of the resulting manifest. This method assumes
1386
+ that requirements are specified in a `requirements.txt` file. You may
1387
+ also specify a different requirements file once you instantiate the
1388
+ manifest.
1389
+
1390
+ The resulting manifest will have:
1391
+ - `files` set to `["main.py"]`
1392
+ - `runtime` set to `ManifestRuntime.PYTHON`
1393
+ - `type` set to `ManifestType.PYTHON`
1394
+ - `python.pip_requirements` set to `"requirements.txt"`.
1395
+ - `configuration.options` populated from the provided `options`.
1396
+
1397
+ Parameters
1398
+ ----------
1399
+ options : nextmv.options.Options
1400
+ The options to include in the manifest.
1401
+ validation : nextmv.options.OptionsEnforcement, default=None
1402
+ The validation rules for the options. This is used to set the
1403
+ `validation` attribute of the `ManifestOptions`.
1404
+
1405
+ Returns
1406
+ -------
1407
+ Manifest
1408
+ The manifest with the given options.
1409
+
1410
+ Examples
1411
+ --------
1412
+ >>> from nextmv.options import Options, Option
1413
+ >>> from nextmv import Manifest
1414
+ >>> opts = Options(
1415
+ ... Option(name="max_runtime", option_type=str, default="60s"),
1416
+ ... Option(name="use_heuristic", option_type=bool, default=True)
1417
+ ... )
1418
+ >>> manifest = Manifest.from_options(opts)
1419
+ >>> manifest.files
1420
+ ['main.py']
1421
+ >>> manifest.python.pip_requirements
1422
+ 'requirements.txt'
1423
+ >>> len(manifest.configuration.options.items)
1424
+ 2
1425
+ >>> manifest.configuration.options.items[0].name
1426
+ 'max_runtime'
1427
+ """
1428
+
1429
+ manifest = cls(
1430
+ files=["main.py"],
1431
+ runtime=ManifestRuntime.PYTHON,
1432
+ type=ManifestType.PYTHON,
1433
+ python=ManifestPython(pip_requirements="requirements.txt"),
1434
+ configuration=ManifestConfiguration(
1435
+ options=ManifestOptions.from_options(options=options, validation=validation),
1436
+ ),
1437
+ )
1438
+
1439
+ return manifest
1440
+
1441
+
1442
+ def default_python_manifest() -> Manifest:
1443
+ """
1444
+ Creates a default Python manifest as a starting point for applications
1445
+ being executed on the Nextmv Platform.
1446
+
1447
+ You can import the `default_python_manifest` function directly from `nextmv`:
1448
+
1449
+ ```python
1450
+ from nextmv import default_python_manifest
1451
+ ```
1452
+
1453
+ Returns
1454
+ -------
1455
+ Manifest
1456
+ A default Python manifest with common settings.
1457
+ """
1458
+
1459
+ m = Manifest(
1460
+ files=["main.py"],
1461
+ runtime=ManifestRuntime.PYTHON,
1462
+ type=ManifestType.PYTHON,
1463
+ python=ManifestPython(pip_requirements="requirements.txt"),
1464
+ )
1465
+
1466
+ return m