modal 0.62.115__py3-none-any.whl → 0.72.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. modal/__init__.py +13 -9
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +402 -398
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +3 -3
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/environments.py CHANGED
@@ -1,14 +1,115 @@
1
1
  # Copyright Modal Labs 2023
2
- from typing import List, Optional
2
+ from dataclasses import dataclass
3
+ from typing import Optional
3
4
 
4
5
  from google.protobuf.empty_pb2 import Empty
6
+ from google.protobuf.message import Message
5
7
  from google.protobuf.wrappers_pb2 import StringValue
6
8
 
7
- from modal.client import _Client
8
- from modal.config import config
9
9
  from modal_proto import api_pb2
10
10
 
11
- from ._utils.async_utils import synchronizer
11
+ from ._resolver import Resolver
12
+ from ._utils.async_utils import synchronize_api, synchronizer
13
+ from ._utils.deprecation import renamed_parameter
14
+ from ._utils.grpc_utils import retry_transient_errors
15
+ from ._utils.name_utils import check_object_name
16
+ from .client import _Client
17
+ from .config import config, logger
18
+ from .object import _Object
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class EnvironmentSettings:
23
+ image_builder_version: str # Ideally would be typed with ImageBuilderVersion literal
24
+ webhook_suffix: str
25
+
26
+
27
+ class _Environment(_Object, type_prefix="en"):
28
+ _settings: EnvironmentSettings
29
+
30
+ def __init__(self):
31
+ """mdmd:hidden"""
32
+ raise RuntimeError(
33
+ "`Environment(...)` constructor is not allowed."
34
+ " Please use `Environment.from_name` or `Environment.lookup` instead."
35
+ )
36
+
37
+ # TODO(michael) Keeping this private for now until we decide what else should be in it
38
+ # And what the rules should be about updates / mutability
39
+ # @property
40
+ # def settings(self) -> EnvironmentSettings:
41
+ # return self._settings
42
+
43
+ def _hydrate_metadata(self, metadata: Message):
44
+ # Overridden concrete implementation of base class method
45
+ assert metadata and isinstance(metadata, api_pb2.EnvironmentMetadata)
46
+ # TODO(michael) should probably expose the `name` from the metadata
47
+ # as the way to discover the name of the "default" environment
48
+
49
+ # Is there a simpler way to go Message -> Dataclass?
50
+ self._settings = EnvironmentSettings(
51
+ image_builder_version=metadata.settings.image_builder_version,
52
+ webhook_suffix=metadata.settings.webhook_suffix,
53
+ )
54
+
55
+ @staticmethod
56
+ @renamed_parameter((2024, 12, 18), "label", "name")
57
+ async def from_name(
58
+ name: str,
59
+ create_if_missing: bool = False,
60
+ ):
61
+ if name:
62
+ # Allow null names for the case where we want to look up the "default" environment,
63
+ # which is defined by the server. It feels messy to have "from_name" without a name, though?
64
+ # We're adding this mostly for internal use right now. We could consider an environment-only
65
+ # alternate constructor, like `Environment.get_default`, rather than exposing "unnamed"
66
+ # environments as part of public API when we make this class more useful.
67
+ check_object_name(name, "Environment")
68
+
69
+ async def _load(self: _Environment, resolver: Resolver, existing_object_id: Optional[str]):
70
+ request = api_pb2.EnvironmentGetOrCreateRequest(
71
+ deployment_name=name,
72
+ object_creation_type=(
73
+ api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
74
+ if create_if_missing
75
+ else api_pb2.OBJECT_CREATION_TYPE_UNSPECIFIED
76
+ ),
77
+ )
78
+ response = await retry_transient_errors(resolver.client.stub.EnvironmentGetOrCreate, request)
79
+ logger.debug(f"Created environment with id {response.environment_id}")
80
+ self._hydrate(response.environment_id, resolver.client, response.metadata)
81
+
82
+ # TODO environment name (and id?) in the repr? (We should make reprs consistently more useful)
83
+ return _Environment._from_loader(_load, "Environment()", is_another_app=True, hydrate_lazily=True)
84
+
85
+ @staticmethod
86
+ @renamed_parameter((2024, 12, 18), "label", "name")
87
+ async def lookup(
88
+ name: str,
89
+ client: Optional[_Client] = None,
90
+ create_if_missing: bool = False,
91
+ ):
92
+ obj = await _Environment.from_name(name, create_if_missing=create_if_missing)
93
+ if client is None:
94
+ client = await _Client.from_env()
95
+ resolver = Resolver(client=client)
96
+ await resolver.load(obj)
97
+ return obj
98
+
99
+
100
+ Environment = synchronize_api(_Environment)
101
+
102
+
103
+ # Needs to be after definition; synchronicity interferes with forward references?
104
+ ENVIRONMENT_CACHE: dict[str, _Environment] = {}
105
+
106
+
107
+ async def _get_environment_cached(name: str, client: _Client) -> _Environment:
108
+ if name in ENVIRONMENT_CACHE:
109
+ return ENVIRONMENT_CACHE[name]
110
+ environment = await _Environment.lookup(name, client)
111
+ ENVIRONMENT_CACHE[name] = environment
112
+ return environment
12
113
 
13
114
 
14
115
  @synchronizer.create_blocking
@@ -53,7 +154,7 @@ async def create_environment(name: str, client: Optional[_Client] = None):
53
154
 
54
155
 
55
156
  @synchronizer.create_blocking
56
- async def list_environments(client: Optional[_Client] = None) -> List[api_pb2.EnvironmentListItem]:
157
+ async def list_environments(client: Optional[_Client] = None) -> list[api_pb2.EnvironmentListItem]:
57
158
  if client is None:
58
159
  client = await _Client.from_env()
59
160
  resp = await client.stub.EnvironmentList(Empty())
modal/environments.pyi CHANGED
@@ -1,47 +1,99 @@
1
+ import google.protobuf.message
1
2
  import modal.client
3
+ import modal.object
2
4
  import modal_proto.api_pb2
3
5
  import typing
4
6
  import typing_extensions
5
7
 
6
- class __delete_environment_spec(typing_extensions.Protocol):
7
- def __call__(self, name: str, client: typing.Union[modal.client.Client, None] = None):
8
- ...
8
+ class EnvironmentSettings:
9
+ image_builder_version: str
10
+ webhook_suffix: str
9
11
 
10
- async def aio(self, *args, **kwargs):
11
- ...
12
+ def __init__(self, image_builder_version: str, webhook_suffix: str) -> None: ...
13
+ def __repr__(self): ...
14
+ def __eq__(self, other): ...
15
+ def __setattr__(self, name, value): ...
16
+ def __delattr__(self, name): ...
17
+ def __hash__(self): ...
12
18
 
13
- delete_environment: __delete_environment_spec
19
+ class _Environment(modal.object._Object):
20
+ _settings: EnvironmentSettings
14
21
 
22
+ def __init__(self): ...
23
+ def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
24
+ @staticmethod
25
+ async def from_name(name: str, create_if_missing: bool = False): ...
26
+ @staticmethod
27
+ async def lookup(
28
+ name: str, client: typing.Optional[modal.client._Client] = None, create_if_missing: bool = False
29
+ ): ...
15
30
 
16
- class __update_environment_spec(typing_extensions.Protocol):
17
- def __call__(self, current_name: str, *, new_name: typing.Union[str, None] = None, new_web_suffix: typing.Union[str, None] = None, client: typing.Union[modal.client.Client, None] = None):
18
- ...
31
+ class Environment(modal.object.Object):
32
+ _settings: EnvironmentSettings
19
33
 
20
- async def aio(self, *args, **kwargs):
21
- ...
34
+ def __init__(self): ...
35
+ def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
22
36
 
23
- update_environment: __update_environment_spec
37
+ class __from_name_spec(typing_extensions.Protocol):
38
+ def __call__(self, name: str, create_if_missing: bool = False): ...
39
+ async def aio(self, name: str, create_if_missing: bool = False): ...
24
40
 
41
+ from_name: __from_name_spec
25
42
 
26
- class __create_environment_spec(typing_extensions.Protocol):
27
- def __call__(self, name: str, client: typing.Union[modal.client.Client, None] = None):
28
- ...
43
+ class __lookup_spec(typing_extensions.Protocol):
44
+ def __call__(
45
+ self, name: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
46
+ ): ...
47
+ async def aio(
48
+ self, name: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
49
+ ): ...
29
50
 
30
- async def aio(self, *args, **kwargs):
31
- ...
51
+ lookup: __lookup_spec
32
52
 
33
- create_environment: __create_environment_spec
53
+ async def _get_environment_cached(name: str, client: modal.client._Client) -> _Environment: ...
34
54
 
55
+ class __delete_environment_spec(typing_extensions.Protocol):
56
+ def __call__(self, name: str, client: typing.Optional[modal.client.Client] = None): ...
57
+ async def aio(self, name: str, client: typing.Optional[modal.client.Client] = None): ...
35
58
 
36
- class __list_environments_spec(typing_extensions.Protocol):
37
- def __call__(self, client: typing.Union[modal.client.Client, None] = None) -> typing.List[modal_proto.api_pb2.EnvironmentListItem]:
38
- ...
59
+ delete_environment: __delete_environment_spec
39
60
 
40
- async def aio(self, *args, **kwargs) -> typing.List[modal_proto.api_pb2.EnvironmentListItem]:
41
- ...
61
+ class __update_environment_spec(typing_extensions.Protocol):
62
+ def __call__(
63
+ self,
64
+ current_name: str,
65
+ *,
66
+ new_name: typing.Optional[str] = None,
67
+ new_web_suffix: typing.Optional[str] = None,
68
+ client: typing.Optional[modal.client.Client] = None,
69
+ ): ...
70
+ async def aio(
71
+ self,
72
+ current_name: str,
73
+ *,
74
+ new_name: typing.Optional[str] = None,
75
+ new_web_suffix: typing.Optional[str] = None,
76
+ client: typing.Optional[modal.client.Client] = None,
77
+ ): ...
78
+
79
+ update_environment: __update_environment_spec
80
+
81
+ class __create_environment_spec(typing_extensions.Protocol):
82
+ def __call__(self, name: str, client: typing.Optional[modal.client.Client] = None): ...
83
+ async def aio(self, name: str, client: typing.Optional[modal.client.Client] = None): ...
84
+
85
+ create_environment: __create_environment_spec
86
+
87
+ class __list_environments_spec(typing_extensions.Protocol):
88
+ def __call__(
89
+ self, client: typing.Optional[modal.client.Client] = None
90
+ ) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
91
+ async def aio(
92
+ self, client: typing.Optional[modal.client.Client] = None
93
+ ) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
42
94
 
43
95
  list_environments: __list_environments_spec
44
96
 
97
+ def ensure_env(environment_name: typing.Optional[str] = None) -> str: ...
45
98
 
46
- def ensure_env(environment_name: typing.Union[str, None] = None) -> str:
47
- ...
99
+ ENVIRONMENT_CACHE: dict[str, _Environment]
modal/exception.py CHANGED
@@ -1,10 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import random
3
3
  import signal
4
- import sys
5
- import warnings
6
- from datetime import date
7
- from typing import Tuple
8
4
 
9
5
 
10
6
  class Error(Exception):
@@ -58,6 +54,10 @@ class InteractiveTimeoutError(TimeoutError):
58
54
  """Raised when interactive frontends time out while trying to connect to a container."""
59
55
 
60
56
 
57
+ class OutputExpiredError(TimeoutError):
58
+ """Raised when the Output exceeds expiration and times out."""
59
+
60
+
61
61
  class AuthError(Error):
62
62
  """Raised when a client has missing or invalid authentication."""
63
63
 
@@ -86,6 +86,14 @@ class DeserializationError(Error):
86
86
  """Raised to provide more context when an error is encountered during deserialization."""
87
87
 
88
88
 
89
+ class SerializationError(Error):
90
+ """Raised to provide more context when an error is encountered during serialization."""
91
+
92
+
93
+ class RequestSizeError(Error):
94
+ """Raised when an operation produces a gRPC request that is rejected by the server for being too large."""
95
+
96
+
89
97
  class DeprecationError(UserWarning):
90
98
  """UserWarning category emitted when a deprecated Modal feature or API is used."""
91
99
 
@@ -96,6 +104,16 @@ class PendingDeprecationError(UserWarning):
96
104
  """Soon to be deprecated feature. Only used intermittently because of multi-repo concerns."""
97
105
 
98
106
 
107
+ class ServerWarning(UserWarning):
108
+ """Warning originating from the Modal server and re-issued in client code."""
109
+
110
+
111
+ class InternalFailure(Error):
112
+ """
113
+ Retriable internal error.
114
+ """
115
+
116
+
99
117
  class _CliUserExecutionError(Exception):
100
118
  """mdmd:hidden
101
119
  Private wrapper for exceptions during when importing or running stubs from the CLI.
@@ -111,45 +129,6 @@ class _CliUserExecutionError(Exception):
111
129
  self.user_source = user_source
112
130
 
113
131
 
114
- # TODO(erikbern): we have something similready in function_utils.py
115
- _INTERNAL_MODULES = ["modal", "synchronicity"]
116
-
117
-
118
- def _is_internal_frame(frame):
119
- module = frame.f_globals["__name__"].split(".")[0]
120
- return module in _INTERNAL_MODULES
121
-
122
-
123
- def deprecation_error(deprecated_on: Tuple[int, int, int], msg: str):
124
- raise DeprecationError(f"Deprecated on {date(*deprecated_on)}: {msg}")
125
-
126
-
127
- def deprecation_warning(
128
- deprecated_on: Tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
129
- ) -> None:
130
- """Utility for getting the proper stack entry.
131
-
132
- See the implementation of the built-in [warnings.warn](https://docs.python.org/3/library/warnings.html#available-functions).
133
- """
134
- filename, lineno = "<unknown>", 0
135
- if show_source:
136
- # Find the last non-Modal line that triggered the warning
137
- try:
138
- frame = sys._getframe()
139
- while frame is not None and _is_internal_frame(frame):
140
- frame = frame.f_back
141
- filename = frame.f_code.co_filename
142
- lineno = frame.f_lineno
143
- except ValueError:
144
- # Use the defaults from above
145
- pass
146
-
147
- warning_cls: type = PendingDeprecationError if pending else DeprecationError
148
-
149
- # This is a lower-level function that warnings.warn uses
150
- warnings.warn_explicit(f"{date(*deprecated_on)}: {msg}", warning_cls, filename, lineno)
151
-
152
-
153
132
  def _simulate_preemption_interrupt(signum, frame):
154
133
  signal.alarm(30) # simulate a SIGKILL after 30s
155
134
  raise KeyboardInterrupt("Simulated preemption interrupt from modal-client!")
@@ -198,3 +177,11 @@ class InputCancellation(BaseException):
198
177
 
199
178
  class ModuleNotMountable(Exception):
200
179
  pass
180
+
181
+
182
+ class ClientClosed(Error):
183
+ pass
184
+
185
+
186
+ class FilesystemExecutionError(Error):
187
+ """Raised when an unknown error is thrown during a container filesystem operation."""
modal/experimental.py CHANGED
@@ -1,9 +1,69 @@
1
1
  # Copyright Modal Labs 2022
2
- from ._container_io_manager import _ContainerIOManager
2
+ from typing import (
3
+ Any,
4
+ Callable,
5
+ )
6
+
7
+ import modal._clustered_functions
8
+ from modal.functions import _Function
9
+
10
+ from ._runtime.container_io_manager import _ContainerIOManager
11
+ from .exception import (
12
+ InvalidError,
13
+ )
14
+ from .partial_function import _PartialFunction, _PartialFunctionFlags
3
15
 
4
16
 
5
17
  def stop_fetching_inputs():
6
18
  """Don't fetch any more inputs from the server, after the current one.
7
19
  The container will exit gracefully after the current input is processed."""
8
-
9
20
  _ContainerIOManager.stop_fetching_inputs()
21
+
22
+
23
+ def get_local_input_concurrency():
24
+ """Get the container's local input concurrency.
25
+ If recently reduced to particular value, it can return a larger number than
26
+ set due to in-progress inputs."""
27
+ return _ContainerIOManager.get_input_concurrency()
28
+
29
+
30
+ def set_local_input_concurrency(concurrency: int):
31
+ """Set the container's local input concurrency. Dynamic concurrency will be disabled.
32
+ When setting to a smaller value, this method will not interrupt in-progress inputs.
33
+ """
34
+ _ContainerIOManager.set_input_concurrency(concurrency)
35
+
36
+
37
+ def clustered(size: int, broadcast: bool = True):
38
+ """Provision clusters of colocated and networked containers for the Function.
39
+
40
+ Parameters:
41
+ size: int
42
+ Number of containers spun up to handle each input.
43
+ broadcast: bool = True
44
+ If True, inputs will be sent simultaneously to each container. Otherwise,
45
+ inputs will be sent only to the rank-0 container, which is responsible for
46
+ delegating to the workers.
47
+ """
48
+
49
+ assert broadcast, "broadcast=False has not been implemented yet!"
50
+
51
+ if size <= 0:
52
+ raise ValueError("cluster size must be greater than 0")
53
+
54
+ def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
55
+ if isinstance(raw_f, _Function):
56
+ raw_f = raw_f.get_raw_f()
57
+ raise InvalidError(
58
+ f"Applying decorators for {raw_f} in the wrong order!\nUsage:\n\n"
59
+ "@app.function()\n@modal.clustered()\ndef clustered_function():\n ..."
60
+ )
61
+ return _PartialFunction(
62
+ raw_f, _PartialFunctionFlags.FUNCTION | _PartialFunctionFlags.CLUSTERED, cluster_size=size
63
+ )
64
+
65
+ return wrapper
66
+
67
+
68
+ def get_cluster_info() -> modal._clustered_functions.ClusterInfo:
69
+ return modal._clustered_functions.get_cluster_info()