modal 1.0.6.dev58__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 (147) hide show
  1. modal/__main__.py +3 -4
  2. modal/_billing.py +80 -0
  3. modal/_clustered_functions.py +7 -3
  4. modal/_clustered_functions.pyi +4 -2
  5. modal/_container_entrypoint.py +41 -49
  6. modal/_functions.py +424 -195
  7. modal/_grpc_client.py +171 -0
  8. modal/_load_context.py +105 -0
  9. modal/_object.py +68 -20
  10. modal/_output.py +58 -45
  11. modal/_partial_function.py +36 -11
  12. modal/_pty.py +7 -3
  13. modal/_resolver.py +21 -35
  14. modal/_runtime/asgi.py +4 -3
  15. modal/_runtime/container_io_manager.py +301 -186
  16. modal/_runtime/container_io_manager.pyi +70 -61
  17. modal/_runtime/execution_context.py +18 -2
  18. modal/_runtime/execution_context.pyi +4 -1
  19. modal/_runtime/gpu_memory_snapshot.py +170 -63
  20. modal/_runtime/user_code_imports.py +28 -58
  21. modal/_serialization.py +57 -1
  22. modal/_utils/async_utils.py +33 -12
  23. modal/_utils/auth_token_manager.py +2 -5
  24. modal/_utils/blob_utils.py +110 -53
  25. modal/_utils/function_utils.py +49 -42
  26. modal/_utils/grpc_utils.py +80 -50
  27. modal/_utils/mount_utils.py +26 -1
  28. modal/_utils/name_utils.py +17 -3
  29. modal/_utils/task_command_router_client.py +536 -0
  30. modal/_utils/time_utils.py +34 -6
  31. modal/app.py +219 -83
  32. modal/app.pyi +229 -56
  33. modal/billing.py +5 -0
  34. modal/{requirements → builder}/2025.06.txt +1 -0
  35. modal/{requirements → builder}/PREVIEW.txt +1 -0
  36. modal/cli/_download.py +19 -3
  37. modal/cli/_traceback.py +3 -2
  38. modal/cli/app.py +4 -4
  39. modal/cli/cluster.py +15 -7
  40. modal/cli/config.py +5 -3
  41. modal/cli/container.py +7 -6
  42. modal/cli/dict.py +22 -16
  43. modal/cli/entry_point.py +12 -5
  44. modal/cli/environment.py +5 -4
  45. modal/cli/import_refs.py +3 -3
  46. modal/cli/launch.py +102 -5
  47. modal/cli/network_file_system.py +9 -13
  48. modal/cli/profile.py +3 -2
  49. modal/cli/programs/launch_instance_ssh.py +94 -0
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/run_marimo.py +95 -0
  52. modal/cli/programs/vscode.py +1 -1
  53. modal/cli/queues.py +57 -26
  54. modal/cli/run.py +58 -16
  55. modal/cli/secret.py +48 -22
  56. modal/cli/utils.py +3 -4
  57. modal/cli/volume.py +28 -25
  58. modal/client.py +13 -116
  59. modal/client.pyi +9 -91
  60. modal/cloud_bucket_mount.py +5 -3
  61. modal/cloud_bucket_mount.pyi +5 -1
  62. modal/cls.py +130 -102
  63. modal/cls.pyi +45 -85
  64. modal/config.py +29 -10
  65. modal/container_process.py +291 -13
  66. modal/container_process.pyi +95 -32
  67. modal/dict.py +282 -63
  68. modal/dict.pyi +423 -73
  69. modal/environments.py +15 -27
  70. modal/environments.pyi +5 -15
  71. modal/exception.py +8 -0
  72. modal/experimental/__init__.py +143 -38
  73. modal/experimental/flash.py +247 -78
  74. modal/experimental/flash.pyi +137 -9
  75. modal/file_io.py +14 -28
  76. modal/file_io.pyi +2 -2
  77. modal/file_pattern_matcher.py +25 -16
  78. modal/functions.pyi +134 -61
  79. modal/image.py +255 -86
  80. modal/image.pyi +300 -62
  81. modal/io_streams.py +436 -126
  82. modal/io_streams.pyi +236 -171
  83. modal/mount.py +62 -157
  84. modal/mount.pyi +45 -172
  85. modal/network_file_system.py +30 -53
  86. modal/network_file_system.pyi +16 -76
  87. modal/object.pyi +42 -8
  88. modal/parallel_map.py +821 -113
  89. modal/parallel_map.pyi +134 -0
  90. modal/partial_function.pyi +4 -1
  91. modal/proxy.py +16 -7
  92. modal/proxy.pyi +10 -2
  93. modal/queue.py +263 -61
  94. modal/queue.pyi +409 -66
  95. modal/runner.py +112 -92
  96. modal/runner.pyi +45 -27
  97. modal/sandbox.py +451 -124
  98. modal/sandbox.pyi +513 -67
  99. modal/secret.py +291 -67
  100. modal/secret.pyi +425 -19
  101. modal/serving.py +7 -11
  102. modal/serving.pyi +7 -8
  103. modal/snapshot.py +11 -8
  104. modal/token_flow.py +4 -4
  105. modal/volume.py +344 -98
  106. modal/volume.pyi +464 -68
  107. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
  108. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  109. modal_docs/mdmd/mdmd.py +11 -1
  110. modal_proto/api.proto +399 -67
  111. modal_proto/api_grpc.py +241 -1
  112. modal_proto/api_pb2.py +1395 -1000
  113. modal_proto/api_pb2.pyi +1239 -79
  114. modal_proto/api_pb2_grpc.py +499 -4
  115. modal_proto/api_pb2_grpc.pyi +162 -14
  116. modal_proto/modal_api_grpc.py +175 -160
  117. modal_proto/sandbox_router.proto +145 -0
  118. modal_proto/sandbox_router_grpc.py +105 -0
  119. modal_proto/sandbox_router_pb2.py +149 -0
  120. modal_proto/sandbox_router_pb2.pyi +333 -0
  121. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  122. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  123. modal_proto/task_command_router.proto +144 -0
  124. modal_proto/task_command_router_grpc.py +105 -0
  125. modal_proto/task_command_router_pb2.py +149 -0
  126. modal_proto/task_command_router_pb2.pyi +333 -0
  127. modal_proto/task_command_router_pb2_grpc.py +203 -0
  128. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  129. modal_version/__init__.py +1 -1
  130. modal-1.0.6.dev58.dist-info/RECORD +0 -183
  131. modal_proto/modal_options_grpc.py +0 -3
  132. modal_proto/options.proto +0 -19
  133. modal_proto/options_grpc.py +0 -3
  134. modal_proto/options_pb2.py +0 -35
  135. modal_proto/options_pb2.pyi +0 -20
  136. modal_proto/options_pb2_grpc.py +0 -4
  137. modal_proto/options_pb2_grpc.pyi +0 -7
  138. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  139. /modal/{requirements → builder}/2023.12.txt +0 -0
  140. /modal/{requirements → builder}/2024.04.txt +0 -0
  141. /modal/{requirements → builder}/2024.10.txt +0 -0
  142. /modal/{requirements → builder}/README.md +0 -0
  143. /modal/{requirements → builder}/base-images.json +0 -0
  144. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  145. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  146. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  147. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
@@ -8,12 +8,8 @@ import typing
8
8
  import urllib.parse
9
9
  import uuid
10
10
  from collections.abc import AsyncIterator
11
- from dataclasses import dataclass
12
- from typing import (
13
- Any,
14
- Optional,
15
- TypeVar,
16
- )
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Optional, TypeVar
17
13
 
18
14
  import grpclib.client
19
15
  import grpclib.config
@@ -28,6 +24,7 @@ from grpclib.protocol import H2Protocol
28
24
  from modal.exception import AuthError, ConnectionError
29
25
  from modal_version import __version__
30
26
 
27
+ from .._traceback import suppress_tb_frames
31
28
  from .async_utils import retry
32
29
  from .logger import logger
33
30
 
@@ -35,6 +32,7 @@ RequestType = TypeVar("RequestType", bound=Message)
35
32
  ResponseType = TypeVar("ResponseType", bound=Message)
36
33
 
37
34
  if typing.TYPE_CHECKING:
35
+ import modal._grpc_client
38
36
  import modal.client
39
37
 
40
38
  # Monkey patches grpclib to have a Modal User Agent header.
@@ -165,7 +163,7 @@ if typing.TYPE_CHECKING:
165
163
 
166
164
 
167
165
  async def unary_stream(
168
- method: "modal.client.UnaryStreamWrapper[RequestType, ResponseType]",
166
+ method: "modal._grpc_client.UnaryStreamWrapper[RequestType, ResponseType]",
169
167
  request: RequestType,
170
168
  metadata: Optional[Any] = None,
171
169
  ) -> AsyncIterator[ResponseType]:
@@ -174,36 +172,66 @@ async def unary_stream(
174
172
  yield item
175
173
 
176
174
 
175
+ @dataclass(frozen=True)
176
+ class Retry:
177
+ base_delay: float = 0.1
178
+ max_delay: float = 1
179
+ delay_factor: float = 2
180
+ max_retries: Optional[int] = 3
181
+ additional_status_codes: list = field(default_factory=list)
182
+ attempt_timeout: Optional[float] = None # timeout for each attempt
183
+ total_timeout: Optional[float] = None # timeout for the entire function call
184
+ attempt_timeout_floor: float = 2.0 # always have at least this much timeout (only for total_timeout)
185
+ warning_message: Optional[RetryWarningMessage] = None
186
+
187
+
177
188
  async def retry_transient_errors(
178
- fn: "modal.client.UnaryUnaryWrapper[RequestType, ResponseType]",
179
- *args,
180
- base_delay: float = 0.1,
181
- max_delay: float = 1,
182
- delay_factor: float = 2,
189
+ fn: "grpclib.client.UnaryUnaryMethod[RequestType, ResponseType]",
190
+ req: RequestType,
183
191
  max_retries: Optional[int] = 3,
184
- additional_status_codes: list = [],
185
- attempt_timeout: Optional[float] = None, # timeout for each attempt
186
- total_timeout: Optional[float] = None, # timeout for the entire function call
187
- attempt_timeout_floor=2.0, # always have at least this much timeout (only for total_timeout)
188
- retry_warning_message: Optional[RetryWarningMessage] = None,
189
- metadata: list[tuple[str, str]] = [],
192
+ ) -> ResponseType:
193
+ """Minimum API version of _retry_transient_errors that works with grpclib.client.UnaryUnaryMethod.
194
+
195
+ Used by modal server.
196
+ """
197
+ return await _retry_transient_errors(fn, req, retry=Retry(max_retries=max_retries))
198
+
199
+
200
+ async def _retry_transient_errors(
201
+ fn: typing.Union[
202
+ "modal._grpc_client.UnaryUnaryWrapper[RequestType, ResponseType]",
203
+ "grpclib.client.UnaryUnaryMethod[RequestType, ResponseType]",
204
+ ],
205
+ req: RequestType,
206
+ retry: Retry,
207
+ metadata: Optional[list[tuple[str, str]]] = None,
190
208
  ) -> ResponseType:
191
209
  """Retry on transient gRPC failures with back-off until max_retries is reached.
192
210
  If max_retries is None, retry forever."""
211
+ import modal._grpc_client
212
+
213
+ if isinstance(fn, modal._grpc_client.UnaryUnaryWrapper):
214
+ fn_callable = fn.direct
215
+ elif isinstance(fn, grpclib.client.UnaryUnaryMethod):
216
+ fn_callable = fn # type: ignore
217
+ else:
218
+ raise ValueError("Only modal._grpc_client.UnaryUnaryWrapper and grpclib.client.UnaryUnaryMethod are supported")
193
219
 
194
- delay = base_delay
220
+ delay = retry.base_delay
195
221
  n_retries = 0
196
222
 
197
- status_codes = [*RETRYABLE_GRPC_STATUS_CODES, *additional_status_codes]
223
+ status_codes = [*RETRYABLE_GRPC_STATUS_CODES, *retry.additional_status_codes]
198
224
 
199
225
  idempotency_key = str(uuid.uuid4())
200
226
 
201
227
  t0 = time.time()
202
- if total_timeout is not None:
203
- total_deadline = t0 + total_timeout
228
+ if retry.total_timeout is not None:
229
+ total_deadline = t0 + retry.total_timeout
204
230
  else:
205
231
  total_deadline = None
206
232
 
233
+ metadata = (metadata or []) + [("x-modal-timestamp", str(time.time()))]
234
+
207
235
  while True:
208
236
  attempt_metadata = [
209
237
  ("x-idempotency-key", idempotency_key),
@@ -213,16 +241,17 @@ async def retry_transient_errors(
213
241
  if n_retries > 0:
214
242
  attempt_metadata.append(("x-retry-delay", str(time.time() - t0)))
215
243
  timeouts = []
216
- if attempt_timeout is not None:
217
- timeouts.append(attempt_timeout)
218
- if total_timeout is not None:
219
- timeouts.append(max(total_deadline - time.time(), attempt_timeout_floor))
244
+ if retry.attempt_timeout is not None:
245
+ timeouts.append(retry.attempt_timeout)
246
+ if retry.total_timeout is not None and total_deadline is not None:
247
+ timeouts.append(max(total_deadline - time.time(), retry.attempt_timeout_floor))
220
248
  if timeouts:
221
249
  timeout = min(timeouts) # In case the function provided both types of timeouts
222
250
  else:
223
251
  timeout = None
224
252
  try:
225
- return await fn(*args, metadata=attempt_metadata, timeout=timeout)
253
+ with suppress_tb_frames(1):
254
+ return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
226
255
  except (StreamTerminatedError, GRPCError, OSError, asyncio.TimeoutError, AttributeError) as exc:
227
256
  if isinstance(exc, GRPCError) and exc.status not in status_codes:
228
257
  if exc.status == Status.UNAUTHENTICATED:
@@ -230,45 +259,46 @@ async def retry_transient_errors(
230
259
  else:
231
260
  raise exc
232
261
 
233
- if max_retries is not None and n_retries >= max_retries:
262
+ if retry.max_retries is not None and n_retries >= retry.max_retries:
234
263
  final_attempt = True
235
- elif total_deadline is not None and time.time() + delay + attempt_timeout_floor >= total_deadline:
264
+ elif total_deadline is not None and time.time() + delay + retry.attempt_timeout_floor >= total_deadline:
236
265
  final_attempt = True
237
266
  else:
238
267
  final_attempt = False
239
268
 
240
- if final_attempt:
241
- logger.debug(
242
- f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
243
- f"{total_deadline=} for {fn.name} ({idempotency_key[:8]})"
244
- )
245
- if isinstance(exc, OSError):
246
- raise ConnectionError(str(exc))
247
- elif isinstance(exc, asyncio.TimeoutError):
248
- raise ConnectionError(str(exc))
249
- else:
269
+ with suppress_tb_frames(1):
270
+ if final_attempt:
271
+ logger.debug(
272
+ f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
273
+ f"{total_deadline=} for {fn.name} ({idempotency_key[:8]})"
274
+ )
275
+ if isinstance(exc, OSError):
276
+ raise ConnectionError(str(exc))
277
+ elif isinstance(exc, asyncio.TimeoutError):
278
+ raise ConnectionError(str(exc))
279
+ else:
280
+ raise exc
281
+
282
+ if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
283
+ # StreamTerminatedError are not properly raised in grpclib<=0.4.7
284
+ # fixed in https://github.com/vmagamedov/grpclib/issues/185
285
+ # TODO: update to newer version (>=0.4.8) once stable
250
286
  raise exc
251
287
 
252
- if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
253
- # StreamTerminatedError are not properly raised in grpclib<=0.4.7
254
- # fixed in https://github.com/vmagamedov/grpclib/issues/185
255
- # TODO: update to newer version (>=0.4.8) once stable
256
- raise exc
257
-
258
288
  logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn.name} ({idempotency_key[:8]})")
259
289
 
260
290
  n_retries += 1
261
291
 
262
292
  if (
263
- retry_warning_message
264
- and n_retries % retry_warning_message.warning_interval == 0
293
+ retry.warning_message
294
+ and n_retries % retry.warning_message.warning_interval == 0
265
295
  and isinstance(exc, GRPCError)
266
- and exc.status in retry_warning_message.errors_to_warn_for
296
+ and exc.status in retry.warning_message.errors_to_warn_for
267
297
  ):
268
- logger.warning(retry_warning_message.message)
298
+ logger.warning(retry.warning_message.message)
269
299
 
270
300
  await asyncio.sleep(delay)
271
- delay = min(delay * delay_factor, max_delay)
301
+ delay = min(delay * retry.delay_factor, retry.max_delay)
272
302
 
273
303
 
274
304
  def find_free_port() -> int:
@@ -3,7 +3,9 @@ import posixpath
3
3
  import typing
4
4
  from collections.abc import Mapping, Sequence
5
5
  from pathlib import PurePath, PurePosixPath
6
- from typing import Union
6
+ from typing import Optional, Union
7
+
8
+ from typing_extensions import TypeGuard
7
9
 
8
10
  from ..cloud_bucket_mount import _CloudBucketMount
9
11
  from ..exception import InvalidError
@@ -76,3 +78,26 @@ def validate_volumes(
76
78
  )
77
79
 
78
80
  return validated_volumes
81
+
82
+
83
+ def validate_only_modal_volumes(
84
+ volumes: Optional[Optional[dict[Union[str, PurePosixPath], _Volume]]],
85
+ caller_name: str,
86
+ ) -> Sequence[tuple[str, _Volume]]:
87
+ """Validate all volumes are `modal.Volume`."""
88
+ if volumes is None:
89
+ return []
90
+
91
+ validated_volumes = validate_volumes(volumes)
92
+
93
+ # Although the typing forbids `_CloudBucketMount` for type checking, one can still pass a `_CloudBucketMount`
94
+ # during runtime, so we'll check the type here.
95
+ def all_modal_volumes(
96
+ vols: Sequence[tuple[str, Union[_Volume, _CloudBucketMount]]],
97
+ ) -> TypeGuard[Sequence[tuple[str, _Volume]]]:
98
+ return all(isinstance(v, _Volume) for _, v in vols)
99
+
100
+ if not all_modal_volumes(validated_volumes):
101
+ raise InvalidError(f"{caller_name} only supports volumes that are modal.Volume")
102
+
103
+ return validated_volumes
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import re
3
+ from collections.abc import Mapping
3
4
 
4
5
  from ..exception import InvalidError
5
6
 
@@ -31,12 +32,25 @@ def is_valid_environment_name(name: str) -> bool:
31
32
  return len(name) <= 64 and re.match(r"^[a-zA-Z0-9][a-zA-Z0-9-_.]+$", name) is not None
32
33
 
33
34
 
34
- def is_valid_tag(tag: str) -> bool:
35
- """Tags are alphanumeric, dashes, periods, and underscores, and must be 50 characters or less"""
36
- pattern = r"^[a-zA-Z0-9._-]{1,50}$"
35
+ def is_valid_tag(tag: str, max_length: int = 50) -> bool:
36
+ """Tags are alphanumeric, dashes, periods, and underscores, and not longer than the max_length."""
37
+ pattern = rf"^[a-zA-Z0-9._-]{{1,{max_length}}}$"
37
38
  return bool(re.match(pattern, tag))
38
39
 
39
40
 
41
+ def check_tag_dict(tags: Mapping[str, str]) -> None:
42
+ rules = (
43
+ "\n\nTags may contain only alphanumeric characters, dashes, periods, or underscores, "
44
+ "and must be 63 characters or less."
45
+ )
46
+ max_length = 63
47
+ for key, value in tags.items():
48
+ if not is_valid_tag(key, max_length):
49
+ raise InvalidError(f"Invalid tag key: {key!r}.{rules}")
50
+ if not is_valid_tag(value, max_length):
51
+ raise InvalidError(f"Invalid tag value: {value!r}.{rules}")
52
+
53
+
40
54
  def check_object_name(name: str, object_type: str) -> None:
41
55
  message = (
42
56
  f"Invalid {object_type} name: '{name}'."