modal 1.1.5.dev66__py3-none-any.whl → 1.3.1.dev8__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 (143) hide show
  1. modal/__init__.py +4 -4
  2. modal/__main__.py +4 -29
  3. modal/_billing.py +84 -0
  4. modal/_clustered_functions.py +1 -3
  5. modal/_container_entrypoint.py +33 -208
  6. modal/_functions.py +171 -138
  7. modal/_grpc_client.py +191 -0
  8. modal/_ipython.py +16 -6
  9. modal/_load_context.py +106 -0
  10. modal/_object.py +72 -21
  11. modal/_output.py +12 -14
  12. modal/_partial_function.py +31 -4
  13. modal/_resolver.py +44 -57
  14. modal/_runtime/container_io_manager.py +30 -28
  15. modal/_runtime/container_io_manager.pyi +42 -44
  16. modal/_runtime/gpu_memory_snapshot.py +9 -7
  17. modal/_runtime/user_code_event_loop.py +80 -0
  18. modal/_runtime/user_code_imports.py +236 -10
  19. modal/_serialization.py +2 -1
  20. modal/_traceback.py +4 -13
  21. modal/_tunnel.py +16 -11
  22. modal/_tunnel.pyi +25 -3
  23. modal/_utils/async_utils.py +337 -10
  24. modal/_utils/auth_token_manager.py +1 -4
  25. modal/_utils/blob_utils.py +29 -22
  26. modal/_utils/function_utils.py +20 -21
  27. modal/_utils/grpc_testing.py +6 -3
  28. modal/_utils/grpc_utils.py +223 -64
  29. modal/_utils/mount_utils.py +26 -1
  30. modal/_utils/name_utils.py +2 -3
  31. modal/_utils/package_utils.py +0 -1
  32. modal/_utils/rand_pb_testing.py +8 -1
  33. modal/_utils/task_command_router_client.py +524 -0
  34. modal/_vendor/cloudpickle.py +144 -48
  35. modal/app.py +285 -105
  36. modal/app.pyi +216 -53
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +6 -3
  39. modal/builder/PREVIEW.txt +2 -1
  40. modal/builder/base-images.json +4 -2
  41. modal/cli/_download.py +19 -3
  42. modal/cli/cluster.py +4 -2
  43. modal/cli/config.py +3 -1
  44. modal/cli/container.py +5 -4
  45. modal/cli/dict.py +5 -2
  46. modal/cli/entry_point.py +26 -2
  47. modal/cli/environment.py +2 -16
  48. modal/cli/launch.py +1 -76
  49. modal/cli/network_file_system.py +5 -20
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/vscode.py +1 -1
  52. modal/cli/queues.py +5 -4
  53. modal/cli/run.py +24 -204
  54. modal/cli/secret.py +1 -2
  55. modal/cli/shell.py +375 -0
  56. modal/cli/utils.py +1 -13
  57. modal/cli/volume.py +11 -17
  58. modal/client.py +16 -125
  59. modal/client.pyi +94 -144
  60. modal/cloud_bucket_mount.py +3 -1
  61. modal/cloud_bucket_mount.pyi +4 -0
  62. modal/cls.py +101 -64
  63. modal/cls.pyi +9 -8
  64. modal/config.py +21 -1
  65. modal/container_process.py +288 -12
  66. modal/container_process.pyi +99 -38
  67. modal/dict.py +72 -33
  68. modal/dict.pyi +88 -57
  69. modal/environments.py +16 -8
  70. modal/environments.pyi +6 -2
  71. modal/exception.py +154 -16
  72. modal/experimental/__init__.py +24 -53
  73. modal/experimental/flash.py +161 -74
  74. modal/experimental/flash.pyi +97 -49
  75. modal/file_io.py +50 -92
  76. modal/file_io.pyi +117 -89
  77. modal/functions.pyi +70 -87
  78. modal/image.py +82 -47
  79. modal/image.pyi +51 -30
  80. modal/io_streams.py +500 -149
  81. modal/io_streams.pyi +279 -189
  82. modal/mount.py +60 -46
  83. modal/mount.pyi +41 -17
  84. modal/network_file_system.py +19 -11
  85. modal/network_file_system.pyi +72 -39
  86. modal/object.pyi +114 -22
  87. modal/parallel_map.py +42 -44
  88. modal/parallel_map.pyi +9 -17
  89. modal/partial_function.pyi +4 -2
  90. modal/proxy.py +14 -6
  91. modal/proxy.pyi +10 -2
  92. modal/queue.py +45 -38
  93. modal/queue.pyi +88 -52
  94. modal/runner.py +96 -96
  95. modal/runner.pyi +44 -27
  96. modal/sandbox.py +225 -107
  97. modal/sandbox.pyi +226 -60
  98. modal/secret.py +58 -56
  99. modal/secret.pyi +28 -13
  100. modal/serving.py +7 -11
  101. modal/serving.pyi +7 -8
  102. modal/snapshot.py +29 -15
  103. modal/snapshot.pyi +18 -10
  104. modal/token_flow.py +1 -1
  105. modal/token_flow.pyi +4 -6
  106. modal/volume.py +102 -55
  107. modal/volume.pyi +125 -66
  108. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  109. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  110. modal_proto/api.proto +141 -70
  111. modal_proto/api_grpc.py +42 -26
  112. modal_proto/api_pb2.py +1123 -1103
  113. modal_proto/api_pb2.pyi +331 -83
  114. modal_proto/api_pb2_grpc.py +80 -48
  115. modal_proto/api_pb2_grpc.pyi +26 -18
  116. modal_proto/modal_api_grpc.py +175 -174
  117. modal_proto/task_command_router.proto +164 -0
  118. modal_proto/task_command_router_grpc.py +138 -0
  119. modal_proto/task_command_router_pb2.py +180 -0
  120. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
  121. modal_proto/task_command_router_pb2_grpc.py +272 -0
  122. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  123. modal_version/__init__.py +1 -1
  124. modal_version/__main__.py +1 -1
  125. modal/cli/programs/launch_instance_ssh.py +0 -94
  126. modal/cli/programs/run_marimo.py +0 -95
  127. modal-1.1.5.dev66.dist-info/RECORD +0 -191
  128. modal_proto/modal_options_grpc.py +0 -3
  129. modal_proto/options.proto +0 -19
  130. modal_proto/options_grpc.py +0 -3
  131. modal_proto/options_pb2.py +0 -35
  132. modal_proto/options_pb2.pyi +0 -20
  133. modal_proto/options_pb2_grpc.py +0 -4
  134. modal_proto/options_pb2_grpc.pyi +0 -7
  135. modal_proto/sandbox_router.proto +0 -125
  136. modal_proto/sandbox_router_grpc.py +0 -89
  137. modal_proto/sandbox_router_pb2.py +0 -128
  138. modal_proto/sandbox_router_pb2_grpc.py +0 -169
  139. modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
  140. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  141. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  142. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  143. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/exception.py CHANGED
@@ -1,7 +1,45 @@
1
1
  # Copyright Modal Labs 2022
2
+ """
3
+ Modal-specific exception types.
4
+
5
+ ## Notes on `grpclib.GRPCError` migration
6
+
7
+ Historically, the Modal SDK could propagate `grpclib.GRPCError` exceptions out
8
+ to user code. As of v1.3, we are in the process of gracefully migrating to
9
+ always raising a Modal exception type in these cases. To avoid breaking user
10
+ code that relies on catching `grpclib.GRPCError`, a subset of Modal exception
11
+ types temporarily inherit from `grpclib.GRPCError`.
12
+
13
+ We encourage users to migrate any code that currently catches `grpclib.GRPCError`
14
+ to instead catch the appropriate Modal exception type. The following mapping
15
+ between GRPCError status codes and Modal exception types is currently in use:
16
+
17
+ ```
18
+ CANCELLED -> ServiceError
19
+ UNKNOWN -> ServiceError
20
+ INVALID_ARGUMENT -> InvalidError
21
+ DEADLINE_EXCEEDED -> ServiceError
22
+ NOT_FOUND -> NotFoundError
23
+ ALREADY_EXISTS -> AlreadyExistsError
24
+ PERMISSION_DENIED -> PermissionDeniedError
25
+ RESOURCE_EXHAUSTED -> ResourceExhaustedError
26
+ FAILED_PRECONDITION -> ConflictError
27
+ ABORTED -> ConflictError
28
+ OUT_OF_RANGE -> InvalidError
29
+ UNIMPLEMENTED -> UnimplementedError
30
+ INTERNAL -> InternalError
31
+ UNAVAILABLE -> ServiceError
32
+ DATA_LOSS -> DataLossError
33
+ UNAUTHENTICATED -> AuthError
34
+ ```
35
+
36
+ """
37
+
2
38
  import random
3
39
  import signal
40
+ from typing import Any, Optional
4
41
 
42
+ import grpclib
5
43
  import synchronicity.exceptions
6
44
 
7
45
  UserCodeException = synchronicity.exceptions.UserCodeException # Deprecated type used for return_exception wrapping
@@ -26,10 +64,116 @@ class Error(Exception):
26
64
  """
27
65
 
28
66
 
29
- class AlreadyExistsError(Error):
67
+ class _GRPCErrorWrapper(grpclib.GRPCError):
68
+ """This transitional class helps us migrate away from propagating `grpclib.GRPCError` to users.
69
+
70
+ It serves two purposes:
71
+ - It avoids abruptly breaking user code that catches `grpclib.GRPCError`
72
+ - It actively warns when users access attributes defined by `grpclib.GRPCError`
73
+
74
+ This won't catch all cases (users might react indiscriminately to GRPCError without checking the status).
75
+
76
+ The mapping between GRPCError status codes and our error types is defined in `modal._grpc_client`.
77
+
78
+ """
79
+
80
+ # These will be set on the instance in our error handling middleware
81
+ _grpc_message: str
82
+ _grpc_status: grpclib.Status
83
+ _grpc_details: Any
84
+
85
+ def __init__(self, message: Optional[str] = None):
86
+ # Override GRPCError's init and repr to behave more like a regular Exception
87
+ # (We don't customize these anywhere in our custom error types currently).
88
+ self._message = message or ""
89
+
90
+ def __repr__(self) -> str:
91
+ return f"{type(self).__name__}({self._message!r})"
92
+
93
+ def _warn_on_grpc_error_attribute_access(self) -> None:
94
+ from ._utils.deprecation import deprecation_warning # Avoid circular import
95
+
96
+ exc_type = type(self).__name__
97
+ deprecation_warning(
98
+ (2025, 12, 9),
99
+ "Modal will stop propagating the `grpclib.GRPCError` type in the future. "
100
+ f"Update your code so that it catches `modal.exception.{exc_type}` directly "
101
+ "to avoid changes to error handling behavior in the future.",
102
+ pending=True,
103
+ )
104
+
105
+ @property
106
+ def message(self) -> str:
107
+ self._warn_on_grpc_error_attribute_access()
108
+ return self._grpc_message
109
+
110
+ @message.setter
111
+ def message(self, value: str) -> None:
112
+ self._grpc_message = value
113
+
114
+ @property
115
+ def status(self) -> grpclib.Status:
116
+ self._warn_on_grpc_error_attribute_access()
117
+ return self._grpc_status
118
+
119
+ @status.setter
120
+ def status(self, value: grpclib.Status) -> None:
121
+ self._grpc_status = value
122
+
123
+ @property
124
+ def details(self) -> Any:
125
+ self._warn_on_grpc_error_attribute_access()
126
+ return self._grpc_details
127
+
128
+ @details.setter
129
+ def details(self, value: Any) -> None:
130
+ self._grpc_details = value
131
+
132
+
133
+ class AlreadyExistsError(Error, _GRPCErrorWrapper):
30
134
  """Raised when a resource creation conflicts with an existing resource."""
31
135
 
32
136
 
137
+ class AuthError(Error, _GRPCErrorWrapper):
138
+ """Raised when a client has missing or invalid authentication."""
139
+
140
+
141
+ class InternalError(Error, _GRPCErrorWrapper):
142
+ """Raised when an internal error occurs in the Modal system."""
143
+
144
+
145
+ class InvalidError(Error, _GRPCErrorWrapper):
146
+ """Raised when user does something invalid."""
147
+
148
+
149
+ class ConflictError(InvalidError, _GRPCErrorWrapper):
150
+ """Raised when a resource conflict occurs between the request and current system state."""
151
+
152
+
153
+ class DataLossError(Error, _GRPCErrorWrapper):
154
+ """Raised when data is lost or corrupted."""
155
+
156
+
157
+ class NotFoundError(Error, _GRPCErrorWrapper):
158
+ """Raised when a requested resource was not found."""
159
+
160
+
161
+ class PermissionDeniedError(Error, _GRPCErrorWrapper):
162
+ """Raised when a user does not have permission to perform the requested operation."""
163
+
164
+
165
+ class ResourceExhaustedError(Error, _GRPCErrorWrapper):
166
+ """Raised when a server-side resource has been exhausted, e.g. a quota or rate limit."""
167
+
168
+
169
+ class ServiceError(Error, _GRPCErrorWrapper):
170
+ """Raised when an error occurs in basic client/server communication."""
171
+
172
+
173
+ class UnimplementedError(Error, _GRPCErrorWrapper):
174
+ """Raised when a requested operation is not implemented or not supported."""
175
+
176
+
33
177
  class RemoteError(Error):
34
178
  """Raised when an error occurs on the Modal server."""
35
179
 
@@ -42,6 +186,10 @@ class SandboxTimeoutError(TimeoutError):
42
186
  """Raised when a Sandbox exceeds its execution duration limit and times out."""
43
187
 
44
188
 
189
+ class ExecTimeoutError(TimeoutError):
190
+ """Raised when a container process exceeds its execution duration limit and times out."""
191
+
192
+
45
193
  class SandboxTerminatedError(Error):
46
194
  """Raised when a Sandbox is terminated for an internal reason."""
47
195
 
@@ -66,26 +214,14 @@ class OutputExpiredError(TimeoutError):
66
214
  """Raised when the Output exceeds expiration and times out."""
67
215
 
68
216
 
69
- class AuthError(Error):
70
- """Raised when a client has missing or invalid authentication."""
71
-
72
-
73
217
  class ConnectionError(Error):
74
218
  """Raised when an issue occurs while connecting to the Modal servers."""
75
219
 
76
220
 
77
- class InvalidError(Error):
78
- """Raised when user does something invalid."""
79
-
80
-
81
221
  class VersionError(Error):
82
222
  """Raised when the current client version of Modal is unsupported."""
83
223
 
84
224
 
85
- class NotFoundError(Error):
86
- """Raised when a requested resource was not found."""
87
-
88
-
89
225
  class ExecutionError(Error):
90
226
  """Raised when something unexpected happened during runtime."""
91
227
 
@@ -116,10 +252,12 @@ class ServerWarning(UserWarning):
116
252
  """Warning originating from the Modal server and re-issued in client code."""
117
253
 
118
254
 
255
+ class AsyncUsageWarning(UserWarning):
256
+ """Warning emitted when a blocking Modal interface is used in an async context."""
257
+
258
+
119
259
  class InternalFailure(Error):
120
- """
121
- Retriable internal error.
122
- """
260
+ """Retriable internal error."""
123
261
 
124
262
 
125
263
  class _CliUserExecutionError(Exception):
@@ -13,15 +13,18 @@ from .._object import _get_environment_name
13
13
  from .._partial_function import _clustered
14
14
  from .._runtime.container_io_manager import _ContainerIOManager
15
15
  from .._utils.async_utils import synchronize_api, synchronizer
16
- from .._utils.deprecation import deprecation_warning
17
- from .._utils.grpc_utils import retry_transient_errors
18
16
  from ..app import _App
19
17
  from ..client import _Client
20
- from ..cls import _Cls, _Obj
18
+ from ..cls import _Cls
21
19
  from ..exception import InvalidError
22
20
  from ..image import DockerfileSpec, ImageBuilderVersion, _Image, _ImageRegistryConfig
23
21
  from ..secret import _Secret
24
- from .flash import flash_forward, flash_get_containers, flash_prometheus_autoscaler # noqa: F401
22
+ from .flash import ( # noqa: F401
23
+ flash_forward,
24
+ flash_get_containers,
25
+ flash_prometheus_autoscaler,
26
+ http_server,
27
+ )
25
28
 
26
29
 
27
30
  def stop_fetching_inputs():
@@ -87,6 +90,19 @@ async def list_deployed_apps(environment_name: str = "", client: Optional[_Clien
87
90
  return app_infos
88
91
 
89
92
 
93
+ @synchronizer.create_blocking
94
+ async def stop_app(name: str, *, environment_name: Optional[str] = None, client: Optional[_Client] = None) -> None:
95
+ """Stop a deployed App.
96
+
97
+ This interface is experimental and may change in the future,
98
+ although the functionality will continue to be supported.
99
+ """
100
+ client_ = client or await _Client.from_env()
101
+ app = await _App.lookup(name, environment_name=environment_name, client=client_)
102
+ req = api_pb2.AppStopRequest(app_id=app.app_id, source=api_pb2.APP_STOP_SOURCE_PYTHON_CLIENT)
103
+ await client_.stub.AppStop(req)
104
+
105
+
90
106
  @synchronizer.create_blocking
91
107
  async def get_app_objects(
92
108
  app_name: str, *, environment_name: Optional[str] = None, client: Optional[_Client] = None
@@ -117,7 +133,7 @@ async def get_app_objects(
117
133
 
118
134
  app = await _App.lookup(app_name, environment_name=environment_name, client=client)
119
135
  req = api_pb2.AppGetLayoutRequest(app_id=app.app_id)
120
- app_layout_resp = await retry_transient_errors(client.stub.AppGetLayout, req)
136
+ app_layout_resp = await client.stub.AppGetLayout(req)
121
137
 
122
138
  app_objects: dict[str, Union[_Function, _Cls]] = {}
123
139
 
@@ -340,52 +356,6 @@ async def notebook_base_image(*, python_version: Optional[str] = None, force_bui
340
356
  )
341
357
 
342
358
 
343
- @synchronizer.create_blocking
344
- async def update_autoscaler(
345
- obj: Union[_Function, _Obj],
346
- *,
347
- min_containers: Optional[int] = None,
348
- max_containers: Optional[int] = None,
349
- buffer_containers: Optional[int] = None,
350
- scaledown_window: Optional[int] = None,
351
- client: Optional[_Client] = None,
352
- ) -> None:
353
- """Update the autoscaler settings for a Function or Obj (instance of a Cls).
354
-
355
- This is an experimental interface for a feature that we will be adding to
356
- replace the existing `.keep_warm()` method. The stable form of this interface
357
- may look different (i.e., it may be a standalone function or a method).
358
-
359
- """
360
- deprecation_warning(
361
- (2025, 5, 5),
362
- "The modal.experimental.update_autoscaler(...) function is now deprecated in favor of"
363
- " a stable `.update_autoscaler(...) method on the corresponding object.",
364
- show_source=True,
365
- )
366
-
367
- settings = api_pb2.AutoscalerSettings(
368
- min_containers=min_containers,
369
- max_containers=max_containers,
370
- buffer_containers=buffer_containers,
371
- scaledown_window=scaledown_window,
372
- )
373
-
374
- if client is None:
375
- client = await _Client.from_env()
376
-
377
- if isinstance(obj, _Function):
378
- f = obj
379
- else:
380
- assert obj._cls._class_service_function is not None
381
- await obj._cls._class_service_function.hydrate(client=client)
382
- f = obj._cached_service_function()
383
- await f.hydrate(client=client)
384
-
385
- request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=f.object_id, settings=settings)
386
- await retry_transient_errors(client.stub.FunctionUpdateSchedulingParams, request)
387
-
388
-
389
359
  @synchronizer.create_blocking
390
360
  async def image_delete(
391
361
  image_id: str,
@@ -394,7 +364,8 @@ async def image_delete(
394
364
  ) -> None:
395
365
  """Delete an Image by its ID.
396
366
 
397
- Deletion is irreversible and will prevent Apps from using the Image.
367
+ Deletion is irreversible and will prevent Functions/Sandboxes from using
368
+ the Image.
398
369
 
399
370
  This is an experimental interface for a feature that we will be adding to
400
371
  the main Image class. The stable form of this interface may look different.
@@ -408,4 +379,4 @@ async def image_delete(
408
379
  client = await _Client.from_env()
409
380
 
410
381
  req = api_pb2.ImageDeleteRequest(image_id=image_id)
411
- await retry_transient_errors(client.stub.ImageDelete, req)
382
+ await client.stub.ImageDelete(req)