modal 1.1.1.dev41__py3-none-any.whl → 1.1.2__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 (68) hide show
  1. modal/__main__.py +1 -2
  2. modal/_container_entrypoint.py +18 -7
  3. modal/_functions.py +135 -13
  4. modal/_object.py +13 -2
  5. modal/_partial_function.py +8 -8
  6. modal/_runtime/asgi.py +3 -2
  7. modal/_runtime/container_io_manager.py +20 -14
  8. modal/_runtime/container_io_manager.pyi +38 -13
  9. modal/_runtime/execution_context.py +18 -2
  10. modal/_runtime/execution_context.pyi +4 -1
  11. modal/_runtime/gpu_memory_snapshot.py +158 -54
  12. modal/_utils/blob_utils.py +83 -24
  13. modal/_utils/function_utils.py +4 -3
  14. modal/_utils/time_utils.py +28 -4
  15. modal/app.py +8 -4
  16. modal/app.pyi +8 -8
  17. modal/cli/dict.py +14 -11
  18. modal/cli/entry_point.py +9 -3
  19. modal/cli/launch.py +102 -4
  20. modal/cli/profile.py +1 -0
  21. modal/cli/programs/launch_instance_ssh.py +94 -0
  22. modal/cli/programs/run_marimo.py +95 -0
  23. modal/cli/queues.py +49 -19
  24. modal/cli/secret.py +45 -18
  25. modal/cli/volume.py +14 -16
  26. modal/client.pyi +2 -10
  27. modal/cls.py +12 -2
  28. modal/cls.pyi +9 -1
  29. modal/config.py +7 -7
  30. modal/dict.py +206 -12
  31. modal/dict.pyi +358 -4
  32. modal/experimental/__init__.py +130 -0
  33. modal/file_io.py +1 -1
  34. modal/file_io.pyi +2 -2
  35. modal/file_pattern_matcher.py +25 -16
  36. modal/functions.pyi +111 -11
  37. modal/image.py +9 -3
  38. modal/image.pyi +7 -7
  39. modal/mount.py +20 -13
  40. modal/mount.pyi +16 -3
  41. modal/network_file_system.py +8 -2
  42. modal/object.pyi +3 -0
  43. modal/parallel_map.py +346 -101
  44. modal/parallel_map.pyi +108 -0
  45. modal/proxy.py +2 -1
  46. modal/queue.py +199 -9
  47. modal/queue.pyi +357 -3
  48. modal/sandbox.py +6 -5
  49. modal/sandbox.pyi +17 -14
  50. modal/secret.py +196 -3
  51. modal/secret.pyi +372 -0
  52. modal/volume.py +239 -23
  53. modal/volume.pyi +405 -10
  54. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/METADATA +2 -2
  55. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/RECORD +68 -66
  56. modal_docs/mdmd/mdmd.py +11 -1
  57. modal_proto/api.proto +37 -10
  58. modal_proto/api_grpc.py +32 -0
  59. modal_proto/api_pb2.py +627 -597
  60. modal_proto/api_pb2.pyi +107 -19
  61. modal_proto/api_pb2_grpc.py +67 -2
  62. modal_proto/api_pb2_grpc.pyi +24 -8
  63. modal_proto/modal_api_grpc.py +2 -0
  64. modal_version/__init__.py +1 -1
  65. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/WHEEL +0 -0
  66. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/entry_points.txt +0 -0
  67. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/licenses/LICENSE +0 -0
  68. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/top_level.txt +0 -0
modal/sandbox.pyi CHANGED
@@ -59,7 +59,7 @@ class _Sandbox(modal._object._Object):
59
59
  image: modal.image._Image,
60
60
  secrets: collections.abc.Sequence[modal.secret._Secret],
61
61
  name: typing.Optional[str] = None,
62
- timeout: typing.Optional[int] = None,
62
+ timeout: int = 300,
63
63
  workdir: typing.Optional[str] = None,
64
64
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
65
65
  cloud: typing.Optional[str] = None,
@@ -95,7 +95,7 @@ class _Sandbox(modal._object._Object):
95
95
  image: typing.Optional[modal.image._Image] = None,
96
96
  secrets: collections.abc.Sequence[modal.secret._Secret] = (),
97
97
  network_file_systems: dict[typing.Union[str, os.PathLike], modal.network_file_system._NetworkFileSystem] = {},
98
- timeout: typing.Optional[int] = None,
98
+ timeout: int = 300,
99
99
  workdir: typing.Optional[str] = None,
100
100
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
101
101
  cloud: typing.Optional[str] = None,
@@ -120,8 +120,9 @@ class _Sandbox(modal._object._Object):
120
120
  client: typing.Optional[modal.client._Client] = None,
121
121
  environment_name: typing.Optional[str] = None,
122
122
  ) -> _Sandbox:
123
- """Create a new Sandbox to run untrusted, arbitrary code. The Sandbox's corresponding container
124
- will be created asynchronously.
123
+ """Create a new Sandbox to run untrusted, arbitrary code.
124
+
125
+ The Sandbox's corresponding container will be created asynchronously.
125
126
 
126
127
  **Usage**
127
128
 
@@ -143,7 +144,7 @@ class _Sandbox(modal._object._Object):
143
144
  secrets: collections.abc.Sequence[modal.secret._Secret] = (),
144
145
  mounts: collections.abc.Sequence[modal.mount._Mount] = (),
145
146
  network_file_systems: dict[typing.Union[str, os.PathLike], modal.network_file_system._NetworkFileSystem] = {},
146
- timeout: typing.Optional[int] = None,
147
+ timeout: int = 300,
147
148
  workdir: typing.Optional[str] = None,
148
149
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
149
150
  cloud: typing.Optional[str] = None,
@@ -368,7 +369,7 @@ class Sandbox(modal.object.Object):
368
369
  image: modal.image.Image,
369
370
  secrets: collections.abc.Sequence[modal.secret.Secret],
370
371
  name: typing.Optional[str] = None,
371
- timeout: typing.Optional[int] = None,
372
+ timeout: int = 300,
372
373
  workdir: typing.Optional[str] = None,
373
374
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
374
375
  cloud: typing.Optional[str] = None,
@@ -407,7 +408,7 @@ class Sandbox(modal.object.Object):
407
408
  network_file_systems: dict[
408
409
  typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
409
410
  ] = {},
410
- timeout: typing.Optional[int] = None,
411
+ timeout: int = 300,
411
412
  workdir: typing.Optional[str] = None,
412
413
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
413
414
  cloud: typing.Optional[str] = None,
@@ -432,8 +433,9 @@ class Sandbox(modal.object.Object):
432
433
  client: typing.Optional[modal.client.Client] = None,
433
434
  environment_name: typing.Optional[str] = None,
434
435
  ) -> Sandbox:
435
- """Create a new Sandbox to run untrusted, arbitrary code. The Sandbox's corresponding container
436
- will be created asynchronously.
436
+ """Create a new Sandbox to run untrusted, arbitrary code.
437
+
438
+ The Sandbox's corresponding container will be created asynchronously.
437
439
 
438
440
  **Usage**
439
441
 
@@ -457,7 +459,7 @@ class Sandbox(modal.object.Object):
457
459
  network_file_systems: dict[
458
460
  typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
459
461
  ] = {},
460
- timeout: typing.Optional[int] = None,
462
+ timeout: int = 300,
461
463
  workdir: typing.Optional[str] = None,
462
464
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
463
465
  cloud: typing.Optional[str] = None,
@@ -482,8 +484,9 @@ class Sandbox(modal.object.Object):
482
484
  client: typing.Optional[modal.client.Client] = None,
483
485
  environment_name: typing.Optional[str] = None,
484
486
  ) -> Sandbox:
485
- """Create a new Sandbox to run untrusted, arbitrary code. The Sandbox's corresponding container
486
- will be created asynchronously.
487
+ """Create a new Sandbox to run untrusted, arbitrary code.
488
+
489
+ The Sandbox's corresponding container will be created asynchronously.
487
490
 
488
491
  **Usage**
489
492
 
@@ -511,7 +514,7 @@ class Sandbox(modal.object.Object):
511
514
  network_file_systems: dict[
512
515
  typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
513
516
  ] = {},
514
- timeout: typing.Optional[int] = None,
517
+ timeout: int = 300,
515
518
  workdir: typing.Optional[str] = None,
516
519
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
517
520
  cloud: typing.Optional[str] = None,
@@ -547,7 +550,7 @@ class Sandbox(modal.object.Object):
547
550
  network_file_systems: dict[
548
551
  typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
549
552
  ] = {},
550
- timeout: typing.Optional[int] = None,
553
+ timeout: int = 300,
551
554
  workdir: typing.Optional[str] = None,
552
555
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
553
556
  cloud: typing.Optional[str] = None,
modal/secret.py CHANGED
@@ -6,6 +6,7 @@ from typing import Optional, Union
6
6
 
7
7
  from google.protobuf.message import Message
8
8
  from grpclib import GRPCError, Status
9
+ from synchronicity import classproperty
9
10
 
10
11
  from modal_proto import api_pb2
11
12
 
@@ -16,9 +17,9 @@ from ._utils.async_utils import synchronize_api
16
17
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
17
18
  from ._utils.grpc_utils import retry_transient_errors
18
19
  from ._utils.name_utils import check_object_name
19
- from ._utils.time_utils import timestamp_to_localized_dt
20
+ from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
20
21
  from .client import _Client
21
- from .exception import InvalidError, NotFoundError
22
+ from .exception import AlreadyExistsError, InvalidError, NotFoundError
22
23
 
23
24
  ENV_DICT_WRONG_TYPE_ERR = "the env_dict argument to Secret has to be a dict[str, Union[str, None]]"
24
25
 
@@ -35,6 +36,176 @@ class SecretInfo:
35
36
  created_by: Optional[str]
36
37
 
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 retry_transient_errors(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 retry_transient_errors(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 retry_transient_errors(obj._client.stub.SecretDelete, req)
204
+
205
+
206
+ SecretManager = synchronize_api(_SecretManager)
207
+
208
+
38
209
  class _Secret(_Object, type_prefix="st"):
39
210
  """Secrets provide a dictionary of environment variables for images.
40
211
 
@@ -47,6 +218,10 @@ class _Secret(_Object, type_prefix="st"):
47
218
 
48
219
  _metadata: Optional[api_pb2.SecretMetadata] = None
49
220
 
221
+ @classproperty
222
+ def objects(cls) -> _SecretManager:
223
+ return _SecretManager
224
+
50
225
  @property
51
226
  def name(self) -> Optional[str]:
52
227
  return self._name
@@ -234,7 +409,8 @@ class _Secret(_Object, type_prefix="st"):
234
409
  raise
235
410
  self._hydrate(response.secret_id, resolver.client, response.metadata)
236
411
 
237
- return _Secret._from_loader(_load, "Secret()", hydrate_lazily=True, name=name)
412
+ rep = _Secret._repr(name, environment_name)
413
+ return _Secret._from_loader(_load, rep, hydrate_lazily=True, name=name)
238
414
 
239
415
  @staticmethod
240
416
  async def lookup(
@@ -273,6 +449,23 @@ class _Secret(_Object, type_prefix="st"):
273
449
  client: Optional[_Client] = None,
274
450
  environment_name: Optional[str] = None,
275
451
  overwrite: bool = False,
452
+ ) -> str:
453
+ """mdmd:hidden"""
454
+ deprecation_warning(
455
+ (2025, 8, 13),
456
+ "The undocumented `modal.Secret.create_deployed` method is deprecated and will be removed "
457
+ "in a future release. It can be replaced with `modal.Secret.objects.create`.",
458
+ )
459
+ return await _Secret._create_deployed(deployment_name, env_dict, namespace, client, environment_name, overwrite)
460
+
461
+ @staticmethod
462
+ async def _create_deployed(
463
+ deployment_name: str,
464
+ env_dict: dict[str, str],
465
+ namespace=None, # mdmd:line-hidden
466
+ client: Optional[_Client] = None,
467
+ environment_name: Optional[str] = None,
468
+ overwrite: bool = False,
276
469
  ) -> str:
277
470
  """mdmd:hidden"""
278
471
  warn_if_passing_namespace(namespace, "modal.Secret.create_deployed")