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.
- modal/__init__.py +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +11 -12
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {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
|
modal/experimental/ipython.py
CHANGED
|
@@ -21,7 +21,7 @@ class ModalMagics(Magics):
|
|
|
21
21
|
**Example:**
|
|
22
22
|
|
|
23
23
|
```python notest
|
|
24
|
-
%modal from
|
|
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 <
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|