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/cli/cluster.py CHANGED
@@ -2,13 +2,13 @@
2
2
  from typing import Optional, Union
3
3
 
4
4
  import typer
5
- from rich.console import Console
6
5
  from rich.text import Text
7
6
 
8
7
  from modal._object import _get_environment_name
8
+ from modal._output import make_console
9
9
  from modal._pty import get_pty_info
10
10
  from modal._utils.async_utils import synchronizer
11
- from modal._utils.time_utils import timestamp_to_local
11
+ from modal._utils.time_utils import timestamp_to_localized_str
12
12
  from modal.cli.utils import ENV_OPTION, display_table, is_tty
13
13
  from modal.client import _Client
14
14
  from modal.config import config
@@ -42,7 +42,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
42
42
  [
43
43
  c.cluster_id,
44
44
  c.app_id,
45
- timestamp_to_local(c.started_at, json) if c.started_at else "Pending",
45
+ timestamp_to_localized_str(c.started_at, json) if c.started_at else "Pending",
46
46
  str(len(c.task_ids)),
47
47
  ]
48
48
  )
@@ -62,7 +62,7 @@ async def shell(
62
62
  if len(res.cluster.task_ids) <= rank:
63
63
  raise typer.Abort(f"No node with rank {rank} in cluster {cluster_id}")
64
64
  task_id = res.cluster.task_ids[rank]
65
- console = Console()
65
+ console = make_console()
66
66
  is_main = "(main)" if rank == 0 else ""
67
67
  console.print(
68
68
  f"Opening shell to node {rank} {is_main} of cluster {cluster_id} (container {task_id})", style="green"
modal/cli/config.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import typer
3
- from rich.console import Console
4
3
 
4
+ from modal._output import make_console
5
5
  from modal.config import _profile, _store_user_config, config
6
6
  from modal.environments import Environment
7
7
 
@@ -24,7 +24,7 @@ def show(redact: bool = typer.Option(True, help="Redact the `token_secret` value
24
24
  if redact and config_dict.get("token_secret"):
25
25
  config_dict["token_secret"] = "***"
26
26
 
27
- console = Console()
27
+ console = make_console()
28
28
  console.print(config_dict)
29
29
 
30
30
 
modal/cli/container.py CHANGED
@@ -8,7 +8,7 @@ from modal._object import _get_environment_name
8
8
  from modal._pty import get_pty_info
9
9
  from modal._utils.async_utils import synchronizer
10
10
  from modal._utils.grpc_utils import retry_transient_errors
11
- from modal._utils.time_utils import timestamp_to_local
11
+ from modal._utils.time_utils import timestamp_to_localized_str
12
12
  from modal.cli.utils import ENV_OPTION, display_table, is_tty, stream_app_logs
13
13
  from modal.client import _Client
14
14
  from modal.config import config
@@ -40,7 +40,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
40
40
  task_stats.task_id,
41
41
  task_stats.app_id,
42
42
  task_stats.app_description,
43
- timestamp_to_local(task_stats.started_at, json) if task_stats.started_at else "Pending",
43
+ timestamp_to_localized_str(task_stats.started_at, json) if task_stats.started_at else "Pending",
44
44
  ]
45
45
  )
46
46
 
modal/cli/dict.py CHANGED
@@ -2,13 +2,13 @@
2
2
  from typing import Optional
3
3
 
4
4
  import typer
5
- from rich.console import Console
6
5
  from typer import Argument, Option, Typer
7
6
 
7
+ from modal._output import make_console
8
8
  from modal._resolver import Resolver
9
9
  from modal._utils.async_utils import synchronizer
10
10
  from modal._utils.grpc_utils import retry_transient_errors
11
- from modal._utils.time_utils import timestamp_to_local
11
+ from modal._utils.time_utils import timestamp_to_localized_str
12
12
  from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
13
13
  from modal.client import _Client
14
14
  from modal.dict import _Dict
@@ -44,7 +44,7 @@ async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
44
44
  request = api_pb2.DictListRequest(environment_name=env)
45
45
  response = await retry_transient_errors(client.stub.DictList, request)
46
46
 
47
- rows = [(d.name, timestamp_to_local(d.created_at, json)) for d in response.dicts]
47
+ rows = [(d.name, timestamp_to_localized_str(d.created_at, json)) for d in response.dicts]
48
48
  display_table(["Name", "Created at"], rows, json)
49
49
 
50
50
 
@@ -85,7 +85,7 @@ async def get(name: str, key: str, *, env: Optional[str] = ENV_OPTION):
85
85
  Note: When using the CLI, keys are always interpreted as having a string type.
86
86
  """
87
87
  d = _Dict.from_name(name, environment_name=env)
88
- console = Console()
88
+ console = make_console()
89
89
  val = await d.get(key)
90
90
  console.print(val)
91
91
 
modal/cli/entry_point.py CHANGED
@@ -3,9 +3,9 @@ import subprocess
3
3
  from typing import Optional
4
4
 
5
5
  import typer
6
- from rich.console import Console
7
6
  from rich.rule import Rule
8
7
 
8
+ from modal._output import make_console
9
9
  from modal._utils.async_utils import synchronizer
10
10
 
11
11
  from . import run
@@ -71,7 +71,7 @@ def check_path():
71
71
  "You may need to give it permissions or use `[white]python -m modal[/white]` as a workaround.[/red]\n"
72
72
  )
73
73
  text += f"See more information here:\n\n[link={url}]{url}[/link]\n"
74
- console = Console()
74
+ console = make_console()
75
75
  console.print(text)
76
76
  console.print(Rule(style="white"))
77
77
 
modal/cli/import_refs.py CHANGED
@@ -19,9 +19,9 @@ from pathlib import Path
19
19
  from typing import Optional, Union, cast
20
20
 
21
21
  import click
22
- from rich.console import Console
23
22
  from rich.markdown import Markdown
24
23
 
24
+ from modal._output import make_console
25
25
  from modal._utils.deprecation import deprecation_warning
26
26
  from modal.app import App, LocalEntrypoint
27
27
  from modal.cls import Cls
@@ -258,7 +258,7 @@ def import_app_from_ref(import_ref: ImportRef, base_cmd: str = "") -> App:
258
258
  app = getattr(module, object_path)
259
259
 
260
260
  if app is None:
261
- error_console = Console(stderr=True)
261
+ error_console = make_console(stderr=True)
262
262
  error_console.print(f"[bold red]Could not find Modal app '{object_path}' in {import_path}.[/bold red]")
263
263
 
264
264
  if not object_path:
@@ -282,7 +282,7 @@ def import_app_from_ref(import_ref: ImportRef, base_cmd: str = "") -> App:
282
282
  def _show_function_ref_help(app_ref: ImportRef, base_cmd: str) -> None:
283
283
  object_path = app_ref.object_path
284
284
  import_path = app_ref.file_or_module
285
- error_console = Console(stderr=True)
285
+ error_console = make_console(stderr=True)
286
286
  if object_path:
287
287
  error_console.print(
288
288
  f"[bold red]Could not find Modal function or local entrypoint"
@@ -7,17 +7,16 @@ from typing import Optional
7
7
  import typer
8
8
  from click import UsageError
9
9
  from grpclib import GRPCError, Status
10
- from rich.console import Console
11
10
  from rich.syntax import Syntax
12
11
  from rich.table import Table
13
12
  from typer import Argument, Typer
14
13
 
15
14
  import modal
16
15
  from modal._location import display_location
17
- from modal._output import OutputManager, ProgressHandler
16
+ from modal._output import OutputManager, ProgressHandler, make_console
18
17
  from modal._utils.async_utils import synchronizer
19
18
  from modal._utils.grpc_utils import retry_transient_errors
20
- from modal._utils.time_utils import timestamp_to_local
19
+ from modal._utils.time_utils import timestamp_to_localized_str
21
20
  from modal.cli._download import _volume_download
22
21
  from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
23
22
  from modal.client import _Client
@@ -45,7 +44,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
45
44
  [
46
45
  item.label,
47
46
  display_location(item.cloud_provider),
48
- timestamp_to_local(item.created_at, json),
47
+ timestamp_to_localized_str(item.created_at, json),
49
48
  ]
50
49
  )
51
50
  display_table(column_names, rows, json, title=f"Shared Volumes{env_part}")
@@ -66,7 +65,7 @@ def create(
66
65
  ):
67
66
  ensure_env(env)
68
67
  modal.NetworkFileSystem.create_deployed(name, environment_name=env)
69
- console = Console()
68
+ console = make_console()
70
69
  console.print(f"Created volume '{name}'. \n\nCode example:\n")
71
70
  usage = Syntax(gen_usage_code(name), "python")
72
71
  console.print(usage)
@@ -93,7 +92,7 @@ async def ls(
93
92
  raise
94
93
 
95
94
  if sys.stdout.isatty():
96
- console = Console()
95
+ console = make_console()
97
96
  console.print(f"Directory listing of '{path}' in '{volume_name}'")
98
97
  table = Table()
99
98
 
@@ -131,7 +130,7 @@ async def put(
131
130
  volume = _NetworkFileSystem.from_name(volume_name)
132
131
  if remote_path.endswith("/"):
133
132
  remote_path = remote_path + os.path.basename(local_path)
134
- console = Console()
133
+ console = make_console()
135
134
 
136
135
  if Path(local_path).is_dir():
137
136
  progress_handler = ProgressHandler(type="upload", console=console)
@@ -184,7 +183,7 @@ async def get(
184
183
  ensure_env(env)
185
184
  destination = Path(local_destination)
186
185
  volume = _NetworkFileSystem.from_name(volume_name)
187
- console = Console()
186
+ console = make_console()
188
187
  progress_handler = ProgressHandler(type="download", console=console)
189
188
  with progress_handler.live:
190
189
  await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
@@ -203,7 +202,7 @@ async def rm(
203
202
  ):
204
203
  ensure_env(env)
205
204
  volume = _NetworkFileSystem.from_name(volume_name)
206
- console = Console()
205
+ console = make_console()
207
206
  try:
208
207
  await volume.remove_file(remote_path, recursive=recursive)
209
208
  console.print(OutputManager.step_completed(f"{remote_path} was deleted successfully!"))
modal/cli/profile.py CHANGED
@@ -5,10 +5,10 @@ import os
5
5
  from typing import Optional
6
6
 
7
7
  import typer
8
- from rich.console import Console
9
8
  from rich.json import JSON
10
9
  from rich.table import Table
11
10
 
11
+ from modal._output import make_console
12
12
  from modal._utils.async_utils import synchronizer
13
13
  from modal.config import Config, _lookup_workspace, _profile, config_profiles, config_set_active_profile
14
14
  from modal.exception import AuthError
@@ -69,7 +69,7 @@ async def list_(json: Optional[bool] = False):
69
69
  except AuthError:
70
70
  env_based_workspace = "Unknown (authentication failure)"
71
71
 
72
- console = Console()
72
+ console = make_console()
73
73
  highlight = "bold green" if env_based_workspace is None else "yellow"
74
74
  if json:
75
75
  json_data = []
modal/cli/queues.py CHANGED
@@ -2,13 +2,13 @@
2
2
  from typing import Optional
3
3
 
4
4
  import typer
5
- from rich.console import Console
6
5
  from typer import Argument, Option, Typer
7
6
 
7
+ from modal._output import make_console
8
8
  from modal._resolver import Resolver
9
9
  from modal._utils.async_utils import synchronizer
10
10
  from modal._utils.grpc_utils import retry_transient_errors
11
- from modal._utils.time_utils import timestamp_to_local
11
+ from modal._utils.time_utils import timestamp_to_localized_str
12
12
  from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
13
13
  from modal.client import _Client
14
14
  from modal.environments import ensure_env
@@ -71,7 +71,7 @@ async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
71
71
  rows = [
72
72
  (
73
73
  q.name,
74
- timestamp_to_local(q.created_at, json),
74
+ timestamp_to_localized_str(q.created_at, json),
75
75
  str(q.num_partitions),
76
76
  str(q.total_size) if q.total_size <= max_total_size else f">{max_total_size}",
77
77
  )
@@ -108,7 +108,7 @@ async def peek(
108
108
  ):
109
109
  """Print the next N items in the queue or queue partition (without removal)."""
110
110
  q = _Queue.from_name(name, environment_name=env)
111
- console = Console()
111
+ console = make_console()
112
112
  i = 0
113
113
  async for item in q.iterate(partition=partition):
114
114
  console.print(item)
@@ -128,5 +128,5 @@ async def len(
128
128
  ):
129
129
  """Print the length of a queue partition or the total length of all partitions."""
130
130
  q = _Queue.from_name(name, environment_name=env)
131
- console = Console()
131
+ console = make_console()
132
132
  console.print(await q.len(partition=partition, total=total))
modal/cli/secret.py CHANGED
@@ -9,13 +9,13 @@ from typing import Optional
9
9
 
10
10
  import click
11
11
  import typer
12
- from rich.console import Console
13
12
  from rich.syntax import Syntax
14
13
  from typer import Argument
15
14
 
15
+ from modal._output import make_console
16
16
  from modal._utils.async_utils import synchronizer
17
17
  from modal._utils.grpc_utils import retry_transient_errors
18
- from modal._utils.time_utils import timestamp_to_local
18
+ from modal._utils.time_utils import timestamp_to_localized_str
19
19
  from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
20
20
  from modal.client import _Client
21
21
  from modal.environments import ensure_env
@@ -38,8 +38,8 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
38
38
  rows.append(
39
39
  [
40
40
  item.label,
41
- timestamp_to_local(item.created_at, json),
42
- timestamp_to_local(item.last_used_at, json) if item.last_used_at else "-",
41
+ timestamp_to_localized_str(item.created_at, json),
42
+ timestamp_to_localized_str(item.last_used_at, json) if item.last_used_at else "-",
43
43
  ]
44
44
  )
45
45
 
@@ -117,7 +117,7 @@ modal secret create my-credentials username=john password="$PASSWORD"
117
117
  await _Secret.create_deployed(secret_name, env_dict, overwrite=force)
118
118
 
119
119
  # Print code sample
120
- console = Console()
120
+ console = make_console()
121
121
  env_var_code = "\n ".join(f'os.getenv("{name}")' for name in env_dict.keys()) if env_dict else "..."
122
122
  example_code = f"""
123
123
  @app.function(secrets=[modal.Secret.from_name("{secret_name}")])
modal/cli/utils.py CHANGED
@@ -7,13 +7,12 @@ from typing import Optional, Union
7
7
  import typer
8
8
  from click import UsageError
9
9
  from grpclib import GRPCError, Status
10
- from rich.console import Console
11
10
  from rich.table import Column, Table
12
11
  from rich.text import Text
13
12
 
14
13
  from modal_proto import api_pb2
15
14
 
16
- from .._output import OutputManager, get_app_logs_loop
15
+ from .._output import OutputManager, get_app_logs_loop, make_console
17
16
  from .._utils.async_utils import synchronizer
18
17
  from ..client import _Client
19
18
  from ..environments import ensure_env
@@ -66,7 +65,7 @@ def _plain(text: Union[Text, str]) -> str:
66
65
 
67
66
 
68
67
  def is_tty() -> bool:
69
- return Console().is_terminal
68
+ return make_console().is_terminal
70
69
 
71
70
 
72
71
  def display_table(
@@ -78,7 +77,7 @@ def display_table(
78
77
  def col_to_str(col: Union[Column, str]) -> str:
79
78
  return str(col.header) if isinstance(col, Column) else col
80
79
 
81
- console = Console()
80
+ console = make_console()
82
81
  if json:
83
82
  json_data = [{col_to_str(col): _plain(row[i]) for i, col in enumerate(columns)} for row in rows]
84
83
  console.print_json(dumps(json_data))
modal/cli/volume.py CHANGED
@@ -7,15 +7,14 @@ from typing import Optional
7
7
  import typer
8
8
  from click import UsageError
9
9
  from grpclib import GRPCError, Status
10
- from rich.console import Console
11
10
  from rich.syntax import Syntax
12
11
  from typer import Argument, Option, Typer
13
12
 
14
13
  import modal
15
- from modal._output import OutputManager, ProgressHandler
14
+ from modal._output import OutputManager, ProgressHandler, make_console
16
15
  from modal._utils.async_utils import synchronizer
17
16
  from modal._utils.grpc_utils import retry_transient_errors
18
- from modal._utils.time_utils import timestamp_to_local
17
+ from modal._utils.time_utils import timestamp_to_localized_str
19
18
  from modal.cli._download import _volume_download
20
19
  from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
21
20
  from modal.client import _Client
@@ -64,7 +63,7 @@ def some_func():
64
63
  os.listdir("/my_vol")
65
64
  """
66
65
 
67
- console = Console()
66
+ console = make_console()
68
67
  console.print(f"Created Volume '{name}' in environment '{env_name}'. \n\nCode example:\n")
69
68
  usage = Syntax(usage_code, "python")
70
69
  console.print(usage)
@@ -96,7 +95,7 @@ async def get(
96
95
  ensure_env(env)
97
96
  destination = Path(local_destination)
98
97
  volume = _Volume.from_name(volume_name, environment_name=env)
99
- console = Console()
98
+ console = make_console()
100
99
  progress_handler = ProgressHandler(type="download", console=console)
101
100
  with progress_handler.live:
102
101
  await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
@@ -117,7 +116,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
117
116
  column_names = ["Name", "Created at"]
118
117
  rows = []
119
118
  for item in response.items:
120
- rows.append([item.label, timestamp_to_local(item.created_at, json)])
119
+ rows.append([item.label, timestamp_to_localized_str(item.created_at, json)])
121
120
  display_table(column_names, rows, json, title=f"Volumes{env_part}")
122
121
 
123
122
 
@@ -164,7 +163,7 @@ async def ls(
164
163
  (
165
164
  entry.path.encode("unicode_escape").decode("utf-8"),
166
165
  filetype,
167
- timestamp_to_local(entry.mtime, False),
166
+ timestamp_to_localized_str(entry.mtime, False),
168
167
  humanize_filesize(entry.size),
169
168
  )
170
169
  )
@@ -197,7 +196,7 @@ async def put(
197
196
 
198
197
  if remote_path.endswith("/"):
199
198
  remote_path = remote_path + os.path.basename(local_path)
200
- console = Console()
199
+ console = make_console()
201
200
  progress_handler = ProgressHandler(type="upload", console=console)
202
201
 
203
202
  if Path(local_path).is_dir():
@@ -245,7 +244,7 @@ async def rm(
245
244
  ):
246
245
  ensure_env(env)
247
246
  volume = _Volume.from_name(volume_name, environment_name=env)
248
- console = Console()
247
+ console = make_console()
249
248
  try:
250
249
  await volume.remove_file(remote_path, recursive=recursive)
251
250
  console.print(OutputManager.step_completed(f"{remote_path} was deleted successfully!"))
modal/client.py CHANGED
@@ -268,6 +268,14 @@ class _Client:
268
268
  # Just used from tests.
269
269
  cls._client_from_env = client
270
270
 
271
+ async def get_input_plane_metadata(self, input_plane_region: str) -> list[tuple[str, str]]:
272
+ assert self._auth_token_manager, "Client must have an instance of auth token manager."
273
+ token = await self._auth_token_manager.get_token()
274
+ return [
275
+ ("x-modal-input-plane-region", input_plane_region),
276
+ ("x-modal-auth-token", token),
277
+ ]
278
+
271
279
  async def _call_safely(self, coro, readable_method: str):
272
280
  """Runs coroutine wrapped in a task that's part of the client's task context
273
281
 
@@ -456,4 +464,3 @@ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
456
464
  self.wrapped_method.channel = await self.client._get_channel(self.server_url)
457
465
  async for response in self.client._call_stream(self.wrapped_method, request, metadata=metadata):
458
466
  yield response
459
-
modal/client.pyi CHANGED
@@ -29,11 +29,7 @@ class _Client:
29
29
  _snapshotted: bool
30
30
 
31
31
  def __init__(
32
- self,
33
- server_url: str,
34
- client_type: int,
35
- credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.0.6.dev61",
32
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.1.1"
37
33
  ):
38
34
  """mdmd:hidden
39
35
  The Modal client object is not intended to be instantiated directly by users.
@@ -112,6 +108,7 @@ class _Client:
112
108
  """mdmd:hidden"""
113
109
  ...
114
110
 
111
+ async def get_input_plane_metadata(self, input_plane_region: str) -> list[tuple[str, str]]: ...
115
112
  async def _call_safely(self, coro, readable_method: str):
116
113
  """Runs coroutine wrapped in a task that's part of the client's task context
117
114
 
@@ -159,11 +156,7 @@ class Client:
159
156
  _snapshotted: bool
160
157
 
161
158
  def __init__(
162
- self,
163
- server_url: str,
164
- client_type: int,
165
- credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.0.6.dev61",
159
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.1.1"
167
160
  ):
168
161
  """mdmd:hidden
169
162
  The Modal client object is not intended to be instantiated directly by users.
@@ -275,6 +268,12 @@ class Client:
275
268
  """mdmd:hidden"""
276
269
  ...
277
270
 
271
+ class __get_input_plane_metadata_spec(typing_extensions.Protocol[SUPERSELF]):
272
+ def __call__(self, /, input_plane_region: str) -> list[tuple[str, str]]: ...
273
+ async def aio(self, /, input_plane_region: str) -> list[tuple[str, str]]: ...
274
+
275
+ get_input_plane_metadata: __get_input_plane_metadata_spec[typing_extensions.Self]
276
+
278
277
  class ___call_safely_spec(typing_extensions.Protocol[SUPERSELF]):
279
278
  def __call__(self, /, coro, readable_method: str):
280
279
  """Runs coroutine wrapped in a task that's part of the client's task context
@@ -144,9 +144,9 @@ class _ContainerProcess(Generic[T]):
144
144
  print("interactive exec is not currently supported on Windows.")
145
145
  return
146
146
 
147
- from rich.console import Console
147
+ from ._output import make_console
148
148
 
149
- console = Console()
149
+ console = make_console()
150
150
 
151
151
  connecting_status = console.status("Connecting...")
152
152
  connecting_status.start()
modal/dict.py CHANGED
@@ -1,7 +1,10 @@
1
1
  # Copyright Modal Labs 2022
2
2
  from collections.abc import AsyncIterator, Mapping
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
3
5
  from typing import Any, Optional
4
6
 
7
+ from google.protobuf.message import Message
5
8
  from grpclib import GRPCError
6
9
  from synchronicity.async_wrap import asynccontextmanager
7
10
 
@@ -14,6 +17,7 @@ from ._utils.async_utils import TaskContext, synchronize_api
14
17
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
15
18
  from ._utils.grpc_utils import retry_transient_errors
16
19
  from ._utils.name_utils import check_object_name
20
+ from ._utils.time_utils import timestamp_to_localized_dt
17
21
  from .client import _Client
18
22
  from .config import logger
19
23
  from .exception import RequestSizeError
@@ -23,6 +27,18 @@ def _serialize_dict(data):
23
27
  return [api_pb2.DictEntry(key=serialize(k), value=serialize(v)) for k, v in data.items()]
24
28
 
25
29
 
30
+ @dataclass
31
+ class DictInfo:
32
+ """Information about the Dict object."""
33
+
34
+ # This dataclass should be limited to information that is unchanging over the lifetime of the Dict,
35
+ # since it is transmitted from the server when the object is hydrated and could be stale when accessed.
36
+
37
+ name: Optional[str]
38
+ created_at: datetime
39
+ created_by: Optional[str]
40
+
41
+
26
42
  class _Dict(_Object, type_prefix="di"):
27
43
  """Distributed dictionary for storage in Modal apps.
28
44
 
@@ -65,12 +81,29 @@ class _Dict(_Object, type_prefix="di"):
65
81
  For more examples, see the [guide](https://modal.com/docs/guide/dicts-and-queues#modal-dicts).
66
82
  """
67
83
 
84
+ _name: Optional[str] = None
85
+ _metadata: Optional[api_pb2.DictMetadata] = None
86
+
68
87
  def __init__(self, data={}):
69
88
  """mdmd:hidden"""
70
89
  raise RuntimeError(
71
90
  "`Dict(...)` constructor is not allowed. Please use `Dict.from_name` or `Dict.ephemeral` instead"
72
91
  )
73
92
 
93
+ @property
94
+ def name(self) -> Optional[str]:
95
+ return self._name
96
+
97
+ def _hydrate_metadata(self, metadata: Optional[Message]):
98
+ if metadata:
99
+ assert isinstance(metadata, api_pb2.DictMetadata)
100
+ self._metadata = metadata
101
+ self._name = metadata.name
102
+
103
+ def _get_metadata(self) -> api_pb2.DictMetadata:
104
+ assert self._metadata
105
+ return self._metadata
106
+
74
107
  @classmethod
75
108
  @asynccontextmanager
76
109
  async def ephemeral(
@@ -112,7 +145,7 @@ class _Dict(_Object, type_prefix="di"):
112
145
  async with TaskContext() as tc:
113
146
  request = api_pb2.DictHeartbeatRequest(dict_id=response.dict_id)
114
147
  tc.infinite_loop(lambda: client.stub.DictHeartbeat(request), sleep=_heartbeat_sleep)
115
- yield cls._new_hydrated(response.dict_id, client, None, is_another_app=True)
148
+ yield cls._new_hydrated(response.dict_id, client, response.metadata, is_another_app=True)
116
149
 
117
150
  @staticmethod
118
151
  def from_name(
@@ -153,9 +186,9 @@ class _Dict(_Object, type_prefix="di"):
153
186
  )
154
187
  response = await resolver.client.stub.DictGetOrCreate(req)
155
188
  logger.debug(f"Created dict with id {response.dict_id}")
156
- self._hydrate(response.dict_id, resolver.client, None)
189
+ self._hydrate(response.dict_id, resolver.client, response.metadata)
157
190
 
158
- return _Dict._from_loader(_load, "Dict()", is_another_app=True, hydrate_lazily=True)
191
+ return _Dict._from_loader(_load, "Dict()", is_another_app=True, hydrate_lazily=True, name=name)
159
192
 
160
193
  @staticmethod
161
194
  async def lookup(
@@ -209,6 +242,17 @@ class _Dict(_Object, type_prefix="di"):
209
242
  req = api_pb2.DictDeleteRequest(dict_id=obj.object_id)
210
243
  await retry_transient_errors(obj._client.stub.DictDelete, req)
211
244
 
245
+ @live_method
246
+ async def info(self) -> DictInfo:
247
+ """Return information about the Dict object."""
248
+ metadata = self._get_metadata()
249
+ creation_info = metadata.creation_info
250
+ return DictInfo(
251
+ name=metadata.name or None,
252
+ created_at=timestamp_to_localized_dt(creation_info.created_at),
253
+ created_by=creation_info.created_by or None,
254
+ )
255
+
212
256
  @live_method
213
257
  async def clear(self) -> None:
214
258
  """Remove all items from the Dict."""