nextmv 0.10.3.dev0__py3-none-any.whl → 0.35.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +39 -0
  3. nextmv/__init__.py +57 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +79 -9
  6. nextmv/cloud/__init__.py +71 -10
  7. nextmv/cloud/acceptance_test.py +888 -17
  8. nextmv/cloud/account.py +154 -10
  9. nextmv/cloud/application.py +3644 -437
  10. nextmv/cloud/batch_experiment.py +292 -33
  11. nextmv/cloud/client.py +354 -53
  12. nextmv/cloud/ensemble.py +247 -0
  13. nextmv/cloud/input_set.py +121 -4
  14. nextmv/cloud/instance.py +125 -0
  15. nextmv/cloud/package.py +474 -0
  16. nextmv/cloud/scenario.py +410 -0
  17. nextmv/cloud/secrets.py +234 -0
  18. nextmv/cloud/url.py +73 -0
  19. nextmv/cloud/version.py +174 -0
  20. nextmv/default_app/.gitignore +1 -0
  21. nextmv/default_app/README.md +32 -0
  22. nextmv/default_app/app.yaml +12 -0
  23. nextmv/default_app/input.json +5 -0
  24. nextmv/default_app/main.py +37 -0
  25. nextmv/default_app/requirements.txt +2 -0
  26. nextmv/default_app/src/__init__.py +0 -0
  27. nextmv/default_app/src/main.py +37 -0
  28. nextmv/default_app/src/visuals.py +36 -0
  29. nextmv/deprecated.py +47 -0
  30. nextmv/input.py +883 -78
  31. nextmv/local/__init__.py +5 -0
  32. nextmv/local/application.py +1263 -0
  33. nextmv/local/executor.py +1040 -0
  34. nextmv/local/geojson_handler.py +323 -0
  35. nextmv/local/local.py +97 -0
  36. nextmv/local/plotly_handler.py +61 -0
  37. nextmv/local/runner.py +274 -0
  38. nextmv/logger.py +80 -9
  39. nextmv/manifest.py +1472 -0
  40. nextmv/model.py +431 -0
  41. nextmv/options.py +968 -78
  42. nextmv/output.py +1363 -231
  43. nextmv/polling.py +287 -0
  44. nextmv/run.py +1623 -0
  45. nextmv/safe.py +145 -0
  46. nextmv/status.py +122 -0
  47. {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/METADATA +51 -288
  48. nextmv-0.35.0.dist-info/RECORD +50 -0
  49. {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/WHEEL +1 -1
  50. nextmv/cloud/status.py +0 -29
  51. nextmv/nextroute/__init__.py +0 -2
  52. nextmv/nextroute/check/__init__.py +0 -26
  53. nextmv/nextroute/check/schema.py +0 -141
  54. nextmv/nextroute/schema/__init__.py +0 -19
  55. nextmv/nextroute/schema/input.py +0 -52
  56. nextmv/nextroute/schema/location.py +0 -13
  57. nextmv/nextroute/schema/output.py +0 -136
  58. nextmv/nextroute/schema/stop.py +0 -61
  59. nextmv/nextroute/schema/vehicle.py +0 -68
  60. nextmv-0.10.3.dev0.dist-info/RECORD +0 -28
  61. {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/licenses/LICENSE +0 -0
nextmv/cloud/client.py CHANGED
@@ -1,33 +1,108 @@
1
- """Module with the client class."""
1
+ """Module with the client class.
2
+
3
+ This module provides the `Client` class for interacting with the Nextmv Cloud
4
+ API, and a helper function `get_size` to determine the size of objects.
5
+
6
+ Classes
7
+ -------
8
+ Client
9
+ Client that interacts directly with the Nextmv Cloud API.
10
+
11
+ Functions
12
+ ---------
13
+ get_size(obj)
14
+ Finds the size of an object in bytes.
15
+ """
2
16
 
3
- import json
4
17
  import os
5
18
  from dataclasses import dataclass, field
6
- from typing import Any, Dict, List, Optional
19
+ from typing import IO, Any
7
20
  from urllib.parse import urljoin
8
21
 
9
22
  import requests
10
23
  import yaml
11
24
  from requests.adapters import HTTPAdapter, Retry
12
25
 
26
+ from nextmv._serialization import deflated_serialize_json
27
+
13
28
  _MAX_LAMBDA_PAYLOAD_SIZE: int = 500 * 1024 * 1024
14
- """Maximum size of the payload handled by the Nextmv Cloud API."""
29
+ """int: Maximum size of the payload handled by the Nextmv Cloud API.
30
+
31
+ This constant defines the upper limit for the size of data payloads that can
32
+ be sent to the Nextmv Cloud API, specifically for lambda functions. It is set
33
+ to 500 MiB.
34
+ """
15
35
 
16
36
 
17
37
  @dataclass
18
38
  class Client:
19
39
  """
20
- Client that interacts directly with the Nextmv Cloud API. The API key will
21
- be searched, in order of precedence, in: the api_key arg in the
22
- constructor, the NEXTMV_API_KEY environment variable, the
23
- ~/.nextmv/config.yaml file used by the Nextmv CLI.
40
+ Client that interacts directly with the Nextmv Cloud API.
41
+
42
+ You can import the `Client` class directly from `cloud`:
43
+
44
+ ```python
45
+ from nextmv.cloud import Client
46
+ ```
47
+
48
+ The API key will be searched, in order of precedence, in:
49
+
50
+ 1. The `api_key` argument in the constructor.
51
+ 2. The `NEXTMV_API_KEY` environment variable.
52
+ 3. The `~/.nextmv/config.yaml` file used by the Nextmv CLI.
53
+
54
+ Parameters
55
+ ----------
56
+ api_key : str, optional
57
+ API key to use for authenticating with the Nextmv Cloud API. If not
58
+ provided, the client will look for the `NEXTMV_API_KEY` environment
59
+ variable.
60
+ allowed_methods : list[str]
61
+ Allowed HTTP methods to use for retries in requests to the Nextmv
62
+ Cloud API. Defaults to ``["GET", "POST", "PUT", "DELETE"]``.
63
+ backoff_factor : float
64
+ Exponential backoff factor to use for requests to the Nextmv Cloud
65
+ API. Defaults to ``1``.
66
+ backoff_jitter : float
67
+ Jitter to use for requests to the Nextmv Cloud API when backing off.
68
+ Defaults to ``0.1``.
69
+ backoff_max : float
70
+ Maximum backoff time to use for requests to the Nextmv Cloud API, in
71
+ seconds. Defaults to ``60``.
72
+ configuration_file : str
73
+ Path to the configuration file used by the Nextmv CLI. Defaults to
74
+ ``"~/.nextmv/config.yaml"``.
75
+ headers : dict[str, str], optional
76
+ Headers to use for requests to the Nextmv Cloud API. Automatically
77
+ set up with the API key.
78
+ max_retries : int
79
+ Maximum number of retries to use for requests to the Nextmv Cloud
80
+ API. Defaults to ``10``.
81
+ status_forcelist : list[int]
82
+ Status codes to retry for requests to the Nextmv Cloud API. Defaults
83
+ to ``[429]``.
84
+ timeout : float
85
+ Timeout to use for requests to the Nextmv Cloud API, in seconds.
86
+ Defaults to ``20``.
87
+ url : str
88
+ URL of the Nextmv Cloud API. Defaults to
89
+ ``"https://api.cloud.nextmv.io"``.
90
+ console_url : str
91
+ URL of the Nextmv Cloud console. Defaults to
92
+ ``"https://cloud.nextmv.io"``.
93
+
94
+ Examples
95
+ --------
96
+ >>> client = Client(api_key="YOUR_API_KEY")
97
+ >>> response = client.request(method="GET", endpoint="/v1/applications")
98
+ >>> print(response.json())
24
99
  """
25
100
 
26
- api_key: Optional[str] = None
101
+ api_key: str | None = None
27
102
  """API key to use for authenticating with the Nextmv Cloud API. If not
28
103
  provided, the client will look for the NEXTMV_API_KEY environment
29
104
  variable."""
30
- allowed_methods: List[str] = field(
105
+ allowed_methods: list[str] = field(
31
106
  default_factory=lambda: ["GET", "POST", "PUT", "DELETE"],
32
107
  )
33
108
  """Allowed HTTP methods to use for retries in requests to the Nextmv Cloud
@@ -42,22 +117,40 @@ class Client:
42
117
  seconds."""
43
118
  configuration_file: str = "~/.nextmv/config.yaml"
44
119
  """Path to the configuration file used by the Nextmv CLI."""
45
- headers: Optional[Dict[str, str]] = None
120
+ headers: dict[str, str] | None = None
46
121
  """Headers to use for requests to the Nextmv Cloud API."""
47
122
  max_retries: int = 10
48
123
  """Maximum number of retries to use for requests to the Nextmv Cloud
49
124
  API."""
50
- status_forcelist: List[int] = field(
51
- default_factory=lambda: [429, 500, 502, 503, 504, 507, 509],
125
+ status_forcelist: list[int] = field(
126
+ default_factory=lambda: [429],
52
127
  )
53
128
  """Status codes to retry for requests to the Nextmv Cloud API."""
54
129
  timeout: float = 20
55
130
  """Timeout to use for requests to the Nextmv Cloud API."""
56
131
  url: str = "https://api.cloud.nextmv.io"
57
132
  """URL of the Nextmv Cloud API."""
133
+ console_url: str = "https://cloud.nextmv.io"
134
+ """URL of the Nextmv Cloud console."""
58
135
 
59
136
  def __post_init__(self):
60
- """Logic to run after the class is initialized."""
137
+ """
138
+ Initializes the client after dataclass construction.
139
+
140
+ This method handles the logic for API key retrieval and header
141
+ setup. It checks for the API key in the constructor, environment
142
+ variables, and the configuration file, in that order.
143
+
144
+ Raises
145
+ ------
146
+ ValueError
147
+ If `api_key` is an empty string.
148
+ If no API key is found in any of the lookup locations.
149
+ If a profile is specified via `NEXTMV_PROFILE` but not found in
150
+ the configuration file.
151
+ If `apikey` is not found in the configuration file for the
152
+ selected profile.
153
+ """
61
154
 
62
155
  if self.api_key is not None and self.api_key != "":
63
156
  self._set_headers_api_key(self.api_key)
@@ -75,7 +168,7 @@ class Client:
75
168
  config_path = os.path.expanduser(self.configuration_file)
76
169
  if not os.path.exists(config_path):
77
170
  raise ValueError(
78
- "no API key set in constructor or NEXTMV_API_KEY env var, and ~/.nextmv/config.yaml does not exist"
171
+ f"no API key set in constructor or NEXTMV_API_KEY env var, and {self.configuration_file} does not exist"
79
172
  )
80
173
 
81
174
  with open(config_path) as f:
@@ -86,11 +179,11 @@ class Client:
86
179
  if profile is not None:
87
180
  parent = config.get(profile)
88
181
  if parent is None:
89
- raise ValueError(f"profile {profile} set via NEXTMV_PROFILE but not found in ~/.nextmv/config.yaml")
182
+ raise ValueError(f"profile {profile} set via NEXTMV_PROFILE but not found in {self.configuration_file}")
90
183
 
91
184
  api_key = parent.get("apikey")
92
185
  if api_key is None:
93
- raise ValueError("no apiKey found in ~/.nextmv/config.yaml")
186
+ raise ValueError(f"no apiKey found in {self.configuration_file}")
94
187
  self.api_key = api_key
95
188
 
96
189
  endpoint = parent.get("endpoint")
@@ -103,46 +196,91 @@ class Client:
103
196
  self,
104
197
  method: str,
105
198
  endpoint: str,
106
- data: Optional[Any] = None,
107
- headers: Optional[Dict[str, str]] = None,
108
- payload: Optional[Dict[str, Any]] = None,
109
- query_params: Optional[Dict[str, Any]] = None,
199
+ data: Any | None = None,
200
+ headers: dict[str, str] | None = None,
201
+ payload: dict[str, Any] | None = None,
202
+ query_params: dict[str, Any] | None = None,
203
+ json_configurations: dict[str, Any] | None = None,
110
204
  ) -> requests.Response:
111
205
  """
112
- Method to make a request to the Nextmv Cloud API.
113
-
114
- Args:
115
- method: HTTP method to use. Valid methods include: GET, POST.
116
- endpoint: Endpoint to send the request to.
117
- data: Data to send with the request.
118
- headers: Headers to send with the request.
119
- payload: Payload to send with the request. Prefer using this over
120
- data.
121
- query_params: Query parameters to send with the request.
122
-
123
- Returns:
124
- Response from the Nextmv Cloud API.
125
-
126
- Raises:
127
- requests.HTTPError: If the response status code is not 2xx.
128
- ValueError: If both data and payload are provided.
129
- ValueError: If the payload size exceeds the maximum allowed size.
130
- ValueError: If the data size exceeds the maximum allowed size.
206
+ Makes a request to the Nextmv Cloud API.
207
+
208
+ Parameters
209
+ ----------
210
+ method : str
211
+ HTTP method to use (e.g., "GET", "POST").
212
+ endpoint : str
213
+ API endpoint to send the request to (e.g., "/v1/applications").
214
+ data : Any, optional
215
+ Data to send in the request body. Typically used for form data.
216
+ Cannot be used if `payload` is also provided.
217
+ headers : dict[str, str], optional
218
+ Additional headers to send with the request. These will override
219
+ the default client headers if keys conflict.
220
+ payload : dict[str, Any], optional
221
+ JSON payload to send with the request. Prefer using this over
222
+ `data` for JSON requests. Cannot be used if `data` is also
223
+ provided.
224
+ query_params : dict[str, Any], optional
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.
231
+
232
+ Returns
233
+ -------
234
+ requests.Response
235
+ The response object from the Nextmv Cloud API.
236
+
237
+ Raises
238
+ ------
239
+ requests.HTTPError
240
+ If the response status code is not in the 2xx range.
241
+ ValueError
242
+ If both `data` and `payload` are provided.
243
+ If the `payload` size exceeds `_MAX_LAMBDA_PAYLOAD_SIZE`.
244
+ If the `data` size exceeds `_MAX_LAMBDA_PAYLOAD_SIZE`.
245
+
246
+ Examples
247
+ --------
248
+ >>> client = Client(api_key="YOUR_API_KEY")
249
+ >>> # Get a list of applications
250
+ >>> response = client.request(method="GET", endpoint="/v1/applications")
251
+ >>> print(response.status_code)
252
+ 200
253
+ >>> # Create a new run
254
+ >>> run_payload = {
255
+ ... "applicationId": "app_id",
256
+ ... "instanceId": "instance_id",
257
+ ... "input": {"value": 10}
258
+ ... }
259
+ >>> response = client.request(
260
+ ... method="POST",
261
+ ... endpoint="/v1/runs",
262
+ ... payload=run_payload
263
+ ... )
264
+ >>> print(response.json()["id"])
265
+ run_xxxxxxxxxxxx
131
266
  """
132
267
 
133
268
  if payload is not None and data is not None:
134
269
  raise ValueError("cannot use both data and payload")
135
270
 
136
- 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
+ ):
137
275
  raise ValueError(
138
- f"payload size of {get_size(payload)} bytes exceeds the maximum "
139
- 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"
140
278
  )
141
279
 
142
- 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:
143
281
  raise ValueError(
144
- f"data size of {get_size(data)} bytes exceeds the maximum "
145
- 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"
146
284
  )
147
285
 
148
286
  session = requests.Session()
@@ -157,7 +295,7 @@ class Client:
157
295
  adapter = HTTPAdapter(max_retries=retries)
158
296
  session.mount("https://", adapter)
159
297
 
160
- kwargs = {
298
+ kwargs: dict[str, Any] = {
161
299
  "url": urljoin(self.url, endpoint),
162
300
  "timeout": self.timeout,
163
301
  }
@@ -165,7 +303,11 @@ class Client:
165
303
  if data is not None:
166
304
  kwargs["data"] = data
167
305
  if payload is not None:
168
- 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")
169
311
  if query_params is not None:
170
312
  kwargs["params"] = query_params
171
313
 
@@ -180,8 +322,110 @@ class Client:
180
322
 
181
323
  return response
182
324
 
325
+ def upload_to_presigned_url(
326
+ self,
327
+ data: dict[str, Any] | str | None,
328
+ url: str,
329
+ json_configurations: dict[str, Any] | None = None,
330
+ tar_file: str | None = None,
331
+ ) -> None:
332
+ """
333
+ Uploads data to a presigned URL.
334
+
335
+ This method is typically used for uploading large input or output files
336
+ directly to cloud storage, bypassing the main API for efficiency.
337
+
338
+ Parameters
339
+ ----------
340
+ data : Union[dict[str, Any], str], optional
341
+ The data to upload. If a dictionary is provided, it will be
342
+ JSON-serialized. If a string is provided, it will be uploaded
343
+ as is.
344
+ url : str
345
+ The presigned URL to which the data will be uploaded.
346
+ json_configurations : dict[str, Any], optional
347
+ Additional configurations for JSON serialization. This allows
348
+ customization of the Python `json.dumps` function, such as
349
+ specifying `indent` for pretty printing or `default` for custom
350
+ serialization functions.
351
+ tar_file : str, optional
352
+ If provided, this will be used to upload a tar file instead of
353
+ a JSON string or dictionary. This is useful for uploading large
354
+ files that are already packaged as a tarball. If this is provided,
355
+ `data` is expected to be `None`.
356
+
357
+ Raises
358
+ ------
359
+ ValueError
360
+ If `data` is not a dictionary or a string.
361
+ requests.HTTPError
362
+ If the upload request fails.
363
+
364
+ Examples
365
+ --------
366
+ Assume `presigned_upload_url` is obtained from a previous API call.
367
+ >>> client = Client(api_key="YOUR_API_KEY")
368
+ >>> input_data = {"value": 42, "items": [1, 2, 3]}
369
+ >>> client.upload_to_presigned_url(data=input_data, url="PRE_SIGNED_URL") # doctest: +SKIP
370
+ """
371
+
372
+ upload_data: str | None = None
373
+ if data is not None:
374
+ if isinstance(data, dict):
375
+ upload_data = deflated_serialize_json(data, json_configurations=json_configurations)
376
+ elif isinstance(data, str):
377
+ upload_data = data
378
+ else:
379
+ raise ValueError("data must be a dictionary or a string")
380
+
381
+ session = requests.Session()
382
+ retries = Retry(
383
+ total=self.max_retries,
384
+ backoff_factor=self.backoff_factor,
385
+ backoff_jitter=self.backoff_jitter,
386
+ backoff_max=self.backoff_max,
387
+ status_forcelist=self.status_forcelist,
388
+ allowed_methods=self.allowed_methods,
389
+ )
390
+ adapter = HTTPAdapter(max_retries=retries)
391
+ session.mount("https://", adapter)
392
+
393
+ kwargs: dict[str, Any] = {
394
+ "url": url,
395
+ "timeout": self.timeout,
396
+ }
397
+
398
+ if upload_data is not None:
399
+ kwargs["data"] = upload_data
400
+ elif tar_file is not None and tar_file != "":
401
+ if not os.path.exists(tar_file):
402
+ raise ValueError(f"tar_file {tar_file} does not exist")
403
+ kwargs["data"] = open(tar_file, "rb")
404
+ else:
405
+ raise ValueError("either data or tar_file must be provided")
406
+
407
+ response = session.put(**kwargs)
408
+
409
+ try:
410
+ response.raise_for_status()
411
+ except requests.HTTPError as e:
412
+ raise requests.HTTPError(
413
+ f"upload to presigned URL {url} failed with "
414
+ f"status code {response.status_code} and message: {response.text}"
415
+ ) from e
416
+
183
417
  def _set_headers_api_key(self, api_key: str) -> None:
184
- """Sets the API key to use for requests to the Nextmv Cloud API."""
418
+ """
419
+ Sets the Authorization and Content-Type headers.
420
+
421
+ This is an internal method used to configure the necessary headers
422
+ for API authentication and content type specification.
423
+
424
+ Parameters
425
+ ----------
426
+ api_key : str
427
+ The API key to be included in the Authorization header.
428
+ """
185
429
 
186
430
  self.headers = {
187
431
  "Authorization": f"Bearer {api_key}",
@@ -189,8 +433,65 @@ class Client:
189
433
  }
190
434
 
191
435
 
192
- def get_size(obj: Dict[str, Any]) -> int:
193
- """Finds the size of an object in bytes."""
436
+ def get_size(obj: dict[str, Any] | IO[bytes] | str, json_configurations: dict[str, Any] | None = None) -> int:
437
+ """
438
+ Finds the size of an object in bytes.
439
+
440
+ This function supports dictionaries (JSON-serialized), file-like objects
441
+ (by reading their content), and strings.
442
+
443
+ Parameters
444
+ ----------
445
+ obj : dict[str, Any] or IO[bytes] or str
446
+ The object whose size is to be determined.
447
+ - If a dict, it's converted to a JSON string.
448
+ - If a file-like object (e.g., opened file), its size is read.
449
+ - If a string, its UTF-8 encoded byte length is calculated.
450
+ json_configurations : dict[str, Any], optional
451
+ Additional configurations for JSON serialization. This allows
452
+ customization of the Python `json.dumps` function, such as specifying
453
+ `indent` for pretty printing or `default` for custom serialization
454
+ functions.
455
+
456
+ Returns
457
+ -------
458
+ int
459
+ The size of the object in bytes.
460
+
461
+ Raises
462
+ ------
463
+ TypeError
464
+ If the object type is not supported (i.e., not a dict,
465
+ file-like object, or string).
466
+
467
+ Examples
468
+ --------
469
+ >>> my_dict = {"key": "value", "number": 123}
470
+ >>> get_size(my_dict)
471
+ 30
472
+ >>> import io
473
+ >>> my_string = "Hello, Nextmv!"
474
+ >>> string_io = io.StringIO(my_string)
475
+ >>> # To get size of underlying buffer for StringIO, we need to encode
476
+ >>> string_bytes_io = io.BytesIO(my_string.encode('utf-8'))
477
+ >>> get_size(string_bytes_io)
478
+ 14
479
+ >>> get_size("Hello, Nextmv!")
480
+ 14
481
+ """
482
+
483
+ if isinstance(obj, dict):
484
+ obj_str = deflated_serialize_json(obj, json_configurations=json_configurations)
485
+ return len(obj_str.encode("utf-8"))
486
+
487
+ elif hasattr(obj, "read"):
488
+ obj.seek(0, 2) # Move the cursor to the end of the file
489
+ size = obj.tell()
490
+ obj.seek(0) # Reset the cursor to the beginning of the file
491
+ return size
492
+
493
+ elif isinstance(obj, str):
494
+ return len(obj.encode("utf-8"))
194
495
 
195
- obj_str = json.dumps(obj, separators=(",", ":"))
196
- return len(obj_str.encode("utf-8"))
496
+ else:
497
+ raise TypeError("Unsupported type. Only dictionaries, file objects (IO[bytes]), and strings are supported.")