nextmv 1.0.0__py3-none-any.whl → 1.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +2 -1
  3. nextmv/__init__.py +4 -0
  4. nextmv/cli/CONTRIBUTING.md +40 -112
  5. nextmv/cli/cloud/__init__.py +0 -4
  6. nextmv/cli/cloud/acceptance/create.py +22 -20
  7. nextmv/cli/cloud/acceptance/delete.py +12 -8
  8. nextmv/cli/cloud/acceptance/get.py +10 -9
  9. nextmv/cli/cloud/acceptance/list.py +3 -3
  10. nextmv/cli/cloud/acceptance/update.py +6 -6
  11. nextmv/cli/cloud/account/__init__.py +3 -3
  12. nextmv/cli/cloud/account/create.py +11 -11
  13. nextmv/cli/cloud/account/delete.py +8 -7
  14. nextmv/cli/cloud/account/get.py +3 -3
  15. nextmv/cli/cloud/account/update.py +5 -5
  16. nextmv/cli/cloud/app/create.py +26 -25
  17. nextmv/cli/cloud/app/delete.py +7 -6
  18. nextmv/cli/cloud/app/exists.py +2 -2
  19. nextmv/cli/cloud/app/get.py +2 -2
  20. nextmv/cli/cloud/app/list.py +3 -3
  21. nextmv/cli/cloud/app/push.py +54 -349
  22. nextmv/cli/cloud/app/update.py +12 -12
  23. nextmv/cli/cloud/batch/create.py +28 -26
  24. nextmv/cli/cloud/batch/delete.py +10 -6
  25. nextmv/cli/cloud/batch/get.py +9 -9
  26. nextmv/cli/cloud/batch/list.py +3 -3
  27. nextmv/cli/cloud/batch/metadata.py +4 -4
  28. nextmv/cli/cloud/batch/update.py +6 -6
  29. nextmv/cli/cloud/data/__init__.py +1 -1
  30. nextmv/cli/cloud/data/upload.py +15 -15
  31. nextmv/cli/cloud/ensemble/__init__.py +0 -2
  32. nextmv/cli/cloud/ensemble/create.py +22 -21
  33. nextmv/cli/cloud/ensemble/delete.py +10 -6
  34. nextmv/cli/cloud/ensemble/get.py +4 -4
  35. nextmv/cli/cloud/ensemble/update.py +9 -9
  36. nextmv/cli/cloud/input_set/__init__.py +0 -2
  37. nextmv/cli/cloud/input_set/create.py +22 -22
  38. nextmv/cli/cloud/input_set/get.py +3 -3
  39. nextmv/cli/cloud/input_set/list.py +3 -3
  40. nextmv/cli/cloud/input_set/update.py +24 -24
  41. nextmv/cli/cloud/instance/create.py +15 -14
  42. nextmv/cli/cloud/instance/delete.py +7 -6
  43. nextmv/cli/cloud/instance/exists.py +2 -2
  44. nextmv/cli/cloud/instance/get.py +2 -2
  45. nextmv/cli/cloud/instance/list.py +3 -3
  46. nextmv/cli/cloud/instance/update.py +14 -14
  47. nextmv/cli/cloud/managed_input/create.py +16 -14
  48. nextmv/cli/cloud/managed_input/delete.py +8 -7
  49. nextmv/cli/cloud/managed_input/get.py +3 -3
  50. nextmv/cli/cloud/managed_input/list.py +3 -3
  51. nextmv/cli/cloud/managed_input/update.py +9 -9
  52. nextmv/cli/cloud/run/cancel.py +2 -2
  53. nextmv/cli/cloud/run/create.py +40 -34
  54. nextmv/cli/cloud/run/get.py +8 -8
  55. nextmv/cli/cloud/run/input.py +4 -4
  56. nextmv/cli/cloud/run/list.py +6 -6
  57. nextmv/cli/cloud/run/logs.py +10 -9
  58. nextmv/cli/cloud/run/metadata.py +4 -4
  59. nextmv/cli/cloud/run/track.py +33 -32
  60. nextmv/cli/cloud/scenario/create.py +21 -21
  61. nextmv/cli/cloud/scenario/delete.py +10 -6
  62. nextmv/cli/cloud/scenario/get.py +9 -9
  63. nextmv/cli/cloud/scenario/list.py +3 -3
  64. nextmv/cli/cloud/scenario/metadata.py +4 -4
  65. nextmv/cli/cloud/scenario/update.py +6 -6
  66. nextmv/cli/cloud/secrets/create.py +17 -17
  67. nextmv/cli/cloud/secrets/delete.py +10 -6
  68. nextmv/cli/cloud/secrets/get.py +4 -4
  69. nextmv/cli/cloud/secrets/list.py +3 -3
  70. nextmv/cli/cloud/secrets/update.py +20 -17
  71. nextmv/cli/cloud/upload/create.py +2 -2
  72. nextmv/cli/cloud/version/create.py +10 -9
  73. nextmv/cli/cloud/version/delete.py +7 -6
  74. nextmv/cli/cloud/version/exists.py +2 -2
  75. nextmv/cli/cloud/version/get.py +2 -2
  76. nextmv/cli/cloud/version/list.py +3 -3
  77. nextmv/cli/cloud/version/update.py +8 -8
  78. nextmv/cli/community/__init__.py +1 -1
  79. nextmv/cli/community/clone.py +204 -20
  80. nextmv/cli/community/list.py +125 -60
  81. nextmv/cli/configuration/config.py +10 -43
  82. nextmv/cli/configuration/create.py +7 -7
  83. nextmv/cli/configuration/delete.py +8 -8
  84. nextmv/cli/configuration/list.py +3 -3
  85. nextmv/cli/main.py +36 -26
  86. nextmv/cli/message.py +54 -71
  87. nextmv/cli/options.py +0 -28
  88. nextmv/cli/version.py +1 -1
  89. nextmv/cloud/__init__.py +38 -14
  90. nextmv/cloud/acceptance_test.py +65 -1
  91. nextmv/cloud/account.py +6 -1
  92. nextmv/cloud/application/__init__.py +75 -18
  93. nextmv/cloud/application/_acceptance.py +8 -13
  94. nextmv/cloud/application/_batch_scenario.py +19 -4
  95. nextmv/cloud/application/_input_set.py +6 -42
  96. nextmv/cloud/application/_instance.py +3 -3
  97. nextmv/cloud/application/_managed_input.py +2 -2
  98. nextmv/cloud/application/_version.py +3 -4
  99. nextmv/cloud/batch_experiment.py +1 -3
  100. nextmv/cloud/integration.py +4 -7
  101. nextmv/deprecated.py +3 -5
  102. nextmv/input.py +52 -0
  103. nextmv/local/runner.py +1 -1
  104. nextmv/model.py +11 -50
  105. nextmv/options.py +256 -11
  106. nextmv/output.py +62 -0
  107. nextmv/run.py +10 -1
  108. nextmv/status.py +51 -1
  109. {nextmv-1.0.0.dist-info → nextmv-1.0.0.dev0.dist-info}/METADATA +4 -5
  110. nextmv-1.0.0.dev0.dist-info/RECORD +158 -0
  111. nextmv/cli/cloud/ensemble/list.py +0 -63
  112. nextmv/cli/cloud/input_set/delete.py +0 -64
  113. nextmv/cli/cloud/shadow/__init__.py +0 -33
  114. nextmv/cli/cloud/shadow/create.py +0 -184
  115. nextmv/cli/cloud/shadow/delete.py +0 -64
  116. nextmv/cli/cloud/shadow/get.py +0 -61
  117. nextmv/cli/cloud/shadow/list.py +0 -63
  118. nextmv/cli/cloud/shadow/metadata.py +0 -66
  119. nextmv/cli/cloud/shadow/start.py +0 -43
  120. nextmv/cli/cloud/shadow/stop.py +0 -53
  121. nextmv/cli/cloud/shadow/update.py +0 -96
  122. nextmv/cli/cloud/switchback/__init__.py +0 -33
  123. nextmv/cli/cloud/switchback/create.py +0 -151
  124. nextmv/cli/cloud/switchback/delete.py +0 -64
  125. nextmv/cli/cloud/switchback/get.py +0 -62
  126. nextmv/cli/cloud/switchback/list.py +0 -63
  127. nextmv/cli/cloud/switchback/metadata.py +0 -68
  128. nextmv/cli/cloud/switchback/start.py +0 -43
  129. nextmv/cli/cloud/switchback/stop.py +0 -53
  130. nextmv/cli/cloud/switchback/update.py +0 -96
  131. nextmv/cli/confirm.py +0 -34
  132. nextmv/cloud/application/_shadow.py +0 -320
  133. nextmv/cloud/application/_switchback.py +0 -332
  134. nextmv/cloud/community.py +0 -446
  135. nextmv/cloud/shadow.py +0 -254
  136. nextmv/cloud/switchback.py +0 -228
  137. nextmv-1.0.0.dist-info/RECORD +0 -185
  138. nextmv-1.0.0.dist-info/entry_points.txt +0 -2
  139. {nextmv-1.0.0.dist-info → nextmv-1.0.0.dev0.dist-info}/WHEEL +0 -0
  140. {nextmv-1.0.0.dist-info → nextmv-1.0.0.dev0.dist-info}/licenses/LICENSE +0 -0
nextmv/model.py CHANGED
@@ -18,8 +18,6 @@ deployed to Nextmv Cloud for execution.
18
18
  import logging
19
19
  import os
20
20
  import shutil
21
- import sqlite3
22
- import time
23
21
  import warnings
24
22
  from dataclasses import dataclass
25
23
  from typing import Any
@@ -86,9 +84,6 @@ def _custom_showwarning(message, category, filename, lineno, file=None, line=Non
86
84
  if "mlflow/pyfunc/__init__.py" in filename:
87
85
  return
88
86
 
89
- if "/mlflow/tracing/provider.py" in filename:
90
- return
91
-
92
87
  _original_showwarning(message, category, filename, lineno, file, line)
93
88
 
94
89
 
@@ -293,6 +288,7 @@ class Model:
293
288
  ) from e
294
289
 
295
290
  finally:
291
+ from mlflow.models import infer_signature
296
292
  from mlflow.pyfunc import PythonModel, save_model
297
293
 
298
294
  class MLFlowModel(PythonModel):
@@ -346,16 +342,12 @@ class Model:
346
342
 
347
343
  _cleanup_python_model(model_dir, configuration, verbose=False)
348
344
 
349
- # Removing this seems to make the "apps from models" experience once
350
- # again. I am not removing it entirely because we might want to
351
- # re-introduce it later on.
352
- # signature = None
353
- # if configuration.options is not None:
354
- # options_dict = configuration.options.to_dict()
355
- # signature = infer_signature(
356
- # model_input={},
357
- # params=options_dict,
358
- # )
345
+ signature = None
346
+ if configuration.options is not None:
347
+ options_dict = configuration.options.to_dict()
348
+ signature = infer_signature(
349
+ params=options_dict,
350
+ )
359
351
 
360
352
  # We use mlflow to save the model to the local filesystem, to be able to
361
353
  # load it later on.
@@ -364,7 +356,7 @@ class Model:
364
356
  path=model_path, # Customize the name of the model location.
365
357
  infer_code_paths=True, # Makes the imports portable.
366
358
  python_model=MLFlowModel(),
367
- # signature=signature, # Please see comment above about keeping this in case we need to go back.
359
+ signature=signature, # Allows us to work with our own `Options` class.
368
360
  )
369
361
 
370
362
  # Create an auxiliary requirements file with the model dependencies.
@@ -423,7 +415,9 @@ def _cleanup_python_model(
423
415
  if os.path.exists(model_path):
424
416
  shutil.rmtree(model_path)
425
417
 
426
- _cleanup_mlflow_db(model_dir)
418
+ mlruns_path = os.path.join(model_dir, "mlruns")
419
+ if os.path.exists(mlruns_path):
420
+ shutil.rmtree(mlruns_path)
427
421
 
428
422
  requirements_file = os.path.join(model_dir, _REQUIREMENTS_FILE)
429
423
  if os.path.exists(requirements_file):
@@ -435,36 +429,3 @@ def _cleanup_python_model(
435
429
 
436
430
  if verbose:
437
431
  log("🧹 Cleaned up Python model artifacts.")
438
-
439
-
440
- def _cleanup_mlflow_db(model_dir: str) -> None:
441
- """
442
- Clean up the mlflow.db file created during model packaging.
443
-
444
- Parameters
445
- ----------
446
- model_dir : str
447
- The directory where the model was saved.
448
- """
449
- mlflow_db_path = os.path.join(model_dir, "mlflow.db")
450
- if not os.path.exists(mlflow_db_path):
451
- return
452
-
453
- # Try to close any open SQLite connections and retry deletion
454
- max_retries = 5
455
- for attempt in range(max_retries):
456
- try:
457
- # Attempt to connect and close to release any locks
458
- try:
459
- conn = sqlite3.connect(mlflow_db_path)
460
- conn.close()
461
- except Exception:
462
- pass
463
- os.remove(mlflow_db_path)
464
- break
465
- except PermissionError:
466
- if attempt < max_retries - 1:
467
- time.sleep(0.5)
468
- else:
469
- log(f"Could not delete {mlflow_db_path} after {max_retries} retries due to file lock.")
470
- break
nextmv/options.py CHANGED
@@ -24,6 +24,155 @@ from dataclasses import dataclass
24
24
  from typing import Any
25
25
 
26
26
  from nextmv.base_model import BaseModel
27
+ from nextmv.deprecated import deprecated
28
+
29
+
30
+ @dataclass
31
+ class Parameter:
32
+ """
33
+ !!! warning
34
+ `Parameter` is deprecated, use `Option` instead.
35
+
36
+ Parameter that is used in a `Configuration`. When a parameter is required,
37
+ it is a good practice to provide a default value for it. This is because
38
+ the configuration will raise an error if a required parameter is not
39
+ provided through a command-line argument, an environment variable or a
40
+ default value.
41
+
42
+ Parameters
43
+ ----------
44
+ name : str
45
+ The name of the parameter.
46
+
47
+ param_type : type
48
+ The type of the parameter.
49
+
50
+ default : Any, optional
51
+ The default value of the parameter. Even though this is optional, it is
52
+ recommended to provide a default value for all parameters.
53
+
54
+ description : str, optional
55
+ An optional description of the parameter. This is useful for generating
56
+ help messages for the configuration.
57
+
58
+ required : bool, optional
59
+ Whether the parameter is required. If a parameter is required, it will
60
+ be an error to not provide a value for it, either through a command-line
61
+ argument, an environment variable or a default value.
62
+
63
+ choices : list[Optional[Any]], optional
64
+ Limits values to a specific set of choices.
65
+
66
+ Examples
67
+ --------
68
+ >>> from nextmv.options import Parameter
69
+ >>> parameter = Parameter("timeout", int, 60, "The maximum timeout in seconds", required=True)
70
+ """
71
+
72
+ name: str
73
+ """The name of the parameter."""
74
+ param_type: type
75
+ """The type of the parameter."""
76
+
77
+ default: Any | None = None
78
+ """The default value of the parameter. Even though this is optional, it is
79
+ recommended to provide a default value for all parameters."""
80
+ description: str | None = None
81
+ """An optional description of the parameter. This is useful for generating
82
+ help messages for the configuration."""
83
+ required: bool = False
84
+ """Whether the parameter is required. If a parameter is required, it will
85
+ be an error to not provide a value for it, either trough a command-line
86
+ argument, an environment variable or a default value."""
87
+ choices: list[Any | None] = None
88
+ """Limits values to a specific set of choices."""
89
+
90
+ def __post_init__(self):
91
+ """
92
+ Post-initialization hook that marks this class as deprecated.
93
+
94
+ This method is automatically called after the object is initialized.
95
+ It displays a deprecation warning to inform users to use the `Option` class instead.
96
+ """
97
+ deprecated(
98
+ name="Parameter",
99
+ reason="`Parameter` is deprecated, use `Option` instead",
100
+ )
101
+
102
+ @classmethod
103
+ def from_dict(cls, data: dict[str, Any]) -> "Parameter":
104
+ """
105
+ !!! warning
106
+ `Parameter` is deprecated, use `Option` instead.
107
+ `Parameter.from_dict` -> `Option.from_dict`
108
+
109
+ Creates an instance of `Parameter` from a dictionary.
110
+
111
+ Parameters
112
+ ----------
113
+ data : dict[str, Any]
114
+ The dictionary representation of a parameter.
115
+
116
+ Returns
117
+ -------
118
+ Parameter
119
+ An instance of `Parameter`.
120
+ """
121
+
122
+ deprecated(
123
+ name="Parameter.from_dict",
124
+ reason="`Parameter` is deprecated, use `Option` instead. Parameter.from_dict -> Option.from_dict",
125
+ )
126
+
127
+ param_type_string = data["param_type"]
128
+ param_type = getattr(builtins, param_type_string.split("'")[1])
129
+
130
+ return Parameter(
131
+ name=data["name"],
132
+ param_type=param_type,
133
+ default=data.get("default"),
134
+ description=data.get("description"),
135
+ required=data.get("required", False),
136
+ choices=data.get("choices"),
137
+ )
138
+
139
+ def to_dict(self) -> dict[str, Any]:
140
+ """
141
+ !!! warning
142
+ `Parameter` is deprecated, use `Option` instead.
143
+ `Parameter.to_dict` -> `Option.to_dict`
144
+
145
+ Converts the parameter to a dict.
146
+
147
+ Returns
148
+ -------
149
+ dict[str, Any]
150
+ The parameter as a dict with its name, type, default value,
151
+ description, required flag, and choices.
152
+
153
+ Examples
154
+ --------
155
+ >>> param = Parameter("timeout", int, 60, "Maximum time in seconds", True)
156
+ >>> param_dict = param.to_dict()
157
+ >>> param_dict["name"]
158
+ 'timeout'
159
+ >>> param_dict["default"]
160
+ 60
161
+ """
162
+
163
+ deprecated(
164
+ name="Parameter.to_dict",
165
+ reason="`Parameter` is deprecated, use `Option` instead. Parameter.to_dict -> Option.to_dict",
166
+ )
167
+
168
+ return {
169
+ "name": self.name,
170
+ "param_type": str(self.param_type),
171
+ "default": self.default,
172
+ "description": self.description,
173
+ "required": self.required,
174
+ "choices": self.choices,
175
+ }
27
176
 
28
177
 
29
178
  @dataclass
@@ -276,7 +425,8 @@ class Options:
276
425
  If a required option is not provided through a command-line
277
426
  argument, an environment variable or a default value.
278
427
  TypeError
279
- If an option is not an `Option`
428
+ If an option is not either an `Option` or `Parameter` (deprecated)
429
+ object.
280
430
  ValueError
281
431
  If an environment variable is not of the type of the corresponding
282
432
  parameter.
@@ -372,6 +522,27 @@ class Options:
372
522
 
373
523
  return cloud_dict
374
524
 
525
+ def parameters_dict(self) -> list[dict[str, Any]]:
526
+ """
527
+ !!! warning
528
+ `Parameter` is deprecated, use `Option` instead. `Options.parameters_dict` -> `Options.options_dict`
529
+
530
+ Converts the options to a list of dicts. Each dict is the dict
531
+ representation of a `Parameter`.
532
+
533
+ Returns
534
+ -------
535
+ list[dict[str, Any]]
536
+ The list of dictionaries (parameter entries).
537
+ """
538
+
539
+ deprecated(
540
+ name="Options.parameters_dict",
541
+ reason="`Parameter` is deprecated, use `Option` instead. Options.parameters_dict -> Options.options_dict",
542
+ )
543
+
544
+ return [param.to_dict() for param in self.options]
545
+
375
546
  def options_dict(self) -> list[dict[str, Any]]:
376
547
  """
377
548
  Converts the `Options` to a list of dicts. Each dict is the dict
@@ -435,7 +606,7 @@ class Options:
435
606
  If a required option is not provided through a command-line
436
607
  argument, an environment variable or a default value.
437
608
  TypeError
438
- If an option is not an `Option` object.
609
+ If an option is not an `Option` or `Parameter` (deprecated) object.
439
610
  ValueError
440
611
  If an environment variable is not of the type of the corresponding
441
612
  parameter.
@@ -557,6 +728,41 @@ class Options:
557
728
 
558
729
  return cls(*options)
559
730
 
731
+ @classmethod
732
+ def from_parameters_dict(cls, parameters_dict: list[dict[str, Any]]) -> "Options":
733
+ """
734
+ !!! warning
735
+
736
+ `Parameter` is deprecated, use `Option` instead.
737
+ `Options.from_parameters_dict` -> `Options.from_options_dict`
738
+
739
+ Creates an instance of `Options` from parameters in dict form. Each
740
+ entry is the dict representation of a `Parameter`.
741
+
742
+ Parameters
743
+ ----------
744
+ parameters_dict : list[dict[str, Any]]
745
+ The list of dictionaries (parameter entries).
746
+
747
+ Returns
748
+ -------
749
+ Options
750
+ An instance of `Options`.
751
+ """
752
+
753
+ deprecated(
754
+ name="Options.from_parameters_dict",
755
+ reason="`Parameter` is deprecated, use `Option` instead. "
756
+ "Options.from_parameters_dict -> Options.from_options_dict",
757
+ )
758
+
759
+ parameters = []
760
+ for parameter_dict in parameters_dict:
761
+ parameter = Parameter.from_dict(parameter_dict)
762
+ parameters.append(parameter)
763
+
764
+ return cls(*parameters)
765
+
560
766
  @classmethod
561
767
  def from_options_dict(cls, options_dict: list[dict[str, Any]]) -> "Options":
562
768
  """
@@ -631,7 +837,7 @@ class Options:
631
837
  If a required option is not provided through a command-line
632
838
  argument, an environment variable or a default value.
633
839
  TypeError
634
- If an option is not an `Option` object.
840
+ If an option is not an `Option` or `Parameter` (deprecated) object.
635
841
  ValueError
636
842
  If an environment variable is not of the type of the corresponding
637
843
  parameter.
@@ -652,8 +858,10 @@ class Options:
652
858
  options_by_field_name: dict[str, Option] = {}
653
859
 
654
860
  for ix, option in enumerate(self.options):
655
- if not isinstance(option, Option):
656
- raise TypeError(f"expected an <Option> object, but got {type(option)} in index {ix}")
861
+ if not isinstance(option, Option) and not isinstance(option, Parameter):
862
+ raise TypeError(
863
+ f"expected an <Option> (or deprecated <Parameter>) object, but got {type(option)} in index {ix}"
864
+ )
657
865
 
658
866
  # See comment below about ipykernel adding a `-f` argument. We
659
867
  # restrict options from having the name 'f' or 'fff' for that
@@ -668,7 +876,7 @@ class Options:
668
876
  option.name = option.name.lstrip("-")
669
877
 
670
878
  kwargs = {
671
- "type": option.option_type if option.option_type is not bool else str,
879
+ "type": self._option_type(option) if self._option_type(option) is not bool else str,
672
880
  "help": self._description(option),
673
881
  }
674
882
 
@@ -695,7 +903,7 @@ class Options:
695
903
  help=argparse.SUPPRESS,
696
904
  default="1",
697
905
  )
698
- args, _ = parser.parse_known_args()
906
+ args = parser.parse_args()
699
907
 
700
908
  for arg in vars(args):
701
909
  if arg == "fff" or arg == "f":
@@ -717,10 +925,12 @@ class Options:
717
925
  env_value = os.getenv(upper_name)
718
926
  if env_value is not None:
719
927
  try:
720
- typed_env_value = option.option_type(env_value) if option.option_type is not bool else env_value
928
+ typed_env_value = (
929
+ self._option_type(option)(env_value) if self._option_type(option) is not bool else env_value
930
+ )
721
931
  except ValueError:
722
932
  raise ValueError(
723
- f'environment variable "{upper_name}" is not of type {option.option_type}'
933
+ f'environment variable "{upper_name}" is not of type {self._option_type(option)}'
724
934
  ) from None
725
935
 
726
936
  value = self._option_value(option, typed_env_value)
@@ -758,6 +968,8 @@ class Options:
758
968
  """
759
969
 
760
970
  description = ""
971
+ if isinstance(option, Parameter):
972
+ description = "DEPRECATED (initialized with <Parameter>, use <Option> instead) "
761
973
 
762
974
  description += f"[env var: {option.name.upper()}]"
763
975
 
@@ -767,7 +979,7 @@ class Options:
767
979
  if option.default is not None:
768
980
  description += f" (default: {option.default})"
769
981
 
770
- description += f" (type: {option.option_type.__name__})"
982
+ description += f" (type: {self._option_type(option).__name__})"
771
983
 
772
984
  if isinstance(option, Option) and option.additional_attributes is not None:
773
985
  description += f" (additional attributes: {option.additional_attributes})"
@@ -808,7 +1020,7 @@ class Options:
808
1020
  other values are converted to False.
809
1021
  """
810
1022
 
811
- opt_type = option.option_type
1023
+ opt_type = self._option_type(option)
812
1024
  if opt_type is not bool:
813
1025
  return value
814
1026
 
@@ -819,6 +1031,39 @@ class Options:
819
1031
 
820
1032
  return False
821
1033
 
1034
+ @staticmethod
1035
+ def _option_type(option: Option | Parameter) -> type:
1036
+ """
1037
+ Get the type of an option.
1038
+
1039
+ This auxiliary function was introduced for backwards compatibility with
1040
+ the deprecated `Parameter` class. Once `Parameter` is removed, this function
1041
+ can be removed as well. When the function is removed, use the
1042
+ `option.option_type` attribute directly, instead of calling this function.
1043
+
1044
+ Parameters
1045
+ ----------
1046
+ option : Union[Option, Parameter]
1047
+ The option to get the type for.
1048
+
1049
+ Returns
1050
+ -------
1051
+ type
1052
+ The type of the option.
1053
+
1054
+ Raises
1055
+ ------
1056
+ TypeError
1057
+ If the option is not an `Option` or `Parameter` object.
1058
+ """
1059
+
1060
+ if isinstance(option, Option):
1061
+ return option.option_type
1062
+ elif isinstance(option, Parameter):
1063
+ return option.param_type
1064
+ else:
1065
+ raise TypeError(f"expected an <Option> (or deprecated <Parameter>) object, but got {type(option)}")
1066
+
822
1067
 
823
1068
  class OptionsEnforcement:
824
1069
  """
nextmv/output.py CHANGED
@@ -66,6 +66,7 @@ from pydantic import AliasChoices, Field
66
66
 
67
67
  from nextmv._serialization import serialize_json
68
68
  from nextmv.base_model import BaseModel
69
+ from nextmv.deprecated import deprecated
69
70
  from nextmv.logger import reset_stdout
70
71
  from nextmv.options import Options
71
72
 
@@ -1517,6 +1518,67 @@ class LocalOutputWriter(OutputWriter):
1517
1518
  )
1518
1519
 
1519
1520
 
1521
+ def write_local(
1522
+ output: Output | dict[str, Any],
1523
+ path: str | None = None,
1524
+ skip_stdout_reset: bool = False,
1525
+ ) -> None:
1526
+ """
1527
+ !!! warning
1528
+ `write_local` is deprecated, use `write` instead.
1529
+
1530
+ Write the output to the local filesystem or stdout.
1531
+
1532
+ This is a convenience function for instantiating a `LocalOutputWriter` and
1533
+ calling its `write` method.
1534
+
1535
+ Parameters
1536
+ ----------
1537
+ output : Union[Output, dict[str, Any]]
1538
+ Output data to write. Can be an Output object or a dictionary.
1539
+ path : str, optional
1540
+ Path to write the output data to. The interpretation depends on the
1541
+ output format:
1542
+
1543
+ - For `OutputFormat.JSON`: File path for the JSON output. If None or
1544
+ empty, writes to stdout.
1545
+ - For `OutputFormat.CSV_ARCHIVE`: Directory path for CSV files. If None
1546
+ or empty, writes to a directory named "output" in the current working
1547
+ directory.
1548
+ skip_stdout_reset : bool, optional
1549
+ Skip resetting stdout before writing the output data. Default is False.
1550
+
1551
+ Raises
1552
+ ------
1553
+ ValueError
1554
+ If the Output.output_format is not supported.
1555
+ TypeError
1556
+ If the output is of an unsupported type.
1557
+
1558
+ Notes
1559
+ -----
1560
+ This function detects if stdout was redirected and resets it to avoid
1561
+ unexpected behavior. If you want to skip this behavior, set the
1562
+ skip_stdout_reset parameter to True.
1563
+
1564
+ Examples
1565
+ --------
1566
+ >>> from nextmv.output import write_local, Output
1567
+ >>> # Write JSON to a file
1568
+ >>> write_local(Output(solution={"result": 42}), path="result.json")
1569
+ >>> # Write JSON to stdout
1570
+ >>> write_local({"simple": "data"})
1571
+ """
1572
+
1573
+ deprecated(
1574
+ name="write_local",
1575
+ reason="`write_local` is deprecated, use `write` instead",
1576
+ )
1577
+
1578
+ writer = LocalOutputWriter()
1579
+ writer.write(output, path, skip_stdout_reset)
1580
+
1581
+
1520
1582
  _LOCAL_OUTPUT_WRITER = LocalOutputWriter()
1521
1583
  """Default LocalOutputWriter instance used by the write function."""
1522
1584
 
nextmv/run.py CHANGED
@@ -53,7 +53,7 @@ from nextmv._serialization import serialize_json
53
53
  from nextmv.base_model import BaseModel
54
54
  from nextmv.input import Input, InputFormat
55
55
  from nextmv.output import Asset, Output, OutputFormat, Statistics
56
- from nextmv.status import StatusV2
56
+ from nextmv.status import Status, StatusV2
57
57
 
58
58
 
59
59
  def run_duration(start: datetime | float, end: datetime | float) -> int:
@@ -520,6 +520,8 @@ class Run(BaseModel):
520
520
  Class name for the execution of a job.
521
521
  runtime : str
522
522
  Runtime environment for the run.
523
+ status : Status
524
+ Deprecated, use status_v2 instead.
523
525
  status_v2 : StatusV2
524
526
  Status of the run.
525
527
  queuing_priority : int, optional
@@ -596,6 +598,8 @@ class Run(BaseModel):
596
598
  status_v2: StatusV2
597
599
  """Status of the run."""
598
600
 
601
+ status: Status | None = None
602
+ """Deprecated, use status_v2 instead."""
599
603
  queuing_priority: int | None = None
600
604
  """Priority of the run in the queue."""
601
605
  queuing_disabled: bool | None = None
@@ -652,6 +656,8 @@ class Metadata(BaseModel):
652
656
  Size of the output in bytes.
653
657
  format : Format
654
658
  Format of the input and output of the run.
659
+ status : Status
660
+ Deprecated: use status_v2.
655
661
  status_v2 : StatusV2
656
662
  Status of the run.
657
663
  """
@@ -676,6 +682,8 @@ class Metadata(BaseModel):
676
682
  """Format of the input and output of the run."""
677
683
  status_v2: StatusV2
678
684
  """Status of the run."""
685
+ status: Status | None = None
686
+ """Deprecated: use status_v2."""
679
687
  statistics: dict[str, Any] | None = None
680
688
  """User defined statistics of the run."""
681
689
 
@@ -859,6 +867,7 @@ class RunInformation(BaseModel):
859
867
  run_type=RunTypeConfiguration(), # Default empty configuration
860
868
  execution_class="", # Not available in RunInformation
861
869
  runtime="", # Not available in RunInformation
870
+ status=self.metadata.status,
862
871
  status_v2=self.metadata.status_v2,
863
872
  # Optional fields that are not available in RunInformation
864
873
  queuing_priority=None,
nextmv/status.py CHANGED
@@ -2,10 +2,13 @@
2
2
  Provides status enums for Nextmv application runs.
3
3
 
4
4
  This module defines enumerations for representing the status of a run in a
5
- Nextmv application.
5
+ Nextmv application. It includes a deprecated `Status` enum and the current
6
+ `StatusV2` enum.
6
7
 
7
8
  Classes
8
9
  -------
10
+ Status
11
+ Deprecated status of a run.
9
12
  StatusV2
10
13
  Represents the status of a run.
11
14
  """
@@ -13,6 +16,53 @@ StatusV2
13
16
  from enum import Enum
14
17
 
15
18
 
19
+ class Status(str, Enum):
20
+ """
21
+ !!! warning
22
+ `Status` is deprecated, use `StatusV2` instead.
23
+
24
+ Status of a run.
25
+
26
+ You can import the `Status` class directly from `nextmv`:
27
+
28
+ ```python
29
+ from nextmv import Status
30
+ ```
31
+
32
+ This enum represents the possible states of a run. It is deprecated and
33
+ `StatusV2` should be used for new implementations.
34
+
35
+ Attributes
36
+ ----------
37
+ failed : str
38
+ Run failed.
39
+ running : str
40
+ Run is running.
41
+ succeeded : str
42
+ Run succeeded.
43
+
44
+ Examples
45
+ --------
46
+ >>> from nextmv.cloud import Status
47
+ >>> current_status = Status.running
48
+ >>> if current_status == Status.succeeded:
49
+ ... print("Run completed successfully.")
50
+ ... elif current_status == Status.failed:
51
+ ... print("Run failed.")
52
+ ... else:
53
+ ... print(f"Run is currently {current_status.value}.")
54
+ Run is currently running.
55
+
56
+ """
57
+
58
+ failed = "failed"
59
+ """Run failed."""
60
+ running = "running"
61
+ """Run is running."""
62
+ succeeded = "succeeded"
63
+ """Run succeeded."""
64
+
65
+
16
66
  class StatusV2(str, Enum):
17
67
  """
18
68
  Status of a run.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 1.0.0
3
+ Version: 1.0.0.dev0
4
4
  Summary: The all-purpose Python SDK for Nextmv
5
5
  Project-URL: Homepage, https://www.nextmv.io
6
6
  Project-URL: Documentation, https://nextmv-py.docs.nextmv.io/en/latest/nextmv/
@@ -229,20 +229,19 @@ Requires-Dist: plotly>=6.0.1; extra == 'all'
229
229
  Provides-Extra: dev
230
230
  Requires-Dist: build>=1.0.3; extra == 'dev'
231
231
  Requires-Dist: folium>=0.20.0; extra == 'dev'
232
- Requires-Dist: mlflow>=3.9.0; extra == 'dev'
233
- Requires-Dist: nextpipe>=0.6.0; extra == 'dev'
232
+ Requires-Dist: mlflow>=2.19.0; extra == 'dev'
233
+ Requires-Dist: nextroute>=1.11.1; extra == 'dev'
234
234
  Requires-Dist: openpyxl>=3.1.5; extra == 'dev'
235
235
  Requires-Dist: pandas>=2.2.3; extra == 'dev'
236
236
  Requires-Dist: plotly>=6.0.1; extra == 'dev'
237
237
  Requires-Dist: pydantic>=2.5.2; extra == 'dev'
238
- Requires-Dist: pytest>=9.0.2; extra == 'dev'
239
238
  Requires-Dist: pyyaml>=6.0.1; extra == 'dev'
240
239
  Requires-Dist: requests>=2.31.0; extra == 'dev'
241
240
  Requires-Dist: ruff>=0.1.7; extra == 'dev'
242
241
  Requires-Dist: twine>=4.0.2; extra == 'dev'
243
242
  Requires-Dist: urllib3>=2.1.0; extra == 'dev'
244
243
  Provides-Extra: notebook
245
- Requires-Dist: mlflow>=3.9.0; extra == 'notebook'
244
+ Requires-Dist: mlflow>=2.19.0; extra == 'notebook'
246
245
  Description-Content-Type: text/markdown
247
246
 
248
247
  # Nextmv Python SDK