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/model.py ADDED
@@ -0,0 +1,431 @@
1
+ """
2
+ Model module for creating and saving decision models in Nextmv Cloud.
3
+
4
+ This module provides the base classes and functionality for creating decision models
5
+ that can be deployed and run in Nextmv Cloud. The main components are:
6
+
7
+ Classes
8
+ -------
9
+ Model
10
+ Base class for defining decision models.
11
+ ModelConfiguration
12
+ Configuration for packaging and deploying models.
13
+
14
+ Models defined using this module can be packaged with their dependencies and
15
+ deployed to Nextmv Cloud for execution.
16
+ """
17
+
18
+ import logging
19
+ import os
20
+ import shutil
21
+ import warnings
22
+ from dataclasses import dataclass
23
+ from typing import Any
24
+
25
+ from nextmv.input import Input
26
+ from nextmv.logger import log
27
+ from nextmv.options import Options, OptionsEnforcement
28
+ from nextmv.output import Output
29
+
30
+ # The following block of code is used to suppress warnings from mlflow. We
31
+ # suppress these warnings because they are not relevant to the user, and they
32
+ # are not actionable.
33
+
34
+ """
35
+ Module-level function and variable to suppress warnings from mlflow.
36
+ """
37
+
38
+ _original_showwarning = warnings.showwarning
39
+ """Original showwarning function from the warnings module."""
40
+
41
+
42
+ def _custom_showwarning(message, category, filename, lineno, file=None, line=None):
43
+ """
44
+ Custom warning handler that suppresses specific mlflow warnings.
45
+
46
+ This function filters out non-actionable warnings from the mlflow library
47
+ to keep the console output clean and relevant for the user.
48
+
49
+ Parameters
50
+ ----------
51
+ message : str
52
+ The warning message.
53
+ category : Warning
54
+ The warning category.
55
+ filename : str
56
+ The filename where the warning was raised.
57
+ lineno : int
58
+ The line number where the warning was raised.
59
+ file : file, optional
60
+ The file to write the warning to.
61
+ line : str, optional
62
+ The line of source code to be included in the warning message.
63
+
64
+ Returns
65
+ -------
66
+ None
67
+ If the warning matches certain patterns, the function returns early
68
+ without showing the warning. Otherwise, it delegates to the original
69
+ warning handler.
70
+ """
71
+ # .../site-packages/mlflow/pyfunc/utils/data_validation.py:134: UserWarning:Add
72
+ # type hints to the `predict` method to enable data validation and automatic
73
+ # signature inference during model logging. Check
74
+ # https://mlflow.org/docs/latest/model/python_model.html#type-hint-usage-in-pythonmodel
75
+ # for more details.
76
+ if "mlflow/pyfunc/utils/data_validation.py" in filename:
77
+ return
78
+
79
+ # .../site-packages/mlflow/pyfunc/__init__.py:3212: UserWarning: An input
80
+ # example was not provided when logging the model. To ensure the model
81
+ # signature functions correctly, specify the `input_example` parameter. See
82
+ # https://mlflow.org/docs/latest/model/signatures.html#model-input-example
83
+ # for more details about the benefits of using input_example.
84
+ if "mlflow/pyfunc/__init__.py" in filename:
85
+ return
86
+
87
+ _original_showwarning(message, category, filename, lineno, file, line)
88
+
89
+
90
+ warnings.showwarning = _custom_showwarning
91
+
92
+ # When working with the `Model`, we expect to be working in a notebook
93
+ # environment, and not interact with the local filesystem a lot. We use the
94
+ # `ModelConfiguration` to specify the dependencies that the `Model` requires.
95
+ # To work with the "push" logic of uploading an app to Nextmv Cloud, we need a
96
+ # requirement file that we use to gather dependencies, install them, and bundle
97
+ # them in the app. This file is used as a placeholder for the dependencies that
98
+ # the model requires and that we install and bundle with the app.
99
+ _REQUIREMENTS_FILE = "model_requirements.txt"
100
+
101
+ # When working in a notebook environment, we don't really create a `main.py`
102
+ # file with the main entrypoint of the program. Because the logic is mostly
103
+ # encoded inside the `Model` class, we need to create a `main.py` file that we
104
+ # can run in Nextmv Cloud. This file is used as that entrypoint.
105
+ _ENTRYPOINT_FILE = "__entrypoint__.py"
106
+
107
+
108
+ # Required mlflow dependency version for model packaging.
109
+ _MLFLOW_DEPENDENCY = "mlflow>=2.18.0"
110
+
111
+
112
+ @dataclass
113
+ class ModelConfiguration:
114
+ """
115
+ Configuration class for Nextmv models.
116
+
117
+ You can import the `ModelConfiguration` class directly from `nextmv`:
118
+
119
+ ```python
120
+ from nextmv import ModelConfiguration
121
+ ```
122
+
123
+ This class holds the configuration for a model, defining how a Python model
124
+ is encoded and loaded for use in Nextmv Cloud.
125
+
126
+ Parameters
127
+ ----------
128
+ name : str
129
+ A personalized name for the model. This is required.
130
+ requirements : list[str], optional
131
+ A list of Python dependencies that the decision model requires,
132
+ formatted as they would appear in a requirements.txt file.
133
+ options : Options, optional
134
+ Options that the decision model requires.
135
+ options_enforcement:
136
+ Enforcement of options for the model. This controls how options
137
+ are handled when the model is run.
138
+
139
+ Examples
140
+ --------
141
+ >>> from nextmv import ModelConfiguration, Options
142
+ >>> config = ModelConfiguration(
143
+ ... name="my_routing_model",
144
+ ... requirements=["nextroute>=1.0.0"],
145
+ ... options=Options({"max_time": 60}),
146
+ ... options_enforcement=OptionsEnforcement(
147
+ strict=True,
148
+ validation_enforce=True
149
+ )
150
+ ... )
151
+ """
152
+
153
+ name: str
154
+ """The name of the decision model."""
155
+ requirements: list[str] | None = None
156
+ """A list of Python dependencies that the decision model requires."""
157
+ options: Options | None = None
158
+ """Options that the decision model requires."""
159
+ options_enforcement: OptionsEnforcement | None = None
160
+ """Enforcement of options for the model."""
161
+
162
+
163
+ class Model:
164
+ """
165
+ Base class for defining decision models that run in Nextmv Cloud.
166
+
167
+ You can import the `Model` class directly from `nextmv`:
168
+
169
+ ```python
170
+ from nextmv import Model
171
+ ```
172
+
173
+ This class serves as a foundation for creating decision models that can be
174
+ deployed to Nextmv Cloud. Subclasses must implement the `solve` method,
175
+ which is the main entry point for processing inputs and producing decisions.
176
+
177
+ Methods
178
+ -------
179
+ solve(input)
180
+ Process input data and produce a decision output.
181
+ save(model_dir, configuration)
182
+ Save the model to the filesystem for deployment.
183
+
184
+ Examples
185
+ --------
186
+ >>> import nextroute
187
+ >>> import nextmv
188
+ >>>
189
+ >>> class DecisionModel(nextmv.Model):
190
+ ... def solve(self, input: nextmv.Input) -> nextmv.Output:
191
+ ... nextroute_input = nextroute.schema.Input.from_dict(input.data)
192
+ ... nextroute_options = nextroute.Options.extract_from_dict(input.options.to_dict())
193
+ ... nextroute_output = nextroute.solve(nextroute_input, nextroute_options)
194
+ ...
195
+ ... return nextmv.Output(
196
+ ... options=input.options,
197
+ ... solution=nextroute_output.solutions[0].to_dict(),
198
+ ... statistics=nextroute_output.statistics.to_dict(),
199
+ ... )
200
+ """
201
+
202
+ def solve(self, input: Input) -> Output:
203
+ """
204
+ Process input data and produce a decision output.
205
+
206
+ This is the main entry point of your model that you must implement in
207
+ subclasses. It receives input data and should process it to produce an
208
+ output containing the solution to the decision problem.
209
+
210
+ Parameters
211
+ ----------
212
+ input : Input
213
+ The input data that the model will use to make a decision.
214
+
215
+ Returns
216
+ -------
217
+ Output
218
+ The output of the model, which is the solution to the decision
219
+ model/problem.
220
+
221
+ Raises
222
+ ------
223
+ NotImplementedError
224
+ When called on the base Model class, as this method must be
225
+ implemented by subclasses.
226
+
227
+ Examples
228
+ --------
229
+ >>> def solve(self, input: Input) -> Output:
230
+ ... # Process input data
231
+ ... result = self._process_data(input.data)
232
+ ...
233
+ ... # Return formatted output
234
+ ... return Output(
235
+ ... options=input.options,
236
+ ... solution=result,
237
+ ... statistics={"processing_time": 0.5}
238
+ ... )
239
+ """
240
+
241
+ raise NotImplementedError
242
+
243
+ def save(model_self, model_dir: str, configuration: ModelConfiguration) -> None:
244
+ """
245
+ Save the model to the local filesystem for deployment.
246
+
247
+ This method packages the model according to the provided configuration,
248
+ creating all necessary files and dependencies for deployment to Nextmv
249
+ Cloud.
250
+
251
+ Parameters
252
+ ----------
253
+ model_dir : str
254
+ The directory where the model will be saved.
255
+ configuration : ModelConfiguration
256
+ The configuration of the model, which defines how the model is
257
+ saved and loaded.
258
+
259
+ Raises
260
+ ------
261
+ ImportError
262
+ If mlflow is not installed, which is required for model packaging.
263
+
264
+ Notes
265
+ -----
266
+ This method uses mlflow for model packaging, creating the necessary
267
+ files and directory structure for deployment.
268
+
269
+ Examples
270
+ --------
271
+ >>> model = MyDecisionModel()
272
+ >>> config = ModelConfiguration(
273
+ ... name="routing_model",
274
+ ... requirements=["pandas", "numpy"]
275
+ ... )
276
+ >>> model.save("/tmp/my_model", config)
277
+ """
278
+
279
+ # mlflow is a big package. We don't want to make it a dependency of
280
+ # `nextmv` because it is not always needed. We only need it if we are
281
+ # working with the "app from model" logic, which involves working with
282
+ # this `Model` class.
283
+ try:
284
+ import mlflow as mlflow
285
+ except ImportError as e:
286
+ raise ImportError(
287
+ "mlflow is not installed. Please install optional dependencies with `pip install nextmv[all]`"
288
+ ) from e
289
+
290
+ finally:
291
+ from mlflow.models import infer_signature
292
+ from mlflow.pyfunc import PythonModel, save_model
293
+
294
+ class MLFlowModel(PythonModel):
295
+ """
296
+ Transient class to translate a Nextmv Decision Model into an MLflow PythonModel.
297
+
298
+ This class complies with the MLflow inference API, implementing a `predict`
299
+ method that calls the user-defined `solve` method of the Nextmv Decision Model.
300
+
301
+ Methods
302
+ -------
303
+ predict(context, model_input, params)
304
+ MLflow-compliant predict method that delegates to the Nextmv model's solve method.
305
+ """
306
+
307
+ def predict(
308
+ self,
309
+ context,
310
+ model_input,
311
+ params: dict[str, Any] | None = None,
312
+ ) -> Any:
313
+ """
314
+ MLflow-compliant prediction method that calls the Nextmv model's solve method.
315
+
316
+ This method enables compatibility with MLflow's python_function model flavor.
317
+
318
+ Parameters
319
+ ----------
320
+ context : mlflow.pyfunc.PythonModelContext
321
+ The MLflow model context.
322
+ model_input : Any
323
+ The input data for prediction, passed to the solve method.
324
+ params : Optional[dict[str, Any]], optional
325
+ Additional parameters for prediction.
326
+
327
+ Returns
328
+ -------
329
+ Any
330
+ The result from the Nextmv model's solve method.
331
+
332
+ Notes
333
+ -----
334
+ This method should not be used or overridden directly. Instead,
335
+ implement the `solve` method in your Nextmv Model subclass.
336
+ """
337
+
338
+ return model_self.solve(model_input)
339
+
340
+ # Some annoying logging from mlflow must be disabled.
341
+ logging.disable(logging.CRITICAL)
342
+
343
+ _cleanup_python_model(model_dir, configuration, verbose=False)
344
+
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
+ )
351
+
352
+ # We use mlflow to save the model to the local filesystem, to be able to
353
+ # load it later on.
354
+ model_path = os.path.join(model_dir, configuration.name)
355
+ save_model(
356
+ path=model_path, # Customize the name of the model location.
357
+ infer_code_paths=True, # Makes the imports portable.
358
+ python_model=MLFlowModel(),
359
+ signature=signature, # Allows us to work with our own `Options` class.
360
+ )
361
+
362
+ # Create an auxiliary requirements file with the model dependencies.
363
+ requirements_file = os.path.join(model_dir, _REQUIREMENTS_FILE)
364
+ with open(requirements_file, "w") as file:
365
+ file.write(f"{_MLFLOW_DEPENDENCY}\n")
366
+ reqs = configuration.requirements
367
+ if reqs is not None:
368
+ for req in reqs:
369
+ file.write(f"{req}\n")
370
+
371
+ # Adds the main.py file to the app_dir by coping the `entrypoint.py` file
372
+ # which is one level up from this file.
373
+ entrypoint_file = os.path.join(os.path.dirname(__file__), _ENTRYPOINT_FILE)
374
+ shutil.copy2(entrypoint_file, os.path.join(model_dir, "main.py"))
375
+
376
+
377
+ def _cleanup_python_model(
378
+ model_dir: str,
379
+ model_configuration: ModelConfiguration | None = None,
380
+ verbose: bool = False,
381
+ ) -> None:
382
+ """
383
+ Clean up Python-specific model packaging artifacts.
384
+
385
+ This function removes temporary files and directories created during the
386
+ model packaging process.
387
+
388
+ Parameters
389
+ ----------
390
+ model_dir : str
391
+ The directory where the model was saved.
392
+ model_configuration : Optional[ModelConfiguration], optional
393
+ The configuration of the model. If None, the function returns early.
394
+ verbose : bool, default=False
395
+ If True, log a message when cleanup is complete.
396
+
397
+ Returns
398
+ -------
399
+ None
400
+ This function does not return anything.
401
+
402
+ Notes
403
+ -----
404
+ Files and directories removed include:
405
+ - The model directory itself
406
+ - The mlruns directory created by MLflow
407
+ - The requirements file
408
+ - The main.py file
409
+ """
410
+
411
+ if model_configuration is None:
412
+ return
413
+
414
+ model_path = os.path.join(model_dir, model_configuration.name)
415
+ if os.path.exists(model_path):
416
+ shutil.rmtree(model_path)
417
+
418
+ mlruns_path = os.path.join(model_dir, "mlruns")
419
+ if os.path.exists(mlruns_path):
420
+ shutil.rmtree(mlruns_path)
421
+
422
+ requirements_file = os.path.join(model_dir, _REQUIREMENTS_FILE)
423
+ if os.path.exists(requirements_file):
424
+ os.remove(requirements_file)
425
+
426
+ main_file = os.path.join(model_dir, "main.py")
427
+ if os.path.exists(main_file):
428
+ os.remove(main_file)
429
+
430
+ if verbose:
431
+ log("🧹 Cleaned up Python model artifacts.")