modal 1.0.6.dev58__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.

Files changed (147) hide show
  1. modal/__main__.py +3 -4
  2. modal/_billing.py +80 -0
  3. modal/_clustered_functions.py +7 -3
  4. modal/_clustered_functions.pyi +4 -2
  5. modal/_container_entrypoint.py +41 -49
  6. modal/_functions.py +424 -195
  7. modal/_grpc_client.py +171 -0
  8. modal/_load_context.py +105 -0
  9. modal/_object.py +68 -20
  10. modal/_output.py +58 -45
  11. modal/_partial_function.py +36 -11
  12. modal/_pty.py +7 -3
  13. modal/_resolver.py +21 -35
  14. modal/_runtime/asgi.py +4 -3
  15. modal/_runtime/container_io_manager.py +301 -186
  16. modal/_runtime/container_io_manager.pyi +70 -61
  17. modal/_runtime/execution_context.py +18 -2
  18. modal/_runtime/execution_context.pyi +4 -1
  19. modal/_runtime/gpu_memory_snapshot.py +170 -63
  20. modal/_runtime/user_code_imports.py +28 -58
  21. modal/_serialization.py +57 -1
  22. modal/_utils/async_utils.py +33 -12
  23. modal/_utils/auth_token_manager.py +2 -5
  24. modal/_utils/blob_utils.py +110 -53
  25. modal/_utils/function_utils.py +49 -42
  26. modal/_utils/grpc_utils.py +80 -50
  27. modal/_utils/mount_utils.py +26 -1
  28. modal/_utils/name_utils.py +17 -3
  29. modal/_utils/task_command_router_client.py +536 -0
  30. modal/_utils/time_utils.py +34 -6
  31. modal/app.py +219 -83
  32. modal/app.pyi +229 -56
  33. modal/billing.py +5 -0
  34. modal/{requirements → builder}/2025.06.txt +1 -0
  35. modal/{requirements → builder}/PREVIEW.txt +1 -0
  36. modal/cli/_download.py +19 -3
  37. modal/cli/_traceback.py +3 -2
  38. modal/cli/app.py +4 -4
  39. modal/cli/cluster.py +15 -7
  40. modal/cli/config.py +5 -3
  41. modal/cli/container.py +7 -6
  42. modal/cli/dict.py +22 -16
  43. modal/cli/entry_point.py +12 -5
  44. modal/cli/environment.py +5 -4
  45. modal/cli/import_refs.py +3 -3
  46. modal/cli/launch.py +102 -5
  47. modal/cli/network_file_system.py +9 -13
  48. modal/cli/profile.py +3 -2
  49. modal/cli/programs/launch_instance_ssh.py +94 -0
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/run_marimo.py +95 -0
  52. modal/cli/programs/vscode.py +1 -1
  53. modal/cli/queues.py +57 -26
  54. modal/cli/run.py +58 -16
  55. modal/cli/secret.py +48 -22
  56. modal/cli/utils.py +3 -4
  57. modal/cli/volume.py +28 -25
  58. modal/client.py +13 -116
  59. modal/client.pyi +9 -91
  60. modal/cloud_bucket_mount.py +5 -3
  61. modal/cloud_bucket_mount.pyi +5 -1
  62. modal/cls.py +130 -102
  63. modal/cls.pyi +45 -85
  64. modal/config.py +29 -10
  65. modal/container_process.py +291 -13
  66. modal/container_process.pyi +95 -32
  67. modal/dict.py +282 -63
  68. modal/dict.pyi +423 -73
  69. modal/environments.py +15 -27
  70. modal/environments.pyi +5 -15
  71. modal/exception.py +8 -0
  72. modal/experimental/__init__.py +143 -38
  73. modal/experimental/flash.py +247 -78
  74. modal/experimental/flash.pyi +137 -9
  75. modal/file_io.py +14 -28
  76. modal/file_io.pyi +2 -2
  77. modal/file_pattern_matcher.py +25 -16
  78. modal/functions.pyi +134 -61
  79. modal/image.py +255 -86
  80. modal/image.pyi +300 -62
  81. modal/io_streams.py +436 -126
  82. modal/io_streams.pyi +236 -171
  83. modal/mount.py +62 -157
  84. modal/mount.pyi +45 -172
  85. modal/network_file_system.py +30 -53
  86. modal/network_file_system.pyi +16 -76
  87. modal/object.pyi +42 -8
  88. modal/parallel_map.py +821 -113
  89. modal/parallel_map.pyi +134 -0
  90. modal/partial_function.pyi +4 -1
  91. modal/proxy.py +16 -7
  92. modal/proxy.pyi +10 -2
  93. modal/queue.py +263 -61
  94. modal/queue.pyi +409 -66
  95. modal/runner.py +112 -92
  96. modal/runner.pyi +45 -27
  97. modal/sandbox.py +451 -124
  98. modal/sandbox.pyi +513 -67
  99. modal/secret.py +291 -67
  100. modal/secret.pyi +425 -19
  101. modal/serving.py +7 -11
  102. modal/serving.pyi +7 -8
  103. modal/snapshot.py +11 -8
  104. modal/token_flow.py +4 -4
  105. modal/volume.py +344 -98
  106. modal/volume.pyi +464 -68
  107. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
  108. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  109. modal_docs/mdmd/mdmd.py +11 -1
  110. modal_proto/api.proto +399 -67
  111. modal_proto/api_grpc.py +241 -1
  112. modal_proto/api_pb2.py +1395 -1000
  113. modal_proto/api_pb2.pyi +1239 -79
  114. modal_proto/api_pb2_grpc.py +499 -4
  115. modal_proto/api_pb2_grpc.pyi +162 -14
  116. modal_proto/modal_api_grpc.py +175 -160
  117. modal_proto/sandbox_router.proto +145 -0
  118. modal_proto/sandbox_router_grpc.py +105 -0
  119. modal_proto/sandbox_router_pb2.py +149 -0
  120. modal_proto/sandbox_router_pb2.pyi +333 -0
  121. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  122. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  123. modal_proto/task_command_router.proto +144 -0
  124. modal_proto/task_command_router_grpc.py +105 -0
  125. modal_proto/task_command_router_pb2.py +149 -0
  126. modal_proto/task_command_router_pb2.pyi +333 -0
  127. modal_proto/task_command_router_pb2_grpc.py +203 -0
  128. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  129. modal_version/__init__.py +1 -1
  130. modal-1.0.6.dev58.dist-info/RECORD +0 -183
  131. modal_proto/modal_options_grpc.py +0 -3
  132. modal_proto/options.proto +0 -19
  133. modal_proto/options_grpc.py +0 -3
  134. modal_proto/options_pb2.py +0 -35
  135. modal_proto/options_pb2.pyi +0 -20
  136. modal_proto/options_pb2_grpc.py +0 -4
  137. modal_proto/options_pb2_grpc.pyi +0 -7
  138. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  139. /modal/{requirements → builder}/2023.12.txt +0 -0
  140. /modal/{requirements → builder}/2024.04.txt +0 -0
  141. /modal/{requirements → builder}/2024.10.txt +0 -0
  142. /modal/{requirements → builder}/README.md +0 -0
  143. /modal/{requirements → builder}/base-images.json +0 -0
  144. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  145. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  146. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  147. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/secret.py CHANGED
@@ -1,24 +1,238 @@
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 ._object import _get_environment_name, _Object
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
18
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
14
- from ._utils.grpc_utils import retry_transient_errors
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
 
@@ -29,10 +243,30 @@ class _Secret(_Object, type_prefix="st"):
29
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, Union[str, None]
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(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
-
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
- return _Secret._from_loader(_load, rep, hydrate_lazily=True)
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(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
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,18 +368,13 @@ 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)
152
-
153
- req = api_pb2.SecretGetOrCreateRequest(
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)
371
+ env_dict = {k: v or "" for k, v in dotenv_values(dotenv_path).items()}
159
372
 
160
- self._hydrate(resp.secret_id, resolver.client, None)
373
+ await _load_from_env_dict(self, load_context, env_dict)
161
374
 
162
- return _Secret._from_loader(_load, "Secret.from_dotenv()", hydrate_lazily=True)
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(
@@ -170,6 +385,7 @@ class _Secret(_Object, type_prefix="st"):
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
 
@@ -187,54 +403,51 @@ class _Secret(_Object, type_prefix="st"):
187
403
  """
188
404
  warn_if_passing_namespace(namespace, "modal.Secret.from_name")
189
405
 
190
- async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
406
+ async def _load(
407
+ self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
408
+ ):
191
409
  req = api_pb2.SecretGetOrCreateRequest(
192
410
  deployment_name=name,
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 resolver.client.stub.SecretGetOrCreate(req)
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, resolver.client, None)
204
-
205
- return _Secret._from_loader(_load, "Secret()", hydrate_lazily=True)
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 lookup(
209
- name: str,
433
+ async def create_deployed(
434
+ deployment_name: str,
435
+ env_dict: dict[str, str],
210
436
  namespace=None, # mdmd:line-hidden
211
437
  client: Optional[_Client] = None,
212
438
  environment_name: Optional[str] = None,
213
- required_keys: list[str] = [],
214
- ) -> "_Secret":
439
+ overwrite: bool = False,
440
+ ) -> str:
215
441
  """mdmd:hidden"""
216
442
  deprecation_warning(
217
- (2025, 1, 27),
218
- "`modal.Secret.lookup` is deprecated and will be removed in a future release."
219
- " It can be replaced with `modal.Secret.from_name`."
220
- "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
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`.",
221
446
  )
222
-
223
- warn_if_passing_namespace(namespace, "modal.Secret.lookup")
224
-
225
- obj = _Secret.from_name(
226
- name,
227
- environment_name=environment_name,
228
- required_keys=required_keys,
229
- )
230
- if client is None:
231
- client = await _Client.from_env()
232
- resolver = Resolver(client=client)
233
- await resolver.load(obj)
234
- return obj
447
+ return await _Secret._create_deployed(deployment_name, env_dict, namespace, client, environment_name, overwrite)
235
448
 
236
449
  @staticmethod
237
- async def create_deployed(
450
+ async def _create_deployed(
238
451
  deployment_name: str,
239
452
  env_dict: dict[str, str],
240
453
  namespace=None, # mdmd:line-hidden
@@ -258,8 +471,19 @@ class _Secret(_Object, type_prefix="st"):
258
471
  object_creation_type=object_creation_type,
259
472
  env_dict=env_dict,
260
473
  )
261
- resp = await retry_transient_errors(client.stub.SecretGetOrCreate, request)
474
+ resp = await client.stub.SecretGetOrCreate(request)
262
475
  return resp.secret_id
263
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
+
264
488
 
265
489
  Secret = synchronize_api(_Secret)