modal 1.1.5.dev66__py3-none-any.whl → 1.3.1.dev8__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 (143) hide show
  1. modal/__init__.py +4 -4
  2. modal/__main__.py +4 -29
  3. modal/_billing.py +84 -0
  4. modal/_clustered_functions.py +1 -3
  5. modal/_container_entrypoint.py +33 -208
  6. modal/_functions.py +171 -138
  7. modal/_grpc_client.py +191 -0
  8. modal/_ipython.py +16 -6
  9. modal/_load_context.py +106 -0
  10. modal/_object.py +72 -21
  11. modal/_output.py +12 -14
  12. modal/_partial_function.py +31 -4
  13. modal/_resolver.py +44 -57
  14. modal/_runtime/container_io_manager.py +30 -28
  15. modal/_runtime/container_io_manager.pyi +42 -44
  16. modal/_runtime/gpu_memory_snapshot.py +9 -7
  17. modal/_runtime/user_code_event_loop.py +80 -0
  18. modal/_runtime/user_code_imports.py +236 -10
  19. modal/_serialization.py +2 -1
  20. modal/_traceback.py +4 -13
  21. modal/_tunnel.py +16 -11
  22. modal/_tunnel.pyi +25 -3
  23. modal/_utils/async_utils.py +337 -10
  24. modal/_utils/auth_token_manager.py +1 -4
  25. modal/_utils/blob_utils.py +29 -22
  26. modal/_utils/function_utils.py +20 -21
  27. modal/_utils/grpc_testing.py +6 -3
  28. modal/_utils/grpc_utils.py +223 -64
  29. modal/_utils/mount_utils.py +26 -1
  30. modal/_utils/name_utils.py +2 -3
  31. modal/_utils/package_utils.py +0 -1
  32. modal/_utils/rand_pb_testing.py +8 -1
  33. modal/_utils/task_command_router_client.py +524 -0
  34. modal/_vendor/cloudpickle.py +144 -48
  35. modal/app.py +285 -105
  36. modal/app.pyi +216 -53
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +6 -3
  39. modal/builder/PREVIEW.txt +2 -1
  40. modal/builder/base-images.json +4 -2
  41. modal/cli/_download.py +19 -3
  42. modal/cli/cluster.py +4 -2
  43. modal/cli/config.py +3 -1
  44. modal/cli/container.py +5 -4
  45. modal/cli/dict.py +5 -2
  46. modal/cli/entry_point.py +26 -2
  47. modal/cli/environment.py +2 -16
  48. modal/cli/launch.py +1 -76
  49. modal/cli/network_file_system.py +5 -20
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/vscode.py +1 -1
  52. modal/cli/queues.py +5 -4
  53. modal/cli/run.py +24 -204
  54. modal/cli/secret.py +1 -2
  55. modal/cli/shell.py +375 -0
  56. modal/cli/utils.py +1 -13
  57. modal/cli/volume.py +11 -17
  58. modal/client.py +16 -125
  59. modal/client.pyi +94 -144
  60. modal/cloud_bucket_mount.py +3 -1
  61. modal/cloud_bucket_mount.pyi +4 -0
  62. modal/cls.py +101 -64
  63. modal/cls.pyi +9 -8
  64. modal/config.py +21 -1
  65. modal/container_process.py +288 -12
  66. modal/container_process.pyi +99 -38
  67. modal/dict.py +72 -33
  68. modal/dict.pyi +88 -57
  69. modal/environments.py +16 -8
  70. modal/environments.pyi +6 -2
  71. modal/exception.py +154 -16
  72. modal/experimental/__init__.py +24 -53
  73. modal/experimental/flash.py +161 -74
  74. modal/experimental/flash.pyi +97 -49
  75. modal/file_io.py +50 -92
  76. modal/file_io.pyi +117 -89
  77. modal/functions.pyi +70 -87
  78. modal/image.py +82 -47
  79. modal/image.pyi +51 -30
  80. modal/io_streams.py +500 -149
  81. modal/io_streams.pyi +279 -189
  82. modal/mount.py +60 -46
  83. modal/mount.pyi +41 -17
  84. modal/network_file_system.py +19 -11
  85. modal/network_file_system.pyi +72 -39
  86. modal/object.pyi +114 -22
  87. modal/parallel_map.py +42 -44
  88. modal/parallel_map.pyi +9 -17
  89. modal/partial_function.pyi +4 -2
  90. modal/proxy.py +14 -6
  91. modal/proxy.pyi +10 -2
  92. modal/queue.py +45 -38
  93. modal/queue.pyi +88 -52
  94. modal/runner.py +96 -96
  95. modal/runner.pyi +44 -27
  96. modal/sandbox.py +225 -107
  97. modal/sandbox.pyi +226 -60
  98. modal/secret.py +58 -56
  99. modal/secret.pyi +28 -13
  100. modal/serving.py +7 -11
  101. modal/serving.pyi +7 -8
  102. modal/snapshot.py +29 -15
  103. modal/snapshot.pyi +18 -10
  104. modal/token_flow.py +1 -1
  105. modal/token_flow.pyi +4 -6
  106. modal/volume.py +102 -55
  107. modal/volume.pyi +125 -66
  108. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  109. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  110. modal_proto/api.proto +141 -70
  111. modal_proto/api_grpc.py +42 -26
  112. modal_proto/api_pb2.py +1123 -1103
  113. modal_proto/api_pb2.pyi +331 -83
  114. modal_proto/api_pb2_grpc.py +80 -48
  115. modal_proto/api_pb2_grpc.pyi +26 -18
  116. modal_proto/modal_api_grpc.py +175 -174
  117. modal_proto/task_command_router.proto +164 -0
  118. modal_proto/task_command_router_grpc.py +138 -0
  119. modal_proto/task_command_router_pb2.py +180 -0
  120. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
  121. modal_proto/task_command_router_pb2_grpc.py +272 -0
  122. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  123. modal_version/__init__.py +1 -1
  124. modal_version/__main__.py +1 -1
  125. modal/cli/programs/launch_instance_ssh.py +0 -94
  126. modal/cli/programs/run_marimo.py +0 -95
  127. modal-1.1.5.dev66.dist-info/RECORD +0 -191
  128. modal_proto/modal_options_grpc.py +0 -3
  129. modal_proto/options.proto +0 -19
  130. modal_proto/options_grpc.py +0 -3
  131. modal_proto/options_pb2.py +0 -35
  132. modal_proto/options_pb2.pyi +0 -20
  133. modal_proto/options_pb2_grpc.py +0 -4
  134. modal_proto/options_pb2_grpc.pyi +0 -7
  135. modal_proto/sandbox_router.proto +0 -125
  136. modal_proto/sandbox_router_grpc.py +0 -89
  137. modal_proto/sandbox_router_pb2.py +0 -128
  138. modal_proto/sandbox_router_pb2_grpc.py +0 -169
  139. modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
  140. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  141. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  142. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  143. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/dict.py CHANGED
@@ -1,16 +1,18 @@
1
1
  # Copyright Modal Labs 2022
2
+ import builtins
2
3
  from collections.abc import AsyncIterator, Mapping
3
4
  from dataclasses import dataclass
4
5
  from datetime import datetime
5
6
  from typing import Any, Optional, Union
6
7
 
7
8
  from google.protobuf.message import Message
8
- from grpclib import GRPCError, Status
9
9
  from synchronicity import classproperty
10
10
  from synchronicity.async_wrap import asynccontextmanager
11
11
 
12
+ from modal._utils.grpc_utils import Retry
12
13
  from modal_proto import api_pb2
13
14
 
15
+ from ._load_context import LoadContext
14
16
  from ._object import (
15
17
  EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
16
18
  _get_environment_name,
@@ -22,12 +24,18 @@ from ._resolver import Resolver
22
24
  from ._serialization import deserialize, serialize
23
25
  from ._utils.async_utils import TaskContext, synchronize_api
24
26
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
25
- from ._utils.grpc_utils import retry_transient_errors
26
27
  from ._utils.name_utils import check_object_name
27
28
  from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
28
29
  from .client import _Client
29
30
  from .config import logger
30
- from .exception import AlreadyExistsError, InvalidError, NotFoundError, RequestSizeError
31
+ from .exception import (
32
+ AlreadyExistsError,
33
+ DeserializationError,
34
+ Error,
35
+ InvalidError,
36
+ NotFoundError,
37
+ RequestSizeError,
38
+ )
31
39
 
32
40
 
33
41
  class _NoDefaultSentinel:
@@ -42,6 +50,25 @@ def _serialize_dict(data):
42
50
  return [api_pb2.DictEntry(key=serialize(k), value=serialize(v)) for k, v in data.items()]
43
51
 
44
52
 
53
+ def _deserialize_dict_key(dict: "_Dict", data: bytes) -> Any:
54
+ try:
55
+ return deserialize(data, dict._client)
56
+ except DeserializationError as exc:
57
+ dict_identifier = f"Dict '{dict.name}'" if dict.name else f"ephemeral Dict {dict.object_id}"
58
+ raise DeserializationError(f"Failed to deserialize a key from {dict_identifier}: {exc}") from exc
59
+
60
+
61
+ def _deserialize_dict_value(dict: "_Dict", data: bytes, key: Any = _NO_DEFAULT) -> Any:
62
+ try:
63
+ return deserialize(data, dict._client)
64
+ except DeserializationError as exc:
65
+ key_identifier = "" if key is _NO_DEFAULT else f" for key {key!r}"
66
+ dict_identifier = f"Dict '{dict.name}'" if dict.name else f"ephemeral Dict {dict.object_id}"
67
+ raise DeserializationError(
68
+ f"Failed to deserialize value{key_identifier} from {dict_identifier}: {exc}"
69
+ ) from exc
70
+
71
+
45
72
  @dataclass
46
73
  class DictInfo:
47
74
  """Information about a Dict object."""
@@ -105,11 +132,9 @@ class _DictManager:
105
132
  object_creation_type=object_creation_type,
106
133
  )
107
134
  try:
108
- await retry_transient_errors(client.stub.DictGetOrCreate, req)
109
- except GRPCError as exc:
110
- if exc.status == Status.ALREADY_EXISTS and not allow_existing:
111
- raise AlreadyExistsError(exc.message)
112
- else:
135
+ await client.stub.DictGetOrCreate(req)
136
+ except AlreadyExistsError:
137
+ if not allow_existing:
113
138
  raise
114
139
 
115
140
  @staticmethod
@@ -119,7 +144,7 @@ class _DictManager:
119
144
  created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
120
145
  environment_name: str = "", # Uses active environment if not specified
121
146
  client: Optional[_Client] = None, # Optional client with Modal credentials
122
- ) -> list["_Dict"]:
147
+ ) -> builtins.list["_Dict"]:
123
148
  """Return a list of hydrated Dict objects.
124
149
 
125
150
  **Examples:**
@@ -157,7 +182,7 @@ class _DictManager:
157
182
  req = api_pb2.DictListRequest(
158
183
  environment_name=_get_environment_name(environment_name), pagination=pagination
159
184
  )
160
- resp = await retry_transient_errors(client.stub.DictList, req)
185
+ resp = await client.stub.DictList(req)
161
186
  items.extend(resp.dicts)
162
187
  finished = (len(resp.dicts) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
163
188
  return finished
@@ -215,7 +240,7 @@ class _DictManager:
215
240
  raise
216
241
  else:
217
242
  req = api_pb2.DictDeleteRequest(dict_id=obj.object_id)
218
- await retry_transient_errors(obj._client.stub.DictDelete, req)
243
+ await obj._client.stub.DictDelete(req)
219
244
 
220
245
 
221
246
  DictManager = synchronize_api(_DictManager)
@@ -327,7 +352,7 @@ class _Dict(_Object, type_prefix="di"):
327
352
  environment_name=_get_environment_name(environment_name),
328
353
  data=serialized,
329
354
  )
330
- response = await retry_transient_errors(client.stub.DictGetOrCreate, request, total_timeout=10.0)
355
+ response = await client.stub.DictGetOrCreate(request, retry=Retry(total_timeout=10.0))
331
356
  async with TaskContext() as tc:
332
357
  request = api_pb2.DictHeartbeatRequest(dict_id=response.dict_id)
333
358
  tc.infinite_loop(lambda: client.stub.DictHeartbeat(request), sleep=_heartbeat_sleep)
@@ -347,6 +372,7 @@ class _Dict(_Object, type_prefix="di"):
347
372
  namespace=None, # mdmd:line-hidden
348
373
  environment_name: Optional[str] = None,
349
374
  create_if_missing: bool = False,
375
+ client: Optional[_Client] = None,
350
376
  ) -> "_Dict":
351
377
  """Reference a named Dict, creating if necessary.
352
378
 
@@ -368,20 +394,27 @@ class _Dict(_Object, type_prefix="di"):
368
394
  "Passing data to `modal.Dict.from_name` is deprecated and will stop working in a future release.",
369
395
  )
370
396
 
371
- async def _load(self: _Dict, resolver: Resolver, existing_object_id: Optional[str]):
397
+ async def _load(self: _Dict, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
372
398
  serialized = _serialize_dict(data if data is not None else {})
373
399
  req = api_pb2.DictGetOrCreateRequest(
374
400
  deployment_name=name,
375
- environment_name=_get_environment_name(environment_name, resolver),
401
+ environment_name=load_context.environment_name,
376
402
  object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
377
403
  data=serialized,
378
404
  )
379
- response = await resolver.client.stub.DictGetOrCreate(req)
405
+ response = await load_context.client.stub.DictGetOrCreate(req)
380
406
  logger.debug(f"Created dict with id {response.dict_id}")
381
- self._hydrate(response.dict_id, resolver.client, response.metadata)
407
+ self._hydrate(response.dict_id, load_context.client, response.metadata)
382
408
 
383
409
  rep = _Dict._repr(name, environment_name)
384
- return _Dict._from_loader(_load, rep, is_another_app=True, hydrate_lazily=True, name=name)
410
+ return _Dict._from_loader(
411
+ _load,
412
+ rep,
413
+ is_another_app=True,
414
+ hydrate_lazily=True,
415
+ name=name,
416
+ load_context_overrides=LoadContext(environment_name=environment_name, client=client),
417
+ )
385
418
 
386
419
  @staticmethod
387
420
  async def delete(
@@ -418,7 +451,7 @@ class _Dict(_Object, type_prefix="di"):
418
451
  async def clear(self) -> None:
419
452
  """Remove all items from the Dict."""
420
453
  req = api_pb2.DictClearRequest(dict_id=self.object_id)
421
- await retry_transient_errors(self._client.stub.DictClear, req)
454
+ await self._client.stub.DictClear(req)
422
455
 
423
456
  @live_method
424
457
  async def get(self, key: Any, default: Optional[Any] = None) -> Any:
@@ -427,16 +460,16 @@ class _Dict(_Object, type_prefix="di"):
427
460
  Returns `default` if key does not exist.
428
461
  """
429
462
  req = api_pb2.DictGetRequest(dict_id=self.object_id, key=serialize(key))
430
- resp = await retry_transient_errors(self._client.stub.DictGet, req)
463
+ resp = await self._client.stub.DictGet(req)
431
464
  if not resp.found:
432
465
  return default
433
- return deserialize(resp.value, self._client)
466
+ return _deserialize_dict_value(self, resp.value, key)
434
467
 
435
468
  @live_method
436
469
  async def contains(self, key: Any) -> bool:
437
470
  """Return if a key is present."""
438
471
  req = api_pb2.DictContainsRequest(dict_id=self.object_id, key=serialize(key))
439
- resp = await retry_transient_errors(self._client.stub.DictContains, req)
472
+ resp = await self._client.stub.DictContains(req)
440
473
  return resp.found
441
474
 
442
475
  @live_method
@@ -446,7 +479,7 @@ class _Dict(_Object, type_prefix="di"):
446
479
  Note: This is an expensive operation and will return at most 100,000.
447
480
  """
448
481
  req = api_pb2.DictLenRequest(dict_id=self.object_id)
449
- resp = await retry_transient_errors(self._client.stub.DictLen, req)
482
+ resp = await self._client.stub.DictLen(req)
450
483
  return resp.len
451
484
 
452
485
  @live_method
@@ -475,9 +508,9 @@ class _Dict(_Object, type_prefix="di"):
475
508
  serialized = _serialize_dict(contents)
476
509
  req = api_pb2.DictUpdateRequest(dict_id=self.object_id, updates=serialized)
477
510
  try:
478
- await retry_transient_errors(self._client.stub.DictUpdate, req)
479
- except GRPCError as exc:
480
- if "status = '413'" in exc.message:
511
+ await self._client.stub.DictUpdate(req)
512
+ except Error as exc:
513
+ if "status = '413'" in str(exc):
481
514
  raise RequestSizeError("Dict.update request is too large") from exc
482
515
  else:
483
516
  raise exc
@@ -493,10 +526,10 @@ class _Dict(_Object, type_prefix="di"):
493
526
  serialized = _serialize_dict(updates)
494
527
  req = api_pb2.DictUpdateRequest(dict_id=self.object_id, updates=serialized, if_not_exists=skip_if_exists)
495
528
  try:
496
- resp = await retry_transient_errors(self._client.stub.DictUpdate, req)
529
+ resp = await self._client.stub.DictUpdate(req)
497
530
  return resp.created
498
- except GRPCError as exc:
499
- if "status = '413'" in exc.message:
531
+ except Error as exc:
532
+ if "status = '413'" in str(exc):
500
533
  raise RequestSizeError("Dict.put request is too large") from exc
501
534
  else:
502
535
  raise exc
@@ -516,12 +549,12 @@ class _Dict(_Object, type_prefix="di"):
516
549
  If key is not found, return default if provided, otherwise raise KeyError.
517
550
  """
518
551
  req = api_pb2.DictPopRequest(dict_id=self.object_id, key=serialize(key))
519
- resp = await retry_transient_errors(self._client.stub.DictPop, req)
552
+ resp = await self._client.stub.DictPop(req)
520
553
  if not resp.found:
521
554
  if default is not _NO_DEFAULT:
522
555
  return default
523
556
  raise KeyError(f"{key} not in dict {self.object_id}")
524
- return deserialize(resp.value, self._client)
557
+ return _deserialize_dict_value(self, resp.value, key)
525
558
 
526
559
  @live_method
527
560
  async def __delitem__(self, key: Any) -> Any:
@@ -548,7 +581,7 @@ class _Dict(_Object, type_prefix="di"):
548
581
  """
549
582
  req = api_pb2.DictContentsRequest(dict_id=self.object_id, keys=True)
550
583
  async for resp in self._client.stub.DictContents.unary_stream(req):
551
- yield deserialize(resp.key, self._client)
584
+ yield _deserialize_dict_key(self, resp.key)
552
585
 
553
586
  @live_method_gen
554
587
  async def values(self) -> AsyncIterator[Any]:
@@ -559,7 +592,11 @@ class _Dict(_Object, type_prefix="di"):
559
592
  """
560
593
  req = api_pb2.DictContentsRequest(dict_id=self.object_id, values=True)
561
594
  async for resp in self._client.stub.DictContents.unary_stream(req):
562
- yield deserialize(resp.value, self._client)
595
+ try:
596
+ key_deser = _deserialize_dict_key(self, resp.key)
597
+ except DeserializationError:
598
+ key_deser = _NO_DEFAULT
599
+ yield _deserialize_dict_value(self, resp.value, key_deser)
563
600
 
564
601
  @live_method_gen
565
602
  async def items(self) -> AsyncIterator[tuple[Any, Any]]:
@@ -570,7 +607,9 @@ class _Dict(_Object, type_prefix="di"):
570
607
  """
571
608
  req = api_pb2.DictContentsRequest(dict_id=self.object_id, keys=True, values=True)
572
609
  async for resp in self._client.stub.DictContents.unary_stream(req):
573
- yield (deserialize(resp.key, self._client), deserialize(resp.value, self._client))
610
+ key_deser = _deserialize_dict_key(self, resp.key)
611
+ value_deser = _deserialize_dict_value(self, resp.value, key_deser)
612
+ yield (key_deser, value_deser)
574
613
 
575
614
 
576
615
  Dict = synchronize_api(_Dict)
modal/dict.pyi CHANGED
@@ -18,6 +18,8 @@ class _NoDefaultSentinel:
18
18
  _NO_DEFAULT: _NoDefaultSentinel
19
19
 
20
20
  def _serialize_dict(data): ...
21
+ def _deserialize_dict_key(dict: _Dict, data: bytes) -> typing.Any: ...
22
+ def _deserialize_dict_value(dict: _Dict, data: bytes, key: typing.Any = ...) -> typing.Any: ...
21
23
 
22
24
  class DictInfo:
23
25
  """Information about a Dict object."""
@@ -222,7 +224,7 @@ class DictManager:
222
224
  """
223
225
  ...
224
226
 
225
- create: __create_spec
227
+ create: typing.ClassVar[__create_spec]
226
228
 
227
229
  class __list_spec(typing_extensions.Protocol):
228
230
  def __call__(
@@ -295,7 +297,7 @@ class DictManager:
295
297
  """
296
298
  ...
297
299
 
298
- list: __list_spec
300
+ list: typing.ClassVar[__list_spec]
299
301
 
300
302
  class __delete_spec(typing_extensions.Protocol):
301
303
  def __call__(
@@ -358,7 +360,7 @@ class DictManager:
358
360
  """
359
361
  ...
360
362
 
361
- delete: __delete_spec
363
+ delete: typing.ClassVar[__delete_spec]
362
364
 
363
365
  class _Dict(modal._object._Object):
364
366
  """Distributed dictionary for storage in Modal apps.
@@ -448,6 +450,7 @@ class _Dict(modal._object._Object):
448
450
  namespace=None,
449
451
  environment_name: typing.Optional[str] = None,
450
452
  create_if_missing: bool = False,
453
+ client: typing.Optional[modal.client._Client] = None,
451
454
  ) -> _Dict:
452
455
  """Reference a named Dict, creating if necessary.
453
456
 
@@ -576,8 +579,6 @@ class _Dict(modal._object._Object):
576
579
  """
577
580
  ...
578
581
 
579
- SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
580
-
581
582
  class Dict(modal.object.Object):
582
583
  """Distributed dictionary for storage in Modal apps.
583
584
 
@@ -633,30 +634,59 @@ class Dict(modal.object.Object):
633
634
  def name(self) -> typing.Optional[str]: ...
634
635
  def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
635
636
  def _get_metadata(self) -> modal_proto.api_pb2.DictMetadata: ...
636
- @classmethod
637
- def ephemeral(
638
- cls: type[Dict],
639
- data: typing.Optional[dict] = None,
640
- client: typing.Optional[modal.client.Client] = None,
641
- environment_name: typing.Optional[str] = None,
642
- _heartbeat_sleep: float = 300,
643
- ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Dict]:
644
- """Creates a new ephemeral Dict within a context manager:
645
637
 
646
- Usage:
647
- ```python
648
- from modal import Dict
638
+ class __ephemeral_spec(typing_extensions.Protocol):
639
+ def __call__(
640
+ self,
641
+ /,
642
+ data: typing.Optional[dict] = None,
643
+ client: typing.Optional[modal.client.Client] = None,
644
+ environment_name: typing.Optional[str] = None,
645
+ _heartbeat_sleep: float = 300,
646
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Dict]:
647
+ """Creates a new ephemeral Dict within a context manager:
649
648
 
650
- with Dict.ephemeral() as d:
651
- d["foo"] = "bar"
652
- ```
649
+ Usage:
650
+ ```python
651
+ from modal import Dict
653
652
 
654
- ```python notest
655
- async with Dict.ephemeral() as d:
656
- await d.put.aio("foo", "bar")
657
- ```
658
- """
659
- ...
653
+ with Dict.ephemeral() as d:
654
+ d["foo"] = "bar"
655
+ ```
656
+
657
+ ```python notest
658
+ async with Dict.ephemeral() as d:
659
+ await d.put.aio("foo", "bar")
660
+ ```
661
+ """
662
+ ...
663
+
664
+ def aio(
665
+ self,
666
+ /,
667
+ data: typing.Optional[dict] = None,
668
+ client: typing.Optional[modal.client.Client] = None,
669
+ environment_name: typing.Optional[str] = None,
670
+ _heartbeat_sleep: float = 300,
671
+ ) -> typing.AsyncContextManager[Dict]:
672
+ """Creates a new ephemeral Dict within a context manager:
673
+
674
+ Usage:
675
+ ```python
676
+ from modal import Dict
677
+
678
+ with Dict.ephemeral() as d:
679
+ d["foo"] = "bar"
680
+ ```
681
+
682
+ ```python notest
683
+ async with Dict.ephemeral() as d:
684
+ await d.put.aio("foo", "bar")
685
+ ```
686
+ """
687
+ ...
688
+
689
+ ephemeral: typing.ClassVar[__ephemeral_spec]
660
690
 
661
691
  @staticmethod
662
692
  def from_name(
@@ -666,6 +696,7 @@ class Dict(modal.object.Object):
666
696
  namespace=None,
667
697
  environment_name: typing.Optional[str] = None,
668
698
  create_if_missing: bool = False,
699
+ client: typing.Optional[modal.client.Client] = None,
669
700
  ) -> Dict:
670
701
  """Reference a named Dict, creating if necessary.
671
702
 
@@ -717,9 +748,9 @@ class Dict(modal.object.Object):
717
748
  """
718
749
  ...
719
750
 
720
- delete: __delete_spec
751
+ delete: typing.ClassVar[__delete_spec]
721
752
 
722
- class __info_spec(typing_extensions.Protocol[SUPERSELF]):
753
+ class __info_spec(typing_extensions.Protocol):
723
754
  def __call__(self, /) -> DictInfo:
724
755
  """Return information about the Dict object."""
725
756
  ...
@@ -728,9 +759,9 @@ class Dict(modal.object.Object):
728
759
  """Return information about the Dict object."""
729
760
  ...
730
761
 
731
- info: __info_spec[typing_extensions.Self]
762
+ info: __info_spec
732
763
 
733
- class __clear_spec(typing_extensions.Protocol[SUPERSELF]):
764
+ class __clear_spec(typing_extensions.Protocol):
734
765
  def __call__(self, /) -> None:
735
766
  """Remove all items from the Dict."""
736
767
  ...
@@ -739,9 +770,9 @@ class Dict(modal.object.Object):
739
770
  """Remove all items from the Dict."""
740
771
  ...
741
772
 
742
- clear: __clear_spec[typing_extensions.Self]
773
+ clear: __clear_spec
743
774
 
744
- class __get_spec(typing_extensions.Protocol[SUPERSELF]):
775
+ class __get_spec(typing_extensions.Protocol):
745
776
  def __call__(self, /, key: typing.Any, default: typing.Optional[typing.Any] = None) -> typing.Any:
746
777
  """Get the value associated with a key.
747
778
 
@@ -756,9 +787,9 @@ class Dict(modal.object.Object):
756
787
  """
757
788
  ...
758
789
 
759
- get: __get_spec[typing_extensions.Self]
790
+ get: __get_spec
760
791
 
761
- class __contains_spec(typing_extensions.Protocol[SUPERSELF]):
792
+ class __contains_spec(typing_extensions.Protocol):
762
793
  def __call__(self, /, key: typing.Any) -> bool:
763
794
  """Return if a key is present."""
764
795
  ...
@@ -767,9 +798,9 @@ class Dict(modal.object.Object):
767
798
  """Return if a key is present."""
768
799
  ...
769
800
 
770
- contains: __contains_spec[typing_extensions.Self]
801
+ contains: __contains_spec
771
802
 
772
- class __len_spec(typing_extensions.Protocol[SUPERSELF]):
803
+ class __len_spec(typing_extensions.Protocol):
773
804
  def __call__(self, /) -> int:
774
805
  """Return the length of the Dict.
775
806
 
@@ -784,9 +815,9 @@ class Dict(modal.object.Object):
784
815
  """
785
816
  ...
786
817
 
787
- len: __len_spec[typing_extensions.Self]
818
+ len: __len_spec
788
819
 
789
- class ____getitem___spec(typing_extensions.Protocol[SUPERSELF]):
820
+ class ____getitem___spec(typing_extensions.Protocol):
790
821
  def __call__(self, /, key: typing.Any) -> typing.Any:
791
822
  """Get the value associated with a key.
792
823
 
@@ -801,9 +832,9 @@ class Dict(modal.object.Object):
801
832
  """
802
833
  ...
803
834
 
804
- __getitem__: ____getitem___spec[typing_extensions.Self]
835
+ __getitem__: ____getitem___spec
805
836
 
806
- class __update_spec(typing_extensions.Protocol[SUPERSELF]):
837
+ class __update_spec(typing_extensions.Protocol):
807
838
  def __call__(self, other: typing.Optional[collections.abc.Mapping] = None, /, **kwargs) -> None:
808
839
  """Update the Dict with additional items."""
809
840
  ...
@@ -812,9 +843,9 @@ class Dict(modal.object.Object):
812
843
  """Update the Dict with additional items."""
813
844
  ...
814
845
 
815
- update: __update_spec[typing_extensions.Self]
846
+ update: __update_spec
816
847
 
817
- class __put_spec(typing_extensions.Protocol[SUPERSELF]):
848
+ class __put_spec(typing_extensions.Protocol):
818
849
  def __call__(self, /, key: typing.Any, value: typing.Any, *, skip_if_exists: bool = False) -> bool:
819
850
  """Add a specific key-value pair to the Dict.
820
851
 
@@ -831,9 +862,9 @@ class Dict(modal.object.Object):
831
862
  """
832
863
  ...
833
864
 
834
- put: __put_spec[typing_extensions.Self]
865
+ put: __put_spec
835
866
 
836
- class ____setitem___spec(typing_extensions.Protocol[SUPERSELF]):
867
+ class ____setitem___spec(typing_extensions.Protocol):
837
868
  def __call__(self, /, key: typing.Any, value: typing.Any) -> None:
838
869
  """Set a specific key-value pair to the Dict.
839
870
 
@@ -848,9 +879,9 @@ class Dict(modal.object.Object):
848
879
  """
849
880
  ...
850
881
 
851
- __setitem__: ____setitem___spec[typing_extensions.Self]
882
+ __setitem__: ____setitem___spec
852
883
 
853
- class __pop_spec(typing_extensions.Protocol[SUPERSELF]):
884
+ class __pop_spec(typing_extensions.Protocol):
854
885
  def __call__(self, /, key: typing.Any, default: typing.Any = ...) -> typing.Any:
855
886
  """Remove a key from the Dict, returning the value if it exists.
856
887
 
@@ -865,9 +896,9 @@ class Dict(modal.object.Object):
865
896
  """
866
897
  ...
867
898
 
868
- pop: __pop_spec[typing_extensions.Self]
899
+ pop: __pop_spec
869
900
 
870
- class ____delitem___spec(typing_extensions.Protocol[SUPERSELF]):
901
+ class ____delitem___spec(typing_extensions.Protocol):
871
902
  def __call__(self, /, key: typing.Any) -> typing.Any:
872
903
  """Delete a key from the Dict.
873
904
 
@@ -882,9 +913,9 @@ class Dict(modal.object.Object):
882
913
  """
883
914
  ...
884
915
 
885
- __delitem__: ____delitem___spec[typing_extensions.Self]
916
+ __delitem__: ____delitem___spec
886
917
 
887
- class ____contains___spec(typing_extensions.Protocol[SUPERSELF]):
918
+ class ____contains___spec(typing_extensions.Protocol):
888
919
  def __call__(self, /, key: typing.Any) -> bool:
889
920
  """Return if a key is present.
890
921
 
@@ -899,9 +930,9 @@ class Dict(modal.object.Object):
899
930
  """
900
931
  ...
901
932
 
902
- __contains__: ____contains___spec[typing_extensions.Self]
933
+ __contains__: ____contains___spec
903
934
 
904
- class __keys_spec(typing_extensions.Protocol[SUPERSELF]):
935
+ class __keys_spec(typing_extensions.Protocol):
905
936
  def __call__(self, /) -> typing.Iterator[typing.Any]:
906
937
  """Return an iterator over the keys in this Dict.
907
938
 
@@ -918,9 +949,9 @@ class Dict(modal.object.Object):
918
949
  """
919
950
  ...
920
951
 
921
- keys: __keys_spec[typing_extensions.Self]
952
+ keys: __keys_spec
922
953
 
923
- class __values_spec(typing_extensions.Protocol[SUPERSELF]):
954
+ class __values_spec(typing_extensions.Protocol):
924
955
  def __call__(self, /) -> typing.Iterator[typing.Any]:
925
956
  """Return an iterator over the values in this Dict.
926
957
 
@@ -937,9 +968,9 @@ class Dict(modal.object.Object):
937
968
  """
938
969
  ...
939
970
 
940
- values: __values_spec[typing_extensions.Self]
971
+ values: __values_spec
941
972
 
942
- class __items_spec(typing_extensions.Protocol[SUPERSELF]):
973
+ class __items_spec(typing_extensions.Protocol):
943
974
  def __call__(self, /) -> typing.Iterator[tuple[typing.Any, typing.Any]]:
944
975
  """Return an iterator over the (key, value) tuples in this Dict.
945
976
 
@@ -956,4 +987,4 @@ class Dict(modal.object.Object):
956
987
  """
957
988
  ...
958
989
 
959
- items: __items_spec[typing_extensions.Self]
990
+ items: __items_spec
modal/environments.py CHANGED
@@ -8,10 +8,10 @@ from google.protobuf.wrappers_pb2 import StringValue
8
8
 
9
9
  from modal_proto import api_pb2
10
10
 
11
+ from ._load_context import LoadContext
11
12
  from ._object import _Object
12
13
  from ._resolver import Resolver
13
14
  from ._utils.async_utils import synchronize_api, synchronizer
14
- from ._utils.grpc_utils import retry_transient_errors
15
15
  from ._utils.name_utils import check_object_name
16
16
  from .client import _Client
17
17
  from .config import config, logger
@@ -53,6 +53,7 @@ class _Environment(_Object, type_prefix="en"):
53
53
  name: str,
54
54
  *,
55
55
  create_if_missing: bool = False,
56
+ client: Optional[_Client] = None,
56
57
  ):
57
58
  if name:
58
59
  # Allow null names for the case where we want to look up the "default" environment,
@@ -62,7 +63,9 @@ class _Environment(_Object, type_prefix="en"):
62
63
  # environments as part of public API when we make this class more useful.
63
64
  check_object_name(name, "Environment")
64
65
 
65
- async def _load(self: _Environment, resolver: Resolver, existing_object_id: Optional[str]):
66
+ async def _load(
67
+ self: _Environment, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
68
+ ):
66
69
  request = api_pb2.EnvironmentGetOrCreateRequest(
67
70
  deployment_name=name,
68
71
  object_creation_type=(
@@ -71,12 +74,17 @@ class _Environment(_Object, type_prefix="en"):
71
74
  else api_pb2.OBJECT_CREATION_TYPE_UNSPECIFIED
72
75
  ),
73
76
  )
74
- response = await retry_transient_errors(resolver.client.stub.EnvironmentGetOrCreate, request)
77
+ response = await load_context.client.stub.EnvironmentGetOrCreate(request)
75
78
  logger.debug(f"Created environment with id {response.environment_id}")
76
- self._hydrate(response.environment_id, resolver.client, response.metadata)
77
-
78
- # TODO environment name (and id?) in the repr? (We should make reprs consistently more useful)
79
- return _Environment._from_loader(_load, "Environment()", is_another_app=True, hydrate_lazily=True)
79
+ self._hydrate(response.environment_id, load_context.client, response.metadata)
80
+
81
+ return _Environment._from_loader(
82
+ _load,
83
+ f"Environment.from_name({name!r})",
84
+ is_another_app=True,
85
+ hydrate_lazily=True,
86
+ load_context_overrides=LoadContext(client=client),
87
+ )
80
88
 
81
89
 
82
90
  Environment = synchronize_api(_Environment)
@@ -89,7 +97,7 @@ ENVIRONMENT_CACHE: dict[str, _Environment] = {}
89
97
  async def _get_environment_cached(name: str, client: _Client) -> _Environment:
90
98
  if name in ENVIRONMENT_CACHE:
91
99
  return ENVIRONMENT_CACHE[name]
92
- environment = await _Environment.from_name(name).hydrate(client)
100
+ environment = await _Environment.from_name(name, client=client).hydrate()
93
101
  ENVIRONMENT_CACHE[name] = environment
94
102
  return environment
95
103
 
modal/environments.pyi CHANGED
@@ -45,7 +45,9 @@ class _Environment(modal._object._Object):
45
45
 
46
46
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
47
47
  @staticmethod
48
- def from_name(name: str, *, create_if_missing: bool = False): ...
48
+ def from_name(
49
+ name: str, *, create_if_missing: bool = False, client: typing.Optional[modal.client._Client] = None
50
+ ): ...
49
51
 
50
52
  class Environment(modal.object.Object):
51
53
  _settings: EnvironmentSettings
@@ -56,7 +58,9 @@ class Environment(modal.object.Object):
56
58
 
57
59
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
58
60
  @staticmethod
59
- def from_name(name: str, *, create_if_missing: bool = False): ...
61
+ def from_name(
62
+ name: str, *, create_if_missing: bool = False, client: typing.Optional[modal.client.Client] = None
63
+ ): ...
60
64
 
61
65
  async def _get_environment_cached(name: str, client: modal.client._Client) -> _Environment: ...
62
66