modal 0.62.16__py3-none-any.whl → 0.72.11__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 +17 -13
  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 +420 -937
  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 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  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 +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  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 +5 -7
  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 +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  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 +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
modal/_resolver.py CHANGED
@@ -1,13 +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.exception import ExecutionError, NotFoundError
10
- from modal_proto import api_pb2
11
+ from ._utils.async_utils import TaskContext
12
+ from .client import _Client
13
+ from .exception import NotFoundError
11
14
 
12
15
  if TYPE_CHECKING:
13
16
  from rich.tree import Tree
@@ -16,61 +19,62 @@ if TYPE_CHECKING:
16
19
 
17
20
 
18
21
  class StatusRow:
19
- def __init__(self, progress: "Optional[Tree]"):
20
- from ._output import (
21
- step_progress,
22
- )
23
-
22
+ def __init__(self, progress: "typing.Optional[Tree]"):
24
23
  self._spinner = None
25
24
  self._step_node = None
26
25
  if progress is not None:
27
- self._spinner = step_progress()
26
+ from ._output import OutputManager
27
+
28
+ self._spinner = OutputManager.step_progress()
28
29
  self._step_node = progress.add(self._spinner)
29
30
 
30
31
  def message(self, message):
31
- from ._output import step_progress_update
32
-
33
32
  if self._spinner is not None:
34
- step_progress_update(self._spinner, message)
33
+ self._spinner.update(text=message)
35
34
 
36
35
  def finish(self, message):
37
- from ._output import step_completed, step_progress_update
38
-
39
36
  if self._step_node is not None:
40
- step_progress_update(self._spinner, message)
41
- 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)
42
41
 
43
42
 
44
43
  class Resolver:
45
- _local_uuid_to_future: Dict[str, Future]
44
+ _local_uuid_to_future: dict[str, Future]
46
45
  _environment_name: Optional[str]
47
46
  _app_id: Optional[str]
48
- _deduplication_cache: Dict[Hashable, Future]
47
+ _deduplication_cache: dict[Hashable, Future]
48
+ _client: _Client
49
49
 
50
50
  def __init__(
51
51
  self,
52
- client=None,
52
+ client: _Client,
53
53
  *,
54
- output_mgr=None,
55
54
  environment_name: Optional[str] = None,
56
55
  app_id: Optional[str] = None,
57
56
  ):
58
- 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
59
64
 
60
- from ._output import step_progress
65
+ tree = Tree(OutputManager.step_progress("Creating objects..."), guide_style="gray50")
66
+ except ImportError:
67
+ tree = None
61
68
 
62
- self._output_mgr = output_mgr
63
69
  self._local_uuid_to_future = {}
64
- self._tree = Tree(step_progress("Creating objects..."), guide_style="gray50")
70
+ self._tree = tree
65
71
  self._client = client
66
72
  self._app_id = app_id
67
73
  self._environment_name = environment_name
68
74
  self._deduplication_cache = {}
69
75
 
70
76
  @property
71
- def app_id(self) -> str:
72
- if self._app_id is None:
73
- raise ExecutionError("Resolver has no app")
77
+ def app_id(self) -> Optional[str]:
74
78
  return self._app_id
75
79
 
76
80
  @property
@@ -117,7 +121,7 @@ class Resolver:
117
121
  async def loader():
118
122
  # Wait for all its dependencies
119
123
  # TODO(erikbern): do we need existing_object_id for those?
120
- await asyncio.gather(*[self.load(dep) for dep in obj.deps()])
124
+ await TaskContext.gather(*[self.load(dep) for dep in obj.deps()])
121
125
 
122
126
  # Load the object itself
123
127
  try:
@@ -127,16 +131,18 @@ class Resolver:
127
131
  raise NotFoundError(exc.message)
128
132
  raise
129
133
 
130
- # Check that the id of functions and classes didn't change
131
- # TODO(erikbern): revisit this once stub assignments have been disallowed
132
- if not obj._is_another_app and (obj.object_id.startswith("fu-") or obj.object_id.startswith("cs-")):
133
- # Persisted refs are ignored because their life cycle is managed independently.
134
- # The same tag on an app can be pointed at different objects.
135
- if existing_object_id is not None and obj.object_id != existing_object_id:
136
- raise Exception(
137
- f"Tried creating an object using existing id {existing_object_id}"
138
- f" but it has id {obj.object_id}"
139
- )
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
+ )
140
146
 
141
147
  return obj
142
148
 
@@ -145,10 +151,11 @@ class Resolver:
145
151
  if deduplication_key is not None:
146
152
  self._deduplication_cache[deduplication_key] = cached_future
147
153
 
154
+ # TODO(elias): print original exception/trace rather than the Resolver-internal trace
148
155
  return await cached_future
149
156
 
150
- def objects(self) -> List["_Object"]:
151
- unique_objects: Dict[str, "_Object"] = {}
157
+ def objects(self) -> list["_Object"]:
158
+ unique_objects: dict[str, "_Object"] = {}
152
159
  for fut in self._local_uuid_to_future.values():
153
160
  if not fut.done():
154
161
  # this will raise an exception if not all loads have been awaited, but that *should* never happen
@@ -161,27 +168,16 @@ class Resolver:
161
168
 
162
169
  @contextlib.contextmanager
163
170
  def display(self):
164
- from ._output import step_completed
171
+ # TODO(erikbern): get rid of this wrapper
172
+ from .output import _get_output_manager
165
173
 
166
- if self._output_mgr is None:
167
- yield
168
- else:
169
- 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):
170
176
  yield
171
- self._tree.label = step_completed("Created objects.")
172
- 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
173
181
 
174
182
  def add_status_row(self) -> StatusRow:
175
183
  return StatusRow(self._tree)
176
-
177
- async def console_write(self, log: api_pb2.TaskLogs):
178
- if self._output_mgr is not None:
179
- await self._output_mgr.put_log_content(log)
180
-
181
- def console_flush(self):
182
- if self._output_mgr is not None:
183
- self._output_mgr.flush_lines()
184
-
185
- def image_snapshot_update(self, image_id: str, task_progress: api_pb2.TaskProgress):
186
- if self._output_mgr is not None:
187
- self._output_mgr.update_snapshot_progress(image_id, task_progress)
modal/_resources.py ADDED
@@ -0,0 +1,51 @@
1
+ # Copyright Modal Labs 2024
2
+ from typing import Optional, Union
3
+
4
+ from modal_proto import api_pb2
5
+
6
+ from .exception import InvalidError
7
+ from .gpu import GPU_T, parse_gpu_config
8
+
9
+
10
+ def convert_fn_config_to_resources_config(
11
+ *,
12
+ cpu: Optional[Union[float, tuple[float, float]]],
13
+ memory: Optional[Union[int, tuple[int, int]]],
14
+ gpu: GPU_T,
15
+ ephemeral_disk: Optional[int],
16
+ ) -> api_pb2.Resources:
17
+ gpu_config = parse_gpu_config(gpu)
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
+
34
+ if memory and isinstance(memory, int):
35
+ memory_mb = memory
36
+ memory_mb_max = 0 # no limit
37
+ elif memory and isinstance(memory, tuple):
38
+ memory_mb, memory_mb_max = memory
39
+ if memory_mb_max < memory_mb:
40
+ raise InvalidError(f"Cannot specify a memory limit lower than request: {memory_mb_max} < {memory_mb}")
41
+ else:
42
+ memory_mb = 0
43
+ memory_mb_max = 0
44
+ return api_pb2.Resources(
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,
51
+ )
@@ -0,0 +1 @@
1
+ # Copyright Modal Labs 2024