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.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +8 -13
- nextmv/__init__.py +53 -0
- nextmv/_serialization.py +96 -0
- nextmv/base_model.py +54 -9
- nextmv/cli/CONTRIBUTING.md +511 -0
- nextmv/cli/__init__.py +0 -0
- nextmv/cli/cloud/__init__.py +47 -0
- nextmv/cli/cloud/acceptance/__init__.py +27 -0
- nextmv/cli/cloud/acceptance/create.py +393 -0
- nextmv/cli/cloud/acceptance/delete.py +68 -0
- nextmv/cli/cloud/acceptance/get.py +104 -0
- nextmv/cli/cloud/acceptance/list.py +62 -0
- nextmv/cli/cloud/acceptance/update.py +95 -0
- nextmv/cli/cloud/account/__init__.py +28 -0
- nextmv/cli/cloud/account/create.py +83 -0
- nextmv/cli/cloud/account/delete.py +60 -0
- nextmv/cli/cloud/account/get.py +66 -0
- nextmv/cli/cloud/account/update.py +70 -0
- nextmv/cli/cloud/app/__init__.py +35 -0
- nextmv/cli/cloud/app/create.py +141 -0
- nextmv/cli/cloud/app/delete.py +58 -0
- nextmv/cli/cloud/app/exists.py +44 -0
- nextmv/cli/cloud/app/get.py +66 -0
- nextmv/cli/cloud/app/list.py +61 -0
- nextmv/cli/cloud/app/push.py +137 -0
- nextmv/cli/cloud/app/update.py +124 -0
- nextmv/cli/cloud/batch/__init__.py +29 -0
- nextmv/cli/cloud/batch/create.py +454 -0
- nextmv/cli/cloud/batch/delete.py +68 -0
- nextmv/cli/cloud/batch/get.py +104 -0
- nextmv/cli/cloud/batch/list.py +63 -0
- nextmv/cli/cloud/batch/metadata.py +66 -0
- nextmv/cli/cloud/batch/update.py +95 -0
- nextmv/cli/cloud/data/__init__.py +26 -0
- nextmv/cli/cloud/data/upload.py +162 -0
- nextmv/cli/cloud/ensemble/__init__.py +31 -0
- nextmv/cli/cloud/ensemble/create.py +414 -0
- nextmv/cli/cloud/ensemble/delete.py +67 -0
- nextmv/cli/cloud/ensemble/get.py +65 -0
- nextmv/cli/cloud/ensemble/update.py +103 -0
- nextmv/cli/cloud/input_set/__init__.py +30 -0
- nextmv/cli/cloud/input_set/create.py +170 -0
- nextmv/cli/cloud/input_set/get.py +63 -0
- nextmv/cli/cloud/input_set/list.py +63 -0
- nextmv/cli/cloud/input_set/update.py +123 -0
- nextmv/cli/cloud/instance/__init__.py +35 -0
- nextmv/cli/cloud/instance/create.py +290 -0
- nextmv/cli/cloud/instance/delete.py +62 -0
- nextmv/cli/cloud/instance/exists.py +39 -0
- nextmv/cli/cloud/instance/get.py +62 -0
- nextmv/cli/cloud/instance/list.py +60 -0
- nextmv/cli/cloud/instance/update.py +216 -0
- nextmv/cli/cloud/managed_input/__init__.py +31 -0
- nextmv/cli/cloud/managed_input/create.py +146 -0
- nextmv/cli/cloud/managed_input/delete.py +65 -0
- nextmv/cli/cloud/managed_input/get.py +63 -0
- nextmv/cli/cloud/managed_input/list.py +60 -0
- nextmv/cli/cloud/managed_input/update.py +97 -0
- nextmv/cli/cloud/run/__init__.py +37 -0
- nextmv/cli/cloud/run/cancel.py +37 -0
- nextmv/cli/cloud/run/create.py +530 -0
- nextmv/cli/cloud/run/get.py +199 -0
- nextmv/cli/cloud/run/input.py +86 -0
- nextmv/cli/cloud/run/list.py +80 -0
- nextmv/cli/cloud/run/logs.py +167 -0
- nextmv/cli/cloud/run/metadata.py +67 -0
- nextmv/cli/cloud/run/track.py +501 -0
- nextmv/cli/cloud/scenario/__init__.py +29 -0
- nextmv/cli/cloud/scenario/create.py +451 -0
- nextmv/cli/cloud/scenario/delete.py +65 -0
- nextmv/cli/cloud/scenario/get.py +102 -0
- nextmv/cli/cloud/scenario/list.py +63 -0
- nextmv/cli/cloud/scenario/metadata.py +67 -0
- nextmv/cli/cloud/scenario/update.py +93 -0
- nextmv/cli/cloud/secrets/__init__.py +33 -0
- nextmv/cli/cloud/secrets/create.py +206 -0
- nextmv/cli/cloud/secrets/delete.py +67 -0
- nextmv/cli/cloud/secrets/get.py +66 -0
- nextmv/cli/cloud/secrets/list.py +60 -0
- nextmv/cli/cloud/secrets/update.py +147 -0
- nextmv/cli/cloud/shadow/__init__.py +33 -0
- nextmv/cli/cloud/shadow/create.py +184 -0
- nextmv/cli/cloud/shadow/delete.py +68 -0
- nextmv/cli/cloud/shadow/get.py +61 -0
- nextmv/cli/cloud/shadow/list.py +63 -0
- nextmv/cli/cloud/shadow/metadata.py +66 -0
- nextmv/cli/cloud/shadow/start.py +43 -0
- nextmv/cli/cloud/shadow/stop.py +43 -0
- nextmv/cli/cloud/shadow/update.py +95 -0
- nextmv/cli/cloud/upload/__init__.py +22 -0
- nextmv/cli/cloud/upload/create.py +39 -0
- nextmv/cli/cloud/version/__init__.py +33 -0
- nextmv/cli/cloud/version/create.py +97 -0
- nextmv/cli/cloud/version/delete.py +62 -0
- nextmv/cli/cloud/version/exists.py +39 -0
- nextmv/cli/cloud/version/get.py +62 -0
- nextmv/cli/cloud/version/list.py +60 -0
- nextmv/cli/cloud/version/update.py +92 -0
- nextmv/cli/community/__init__.py +24 -0
- nextmv/cli/community/clone.py +270 -0
- nextmv/cli/community/list.py +265 -0
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +195 -0
- nextmv/cli/configuration/create.py +94 -0
- nextmv/cli/configuration/delete.py +67 -0
- nextmv/cli/configuration/list.py +77 -0
- nextmv/cli/main.py +188 -0
- nextmv/cli/message.py +153 -0
- nextmv/cli/options.py +206 -0
- nextmv/cli/version.py +38 -0
- nextmv/cloud/__init__.py +71 -17
- nextmv/cloud/acceptance_test.py +757 -51
- nextmv/cloud/account.py +406 -17
- nextmv/cloud/application/__init__.py +957 -0
- nextmv/cloud/application/_acceptance.py +419 -0
- nextmv/cloud/application/_batch_scenario.py +860 -0
- nextmv/cloud/application/_ensemble.py +251 -0
- nextmv/cloud/application/_input_set.py +227 -0
- nextmv/cloud/application/_instance.py +289 -0
- nextmv/cloud/application/_managed_input.py +227 -0
- nextmv/cloud/application/_run.py +1393 -0
- nextmv/cloud/application/_secrets.py +294 -0
- nextmv/cloud/application/_shadow.py +314 -0
- nextmv/cloud/application/_utils.py +54 -0
- nextmv/cloud/application/_version.py +303 -0
- nextmv/cloud/assets.py +48 -0
- nextmv/cloud/batch_experiment.py +294 -33
- nextmv/cloud/client.py +307 -66
- nextmv/cloud/ensemble.py +247 -0
- nextmv/cloud/input_set.py +120 -2
- nextmv/cloud/instance.py +133 -8
- nextmv/cloud/integration.py +533 -0
- nextmv/cloud/package.py +168 -53
- nextmv/cloud/scenario.py +410 -0
- nextmv/cloud/secrets.py +234 -0
- nextmv/cloud/shadow.py +190 -0
- nextmv/cloud/url.py +73 -0
- nextmv/cloud/version.py +132 -4
- nextmv/default_app/.gitignore +1 -0
- nextmv/default_app/README.md +32 -0
- nextmv/default_app/app.yaml +12 -0
- nextmv/default_app/input.json +5 -0
- nextmv/default_app/main.py +37 -0
- nextmv/default_app/requirements.txt +2 -0
- nextmv/default_app/src/__init__.py +0 -0
- nextmv/default_app/src/visuals.py +36 -0
- nextmv/deprecated.py +47 -0
- nextmv/input.py +861 -90
- nextmv/local/__init__.py +5 -0
- nextmv/local/application.py +1251 -0
- nextmv/local/executor.py +1042 -0
- nextmv/local/geojson_handler.py +323 -0
- nextmv/local/local.py +97 -0
- nextmv/local/plotly_handler.py +61 -0
- nextmv/local/runner.py +274 -0
- nextmv/logger.py +80 -9
- nextmv/manifest.py +1466 -0
- nextmv/model.py +241 -66
- nextmv/options.py +708 -115
- nextmv/output.py +1301 -274
- nextmv/polling.py +325 -0
- nextmv/run.py +1702 -0
- nextmv/safe.py +145 -0
- nextmv/status.py +122 -0
- nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
- nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
- {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
- nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
- nextmv/cloud/application.py +0 -1405
- nextmv/cloud/manifest.py +0 -234
- nextmv/cloud/status.py +0 -29
- nextmv-0.18.0.dist-info/METADATA +0 -770
- nextmv-0.18.0.dist-info/RECORD +0 -25
- {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
|
-
"""
|
|
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
|
-
"""
|
|
10
|
-
to
|
|
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
|
-
|
|
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
|
-
"""
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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)
|