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