nextmv 0.28.3.dev1__tar.gz → 0.28.3.dev2__tar.gz

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 (63) hide show
  1. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/PKG-INFO +1 -1
  2. nextmv-0.28.3.dev2/nextmv/__about__.py +1 -0
  3. nextmv-0.28.3.dev2/nextmv/_serialization.py +96 -0
  4. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/application.py +33 -12
  5. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/client.py +38 -12
  6. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/run.py +3 -3
  7. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/input.py +2 -1
  8. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/output.py +10 -52
  9. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/cloud/test_application.py +30 -4
  10. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/test_output.py +1 -9
  11. nextmv-0.28.3.dev2/tests/test_serialization.py +96 -0
  12. nextmv-0.28.3.dev1/nextmv/__about__.py +0 -1
  13. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/.gitignore +0 -0
  14. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/LICENSE +0 -0
  15. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/README.md +0 -0
  16. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/__entrypoint__.py +0 -0
  17. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/__init__.py +0 -0
  18. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/base_model.py +0 -0
  19. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/__init__.py +0 -0
  20. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/acceptance_test.py +0 -0
  21. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/account.py +0 -0
  22. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/batch_experiment.py +0 -0
  23. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/input_set.py +0 -0
  24. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/instance.py +0 -0
  25. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/manifest.py +0 -0
  26. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/package.py +0 -0
  27. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/safe.py +0 -0
  28. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/scenario.py +0 -0
  29. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/secrets.py +0 -0
  30. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/status.py +0 -0
  31. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/cloud/version.py +0 -0
  32. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/deprecated.py +0 -0
  33. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/logger.py +0 -0
  34. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/model.py +0 -0
  35. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/nextmv/options.py +0 -0
  36. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/pyproject.toml +0 -0
  37. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/requirements.txt +0 -0
  38. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/__init__.py +0 -0
  39. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/cloud/__init__.py +0 -0
  40. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/cloud/app.yaml +0 -0
  41. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/cloud/test_client.py +0 -0
  42. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/cloud/test_manifest.py +0 -0
  43. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/cloud/test_package.py +0 -0
  44. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/cloud/test_run.py +0 -0
  45. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/cloud/test_safe_name_id.py +0 -0
  46. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/cloud/test_scenario.py +0 -0
  47. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/scripts/__init__.py +0 -0
  48. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/scripts/options1.py +0 -0
  49. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/scripts/options2.py +0 -0
  50. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/scripts/options3.py +0 -0
  51. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/scripts/options4.py +0 -0
  52. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/scripts/options5.py +0 -0
  53. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/scripts/options6.py +0 -0
  54. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/scripts/options7.py +0 -0
  55. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/scripts/options_deprecated.py +0 -0
  56. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/test_base_model.py +0 -0
  57. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/test_entrypoint/__init__.py +0 -0
  58. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/test_entrypoint/test_entrypoint.py +0 -0
  59. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/test_input.py +0 -0
  60. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/test_logger.py +0 -0
  61. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/test_model.py +0 -0
  62. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/test_options.py +0 -0
  63. {nextmv-0.28.3.dev1 → nextmv-0.28.3.dev2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 0.28.3.dev1
3
+ Version: 0.28.3.dev2
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://www.nextmv.io/docs/python-sdks/nextmv/installation
@@ -0,0 +1 @@
1
+ __version__ = "v0.28.3.dev2"
@@ -0,0 +1,96 @@
1
+ import datetime
2
+ import json
3
+ from typing import Any, Union
4
+
5
+
6
+ def deflated_serialize_json(obj: Union[dict, list], json_configurations: dict[str, Any] = None) -> str:
7
+ """
8
+ Serialize a Python object (dict or list) to a JSON string with default configuration for a deflated format.
9
+
10
+ Parameters
11
+ ----------
12
+ obj : Union[dict, list]
13
+ The Python object to serialize.
14
+ json_configurations : dict, optional
15
+ Additional configurations for JSON serialization. This allows customization
16
+ of the Python `json.dumps` function. You can specify parameters like `indent`
17
+ for pretty printing or `default` for custom serialization functions.
18
+
19
+ Returns
20
+ -------
21
+ str
22
+ A JSON string representation of the object.
23
+ """
24
+
25
+ # Apply a default configuration if not provided targeting a deflated format
26
+ json_configurations = json_configurations or {}
27
+ if "default" not in json_configurations:
28
+ json_configurations["default"] = _custom_serial
29
+ if "separators" not in json_configurations:
30
+ json_configurations["separators"] = (",", ":")
31
+
32
+ return json.dumps(
33
+ obj,
34
+ **json_configurations,
35
+ )
36
+
37
+
38
+ def serialize_json(obj: Union[dict, list], json_configurations: dict[str, Any] = None) -> str:
39
+ """
40
+ Serialize a Python object (dict or list) to a JSON string.
41
+
42
+ Parameters
43
+ ----------
44
+ obj : Union[dict, list]
45
+ The Python object to serialize.
46
+ json_configurations : dict, optional
47
+ Additional configurations for JSON serialization. This allows customization
48
+ of the Python `json.dumps` function. You can specify parameters like `indent`
49
+ for pretty printing or `default` for custom serialization functions.
50
+
51
+ Returns
52
+ -------
53
+ str
54
+ A JSON string representation of the object.
55
+ """
56
+
57
+ # Apply some default configuration if not provided
58
+ json_configurations = json_configurations or {}
59
+ if "default" not in json_configurations:
60
+ json_configurations["default"] = _custom_serial
61
+ if "indent" not in json_configurations:
62
+ json_configurations["indent"] = 2
63
+
64
+ return json.dumps(
65
+ obj,
66
+ **json_configurations,
67
+ )
68
+
69
+
70
+ def _custom_serial(obj: Any) -> str:
71
+ """
72
+ JSON serializer for objects not serializable by default json serializer.
73
+
74
+ This function provides custom serialization for datetime objects, converting
75
+ them to ISO format strings.
76
+
77
+ Parameters
78
+ ----------
79
+ obj : Any
80
+ The object to serialize.
81
+
82
+ Returns
83
+ -------
84
+ str
85
+ The serialized representation of the object.
86
+
87
+ Raises
88
+ ------
89
+ TypeError
90
+ If the object type is not supported for serialization.
91
+ """
92
+
93
+ if isinstance(obj, (datetime.datetime, datetime.date)):
94
+ return obj.isoformat()
95
+
96
+ raise TypeError(f"Type {type(obj)} not serializable")
@@ -33,6 +33,7 @@ from typing import Any, Optional, Union
33
33
 
34
34
  import requests
35
35
 
36
+ from nextmv._serialization import deflated_serialize_json
36
37
  from nextmv.base_model import BaseModel
37
38
  from nextmv.cloud import package
38
39
  from nextmv.cloud.acceptance_test import AcceptanceTest, ExperimentStatus, Metric
@@ -184,10 +185,12 @@ class PollingOptions:
184
185
  """
185
186
  max_delay: float = 20
186
187
  """Maximum delay to use between polls, in seconds."""
187
- max_duration: float = 300
188
- """Maximum duration of the polling strategy, in seconds."""
189
- max_tries: int = 100
190
- """Maximum number of tries to use."""
188
+ max_duration: float = -1
189
+ """
190
+ Maximum duration of the polling strategy, in seconds. A negative value means no limit.
191
+ """
192
+ max_tries: int = -1
193
+ """Maximum number of tries to use. A negative value means no limit."""
191
194
  jitter: float = 1
192
195
  """
193
196
  Jitter to use for the polling strategy. A uniform distribution is sampled
@@ -1490,6 +1493,7 @@ class Application:
1490
1493
  configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
1491
1494
  batch_experiment_id: Optional[str] = None,
1492
1495
  external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None,
1496
+ json_configurations: Optional[dict[str, Any]] = None,
1493
1497
  ) -> str:
1494
1498
  """
1495
1499
  Submit an input to start a new run of the application. Returns the
@@ -1538,6 +1542,9 @@ class Application:
1538
1542
  configuration. This is used when the run is an external run. We
1539
1543
  suggest that instead of specifying this parameter, you use the
1540
1544
  `track_run` method of the class.
1545
+ json_configurations: Optional[dict[str, Any]]
1546
+ Optional configurations for JSON serialization. This is used to
1547
+ customize the serialization before data is sent.
1541
1548
 
1542
1549
  Returns
1543
1550
  ----------
@@ -1588,7 +1595,7 @@ class Application:
1588
1595
  if isinstance(v, str):
1589
1596
  options_dict[k] = v
1590
1597
  else:
1591
- options_dict[k] = json.dumps(v)
1598
+ options_dict[k] = deflated_serialize_json(v, json_configurations=json_configurations)
1592
1599
 
1593
1600
  payload = {}
1594
1601
  if upload_id_used:
@@ -1626,6 +1633,7 @@ class Application:
1626
1633
  endpoint=f"{self.endpoint}/runs",
1627
1634
  payload=payload,
1628
1635
  query_params=query_params,
1636
+ json_configurations=json_configurations,
1629
1637
  )
1630
1638
 
1631
1639
  return response.json()["run_id"]
@@ -2791,6 +2799,7 @@ class Application:
2791
2799
  self,
2792
2800
  input: Union[dict[str, Any], str],
2793
2801
  upload_url: UploadURL,
2802
+ json_configurations: Optional[dict[str, Any]] = None,
2794
2803
  ) -> None:
2795
2804
  """
2796
2805
  Upload large input data to the provided upload URL.
@@ -2806,6 +2815,9 @@ class Application:
2806
2815
  converted to JSON, or a pre-formatted JSON string.
2807
2816
  upload_url : UploadURL
2808
2817
  Upload URL object containing the pre-signed URL to use for uploading.
2818
+ json_configurations : Optional[dict[str, Any]], default=None
2819
+ Optional configurations for JSON serialization. If provided, these
2820
+ configurations will be used when serializing the data via `json.dumps`.
2809
2821
 
2810
2822
  Returns
2811
2823
  -------
@@ -2830,7 +2842,7 @@ class Application:
2830
2842
  """
2831
2843
 
2832
2844
  if isinstance(input, dict):
2833
- input = json.dumps(input)
2845
+ input = deflated_serialize_json(input, json_configurations=json_configurations)
2834
2846
 
2835
2847
  self.client.upload_to_presigned_url(
2836
2848
  url=upload_url.upload_url,
@@ -3174,7 +3186,11 @@ class Application:
3174
3186
  raise ValueError(f"Unknown scenario input type: {scenario.scenario_input.scenario_input_type}")
3175
3187
 
3176
3188
 
3177
- def poll(polling_options: PollingOptions, polling_func: Callable[[], tuple[Any, bool]]) -> Any: # noqa: C901
3189
+ def poll( # noqa: C901
3190
+ polling_options: PollingOptions,
3191
+ polling_func: Callable[[], tuple[Any, bool]],
3192
+ __sleep_func: Callable[[float], None] = time.sleep,
3193
+ ) -> Any:
3178
3194
  """
3179
3195
  Poll a function until it succeeds or the polling strategy is exhausted.
3180
3196
 
@@ -3247,14 +3263,20 @@ def poll(polling_options: PollingOptions, polling_func: Callable[[], tuple[Any,
3247
3263
  if polling_options.verbose:
3248
3264
  log(f"polling | sleeping for initial delay: {polling_options.initial_delay}")
3249
3265
 
3250
- time.sleep(polling_options.initial_delay)
3266
+ __sleep_func(polling_options.initial_delay)
3251
3267
 
3252
3268
  start_time = time.time()
3253
3269
  stopped = False
3254
3270
 
3255
3271
  # Begin the polling process.
3256
3272
  max_reached = False
3257
- for ix in range(polling_options.max_tries):
3273
+ ix = 0
3274
+ while True:
3275
+ # Check if we reached the maximum number of tries. Break if so.
3276
+ if ix >= polling_options.max_tries and polling_options.max_tries >= 0:
3277
+ break
3278
+ ix += 1
3279
+
3258
3280
  # Check is we should stop polling according to the stop callback.
3259
3281
  if polling_options.stop is not None and polling_options.stop():
3260
3282
  stopped = True
@@ -3274,13 +3296,12 @@ def poll(polling_options: PollingOptions, polling_func: Callable[[], tuple[Any,
3274
3296
  if polling_options.verbose:
3275
3297
  log(f"polling | elapsed time: {passed}")
3276
3298
 
3277
- if passed >= polling_options.max_duration:
3299
+ if passed >= polling_options.max_duration and polling_options.max_duration >= 0:
3278
3300
  raise TimeoutError(
3279
3301
  f"polling did not succeed after {passed} seconds, exceeds max duration: {polling_options.max_duration}",
3280
3302
  )
3281
3303
 
3282
3304
  # Calculate the delay.
3283
- delay = 0.0
3284
3305
  if max_reached:
3285
3306
  # If we already reached the maximum, we don't want to further calculate the
3286
3307
  # delay to avoid overflows.
@@ -3301,7 +3322,7 @@ def poll(polling_options: PollingOptions, polling_func: Callable[[], tuple[Any,
3301
3322
  if polling_options.verbose:
3302
3323
  log(f"polling | sleeping for duration: {sleep_duration}")
3303
3324
 
3304
- time.sleep(sleep_duration)
3325
+ __sleep_func(sleep_duration)
3305
3326
 
3306
3327
  if stopped:
3307
3328
  log("polling | stop condition met, stopping polling")
@@ -14,7 +14,6 @@ get_size(obj)
14
14
  Finds the size of an object in bytes.
15
15
  """
16
16
 
17
- import json
18
17
  import os
19
18
  from dataclasses import dataclass, field
20
19
  from typing import IO, Any, Optional, Union
@@ -24,6 +23,8 @@ import requests
24
23
  import yaml
25
24
  from requests.adapters import HTTPAdapter, Retry
26
25
 
26
+ from nextmv._serialization import deflated_serialize_json
27
+
27
28
  _MAX_LAMBDA_PAYLOAD_SIZE: int = 500 * 1024 * 1024
28
29
  """int: Maximum size of the payload handled by the Nextmv Cloud API.
29
30
 
@@ -199,6 +200,7 @@ class Client:
199
200
  headers: Optional[dict[str, str]] = None,
200
201
  payload: Optional[dict[str, Any]] = None,
201
202
  query_params: Optional[dict[str, Any]] = None,
203
+ json_configurations: Optional[dict[str, Any]] = None,
202
204
  ) -> requests.Response:
203
205
  """
204
206
  Makes a request to the Nextmv Cloud API.
@@ -221,6 +223,11 @@ class Client:
221
223
  provided.
222
224
  query_params : dict[str, Any], optional
223
225
  Query parameters to append to the request URL.
226
+ json_configurations : dict[str, Any], optional
227
+ Additional configurations for JSON serialization. This allows
228
+ customization of the Python `json.dumps` function, such as
229
+ specifying `indent` for pretty printing or `default` for custom
230
+ serialization functions.
224
231
 
225
232
  Returns
226
233
  -------
@@ -261,16 +268,19 @@ class Client:
261
268
  if payload is not None and data is not None:
262
269
  raise ValueError("cannot use both data and payload")
263
270
 
264
- if payload is not None and get_size(payload) > _MAX_LAMBDA_PAYLOAD_SIZE:
271
+ if (
272
+ payload is not None
273
+ and get_size(payload, json_configurations=json_configurations) > _MAX_LAMBDA_PAYLOAD_SIZE
274
+ ):
265
275
  raise ValueError(
266
- f"payload size of {get_size(payload)} bytes exceeds the maximum "
267
- f"allowed size of {_MAX_LAMBDA_PAYLOAD_SIZE} bytes"
276
+ f"payload size of {get_size(payload, json_configurations=json_configurations)} bytes exceeds "
277
+ + f"the maximum allowed size of {_MAX_LAMBDA_PAYLOAD_SIZE} bytes"
268
278
  )
269
279
 
270
- if data is not None and get_size(data) > _MAX_LAMBDA_PAYLOAD_SIZE:
280
+ if data is not None and get_size(data, json_configurations=json_configurations) > _MAX_LAMBDA_PAYLOAD_SIZE:
271
281
  raise ValueError(
272
- f"data size of {get_size(data)} bytes exceeds the maximum "
273
- f"allowed size of {_MAX_LAMBDA_PAYLOAD_SIZE} bytes"
282
+ f"data size of {get_size(data, json_configurations=json_configurations)} bytes exceeds "
283
+ + f"the maximum allowed size of {_MAX_LAMBDA_PAYLOAD_SIZE} bytes"
274
284
  )
275
285
 
276
286
  session = requests.Session()
@@ -293,7 +303,11 @@ class Client:
293
303
  if data is not None:
294
304
  kwargs["data"] = data
295
305
  if payload is not None:
296
- kwargs["json"] = payload
306
+ if isinstance(payload, (dict, list)):
307
+ data = deflated_serialize_json(payload, json_configurations=json_configurations)
308
+ kwargs["data"] = data
309
+ else:
310
+ raise ValueError("payload must be a dictionary or a list")
297
311
  if query_params is not None:
298
312
  kwargs["params"] = query_params
299
313
 
@@ -308,7 +322,9 @@ class Client:
308
322
 
309
323
  return response
310
324
 
311
- def upload_to_presigned_url(self, data: Union[dict[str, Any], str], url: str) -> None:
325
+ def upload_to_presigned_url(
326
+ self, data: Union[dict[str, Any], str], url: str, json_configurations: Optional[dict[str, Any]] = None
327
+ ) -> None:
312
328
  """
313
329
  Uploads data to a presigned URL.
314
330
 
@@ -323,6 +339,11 @@ class Client:
323
339
  as is.
324
340
  url : str
325
341
  The presigned URL to which the data will be uploaded.
342
+ json_configurations : dict[str, Any], optional
343
+ Additional configurations for JSON serialization. This allows
344
+ customization of the Python `json.dumps` function, such as
345
+ specifying `indent` for pretty printing or `default` for custom
346
+ serialization functions.
326
347
 
327
348
  Raises
328
349
  ------
@@ -341,7 +362,7 @@ class Client:
341
362
 
342
363
  upload_data: Optional[str] = None
343
364
  if isinstance(data, dict):
344
- upload_data = json.dumps(data, separators=(",", ":"))
365
+ upload_data = deflated_serialize_json(data, json_configurations=json_configurations)
345
366
  elif isinstance(data, str):
346
367
  upload_data = data
347
368
  else:
@@ -393,7 +414,7 @@ class Client:
393
414
  }
394
415
 
395
416
 
396
- def get_size(obj: Union[dict[str, Any], IO[bytes], str]) -> int:
417
+ def get_size(obj: Union[dict[str, Any], IO[bytes], str], json_configurations: Optional[dict[str, Any]] = None) -> int:
397
418
  """
398
419
  Finds the size of an object in bytes.
399
420
 
@@ -407,6 +428,11 @@ def get_size(obj: Union[dict[str, Any], IO[bytes], str]) -> int:
407
428
  - If a dict, it's converted to a JSON string.
408
429
  - If a file-like object (e.g., opened file), its size is read.
409
430
  - If a string, its UTF-8 encoded byte length is calculated.
431
+ json_configurations : dict[str, Any], optional
432
+ Additional configurations for JSON serialization. This allows
433
+ customization of the Python `json.dumps` function, such as specifying
434
+ `indent` for pretty printing or `default` for custom serialization
435
+ functions.
410
436
 
411
437
  Returns
412
438
  -------
@@ -436,7 +462,7 @@ def get_size(obj: Union[dict[str, Any], IO[bytes], str]) -> int:
436
462
  """
437
463
 
438
464
  if isinstance(obj, dict):
439
- obj_str = json.dumps(obj, separators=(",", ":"))
465
+ obj_str = deflated_serialize_json(obj, json_configurations=json_configurations)
440
466
  return len(obj_str.encode("utf-8"))
441
467
 
442
468
  elif hasattr(obj, "read"):
@@ -39,7 +39,6 @@ run_duration(start, end)
39
39
  Calculate the duration of a run in milliseconds.
40
40
  """
41
41
 
42
- import json
43
42
  from dataclasses import dataclass
44
43
  from datetime import datetime
45
44
  from enum import Enum
@@ -47,6 +46,7 @@ from typing import Any, Optional, Union
47
46
 
48
47
  from pydantic import AliasChoices, Field
49
48
 
49
+ from nextmv._serialization import serialize_json
50
50
  from nextmv.base_model import BaseModel
51
51
  from nextmv.cloud.status import Status, StatusV2
52
52
  from nextmv.input import Input, InputFormat
@@ -628,7 +628,7 @@ class TrackedRun:
628
628
  raise ValueError("Input.input_format must be JSON.")
629
629
  elif isinstance(self.input, dict):
630
630
  try:
631
- _ = json.dumps(self.input)
631
+ _ = serialize_json(self.input)
632
632
  except (TypeError, OverflowError) as e:
633
633
  raise ValueError("Input is dict[str, Any] but it is not JSON serializable") from e
634
634
 
@@ -637,7 +637,7 @@ class TrackedRun:
637
637
  raise ValueError("Output.output_format must be JSON.")
638
638
  elif isinstance(self.output, dict):
639
639
  try:
640
- _ = json.dumps(self.output)
640
+ _ = serialize_json(self.output)
641
641
  except (TypeError, OverflowError) as e:
642
642
  raise ValueError("Output is dict[str, Any] but it is not JSON serializable") from e
643
643
 
@@ -31,6 +31,7 @@ from dataclasses import dataclass
31
31
  from enum import Enum
32
32
  from typing import Any, Optional, Union
33
33
 
34
+ from nextmv._serialization import serialize_json
34
35
  from nextmv.deprecated import deprecated
35
36
  from nextmv.options import Options
36
37
 
@@ -139,7 +140,7 @@ class Input:
139
140
 
140
141
  if self.input_format == InputFormat.JSON:
141
142
  try:
142
- _ = json.dumps(self.data)
143
+ _ = serialize_json(self.data)
143
144
  except (TypeError, OverflowError) as e:
144
145
  raise ValueError(
145
146
  f"Input has input_format InputFormat.JSON and "
@@ -42,8 +42,6 @@ write
42
42
 
43
43
  import copy
44
44
  import csv
45
- import datetime
46
- import json
47
45
  import os
48
46
  import sys
49
47
  from dataclasses import dataclass
@@ -52,6 +50,7 @@ from typing import Any, Optional, Union
52
50
 
53
51
  from pydantic import AliasChoices, Field
54
52
 
53
+ from nextmv._serialization import serialize_json
55
54
  from nextmv.base_model import BaseModel
56
55
  from nextmv.deprecated import deprecated
57
56
  from nextmv.logger import reset_stdout
@@ -644,7 +643,7 @@ class Output:
644
643
 
645
644
  if self.output_format == OutputFormat.JSON:
646
645
  try:
647
- _ = json.dumps(self.solution, default=_custom_serial)
646
+ _ = serialize_json(self.solution)
648
647
  except (TypeError, OverflowError) as e:
649
648
  raise ValueError(
650
649
  f"Output has output_format OutputFormat.JSON and "
@@ -722,12 +721,6 @@ class Output:
722
721
  and self.csv_configurations != {}
723
722
  ):
724
723
  output_dict["csv_configurations"] = self.csv_configurations
725
- elif (
726
- self.output_format == OutputFormat.JSON
727
- and self.json_configurations is not None
728
- and self.json_configurations != {}
729
- ):
730
- output_dict["json_configurations"] = self.json_configurations
731
724
 
732
725
  return output_dict
733
726
 
@@ -822,19 +815,9 @@ class LocalOutputWriter(OutputWriter):
822
815
  if hasattr(output, "json_configurations") and output.json_configurations is not None:
823
816
  json_configurations = output.json_configurations
824
817
 
825
- indent, custom_serial = 2, _custom_serial
826
- if "indent" in json_configurations:
827
- indent = json_configurations["indent"]
828
- del json_configurations["indent"]
829
- if "default" in json_configurations:
830
- custom_serial = json_configurations["default"]
831
- del json_configurations["default"]
832
-
833
- serialized = json.dumps(
818
+ serialized = serialize_json(
834
819
  output_dict,
835
- indent=indent,
836
- default=custom_serial,
837
- **json_configurations,
820
+ json_configurations=json_configurations,
838
821
  )
839
822
 
840
823
  if path is None or path == "":
@@ -877,13 +860,17 @@ class LocalOutputWriter(OutputWriter):
877
860
  if not os.path.exists(dir_path):
878
861
  os.makedirs(dir_path)
879
862
 
880
- serialized = json.dumps(
863
+ json_configurations = {}
864
+ if hasattr(output, "json_configurations") and output.json_configurations is not None:
865
+ json_configurations = output.json_configurations
866
+
867
+ serialized = serialize_json(
881
868
  {
882
869
  "options": output_dict.get("options", {}),
883
870
  "statistics": output_dict.get("statistics", {}),
884
871
  "assets": output_dict.get("assets", []),
885
872
  },
886
- indent=2,
873
+ json_configurations=json_configurations,
887
874
  )
888
875
  print(serialized, file=sys.stdout)
889
876
 
@@ -1118,32 +1105,3 @@ def write(
1118
1105
  """
1119
1106
 
1120
1107
  writer.write(output, path, skip_stdout_reset)
1121
-
1122
-
1123
- def _custom_serial(obj: Any) -> str:
1124
- """
1125
- JSON serializer for objects not serializable by default json serializer.
1126
-
1127
- This function provides custom serialization for datetime objects, converting
1128
- them to ISO format strings.
1129
-
1130
- Parameters
1131
- ----------
1132
- obj : Any
1133
- The object to serialize.
1134
-
1135
- Returns
1136
- -------
1137
- str
1138
- The serialized representation of the object.
1139
-
1140
- Raises
1141
- ------
1142
- TypeError
1143
- If the object type is not supported for serialization.
1144
- """
1145
-
1146
- if isinstance(obj, (datetime.datetime | datetime.date)):
1147
- return obj.isoformat()
1148
-
1149
- raise TypeError(f"Type {type(obj)} not serializable")
@@ -4,6 +4,11 @@ from typing import Any
4
4
  from nextmv.cloud.application import PollingOptions, poll
5
5
 
6
6
 
7
+ # This is a dummy function to avoid actually sleeping during tests.
8
+ def no_sleep(value: float) -> None:
9
+ return
10
+
11
+
7
12
  class TestApplication(unittest.TestCase):
8
13
  def test_poll(self):
9
14
  counter = 0
@@ -17,9 +22,9 @@ class TestApplication(unittest.TestCase):
17
22
 
18
23
  return "result", True
19
24
 
20
- polling_options = PollingOptions(verbose=True)
25
+ polling_options = PollingOptions()
21
26
 
22
- result = poll(polling_options, polling_func)
27
+ result = poll(polling_options, polling_func, no_sleep)
23
28
 
24
29
  self.assertEqual(result, "result")
25
30
 
@@ -42,8 +47,29 @@ class TestApplication(unittest.TestCase):
42
47
  if counter == 3:
43
48
  return True
44
49
 
45
- polling_options = PollingOptions(verbose=True, stop=stop)
50
+ polling_options = PollingOptions(stop=stop)
46
51
 
47
- result = poll(polling_options, polling_func)
52
+ result = poll(polling_options, polling_func, no_sleep)
48
53
 
49
54
  self.assertIsNone(result)
55
+
56
+ def test_poll_long(self):
57
+ counter = 0
58
+ max_tries = 1000000
59
+
60
+ def polling_func() -> tuple[Any, bool]:
61
+ nonlocal counter
62
+ counter += 1
63
+
64
+ if counter < max_tries:
65
+ return "result", False
66
+
67
+ return "result", True
68
+
69
+ polling_options = PollingOptions(
70
+ max_tries=max_tries + 1,
71
+ )
72
+
73
+ result = poll(polling_options, polling_func, no_sleep)
74
+
75
+ self.assertEqual(result, "result")
@@ -118,13 +118,6 @@ class TestOutput(unittest.TestCase):
118
118
  result = output.to_dict()
119
119
  self.assertEqual(result["assets"][0]["name"], "asset3")
120
120
 
121
- # Test with JSON configurations
122
- json_config = {"indent": 4, "sort_keys": True}
123
- output = nextmv.Output(output_format=nextmv.OutputFormat.JSON, json_configurations=json_config)
124
- result = output.to_dict()
125
- self.assertEqual(result["json_configurations"]["indent"], 4)
126
- self.assertEqual(result["json_configurations"]["sort_keys"], True)
127
-
128
121
  # Test with CSV configurations
129
122
  csv_config = {"delimiter": ";", "quoting": csv.QUOTE_NONNUMERIC}
130
123
  output = nextmv.Output(output_format=nextmv.OutputFormat.CSV_ARCHIVE, csv_configurations=csv_config)
@@ -183,7 +176,6 @@ class TestOutput(unittest.TestCase):
183
176
  self.assertEqual(result["assets"][0]["name"], "asset1")
184
177
  self.assertEqual(result["assets"][0]["visual"]["schema"], "chartjs")
185
178
  self.assertEqual(result["solution"]["value"], 42)
186
- self.assertEqual(result["json_configurations"]["indent"], 4)
187
179
 
188
180
  def test_local_writer_json_stdout_default(self):
189
181
  output = nextmv.Output(
@@ -262,7 +254,7 @@ class TestOutput(unittest.TestCase):
262
254
 
263
255
  self.assertEqual(
264
256
  mock_stdout.getvalue(),
265
- '{"assets":[],"json_configurations":{"separators":[",",":"],"sort_keys":true},"options":{},"solution":{"empanadas":"are_life"},"statistics":{"foo":"bar"}}\n',
257
+ '{"assets":[],"options":{},"solution":{"empanadas":"are_life"},"statistics":{"foo":"bar"}}\n',
266
258
  )
267
259
 
268
260
  def test_local_writer_json_stdout_with_options(self):
@@ -0,0 +1,96 @@
1
+ import datetime
2
+ import json
3
+ import unittest
4
+
5
+ import nextmv._serialization
6
+
7
+
8
+ class TestSerialization(unittest.TestCase):
9
+ """Tests for the common serialization functionality."""
10
+
11
+ def test_default_serialization(self):
12
+ """Test the default serialization"""
13
+
14
+ data = {
15
+ "name": "Test",
16
+ "value": 42,
17
+ "timestamp": nextmv._serialization._custom_serial(datetime.datetime(2023, 10, 1)),
18
+ }
19
+ serialized = nextmv._serialization.serialize_json(data)
20
+ expected = json.dumps(
21
+ {
22
+ "name": "Test",
23
+ "value": 42,
24
+ "timestamp": "2023-10-01T00:00:00",
25
+ },
26
+ indent=2,
27
+ )
28
+ self.assertEqual(serialized, expected)
29
+
30
+ def test_default_deflated_serialization(self):
31
+ """Test the default deflated serialization"""
32
+
33
+ data = {
34
+ "name": "Test",
35
+ "value": 42,
36
+ "timestamp": nextmv._serialization._custom_serial(datetime.datetime(2023, 10, 1)),
37
+ }
38
+ serialized = nextmv._serialization.deflated_serialize_json(data)
39
+ expected = json.dumps(
40
+ {
41
+ "name": "Test",
42
+ "value": 42,
43
+ "timestamp": "2023-10-01T00:00:00",
44
+ },
45
+ separators=(",", ":"),
46
+ )
47
+ self.assertEqual(serialized, expected)
48
+
49
+ def test_custom_serialization(self):
50
+ """Test custom serialization with additional configurations"""
51
+
52
+ data = {
53
+ "name": "Test",
54
+ "value": 42,
55
+ "timestamp": nextmv._serialization._custom_serial(datetime.datetime(2023, 10, 1)),
56
+ }
57
+ json_configurations = {
58
+ "indent": 2,
59
+ "default": nextmv._serialization._custom_serial,
60
+ "separators": (",", ": "),
61
+ }
62
+ serialized = nextmv._serialization.serialize_json(data, json_configurations)
63
+ expected = json.dumps(
64
+ {
65
+ "name": "Test",
66
+ "value": 42,
67
+ "timestamp": "2023-10-01T00:00:00",
68
+ },
69
+ indent=2,
70
+ separators=(",", ": "),
71
+ )
72
+ self.assertEqual(serialized, expected)
73
+
74
+ def test_compressed_serialization(self):
75
+ """Test a requested compressed serialization"""
76
+
77
+ data = {
78
+ "name": "Test",
79
+ "value": 42,
80
+ "timestamp": nextmv._serialization._custom_serial(datetime.datetime(2023, 10, 1)),
81
+ }
82
+ json_configurations = {
83
+ "separators": (",", ":"), # Remove spaces for a compressed format
84
+ "indent": None, # No indentation for compressed format
85
+ }
86
+ serialized = nextmv._serialization.serialize_json(data, json_configurations)
87
+ expected = json.dumps(
88
+ {
89
+ "name": "Test",
90
+ "value": 42,
91
+ "timestamp": "2023-10-01T00:00:00",
92
+ },
93
+ separators=(",", ":"),
94
+ indent=None,
95
+ )
96
+ self.assertEqual(serialized, expected)
@@ -1 +0,0 @@
1
- __version__ = "v0.28.3.dev1"
File without changes
File without changes
File without changes