modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__init__.py +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +11 -12
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/schedule.py
CHANGED
|
@@ -13,7 +13,7 @@ class Cron(Schedule):
|
|
|
13
13
|
"""Cron jobs are a type of schedule, specified using the
|
|
14
14
|
[Unix cron tab](https://crontab.guru/) syntax.
|
|
15
15
|
|
|
16
|
-
The alternative schedule type is the [`modal.Period`](/docs/reference/modal.Period).
|
|
16
|
+
The alternative schedule type is the [`modal.Period`](https://modal.com/docs/reference/modal.Period).
|
|
17
17
|
|
|
18
18
|
**Usage**
|
|
19
19
|
|
modal/secret.py
CHANGED
|
@@ -1,38 +1,272 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
3
5
|
from typing import Optional, Union
|
|
4
6
|
|
|
7
|
+
from google.protobuf.message import Message
|
|
5
8
|
from grpclib import GRPCError, Status
|
|
9
|
+
from synchronicity import classproperty
|
|
6
10
|
|
|
7
11
|
from modal_proto import api_pb2
|
|
8
12
|
|
|
9
|
-
from .
|
|
13
|
+
from ._load_context import LoadContext
|
|
14
|
+
from ._object import _get_environment_name, _Object, live_method
|
|
10
15
|
from ._resolver import Resolver
|
|
11
16
|
from ._runtime.execution_context import is_local
|
|
12
17
|
from ._utils.async_utils import synchronize_api
|
|
13
|
-
from ._utils.deprecation import deprecation_warning
|
|
14
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
18
|
+
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
|
15
19
|
from ._utils.name_utils import check_object_name
|
|
20
|
+
from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
|
|
16
21
|
from .client import _Client
|
|
17
|
-
from .exception import InvalidError, NotFoundError
|
|
22
|
+
from .exception import AlreadyExistsError, InvalidError, NotFoundError
|
|
18
23
|
|
|
19
24
|
ENV_DICT_WRONG_TYPE_ERR = "the env_dict argument to Secret has to be a dict[str, Union[str, None]]"
|
|
20
25
|
|
|
21
26
|
|
|
27
|
+
@dataclass
|
|
28
|
+
class SecretInfo:
|
|
29
|
+
"""Information about the Secret object."""
|
|
30
|
+
|
|
31
|
+
# This dataclass should be limited to information that is unchanging over the lifetime of the Secret,
|
|
32
|
+
# since it is transmitted from the server when the object is hydrated and could be stale when accessed.
|
|
33
|
+
|
|
34
|
+
name: Optional[str]
|
|
35
|
+
created_at: datetime
|
|
36
|
+
created_by: Optional[str]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _SecretManager:
|
|
40
|
+
"""Namespace with methods for managing named Secret objects."""
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
async def create(
|
|
44
|
+
name: str, # Name to use for the new Secret
|
|
45
|
+
env_dict: dict[str, str], # Key-value pairs to set in the Secret
|
|
46
|
+
*,
|
|
47
|
+
allow_existing: bool = False, # If True, no-op when the Secret already exists
|
|
48
|
+
environment_name: Optional[str] = None, # Uses active environment if not specified
|
|
49
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Create a new Secret object.
|
|
52
|
+
|
|
53
|
+
**Examples:**
|
|
54
|
+
|
|
55
|
+
```python notest
|
|
56
|
+
contents = {"MY_KEY": "my-value", "MY_OTHER_KEY": "my-other-value"}
|
|
57
|
+
modal.Secret.objects.create("my-secret", contents)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Secrets will be created in the active environment, or another one can be specified:
|
|
61
|
+
|
|
62
|
+
```python notest
|
|
63
|
+
modal.Secret.objects.create("my-secret", contents, environment_name="dev")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
By default, an error will be raised if the Secret already exists, but passing
|
|
67
|
+
`allow_existing=True` will make the creation attempt a no-op in this case.
|
|
68
|
+
If the `env_dict` data differs from the existing Secret, it will be ignored.
|
|
69
|
+
|
|
70
|
+
```python notest
|
|
71
|
+
modal.Secret.objects.create("my-secret", contents, allow_existing=True)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Note that this method does not return a local instance of the Secret. You can use
|
|
75
|
+
`modal.Secret.from_name` to perform a lookup after creation.
|
|
76
|
+
|
|
77
|
+
Added in v1.1.2.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
check_object_name(name, "Secret")
|
|
81
|
+
client = await _Client.from_env() if client is None else client
|
|
82
|
+
object_creation_type = (
|
|
83
|
+
api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
|
|
84
|
+
if allow_existing
|
|
85
|
+
else api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS
|
|
86
|
+
)
|
|
87
|
+
req = api_pb2.SecretGetOrCreateRequest(
|
|
88
|
+
deployment_name=name,
|
|
89
|
+
environment_name=_get_environment_name(environment_name),
|
|
90
|
+
object_creation_type=object_creation_type,
|
|
91
|
+
env_dict=env_dict,
|
|
92
|
+
)
|
|
93
|
+
try:
|
|
94
|
+
await client.stub.SecretGetOrCreate(req)
|
|
95
|
+
except GRPCError as exc:
|
|
96
|
+
if exc.status == Status.ALREADY_EXISTS and not allow_existing:
|
|
97
|
+
raise AlreadyExistsError(exc.message)
|
|
98
|
+
else:
|
|
99
|
+
raise
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
async def list(
|
|
103
|
+
*,
|
|
104
|
+
max_objects: Optional[int] = None, # Limit requests to this size
|
|
105
|
+
created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
|
|
106
|
+
environment_name: str = "", # Uses active environment if not specified
|
|
107
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
108
|
+
) -> list["_Secret"]:
|
|
109
|
+
"""Return a list of hydrated Secret objects.
|
|
110
|
+
|
|
111
|
+
**Examples:**
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
secrets = modal.Secret.objects.list()
|
|
115
|
+
print([s.name for s in secrets])
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Secrets will be retreived from the active environment, or another one can be specified:
|
|
119
|
+
|
|
120
|
+
```python notest
|
|
121
|
+
dev_secrets = modal.Secret.objects.list(environment_name="dev")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
By default, all named Secrets are returned, newest to oldest. It's also possible to limit the
|
|
125
|
+
number of results and to filter by creation date:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
secrets = modal.Secret.objects.list(max_objects=10, created_before="2025-01-01")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Added in v1.1.2.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
client = await _Client.from_env() if client is None else client
|
|
135
|
+
if max_objects is not None and max_objects < 0:
|
|
136
|
+
raise InvalidError("max_objects cannot be negative")
|
|
137
|
+
|
|
138
|
+
items: list[api_pb2.SecretListItem] = []
|
|
139
|
+
|
|
140
|
+
async def retrieve_page(created_before: float) -> bool:
|
|
141
|
+
max_page_size = 100 if max_objects is None else min(100, max_objects - len(items))
|
|
142
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
|
143
|
+
req = api_pb2.SecretListRequest(
|
|
144
|
+
environment_name=_get_environment_name(environment_name), pagination=pagination
|
|
145
|
+
)
|
|
146
|
+
resp = await client.stub.SecretList(req)
|
|
147
|
+
items.extend(resp.items)
|
|
148
|
+
finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
|
|
149
|
+
return finished
|
|
150
|
+
|
|
151
|
+
finished = await retrieve_page(as_timestamp(created_before))
|
|
152
|
+
while True:
|
|
153
|
+
if finished:
|
|
154
|
+
break
|
|
155
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
|
156
|
+
|
|
157
|
+
secrets = [
|
|
158
|
+
_Secret._new_hydrated(
|
|
159
|
+
item.secret_id,
|
|
160
|
+
client,
|
|
161
|
+
item.metadata,
|
|
162
|
+
is_another_app=True,
|
|
163
|
+
rep=_Secret._repr(item.label, environment_name),
|
|
164
|
+
)
|
|
165
|
+
for item in items
|
|
166
|
+
]
|
|
167
|
+
return secrets[:max_objects] if max_objects is not None else secrets
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
async def delete(
|
|
171
|
+
name: str, # Name of the Secret to delete
|
|
172
|
+
*,
|
|
173
|
+
allow_missing: bool = False, # If True, don't raise an error if the Secret doesn't exist
|
|
174
|
+
environment_name: Optional[str] = None, # Uses active environment if not specified
|
|
175
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
176
|
+
):
|
|
177
|
+
"""Delete a named Secret.
|
|
178
|
+
|
|
179
|
+
Warning: Deletion is irreversible and will affect any Apps currently using the Secret.
|
|
180
|
+
|
|
181
|
+
**Examples:**
|
|
182
|
+
|
|
183
|
+
```python notest
|
|
184
|
+
await modal.Secret.objects.delete("my-secret")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Secrets will be deleted from the active environment, or another one can be specified:
|
|
188
|
+
|
|
189
|
+
```python notest
|
|
190
|
+
await modal.Secret.objects.delete("my-secret", environment_name="dev")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Added in v1.1.2.
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
obj = await _Secret.from_name(name, environment_name=environment_name).hydrate(client)
|
|
198
|
+
except NotFoundError:
|
|
199
|
+
if not allow_missing:
|
|
200
|
+
raise
|
|
201
|
+
else:
|
|
202
|
+
req = api_pb2.SecretDeleteRequest(secret_id=obj.object_id)
|
|
203
|
+
await obj._client.stub.SecretDelete(req)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
SecretManager = synchronize_api(_SecretManager)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def _load_from_env_dict(instance: "_Secret", load_context: LoadContext, env_dict: dict[str, str]):
|
|
210
|
+
"""helper method for loaders .from_dict and .from_dotenv etc."""
|
|
211
|
+
if load_context.app_id is not None:
|
|
212
|
+
req = api_pb2.SecretGetOrCreateRequest(
|
|
213
|
+
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
|
|
214
|
+
env_dict=env_dict,
|
|
215
|
+
app_id=load_context.app_id,
|
|
216
|
+
environment_name=load_context.environment_name,
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
req = api_pb2.SecretGetOrCreateRequest(
|
|
220
|
+
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL,
|
|
221
|
+
env_dict=env_dict,
|
|
222
|
+
environment_name=load_context.environment_name,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
resp = await load_context.client.stub.SecretGetOrCreate(req)
|
|
227
|
+
except GRPCError as exc:
|
|
228
|
+
if exc.status == Status.INVALID_ARGUMENT:
|
|
229
|
+
raise InvalidError(exc.message)
|
|
230
|
+
if exc.status == Status.FAILED_PRECONDITION:
|
|
231
|
+
raise InvalidError(exc.message)
|
|
232
|
+
raise
|
|
233
|
+
instance._hydrate(resp.secret_id, load_context.client, resp.metadata)
|
|
234
|
+
|
|
235
|
+
|
|
22
236
|
class _Secret(_Object, type_prefix="st"):
|
|
23
237
|
"""Secrets provide a dictionary of environment variables for images.
|
|
24
238
|
|
|
25
239
|
Secrets are a secure way to add credentials and other sensitive information
|
|
26
240
|
to the containers your functions run in. You can create and edit secrets on
|
|
27
|
-
[the dashboard](/secrets), or programmatically from Python code.
|
|
241
|
+
[the dashboard](https://modal.com/secrets), or programmatically from Python code.
|
|
28
242
|
|
|
29
|
-
See [the secrets guide page](/docs/guide/secrets) for more information.
|
|
243
|
+
See [the secrets guide page](https://modal.com/docs/guide/secrets) for more information.
|
|
30
244
|
"""
|
|
31
245
|
|
|
246
|
+
_metadata: Optional[api_pb2.SecretMetadata] = None
|
|
247
|
+
|
|
248
|
+
@classproperty
|
|
249
|
+
def objects(cls) -> _SecretManager:
|
|
250
|
+
return _SecretManager
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def name(self) -> Optional[str]:
|
|
254
|
+
return self._name
|
|
255
|
+
|
|
256
|
+
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
257
|
+
if metadata:
|
|
258
|
+
assert isinstance(metadata, api_pb2.SecretMetadata)
|
|
259
|
+
self._metadata = metadata
|
|
260
|
+
self._name = metadata.name
|
|
261
|
+
|
|
262
|
+
def _get_metadata(self) -> api_pb2.SecretMetadata:
|
|
263
|
+
assert self._metadata
|
|
264
|
+
return self._metadata
|
|
265
|
+
|
|
32
266
|
@staticmethod
|
|
33
267
|
def from_dict(
|
|
34
268
|
env_dict: dict[
|
|
35
|
-
str,
|
|
269
|
+
str, Optional[str]
|
|
36
270
|
] = {}, # dict of entries to be inserted as environment variables in functions using the secret
|
|
37
271
|
) -> "_Secret":
|
|
38
272
|
"""Create a secret from a str-str dictionary. Values can also be `None`, which is ignored.
|
|
@@ -53,30 +287,14 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
53
287
|
if not all(isinstance(v, str) for v in env_dict_filtered.values()):
|
|
54
288
|
raise InvalidError(ENV_DICT_WRONG_TYPE_ERR)
|
|
55
289
|
|
|
56
|
-
async def _load(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
object_creation_type = api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL
|
|
61
|
-
|
|
62
|
-
req = api_pb2.SecretGetOrCreateRequest(
|
|
63
|
-
object_creation_type=object_creation_type,
|
|
64
|
-
env_dict=env_dict_filtered,
|
|
65
|
-
app_id=resolver.app_id,
|
|
66
|
-
environment_name=resolver.environment_name,
|
|
67
|
-
)
|
|
68
|
-
try:
|
|
69
|
-
resp = await resolver.client.stub.SecretGetOrCreate(req)
|
|
70
|
-
except GRPCError as exc:
|
|
71
|
-
if exc.status == Status.INVALID_ARGUMENT:
|
|
72
|
-
raise InvalidError(exc.message)
|
|
73
|
-
if exc.status == Status.FAILED_PRECONDITION:
|
|
74
|
-
raise InvalidError(exc.message)
|
|
75
|
-
raise
|
|
76
|
-
self._hydrate(resp.secret_id, resolver.client, None)
|
|
290
|
+
async def _load(
|
|
291
|
+
self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
292
|
+
):
|
|
293
|
+
await _load_from_env_dict(self, load_context, env_dict_filtered)
|
|
77
294
|
|
|
78
295
|
rep = f"Secret.from_dict([{', '.join(env_dict.keys())}])"
|
|
79
|
-
|
|
296
|
+
# TODO: scoping - these should probably not be lazily hydrated without having an app and/or sandbox association
|
|
297
|
+
return _Secret._from_loader(_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty())
|
|
80
298
|
|
|
81
299
|
@staticmethod
|
|
82
300
|
def from_local_environ(
|
|
@@ -96,7 +314,7 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
96
314
|
return _Secret.from_dict({})
|
|
97
315
|
|
|
98
316
|
@staticmethod
|
|
99
|
-
def from_dotenv(path=None, *, filename=".env") -> "_Secret":
|
|
317
|
+
def from_dotenv(path=None, *, filename=".env", client: Optional[_Client] = None) -> "_Secret":
|
|
100
318
|
"""Create secrets from a .env file automatically.
|
|
101
319
|
|
|
102
320
|
If no argument is provided, it will use the current working directory as the starting
|
|
@@ -124,7 +342,9 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
124
342
|
```
|
|
125
343
|
"""
|
|
126
344
|
|
|
127
|
-
async def _load(
|
|
345
|
+
async def _load(
|
|
346
|
+
self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
347
|
+
):
|
|
128
348
|
try:
|
|
129
349
|
from dotenv import dotenv_values, find_dotenv
|
|
130
350
|
from dotenv.main import _walk_to_root
|
|
@@ -148,28 +368,24 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
148
368
|
# To simplify this, we just support the cwd and don't do any automatic path inference.
|
|
149
369
|
dotenv_path = find_dotenv(filename, usecwd=True)
|
|
150
370
|
|
|
151
|
-
env_dict = dotenv_values(dotenv_path)
|
|
371
|
+
env_dict = {k: v or "" for k, v in dotenv_values(dotenv_path).items()}
|
|
152
372
|
|
|
153
|
-
|
|
154
|
-
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
|
|
155
|
-
env_dict=env_dict,
|
|
156
|
-
app_id=resolver.app_id,
|
|
157
|
-
)
|
|
158
|
-
resp = await resolver.client.stub.SecretGetOrCreate(req)
|
|
373
|
+
await _load_from_env_dict(self, load_context, env_dict)
|
|
159
374
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
375
|
+
return _Secret._from_loader(
|
|
376
|
+
_load, "Secret.from_dotenv()", hydrate_lazily=True, load_context_overrides=LoadContext(client=client)
|
|
377
|
+
)
|
|
163
378
|
|
|
164
379
|
@staticmethod
|
|
165
380
|
def from_name(
|
|
166
381
|
name: str,
|
|
167
382
|
*,
|
|
168
|
-
namespace=
|
|
383
|
+
namespace=None, # mdmd:line-hidden
|
|
169
384
|
environment_name: Optional[str] = None,
|
|
170
385
|
required_keys: list[
|
|
171
386
|
str
|
|
172
387
|
] = [], # Optionally, a list of required environment variables (will be asserted server-side)
|
|
388
|
+
client: Optional[_Client] = None,
|
|
173
389
|
) -> "_Secret":
|
|
174
390
|
"""Reference a Secret by its name.
|
|
175
391
|
|
|
@@ -185,59 +401,63 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
185
401
|
...
|
|
186
402
|
```
|
|
187
403
|
"""
|
|
404
|
+
warn_if_passing_namespace(namespace, "modal.Secret.from_name")
|
|
188
405
|
|
|
189
|
-
async def _load(
|
|
406
|
+
async def _load(
|
|
407
|
+
self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
408
|
+
):
|
|
190
409
|
req = api_pb2.SecretGetOrCreateRequest(
|
|
191
410
|
deployment_name=name,
|
|
192
|
-
|
|
193
|
-
environment_name=_get_environment_name(environment_name, resolver),
|
|
411
|
+
environment_name=load_context.environment_name,
|
|
194
412
|
required_keys=required_keys,
|
|
195
413
|
)
|
|
196
414
|
try:
|
|
197
|
-
response = await
|
|
415
|
+
response = await load_context.client.stub.SecretGetOrCreate(req)
|
|
198
416
|
except GRPCError as exc:
|
|
199
417
|
if exc.status == Status.NOT_FOUND:
|
|
200
418
|
raise NotFoundError(exc.message)
|
|
201
419
|
else:
|
|
202
420
|
raise
|
|
203
|
-
self._hydrate(response.secret_id,
|
|
204
|
-
|
|
205
|
-
|
|
421
|
+
self._hydrate(response.secret_id, load_context.client, response.metadata)
|
|
422
|
+
|
|
423
|
+
rep = _Secret._repr(name, environment_name)
|
|
424
|
+
return _Secret._from_loader(
|
|
425
|
+
_load,
|
|
426
|
+
rep,
|
|
427
|
+
hydrate_lazily=True,
|
|
428
|
+
name=name,
|
|
429
|
+
load_context_overrides=LoadContext(environment_name=environment_name, client=client),
|
|
430
|
+
)
|
|
206
431
|
|
|
207
432
|
@staticmethod
|
|
208
|
-
async def
|
|
209
|
-
|
|
210
|
-
|
|
433
|
+
async def create_deployed(
|
|
434
|
+
deployment_name: str,
|
|
435
|
+
env_dict: dict[str, str],
|
|
436
|
+
namespace=None, # mdmd:line-hidden
|
|
211
437
|
client: Optional[_Client] = None,
|
|
212
438
|
environment_name: Optional[str] = None,
|
|
213
|
-
|
|
214
|
-
) ->
|
|
439
|
+
overwrite: bool = False,
|
|
440
|
+
) -> str:
|
|
215
441
|
"""mdmd:hidden"""
|
|
216
442
|
deprecation_warning(
|
|
217
|
-
(2025,
|
|
218
|
-
"`modal.Secret.
|
|
219
|
-
" It can be replaced with `modal.Secret.
|
|
220
|
-
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
221
|
-
)
|
|
222
|
-
obj = _Secret.from_name(
|
|
223
|
-
name, namespace=namespace, environment_name=environment_name, required_keys=required_keys
|
|
443
|
+
(2025, 8, 13),
|
|
444
|
+
"The undocumented `modal.Secret.create_deployed` method is deprecated and will be removed "
|
|
445
|
+
"in a future release. It can be replaced with `modal.Secret.objects.create`.",
|
|
224
446
|
)
|
|
225
|
-
|
|
226
|
-
client = await _Client.from_env()
|
|
227
|
-
resolver = Resolver(client=client)
|
|
228
|
-
await resolver.load(obj)
|
|
229
|
-
return obj
|
|
447
|
+
return await _Secret._create_deployed(deployment_name, env_dict, namespace, client, environment_name, overwrite)
|
|
230
448
|
|
|
231
449
|
@staticmethod
|
|
232
|
-
async def
|
|
450
|
+
async def _create_deployed(
|
|
233
451
|
deployment_name: str,
|
|
234
452
|
env_dict: dict[str, str],
|
|
235
|
-
namespace=
|
|
453
|
+
namespace=None, # mdmd:line-hidden
|
|
236
454
|
client: Optional[_Client] = None,
|
|
237
455
|
environment_name: Optional[str] = None,
|
|
238
456
|
overwrite: bool = False,
|
|
239
457
|
) -> str:
|
|
240
458
|
"""mdmd:hidden"""
|
|
459
|
+
warn_if_passing_namespace(namespace, "modal.Secret.create_deployed")
|
|
460
|
+
|
|
241
461
|
check_object_name(deployment_name, "Secret")
|
|
242
462
|
if client is None:
|
|
243
463
|
client = await _Client.from_env()
|
|
@@ -247,13 +467,23 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
247
467
|
object_creation_type = api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS
|
|
248
468
|
request = api_pb2.SecretGetOrCreateRequest(
|
|
249
469
|
deployment_name=deployment_name,
|
|
250
|
-
namespace=namespace,
|
|
251
470
|
environment_name=_get_environment_name(environment_name),
|
|
252
471
|
object_creation_type=object_creation_type,
|
|
253
472
|
env_dict=env_dict,
|
|
254
473
|
)
|
|
255
|
-
resp = await
|
|
474
|
+
resp = await client.stub.SecretGetOrCreate(request)
|
|
256
475
|
return resp.secret_id
|
|
257
476
|
|
|
477
|
+
@live_method
|
|
478
|
+
async def info(self) -> SecretInfo:
|
|
479
|
+
"""Return information about the Secret object."""
|
|
480
|
+
metadata = self._get_metadata()
|
|
481
|
+
creation_info = metadata.creation_info
|
|
482
|
+
return SecretInfo(
|
|
483
|
+
name=metadata.name or None,
|
|
484
|
+
created_at=timestamp_to_localized_dt(creation_info.created_at),
|
|
485
|
+
created_by=creation_info.created_by or None,
|
|
486
|
+
)
|
|
487
|
+
|
|
258
488
|
|
|
259
489
|
Secret = synchronize_api(_Secret)
|