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/_resolver.py CHANGED
@@ -1,14 +1,16 @@
1
1
  # Copyright Modal Labs 2023
2
2
  import asyncio
3
3
  import contextlib
4
+ import typing
4
5
  from asyncio import Future
5
- from typing import TYPE_CHECKING, Dict, Hashable, List, Optional
6
+ from collections.abc import Hashable
7
+ from typing import TYPE_CHECKING, Optional
6
8
 
7
9
  from grpclib import GRPCError, Status
8
10
 
9
- from modal_proto import api_pb2
10
-
11
- from .exception import ExecutionError, NotFoundError
11
+ from ._utils.async_utils import TaskContext
12
+ from .client import _Client
13
+ from .exception import NotFoundError
12
14
 
13
15
  if TYPE_CHECKING:
14
16
  from rich.tree import Tree
@@ -17,61 +19,62 @@ if TYPE_CHECKING:
17
19
 
18
20
 
19
21
  class StatusRow:
20
- def __init__(self, progress: "Optional[Tree]"):
21
- from ._output import (
22
- step_progress,
23
- )
24
-
22
+ def __init__(self, progress: "typing.Optional[Tree]"):
25
23
  self._spinner = None
26
24
  self._step_node = None
27
25
  if progress is not None:
28
- self._spinner = step_progress()
26
+ from ._output import OutputManager
27
+
28
+ self._spinner = OutputManager.step_progress()
29
29
  self._step_node = progress.add(self._spinner)
30
30
 
31
31
  def message(self, message):
32
- from ._output import step_progress_update
33
-
34
32
  if self._spinner is not None:
35
- step_progress_update(self._spinner, message)
33
+ self._spinner.update(text=message)
36
34
 
37
35
  def finish(self, message):
38
- from ._output import step_completed, step_progress_update
39
-
40
36
  if self._step_node is not None:
41
- step_progress_update(self._spinner, message)
42
- self._step_node.label = step_completed(message, is_substep=True)
37
+ from ._output import OutputManager
38
+
39
+ self._spinner.update(text=message)
40
+ self._step_node.label = OutputManager.substep_completed(message)
43
41
 
44
42
 
45
43
  class Resolver:
46
- _local_uuid_to_future: Dict[str, Future]
44
+ _local_uuid_to_future: dict[str, Future]
47
45
  _environment_name: Optional[str]
48
46
  _app_id: Optional[str]
49
- _deduplication_cache: Dict[Hashable, Future]
47
+ _deduplication_cache: dict[Hashable, Future]
48
+ _client: _Client
50
49
 
51
50
  def __init__(
52
51
  self,
53
- client=None,
52
+ client: _Client,
54
53
  *,
55
- output_mgr=None,
56
54
  environment_name: Optional[str] = None,
57
55
  app_id: Optional[str] = None,
58
56
  ):
59
- from rich.tree import Tree
57
+ try:
58
+ # TODO(michael) If we don't clean this up more thoroughly, it would probably
59
+ # be good to have a single source of truth for "rich is installed" rather than
60
+ # doing a try/catch everywhere we want to use it.
61
+ from rich.tree import Tree
62
+
63
+ from ._output import OutputManager
60
64
 
61
- from ._output import step_progress
65
+ tree = Tree(OutputManager.step_progress("Creating objects..."), guide_style="gray50")
66
+ except ImportError:
67
+ tree = None
62
68
 
63
- self._output_mgr = output_mgr
64
69
  self._local_uuid_to_future = {}
65
- self._tree = Tree(step_progress("Creating objects..."), guide_style="gray50")
70
+ self._tree = tree
66
71
  self._client = client
67
72
  self._app_id = app_id
68
73
  self._environment_name = environment_name
69
74
  self._deduplication_cache = {}
70
75
 
71
76
  @property
72
- def app_id(self) -> str:
73
- if self._app_id is None:
74
- raise ExecutionError("Resolver has no app")
77
+ def app_id(self) -> Optional[str]:
75
78
  return self._app_id
76
79
 
77
80
  @property
@@ -118,7 +121,7 @@ class Resolver:
118
121
  async def loader():
119
122
  # Wait for all its dependencies
120
123
  # TODO(erikbern): do we need existing_object_id for those?
121
- await asyncio.gather(*[self.load(dep) for dep in obj.deps()])
124
+ await TaskContext.gather(*[self.load(dep) for dep in obj.deps()])
122
125
 
123
126
  # Load the object itself
124
127
  try:
@@ -128,16 +131,18 @@ class Resolver:
128
131
  raise NotFoundError(exc.message)
129
132
  raise
130
133
 
131
- # Check that the id of functions and classes didn't change
132
- # TODO(erikbern): revisit this once stub assignments have been disallowed
133
- if not obj._is_another_app and (obj.object_id.startswith("fu-") or obj.object_id.startswith("cs-")):
134
- # Persisted refs are ignored because their life cycle is managed independently.
135
- # The same tag on an app can be pointed at different objects.
136
- if existing_object_id is not None and obj.object_id != existing_object_id:
137
- raise Exception(
138
- f"Tried creating an object using existing id {existing_object_id}"
139
- f" but it has id {obj.object_id}"
140
- )
134
+ # Check that the id of functions didn't change
135
+ # Persisted refs are ignored because their life cycle is managed independently.
136
+ if (
137
+ not obj._is_another_app
138
+ and existing_object_id is not None
139
+ and existing_object_id.startswith("fu-")
140
+ and obj.object_id != existing_object_id
141
+ ):
142
+ raise Exception(
143
+ f"Tried creating an object using existing id {existing_object_id}"
144
+ f" but it has id {obj.object_id}"
145
+ )
141
146
 
142
147
  return obj
143
148
 
@@ -146,10 +151,11 @@ class Resolver:
146
151
  if deduplication_key is not None:
147
152
  self._deduplication_cache[deduplication_key] = cached_future
148
153
 
154
+ # TODO(elias): print original exception/trace rather than the Resolver-internal trace
149
155
  return await cached_future
150
156
 
151
- def objects(self) -> List["_Object"]:
152
- unique_objects: Dict[str, "_Object"] = {}
157
+ def objects(self) -> list["_Object"]:
158
+ unique_objects: dict[str, "_Object"] = {}
153
159
  for fut in self._local_uuid_to_future.values():
154
160
  if not fut.done():
155
161
  # this will raise an exception if not all loads have been awaited, but that *should* never happen
@@ -162,27 +168,16 @@ class Resolver:
162
168
 
163
169
  @contextlib.contextmanager
164
170
  def display(self):
165
- from ._output import step_completed
171
+ # TODO(erikbern): get rid of this wrapper
172
+ from .output import _get_output_manager
166
173
 
167
- if self._output_mgr is None:
168
- yield
169
- else:
170
- with self._output_mgr.ctx_if_visible(self._output_mgr.make_live(self._tree)):
174
+ if self._tree and (output_mgr := _get_output_manager()):
175
+ with output_mgr.make_live(self._tree):
171
176
  yield
172
- self._tree.label = step_completed("Created objects.")
173
- self._output_mgr.print_if_visible(self._tree)
177
+ self._tree.label = output_mgr.step_completed("Created objects.")
178
+ output_mgr.print(self._tree)
179
+ else:
180
+ yield
174
181
 
175
182
  def add_status_row(self) -> StatusRow:
176
183
  return StatusRow(self._tree)
177
-
178
- async def console_write(self, log: api_pb2.TaskLogs):
179
- if self._output_mgr is not None:
180
- await self._output_mgr.put_log_content(log)
181
-
182
- def console_flush(self):
183
- if self._output_mgr is not None:
184
- self._output_mgr.flush_lines()
185
-
186
- def image_snapshot_update(self, image_id: str, task_progress: api_pb2.TaskProgress):
187
- if self._output_mgr is not None:
188
- self._output_mgr.update_snapshot_progress(image_id, task_progress)
modal/_resources.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # Copyright Modal Labs 2024
2
- from typing import Optional, Tuple, Union
2
+ from typing import Optional, Union
3
3
 
4
4
  from modal_proto import api_pb2
5
5
 
@@ -9,14 +9,28 @@ from .gpu import GPU_T, parse_gpu_config
9
9
 
10
10
  def convert_fn_config_to_resources_config(
11
11
  *,
12
- cpu: Optional[float],
13
- memory: Optional[Union[int, Tuple[int, int]]],
12
+ cpu: Optional[Union[float, tuple[float, float]]],
13
+ memory: Optional[Union[int, tuple[int, int]]],
14
14
  gpu: GPU_T,
15
+ ephemeral_disk: Optional[int],
15
16
  ) -> api_pb2.Resources:
16
- if cpu is not None and cpu < 0.1:
17
- raise InvalidError(f"Invalid fractional CPU value {cpu}. Cannot have less than 0.10 CPU resources.")
18
17
  gpu_config = parse_gpu_config(gpu)
19
- milli_cpu = int(1000 * cpu) if cpu is not None else None
18
+ if cpu and isinstance(cpu, tuple):
19
+ if not cpu[0]:
20
+ raise InvalidError("CPU request must be a positive number")
21
+ elif not cpu[1]:
22
+ raise InvalidError("CPU limit must be a positive number")
23
+ milli_cpu = int(1000 * cpu[0])
24
+ milli_cpu_max = int(1000 * cpu[1])
25
+ if milli_cpu_max < milli_cpu:
26
+ raise InvalidError(f"Cannot specify a CPU limit lower than request: {milli_cpu_max} < {milli_cpu}")
27
+ elif cpu and isinstance(cpu, (float, int)):
28
+ milli_cpu = int(1000 * cpu)
29
+ milli_cpu_max = None
30
+ else:
31
+ milli_cpu = None
32
+ milli_cpu_max = None
33
+
20
34
  if memory and isinstance(memory, int):
21
35
  memory_mb = memory
22
36
  memory_mb_max = 0 # no limit
@@ -28,5 +42,10 @@ def convert_fn_config_to_resources_config(
28
42
  memory_mb = 0
29
43
  memory_mb_max = 0
30
44
  return api_pb2.Resources(
31
- milli_cpu=milli_cpu, gpu_config=gpu_config, memory_mb=memory_mb, memory_mb_max=memory_mb_max
45
+ milli_cpu=milli_cpu,
46
+ milli_cpu_max=milli_cpu_max,
47
+ gpu_config=gpu_config,
48
+ memory_mb=memory_mb,
49
+ memory_mb_max=memory_mb_max,
50
+ ephemeral_disk_mb=ephemeral_disk,
32
51
  )
@@ -0,0 +1 @@
1
+ # Copyright Modal Labs 2024