modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (160) hide show
  1. modal/__init__.py +0 -2
  2. modal/__main__.py +3 -4
  3. modal/_billing.py +80 -0
  4. modal/_clustered_functions.py +7 -3
  5. modal/_clustered_functions.pyi +15 -3
  6. modal/_container_entrypoint.py +51 -69
  7. modal/_functions.py +508 -240
  8. modal/_grpc_client.py +171 -0
  9. modal/_load_context.py +105 -0
  10. modal/_object.py +81 -21
  11. modal/_output.py +58 -45
  12. modal/_partial_function.py +48 -73
  13. modal/_pty.py +7 -3
  14. modal/_resolver.py +26 -46
  15. modal/_runtime/asgi.py +4 -3
  16. modal/_runtime/container_io_manager.py +358 -220
  17. modal/_runtime/container_io_manager.pyi +296 -101
  18. modal/_runtime/execution_context.py +18 -2
  19. modal/_runtime/execution_context.pyi +64 -7
  20. modal/_runtime/gpu_memory_snapshot.py +262 -57
  21. modal/_runtime/user_code_imports.py +28 -58
  22. modal/_serialization.py +90 -6
  23. modal/_traceback.py +42 -1
  24. modal/_tunnel.pyi +380 -12
  25. modal/_utils/async_utils.py +84 -29
  26. modal/_utils/auth_token_manager.py +111 -0
  27. modal/_utils/blob_utils.py +181 -58
  28. modal/_utils/deprecation.py +19 -0
  29. modal/_utils/function_utils.py +91 -47
  30. modal/_utils/grpc_utils.py +89 -66
  31. modal/_utils/mount_utils.py +26 -1
  32. modal/_utils/name_utils.py +17 -3
  33. modal/_utils/task_command_router_client.py +536 -0
  34. modal/_utils/time_utils.py +34 -6
  35. modal/app.py +256 -88
  36. modal/app.pyi +909 -92
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +18 -0
  39. modal/builder/PREVIEW.txt +18 -0
  40. modal/builder/base-images.json +58 -0
  41. modal/cli/_download.py +19 -3
  42. modal/cli/_traceback.py +3 -2
  43. modal/cli/app.py +4 -4
  44. modal/cli/cluster.py +15 -7
  45. modal/cli/config.py +5 -3
  46. modal/cli/container.py +7 -6
  47. modal/cli/dict.py +22 -16
  48. modal/cli/entry_point.py +12 -5
  49. modal/cli/environment.py +5 -4
  50. modal/cli/import_refs.py +3 -3
  51. modal/cli/launch.py +102 -5
  52. modal/cli/network_file_system.py +11 -12
  53. modal/cli/profile.py +3 -2
  54. modal/cli/programs/launch_instance_ssh.py +94 -0
  55. modal/cli/programs/run_jupyter.py +1 -1
  56. modal/cli/programs/run_marimo.py +95 -0
  57. modal/cli/programs/vscode.py +1 -1
  58. modal/cli/queues.py +57 -26
  59. modal/cli/run.py +91 -23
  60. modal/cli/secret.py +48 -22
  61. modal/cli/token.py +7 -8
  62. modal/cli/utils.py +4 -7
  63. modal/cli/volume.py +31 -25
  64. modal/client.py +15 -85
  65. modal/client.pyi +183 -62
  66. modal/cloud_bucket_mount.py +5 -3
  67. modal/cloud_bucket_mount.pyi +197 -5
  68. modal/cls.py +200 -126
  69. modal/cls.pyi +446 -68
  70. modal/config.py +29 -11
  71. modal/container_process.py +319 -19
  72. modal/container_process.pyi +190 -20
  73. modal/dict.py +290 -71
  74. modal/dict.pyi +835 -83
  75. modal/environments.py +15 -27
  76. modal/environments.pyi +46 -24
  77. modal/exception.py +14 -2
  78. modal/experimental/__init__.py +194 -40
  79. modal/experimental/flash.py +618 -0
  80. modal/experimental/flash.pyi +380 -0
  81. modal/experimental/ipython.py +11 -7
  82. modal/file_io.py +29 -36
  83. modal/file_io.pyi +251 -53
  84. modal/file_pattern_matcher.py +56 -16
  85. modal/functions.pyi +673 -92
  86. modal/gpu.py +1 -1
  87. modal/image.py +528 -176
  88. modal/image.pyi +1572 -145
  89. modal/io_streams.py +458 -128
  90. modal/io_streams.pyi +433 -52
  91. modal/mount.py +216 -151
  92. modal/mount.pyi +225 -78
  93. modal/network_file_system.py +45 -62
  94. modal/network_file_system.pyi +277 -56
  95. modal/object.pyi +93 -17
  96. modal/parallel_map.py +942 -129
  97. modal/parallel_map.pyi +294 -15
  98. modal/partial_function.py +0 -2
  99. modal/partial_function.pyi +234 -19
  100. modal/proxy.py +17 -8
  101. modal/proxy.pyi +36 -3
  102. modal/queue.py +270 -65
  103. modal/queue.pyi +817 -57
  104. modal/runner.py +115 -101
  105. modal/runner.pyi +205 -49
  106. modal/sandbox.py +512 -136
  107. modal/sandbox.pyi +845 -111
  108. modal/schedule.py +1 -1
  109. modal/secret.py +300 -70
  110. modal/secret.pyi +589 -34
  111. modal/serving.py +7 -11
  112. modal/serving.pyi +7 -8
  113. modal/snapshot.py +11 -8
  114. modal/snapshot.pyi +25 -4
  115. modal/token_flow.py +4 -4
  116. modal/token_flow.pyi +28 -8
  117. modal/volume.py +416 -158
  118. modal/volume.pyi +1117 -121
  119. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
  120. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  121. modal_docs/mdmd/mdmd.py +17 -4
  122. modal_proto/api.proto +534 -79
  123. modal_proto/api_grpc.py +337 -1
  124. modal_proto/api_pb2.py +1522 -968
  125. modal_proto/api_pb2.pyi +1619 -134
  126. modal_proto/api_pb2_grpc.py +699 -4
  127. modal_proto/api_pb2_grpc.pyi +226 -14
  128. modal_proto/modal_api_grpc.py +175 -154
  129. modal_proto/sandbox_router.proto +145 -0
  130. modal_proto/sandbox_router_grpc.py +105 -0
  131. modal_proto/sandbox_router_pb2.py +149 -0
  132. modal_proto/sandbox_router_pb2.pyi +333 -0
  133. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  134. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  135. modal_proto/task_command_router.proto +144 -0
  136. modal_proto/task_command_router_grpc.py +105 -0
  137. modal_proto/task_command_router_pb2.py +149 -0
  138. modal_proto/task_command_router_pb2.pyi +333 -0
  139. modal_proto/task_command_router_pb2_grpc.py +203 -0
  140. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  141. modal_version/__init__.py +1 -1
  142. modal/requirements/PREVIEW.txt +0 -16
  143. modal/requirements/base-images.json +0 -26
  144. modal-1.0.3.dev10.dist-info/RECORD +0 -179
  145. modal_proto/modal_options_grpc.py +0 -3
  146. modal_proto/options.proto +0 -19
  147. modal_proto/options_grpc.py +0 -3
  148. modal_proto/options_pb2.py +0 -35
  149. modal_proto/options_pb2.pyi +0 -20
  150. modal_proto/options_pb2_grpc.py +0 -4
  151. modal_proto/options_pb2_grpc.pyi +0 -7
  152. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  153. /modal/{requirements → builder}/2023.12.txt +0 -0
  154. /modal/{requirements → builder}/2024.04.txt +0 -0
  155. /modal/{requirements → builder}/2024.10.txt +0 -0
  156. /modal/{requirements → builder}/README.md +0 -0
  157. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  158. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  159. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  160. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,380 @@
1
+ import modal.client
2
+ import subprocess
3
+ import typing
4
+ import typing_extensions
5
+
6
+ class _FlashManager:
7
+ def __init__(
8
+ self,
9
+ client: modal.client._Client,
10
+ port: int,
11
+ process: typing.Optional[subprocess.Popen] = None,
12
+ health_check_url: typing.Optional[str] = None,
13
+ ):
14
+ """Initialize self. See help(type(self)) for accurate signature."""
15
+ ...
16
+
17
+ async def is_port_connection_healthy(
18
+ self, process: typing.Optional[subprocess.Popen], timeout: float = 0.5
19
+ ) -> tuple[bool, typing.Optional[Exception]]: ...
20
+ async def _start(self): ...
21
+ async def _drain_container(self):
22
+ """Background task that checks if we've encountered too many failures and drains the container if so."""
23
+ ...
24
+
25
+ async def _run_heartbeat(self, host: str, port: int): ...
26
+ def get_container_url(self): ...
27
+ async def stop(self): ...
28
+ async def close(self): ...
29
+
30
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
31
+
32
+ class FlashManager:
33
+ def __init__(
34
+ self,
35
+ client: modal.client.Client,
36
+ port: int,
37
+ process: typing.Optional[subprocess.Popen] = None,
38
+ health_check_url: typing.Optional[str] = None,
39
+ ): ...
40
+
41
+ class __is_port_connection_healthy_spec(typing_extensions.Protocol[SUPERSELF]):
42
+ def __call__(
43
+ self, /, process: typing.Optional[subprocess.Popen], timeout: float = 0.5
44
+ ) -> tuple[bool, typing.Optional[Exception]]: ...
45
+ async def aio(
46
+ self, /, process: typing.Optional[subprocess.Popen], timeout: float = 0.5
47
+ ) -> tuple[bool, typing.Optional[Exception]]: ...
48
+
49
+ is_port_connection_healthy: __is_port_connection_healthy_spec[typing_extensions.Self]
50
+
51
+ class ___start_spec(typing_extensions.Protocol[SUPERSELF]):
52
+ def __call__(self, /): ...
53
+ async def aio(self, /): ...
54
+
55
+ _start: ___start_spec[typing_extensions.Self]
56
+
57
+ class ___drain_container_spec(typing_extensions.Protocol[SUPERSELF]):
58
+ def __call__(self, /):
59
+ """Background task that checks if we've encountered too many failures and drains the container if so."""
60
+ ...
61
+
62
+ async def aio(self, /):
63
+ """Background task that checks if we've encountered too many failures and drains the container if so."""
64
+ ...
65
+
66
+ _drain_container: ___drain_container_spec[typing_extensions.Self]
67
+
68
+ class ___run_heartbeat_spec(typing_extensions.Protocol[SUPERSELF]):
69
+ def __call__(self, /, host: str, port: int): ...
70
+ async def aio(self, /, host: str, port: int): ...
71
+
72
+ _run_heartbeat: ___run_heartbeat_spec[typing_extensions.Self]
73
+
74
+ def get_container_url(self): ...
75
+
76
+ class __stop_spec(typing_extensions.Protocol[SUPERSELF]):
77
+ def __call__(self, /): ...
78
+ async def aio(self, /): ...
79
+
80
+ stop: __stop_spec[typing_extensions.Self]
81
+
82
+ class __close_spec(typing_extensions.Protocol[SUPERSELF]):
83
+ def __call__(self, /): ...
84
+ async def aio(self, /): ...
85
+
86
+ close: __close_spec[typing_extensions.Self]
87
+
88
+ class __flash_forward_spec(typing_extensions.Protocol):
89
+ def __call__(
90
+ self,
91
+ /,
92
+ port: int,
93
+ process: typing.Optional[subprocess.Popen] = None,
94
+ health_check_url: typing.Optional[str] = None,
95
+ ) -> FlashManager:
96
+ """Forward a port to the Modal Flash service, exposing that port as a stable web endpoint.
97
+ This is a highly experimental method that can break or be removed at any time without warning.
98
+ Do not use this method unless explicitly instructed to do so by Modal support.
99
+ """
100
+ ...
101
+
102
+ async def aio(
103
+ self,
104
+ /,
105
+ port: int,
106
+ process: typing.Optional[subprocess.Popen] = None,
107
+ health_check_url: typing.Optional[str] = None,
108
+ ) -> FlashManager:
109
+ """Forward a port to the Modal Flash service, exposing that port as a stable web endpoint.
110
+ This is a highly experimental method that can break or be removed at any time without warning.
111
+ Do not use this method unless explicitly instructed to do so by Modal support.
112
+ """
113
+ ...
114
+
115
+ flash_forward: __flash_forward_spec
116
+
117
+ class _FlashPrometheusAutoscaler:
118
+ def __init__(
119
+ self,
120
+ client: modal.client._Client,
121
+ app_name: str,
122
+ cls_name: str,
123
+ metrics_endpoint: str,
124
+ target_metric: str,
125
+ target_metric_value: float,
126
+ min_containers: typing.Optional[int],
127
+ max_containers: typing.Optional[int],
128
+ buffer_containers: typing.Optional[int],
129
+ scale_up_tolerance: float,
130
+ scale_down_tolerance: float,
131
+ scale_up_stabilization_window_seconds: int,
132
+ scale_down_stabilization_window_seconds: int,
133
+ autoscaling_interval_seconds: int,
134
+ ):
135
+ """Initialize self. See help(type(self)) for accurate signature."""
136
+ ...
137
+
138
+ async def start(self): ...
139
+ async def _run_autoscaler_loop(self): ...
140
+ async def _compute_target_containers(self, current_replicas: int) -> int:
141
+ """Gets metrics from container to autoscale up or down."""
142
+ ...
143
+
144
+ def _calculate_desired_replicas(
145
+ self,
146
+ n_current_replicas: int,
147
+ sum_metric: float,
148
+ n_containers_with_metrics: int,
149
+ n_total_containers: int,
150
+ target_metric_value: float,
151
+ ) -> int:
152
+ """Calculate the desired number of replicas to autoscale to."""
153
+ ...
154
+
155
+ async def _get_scaling_info(self, containers) -> tuple[float, int]:
156
+ """Get metrics using container exposed metrics endpoints."""
157
+ ...
158
+
159
+ async def _get_metrics(self, url: str) -> typing.Optional[dict[str, list[typing.Any]]]: ...
160
+ async def _get_all_containers(self): ...
161
+ async def _set_target_slots(self, target_slots: int): ...
162
+ def _make_scaling_decision(
163
+ self,
164
+ current_replicas: int,
165
+ autoscaling_decisions: list[tuple[float, int]],
166
+ scale_up_stabilization_window_seconds: int = 0,
167
+ scale_down_stabilization_window_seconds: int = 300,
168
+ min_containers: typing.Optional[int] = None,
169
+ max_containers: typing.Optional[int] = None,
170
+ buffer_containers: typing.Optional[int] = None,
171
+ ) -> int:
172
+ """Return the target number of containers following (simplified) Kubernetes HPA
173
+ stabilization-window semantics.
174
+
175
+ Args:
176
+ current_replicas: Current number of running Pods/containers.
177
+ autoscaling_decisions: List of (timestamp, desired_replicas) pairs, where
178
+ timestamp is a UNIX epoch float (seconds).
179
+ The list *must* contain at least one entry and should
180
+ already include the most-recent measurement.
181
+ scale_up_stabilization_window_seconds: 0 disables the up-window.
182
+ scale_down_stabilization_window_seconds: 0 disables the down-window.
183
+ min_containers / max_containers: Clamp the final decision to this range.
184
+
185
+ Returns:
186
+ The target number of containers.
187
+ """
188
+ ...
189
+
190
+ async def stop(self): ...
191
+
192
+ class FlashPrometheusAutoscaler:
193
+ def __init__(
194
+ self,
195
+ client: modal.client.Client,
196
+ app_name: str,
197
+ cls_name: str,
198
+ metrics_endpoint: str,
199
+ target_metric: str,
200
+ target_metric_value: float,
201
+ min_containers: typing.Optional[int],
202
+ max_containers: typing.Optional[int],
203
+ buffer_containers: typing.Optional[int],
204
+ scale_up_tolerance: float,
205
+ scale_down_tolerance: float,
206
+ scale_up_stabilization_window_seconds: int,
207
+ scale_down_stabilization_window_seconds: int,
208
+ autoscaling_interval_seconds: int,
209
+ ): ...
210
+
211
+ class __start_spec(typing_extensions.Protocol[SUPERSELF]):
212
+ def __call__(self, /): ...
213
+ async def aio(self, /): ...
214
+
215
+ start: __start_spec[typing_extensions.Self]
216
+
217
+ class ___run_autoscaler_loop_spec(typing_extensions.Protocol[SUPERSELF]):
218
+ def __call__(self, /): ...
219
+ async def aio(self, /): ...
220
+
221
+ _run_autoscaler_loop: ___run_autoscaler_loop_spec[typing_extensions.Self]
222
+
223
+ class ___compute_target_containers_spec(typing_extensions.Protocol[SUPERSELF]):
224
+ def __call__(self, /, current_replicas: int) -> int:
225
+ """Gets metrics from container to autoscale up or down."""
226
+ ...
227
+
228
+ async def aio(self, /, current_replicas: int) -> int:
229
+ """Gets metrics from container to autoscale up or down."""
230
+ ...
231
+
232
+ _compute_target_containers: ___compute_target_containers_spec[typing_extensions.Self]
233
+
234
+ def _calculate_desired_replicas(
235
+ self,
236
+ n_current_replicas: int,
237
+ sum_metric: float,
238
+ n_containers_with_metrics: int,
239
+ n_total_containers: int,
240
+ target_metric_value: float,
241
+ ) -> int:
242
+ """Calculate the desired number of replicas to autoscale to."""
243
+ ...
244
+
245
+ class ___get_scaling_info_spec(typing_extensions.Protocol[SUPERSELF]):
246
+ def __call__(self, /, containers) -> tuple[float, int]:
247
+ """Get metrics using container exposed metrics endpoints."""
248
+ ...
249
+
250
+ async def aio(self, /, containers) -> tuple[float, int]:
251
+ """Get metrics using container exposed metrics endpoints."""
252
+ ...
253
+
254
+ _get_scaling_info: ___get_scaling_info_spec[typing_extensions.Self]
255
+
256
+ class ___get_metrics_spec(typing_extensions.Protocol[SUPERSELF]):
257
+ def __call__(self, /, url: str) -> typing.Optional[dict[str, list[typing.Any]]]: ...
258
+ async def aio(self, /, url: str) -> typing.Optional[dict[str, list[typing.Any]]]: ...
259
+
260
+ _get_metrics: ___get_metrics_spec[typing_extensions.Self]
261
+
262
+ class ___get_all_containers_spec(typing_extensions.Protocol[SUPERSELF]):
263
+ def __call__(self, /): ...
264
+ async def aio(self, /): ...
265
+
266
+ _get_all_containers: ___get_all_containers_spec[typing_extensions.Self]
267
+
268
+ class ___set_target_slots_spec(typing_extensions.Protocol[SUPERSELF]):
269
+ def __call__(self, /, target_slots: int): ...
270
+ async def aio(self, /, target_slots: int): ...
271
+
272
+ _set_target_slots: ___set_target_slots_spec[typing_extensions.Self]
273
+
274
+ def _make_scaling_decision(
275
+ self,
276
+ current_replicas: int,
277
+ autoscaling_decisions: list[tuple[float, int]],
278
+ scale_up_stabilization_window_seconds: int = 0,
279
+ scale_down_stabilization_window_seconds: int = 300,
280
+ min_containers: typing.Optional[int] = None,
281
+ max_containers: typing.Optional[int] = None,
282
+ buffer_containers: typing.Optional[int] = None,
283
+ ) -> int:
284
+ """Return the target number of containers following (simplified) Kubernetes HPA
285
+ stabilization-window semantics.
286
+
287
+ Args:
288
+ current_replicas: Current number of running Pods/containers.
289
+ autoscaling_decisions: List of (timestamp, desired_replicas) pairs, where
290
+ timestamp is a UNIX epoch float (seconds).
291
+ The list *must* contain at least one entry and should
292
+ already include the most-recent measurement.
293
+ scale_up_stabilization_window_seconds: 0 disables the up-window.
294
+ scale_down_stabilization_window_seconds: 0 disables the down-window.
295
+ min_containers / max_containers: Clamp the final decision to this range.
296
+
297
+ Returns:
298
+ The target number of containers.
299
+ """
300
+ ...
301
+
302
+ class __stop_spec(typing_extensions.Protocol[SUPERSELF]):
303
+ def __call__(self, /): ...
304
+ async def aio(self, /): ...
305
+
306
+ stop: __stop_spec[typing_extensions.Self]
307
+
308
+ class __flash_prometheus_autoscaler_spec(typing_extensions.Protocol):
309
+ def __call__(
310
+ self,
311
+ /,
312
+ app_name: str,
313
+ cls_name: str,
314
+ metrics_endpoint: str,
315
+ target_metric: str,
316
+ target_metric_value: float,
317
+ min_containers: typing.Optional[int] = None,
318
+ max_containers: typing.Optional[int] = None,
319
+ scale_up_tolerance: float = 0.1,
320
+ scale_down_tolerance: float = 0.1,
321
+ scale_up_stabilization_window_seconds: int = 0,
322
+ scale_down_stabilization_window_seconds: int = 300,
323
+ autoscaling_interval_seconds: int = 15,
324
+ buffer_containers: typing.Optional[int] = None,
325
+ ) -> FlashPrometheusAutoscaler:
326
+ """Autoscale a Flash service based on containers' Prometheus metrics.
327
+
328
+ The package `prometheus_client` is required to use this method.
329
+
330
+ This is a highly experimental method that can break or be removed at any time without warning.
331
+ Do not use this method unless explicitly instructed to do so by Modal support.
332
+ """
333
+ ...
334
+
335
+ async def aio(
336
+ self,
337
+ /,
338
+ app_name: str,
339
+ cls_name: str,
340
+ metrics_endpoint: str,
341
+ target_metric: str,
342
+ target_metric_value: float,
343
+ min_containers: typing.Optional[int] = None,
344
+ max_containers: typing.Optional[int] = None,
345
+ scale_up_tolerance: float = 0.1,
346
+ scale_down_tolerance: float = 0.1,
347
+ scale_up_stabilization_window_seconds: int = 0,
348
+ scale_down_stabilization_window_seconds: int = 300,
349
+ autoscaling_interval_seconds: int = 15,
350
+ buffer_containers: typing.Optional[int] = None,
351
+ ) -> FlashPrometheusAutoscaler:
352
+ """Autoscale a Flash service based on containers' Prometheus metrics.
353
+
354
+ The package `prometheus_client` is required to use this method.
355
+
356
+ This is a highly experimental method that can break or be removed at any time without warning.
357
+ Do not use this method unless explicitly instructed to do so by Modal support.
358
+ """
359
+ ...
360
+
361
+ flash_prometheus_autoscaler: __flash_prometheus_autoscaler_spec
362
+
363
+ class __flash_get_containers_spec(typing_extensions.Protocol):
364
+ def __call__(self, /, app_name: str, cls_name: str) -> list[dict[str, typing.Any]]:
365
+ """Return a list of flash containers for a deployed Flash service.
366
+
367
+ This is a highly experimental method that can break or be removed at any time without warning.
368
+ Do not use this method unless explicitly instructed to do so by Modal support.
369
+ """
370
+ ...
371
+
372
+ async def aio(self, /, app_name: str, cls_name: str) -> list[dict[str, typing.Any]]:
373
+ """Return a list of flash containers for a deployed Flash service.
374
+
375
+ This is a highly experimental method that can break or be removed at any time without warning.
376
+ Do not use this method unless explicitly instructed to do so by Modal support.
377
+ """
378
+ ...
379
+
380
+ flash_get_containers: __flash_get_containers_spec
@@ -21,7 +21,7 @@ class ModalMagics(Magics):
21
21
  **Example:**
22
22
 
23
23
  ```python notest
24
- %modal from main/my-app import my_function, MyClass as Foo
24
+ %modal from my-app import my_function, MyClass as Foo
25
25
 
26
26
  # Now you can call my_function() and Foo from your notebook.
27
27
  my_function.remote()
@@ -30,7 +30,7 @@ class ModalMagics(Magics):
30
30
  """
31
31
  line = line.strip()
32
32
  if not line.startswith("from "):
33
- print("Invalid syntax. Use: %modal from <env>/<app> import <function|Class>[, <function|Class> [as alias]]")
33
+ print("Invalid syntax. Use: %modal from [env/]<app> import <function|Class>[, <function|Class> [as alias]]")
34
34
  return
35
35
 
36
36
  # Remove the initial "from "
@@ -40,11 +40,12 @@ class ModalMagics(Magics):
40
40
  print("Invalid syntax. Missing 'import' keyword.")
41
41
  return
42
42
 
43
- # Parse environment and app from "env/app"
43
+ # Parse environment and app from "[env/]app"
44
+ environment: str | None
44
45
  if "/" not in env_app_part:
45
- print("Invalid app specification. Expected format: <env>/<app>")
46
- return
47
- environment, app = env_app_part.split("/", 1)
46
+ environment, app = None, env_app_part
47
+ else:
48
+ environment, app = env_app_part.split("/", 1)
48
49
 
49
50
  # Parse the import items (multiple imports separated by commas)
50
51
  import_items = [item.strip() for item in import_part.split(",")]
@@ -73,7 +74,10 @@ class ModalMagics(Magics):
73
74
 
74
75
  # Set the loaded object in the notebook namespace
75
76
  self.shell.user_ns[alias] = obj # type: ignore
76
- print(f"Loaded {alias!r} from environment {environment!r} and app {app!r}.")
77
+ if environment:
78
+ print(f"Loaded {alias!r} from environment {environment!r} and app {app!r}.")
79
+ else:
80
+ print(f"Loaded {alias!r} from app {app!r}.")
77
81
 
78
82
 
79
83
  def load_ipython_extension(ipython):
modal/file_io.py CHANGED
@@ -13,7 +13,6 @@ import json
13
13
  from grpclib.exceptions import GRPCError, StreamTerminatedError
14
14
 
15
15
  from modal._utils.async_utils import TaskContext
16
- from modal._utils.grpc_utils import retry_transient_errors
17
16
  from modal.exception import ClientClosed
18
17
  from modal_proto import api_pb2
19
18
 
@@ -57,8 +56,7 @@ async def _delete_bytes(file: "_FileIO", start: Optional[int] = None, end: Optio
57
56
  if start is not None and end is not None:
58
57
  if start >= end:
59
58
  raise ValueError("start must be less than end")
60
- resp = await retry_transient_errors(
61
- file._client.stub.ContainerFilesystemExec,
59
+ resp = await file._client.stub.ContainerFilesystemExec(
62
60
  api_pb2.ContainerFilesystemExecRequest(
63
61
  file_delete_bytes_request=api_pb2.ContainerFileDeleteBytesRequest(
64
62
  file_descriptor=file._file_descriptor,
@@ -85,8 +83,7 @@ async def _replace_bytes(file: "_FileIO", data: bytes, start: Optional[int] = No
85
83
  raise InvalidError("start must be less than end")
86
84
  if len(data) > WRITE_CHUNK_SIZE:
87
85
  raise InvalidError("Write request payload exceeds 16 MiB limit")
88
- resp = await retry_transient_errors(
89
- file._client.stub.ContainerFilesystemExec,
86
+ resp = await file._client.stub.ContainerFilesystemExec(
90
87
  api_pb2.ContainerFilesystemExecRequest(
91
88
  file_write_replace_bytes_request=api_pb2.ContainerFileWriteReplaceBytesRequest(
92
89
  file_descriptor=file._file_descriptor,
@@ -117,13 +114,18 @@ class FileWatchEvent:
117
114
  # The FileIO class is designed to mimic Python's io.FileIO
118
115
  # See https://github.com/python/cpython/blob/main/Lib/_pyio.py#L1459
119
116
  class _FileIO(Generic[T]):
120
- """FileIO handle, used in the Sandbox filesystem API.
117
+ """[Alpha] FileIO handle, used in the Sandbox filesystem API.
121
118
 
122
119
  The API is designed to mimic Python's io.FileIO.
123
120
 
121
+ Currently this API is in Alpha and is subject to change. File I/O operations
122
+ may be limited in size to 100 MiB, and the throughput of requests is
123
+ restricted in the current implementation. For our recommendations on large file transfers
124
+ see the Sandbox [filesystem access guide](https://modal.com/docs/guide/sandbox-files).
125
+
124
126
  **Usage**
125
127
 
126
- ```python
128
+ ```python notest
127
129
  import modal
128
130
 
129
131
  app = modal.App.lookup("my-app", create_if_missing=True)
@@ -144,11 +146,12 @@ class _FileIO(Generic[T]):
144
146
  _task_id: str = ""
145
147
  _file_descriptor: str = ""
146
148
  _client: _Client
147
- _watch_output_buffer: list[Optional[bytes]] = []
149
+ _watch_output_buffer: list[Union[Optional[bytes], Exception]] = []
148
150
 
149
151
  def __init__(self, client: _Client, task_id: str) -> None:
150
152
  self._client = client
151
153
  self._task_id = task_id
154
+ self._watch_output_buffer = []
152
155
 
153
156
  def _validate_mode(self, mode: str) -> None:
154
157
  if not any(char in mode for char in "rwax"):
@@ -173,11 +176,7 @@ class _FileIO(Generic[T]):
173
176
  raise ValueError(f"Invalid file mode: {mode}")
174
177
  seen_chars.add(char)
175
178
 
176
- def _handle_error(self, error: api_pb2.SystemErrorMessage) -> None:
177
- error_class = ERROR_MAPPING.get(error.error_code, FilesystemExecutionError)
178
- raise error_class(error.error_message)
179
-
180
- async def _consume_output(self, exec_id: str) -> AsyncIterator[Optional[bytes]]:
179
+ async def _consume_output(self, exec_id: str) -> AsyncIterator[Union[Optional[bytes], Exception]]:
181
180
  req = api_pb2.ContainerFilesystemExecGetOutputRequest(
182
181
  exec_id=exec_id,
183
182
  timeout=55,
@@ -187,7 +186,8 @@ class _FileIO(Generic[T]):
187
186
  yield None
188
187
  break
189
188
  if batch.HasField("error"):
190
- self._handle_error(batch.error)
189
+ error_class = ERROR_MAPPING.get(batch.error.error_code, FilesystemExecutionError)
190
+ yield error_class(batch.error.error_message)
191
191
  for message in batch.output:
192
192
  yield message
193
193
 
@@ -236,6 +236,8 @@ class _FileIO(Generic[T]):
236
236
  if data is None:
237
237
  completed = True
238
238
  break
239
+ if isinstance(data, Exception):
240
+ raise data
239
241
  output += data
240
242
  except (GRPCError, StreamTerminatedError) as exc:
241
243
  if retries_remaining > 0:
@@ -256,8 +258,7 @@ class _FileIO(Generic[T]):
256
258
  raise TypeError("Expected str when in text mode")
257
259
 
258
260
  async def _open_file(self, path: str, mode: str) -> None:
259
- resp = await retry_transient_errors(
260
- self._client.stub.ContainerFilesystemExec,
261
+ resp = await self._client.stub.ContainerFilesystemExec(
261
262
  api_pb2.ContainerFilesystemExecRequest(
262
263
  file_open_request=api_pb2.ContainerFileOpenRequest(path=path, mode=mode),
263
264
  task_id=self._task_id,
@@ -280,8 +281,7 @@ class _FileIO(Generic[T]):
280
281
  return self
281
282
 
282
283
  async def _make_read_request(self, n: Optional[int]) -> bytes:
283
- resp = await retry_transient_errors(
284
- self._client.stub.ContainerFilesystemExec,
284
+ resp = await self._client.stub.ContainerFilesystemExec(
285
285
  api_pb2.ContainerFilesystemExecRequest(
286
286
  file_read_request=api_pb2.ContainerFileReadRequest(file_descriptor=self._file_descriptor, n=n),
287
287
  task_id=self._task_id,
@@ -304,8 +304,7 @@ class _FileIO(Generic[T]):
304
304
  """Read a single line from the current position."""
305
305
  self._check_closed()
306
306
  self._check_readable()
307
- resp = await retry_transient_errors(
308
- self._client.stub.ContainerFilesystemExec,
307
+ resp = await self._client.stub.ContainerFilesystemExec(
309
308
  api_pb2.ContainerFilesystemExecRequest(
310
309
  file_read_line_request=api_pb2.ContainerFileReadLineRequest(file_descriptor=self._file_descriptor),
311
310
  task_id=self._task_id,
@@ -346,8 +345,7 @@ class _FileIO(Generic[T]):
346
345
  raise ValueError("Write request payload exceeds 1 GiB limit")
347
346
  for i in range(0, len(data), WRITE_CHUNK_SIZE):
348
347
  chunk = data[i : i + WRITE_CHUNK_SIZE]
349
- resp = await retry_transient_errors(
350
- self._client.stub.ContainerFilesystemExec,
348
+ resp = await self._client.stub.ContainerFilesystemExec(
351
349
  api_pb2.ContainerFilesystemExecRequest(
352
350
  file_write_request=api_pb2.ContainerFileWriteRequest(
353
351
  file_descriptor=self._file_descriptor,
@@ -362,8 +360,7 @@ class _FileIO(Generic[T]):
362
360
  """Flush the buffer to disk."""
363
361
  self._check_closed()
364
362
  self._check_writable()
365
- resp = await retry_transient_errors(
366
- self._client.stub.ContainerFilesystemExec,
363
+ resp = await self._client.stub.ContainerFilesystemExec(
367
364
  api_pb2.ContainerFilesystemExecRequest(
368
365
  file_flush_request=api_pb2.ContainerFileFlushRequest(file_descriptor=self._file_descriptor),
369
366
  task_id=self._task_id,
@@ -388,8 +385,7 @@ class _FileIO(Generic[T]):
388
385
  (relative to the current position) and 2 (relative to the file's end).
389
386
  """
390
387
  self._check_closed()
391
- resp = await retry_transient_errors(
392
- self._client.stub.ContainerFilesystemExec,
388
+ resp = await self._client.stub.ContainerFilesystemExec(
393
389
  api_pb2.ContainerFilesystemExecRequest(
394
390
  file_seek_request=api_pb2.ContainerFileSeekRequest(
395
391
  file_descriptor=self._file_descriptor,
@@ -405,8 +401,7 @@ class _FileIO(Generic[T]):
405
401
  async def ls(cls, path: str, client: _Client, task_id: str) -> list[str]:
406
402
  """List the contents of the provided directory."""
407
403
  self = _FileIO(client, task_id)
408
- resp = await retry_transient_errors(
409
- self._client.stub.ContainerFilesystemExec,
404
+ resp = await self._client.stub.ContainerFilesystemExec(
410
405
  api_pb2.ContainerFilesystemExecRequest(
411
406
  file_ls_request=api_pb2.ContainerFileLsRequest(path=path),
412
407
  task_id=task_id,
@@ -422,8 +417,7 @@ class _FileIO(Generic[T]):
422
417
  async def mkdir(cls, path: str, client: _Client, task_id: str, parents: bool = False) -> None:
423
418
  """Create a new directory."""
424
419
  self = _FileIO(client, task_id)
425
- resp = await retry_transient_errors(
426
- self._client.stub.ContainerFilesystemExec,
420
+ resp = await self._client.stub.ContainerFilesystemExec(
427
421
  api_pb2.ContainerFilesystemExecRequest(
428
422
  file_mkdir_request=api_pb2.ContainerFileMkdirRequest(path=path, make_parents=parents),
429
423
  task_id=self._task_id,
@@ -435,8 +429,7 @@ class _FileIO(Generic[T]):
435
429
  async def rm(cls, path: str, client: _Client, task_id: str, recursive: bool = False) -> None:
436
430
  """Remove a file or directory in the Sandbox."""
437
431
  self = _FileIO(client, task_id)
438
- resp = await retry_transient_errors(
439
- self._client.stub.ContainerFilesystemExec,
432
+ resp = await self._client.stub.ContainerFilesystemExec(
440
433
  api_pb2.ContainerFilesystemExecRequest(
441
434
  file_rm_request=api_pb2.ContainerFileRmRequest(path=path, recursive=recursive),
442
435
  task_id=self._task_id,
@@ -455,8 +448,7 @@ class _FileIO(Generic[T]):
455
448
  timeout: Optional[int] = None,
456
449
  ) -> AsyncIterator[FileWatchEvent]:
457
450
  self = _FileIO(client, task_id)
458
- resp = await retry_transient_errors(
459
- self._client.stub.ContainerFilesystemExec,
451
+ resp = await self._client.stub.ContainerFilesystemExec(
460
452
  api_pb2.ContainerFilesystemExecRequest(
461
453
  file_watch_request=api_pb2.ContainerFileWatchRequest(
462
454
  path=path,
@@ -475,6 +467,8 @@ class _FileIO(Generic[T]):
475
467
  item = self._watch_output_buffer.pop(0)
476
468
  if item is None:
477
469
  break
470
+ if isinstance(item, Exception):
471
+ raise item
478
472
  buffer += item
479
473
  # a single event may be split across multiple messages
480
474
  # the end of an event is marked by two newlines
@@ -496,8 +490,7 @@ class _FileIO(Generic[T]):
496
490
 
497
491
  async def _close(self) -> None:
498
492
  # Buffer is flushed by the runner on close
499
- resp = await retry_transient_errors(
500
- self._client.stub.ContainerFilesystemExec,
493
+ resp = await self._client.stub.ContainerFilesystemExec(
501
494
  api_pb2.ContainerFilesystemExecRequest(
502
495
  file_close_request=api_pb2.ContainerFileCloseRequest(file_descriptor=self._file_descriptor),
503
496
  task_id=self._task_id,