nextmv 0.18.0__py3-none-any.whl → 1.0.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +8 -13
  3. nextmv/__init__.py +53 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +54 -9
  6. nextmv/cli/CONTRIBUTING.md +511 -0
  7. nextmv/cli/__init__.py +0 -0
  8. nextmv/cli/cloud/__init__.py +47 -0
  9. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  10. nextmv/cli/cloud/acceptance/create.py +393 -0
  11. nextmv/cli/cloud/acceptance/delete.py +68 -0
  12. nextmv/cli/cloud/acceptance/get.py +104 -0
  13. nextmv/cli/cloud/acceptance/list.py +62 -0
  14. nextmv/cli/cloud/acceptance/update.py +95 -0
  15. nextmv/cli/cloud/account/__init__.py +28 -0
  16. nextmv/cli/cloud/account/create.py +83 -0
  17. nextmv/cli/cloud/account/delete.py +60 -0
  18. nextmv/cli/cloud/account/get.py +66 -0
  19. nextmv/cli/cloud/account/update.py +70 -0
  20. nextmv/cli/cloud/app/__init__.py +35 -0
  21. nextmv/cli/cloud/app/create.py +141 -0
  22. nextmv/cli/cloud/app/delete.py +58 -0
  23. nextmv/cli/cloud/app/exists.py +44 -0
  24. nextmv/cli/cloud/app/get.py +66 -0
  25. nextmv/cli/cloud/app/list.py +61 -0
  26. nextmv/cli/cloud/app/push.py +137 -0
  27. nextmv/cli/cloud/app/update.py +124 -0
  28. nextmv/cli/cloud/batch/__init__.py +29 -0
  29. nextmv/cli/cloud/batch/create.py +454 -0
  30. nextmv/cli/cloud/batch/delete.py +68 -0
  31. nextmv/cli/cloud/batch/get.py +104 -0
  32. nextmv/cli/cloud/batch/list.py +63 -0
  33. nextmv/cli/cloud/batch/metadata.py +66 -0
  34. nextmv/cli/cloud/batch/update.py +95 -0
  35. nextmv/cli/cloud/data/__init__.py +26 -0
  36. nextmv/cli/cloud/data/upload.py +162 -0
  37. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  38. nextmv/cli/cloud/ensemble/create.py +414 -0
  39. nextmv/cli/cloud/ensemble/delete.py +67 -0
  40. nextmv/cli/cloud/ensemble/get.py +65 -0
  41. nextmv/cli/cloud/ensemble/update.py +103 -0
  42. nextmv/cli/cloud/input_set/__init__.py +30 -0
  43. nextmv/cli/cloud/input_set/create.py +170 -0
  44. nextmv/cli/cloud/input_set/get.py +63 -0
  45. nextmv/cli/cloud/input_set/list.py +63 -0
  46. nextmv/cli/cloud/input_set/update.py +123 -0
  47. nextmv/cli/cloud/instance/__init__.py +35 -0
  48. nextmv/cli/cloud/instance/create.py +290 -0
  49. nextmv/cli/cloud/instance/delete.py +62 -0
  50. nextmv/cli/cloud/instance/exists.py +39 -0
  51. nextmv/cli/cloud/instance/get.py +62 -0
  52. nextmv/cli/cloud/instance/list.py +60 -0
  53. nextmv/cli/cloud/instance/update.py +216 -0
  54. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  55. nextmv/cli/cloud/managed_input/create.py +146 -0
  56. nextmv/cli/cloud/managed_input/delete.py +65 -0
  57. nextmv/cli/cloud/managed_input/get.py +63 -0
  58. nextmv/cli/cloud/managed_input/list.py +60 -0
  59. nextmv/cli/cloud/managed_input/update.py +97 -0
  60. nextmv/cli/cloud/run/__init__.py +37 -0
  61. nextmv/cli/cloud/run/cancel.py +37 -0
  62. nextmv/cli/cloud/run/create.py +530 -0
  63. nextmv/cli/cloud/run/get.py +199 -0
  64. nextmv/cli/cloud/run/input.py +86 -0
  65. nextmv/cli/cloud/run/list.py +80 -0
  66. nextmv/cli/cloud/run/logs.py +167 -0
  67. nextmv/cli/cloud/run/metadata.py +67 -0
  68. nextmv/cli/cloud/run/track.py +501 -0
  69. nextmv/cli/cloud/scenario/__init__.py +29 -0
  70. nextmv/cli/cloud/scenario/create.py +451 -0
  71. nextmv/cli/cloud/scenario/delete.py +65 -0
  72. nextmv/cli/cloud/scenario/get.py +102 -0
  73. nextmv/cli/cloud/scenario/list.py +63 -0
  74. nextmv/cli/cloud/scenario/metadata.py +67 -0
  75. nextmv/cli/cloud/scenario/update.py +93 -0
  76. nextmv/cli/cloud/secrets/__init__.py +33 -0
  77. nextmv/cli/cloud/secrets/create.py +206 -0
  78. nextmv/cli/cloud/secrets/delete.py +67 -0
  79. nextmv/cli/cloud/secrets/get.py +66 -0
  80. nextmv/cli/cloud/secrets/list.py +60 -0
  81. nextmv/cli/cloud/secrets/update.py +147 -0
  82. nextmv/cli/cloud/shadow/__init__.py +33 -0
  83. nextmv/cli/cloud/shadow/create.py +184 -0
  84. nextmv/cli/cloud/shadow/delete.py +68 -0
  85. nextmv/cli/cloud/shadow/get.py +61 -0
  86. nextmv/cli/cloud/shadow/list.py +63 -0
  87. nextmv/cli/cloud/shadow/metadata.py +66 -0
  88. nextmv/cli/cloud/shadow/start.py +43 -0
  89. nextmv/cli/cloud/shadow/stop.py +43 -0
  90. nextmv/cli/cloud/shadow/update.py +95 -0
  91. nextmv/cli/cloud/upload/__init__.py +22 -0
  92. nextmv/cli/cloud/upload/create.py +39 -0
  93. nextmv/cli/cloud/version/__init__.py +33 -0
  94. nextmv/cli/cloud/version/create.py +97 -0
  95. nextmv/cli/cloud/version/delete.py +62 -0
  96. nextmv/cli/cloud/version/exists.py +39 -0
  97. nextmv/cli/cloud/version/get.py +62 -0
  98. nextmv/cli/cloud/version/list.py +60 -0
  99. nextmv/cli/cloud/version/update.py +92 -0
  100. nextmv/cli/community/__init__.py +24 -0
  101. nextmv/cli/community/clone.py +270 -0
  102. nextmv/cli/community/list.py +265 -0
  103. nextmv/cli/configuration/__init__.py +23 -0
  104. nextmv/cli/configuration/config.py +195 -0
  105. nextmv/cli/configuration/create.py +94 -0
  106. nextmv/cli/configuration/delete.py +67 -0
  107. nextmv/cli/configuration/list.py +77 -0
  108. nextmv/cli/main.py +188 -0
  109. nextmv/cli/message.py +153 -0
  110. nextmv/cli/options.py +206 -0
  111. nextmv/cli/version.py +38 -0
  112. nextmv/cloud/__init__.py +71 -17
  113. nextmv/cloud/acceptance_test.py +757 -51
  114. nextmv/cloud/account.py +406 -17
  115. nextmv/cloud/application/__init__.py +957 -0
  116. nextmv/cloud/application/_acceptance.py +419 -0
  117. nextmv/cloud/application/_batch_scenario.py +860 -0
  118. nextmv/cloud/application/_ensemble.py +251 -0
  119. nextmv/cloud/application/_input_set.py +227 -0
  120. nextmv/cloud/application/_instance.py +289 -0
  121. nextmv/cloud/application/_managed_input.py +227 -0
  122. nextmv/cloud/application/_run.py +1393 -0
  123. nextmv/cloud/application/_secrets.py +294 -0
  124. nextmv/cloud/application/_shadow.py +314 -0
  125. nextmv/cloud/application/_utils.py +54 -0
  126. nextmv/cloud/application/_version.py +303 -0
  127. nextmv/cloud/assets.py +48 -0
  128. nextmv/cloud/batch_experiment.py +294 -33
  129. nextmv/cloud/client.py +307 -66
  130. nextmv/cloud/ensemble.py +247 -0
  131. nextmv/cloud/input_set.py +120 -2
  132. nextmv/cloud/instance.py +133 -8
  133. nextmv/cloud/integration.py +533 -0
  134. nextmv/cloud/package.py +168 -53
  135. nextmv/cloud/scenario.py +410 -0
  136. nextmv/cloud/secrets.py +234 -0
  137. nextmv/cloud/shadow.py +190 -0
  138. nextmv/cloud/url.py +73 -0
  139. nextmv/cloud/version.py +132 -4
  140. nextmv/default_app/.gitignore +1 -0
  141. nextmv/default_app/README.md +32 -0
  142. nextmv/default_app/app.yaml +12 -0
  143. nextmv/default_app/input.json +5 -0
  144. nextmv/default_app/main.py +37 -0
  145. nextmv/default_app/requirements.txt +2 -0
  146. nextmv/default_app/src/__init__.py +0 -0
  147. nextmv/default_app/src/visuals.py +36 -0
  148. nextmv/deprecated.py +47 -0
  149. nextmv/input.py +861 -90
  150. nextmv/local/__init__.py +5 -0
  151. nextmv/local/application.py +1251 -0
  152. nextmv/local/executor.py +1042 -0
  153. nextmv/local/geojson_handler.py +323 -0
  154. nextmv/local/local.py +97 -0
  155. nextmv/local/plotly_handler.py +61 -0
  156. nextmv/local/runner.py +274 -0
  157. nextmv/logger.py +80 -9
  158. nextmv/manifest.py +1466 -0
  159. nextmv/model.py +241 -66
  160. nextmv/options.py +708 -115
  161. nextmv/output.py +1301 -274
  162. nextmv/polling.py +325 -0
  163. nextmv/run.py +1702 -0
  164. nextmv/safe.py +145 -0
  165. nextmv/status.py +122 -0
  166. nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
  167. nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
  168. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
  169. nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
  170. nextmv/cloud/application.py +0 -1405
  171. nextmv/cloud/manifest.py +0 -234
  172. nextmv/cloud/status.py +0 -29
  173. nextmv-0.18.0.dist-info/METADATA +0 -770
  174. nextmv-0.18.0.dist-info/RECORD +0 -25
  175. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
nextmv/local/runner.py ADDED
@@ -0,0 +1,274 @@
1
+ """
2
+ Runner module for executing local runs.
3
+
4
+ This module provides functionality to execute local runs.
5
+
6
+ Functions
7
+ ---------
8
+ run
9
+ Function to execute a local run.
10
+ new_run
11
+ Function to initialize a new run.
12
+ record_input
13
+ Function to write the input to the appropriate location.
14
+ """
15
+
16
+ import importlib.util
17
+ import json
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ from datetime import datetime, timezone
23
+ from typing import Any
24
+
25
+ from nextmv.input import INPUTS_KEY
26
+ from nextmv.local.local import DEFAULT_INPUT_JSON_FILE, NEXTMV_DIR, RUNS_KEY, calculate_files_size
27
+ from nextmv.manifest import Manifest
28
+ from nextmv.run import Format, FormatInput, Metadata, RunInformation, StatusV2
29
+ from nextmv.safe import safe_id
30
+
31
+
32
+ def run(
33
+ app_id: str,
34
+ src: str,
35
+ manifest: Manifest,
36
+ run_config: dict[str, Any],
37
+ name: str | None = None,
38
+ description: str | None = None,
39
+ input_data: dict[str, Any] | str | None = None,
40
+ inputs_dir_path: str | None = None,
41
+ options: dict[str, Any] | None = None,
42
+ ) -> str:
43
+ """
44
+ Execute a local run.
45
+
46
+ This method recreates, partially, what the Nextmv Cloud does in the backend
47
+ when running an application. A run ID is generated, a run directory is
48
+ created, and the input data is recorded. Then, a subprocess is started to
49
+ execute the application run in a detached manner. This means that the
50
+ application run is not waited upon.
51
+
52
+ Parameters
53
+ ----------
54
+ app_id : str
55
+ The ID of the application.
56
+ src : str
57
+ The path to the application source code.
58
+ manifest : Manifest
59
+ The application manifest.
60
+ run_config : dict[str, Any]
61
+ The run configuration.
62
+ name : Optional[str], optional
63
+ The name for the run, by default None.
64
+ description : Optional[str], optional
65
+ The description for the run, by default None.
66
+ input_data : Optional[Union[dict[str, Any], str]], optional
67
+ The input data for the run, by default None. If `inputs_dir_path` is
68
+ provided, this parameter is ignored.
69
+ inputs_dir_path : Optional[str], optional
70
+ The path to the directory containing input files, by default None. If
71
+ provided, this parameter takes precedence over `input_data`.
72
+ options : Optional[dict[str, Any]], optional
73
+ Additional options for the run, by default None.
74
+
75
+ Returns
76
+ -------
77
+ str
78
+ The ID of the created run.
79
+ """
80
+
81
+ # Check for required optional dependencies
82
+ missing_deps = []
83
+ if importlib.util.find_spec("folium") is None:
84
+ missing_deps.append("folium")
85
+ if importlib.util.find_spec("plotly") is None:
86
+ missing_deps.append("plotly")
87
+
88
+ if missing_deps:
89
+ raise ImportError(
90
+ f"{' and '.join(missing_deps)} {'is' if len(missing_deps) == 1 else 'are'} not installed. "
91
+ "Please install optional dependencies with `pip install nextmv[all]`"
92
+ )
93
+
94
+ # Initialize the run: create the ID, dir, and write the input.
95
+ run_id = safe_id("local")
96
+ run_dir = new_run(
97
+ app_id=app_id,
98
+ src=src,
99
+ run_id=run_id,
100
+ run_config=run_config,
101
+ name=name,
102
+ description=description,
103
+ )
104
+ record_input(
105
+ run_dir=run_dir,
106
+ run_id=run_id,
107
+ input_data=input_data,
108
+ inputs_dir_path=inputs_dir_path,
109
+ )
110
+
111
+ # Start the process as a daemon (detached) so we don't wait for it to
112
+ # finish. We send the input via stdin and close it immediately without
113
+ # waiting. We call the `executor.py` script to do the actual execution.
114
+ stdin_input = json.dumps(
115
+ {
116
+ "run_id": run_id,
117
+ "src": os.path.abspath(src),
118
+ "manifest_dict": manifest.to_dict(),
119
+ "run_dir": os.path.abspath(run_dir),
120
+ "run_config": run_config,
121
+ "input_data": input_data,
122
+ "inputs_dir_path": os.path.abspath(inputs_dir_path) if inputs_dir_path is not None else None,
123
+ "options": options,
124
+ }
125
+ )
126
+ args = [sys.executable, "executor.py"]
127
+ process = subprocess.Popen(
128
+ args,
129
+ env=os.environ,
130
+ text=True,
131
+ stdin=subprocess.PIPE,
132
+ stdout=subprocess.DEVNULL,
133
+ stderr=subprocess.DEVNULL,
134
+ cwd=os.path.dirname(__file__),
135
+ start_new_session=True, # Detach from parent process
136
+ )
137
+ process.stdin.write(stdin_input)
138
+ process.stdin.close()
139
+
140
+ return run_id
141
+
142
+
143
+ def new_run(
144
+ app_id: str,
145
+ src: str,
146
+ run_id: str,
147
+ run_config: dict[str, Any],
148
+ name: str | None = None,
149
+ description: str | None = None,
150
+ ) -> str:
151
+ """
152
+ Initializes a new run.
153
+
154
+ The run information is recorded in a JSON file within the run directory.
155
+
156
+ Parameters
157
+ ----------
158
+ app_id : str
159
+ The ID of the application.
160
+ src : str
161
+ The path to the application source code.
162
+ run_id : str
163
+ The ID of the run.
164
+ run_config : dict[str, Any]
165
+ The run configuration.
166
+ name : Optional[str], optional
167
+ The name for the run, by default None.
168
+ description : Optional[str], optional
169
+ The description for the run, by default None.
170
+
171
+ Returns
172
+ -------
173
+ str
174
+ The path to the new run directory.
175
+ """
176
+
177
+ # First, ensure the runs directory exists.
178
+ runs_dir = os.path.join(src, NEXTMV_DIR, RUNS_KEY)
179
+ os.makedirs(runs_dir, exist_ok=True)
180
+
181
+ # Create a new run directory.
182
+ run_dir = os.path.join(runs_dir, run_id)
183
+ os.makedirs(run_dir, exist_ok=True)
184
+
185
+ # Create the run information file.
186
+ created_at = datetime.now(timezone.utc)
187
+ metadata = Metadata(
188
+ application_id=app_id,
189
+ application_instance_id="",
190
+ application_version_id="",
191
+ created_at=created_at,
192
+ duration=0.0,
193
+ error="",
194
+ input_size=0.0,
195
+ output_size=0.0,
196
+ format=Format(
197
+ format_input=FormatInput(
198
+ input_type=run_config["format"]["input"]["type"],
199
+ ),
200
+ ),
201
+ status_v2=StatusV2.queued,
202
+ )
203
+
204
+ if description is None:
205
+ description = f"Local run created at {created_at.isoformat().replace('+00:00', 'Z')}"
206
+
207
+ if name is None:
208
+ name = f"local run {run_id}"
209
+
210
+ information = RunInformation(
211
+ description=description,
212
+ id=run_id,
213
+ metadata=metadata,
214
+ name=name,
215
+ user_email="",
216
+ )
217
+ with open(os.path.join(run_dir, f"{run_id}.json"), "w") as f:
218
+ json.dump(information.to_dict(), f, indent=2)
219
+
220
+ return run_dir
221
+
222
+
223
+ def record_input(
224
+ run_dir: str,
225
+ run_id: str,
226
+ input_data: dict[str, Any] | str | None = None,
227
+ inputs_dir_path: str | None = None,
228
+ ) -> None:
229
+ """
230
+ Writes the input to the appropriate location.
231
+
232
+ The size of the input is calculated and recorded in the run information.
233
+
234
+ Parameters
235
+ ----------
236
+ run_dir : str
237
+ The path to the run directory.
238
+ run_id : str
239
+ The ID of the run.
240
+ input_data : Optional[Union[dict[str, Any], str]], optional
241
+ The input data for the run, by default None. If `inputs_dir_path` is
242
+ provided, this parameter is ignored.
243
+ inputs_dir_path : Optional[str], optional
244
+ The path to the directory containing input files, by default None. If
245
+ provided, this parameter takes precedence over `input_data`.
246
+ """
247
+
248
+ # Create the inputs directory.
249
+ run_inputs_dir = os.path.join(run_dir, INPUTS_KEY)
250
+ os.makedirs(run_inputs_dir, exist_ok=True)
251
+
252
+ if inputs_dir_path is not None and inputs_dir_path != "":
253
+ # If we specify an inputs directory, we ignore the input_data.
254
+ # Copy all files from inputs_dir_path to run_inputs_dir
255
+ if os.path.exists(inputs_dir_path) and os.path.isdir(inputs_dir_path):
256
+ shutil.copytree(inputs_dir_path, run_inputs_dir, dirs_exist_ok=True)
257
+
258
+ elif isinstance(input_data, dict):
259
+ # If no inputs_dir_path is provided, try a single JSON input.
260
+ with open(os.path.join(run_inputs_dir, DEFAULT_INPUT_JSON_FILE), "w") as f:
261
+ json.dump(input_data, f, indent=2)
262
+
263
+ elif isinstance(input_data, str):
264
+ # If no inputs_dir_path is provided, try a single TEXT input.
265
+ with open(os.path.join(run_inputs_dir, "input"), "w") as f:
266
+ f.write(input_data)
267
+
268
+ else:
269
+ raise ValueError(
270
+ "Invalid input data type: input_data must be a dict or str, or inputs_dir_path must be provided."
271
+ )
272
+
273
+ # Update the input size in the run information file.
274
+ calculate_files_size(run_dir, run_id, run_inputs_dir, metadata_key="input_size")
nextmv/logger.py CHANGED
@@ -1,25 +1,83 @@
1
- """Logger that writes to stderr."""
1
+ """
2
+ Logger module that writes to stderr.
3
+
4
+ This module provides utilities for redirecting standard output to standard error
5
+ and for writing log messages directly to stderr.
6
+
7
+ Functions
8
+ ---------
9
+ redirect_stdout
10
+ Redirect all messages written to stdout to stderr.
11
+ reset_stdout
12
+ Reset stdout to its original value.
13
+ log
14
+ Log a message to stderr.
15
+ """
2
16
 
3
17
  import sys
4
18
 
19
+ # Original stdout reference held when redirection is active
5
20
  __original_stdout = None
21
+ # Flag to track if stdout has been redirected
22
+ __stdout_redirected = False
6
23
 
7
24
 
8
25
  def redirect_stdout() -> None:
9
- """Redirect all messages written to stdout to stderr. When you do not want
10
- to redirect stdout anymore, call `reset_stdout`."""
26
+ """
27
+ Redirect all messages written to stdout to stderr.
28
+
29
+ You can import the `redirect_stdout` function directly from `nextmv`:
11
30
 
12
- global __original_stdout
31
+ ```python
32
+ from nextmv import redirect_stdout
33
+ ```
34
+
35
+ This function captures the current sys.stdout and replaces it with sys.stderr.
36
+ When redirection is no longer needed, call `reset_stdout()` to restore the
37
+ original stdout.
38
+
39
+ Examples
40
+ --------
41
+ >>> redirect_stdout()
42
+ >>> print("This will go to stderr")
43
+ >>> reset_stdout()
44
+ >>> print("This will go to stdout")
45
+ """
46
+
47
+ global __original_stdout, __stdout_redirected
48
+ if __stdout_redirected:
49
+ return
50
+ __stdout_redirected = True
13
51
 
14
52
  __original_stdout = sys.stdout
15
53
  sys.stdout = sys.stderr
16
54
 
17
55
 
18
56
  def reset_stdout() -> None:
19
- """Reset stdout to its original value. This function should always be
20
- called after `redirect_stdout` to avoid unexpected behavior."""
57
+ """
58
+ Reset stdout to its original value.
59
+
60
+ You can import the `reset_stdout` function directly from `nextmv`:
61
+
62
+ ```python
63
+ from nextmv import reset_stdout
64
+ ```
21
65
 
22
- global __original_stdout
66
+ This function should always be called after `redirect_stdout()` to avoid
67
+ unexpected behavior. It restores the original stdout that was captured
68
+ during redirection.
69
+
70
+ Examples
71
+ --------
72
+ >>> redirect_stdout()
73
+ >>> print("This will go to stderr")
74
+ >>> reset_stdout()
75
+ >>> print("This will go to stdout")
76
+ """
77
+ global __original_stdout, __stdout_redirected
78
+ if not __stdout_redirected:
79
+ return
80
+ __stdout_redirected = False
23
81
 
24
82
  if __original_stdout is None:
25
83
  sys.stdout = sys.__stdout__
@@ -33,8 +91,21 @@ def log(message: str) -> None:
33
91
  """
34
92
  Log a message to stderr.
35
93
 
36
- Args:
37
- message: The message to log.
94
+ You can import the `log` function directly from `nextmv`:
95
+
96
+ ```python
97
+ from nextmv import log
98
+ ```
99
+
100
+ Parameters
101
+ ----------
102
+ message : str
103
+ The message to log.
104
+
105
+ Examples
106
+ --------
107
+ >>> log("An error occurred")
108
+ An error occurred
38
109
  """
39
110
 
40
111
  print(message, file=sys.stderr)