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/secret.py CHANGED
@@ -1,15 +1,17 @@
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
12
- from .app import is_local
14
+ from ._utils.name_utils import check_object_name
13
15
  from .client import _Client
14
16
  from .exception import InvalidError, NotFoundError
15
17
  from .object import _get_environment_name, _Object
@@ -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
  ):
@@ -37,7 +39,7 @@ class _Secret(_Object, type_prefix="st"):
37
39
 
38
40
  Usage:
39
41
  ```python
40
- @stub.function(secrets=[modal.Secret.from_dict({"FOO": "bar"})])
42
+ @app.function(secrets=[modal.Secret.from_dict({"FOO": "bar"})])
41
43
  def run():
42
44
  print(os.environ["FOO"])
43
45
  ```
@@ -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,13 +90,13 @@ 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({})
89
97
 
90
98
  @staticmethod
91
- def from_dotenv(path=None):
99
+ def from_dotenv(path=None, *, filename=".env"):
92
100
  """Create secrets from a .env file automatically.
93
101
 
94
102
  If no argument is provided, it will use the current working directory as the starting
@@ -98,13 +106,22 @@ class _Secret(_Object, type_prefix="st"):
98
106
  If called with an argument, it will use that as a starting point for finding `.env` files.
99
107
  In particular, you can call it like this:
100
108
  ```python
101
- @stub.function(secrets=[modal.Secret.from_dotenv(__file__)])
109
+ @app.function(secrets=[modal.Secret.from_dotenv(__file__)])
102
110
  def run():
103
111
  print(os.environ["USERNAME"]) # Assumes USERNAME is defined in your .env file
104
112
  ```
105
113
 
106
114
  This will use the location of the script calling `modal.Secret.from_dotenv` as a
107
115
  starting point for finding the `.env` file.
116
+
117
+ A file named `.env` is expected by default, but this can be overridden with the `filename`
118
+ keyword argument:
119
+
120
+ ```python
121
+ @app.function(secrets=[modal.Secret.from_dotenv(filename=".env-dev")])
122
+ def run():
123
+ ...
124
+ ```
108
125
  """
109
126
 
110
127
  async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
@@ -119,17 +136,17 @@ class _Secret(_Object, type_prefix="st"):
119
136
  if path is not None:
120
137
  # This basically implements the logic in find_dotenv
121
138
  for dirname in _walk_to_root(path):
122
- check_path = os.path.join(dirname, ".env")
139
+ check_path = os.path.join(dirname, filename)
123
140
  if os.path.isfile(check_path):
124
141
  dotenv_path = check_path
125
142
  break
126
143
  else:
127
144
  dotenv_path = ""
128
145
  else:
129
- # TODO(erikbern): dotenv tries to locate .env files based on the location of the file in the stack frame.
130
- # 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.
131
148
  # To simplify this, we just support the cwd and don't do any automatic path inference.
132
- dotenv_path = find_dotenv(usecwd=True)
149
+ dotenv_path = find_dotenv(filename, usecwd=True)
133
150
 
134
151
  env_dict = dotenv_values(dotenv_path)
135
152
 
@@ -142,20 +159,28 @@ class _Secret(_Object, type_prefix="st"):
142
159
 
143
160
  self._hydrate(resp.secret_id, resolver.client, None)
144
161
 
145
- return _Secret._from_loader(_load, "Secret.from_dotenv()")
162
+ return _Secret._from_loader(_load, "Secret.from_dotenv()", hydrate_lazily=True)
146
163
 
147
164
  @staticmethod
165
+ @renamed_parameter((2024, 12, 18), "label", "name")
148
166
  def from_name(
149
- label: str,
167
+ name: str,
150
168
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
151
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)
152
173
  ) -> "_Secret":
153
- """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.
154
179
 
155
180
  ```python
156
181
  secret = modal.Secret.from_name("my-secret")
157
182
 
158
- @stub.function(secrets=[secret])
183
+ @app.function(secrets=[secret])
159
184
  def run():
160
185
  ...
161
186
  ```
@@ -163,9 +188,10 @@ class _Secret(_Object, type_prefix="st"):
163
188
 
164
189
  async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
165
190
  req = api_pb2.SecretGetOrCreateRequest(
166
- deployment_name=label,
191
+ deployment_name=name,
167
192
  namespace=namespace,
168
193
  environment_name=_get_environment_name(environment_name, resolver),
194
+ required_keys=required_keys,
169
195
  )
170
196
  try:
171
197
  response = await resolver.client.stub.SecretGetOrCreate(req)
@@ -176,23 +202,21 @@ class _Secret(_Object, type_prefix="st"):
176
202
  raise
177
203
  self._hydrate(response.secret_id, resolver.client, None)
178
204
 
179
- return _Secret._from_loader(_load, "Secret()")
205
+ return _Secret._from_loader(_load, "Secret()", hydrate_lazily=True)
180
206
 
181
207
  @staticmethod
208
+ @renamed_parameter((2024, 12, 18), "label", "name")
182
209
  async def lookup(
183
- label: str,
210
+ name: str,
184
211
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
185
212
  client: Optional[_Client] = None,
186
213
  environment_name: Optional[str] = None,
214
+ required_keys: list[str] = [],
187
215
  ) -> "_Secret":
188
- """Lookup a secret with a given name
189
-
190
- ```python
191
- s = modal.Secret.lookup("my-secret")
192
- print(s.object_id)
193
- ```
194
- """
195
- 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
+ )
196
220
  if client is None:
197
221
  client = await _Client.from_env()
198
222
  resolver = Resolver(client=client)
@@ -202,13 +226,14 @@ class _Secret(_Object, type_prefix="st"):
202
226
  @staticmethod
203
227
  async def create_deployed(
204
228
  deployment_name: str,
205
- env_dict: Dict[str, str],
229
+ env_dict: dict[str, str],
206
230
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
207
231
  client: Optional[_Client] = None,
208
232
  environment_name: Optional[str] = None,
209
233
  overwrite: bool = False,
210
234
  ) -> str:
211
235
  """mdmd:hidden"""
236
+ check_object_name(deployment_name, "Secret")
212
237
  if client is None:
213
238
  client = await _Client.from_env()
214
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):
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):
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,43 +1,48 @@
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
- from .cli.import_refs import import_stub
17
+ from .cli.import_refs import import_app
18
18
  from .client import _Client
19
19
  from .config import config
20
- from .runner import _run_stub, 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
- from .stub import _Stub
24
+ from .app import _App
24
25
  else:
25
- _Stub = TypeVar("_Stub")
26
+ _App = TypeVar("_App")
26
27
 
27
28
 
28
- def _run_serve(stub_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
- _stub = import_stub(stub_ref)
31
- blocking_stub = synchronizer._translate_out(_stub, Interface.BLOCKING)
32
- serve_update(blocking_stub, existing_app_id, is_ready, environment_name)
31
+ _app = import_app(app_ref)
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(
36
- stub_ref: str, existing_app_id: str, environment_name: str, timeout: float = 5.0
39
+ app_ref: str, existing_app_id: str, environment_name: str, timeout: float = 5.0
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=(stub_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,28 +50,27 @@ 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
63
68
 
64
69
 
65
70
  async def _run_watch_loop(
66
- stub_ref: str,
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,55 +79,46 @@ 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)
86
- curr_proc = await _restart_serve(stub_ref, existing_app_id=app_id, environment_name=environment_name)
90
+ await _terminate(curr_proc)
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_stub_description(stub_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(stub_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
- async def _serve_stub(
103
- stub: "_Stub",
104
- stub_ref: str,
105
- stdout: Optional[io.TextIOWrapper] = None,
106
- show_progress: bool = True,
107
- _watcher: Optional[AsyncGenerator[Set[str], None]] = None, # for testing
97
+ async def _serve_app(
98
+ app: "_App",
99
+ app_ref: str,
100
+ _watcher: Optional[AsyncGenerator[set[str], None]] = None, # for testing
108
101
  environment_name: Optional[str] = None,
109
- ) -> AsyncGenerator["_Stub", None]:
102
+ ) -> AsyncGenerator["_App", None]:
110
103
  if environment_name is None:
111
104
  environment_name = config.get("environment")
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 = stub._get_watch_mounts()
120
- watcher = watch(mounts_to_watch, output_mgr)
121
-
122
- async with _run_stub(stub, client=client, output_mgr=output_mgr, environment_name=environment_name):
123
- client.set_pre_stop(stub._local_app.disconnect)
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)
124
114
  async with TaskContext(grace=0.1) as tc:
125
- tc.create_task(_run_watch_loop(stub_ref, stub.app_id, output_mgr, watcher, environment_name))
126
- yield stub
115
+ tc.create_task(_run_watch_loop(app_ref, app.app_id, watcher, environment_name))
116
+ yield app
117
+
118
+
119
+ def _serve_stub(*args, **kwargs):
120
+ deprecation_error((2024, 5, 1), "`serve_stub` is deprecated. Please use `serve_app` instead.")
127
121
 
128
122
 
123
+ serve_app = synchronize_api(_serve_app)
129
124
  serve_stub = synchronize_api(_serve_stub)
modal/serving.pyi CHANGED
@@ -1,42 +1,50 @@
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
6
5
  import typing
7
6
  import typing_extensions
8
7
 
9
- _Stub = typing.TypeVar("_Stub")
10
-
11
- def _run_serve(stub_ref: str, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str):
12
- ...
13
-
14
-
15
- async def _restart_serve(stub_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(stub_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_stub_description(stub_ref: str) -> str:
28
- ...
29
-
30
-
31
- def _serve_stub(stub: _Stub, stub_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[_Stub]:
32
- ...
33
-
34
-
35
- class __serve_stub_spec(typing_extensions.Protocol):
36
- def __call__(self, stub: _Stub, stub_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[_Stub]:
37
- ...
38
-
39
- def aio(self, stub: _Stub, stub_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[_Stub]:
40
- ...
41
-
42
- serve_stub: __serve_stub_spec
8
+ _App = typing.TypeVar("_App")
9
+
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): ...
31
+
32
+ class __serve_app_spec(typing_extensions.Protocol):
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]: ...
47
+
48
+ serve_app: __serve_app_spec
49
+
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