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.
- modal/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {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
|
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:
|
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:
|
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=
|
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:
|
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.
|
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
|
139
|
-
# Since the modal code "intermediates" this, a .env file in
|
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
|
-
|
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
|
-
"""
|
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=
|
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
|
-
|
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
|
-
"""
|
198
|
-
|
199
|
-
|
200
|
-
|
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:
|
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:
|
9
|
-
...
|
10
|
-
|
8
|
+
def from_dict(env_dict: dict[str, typing.Optional[str]] = {}): ...
|
11
9
|
@staticmethod
|
12
|
-
def from_local_environ(env_keys:
|
13
|
-
...
|
14
|
-
|
10
|
+
def from_local_environ(env_keys: list[str]): ...
|
15
11
|
@staticmethod
|
16
|
-
def from_dotenv(path=None, *, filename=
|
17
|
-
...
|
18
|
-
|
12
|
+
def from_dotenv(path=None, *, filename=".env"): ...
|
19
13
|
@staticmethod
|
20
|
-
def from_name(
|
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(
|
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(
|
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:
|
38
|
-
...
|
39
|
-
|
38
|
+
def from_dict(env_dict: dict[str, typing.Optional[str]] = {}): ...
|
40
39
|
@staticmethod
|
41
|
-
def from_local_environ(env_keys:
|
42
|
-
...
|
43
|
-
|
40
|
+
def from_local_environ(env_keys: list[str]): ...
|
44
41
|
@staticmethod
|
45
|
-
def from_dotenv(path=None, *, filename=
|
46
|
-
...
|
47
|
-
|
42
|
+
def from_dotenv(path=None, *, filename=".env"): ...
|
48
43
|
@staticmethod
|
49
|
-
def from_name(
|
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__(
|
54
|
-
|
55
|
-
|
56
|
-
|
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__(
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
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,
|
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 .
|
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
|
32
|
-
|
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
|
-
|
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],
|
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
|
60
|
+
if output_mgr := _get_output_manager():
|
61
|
+
output_mgr.print(f"Serve process {proc.pid} terminated")
|
56
62
|
else:
|
57
|
-
output_mgr
|
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
|
-
|
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
|
-
|
79
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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,
|
115
|
+
tc.create_task(_run_watch_loop(app_ref, app.app_id, watcher, environment_name))
|
127
116
|
yield app
|
128
117
|
|
129
118
|
|
130
|
-
|
119
|
+
def _serve_stub(*args, **kwargs):
|
120
|
+
deprecation_error((2024, 5, 1), "`serve_stub` is deprecated. Please use `serve_app` instead.")
|
131
121
|
|
132
|
-
|
133
|
-
|
134
|
-
serve_stub =
|
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
|
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(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def
|
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__(
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
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[
|
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
|
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
|
|