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.
- modal/__init__.py +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- 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 +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- 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 +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.dist-info → modal-0.72.11.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 +5 -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 +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- 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_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- 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 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- 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 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- 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 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- 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 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {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
|
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 .
|
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:
|
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
|
-
@
|
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:
|
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,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.
|
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
|
-
@
|
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,
|
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
|
130
|
-
# 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.
|
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
|
-
|
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
|
-
"""
|
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
|
-
@
|
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=
|
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
|
-
|
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
|
-
"""
|
189
|
-
|
190
|
-
|
191
|
-
|
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:
|
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:
|
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):
|
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):
|
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,43 +1,48 @@
|
|
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
|
-
from .cli.import_refs import
|
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
|
-
from .
|
24
|
+
from .app import _App
|
24
25
|
else:
|
25
|
-
|
26
|
+
_App = TypeVar("_App")
|
26
27
|
|
27
28
|
|
28
|
-
def _run_serve(
|
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
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
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],
|
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
|
63
68
|
|
64
69
|
|
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,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
|
-
|
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
|
86
|
-
curr_proc = await _restart_serve(
|
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
|
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
|
103
|
-
|
104
|
-
|
105
|
-
|
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["
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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(
|
126
|
-
yield
|
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
|
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
|
-
|
10
|
-
|
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 _serve_stub(
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
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
|
|