modal 0.68.42__py3-none-any.whl → 0.70.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.
modal/image.py CHANGED
@@ -31,6 +31,9 @@ from ._serialization import serialize
31
31
  from ._utils.async_utils import synchronize_api
32
32
  from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
33
33
  from ._utils.deprecation import deprecation_error, deprecation_warning
34
+ from ._utils.docker_utils import (
35
+ extract_copy_command_patterns,
36
+ )
34
37
  from ._utils.function_utils import FunctionInfo
35
38
  from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
36
39
  from .client import _Client
@@ -38,7 +41,7 @@ from .cloud_bucket_mount import _CloudBucketMount
38
41
  from .config import config, logger, user_config_path
39
42
  from .environments import _get_environment_cached
40
43
  from .exception import InvalidError, NotFoundError, RemoteError, VersionError
41
- from .file_pattern_matcher import NON_PYTHON_FILES
44
+ from .file_pattern_matcher import NON_PYTHON_FILES, FilePatternMatcher, _ignore_fn
42
45
  from .gpu import GPU_T, parse_gpu_config
43
46
  from .mount import _Mount, python_standalone_mount_name
44
47
  from .network_file_system import _NetworkFileSystem
@@ -236,6 +239,33 @@ def _get_image_builder_version(server_version: ImageBuilderVersion) -> ImageBuil
236
239
  return version
237
240
 
238
241
 
242
+ def _create_context_mount(
243
+ docker_commands: Sequence[str],
244
+ ignore_fn: Callable[[Path], bool],
245
+ context_dir: Path,
246
+ ) -> Optional[_Mount]:
247
+ """
248
+ Creates a context mount from a list of docker commands.
249
+
250
+ 1. Paths are evaluated relative to context_dir.
251
+ 2. First selects inclusions based on COPY commands in the list of commands.
252
+ 3. Then ignore any files as per the ignore predicate.
253
+ """
254
+ copy_patterns = extract_copy_command_patterns(docker_commands)
255
+ if not copy_patterns:
256
+ return None # no mount needed
257
+ include_fn = FilePatternMatcher(*copy_patterns)
258
+
259
+ def ignore_with_include(source: Path) -> bool:
260
+ relative_source = source.relative_to(context_dir)
261
+ if not include_fn(relative_source) or ignore_fn(relative_source):
262
+ return True
263
+
264
+ return False
265
+
266
+ return _Mount._add_local_dir(Path("./"), PurePosixPath("/"), ignore=ignore_with_include)
267
+
268
+
239
269
  class _ImageRegistryConfig:
240
270
  """mdmd:hidden"""
241
271
 
@@ -396,7 +426,7 @@ class _Image(_Object, type_prefix="im"):
396
426
  build_function: Optional["modal.functions._Function"] = None,
397
427
  build_function_input: Optional[api_pb2.FunctionInput] = None,
398
428
  image_registry_config: Optional[_ImageRegistryConfig] = None,
399
- context_mount: Optional[_Mount] = None,
429
+ context_mount_function: Optional[Callable[[], Optional[_Mount]]] = None,
400
430
  force_build: bool = False,
401
431
  # For internal use only.
402
432
  _namespace: "api_pb2.DeploymentNamespace.ValueType" = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
@@ -423,13 +453,15 @@ class _Image(_Object, type_prefix="im"):
423
453
  deps = tuple(base_images.values()) + tuple(secrets)
424
454
  if build_function:
425
455
  deps += (build_function,)
426
- if context_mount:
427
- deps += (context_mount,)
428
456
  if image_registry_config and image_registry_config.secret:
429
457
  deps += (image_registry_config.secret,)
430
458
  return deps
431
459
 
432
460
  async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
461
+ context_mount = context_mount_function() if context_mount_function else None
462
+ if context_mount:
463
+ await resolver.load(context_mount)
464
+
433
465
  if _do_assert_no_mount_layers:
434
466
  for image in base_images.values():
435
467
  # base images can't have
@@ -596,7 +628,7 @@ class _Image(_Object, type_prefix="im"):
596
628
  return _Image._from_args(
597
629
  base_images={"base": self},
598
630
  dockerfile_function=build_dockerfile,
599
- context_mount=mount,
631
+ context_mount_function=lambda: mount,
600
632
  )
601
633
 
602
634
  def add_local_file(self, local_path: Union[str, Path], remote_path: str, *, copy: bool = False) -> "_Image":
@@ -684,7 +716,7 @@ class _Image(_Object, type_prefix="im"):
684
716
  # + make default remote_path="./"
685
717
  raise InvalidError("image.add_local_dir() currently only supports absolute remote_path values")
686
718
 
687
- mount = _Mount._add_local_dir(Path(local_path), Path(remote_path), ignore)
719
+ mount = _Mount._add_local_dir(Path(local_path), PurePosixPath(remote_path), ignore=_ignore_fn(ignore))
688
720
  return self._add_mount_layer_or_copy(mount, copy=copy)
689
721
 
690
722
  def copy_local_file(self, local_path: Union[str, Path], remote_path: Union[str, Path] = "./") -> "_Image":
@@ -695,7 +727,6 @@ class _Image(_Object, type_prefix="im"):
695
727
  """
696
728
  # TODO(elias): add pending deprecation with suggestion to use add_* instead
697
729
  basename = str(Path(local_path).name)
698
- mount = _Mount.from_local_file(local_path, remote_path=f"/{basename}")
699
730
 
700
731
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
701
732
  return DockerfileSpec(commands=["FROM base", f"COPY {basename} {remote_path}"], context_files={})
@@ -703,7 +734,7 @@ class _Image(_Object, type_prefix="im"):
703
734
  return _Image._from_args(
704
735
  base_images={"base": self},
705
736
  dockerfile_function=build_dockerfile,
706
- context_mount=mount,
737
+ context_mount_function=lambda: _Mount.from_local_file(local_path, remote_path=f"/{basename}"),
707
738
  )
708
739
 
709
740
  def add_local_python_source(
@@ -790,15 +821,15 @@ class _Image(_Object, type_prefix="im"):
790
821
  ```
791
822
  """
792
823
 
793
- mount = _Mount._add_local_dir(Path(local_path), Path("/"), ignore)
794
-
795
824
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
796
825
  return DockerfileSpec(commands=["FROM base", f"COPY . {remote_path}"], context_files={})
797
826
 
798
827
  return _Image._from_args(
799
828
  base_images={"base": self},
800
829
  dockerfile_function=build_dockerfile,
801
- context_mount=mount,
830
+ context_mount_function=lambda: _Mount._add_local_dir(
831
+ Path(local_path), PurePosixPath("/"), ignore=_ignore_fn(ignore)
832
+ ),
802
833
  )
803
834
 
804
835
  def pip_install(
@@ -1156,12 +1187,29 @@ class _Image(_Object, type_prefix="im"):
1156
1187
  # modal.Mount with local files to supply as build context for COPY commands
1157
1188
  context_mount: Optional[_Mount] = None,
1158
1189
  force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
1190
+ ignore: Union[Sequence[str], Callable[[Path], bool]] = (),
1159
1191
  ) -> "_Image":
1160
1192
  """Extend an image with arbitrary Dockerfile-like commands."""
1161
1193
  cmds = _flatten_str_args("dockerfile_commands", "dockerfile_commands", dockerfile_commands)
1162
1194
  if not cmds:
1163
1195
  return self
1164
1196
 
1197
+ if context_mount:
1198
+ if ignore:
1199
+ raise InvalidError("Cannot set both `context_mount` and `ignore`")
1200
+
1201
+ def identity_context_mount_fn() -> Optional[_Mount]:
1202
+ return context_mount
1203
+
1204
+ context_mount_function = identity_context_mount_fn
1205
+ else:
1206
+
1207
+ def auto_created_context_mount_fn() -> Optional[_Mount]:
1208
+ # use COPY commands and ignore patterns to construct implicit context mount
1209
+ return _create_context_mount(cmds, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
1210
+
1211
+ context_mount_function = auto_created_context_mount_fn
1212
+
1165
1213
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
1166
1214
  return DockerfileSpec(commands=["FROM base", *cmds], context_files=context_files)
1167
1215
 
@@ -1170,7 +1218,7 @@ class _Image(_Object, type_prefix="im"):
1170
1218
  dockerfile_function=build_dockerfile,
1171
1219
  secrets=secrets,
1172
1220
  gpu_config=parse_gpu_config(gpu),
1173
- context_mount=context_mount,
1221
+ context_mount_function=context_mount_function,
1174
1222
  force_build=self.force_build or force_build,
1175
1223
  )
1176
1224
 
@@ -1400,11 +1448,15 @@ class _Image(_Object, type_prefix="im"):
1400
1448
  modal.Image.from_registry("nvcr.io/nvidia/pytorch:22.12-py3")
1401
1449
  ```
1402
1450
  """
1403
- context_mount = None
1404
- if add_python:
1405
- context_mount = _Mount.from_name(
1406
- python_standalone_mount_name(add_python),
1407
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
1451
+
1452
+ def context_mount_function() -> Optional[_Mount]:
1453
+ return (
1454
+ _Mount.from_name(
1455
+ python_standalone_mount_name(add_python),
1456
+ namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
1457
+ )
1458
+ if add_python
1459
+ else None
1408
1460
  )
1409
1461
 
1410
1462
  if "image_registry_config" not in kwargs and secret is not None:
@@ -1417,7 +1469,7 @@ class _Image(_Object, type_prefix="im"):
1417
1469
 
1418
1470
  return _Image._from_args(
1419
1471
  dockerfile_function=build_dockerfile,
1420
- context_mount=context_mount,
1472
+ context_mount_function=context_mount_function,
1421
1473
  force_build=force_build,
1422
1474
  **kwargs,
1423
1475
  )
@@ -1531,6 +1583,7 @@ class _Image(_Object, type_prefix="im"):
1531
1583
  secrets: Sequence[_Secret] = [],
1532
1584
  gpu: GPU_T = None,
1533
1585
  add_python: Optional[str] = None,
1586
+ ignore: Union[Sequence[str], Callable[[Path], bool]] = (),
1534
1587
  ) -> "_Image":
1535
1588
  """Build a Modal image from a local Dockerfile.
1536
1589
 
@@ -1542,22 +1595,23 @@ class _Image(_Object, type_prefix="im"):
1542
1595
  ```python
1543
1596
  image = modal.Image.from_dockerfile("./Dockerfile", add_python="3.12")
1544
1597
  ```
1598
+ """
1545
1599
 
1546
- If your Dockerfile uses `COPY` instructions which copy data from the local context of the
1547
- build into the image, this local data must be uploaded to Modal via a context mount:
1600
+ if context_mount:
1601
+ if ignore:
1602
+ raise InvalidError("Cannot set both `context_mount` and `ignore`")
1548
1603
 
1549
- ```python
1550
- image = modal.Image.from_dockerfile(
1551
- "./Dockerfile",
1552
- context_mount=modal.Mount.from_local_dir(
1553
- local_path="src",
1554
- remote_path=".", # to current WORKDIR
1555
- ),
1556
- )
1557
- ```
1604
+ def identity_context_mount_fn() -> Optional[_Mount]:
1605
+ return context_mount
1558
1606
 
1559
- The context mount will allow a `COPY src/ src/` instruction to succeed in Modal's remote builder.
1560
- """
1607
+ context_mount_function = identity_context_mount_fn
1608
+ else:
1609
+
1610
+ def auto_created_context_mount_fn() -> Optional[_Mount]:
1611
+ lines = Path(path).read_text("utf8").splitlines()
1612
+ return _create_context_mount(lines, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
1613
+
1614
+ context_mount_function = auto_created_context_mount_fn
1561
1615
 
1562
1616
  # --- Build the base dockerfile
1563
1617
 
@@ -1569,7 +1623,7 @@ class _Image(_Object, type_prefix="im"):
1569
1623
  gpu_config = parse_gpu_config(gpu)
1570
1624
  base_image = _Image._from_args(
1571
1625
  dockerfile_function=build_dockerfile_base,
1572
- context_mount=context_mount,
1626
+ context_mount_function=context_mount_function,
1573
1627
  gpu_config=gpu_config,
1574
1628
  secrets=secrets,
1575
1629
  )
@@ -1578,13 +1632,15 @@ class _Image(_Object, type_prefix="im"):
1578
1632
  # This happening in two steps is probably a vestigial consequence of previous limitations,
1579
1633
  # but it will be difficult to merge them without forcing rebuilds of images.
1580
1634
 
1581
- if add_python:
1582
- context_mount = _Mount.from_name(
1583
- python_standalone_mount_name(add_python),
1584
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
1635
+ def add_python_mount():
1636
+ return (
1637
+ _Mount.from_name(
1638
+ python_standalone_mount_name(add_python),
1639
+ namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
1640
+ )
1641
+ if add_python
1642
+ else None
1585
1643
  )
1586
- else:
1587
- context_mount = None
1588
1644
 
1589
1645
  def build_dockerfile_python(version: ImageBuilderVersion) -> DockerfileSpec:
1590
1646
  commands = _Image._registry_setup_commands("base", version, [], add_python)
@@ -1595,7 +1651,7 @@ class _Image(_Object, type_prefix="im"):
1595
1651
  return _Image._from_args(
1596
1652
  base_images={"base": base_image},
1597
1653
  dockerfile_function=build_dockerfile_python,
1598
- context_mount=context_mount,
1654
+ context_mount_function=add_python_mount,
1599
1655
  force_build=force_build,
1600
1656
  )
1601
1657
 
modal/image.pyi CHANGED
@@ -44,6 +44,11 @@ def _make_pip_install_args(
44
44
  def _get_image_builder_version(
45
45
  server_version: typing.Literal["2023.12", "2024.04", "2024.10"],
46
46
  ) -> typing.Literal["2023.12", "2024.04", "2024.10"]: ...
47
+ def _create_context_mount(
48
+ docker_commands: collections.abc.Sequence[str],
49
+ ignore_fn: typing.Callable[[pathlib.Path], bool],
50
+ context_dir: pathlib.Path,
51
+ ) -> typing.Optional[modal.mount._Mount]: ...
47
52
 
48
53
  class _ImageRegistryConfig:
49
54
  def __init__(self, registry_auth_type: int = 0, secret: typing.Optional[modal.secret._Secret] = None): ...
@@ -87,7 +92,7 @@ class _Image(modal.object._Object):
87
92
  build_function: typing.Optional[modal.functions._Function] = None,
88
93
  build_function_input: typing.Optional[modal_proto.api_pb2.FunctionInput] = None,
89
94
  image_registry_config: typing.Optional[_ImageRegistryConfig] = None,
90
- context_mount: typing.Optional[modal.mount._Mount] = None,
95
+ context_mount_function: typing.Optional[typing.Callable[[], typing.Optional[modal.mount._Mount]]] = None,
91
96
  force_build: bool = False,
92
97
  _namespace: int = 1,
93
98
  _do_assert_no_mount_layers: bool = True,
@@ -195,6 +200,7 @@ class _Image(modal.object._Object):
195
200
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
196
201
  context_mount: typing.Optional[modal.mount._Mount] = None,
197
202
  force_build: bool = False,
203
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
198
204
  ) -> _Image: ...
199
205
  def entrypoint(self, entrypoint_commands: list[str]) -> _Image: ...
200
206
  def shell(self, shell_commands: list[str]) -> _Image: ...
@@ -280,6 +286,7 @@ class _Image(modal.object._Object):
280
286
  secrets: collections.abc.Sequence[modal.secret._Secret] = [],
281
287
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
282
288
  add_python: typing.Optional[str] = None,
289
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
283
290
  ) -> _Image: ...
284
291
  @staticmethod
285
292
  def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> _Image: ...
@@ -346,7 +353,7 @@ class Image(modal.object.Object):
346
353
  build_function: typing.Optional[modal.functions.Function] = None,
347
354
  build_function_input: typing.Optional[modal_proto.api_pb2.FunctionInput] = None,
348
355
  image_registry_config: typing.Optional[_ImageRegistryConfig] = None,
349
- context_mount: typing.Optional[modal.mount.Mount] = None,
356
+ context_mount_function: typing.Optional[typing.Callable[[], typing.Optional[modal.mount.Mount]]] = None,
350
357
  force_build: bool = False,
351
358
  _namespace: int = 1,
352
359
  _do_assert_no_mount_layers: bool = True,
@@ -454,6 +461,7 @@ class Image(modal.object.Object):
454
461
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
455
462
  context_mount: typing.Optional[modal.mount.Mount] = None,
456
463
  force_build: bool = False,
464
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
457
465
  ) -> Image: ...
458
466
  def entrypoint(self, entrypoint_commands: list[str]) -> Image: ...
459
467
  def shell(self, shell_commands: list[str]) -> Image: ...
@@ -539,6 +547,7 @@ class Image(modal.object.Object):
539
547
  secrets: collections.abc.Sequence[modal.secret.Secret] = [],
540
548
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
541
549
  add_python: typing.Optional[str] = None,
550
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
542
551
  ) -> Image: ...
543
552
  @staticmethod
544
553
  def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> Image: ...
modal/mount.py CHANGED
@@ -16,6 +16,7 @@ from typing import Callable, Optional, Sequence, Union
16
16
  from google.protobuf.message import Message
17
17
 
18
18
  import modal.exception
19
+ import modal.file_pattern_matcher
19
20
  from modal_proto import api_pb2
20
21
  from modal_version import __version__
21
22
 
@@ -105,7 +106,7 @@ class _MountFile(_MountEntry):
105
106
  return str(self.local_file)
106
107
 
107
108
  def get_files_to_upload(self):
108
- local_file = self.local_file.expanduser().absolute()
109
+ local_file = self.local_file.resolve()
109
110
  if not local_file.exists():
110
111
  raise FileNotFoundError(local_file)
111
112
 
@@ -131,6 +132,8 @@ class _MountDir(_MountEntry):
131
132
  return str(self.local_dir.expanduser().absolute())
132
133
 
133
134
  def get_files_to_upload(self):
135
+ # we can't use .resolve() eagerly here since that could end up "renaming" symlinked files
136
+ # see test_mount_directory_with_symlinked_file
134
137
  local_dir = self.local_dir.expanduser().absolute()
135
138
 
136
139
  if not local_dir.exists():
@@ -145,10 +148,11 @@ class _MountDir(_MountEntry):
145
148
  gen = (dir_entry.path for dir_entry in os.scandir(local_dir) if dir_entry.is_file())
146
149
 
147
150
  for local_filename in gen:
148
- if not self.ignore(Path(local_filename)):
149
- local_relpath = Path(local_filename).expanduser().absolute().relative_to(local_dir)
151
+ local_path = Path(local_filename)
152
+ if not self.ignore(local_path):
153
+ local_relpath = local_path.expanduser().absolute().relative_to(local_dir)
150
154
  mount_path = self.remote_path / local_relpath.as_posix()
151
- yield local_filename, mount_path
155
+ yield local_path.resolve(), mount_path
152
156
 
153
157
  def watch_entry(self):
154
158
  return self.local_dir.resolve().expanduser(), None
@@ -322,12 +326,9 @@ class _Mount(_Object, type_prefix="mo"):
322
326
  @staticmethod
323
327
  def _add_local_dir(
324
328
  local_path: Path,
325
- remote_path: Path,
326
- ignore: Union[Sequence[str], Callable[[Path], bool]] = [],
329
+ remote_path: PurePosixPath,
330
+ ignore: Callable[[Path], bool] = modal.file_pattern_matcher._NOTHING,
327
331
  ):
328
- if isinstance(ignore, list):
329
- ignore = FilePatternMatcher(*ignore)
330
-
331
332
  return _Mount._new()._extend(
332
333
  _MountDir(
333
334
  local_dir=local_path,
modal/mount.pyi CHANGED
@@ -94,8 +94,8 @@ class _Mount(modal.object._Object):
94
94
  @staticmethod
95
95
  def _add_local_dir(
96
96
  local_path: pathlib.Path,
97
- remote_path: pathlib.Path,
98
- ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool]] = [],
97
+ remote_path: pathlib.PurePosixPath,
98
+ ignore: typing.Callable[[pathlib.Path], bool] = modal.file_pattern_matcher._NOTHING,
99
99
  ): ...
100
100
  def add_local_dir(
101
101
  self,
@@ -176,8 +176,8 @@ class Mount(modal.object.Object):
176
176
  @staticmethod
177
177
  def _add_local_dir(
178
178
  local_path: pathlib.Path,
179
- remote_path: pathlib.Path,
180
- ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool]] = [],
179
+ remote_path: pathlib.PurePosixPath,
180
+ ignore: typing.Callable[[pathlib.Path], bool] = modal.file_pattern_matcher._NOTHING,
181
181
  ): ...
182
182
  def add_local_dir(
183
183
  self,
modal/partial_function.py CHANGED
@@ -253,7 +253,7 @@ def _web_endpoint(
253
253
  custom_domains: Optional[
254
254
  Iterable[str]
255
255
  ] = None, # Create an endpoint using a custom domain fully-qualified domain name (FQDN).
256
- requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests to the endpoint
256
+ requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
257
257
  wait_for_response: bool = True, # DEPRECATED: this must always be True now
258
258
  ) -> Callable[[Callable[P, ReturnType]], _PartialFunction[P, ReturnType, ReturnType]]:
259
259
  """Register a basic web endpoint with this application.
@@ -316,7 +316,7 @@ def _asgi_app(
316
316
  *,
317
317
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
318
318
  custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
319
- requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests to the endpoint
319
+ requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
320
320
  wait_for_response: bool = True, # DEPRECATED: this must always be True now
321
321
  ) -> Callable[[Callable[..., Any]], _PartialFunction]:
322
322
  """Decorator for registering an ASGI app with a Modal function.
@@ -392,7 +392,7 @@ def _wsgi_app(
392
392
  *,
393
393
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
394
394
  custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
395
- requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests to the endpoint
395
+ requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
396
396
  wait_for_response: bool = True, # DEPRECATED: this must always be True now
397
397
  ) -> Callable[[Callable[..., Any]], _PartialFunction]:
398
398
  """Decorator for registering a WSGI app with a Modal function.
@@ -469,7 +469,7 @@ def _web_server(
469
469
  startup_timeout: float = 5.0, # Maximum number of seconds to wait for the web server to start.
470
470
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
471
471
  custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
472
- requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests to the endpoint
472
+ requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
473
473
  ) -> Callable[[Callable[..., Any]], _PartialFunction]:
474
474
  """Decorator that registers an HTTP web server inside the container.
475
475
 
modal/runner.py CHANGED
@@ -30,7 +30,7 @@ from .exception import InteractiveTimeoutError, InvalidError, RemoteError, _CliU
30
30
  from .functions import _Function
31
31
  from .object import _get_environment_name, _Object
32
32
  from .output import _get_output_manager, enable_output
33
- from .running_app import RunningApp
33
+ from .running_app import RunningApp, running_app_from_layout
34
34
  from .sandbox import _Sandbox
35
35
  from .secret import _Secret
36
36
  from .stream_type import StreamType
@@ -54,15 +54,19 @@ async def _heartbeat(client: _Client, app_id: str) -> None:
54
54
 
55
55
  async def _init_local_app_existing(client: _Client, existing_app_id: str, environment_name: str) -> RunningApp:
56
56
  # Get all the objects first
57
- obj_req = api_pb2.AppGetObjectsRequest(app_id=existing_app_id, only_class_function=True)
57
+ obj_req = api_pb2.AppGetLayoutRequest(app_id=existing_app_id)
58
58
  obj_resp, _ = await gather_cancel_on_exc(
59
- retry_transient_errors(client.stub.AppGetObjects, obj_req),
59
+ retry_transient_errors(client.stub.AppGetLayout, obj_req),
60
60
  # Cache the environment associated with the app now as we will use it later
61
61
  _get_environment_cached(environment_name, client),
62
62
  )
63
63
  app_page_url = f"https://modal.com/apps/{existing_app_id}" # TODO (elias): this should come from the backend
64
- object_ids = {item.tag: item.object.object_id for item in obj_resp.items}
65
- return RunningApp(existing_app_id, app_page_url=app_page_url, tag_to_object_id=object_ids, client=client)
64
+ return running_app_from_layout(
65
+ existing_app_id,
66
+ obj_resp.app_layout,
67
+ client,
68
+ app_page_url=app_page_url,
69
+ )
66
70
 
67
71
 
68
72
  async def _init_local_app_new(
modal/running_app.py CHANGED
@@ -4,16 +4,42 @@ from typing import Optional
4
4
 
5
5
  from google.protobuf.message import Message
6
6
 
7
+ from modal._utils.grpc_utils import get_proto_oneof
8
+ from modal_proto import api_pb2
9
+
7
10
  from .client import _Client
8
11
 
9
12
 
10
13
  @dataclass
11
14
  class RunningApp:
12
15
  app_id: str
16
+ client: _Client
13
17
  environment_name: Optional[str] = None
14
18
  app_page_url: Optional[str] = None
15
19
  app_logs_url: Optional[str] = None
16
20
  tag_to_object_id: dict[str, str] = field(default_factory=dict)
17
21
  object_handle_metadata: dict[str, Optional[Message]] = field(default_factory=dict)
18
22
  interactive: bool = False
19
- client: Optional[_Client] = None
23
+
24
+
25
+ def running_app_from_layout(
26
+ app_id: str,
27
+ app_layout: api_pb2.AppLayout,
28
+ client: _Client,
29
+ environment_name: Optional[str] = None,
30
+ app_page_url: Optional[str] = None,
31
+ ) -> RunningApp:
32
+ tag_to_object_id = dict(**app_layout.function_ids, **app_layout.class_ids)
33
+ object_handle_metadata = {}
34
+ for obj in app_layout.objects:
35
+ handle_metadata: Optional[Message] = get_proto_oneof(obj, "handle_metadata_oneof")
36
+ object_handle_metadata[obj.object_id] = handle_metadata
37
+
38
+ return RunningApp(
39
+ app_id,
40
+ client,
41
+ environment_name=environment_name,
42
+ tag_to_object_id=tag_to_object_id,
43
+ object_handle_metadata=object_handle_metadata,
44
+ app_page_url=app_page_url,
45
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.68.42
3
+ Version: 0.70.2
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -21,7 +21,7 @@ Requires-Dist: fastapi
21
21
  Requires-Dist: grpclib (==0.4.7)
22
22
  Requires-Dist: protobuf (!=4.24.0,<6.0,>=3.19)
23
23
  Requires-Dist: rich (>=12.0.0)
24
- Requires-Dist: synchronicity (~=0.9.7)
24
+ Requires-Dist: synchronicity (~=0.9.8)
25
25
  Requires-Dist: toml
26
26
  Requires-Dist: typer (>=0.9)
27
27
  Requires-Dist: types-certifi