modal 1.0.6.dev61__py3-none-any.whl → 1.1.1__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 (75) hide show
  1. modal/__main__.py +2 -2
  2. modal/_clustered_functions.py +3 -0
  3. modal/_clustered_functions.pyi +3 -2
  4. modal/_functions.py +78 -26
  5. modal/_object.py +9 -1
  6. modal/_output.py +14 -25
  7. modal/_runtime/gpu_memory_snapshot.py +158 -54
  8. modal/_utils/async_utils.py +6 -4
  9. modal/_utils/auth_token_manager.py +1 -1
  10. modal/_utils/blob_utils.py +16 -21
  11. modal/_utils/function_utils.py +16 -4
  12. modal/_utils/time_utils.py +8 -4
  13. modal/app.py +0 -4
  14. modal/app.pyi +0 -4
  15. modal/cli/_traceback.py +3 -2
  16. modal/cli/app.py +4 -4
  17. modal/cli/cluster.py +4 -4
  18. modal/cli/config.py +2 -2
  19. modal/cli/container.py +2 -2
  20. modal/cli/dict.py +4 -4
  21. modal/cli/entry_point.py +2 -2
  22. modal/cli/import_refs.py +3 -3
  23. modal/cli/network_file_system.py +8 -9
  24. modal/cli/profile.py +2 -2
  25. modal/cli/queues.py +5 -5
  26. modal/cli/secret.py +5 -5
  27. modal/cli/utils.py +3 -4
  28. modal/cli/volume.py +8 -9
  29. modal/client.py +8 -1
  30. modal/client.pyi +9 -10
  31. modal/container_process.py +2 -2
  32. modal/dict.py +47 -3
  33. modal/dict.pyi +55 -0
  34. modal/exception.py +4 -0
  35. modal/experimental/__init__.py +1 -1
  36. modal/experimental/flash.py +18 -2
  37. modal/experimental/flash.pyi +19 -0
  38. modal/functions.pyi +6 -7
  39. modal/image.py +26 -10
  40. modal/image.pyi +12 -4
  41. modal/mount.py +1 -1
  42. modal/object.pyi +4 -0
  43. modal/parallel_map.py +432 -4
  44. modal/parallel_map.pyi +28 -0
  45. modal/queue.py +46 -3
  46. modal/queue.pyi +53 -0
  47. modal/sandbox.py +105 -25
  48. modal/sandbox.pyi +108 -18
  49. modal/secret.py +48 -5
  50. modal/secret.pyi +55 -0
  51. modal/token_flow.py +3 -3
  52. modal/volume.py +49 -18
  53. modal/volume.pyi +50 -8
  54. {modal-1.0.6.dev61.dist-info → modal-1.1.1.dist-info}/METADATA +2 -2
  55. {modal-1.0.6.dev61.dist-info → modal-1.1.1.dist-info}/RECORD +75 -75
  56. modal_proto/api.proto +140 -14
  57. modal_proto/api_grpc.py +80 -0
  58. modal_proto/api_pb2.py +927 -756
  59. modal_proto/api_pb2.pyi +488 -34
  60. modal_proto/api_pb2_grpc.py +166 -0
  61. modal_proto/api_pb2_grpc.pyi +52 -0
  62. modal_proto/modal_api_grpc.py +5 -0
  63. modal_version/__init__.py +1 -1
  64. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  65. /modal/{requirements → builder}/2023.12.txt +0 -0
  66. /modal/{requirements → builder}/2024.04.txt +0 -0
  67. /modal/{requirements → builder}/2024.10.txt +0 -0
  68. /modal/{requirements → builder}/2025.06.txt +0 -0
  69. /modal/{requirements → builder}/PREVIEW.txt +0 -0
  70. /modal/{requirements → builder}/README.md +0 -0
  71. /modal/{requirements → builder}/base-images.json +0 -0
  72. {modal-1.0.6.dev61.dist-info → modal-1.1.1.dist-info}/WHEEL +0 -0
  73. {modal-1.0.6.dev61.dist-info → modal-1.1.1.dist-info}/entry_points.txt +0 -0
  74. {modal-1.0.6.dev61.dist-info → modal-1.1.1.dist-info}/licenses/LICENSE +0 -0
  75. {modal-1.0.6.dev61.dist-info → modal-1.1.1.dist-info}/top_level.txt +0 -0
modal/sandbox.pyi CHANGED
@@ -25,7 +25,18 @@ import os
25
25
  import typing
26
26
  import typing_extensions
27
27
 
28
- def _validate_exec_args(entrypoint_args: collections.abc.Sequence[str]) -> None: ...
28
+ def _validate_exec_args(args: collections.abc.Sequence[str]) -> None: ...
29
+
30
+ class DefaultSandboxNameOverride(str):
31
+ """A singleton class that represents the default sandbox name override.
32
+
33
+ It is used to indicate that the sandbox name should not be overridden.
34
+ """
35
+ def __repr__(self) -> str:
36
+ """Return repr(self)."""
37
+ ...
38
+
39
+ _DEFAULT_SANDBOX_NAME_OVERRIDE: DefaultSandboxNameOverride
29
40
 
30
41
  class _Sandbox(modal._object._Object):
31
42
  """A `Sandbox` object lets you interact with a running sandbox. This API is similar to Python's
@@ -44,9 +55,10 @@ class _Sandbox(modal._object._Object):
44
55
 
45
56
  @staticmethod
46
57
  def _new(
47
- entrypoint_args: collections.abc.Sequence[str],
58
+ args: collections.abc.Sequence[str],
48
59
  image: modal.image._Image,
49
60
  secrets: collections.abc.Sequence[modal.secret._Secret],
61
+ name: typing.Optional[str] = None,
50
62
  timeout: typing.Optional[int] = None,
51
63
  workdir: typing.Optional[str] = None,
52
64
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
@@ -67,6 +79,7 @@ class _Sandbox(modal._object._Object):
67
79
  h2_ports: collections.abc.Sequence[int] = [],
68
80
  unencrypted_ports: collections.abc.Sequence[int] = [],
69
81
  proxy: typing.Optional[modal.proxy._Proxy] = None,
82
+ experimental_options: typing.Optional[dict[str, bool]] = None,
70
83
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
71
84
  enable_snapshot: bool = False,
72
85
  verbose: bool = False,
@@ -76,8 +89,9 @@ class _Sandbox(modal._object._Object):
76
89
 
77
90
  @staticmethod
78
91
  async def create(
79
- *entrypoint_args: str,
92
+ *args: str,
80
93
  app: typing.Optional[modal.app._App] = None,
94
+ name: typing.Optional[str] = None,
81
95
  image: typing.Optional[modal.image._Image] = None,
82
96
  secrets: collections.abc.Sequence[modal.secret._Secret] = (),
83
97
  network_file_systems: dict[typing.Union[str, os.PathLike], modal.network_file_system._NetworkFileSystem] = {},
@@ -100,6 +114,7 @@ class _Sandbox(modal._object._Object):
100
114
  unencrypted_ports: collections.abc.Sequence[int] = [],
101
115
  proxy: typing.Optional[modal.proxy._Proxy] = None,
102
116
  verbose: bool = False,
117
+ experimental_options: typing.Optional[dict[str, bool]] = None,
103
118
  _experimental_enable_snapshot: bool = False,
104
119
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
105
120
  client: typing.Optional[modal.client._Client] = None,
@@ -121,8 +136,9 @@ class _Sandbox(modal._object._Object):
121
136
 
122
137
  @staticmethod
123
138
  async def _create(
124
- *entrypoint_args: str,
139
+ *args: str,
125
140
  app: typing.Optional[modal.app._App] = None,
141
+ name: typing.Optional[str] = None,
126
142
  image: typing.Optional[modal.image._Image] = None,
127
143
  secrets: collections.abc.Sequence[modal.secret._Secret] = (),
128
144
  mounts: collections.abc.Sequence[modal.mount._Mount] = (),
@@ -145,12 +161,28 @@ class _Sandbox(modal._object._Object):
145
161
  h2_ports: collections.abc.Sequence[int] = [],
146
162
  unencrypted_ports: collections.abc.Sequence[int] = [],
147
163
  proxy: typing.Optional[modal.proxy._Proxy] = None,
164
+ experimental_options: typing.Optional[dict[str, bool]] = None,
148
165
  _experimental_enable_snapshot: bool = False,
149
166
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
150
167
  client: typing.Optional[modal.client._Client] = None,
151
168
  verbose: bool = False,
152
169
  ): ...
153
170
  def _hydrate_metadata(self, handle_metadata: typing.Optional[google.protobuf.message.Message]): ...
171
+ @staticmethod
172
+ async def from_name(
173
+ app_name: str,
174
+ name: str,
175
+ *,
176
+ environment_name: typing.Optional[str] = None,
177
+ client: typing.Optional[modal.client._Client] = None,
178
+ ) -> _Sandbox:
179
+ """Get a running Sandbox by name from the given app.
180
+
181
+ Raises a modal.exception.NotFoundError if no running sandbox is found with the given name.
182
+ A Sandbox's name is the `name` argument passed to `Sandbox.create`.
183
+ """
184
+ ...
185
+
154
186
  @staticmethod
155
187
  async def from_id(sandbox_id: str, client: typing.Optional[modal.client._Client] = None) -> _Sandbox:
156
188
  """Construct a Sandbox from an id and look up the Sandbox result.
@@ -212,7 +244,7 @@ class _Sandbox(modal._object._Object):
212
244
  @typing.overload
213
245
  async def exec(
214
246
  self,
215
- *cmds: str,
247
+ *args: str,
216
248
  pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
217
249
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
218
250
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
@@ -226,7 +258,7 @@ class _Sandbox(modal._object._Object):
226
258
  @typing.overload
227
259
  async def exec(
228
260
  self,
229
- *cmds: str,
261
+ *args: str,
230
262
  pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
231
263
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
232
264
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
@@ -240,7 +272,10 @@ class _Sandbox(modal._object._Object):
240
272
  async def _experimental_snapshot(self) -> modal.snapshot._SandboxSnapshot: ...
241
273
  @staticmethod
242
274
  async def _experimental_from_snapshot(
243
- snapshot: modal.snapshot._SandboxSnapshot, client: typing.Optional[modal.client._Client] = None
275
+ snapshot: modal.snapshot._SandboxSnapshot,
276
+ client: typing.Optional[modal.client._Client] = None,
277
+ *,
278
+ name: typing.Optional[str] = _DEFAULT_SANDBOX_NAME_OVERRIDE,
244
279
  ): ...
245
280
  @typing.overload
246
281
  async def open(self, path: str, mode: _typeshed.OpenTextMode) -> modal.file_io._FileIO[str]: ...
@@ -329,9 +364,10 @@ class Sandbox(modal.object.Object):
329
364
 
330
365
  @staticmethod
331
366
  def _new(
332
- entrypoint_args: collections.abc.Sequence[str],
367
+ args: collections.abc.Sequence[str],
333
368
  image: modal.image.Image,
334
369
  secrets: collections.abc.Sequence[modal.secret.Secret],
370
+ name: typing.Optional[str] = None,
335
371
  timeout: typing.Optional[int] = None,
336
372
  workdir: typing.Optional[str] = None,
337
373
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
@@ -351,6 +387,7 @@ class Sandbox(modal.object.Object):
351
387
  h2_ports: collections.abc.Sequence[int] = [],
352
388
  unencrypted_ports: collections.abc.Sequence[int] = [],
353
389
  proxy: typing.Optional[modal.proxy.Proxy] = None,
390
+ experimental_options: typing.Optional[dict[str, bool]] = None,
354
391
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
355
392
  enable_snapshot: bool = False,
356
393
  verbose: bool = False,
@@ -362,8 +399,9 @@ class Sandbox(modal.object.Object):
362
399
  def __call__(
363
400
  self,
364
401
  /,
365
- *entrypoint_args: str,
402
+ *args: str,
366
403
  app: typing.Optional[modal.app.App] = None,
404
+ name: typing.Optional[str] = None,
367
405
  image: typing.Optional[modal.image.Image] = None,
368
406
  secrets: collections.abc.Sequence[modal.secret.Secret] = (),
369
407
  network_file_systems: dict[
@@ -388,6 +426,7 @@ class Sandbox(modal.object.Object):
388
426
  unencrypted_ports: collections.abc.Sequence[int] = [],
389
427
  proxy: typing.Optional[modal.proxy.Proxy] = None,
390
428
  verbose: bool = False,
429
+ experimental_options: typing.Optional[dict[str, bool]] = None,
391
430
  _experimental_enable_snapshot: bool = False,
392
431
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
393
432
  client: typing.Optional[modal.client.Client] = None,
@@ -410,8 +449,9 @@ class Sandbox(modal.object.Object):
410
449
  async def aio(
411
450
  self,
412
451
  /,
413
- *entrypoint_args: str,
452
+ *args: str,
414
453
  app: typing.Optional[modal.app.App] = None,
454
+ name: typing.Optional[str] = None,
415
455
  image: typing.Optional[modal.image.Image] = None,
416
456
  secrets: collections.abc.Sequence[modal.secret.Secret] = (),
417
457
  network_file_systems: dict[
@@ -436,6 +476,7 @@ class Sandbox(modal.object.Object):
436
476
  unencrypted_ports: collections.abc.Sequence[int] = [],
437
477
  proxy: typing.Optional[modal.proxy.Proxy] = None,
438
478
  verbose: bool = False,
479
+ experimental_options: typing.Optional[dict[str, bool]] = None,
439
480
  _experimental_enable_snapshot: bool = False,
440
481
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
441
482
  client: typing.Optional[modal.client.Client] = None,
@@ -461,8 +502,9 @@ class Sandbox(modal.object.Object):
461
502
  def __call__(
462
503
  self,
463
504
  /,
464
- *entrypoint_args: str,
505
+ *args: str,
465
506
  app: typing.Optional[modal.app.App] = None,
507
+ name: typing.Optional[str] = None,
466
508
  image: typing.Optional[modal.image.Image] = None,
467
509
  secrets: collections.abc.Sequence[modal.secret.Secret] = (),
468
510
  mounts: collections.abc.Sequence[modal.mount.Mount] = (),
@@ -487,6 +529,7 @@ class Sandbox(modal.object.Object):
487
529
  h2_ports: collections.abc.Sequence[int] = [],
488
530
  unencrypted_ports: collections.abc.Sequence[int] = [],
489
531
  proxy: typing.Optional[modal.proxy.Proxy] = None,
532
+ experimental_options: typing.Optional[dict[str, bool]] = None,
490
533
  _experimental_enable_snapshot: bool = False,
491
534
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
492
535
  client: typing.Optional[modal.client.Client] = None,
@@ -495,8 +538,9 @@ class Sandbox(modal.object.Object):
495
538
  async def aio(
496
539
  self,
497
540
  /,
498
- *entrypoint_args: str,
541
+ *args: str,
499
542
  app: typing.Optional[modal.app.App] = None,
543
+ name: typing.Optional[str] = None,
500
544
  image: typing.Optional[modal.image.Image] = None,
501
545
  secrets: collections.abc.Sequence[modal.secret.Secret] = (),
502
546
  mounts: collections.abc.Sequence[modal.mount.Mount] = (),
@@ -521,6 +565,7 @@ class Sandbox(modal.object.Object):
521
565
  h2_ports: collections.abc.Sequence[int] = [],
522
566
  unencrypted_ports: collections.abc.Sequence[int] = [],
523
567
  proxy: typing.Optional[modal.proxy.Proxy] = None,
568
+ experimental_options: typing.Optional[dict[str, bool]] = None,
524
569
  _experimental_enable_snapshot: bool = False,
525
570
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
526
571
  client: typing.Optional[modal.client.Client] = None,
@@ -531,6 +576,41 @@ class Sandbox(modal.object.Object):
531
576
 
532
577
  def _hydrate_metadata(self, handle_metadata: typing.Optional[google.protobuf.message.Message]): ...
533
578
 
579
+ class __from_name_spec(typing_extensions.Protocol):
580
+ def __call__(
581
+ self,
582
+ /,
583
+ app_name: str,
584
+ name: str,
585
+ *,
586
+ environment_name: typing.Optional[str] = None,
587
+ client: typing.Optional[modal.client.Client] = None,
588
+ ) -> Sandbox:
589
+ """Get a running Sandbox by name from the given app.
590
+
591
+ Raises a modal.exception.NotFoundError if no running sandbox is found with the given name.
592
+ A Sandbox's name is the `name` argument passed to `Sandbox.create`.
593
+ """
594
+ ...
595
+
596
+ async def aio(
597
+ self,
598
+ /,
599
+ app_name: str,
600
+ name: str,
601
+ *,
602
+ environment_name: typing.Optional[str] = None,
603
+ client: typing.Optional[modal.client.Client] = None,
604
+ ) -> Sandbox:
605
+ """Get a running Sandbox by name from the given app.
606
+
607
+ Raises a modal.exception.NotFoundError if no running sandbox is found with the given name.
608
+ A Sandbox's name is the `name` argument passed to `Sandbox.create`.
609
+ """
610
+ ...
611
+
612
+ from_name: __from_name_spec
613
+
534
614
  class __from_id_spec(typing_extensions.Protocol):
535
615
  def __call__(self, /, sandbox_id: str, client: typing.Optional[modal.client.Client] = None) -> Sandbox:
536
616
  """Construct a Sandbox from an id and look up the Sandbox result.
@@ -678,7 +758,7 @@ class Sandbox(modal.object.Object):
678
758
  def __call__(
679
759
  self,
680
760
  /,
681
- *cmds: str,
761
+ *args: str,
682
762
  pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
683
763
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
684
764
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
@@ -693,7 +773,7 @@ class Sandbox(modal.object.Object):
693
773
  def __call__(
694
774
  self,
695
775
  /,
696
- *cmds: str,
776
+ *args: str,
697
777
  pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
698
778
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
699
779
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
@@ -708,7 +788,7 @@ class Sandbox(modal.object.Object):
708
788
  async def aio(
709
789
  self,
710
790
  /,
711
- *cmds: str,
791
+ *args: str,
712
792
  pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
713
793
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
714
794
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
@@ -723,7 +803,7 @@ class Sandbox(modal.object.Object):
723
803
  async def aio(
724
804
  self,
725
805
  /,
726
- *cmds: str,
806
+ *args: str,
727
807
  pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
728
808
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
729
809
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
@@ -745,10 +825,20 @@ class Sandbox(modal.object.Object):
745
825
 
746
826
  class ___experimental_from_snapshot_spec(typing_extensions.Protocol):
747
827
  def __call__(
748
- self, /, snapshot: modal.snapshot.SandboxSnapshot, client: typing.Optional[modal.client.Client] = None
828
+ self,
829
+ /,
830
+ snapshot: modal.snapshot.SandboxSnapshot,
831
+ client: typing.Optional[modal.client.Client] = None,
832
+ *,
833
+ name: typing.Optional[str] = _DEFAULT_SANDBOX_NAME_OVERRIDE,
749
834
  ): ...
750
835
  async def aio(
751
- self, /, snapshot: modal.snapshot.SandboxSnapshot, client: typing.Optional[modal.client.Client] = None
836
+ self,
837
+ /,
838
+ snapshot: modal.snapshot.SandboxSnapshot,
839
+ client: typing.Optional[modal.client.Client] = None,
840
+ *,
841
+ name: typing.Optional[str] = _DEFAULT_SANDBOX_NAME_OVERRIDE,
752
842
  ): ...
753
843
 
754
844
  _experimental_from_snapshot: ___experimental_from_snapshot_spec
modal/secret.py CHANGED
@@ -1,24 +1,40 @@
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
6
9
 
7
10
  from modal_proto import api_pb2
8
11
 
9
- from ._object import _get_environment_name, _Object
12
+ from ._object import _get_environment_name, _Object, live_method
10
13
  from ._resolver import Resolver
11
14
  from ._runtime.execution_context import is_local
12
15
  from ._utils.async_utils import synchronize_api
13
16
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
14
17
  from ._utils.grpc_utils import retry_transient_errors
15
18
  from ._utils.name_utils import check_object_name
19
+ from ._utils.time_utils import timestamp_to_localized_dt
16
20
  from .client import _Client
17
21
  from .exception import InvalidError, NotFoundError
18
22
 
19
23
  ENV_DICT_WRONG_TYPE_ERR = "the env_dict argument to Secret has to be a dict[str, Union[str, None]]"
20
24
 
21
25
 
26
+ @dataclass
27
+ class SecretInfo:
28
+ """Information about the Secret object."""
29
+
30
+ # This dataclass should be limited to information that is unchanging over the lifetime of the Secret,
31
+ # since it is transmitted from the server when the object is hydrated and could be stale when accessed.
32
+
33
+ name: Optional[str]
34
+ created_at: datetime
35
+ created_by: Optional[str]
36
+
37
+
22
38
  class _Secret(_Object, type_prefix="st"):
23
39
  """Secrets provide a dictionary of environment variables for images.
24
40
 
@@ -29,6 +45,22 @@ class _Secret(_Object, type_prefix="st"):
29
45
  See [the secrets guide page](https://modal.com/docs/guide/secrets) for more information.
30
46
  """
31
47
 
48
+ _metadata: Optional[api_pb2.SecretMetadata] = None
49
+
50
+ @property
51
+ def name(self) -> Optional[str]:
52
+ return self._name
53
+
54
+ def _hydrate_metadata(self, metadata: Optional[Message]):
55
+ if metadata:
56
+ assert isinstance(metadata, api_pb2.SecretMetadata)
57
+ self._metadata = metadata
58
+ self._name = metadata.name
59
+
60
+ def _get_metadata(self) -> api_pb2.SecretMetadata:
61
+ assert self._metadata
62
+ return self._metadata
63
+
32
64
  @staticmethod
33
65
  def from_dict(
34
66
  env_dict: dict[
@@ -73,7 +105,7 @@ class _Secret(_Object, type_prefix="st"):
73
105
  if exc.status == Status.FAILED_PRECONDITION:
74
106
  raise InvalidError(exc.message)
75
107
  raise
76
- self._hydrate(resp.secret_id, resolver.client, None)
108
+ self._hydrate(resp.secret_id, resolver.client, resp.metadata)
77
109
 
78
110
  rep = f"Secret.from_dict([{', '.join(env_dict.keys())}])"
79
111
  return _Secret._from_loader(_load, rep, hydrate_lazily=True)
@@ -157,7 +189,7 @@ class _Secret(_Object, type_prefix="st"):
157
189
  )
158
190
  resp = await resolver.client.stub.SecretGetOrCreate(req)
159
191
 
160
- self._hydrate(resp.secret_id, resolver.client, None)
192
+ self._hydrate(resp.secret_id, resolver.client, resp.metadata)
161
193
 
162
194
  return _Secret._from_loader(_load, "Secret.from_dotenv()", hydrate_lazily=True)
163
195
 
@@ -200,9 +232,9 @@ class _Secret(_Object, type_prefix="st"):
200
232
  raise NotFoundError(exc.message)
201
233
  else:
202
234
  raise
203
- self._hydrate(response.secret_id, resolver.client, None)
235
+ self._hydrate(response.secret_id, resolver.client, response.metadata)
204
236
 
205
- return _Secret._from_loader(_load, "Secret()", hydrate_lazily=True)
237
+ return _Secret._from_loader(_load, "Secret()", hydrate_lazily=True, name=name)
206
238
 
207
239
  @staticmethod
208
240
  async def lookup(
@@ -261,5 +293,16 @@ class _Secret(_Object, type_prefix="st"):
261
293
  resp = await retry_transient_errors(client.stub.SecretGetOrCreate, request)
262
294
  return resp.secret_id
263
295
 
296
+ @live_method
297
+ async def info(self) -> SecretInfo:
298
+ """Return information about the Secret object."""
299
+ metadata = self._get_metadata()
300
+ creation_info = metadata.creation_info
301
+ return SecretInfo(
302
+ name=metadata.name or None,
303
+ created_at=timestamp_to_localized_dt(creation_info.created_at),
304
+ created_by=creation_info.created_by or None,
305
+ )
306
+
264
307
 
265
308
  Secret = synchronize_api(_Secret)
modal/secret.pyi CHANGED
@@ -1,9 +1,33 @@
1
+ import datetime
2
+ import google.protobuf.message
1
3
  import modal._object
2
4
  import modal.client
3
5
  import modal.object
6
+ import modal_proto.api_pb2
4
7
  import typing
5
8
  import typing_extensions
6
9
 
10
+ class SecretInfo:
11
+ """Information about the Secret object."""
12
+
13
+ name: typing.Optional[str]
14
+ created_at: datetime.datetime
15
+ created_by: typing.Optional[str]
16
+
17
+ def __init__(
18
+ self, name: typing.Optional[str], created_at: datetime.datetime, created_by: typing.Optional[str]
19
+ ) -> None:
20
+ """Initialize self. See help(type(self)) for accurate signature."""
21
+ ...
22
+
23
+ def __repr__(self):
24
+ """Return repr(self)."""
25
+ ...
26
+
27
+ def __eq__(self, other):
28
+ """Return self==value."""
29
+ ...
30
+
7
31
  class _Secret(modal._object._Object):
8
32
  """Secrets provide a dictionary of environment variables for images.
9
33
 
@@ -13,6 +37,13 @@ class _Secret(modal._object._Object):
13
37
 
14
38
  See [the secrets guide page](https://modal.com/docs/guide/secrets) for more information.
15
39
  """
40
+
41
+ _metadata: typing.Optional[modal_proto.api_pb2.SecretMetadata]
42
+
43
+ @property
44
+ def name(self) -> typing.Optional[str]: ...
45
+ def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
46
+ def _get_metadata(self) -> modal_proto.api_pb2.SecretMetadata: ...
16
47
  @staticmethod
17
48
  def from_dict(env_dict: dict[str, typing.Optional[str]] = {}) -> _Secret:
18
49
  """Create a secret from a str-str dictionary. Values can also be `None`, which is ignored.
@@ -104,6 +135,12 @@ class _Secret(modal._object._Object):
104
135
  """mdmd:hidden"""
105
136
  ...
106
137
 
138
+ async def info(self) -> SecretInfo:
139
+ """Return information about the Secret object."""
140
+ ...
141
+
142
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
143
+
107
144
  class Secret(modal.object.Object):
108
145
  """Secrets provide a dictionary of environment variables for images.
109
146
 
@@ -113,10 +150,17 @@ class Secret(modal.object.Object):
113
150
 
114
151
  See [the secrets guide page](https://modal.com/docs/guide/secrets) for more information.
115
152
  """
153
+
154
+ _metadata: typing.Optional[modal_proto.api_pb2.SecretMetadata]
155
+
116
156
  def __init__(self, *args, **kwargs):
117
157
  """mdmd:hidden"""
118
158
  ...
119
159
 
160
+ @property
161
+ def name(self) -> typing.Optional[str]: ...
162
+ def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
163
+ def _get_metadata(self) -> modal_proto.api_pb2.SecretMetadata: ...
120
164
  @staticmethod
121
165
  def from_dict(env_dict: dict[str, typing.Optional[str]] = {}) -> Secret:
122
166
  """Create a secret from a str-str dictionary. Values can also be `None`, which is ignored.
@@ -240,3 +284,14 @@ class Secret(modal.object.Object):
240
284
  ...
241
285
 
242
286
  create_deployed: __create_deployed_spec
287
+
288
+ class __info_spec(typing_extensions.Protocol[SUPERSELF]):
289
+ def __call__(self, /) -> SecretInfo:
290
+ """Return information about the Secret object."""
291
+ ...
292
+
293
+ async def aio(self, /) -> SecretInfo:
294
+ """Return information about the Secret object."""
295
+ ...
296
+
297
+ info: __info_spec[typing_extensions.Self]
modal/token_flow.py CHANGED
@@ -6,11 +6,11 @@ from collections.abc import AsyncGenerator
6
6
  from typing import Optional
7
7
 
8
8
  import aiohttp.web
9
- from rich.console import Console
10
9
  from synchronicity.async_wrap import asynccontextmanager
11
10
 
12
11
  from modal_proto import api_pb2
13
12
 
13
+ from ._output import make_console
14
14
  from ._utils.async_utils import synchronize_api
15
15
  from ._utils.http_utils import run_temporary_http_server
16
16
  from .client import _Client
@@ -76,7 +76,7 @@ async def _new_token(
76
76
  ):
77
77
  server_url = config.get("server_url", profile=profile)
78
78
 
79
- console = Console()
79
+ console = make_console()
80
80
 
81
81
  result: Optional[api_pb2.TokenFlowWaitResponse] = None
82
82
  async with _Client.anonymous(server_url) as client:
@@ -133,7 +133,7 @@ async def _set_token(
133
133
  ):
134
134
  # TODO add server_url as a parameter for verification?
135
135
  server_url = config.get("server_url", profile=profile)
136
- console = Console()
136
+ console = make_console()
137
137
  if verify:
138
138
  console.print(f"Verifying token against [blue]{server_url}[/blue]")
139
139
  await _Client.verify(server_url, (token_id, token_secret))
modal/volume.py CHANGED
@@ -11,6 +11,7 @@ import time
11
11
  import typing
12
12
  from collections.abc import AsyncGenerator, AsyncIterator, Generator, Sequence
13
13
  from dataclasses import dataclass
14
+ from datetime import datetime
14
15
  from io import BytesIO
15
16
  from pathlib import Path, PurePosixPath
16
17
  from typing import (
@@ -54,6 +55,7 @@ from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
54
55
  from ._utils.grpc_utils import retry_transient_errors
55
56
  from ._utils.http_utils import ClientSessionRegistry
56
57
  from ._utils.name_utils import check_object_name
58
+ from ._utils.time_utils import timestamp_to_localized_dt
57
59
  from .client import _Client
58
60
  from .config import logger
59
61
 
@@ -92,6 +94,18 @@ class FileEntry:
92
94
  )
93
95
 
94
96
 
97
+ @dataclass
98
+ class VolumeInfo:
99
+ """Information about the Volume object."""
100
+
101
+ # This dataclass should be limited to information that is unchanging over the lifetime of the Volume,
102
+ # since it is transmitted from the server when the object is hydrated and could be stale when accessed.
103
+
104
+ name: Optional[str]
105
+ created_at: datetime
106
+ created_by: Optional[str]
107
+
108
+
95
109
  class _Volume(_Object, type_prefix="vo"):
96
110
  """A writeable volume that can be used to share files between one or more Modal functions.
97
111
 
@@ -167,6 +181,19 @@ class _Volume(_Object, type_prefix="vo"):
167
181
  obj = _Volume._from_loader(_load, "Volume()", hydrate_lazily=True, deps=lambda: [self])
168
182
  return obj
169
183
 
184
+ @property
185
+ def name(self) -> Optional[str]:
186
+ return self._name
187
+
188
+ def _hydrate_metadata(self, metadata: Optional[Message]):
189
+ if metadata:
190
+ assert isinstance(metadata, api_pb2.VolumeMetadata)
191
+ self._metadata = metadata
192
+ self._name = metadata.name
193
+
194
+ def _get_metadata(self) -> Optional[Message]:
195
+ return self._metadata
196
+
170
197
  async def _get_lock(self):
171
198
  # To (mostly*) prevent multiple concurrent operations on the same volume, which can cause problems under
172
199
  # some unlikely circumstances.
@@ -181,6 +208,14 @@ class _Volume(_Object, type_prefix="vo"):
181
208
  self._lock = asyncio.Lock()
182
209
  return self._lock
183
210
 
211
+ @property
212
+ def _is_v1(self) -> bool:
213
+ return self._metadata.version in [
214
+ None,
215
+ api_pb2.VolumeFsVersion.VOLUME_FS_VERSION_UNSPECIFIED,
216
+ api_pb2.VolumeFsVersion.VOLUME_FS_VERSION_V1,
217
+ ]
218
+
184
219
  @staticmethod
185
220
  def from_name(
186
221
  name: str,
@@ -220,24 +255,7 @@ class _Volume(_Object, type_prefix="vo"):
220
255
  response = await resolver.client.stub.VolumeGetOrCreate(req)
221
256
  self._hydrate(response.volume_id, resolver.client, response.metadata)
222
257
 
223
- return _Volume._from_loader(_load, "Volume()", hydrate_lazily=True)
224
-
225
- def _hydrate_metadata(self, metadata: Optional[Message]):
226
- if metadata and isinstance(metadata, api_pb2.VolumeMetadata):
227
- self._metadata = metadata
228
- else:
229
- raise TypeError("_hydrate_metadata() requires an `api_pb2.VolumeMetadata` to determine volume version")
230
-
231
- def _get_metadata(self) -> Optional[Message]:
232
- return self._metadata
233
-
234
- @property
235
- def _is_v1(self) -> bool:
236
- return self._metadata.version in [
237
- None,
238
- api_pb2.VolumeFsVersion.VOLUME_FS_VERSION_UNSPECIFIED,
239
- api_pb2.VolumeFsVersion.VOLUME_FS_VERSION_V1,
240
- ]
258
+ return _Volume._from_loader(_load, "Volume()", hydrate_lazily=True, name=name)
241
259
 
242
260
  @classmethod
243
261
  @asynccontextmanager
@@ -338,6 +356,19 @@ class _Volume(_Object, type_prefix="vo"):
338
356
  resp = await retry_transient_errors(client.stub.VolumeGetOrCreate, request)
339
357
  return resp.volume_id
340
358
 
359
+ @live_method
360
+ async def info(self) -> VolumeInfo:
361
+ """Return information about the Volume object."""
362
+ metadata = self._get_metadata()
363
+ if not metadata:
364
+ return VolumeInfo()
365
+ creation_info = metadata.creation_info
366
+ return VolumeInfo(
367
+ name=metadata.name or None,
368
+ created_at=timestamp_to_localized_dt(creation_info.created_at),
369
+ created_by=creation_info.created_by or None,
370
+ )
371
+
341
372
  @live_method
342
373
  async def _do_reload(self, lock=True):
343
374
  async with (await self._get_lock()) if lock else asyncnullcontext():