modal 1.0.6.dev5__py3-none-any.whl → 1.0.6.dev7__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.

@@ -275,7 +275,9 @@ async def blob_upload(payload: bytes, stub: ModalClientModal) -> str:
275
275
  blob_id = await _blob_upload(upload_hashes, payload, stub)
276
276
  dur_s = max(time.time() - t0, 0.001) # avoid division by zero
277
277
  throughput_mib_s = (size_mib) / dur_s
278
- logger.debug(f"Uploaded large blob of size {size_mib:.2f} MiB ({throughput_mib_s:.2f} MiB/s). {blob_id}")
278
+ logger.debug(
279
+ f"Uploaded large blob of size {size_mib:.2f} MiB ({throughput_mib_s:.2f} MiB/s, total {dur_s:.2f}s). {blob_id}"
280
+ )
279
281
  return blob_id
280
282
 
281
283
 
@@ -314,7 +316,9 @@ async def blob_download(blob_id: str, stub: ModalClientModal) -> bytes:
314
316
  size_mib = len(data) / 1024 / 1024
315
317
  dur_s = max(time.time() - t0, 0.001) # avoid division by zero
316
318
  throughput_mib_s = size_mib / dur_s
317
- logger.debug(f"Downloaded large blob {blob_id} of size {size_mib:.2f} MiB ({throughput_mib_s:.2f} MiB/s)")
319
+ logger.debug(
320
+ f"Downloaded large blob {blob_id} of size {size_mib:.2f} MiB ({throughput_mib_s:.2f} MiB/s, total {dur_s:.2f}s)"
321
+ )
318
322
  return data
319
323
 
320
324
 
modal/client.pyi CHANGED
@@ -31,7 +31,7 @@ class _Client:
31
31
  server_url: str,
32
32
  client_type: int,
33
33
  credentials: typing.Optional[tuple[str, str]],
34
- version: str = "1.0.6.dev5",
34
+ version: str = "1.0.6.dev7",
35
35
  ):
36
36
  """mdmd:hidden
37
37
  The Modal client object is not intended to be instantiated directly by users.
@@ -160,7 +160,7 @@ class Client:
160
160
  server_url: str,
161
161
  client_type: int,
162
162
  credentials: typing.Optional[tuple[str, str]],
163
- version: str = "1.0.6.dev5",
163
+ version: str = "1.0.6.dev7",
164
164
  ):
165
165
  """mdmd:hidden
166
166
  The Modal client object is not intended to be instantiated directly by users.
@@ -46,6 +46,18 @@ class _AbstractPatternMatcher:
46
46
 
47
47
  return super().__repr__()
48
48
 
49
+ @abstractmethod
50
+ def can_prune_directories(self) -> bool:
51
+ """
52
+ Returns True if this pattern matcher allows safe early directory pruning.
53
+
54
+ Directory pruning is safe when matching directories can be skipped entirely
55
+ without missing any files that should be included.
56
+
57
+ An example where pruning is not safe is for inverted patterns, like "!**/*.py".
58
+ """
59
+ ...
60
+
49
61
  @abstractmethod
50
62
  def __call__(self, path: Path) -> bool: ...
51
63
 
@@ -54,6 +66,15 @@ class _CustomPatternMatcher(_AbstractPatternMatcher):
54
66
  def __init__(self, predicate: Callable[[Path], bool]):
55
67
  self._predicate = predicate
56
68
 
69
+ def can_prune_directories(self) -> bool:
70
+ """
71
+ Custom pattern matchers (like negated matchers) cannot safely prune directories.
72
+
73
+ Since these are arbitrary predicates, we cannot determine if a directory
74
+ can be safely skipped without evaluating all files within it.
75
+ """
76
+ return False
77
+
57
78
  def __call__(self, path: Path) -> bool:
58
79
  return self._predicate(path)
59
80
 
@@ -173,6 +194,16 @@ class FilePatternMatcher(_AbstractPatternMatcher):
173
194
 
174
195
  return matched
175
196
 
197
+ def can_prune_directories(self) -> bool:
198
+ """
199
+ Returns True if this pattern matcher allows safe early directory pruning.
200
+
201
+ Directory pruning is safe when matching directories can be skipped entirely
202
+ without missing any files that should be included. This is for example not
203
+ safe when we have inverted/negated ignore patterns (e.g. "!**/*.py").
204
+ """
205
+ return not any(pattern.exclusion for pattern in self.patterns)
206
+
176
207
  def __call__(self, file_path: Path) -> bool:
177
208
  if self._delayed_init:
178
209
  self._delayed_init()
modal/functions.pyi CHANGED
@@ -428,7 +428,7 @@ class Function(
428
428
 
429
429
  _call_generator: ___call_generator_spec[typing_extensions.Self]
430
430
 
431
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
431
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
432
432
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
433
433
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
434
434
  ...
@@ -437,7 +437,7 @@ class Function(
437
437
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
438
438
  ...
439
439
 
440
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
440
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
441
441
 
442
442
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
443
443
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -464,7 +464,7 @@ class Function(
464
464
  """
465
465
  ...
466
466
 
467
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
467
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
468
468
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
469
469
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
470
470
 
@@ -488,7 +488,7 @@ class Function(
488
488
  ...
489
489
 
490
490
  _experimental_spawn: ___experimental_spawn_spec[
491
- modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
491
+ modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
492
492
  ]
493
493
 
494
494
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
@@ -497,7 +497,7 @@ class Function(
497
497
 
498
498
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
499
499
 
500
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
500
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
501
501
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
502
502
  """Calls the function with the given arguments, without waiting for the results.
503
503
 
@@ -518,7 +518,7 @@ class Function(
518
518
  """
519
519
  ...
520
520
 
521
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
521
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
522
522
 
523
523
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
524
524
  """Return the inner Python object wrapped by this Modal Function."""
modal/mount.py CHANGED
@@ -8,7 +8,7 @@ import re
8
8
  import time
9
9
  import typing
10
10
  import warnings
11
- from collections.abc import AsyncGenerator
11
+ from collections.abc import AsyncGenerator, Generator
12
12
  from pathlib import Path, PurePosixPath
13
13
  from typing import Callable, Optional, Sequence, Union
14
14
 
@@ -133,12 +133,27 @@ class _MountFile(_MountEntry):
133
133
  class _MountDir(_MountEntry):
134
134
  local_dir: Path
135
135
  remote_path: PurePosixPath
136
- ignore: Callable[[Path], bool]
136
+ ignore: Union[Callable[[Path], bool], modal.file_pattern_matcher._AbstractPatternMatcher]
137
137
  recursive: bool
138
138
 
139
139
  def description(self):
140
140
  return str(self.local_dir.expanduser().absolute())
141
141
 
142
+ def _walk_and_prune(self, top_dir: Path) -> Generator[str, None, None]:
143
+ """Walk directories and prune ignored directories early."""
144
+ for root, dirs, files in os.walk(top_dir, topdown=True):
145
+ # with topdown=True, os.walk allows modifying the dirs list in-place, and will only
146
+ # recurse into dirs that are not ignored.
147
+ dirs[:] = [d for d in dirs if not self.ignore(Path(os.path.join(root, d)).relative_to(top_dir))]
148
+ for file in files:
149
+ yield os.path.join(root, file)
150
+
151
+ def _walk_all(self, top_dir: Path) -> Generator[str, None, None]:
152
+ """Walk all directories without early pruning - safe for complex/inverted ignore patterns."""
153
+ for root, _, files in os.walk(top_dir):
154
+ for file in files:
155
+ yield os.path.join(root, file)
156
+
142
157
  def get_files_to_upload(self):
143
158
  # we can't use .resolve() eagerly here since that could end up "renaming" symlinked files
144
159
  # see test_mount_directory_with_symlinked_file
@@ -153,7 +168,13 @@ class _MountDir(_MountEntry):
153
168
  raise NotADirectoryError(msg)
154
169
 
155
170
  if self.recursive:
156
- gen = (os.path.join(root, name) for root, dirs, files in os.walk(local_dir) for name in files)
171
+ if (
172
+ isinstance(self.ignore, modal.file_pattern_matcher._AbstractPatternMatcher)
173
+ and self.ignore.can_prune_directories()
174
+ ):
175
+ gen = self._walk_and_prune(local_dir)
176
+ else:
177
+ gen = self._walk_all(local_dir)
157
178
  else:
158
179
  gen = (dir_entry.path for dir_entry in os.scandir(local_dir) if dir_entry.is_file())
159
180
 
modal/mount.pyi CHANGED
@@ -4,6 +4,7 @@ import modal._object
4
4
  import modal._resolver
5
5
  import modal._utils.blob_utils
6
6
  import modal.client
7
+ import modal.file_pattern_matcher
7
8
  import modal.object
8
9
  import modal_proto.api_pb2
9
10
  import pathlib
@@ -49,14 +50,24 @@ class _MountFile(_MountEntry):
49
50
  ...
50
51
 
51
52
  class _MountDir(_MountEntry):
52
- """_MountDir(local_dir: pathlib.Path, remote_path: pathlib.PurePosixPath, ignore: Callable[[pathlib.Path], bool], recursive: bool)"""
53
+ """_MountDir(local_dir: pathlib.Path, remote_path: pathlib.PurePosixPath, ignore: Union[Callable[[pathlib.Path], bool], modal.file_pattern_matcher._AbstractPatternMatcher], recursive: bool)"""
53
54
 
54
55
  local_dir: pathlib.Path
55
56
  remote_path: pathlib.PurePosixPath
56
- ignore: collections.abc.Callable[[pathlib.Path], bool]
57
+ ignore: typing.Union[
58
+ collections.abc.Callable[[pathlib.Path], bool], modal.file_pattern_matcher._AbstractPatternMatcher
59
+ ]
57
60
  recursive: bool
58
61
 
59
62
  def description(self): ...
63
+ def _walk_and_prune(self, top_dir: pathlib.Path) -> collections.abc.Generator[str, None, None]:
64
+ """Walk directories and prune ignored directories early."""
65
+ ...
66
+
67
+ def _walk_all(self, top_dir: pathlib.Path) -> collections.abc.Generator[str, None, None]:
68
+ """Walk all directories without early pruning - safe for complex/inverted ignore patterns."""
69
+ ...
70
+
60
71
  def get_files_to_upload(self): ...
61
72
  def watch_entry(self): ...
62
73
  def top_level_paths(self) -> list[tuple[pathlib.Path, pathlib.PurePosixPath]]: ...
@@ -64,7 +75,9 @@ class _MountDir(_MountEntry):
64
75
  self,
65
76
  local_dir: pathlib.Path,
66
77
  remote_path: pathlib.PurePosixPath,
67
- ignore: collections.abc.Callable[[pathlib.Path], bool],
78
+ ignore: typing.Union[
79
+ collections.abc.Callable[[pathlib.Path], bool], modal.file_pattern_matcher._AbstractPatternMatcher
80
+ ],
68
81
  recursive: bool,
69
82
  ) -> None:
70
83
  """Initialize self. See help(type(self)) for accurate signature."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.0.6.dev5
3
+ Version: 1.0.6.dev7
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -22,7 +22,7 @@ modal/app.py,sha256=fCKq3TJ2Y5LB2WKNs6pp_5XECNH5avUL01jQljuoYRU,46603
22
22
  modal/app.pyi,sha256=Z6wi_dkXywiaM2rvAvguj2Wgu9ZgPjMSLl1nH1a7EYI,42243
23
23
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
24
24
  modal/client.py,sha256=OwISJvkgMb-rHm9Gc4i-7YcDgGiZgwJ7F_PzwZH7a6Q,16847
25
- modal/client.pyi,sha256=Cp104g6AL3YFJRPLq7CLiM6rytUFg65fxAy0nc24jbM,15079
25
+ modal/client.pyi,sha256=Sf6qZOzIFzhgAEohtVTfB19bIVnBkI8lL2NTPadzUn4,15079
26
26
  modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
27
27
  modal/cloud_bucket_mount.pyi,sha256=-qSfYAQvIoO_l2wsCCGTG5ZUwQieNKXdAO00yP1-LYU,7394
28
28
  modal/cls.py,sha256=EFrM949jNXJpmwB2G_1d28b8IpHShfKIEIaiPkZqeOU,39881
@@ -37,16 +37,16 @@ modal/environments.pyi,sha256=9-KtrzAcUe55cCP4020lSUD7-fWS7OPakAHssq4-bro,4219
37
37
  modal/exception.py,sha256=RjfKTJH7-Gcf_33BGkvDch-ry1Zx9u6-0QLViNxNTaU,5520
38
38
  modal/file_io.py,sha256=SCBfLk5gRieqdTVlA_f-2YHHtRp7Iy_sA6iR1zPsO3c,21100
39
39
  modal/file_io.pyi,sha256=_Hm-59MrppfuBYxtzdJkA2Jf9zI5LlbPh_0gURk0_7s,15222
40
- modal/file_pattern_matcher.py,sha256=wov-otB5M1oTdrYDtR2_VgacYin2srdtAP4McA1Cqzw,6516
40
+ modal/file_pattern_matcher.py,sha256=urAue8es8jxqX94k9EYoZxxhtfgOlsEES8lbFHOorzc,7734
41
41
  modal/functions.py,sha256=kcNHvqeGBxPI7Cgd57NIBBghkfbeFJzXO44WW0jSmao,325
42
- modal/functions.pyi,sha256=ffW_kkU8AxMuV77ltmjK3nslXW_2iwEjKsT-Cgd4Trs,34840
42
+ modal/functions.pyi,sha256=FJe_91dSrMCRNVT-YV1UhtxFKzIvL_C5q8xdk08-wT8,34840
43
43
  modal/gpu.py,sha256=Fe5ORvVPDIstSq1xjmM6OoNgLYFWvogP9r5BgmD3hYg,6769
44
44
  modal/image.py,sha256=3oF3wo5uJ9tlOV8yotixBJ-ZR1tOkRE1Qvq4E3G7Fhk,94159
45
45
  modal/image.pyi,sha256=ha8QhMPDRmfTSoRlQ8CgV4-AZ7kYKK_-R2UXLyGPgQU,68172
46
46
  modal/io_streams.py,sha256=FUDpBsVK8isqwyC7DtAcQZhaHlMFSaNZGhYJOg-SFW0,15590
47
47
  modal/io_streams.pyi,sha256=5b3b93ztZeR8IpJtNIGffX24QLPgocE4-gAps8y7CKU,13824
48
- modal/mount.py,sha256=7gdzBNueSjPsgsdisMv1bv5SncEQyWc6D-YlAbpze3s,35523
49
- modal/mount.pyi,sha256=Q4ryg68yyfgJh4dWqcs4y9jvcF8JArU8tIUZhdjRHac,19613
48
+ modal/mount.py,sha256=VrExPZApUnuh9shJx-tzdmQJ9ASTSK-SEaYHDBkQl-k,36632
49
+ modal/mount.pyi,sha256=n6AuS8J3bTCQj750nVZZdVBvzCAlSM2fyxAt_5LLFik,20264
50
50
  modal/network_file_system.py,sha256=92U94Wk2fP40LlgLDIHkTqQ-zc21YxaG6SdFQy8SudU,14731
51
51
  modal/network_file_system.pyi,sha256=Td_IobHr84iLo_9LZKQ4tNdUB60yjX8QWBaFiUvhfi8,17685
52
52
  modal/object.py,sha256=bTeskuY8JFrESjU4_UL_nTwYlBQdOLmVaOX3X6EMxsg,164
@@ -92,7 +92,7 @@ modal/_runtime/user_code_imports.py,sha256=78wJyleqY2RVibqcpbDQyfWVBVT9BjyHPeoV9
92
92
  modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
93
93
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
94
94
  modal/_utils/async_utils.py,sha256=MhSCsCL8GqIVFWoHubU_899IH-JBZAiiqadG9Wri2l4,29361
95
- modal/_utils/blob_utils.py,sha256=uT_MlcYgS_Qo0EsuQw200_HQdsyNAkWbBe8K1YBrUsw,19838
95
+ modal/_utils/blob_utils.py,sha256=NViIia3A5rdCpN712UDwiIiWtCZFoK_9DGq-xNX1z6A,19906
96
96
  modal/_utils/bytes_io_segment_payload.py,sha256=vaXPq8b52-x6G2hwE7SrjS58pg_aRm7gV3bn3yjmTzQ,4261
97
97
  modal/_utils/deprecation.py,sha256=-Bgg7jZdcJU8lROy18YyVnQYbM8hue-hVmwJqlWAGH0,5504
98
98
  modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
@@ -147,7 +147,7 @@ modal/requirements/2024.10.txt,sha256=qD-5cVIVM9wXesJ6JC89Ew-3m2KjEElUz3jaw_MddR
147
147
  modal/requirements/PREVIEW.txt,sha256=KxDaVTOwatHvboDo4lorlgJ7-n-MfAwbPwxJ0zcJqrs,312
148
148
  modal/requirements/README.md,sha256=9tK76KP0Uph7O0M5oUgsSwEZDj5y-dcUPsnpR0Sc-Ik,854
149
149
  modal/requirements/base-images.json,sha256=f1bwyp2UkM844eoO9Qk30gQw_xrMqKpMSeJ6MErXnEk,995
150
- modal-1.0.6.dev5.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
150
+ modal-1.0.6.dev7.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
151
151
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
152
152
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
153
153
  modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
@@ -170,10 +170,10 @@ modal_proto/options_pb2.pyi,sha256=l7DBrbLO7q3Ir-XDkWsajm0d0TQqqrfuX54i4BMpdQg,1
170
170
  modal_proto/options_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
171
171
  modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0yJSI,247
172
172
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
173
- modal_version/__init__.py,sha256=KONbjKXNKx4J-giUGLlC3Wktw8-rQBrVeV4wSv7wj20,120
173
+ modal_version/__init__.py,sha256=LhpZZvv8XrA5pB3BlP89kaxoLYKr4FqpFn-1KFxmJM4,120
174
174
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
175
- modal-1.0.6.dev5.dist-info/METADATA,sha256=px9BLuBKBZsS3WkwU6Kugf6D8Nol3Ycvw-cjFVaZBQo,2461
176
- modal-1.0.6.dev5.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
177
- modal-1.0.6.dev5.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
178
- modal-1.0.6.dev5.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
179
- modal-1.0.6.dev5.dist-info/RECORD,,
175
+ modal-1.0.6.dev7.dist-info/METADATA,sha256=MbeO2WcpWPifknwp4fqT9sipwP1OO5cHwgIxzoI7mmY,2461
176
+ modal-1.0.6.dev7.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
177
+ modal-1.0.6.dev7.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
178
+ modal-1.0.6.dev7.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
179
+ modal-1.0.6.dev7.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.0.6.dev5"
4
+ __version__ = "1.0.6.dev7"