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/secret.py CHANGED
@@ -1,17 +1,19 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import os
3
- from typing import Dict, List, Optional, Union
3
+ from typing import Optional, Union
4
4
 
5
5
  from grpclib import GRPCError, Status
6
6
 
7
7
  from modal_proto import api_pb2
8
8
 
9
9
  from ._resolver import Resolver
10
+ from ._runtime.execution_context import is_local
10
11
  from ._utils.async_utils import synchronize_api
12
+ from ._utils.deprecation import renamed_parameter
11
13
  from ._utils.grpc_utils import retry_transient_errors
14
+ from ._utils.name_utils import check_object_name
12
15
  from .client import _Client
13
16
  from .exception import InvalidError, NotFoundError
14
- from .execution_context import is_local
15
17
  from .object import _get_environment_name, _Object
16
18
 
17
19
  ENV_DICT_WRONG_TYPE_ERR = "the env_dict argument to Secret has to be a dict[str, Union[str, None]]"
@@ -29,7 +31,7 @@ class _Secret(_Object, type_prefix="st"):
29
31
 
30
32
  @staticmethod
31
33
  def from_dict(
32
- env_dict: Dict[
34
+ env_dict: dict[
33
35
  str, Union[str, None]
34
36
  ] = {}, # dict of entries to be inserted as environment variables in functions using the secret
35
37
  ):
@@ -45,17 +47,23 @@ class _Secret(_Object, type_prefix="st"):
45
47
  if not isinstance(env_dict, dict):
46
48
  raise InvalidError(ENV_DICT_WRONG_TYPE_ERR)
47
49
 
48
- env_dict_filtered: Dict[str, str] = {k: v for k, v in env_dict.items() if v is not None}
50
+ env_dict_filtered: dict[str, str] = {k: v for k, v in env_dict.items() if v is not None}
49
51
  if not all(isinstance(k, str) for k in env_dict_filtered.keys()):
50
52
  raise InvalidError(ENV_DICT_WRONG_TYPE_ERR)
51
53
  if not all(isinstance(v, str) for v in env_dict_filtered.values()):
52
54
  raise InvalidError(ENV_DICT_WRONG_TYPE_ERR)
53
55
 
54
56
  async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
57
+ if resolver.app_id is not None:
58
+ object_creation_type = api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP
59
+ else:
60
+ object_creation_type = api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL
61
+
55
62
  req = api_pb2.SecretGetOrCreateRequest(
56
- object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
63
+ object_creation_type=object_creation_type,
57
64
  env_dict=env_dict_filtered,
58
65
  app_id=resolver.app_id,
66
+ environment_name=resolver.environment_name,
59
67
  )
60
68
  try:
61
69
  resp = await resolver.client.stub.SecretGetOrCreate(req)
@@ -68,11 +76,11 @@ class _Secret(_Object, type_prefix="st"):
68
76
  self._hydrate(resp.secret_id, resolver.client, None)
69
77
 
70
78
  rep = f"Secret.from_dict([{', '.join(env_dict.keys())}])"
71
- return _Secret._from_loader(_load, rep)
79
+ return _Secret._from_loader(_load, rep, hydrate_lazily=True)
72
80
 
73
81
  @staticmethod
74
82
  def from_local_environ(
75
- env_keys: List[str], # list of local env vars to be included for remote execution
83
+ env_keys: list[str], # list of local env vars to be included for remote execution
76
84
  ):
77
85
  """Create secrets from local environment variables automatically."""
78
86
 
@@ -82,7 +90,7 @@ class _Secret(_Object, type_prefix="st"):
82
90
  except KeyError as exc:
83
91
  missing_key = exc.args[0]
84
92
  raise InvalidError(
85
- f"Could not find local environment variable '{missing_key}' for Secret.from_local_env_vars"
93
+ f"Could not find local environment variable '{missing_key}' for Secret.from_local_environ"
86
94
  )
87
95
 
88
96
  return _Secret.from_dict({})
@@ -135,8 +143,8 @@ class _Secret(_Object, type_prefix="st"):
135
143
  else:
136
144
  dotenv_path = ""
137
145
  else:
138
- # TODO(erikbern): dotenv tries to locate .env files based on the location of the file in the stack frame.
139
- # Since the modal code "intermediates" this, a .env file in the user's local directory won't be picked up.
146
+ # TODO(erikbern): dotenv tries to locate .env files based on location of the file in the stack frame.
147
+ # Since the modal code "intermediates" this, a .env file in user's local directory won't be picked up.
140
148
  # To simplify this, we just support the cwd and don't do any automatic path inference.
141
149
  dotenv_path = find_dotenv(filename, usecwd=True)
142
150
 
@@ -151,15 +159,23 @@ class _Secret(_Object, type_prefix="st"):
151
159
 
152
160
  self._hydrate(resp.secret_id, resolver.client, None)
153
161
 
154
- return _Secret._from_loader(_load, "Secret.from_dotenv()")
162
+ return _Secret._from_loader(_load, "Secret.from_dotenv()", hydrate_lazily=True)
155
163
 
156
164
  @staticmethod
165
+ @renamed_parameter((2024, 12, 18), "label", "name")
157
166
  def from_name(
158
- label: str,
167
+ name: str,
159
168
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
160
169
  environment_name: Optional[str] = None,
170
+ required_keys: list[
171
+ str
172
+ ] = [], # Optionally, a list of required environment variables (will be asserted server-side)
161
173
  ) -> "_Secret":
162
- """Create a reference to a persisted Secret
174
+ """Reference a Secret by its name.
175
+
176
+ In contrast to most other Modal objects, named Secrets must be provisioned
177
+ from the Dashboard. See other methods for alternate ways of creating a new
178
+ Secret from code.
163
179
 
164
180
  ```python
165
181
  secret = modal.Secret.from_name("my-secret")
@@ -172,9 +188,10 @@ class _Secret(_Object, type_prefix="st"):
172
188
 
173
189
  async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
174
190
  req = api_pb2.SecretGetOrCreateRequest(
175
- deployment_name=label,
191
+ deployment_name=name,
176
192
  namespace=namespace,
177
193
  environment_name=_get_environment_name(environment_name, resolver),
194
+ required_keys=required_keys,
178
195
  )
179
196
  try:
180
197
  response = await resolver.client.stub.SecretGetOrCreate(req)
@@ -185,23 +202,21 @@ class _Secret(_Object, type_prefix="st"):
185
202
  raise
186
203
  self._hydrate(response.secret_id, resolver.client, None)
187
204
 
188
- return _Secret._from_loader(_load, "Secret()")
205
+ return _Secret._from_loader(_load, "Secret()", hydrate_lazily=True)
189
206
 
190
207
  @staticmethod
208
+ @renamed_parameter((2024, 12, 18), "label", "name")
191
209
  async def lookup(
192
- label: str,
210
+ name: str,
193
211
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
194
212
  client: Optional[_Client] = None,
195
213
  environment_name: Optional[str] = None,
214
+ required_keys: list[str] = [],
196
215
  ) -> "_Secret":
197
- """Lookup a secret with a given name
198
-
199
- ```python
200
- s = modal.Secret.lookup("my-secret")
201
- print(s.object_id)
202
- ```
203
- """
204
- obj = _Secret.from_name(label, namespace=namespace, environment_name=environment_name)
216
+ """mdmd:hidden"""
217
+ obj = _Secret.from_name(
218
+ name, namespace=namespace, environment_name=environment_name, required_keys=required_keys
219
+ )
205
220
  if client is None:
206
221
  client = await _Client.from_env()
207
222
  resolver = Resolver(client=client)
@@ -211,13 +226,14 @@ class _Secret(_Object, type_prefix="st"):
211
226
  @staticmethod
212
227
  async def create_deployed(
213
228
  deployment_name: str,
214
- env_dict: Dict[str, str],
229
+ env_dict: dict[str, str],
215
230
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
216
231
  client: Optional[_Client] = None,
217
232
  environment_name: Optional[str] = None,
218
233
  overwrite: bool = False,
219
234
  ) -> str:
220
235
  """mdmd:hidden"""
236
+ check_object_name(deployment_name, "Secret")
221
237
  if client is None:
222
238
  client = await _Client.from_env()
223
239
  if overwrite:
modal/secret.pyi CHANGED
@@ -5,64 +5,84 @@ import typing_extensions
5
5
 
6
6
  class _Secret(modal.object._Object):
7
7
  @staticmethod
8
- def from_dict(env_dict: typing.Dict[str, typing.Union[str, None]] = {}):
9
- ...
10
-
8
+ def from_dict(env_dict: dict[str, typing.Optional[str]] = {}): ...
11
9
  @staticmethod
12
- def from_local_environ(env_keys: typing.List[str]):
13
- ...
14
-
10
+ def from_local_environ(env_keys: list[str]): ...
15
11
  @staticmethod
16
- def from_dotenv(path=None, *, filename='.env'):
17
- ...
18
-
12
+ def from_dotenv(path=None, *, filename=".env"): ...
19
13
  @staticmethod
20
- def from_name(label: str, namespace=1, environment_name: typing.Union[str, None] = None) -> _Secret:
21
- ...
22
-
14
+ def from_name(
15
+ name: str, namespace=1, environment_name: typing.Optional[str] = None, required_keys: list[str] = []
16
+ ) -> _Secret: ...
23
17
  @staticmethod
24
- async def lookup(label: str, namespace=1, client: typing.Union[modal.client._Client, None] = None, environment_name: typing.Union[str, None] = None) -> _Secret:
25
- ...
26
-
18
+ async def lookup(
19
+ name: str,
20
+ namespace=1,
21
+ client: typing.Optional[modal.client._Client] = None,
22
+ environment_name: typing.Optional[str] = None,
23
+ required_keys: list[str] = [],
24
+ ) -> _Secret: ...
27
25
  @staticmethod
28
- async def create_deployed(deployment_name: str, env_dict: typing.Dict[str, str], namespace=1, client: typing.Union[modal.client._Client, None] = None, environment_name: typing.Union[str, None] = None, overwrite: bool = False) -> str:
29
- ...
30
-
26
+ async def create_deployed(
27
+ deployment_name: str,
28
+ env_dict: dict[str, str],
29
+ namespace=1,
30
+ client: typing.Optional[modal.client._Client] = None,
31
+ environment_name: typing.Optional[str] = None,
32
+ overwrite: bool = False,
33
+ ) -> str: ...
31
34
 
32
35
  class Secret(modal.object.Object):
33
- def __init__(self, *args, **kwargs):
34
- ...
35
-
36
+ def __init__(self, *args, **kwargs): ...
36
37
  @staticmethod
37
- def from_dict(env_dict: typing.Dict[str, typing.Union[str, None]] = {}):
38
- ...
39
-
38
+ def from_dict(env_dict: dict[str, typing.Optional[str]] = {}): ...
40
39
  @staticmethod
41
- def from_local_environ(env_keys: typing.List[str]):
42
- ...
43
-
40
+ def from_local_environ(env_keys: list[str]): ...
44
41
  @staticmethod
45
- def from_dotenv(path=None, *, filename='.env'):
46
- ...
47
-
42
+ def from_dotenv(path=None, *, filename=".env"): ...
48
43
  @staticmethod
49
- def from_name(label: str, namespace=1, environment_name: typing.Union[str, None] = None) -> Secret:
50
- ...
44
+ def from_name(
45
+ name: str, namespace=1, environment_name: typing.Optional[str] = None, required_keys: list[str] = []
46
+ ) -> Secret: ...
51
47
 
52
48
  class __lookup_spec(typing_extensions.Protocol):
53
- def __call__(self, label: str, namespace=1, client: typing.Union[modal.client.Client, None] = None, environment_name: typing.Union[str, None] = None) -> Secret:
54
- ...
55
-
56
- async def aio(self, *args, **kwargs) -> Secret:
57
- ...
49
+ def __call__(
50
+ self,
51
+ name: str,
52
+ namespace=1,
53
+ client: typing.Optional[modal.client.Client] = None,
54
+ environment_name: typing.Optional[str] = None,
55
+ required_keys: list[str] = [],
56
+ ) -> Secret: ...
57
+ async def aio(
58
+ self,
59
+ name: str,
60
+ namespace=1,
61
+ client: typing.Optional[modal.client.Client] = None,
62
+ environment_name: typing.Optional[str] = None,
63
+ required_keys: list[str] = [],
64
+ ) -> Secret: ...
58
65
 
59
66
  lookup: __lookup_spec
60
67
 
61
68
  class __create_deployed_spec(typing_extensions.Protocol):
62
- def __call__(self, deployment_name: str, env_dict: typing.Dict[str, str], namespace=1, client: typing.Union[modal.client.Client, None] = None, environment_name: typing.Union[str, None] = None, overwrite: bool = False) -> str:
63
- ...
64
-
65
- async def aio(self, *args, **kwargs) -> str:
66
- ...
69
+ def __call__(
70
+ self,
71
+ deployment_name: str,
72
+ env_dict: dict[str, str],
73
+ namespace=1,
74
+ client: typing.Optional[modal.client.Client] = None,
75
+ environment_name: typing.Optional[str] = None,
76
+ overwrite: bool = False,
77
+ ) -> str: ...
78
+ async def aio(
79
+ self,
80
+ deployment_name: str,
81
+ env_dict: dict[str, str],
82
+ namespace=1,
83
+ client: typing.Optional[modal.client.Client] = None,
84
+ environment_name: typing.Optional[str] = None,
85
+ overwrite: bool = False,
86
+ ) -> str: ...
67
87
 
68
88
  create_deployed: __create_deployed_spec
modal/serving.py CHANGED
@@ -1,23 +1,24 @@
1
1
  # Copyright Modal Labs 2023
2
- import io
3
2
  import multiprocessing
4
3
  import platform
5
- import sys
4
+ from collections.abc import AsyncGenerator
6
5
  from multiprocessing.context import SpawnProcess
7
6
  from multiprocessing.synchronize import Event
8
- from typing import TYPE_CHECKING, AsyncGenerator, Optional, Set, TypeVar
7
+ from typing import TYPE_CHECKING, Optional, TypeVar
9
8
 
10
- from synchronicity import Interface
11
9
  from synchronicity.async_wrap import asynccontextmanager
12
10
 
13
- from ._output import OutputManager
11
+ from modal._output import OutputManager
12
+
14
13
  from ._utils.async_utils import TaskContext, asyncify, synchronize_api, synchronizer
14
+ from ._utils.deprecation import deprecation_error
15
15
  from ._utils.logger import logger
16
16
  from ._watcher import watch
17
17
  from .cli.import_refs import import_app
18
18
  from .client import _Client
19
19
  from .config import config
20
- from .runner import _disconnect, _run_app, serve_update
20
+ from .output import _get_output_manager, enable_output
21
+ from .runner import _run_app, serve_update
21
22
 
22
23
  if TYPE_CHECKING:
23
24
  from .app import _App
@@ -25,11 +26,13 @@ else:
25
26
  _App = TypeVar("_App")
26
27
 
27
28
 
28
- def _run_serve(app_ref: str, existing_app_id: str, is_ready: Event, environment_name: str):
29
+ def _run_serve(app_ref: str, existing_app_id: str, is_ready: Event, environment_name: str, show_progress: bool):
29
30
  # subprocess entrypoint
30
31
  _app = import_app(app_ref)
31
- blocking_app = synchronizer._translate_out(_app, Interface.BLOCKING)
32
- serve_update(blocking_app, existing_app_id, is_ready, environment_name)
32
+ blocking_app = synchronizer._translate_out(_app)
33
+
34
+ with enable_output(show_progress=show_progress):
35
+ serve_update(blocking_app, existing_app_id, is_ready, environment_name)
33
36
 
34
37
 
35
38
  async def _restart_serve(
@@ -37,7 +40,9 @@ async def _restart_serve(
37
40
  ) -> SpawnProcess:
38
41
  ctx = multiprocessing.get_context("spawn") # Needed to reload the interpreter
39
42
  is_ready = ctx.Event()
40
- p = ctx.Process(target=_run_serve, args=(app_ref, existing_app_id, is_ready, environment_name))
43
+ output_mgr = OutputManager.get()
44
+ show_progress = output_mgr is not None
45
+ p = ctx.Process(target=_run_serve, args=(app_ref, existing_app_id, is_ready, environment_name, show_progress))
41
46
  p.start()
42
47
  await asyncify(is_ready.wait)(timeout)
43
48
  # TODO(erikbern): we don't fail if the above times out, but that's somewhat intentional, since
@@ -45,18 +50,18 @@ async def _restart_serve(
45
50
  return p
46
51
 
47
52
 
48
- async def _terminate(proc: Optional[SpawnProcess], output_mgr: OutputManager, timeout: float = 5.0):
53
+ async def _terminate(proc: Optional[SpawnProcess], timeout: float = 5.0):
49
54
  if proc is None:
50
55
  return
51
56
  try:
52
57
  proc.terminate()
53
58
  await asyncify(proc.join)(timeout)
54
59
  if proc.exitcode is not None:
55
- output_mgr.print_if_visible(f"Serve process {proc.pid} terminated")
60
+ if output_mgr := _get_output_manager():
61
+ output_mgr.print(f"Serve process {proc.pid} terminated")
56
62
  else:
57
- output_mgr.print_if_visible(
58
- f"[red]Serve process {proc.pid} didn't terminate after {timeout}s, killing it[/red]"
59
- )
63
+ if output_mgr := _get_output_manager():
64
+ output_mgr.print(f"[red]Serve process {proc.pid} didn't terminate after {timeout}s, killing it[/red]")
60
65
  proc.kill()
61
66
  except ProcessLookupError:
62
67
  pass # Child process already finished
@@ -65,8 +70,7 @@ async def _terminate(proc: Optional[SpawnProcess], output_mgr: OutputManager, ti
65
70
  async def _run_watch_loop(
66
71
  app_ref: str,
67
72
  app_id: str,
68
- output_mgr: OutputManager,
69
- watcher: AsyncGenerator[Set[str], None],
73
+ watcher: AsyncGenerator[set[str], None],
70
74
  environment_name: str,
71
75
  ):
72
76
  unsupported_msg = None
@@ -75,36 +79,25 @@ async def _run_watch_loop(
75
79
  " This can hopefully be fixed in a future version of Modal."
76
80
 
77
81
  if unsupported_msg:
78
- async for _ in watcher:
79
- output_mgr.print_if_visible(unsupported_msg)
82
+ if output_mgr := _get_output_manager():
83
+ async for _ in watcher:
84
+ output_mgr.print(unsupported_msg)
80
85
  else:
81
86
  curr_proc = None
82
87
  try:
83
88
  async for trigger_files in watcher:
84
89
  logger.debug(f"The following files triggered an app update: {', '.join(trigger_files)}")
85
- await _terminate(curr_proc, output_mgr)
90
+ await _terminate(curr_proc)
86
91
  curr_proc = await _restart_serve(app_ref, existing_app_id=app_id, environment_name=environment_name)
87
92
  finally:
88
- await _terminate(curr_proc, output_mgr)
89
-
90
-
91
- def _get_clean_app_description(app_ref: str) -> str:
92
- # If possible, consider the 'ref' argument the start of the app's args. Everything
93
- # before it Modal CLI cruft (eg. `modal serve --timeout 1.0`).
94
- try:
95
- func_ref_arg_idx = sys.argv.index(app_ref)
96
- return " ".join(sys.argv[func_ref_arg_idx:])
97
- except ValueError:
98
- return " ".join(sys.argv)
93
+ await _terminate(curr_proc)
99
94
 
100
95
 
101
96
  @asynccontextmanager
102
97
  async def _serve_app(
103
98
  app: "_App",
104
99
  app_ref: str,
105
- stdout: Optional[io.TextIOWrapper] = None,
106
- show_progress: bool = True,
107
- _watcher: Optional[AsyncGenerator[Set[str], None]] = None, # for testing
100
+ _watcher: Optional[AsyncGenerator[set[str], None]] = None, # for testing
108
101
  environment_name: Optional[str] = None,
109
102
  ) -> AsyncGenerator["_App", None]:
110
103
  if environment_name is None:
@@ -112,23 +105,20 @@ async def _serve_app(
112
105
 
113
106
  client = await _Client.from_env()
114
107
 
115
- output_mgr = OutputManager(stdout, show_progress, "Running app...")
116
- if _watcher is not None:
117
- watcher = _watcher # Only used by tests
118
- else:
119
- mounts_to_watch = app._get_watch_mounts()
120
- watcher = watch(mounts_to_watch, output_mgr)
121
-
122
- async with _run_app(app, client=client, output_mgr=output_mgr, environment_name=environment_name):
123
- app_id: str = app.app_id
124
- client.set_pre_stop(lambda: _disconnect(client, app_id))
108
+ async with _run_app(app, client=client, environment_name=environment_name):
109
+ if _watcher is not None:
110
+ watcher = _watcher # Only used by tests
111
+ else:
112
+ mounts_to_watch = app._get_watch_mounts()
113
+ watcher = watch(mounts_to_watch)
125
114
  async with TaskContext(grace=0.1) as tc:
126
- tc.create_task(_run_watch_loop(app_ref, app.app_id, output_mgr, watcher, environment_name))
115
+ tc.create_task(_run_watch_loop(app_ref, app.app_id, watcher, environment_name))
127
116
  yield app
128
117
 
129
118
 
130
- serve_app = synchronize_api(_serve_app)
119
+ def _serve_stub(*args, **kwargs):
120
+ deprecation_error((2024, 5, 1), "`serve_stub` is deprecated. Please use `serve_app` instead.")
131
121
 
132
- # Soon-to-be-deprecated ones, add warning soon
133
- _serve_stub = _serve_app
134
- serve_stub = serve_app
122
+
123
+ serve_app = synchronize_api(_serve_app)
124
+ serve_stub = synchronize_api(_serve_stub)
modal/serving.pyi CHANGED
@@ -1,5 +1,4 @@
1
- import _io
2
- import modal._output
1
+ import collections.abc
3
2
  import multiprocessing.context
4
3
  import multiprocessing.synchronize
5
4
  import synchronicity.combined_types
@@ -8,49 +7,44 @@ import typing_extensions
8
7
 
9
8
  _App = typing.TypeVar("_App")
10
9
 
11
- def _run_serve(app_ref: str, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str):
12
- ...
13
-
14
-
15
- async def _restart_serve(app_ref: str, existing_app_id: str, environment_name: str, timeout: float = 5.0) -> multiprocessing.context.SpawnProcess:
16
- ...
17
-
18
-
19
- async def _terminate(proc: typing.Union[multiprocessing.context.SpawnProcess, None], output_mgr: modal._output.OutputManager, timeout: float = 5.0):
20
- ...
21
-
22
-
23
- async def _run_watch_loop(app_ref: str, app_id: str, output_mgr: modal._output.OutputManager, watcher: typing.AsyncGenerator[typing.Set[str], None], environment_name: str):
24
- ...
25
-
26
-
27
- def _get_clean_app_description(app_ref: str) -> str:
28
- ...
29
-
30
-
31
- def _serve_app(app: _App, app_ref: str, stdout: typing.Union[_io.TextIOWrapper, None] = None, show_progress: bool = True, _watcher: typing.Union[typing.AsyncGenerator[typing.Set[str], None], None] = None, environment_name: typing.Union[str, None] = None) -> typing.AsyncContextManager[_App]:
32
- ...
33
-
10
+ def _run_serve(
11
+ app_ref: str,
12
+ existing_app_id: str,
13
+ is_ready: multiprocessing.synchronize.Event,
14
+ environment_name: str,
15
+ show_progress: bool,
16
+ ): ...
17
+ async def _restart_serve(
18
+ app_ref: str, existing_app_id: str, environment_name: str, timeout: float = 5.0
19
+ ) -> multiprocessing.context.SpawnProcess: ...
20
+ async def _terminate(proc: typing.Optional[multiprocessing.context.SpawnProcess], timeout: float = 5.0): ...
21
+ async def _run_watch_loop(
22
+ app_ref: str, app_id: str, watcher: collections.abc.AsyncGenerator[set[str], None], environment_name: str
23
+ ): ...
24
+ def _serve_app(
25
+ app: _App,
26
+ app_ref: str,
27
+ _watcher: typing.Optional[collections.abc.AsyncGenerator[set[str], None]] = None,
28
+ environment_name: typing.Optional[str] = None,
29
+ ) -> typing.AsyncContextManager[_App]: ...
30
+ def _serve_stub(*args, **kwargs): ...
34
31
 
35
32
  class __serve_app_spec(typing_extensions.Protocol):
36
- def __call__(self, app: _App, app_ref: str, stdout: typing.Union[_io.TextIOWrapper, None] = None, show_progress: bool = True, _watcher: typing.Union[typing.Generator[typing.Set[str], None, None], None] = None, environment_name: typing.Union[str, None] = None) -> synchronicity.combined_types.AsyncAndBlockingContextManager[_App]:
37
- ...
38
-
39
- def aio(self, app: _App, app_ref: str, stdout: typing.Union[_io.TextIOWrapper, None] = None, show_progress: bool = True, _watcher: typing.Union[typing.AsyncGenerator[typing.Set[str], None], None] = None, environment_name: typing.Union[str, None] = None) -> typing.AsyncContextManager[_App]:
40
- ...
33
+ def __call__(
34
+ self,
35
+ app: _App,
36
+ app_ref: str,
37
+ _watcher: typing.Optional[typing.Generator[set[str], None, None]] = None,
38
+ environment_name: typing.Optional[str] = None,
39
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[_App]: ...
40
+ def aio(
41
+ self,
42
+ app: _App,
43
+ app_ref: str,
44
+ _watcher: typing.Optional[collections.abc.AsyncGenerator[set[str], None]] = None,
45
+ environment_name: typing.Optional[str] = None,
46
+ ) -> typing.AsyncContextManager[_App]: ...
41
47
 
42
48
  serve_app: __serve_app_spec
43
49
 
44
-
45
- def _serve_stub(app: _App, app_ref: str, stdout: typing.Union[_io.TextIOWrapper, None] = None, show_progress: bool = True, _watcher: typing.Union[typing.AsyncGenerator[typing.Set[str], None], None] = None, environment_name: typing.Union[str, None] = None) -> typing.AsyncContextManager[_App]:
46
- ...
47
-
48
-
49
- class __serve_stub_spec(typing_extensions.Protocol):
50
- def __call__(self, app: _App, app_ref: str, stdout: typing.Union[_io.TextIOWrapper, None] = None, show_progress: bool = True, _watcher: typing.Union[typing.Generator[typing.Set[str], None, None], None] = None, environment_name: typing.Union[str, None] = None) -> synchronicity.combined_types.AsyncAndBlockingContextManager[_App]:
51
- ...
52
-
53
- def aio(self, app: _App, app_ref: str, stdout: typing.Union[_io.TextIOWrapper, None] = None, show_progress: bool = True, _watcher: typing.Union[typing.AsyncGenerator[typing.Set[str], None], None] = None, environment_name: typing.Union[str, None] = None) -> typing.AsyncContextManager[_App]:
54
- ...
55
-
56
- serve_stub: __serve_stub_spec
50
+ def serve_stub(*args, **kwargs): ...
modal/stream_type.py ADDED
@@ -0,0 +1,15 @@
1
+ # Copyright Modal Labs 2022
2
+ import subprocess
3
+ from enum import Enum
4
+
5
+
6
+ class StreamType(Enum):
7
+ # Discard all logs from the stream.
8
+ DEVNULL = subprocess.DEVNULL
9
+ # Store logs in a pipe to be read by the client.
10
+ PIPE = subprocess.PIPE
11
+ # Print logs to stdout immediately.
12
+ STDOUT = subprocess.STDOUT
13
+
14
+ def __repr__(self):
15
+ return f"{self.__module__}.{self.__class__.__name__}.{self.name}"
modal/token_flow.py CHANGED
@@ -2,7 +2,8 @@
2
2
  import itertools
3
3
  import os
4
4
  import webbrowser
5
- from typing import AsyncGenerator, Optional, Tuple
5
+ from collections.abc import AsyncGenerator
6
+ from typing import Optional
6
7
 
7
8
  import aiohttp.web
8
9
  from rich.console import Console
@@ -24,7 +25,7 @@ class _TokenFlow:
24
25
  @asynccontextmanager
25
26
  async def start(
26
27
  self, utm_source: Optional[str] = None, next_url: Optional[str] = None
27
- ) -> AsyncGenerator[Tuple[str, str, str], None]:
28
+ ) -> AsyncGenerator[tuple[str, str, str], None]:
28
29
  """mdmd:hidden"""
29
30
  # Run a temporary http server returning the token id on /
30
31
  # This helps us add direct validation later
@@ -153,7 +154,8 @@ async def _set_token(
153
154
  with console.status("Storing token", spinner="dots"):
154
155
  _store_user_config(config_data, profile=profile, active_profile=active_profile)
155
156
  console.print(
156
- f"[green]Token written to [magenta]{user_config_path}[/magenta] in profile [magenta]{profile}[/magenta].[/green]"
157
+ f"[green]Token written to [magenta]{user_config_path}[/magenta] in profile "
158
+ f"[magenta]{profile}[/magenta].[/green]"
157
159
  )
158
160
 
159
161