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
@@ -0,0 +1,323 @@
1
+ """
2
+ GeoJSON visualization handler module.
3
+
4
+ This module provides functionality to handle GeoJSON visualizations by converting
5
+ them to interactive HTML maps using Folium. It supports various GeoJSON formats
6
+ and automatically calculates optimal map positioning and zoom levels.
7
+
8
+ Functions
9
+ ---------
10
+ handle_geojson_visual
11
+ Handle and write GeoJSON visuals to HTML files.
12
+ extract_coordinates
13
+ Recursively extract coordinates from nested coordinate structures.
14
+ calculate_map_center_and_zoom
15
+ Calculate the optimal center and zoom level for a GeoJSON map.
16
+ extract_geojson_fields
17
+ Extract available fields for tooltip and popup from GeoJSON data.
18
+ create_geojson_map
19
+ Create a folium map with GeoJSON data.
20
+ """
21
+
22
+ import json
23
+ import os
24
+
25
+ import folium
26
+
27
+ from nextmv.logger import log
28
+ from nextmv.output import Asset
29
+
30
+
31
+ def handle_geojson_visual(asset: Asset, visuals_dir: str) -> None:
32
+ """
33
+ Handle and write GeoJSON visuals to HTML files.
34
+
35
+ This function processes GeoJSON visualization assets and converts them to
36
+ interactive HTML maps using Folium. It handles multiple content formats
37
+ including dictionaries, lists, and JSON strings. Each visualization is
38
+ converted to a map with appropriate positioning and saved as an HTML file.
39
+
40
+ Parameters
41
+ ----------
42
+ asset : Asset
43
+ The asset containing the GeoJSON visualization data. The content can be
44
+ a dictionary (single GeoJSON), a list (multiple GeoJSONs), or a JSON
45
+ string representation.
46
+ visuals_dir : str
47
+ The directory path where the HTML files will be written.
48
+
49
+ Notes
50
+ -----
51
+ - For list content, each GeoJSON is saved with an index suffix
52
+ (e.g., "map_0.html", "map_1.html")
53
+ - For dict content, the GeoJSON is saved with the asset label
54
+ (e.g., "map.html")
55
+ - String content is parsed as JSON before processing
56
+ - Invalid JSON strings or unsupported content types are ignored with
57
+ appropriate logging
58
+ """
59
+ if isinstance(asset.content, list):
60
+ for ix, content in enumerate(asset.content):
61
+ if isinstance(content, dict):
62
+ layer_name = f"{asset.visual.label} Layer {ix + 1}"
63
+ m = create_geojson_map(content, layer_name)
64
+ m.save(os.path.join(visuals_dir, f"{asset.visual.label}_{ix}.html"))
65
+ return
66
+
67
+ if isinstance(asset.content, dict):
68
+ layer_name = f"{asset.visual.label} Layer"
69
+ m = create_geojson_map(asset.content, layer_name)
70
+ m.save(os.path.join(visuals_dir, f"{asset.visual.label}.html"))
71
+ return
72
+
73
+ if isinstance(asset.content, str):
74
+ try:
75
+ geojson_data = json.loads(asset.content)
76
+ layer_name = f"{asset.visual.label} Layer"
77
+ m = create_geojson_map(geojson_data, layer_name)
78
+ m.save(os.path.join(visuals_dir, f"{asset.visual.label}.html"))
79
+ except json.JSONDecodeError:
80
+ log(f"Warning: Could not parse GeoJSON string content for {asset.visual.label}")
81
+ return
82
+
83
+ # If there is a different content type for geojson visuals, we ignore it for now
84
+
85
+
86
+ def extract_coordinates(coords, all_coords) -> None:
87
+ """
88
+ Recursively extract coordinates from nested coordinate structures.
89
+
90
+ This function traverses nested coordinate structures commonly found in
91
+ GeoJSON geometries and extracts all coordinate pairs. It handles various
92
+ geometry types by recursively processing nested arrays until it finds
93
+ coordinate pairs in [longitude, latitude] format.
94
+
95
+ Parameters
96
+ ----------
97
+ coords : list or tuple
98
+ The coordinate structure to extract from. Can be a nested list/tuple
99
+ containing coordinate pairs or other nested structures.
100
+ all_coords : list
101
+ A list to accumulate all extracted coordinate pairs. This list is
102
+ modified in-place to store [longitude, latitude] pairs.
103
+
104
+ Notes
105
+ -----
106
+ - Coordinate pairs are identified as lists/tuples with exactly 2 numeric
107
+ elements
108
+ - The function expects coordinates in [longitude, latitude] format as per
109
+ GeoJSON specification
110
+ - Nested structures are recursively processed to handle complex geometries
111
+ like Polygons and MultiPolygons
112
+ """
113
+ if isinstance(coords, list):
114
+ if len(coords) == 2 and isinstance(coords[0], (int, float)) and isinstance(coords[1], (int, float)):
115
+ # This is a coordinate pair [lon, lat]
116
+ all_coords.append(coords)
117
+ else:
118
+ # This is a nested structure, recurse
119
+ for coord in coords:
120
+ extract_coordinates(coord, all_coords)
121
+
122
+
123
+ def calculate_map_center_and_zoom(geojson_data: dict) -> tuple[float, float, int]:
124
+ """
125
+ Calculate the optimal center and zoom level for a GeoJSON map.
126
+
127
+ This function analyzes the geographic extent of GeoJSON features to
128
+ determine the best map center point and zoom level for visualization.
129
+ It extracts all coordinates from the features, calculates the centroid,
130
+ and determines an appropriate zoom level based on the data's geographic
131
+ spread.
132
+
133
+ Parameters
134
+ ----------
135
+ geojson_data : dict
136
+ A GeoJSON object containing features with geometric data. Should
137
+ follow the GeoJSON specification with a "features" key containing
138
+ an array of feature objects.
139
+
140
+ Returns
141
+ -------
142
+ tuple[float, float, int]
143
+ A tuple containing (center_latitude, center_longitude, zoom_level).
144
+ - center_latitude : float
145
+ The latitude coordinate for the map center
146
+ - center_longitude : float
147
+ The longitude coordinate for the map center
148
+ - zoom_level : int
149
+ The recommended zoom level (typically 4-12)
150
+
151
+ Notes
152
+ -----
153
+ - Default center is New York City (40.7128, -74.0060) with zoom level 12
154
+ - Zoom levels are calculated based on coordinate range:
155
+ - Range > 10 degrees: zoom level 4 (continental view)
156
+ - Range > 1 degree: zoom level 8 (regional view)
157
+ - Range > 0.1 degree: zoom level 10 (city view)
158
+ - Smaller ranges: zoom level 12 (neighborhood view)
159
+ - Falls back to defaults if no valid coordinates are found or errors occur
160
+ """
161
+ default_lat, default_lon, default_zoom = 40.7128, -74.0060, 12
162
+
163
+ try:
164
+ if "features" not in geojson_data or not geojson_data["features"]:
165
+ return default_lat, default_lon, default_zoom
166
+
167
+ # Calculate bounds from all features
168
+ all_coords = []
169
+ for feature in geojson_data["features"]:
170
+ if feature.get("geometry", {}).get("coordinates"):
171
+ coords = feature["geometry"]["coordinates"]
172
+ extract_coordinates(coords, all_coords)
173
+
174
+ if not all_coords:
175
+ return default_lat, default_lon, default_zoom
176
+
177
+ lats = [coord[1] for coord in all_coords]
178
+ lons = [coord[0] for coord in all_coords]
179
+ center_lat = sum(lats) / len(lats)
180
+ center_lon = sum(lons) / len(lons)
181
+
182
+ # Adjust zoom based on coordinate spread
183
+ lat_range = max(lats) - min(lats)
184
+ lon_range = max(lons) - min(lons)
185
+ max_range = max(lat_range, lon_range)
186
+
187
+ if max_range > 10:
188
+ zoom_level = 4
189
+ elif max_range > 1:
190
+ zoom_level = 8
191
+ elif max_range > 0.1:
192
+ zoom_level = 10
193
+ else:
194
+ zoom_level = default_zoom
195
+
196
+ return center_lat, center_lon, zoom_level
197
+
198
+ except (KeyError, TypeError, ValueError, IndexError) as e:
199
+ log(f"Warning: Error calculating map center and zoom from GeoJSON data: {e}")
200
+ return default_lat, default_lon, default_zoom
201
+ except Exception as e:
202
+ log(f"Warning: Unexpected error calculating map center and zoom: {e}")
203
+ return default_lat, default_lon, default_zoom
204
+
205
+
206
+ def extract_geojson_fields(geojson_data: dict) -> tuple[list[str], list[str]]:
207
+ """
208
+ Extract available fields for tooltip and popup from GeoJSON data.
209
+
210
+ This function analyzes the properties of GeoJSON features to identify
211
+ suitable fields for displaying in map tooltips and popups. It prioritizes
212
+ common field names and limits the number of fields to maintain usability.
213
+
214
+ Parameters
215
+ ----------
216
+ geojson_data : dict
217
+ A GeoJSON object containing features with properties. Should follow
218
+ the GeoJSON specification with features containing properties objects.
219
+
220
+ Returns
221
+ -------
222
+ tuple[list[str], list[str]]
223
+ A tuple containing (tooltip_fields, popup_fields).
224
+ - tooltip_fields : list[str]
225
+ List of field names suitable for tooltips (max 3 fields)
226
+ - popup_fields : list[str]
227
+ List of field names suitable for popups (max 5 fields)
228
+
229
+ Notes
230
+ -----
231
+ - Prioritizes common field names: "name", "title", "label", "id",
232
+ "popupContent", "description"
233
+ - Tooltip fields are limited to 3 to prevent overcrowding
234
+ - Popup fields are limited to 5 to maintain readability
235
+ - Returns empty lists if no features or properties are found
236
+ - Gracefully handles malformed GeoJSON data by returning empty lists
237
+ """
238
+ tooltip_fields = []
239
+ popup_fields = []
240
+
241
+ try:
242
+ if "features" not in geojson_data or not geojson_data["features"]:
243
+ return tooltip_fields, popup_fields
244
+
245
+ # Get fields from the first feature's properties
246
+ first_feature = geojson_data["features"][0]
247
+ if "properties" not in first_feature or not first_feature["properties"]:
248
+ return tooltip_fields, popup_fields
249
+
250
+ available_fields = list(first_feature["properties"].keys())
251
+ # Prioritize common field names for tooltip/popup
252
+ priority_fields = ["name", "title", "label", "id", "popupContent", "description"]
253
+
254
+ for field in priority_fields:
255
+ if field in available_fields:
256
+ tooltip_fields.append(field)
257
+ popup_fields.append(field)
258
+
259
+ # Add remaining fields up to a reasonable limit
260
+ for field in available_fields:
261
+ if field not in tooltip_fields and len(tooltip_fields) < 3:
262
+ tooltip_fields.append(field)
263
+ if field not in popup_fields and len(popup_fields) < 5:
264
+ popup_fields.append(field)
265
+
266
+ except (KeyError, TypeError, IndexError) as e:
267
+ log(f"Warning: Error extracting GeoJSON fields: {e}")
268
+ except Exception as e:
269
+ log(f"Warning: Unexpected error extracting GeoJSON fields: {e}")
270
+
271
+ return tooltip_fields, popup_fields
272
+
273
+
274
+ def create_geojson_map(geojson_data: dict, layer_name: str = "GeoJSON Layer") -> folium.Map:
275
+ """
276
+ Create a folium map with GeoJSON data.
277
+
278
+ This function creates an interactive map using Folium with the provided
279
+ GeoJSON data. It automatically calculates the optimal center point and
280
+ zoom level, extracts relevant fields for tooltips and popups, and
281
+ configures the map with appropriate interactive features.
282
+
283
+ Parameters
284
+ ----------
285
+ geojson_data : dict
286
+ A GeoJSON object containing the geographic data to display. Should
287
+ follow the GeoJSON specification with features and geometries.
288
+ layer_name : str, optional
289
+ The name to assign to the GeoJSON layer in the map, by default
290
+ "GeoJSON Layer". This name appears in the layer control widget.
291
+
292
+ Returns
293
+ -------
294
+ folium.Map
295
+ A configured Folium map object with the GeoJSON data added as a layer.
296
+ The map includes tooltips, popups, and layer controls when applicable.
297
+
298
+ Notes
299
+ -----
300
+ - Map center and zoom are automatically calculated based on the data extent
301
+ - Tooltips are added if suitable fields are found in feature properties
302
+ - Popups are added if suitable fields are found in feature properties
303
+ - Layer control is always added to allow toggling of the GeoJSON layer
304
+ - The map uses default Folium styling and can be further customized
305
+ """
306
+ center_lat, center_lon, zoom_level = calculate_map_center_and_zoom(geojson_data)
307
+ tooltip_fields, popup_fields = extract_geojson_fields(geojson_data)
308
+
309
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom_level)
310
+
311
+ # Create GeoJson layer with dynamic tooltip and popup configuration
312
+ geojson_kwargs = {"name": layer_name}
313
+
314
+ if tooltip_fields:
315
+ geojson_kwargs["tooltip"] = folium.GeoJsonTooltip(fields=tooltip_fields)
316
+
317
+ if popup_fields:
318
+ geojson_kwargs["popup"] = folium.GeoJsonPopup(fields=popup_fields)
319
+
320
+ folium.GeoJson(geojson_data, **geojson_kwargs).add_to(m)
321
+ folium.LayerControl().add_to(m)
322
+
323
+ return m
nextmv/local/local.py ADDED
@@ -0,0 +1,97 @@
1
+ """
2
+ Local module to hold convenience functions used in the `local` package.
3
+
4
+ Functions
5
+ ----------
6
+ calculate_files_size
7
+ Function to calculate the total size of files in a directory.
8
+
9
+ Attributes
10
+ ----------
11
+ OUTPUT_KEY : str
12
+ Output key constant used for identifying output in the run output.
13
+ LOGS_KEY : str
14
+ Logs key constant used for identifying logs in the run output.
15
+ LOGS_FILE : str
16
+ Constant used for identifying the file used for logging.
17
+ DEFAULT_OUTPUT_JSON_FILE : str
18
+ Constant for the default output JSON file name.
19
+ RUNS_KEY : str
20
+ Runs key constant used for identifying the runs directory in the nextmv
21
+ location.
22
+ NEXTMV_DIR : str
23
+ Constant for the Nextmv directory name.
24
+ DEFAULT_INPUT_JSON_FILE : str
25
+ Constant for the default input JSON file name.
26
+ """
27
+
28
+ import json
29
+ import os
30
+
31
+ OUTPUT_KEY = "output"
32
+ """
33
+ Output key constant used for identifying output in the run output.
34
+ """
35
+ LOGS_KEY = "logs"
36
+ """
37
+ Logs key constant used for identifying logs in the run output.
38
+ """
39
+ LOGS_FILE = "logs.log"
40
+ """
41
+ Constant used for identifying the file used for logging.
42
+ """
43
+ DEFAULT_OUTPUT_JSON_FILE = "solution.json"
44
+ """
45
+ Constant for the default output JSON file name.
46
+ """
47
+ RUNS_KEY = "runs"
48
+ """
49
+ Runs key constant used for identifying the runs directory in the nextmv
50
+ location.
51
+ """
52
+ NEXTMV_DIR = ".nextmv"
53
+ """
54
+ Constant for the Nextmv directory name.
55
+ """
56
+ DEFAULT_INPUT_JSON_FILE = "input.json"
57
+ """
58
+ Constant for the default input JSON file name.
59
+ """
60
+
61
+
62
+ def calculate_files_size(run_dir: str, run_id: str, dir_path: str, metadata_key: str) -> None:
63
+ """
64
+ Calculates the total size of the files in a directory, in bytes.
65
+
66
+ The calculated size is stored in the run information metadata under the
67
+ specified key.
68
+
69
+ Parameters
70
+ ----------
71
+ run_dir : str
72
+ The path to the run directory.
73
+ run_id : str
74
+ The ID of the run.
75
+ dir_path : str
76
+ The path to the directory whose size is to be calculated.
77
+ metadata_key : str
78
+ The key under which to store the calculated size in the run information
79
+ metadata.
80
+ """
81
+
82
+ total_size = 0
83
+ for dirpath, _, filenames in os.walk(dir_path):
84
+ for f in filenames:
85
+ fp = os.path.join(dirpath, f)
86
+ # Skip if it is a symbolic link
87
+ if os.path.islink(fp):
88
+ continue
89
+ total_size += os.path.getsize(fp)
90
+
91
+ info_file = os.path.join(run_dir, f"{run_id}.json")
92
+ with open(info_file, "r+") as f:
93
+ info = json.load(f)
94
+ info["metadata"][metadata_key] = total_size
95
+ f.seek(0)
96
+ json.dump(info, f, indent=2)
97
+ f.truncate()
@@ -0,0 +1,61 @@
1
+ """
2
+ Plotly visualization handler module.
3
+
4
+ This module provides functionality to handle Plotly visualizations by converting
5
+ them to HTML files for local run processing.
6
+
7
+ Functions
8
+ ---------
9
+ handle_plotly_visual
10
+ Handle and write Plotly visuals to HTML files.
11
+ """
12
+
13
+ import json
14
+ import os
15
+
16
+ import plotly.io as pio
17
+
18
+ from nextmv.output import Asset
19
+
20
+
21
+ def handle_plotly_visual(asset: Asset, visuals_dir: str) -> None:
22
+ """
23
+ Handle and write Plotly visuals to HTML files.
24
+
25
+ This function processes Plotly visualization assets and converts them to
26
+ HTML files. It handles both single visualizations (dict content) and
27
+ multiple visualizations (list content). Each visualization is converted
28
+ from JSON format to a Plotly figure and then saved as an HTML file.
29
+
30
+ Parameters
31
+ ----------
32
+ asset : Asset
33
+ The asset containing the Plotly visualization data. The content can be
34
+ either a dictionary (single visualization) or a list (multiple
35
+ visualizations).
36
+ visuals_dir : str
37
+ The directory path where the HTML files will be written.
38
+
39
+ Notes
40
+ -----
41
+ - For list content, each visualization is saved with an index suffix
42
+ (e.g., "chart_0.html", "chart_1.html")
43
+ - For dict content, the visualization is saved with the asset label
44
+ (e.g., "chart.html")
45
+ - Content types other than dict or list are currently ignored
46
+ """
47
+ if isinstance(asset.content, list):
48
+ for ix, content in enumerate(asset.content):
49
+ fig = pio.from_json(json.dumps(content))
50
+ fig.write_html(os.path.join(visuals_dir, f"{asset.visual.label}_{ix}.html"))
51
+
52
+ return
53
+
54
+ if isinstance(asset.content, dict):
55
+ fig = pio.from_json(json.dumps(asset.content))
56
+ fig.write_html(os.path.join(visuals_dir, f"{asset.visual.label}.html"))
57
+
58
+ return
59
+
60
+ # If there is a different content type for plotly visuals, we ignore it for
61
+ # now.