modal 0.62.16__py3-none-any.whl → 0.72.11__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.
Files changed (220) hide show
  1. modal/__init__.py +17 -13
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +420 -937
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +5 -7
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,10 @@
1
1
  # Copyright Modal Labs 2023
2
+ import functools
2
3
  import os
3
4
  import time
5
+ from collections.abc import AsyncIterator
4
6
  from pathlib import Path, PurePosixPath
5
- from typing import AsyncIterator, BinaryIO, List, Optional, Tuple, Type, Union
7
+ from typing import Any, BinaryIO, Callable, Optional, Union
6
8
 
7
9
  from grpclib import GRPCError, Status
8
10
  from synchronicity.async_wrap import asynccontextmanager
@@ -11,12 +13,14 @@ import modal
11
13
  from modal_proto import api_pb2
12
14
 
13
15
  from ._resolver import Resolver
14
- from ._utils.async_utils import ConcurrencyPool, TaskContext, synchronize_api
16
+ from ._utils.async_utils import TaskContext, aclosing, async_map, sync_or_async_iter, synchronize_api
15
17
  from ._utils.blob_utils import LARGE_FILE_LIMIT, blob_iter, blob_upload_file
16
- from ._utils.grpc_utils import retry_transient_errors, unary_stream
18
+ from ._utils.deprecation import renamed_parameter
19
+ from ._utils.grpc_utils import retry_transient_errors
17
20
  from ._utils.hash_utils import get_sha256_hex
21
+ from ._utils.name_utils import check_object_name
18
22
  from .client import _Client
19
- from .exception import deprecation_warning
23
+ from .exception import InvalidError
20
24
  from .object import (
21
25
  EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
22
26
  _get_environment_name,
@@ -24,6 +28,7 @@ from .object import (
24
28
  live_method,
25
29
  live_method_gen,
26
30
  )
31
+ from .volume import FileEntry
27
32
 
28
33
  NETWORK_FILE_SYSTEM_PUT_FILE_CLIENT_TIMEOUT = (
29
34
  10 * 60
@@ -31,9 +36,9 @@ NETWORK_FILE_SYSTEM_PUT_FILE_CLIENT_TIMEOUT = (
31
36
 
32
37
 
33
38
  def network_file_system_mount_protos(
34
- validated_network_file_systems: List[Tuple[str, "_NetworkFileSystem"]],
39
+ validated_network_file_systems: list[tuple[str, "_NetworkFileSystem"]],
35
40
  allow_cross_region_volumes: bool,
36
- ) -> List[api_pb2.SharedVolumeMount]:
41
+ ) -> list[api_pb2.SharedVolumeMount]:
37
42
  network_file_system_mounts = []
38
43
  # Relies on dicts being ordered (true as of Python 3.6).
39
44
  for path, volume in validated_network_file_systems:
@@ -59,20 +64,20 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
59
64
  import modal
60
65
 
61
66
  nfs = modal.NetworkFileSystem.from_name("my-nfs", create_if_missing=True)
62
- stub = modal.Stub()
67
+ app = modal.App()
63
68
 
64
- @stub.function(network_file_systems={"/root/foo": nfs})
69
+ @app.function(network_file_systems={"/root/foo": nfs})
65
70
  def f():
66
71
  pass
67
72
 
68
- @stub.function(network_file_systems={"/root/goo": nfs})
73
+ @app.function(network_file_systems={"/root/goo": nfs})
69
74
  def g():
70
75
  pass
71
76
  ```
72
77
 
73
78
  Also see the CLI methods for accessing network file systems:
74
79
 
75
- ```bash
80
+ ```
76
81
  modal nfs --help
77
82
  ```
78
83
 
@@ -86,68 +91,52 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
86
91
  """
87
92
 
88
93
  @staticmethod
89
- def new(cloud: Optional[str] = None) -> "_NetworkFileSystem":
90
- """`NetworkFileSystem.new` is deprecated.
91
-
92
- Please use `NetworkFileSystem.from_name` (for persisted) or `NetworkFileSystem.ephemeral`
93
- (for ephemeral) network filesystems.
94
- """
95
- deprecation_warning((2024, 3, 20), NetworkFileSystem.new.__doc__)
96
-
97
- async def _load(self: _NetworkFileSystem, resolver: Resolver, existing_object_id: Optional[str]):
98
- status_row = resolver.add_status_row()
99
- if existing_object_id:
100
- # Volume already exists; do nothing.
101
- self._hydrate(existing_object_id, resolver.client, None)
102
- return
103
-
104
- if cloud:
105
- deprecation_warning((2024, 1, 17), "Argument `cloud` is deprecated (has no effect).")
106
-
107
- status_row.message("Creating network file system...")
108
- req = api_pb2.SharedVolumeCreateRequest(app_id=resolver.app_id)
109
- resp = await retry_transient_errors(resolver.client.stub.SharedVolumeCreate, req)
110
- status_row.finish("Created network file system.")
111
- self._hydrate(resp.shared_volume_id, resolver.client, None)
112
-
113
- return _NetworkFileSystem._from_loader(_load, "NetworkFileSystem()")
114
-
115
- @staticmethod
94
+ @renamed_parameter((2024, 12, 18), "label", "name")
116
95
  def from_name(
117
- label: str,
96
+ name: str,
118
97
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
119
98
  environment_name: Optional[str] = None,
120
99
  create_if_missing: bool = False,
121
100
  ) -> "_NetworkFileSystem":
122
- """Create a reference to a persisted network filesystem, optionally creating it lazily.
101
+ """Reference a NetworkFileSystem by its name, creating if necessary.
123
102
 
124
- **Examples**
103
+ In contrast to `modal.NetworkFileSystem.lookup`, this is a lazy method
104
+ that defers hydrating the local object with metadata from Modal servers
105
+ until the first time it is actually used.
125
106
 
126
107
  ```python notest
127
- volume = NetworkFileSystem.from_name("my-volume", create_if_missing=True)
108
+ nfs = NetworkFileSystem.from_name("my-nfs", create_if_missing=True)
128
109
 
129
- @stub.function(network_file_systems={"/vol": volume})
110
+ @app.function(network_file_systems={"/data": nfs})
130
111
  def f():
131
112
  pass
132
113
  ```
133
114
  """
115
+ check_object_name(name, "NetworkFileSystem")
134
116
 
135
117
  async def _load(self: _NetworkFileSystem, resolver: Resolver, existing_object_id: Optional[str]):
136
118
  req = api_pb2.SharedVolumeGetOrCreateRequest(
137
- deployment_name=label,
119
+ deployment_name=name,
138
120
  namespace=namespace,
139
121
  environment_name=_get_environment_name(environment_name, resolver),
140
122
  object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
141
123
  )
142
- response = await resolver.client.stub.SharedVolumeGetOrCreate(req)
143
- self._hydrate(response.shared_volume_id, resolver.client, None)
144
-
145
- return _NetworkFileSystem._from_loader(_load, "NetworkFileSystem()")
124
+ try:
125
+ response = await resolver.client.stub.SharedVolumeGetOrCreate(req)
126
+ self._hydrate(response.shared_volume_id, resolver.client, None)
127
+ except GRPCError as exc:
128
+ if exc.status == Status.NOT_FOUND and exc.message == "App has wrong entity vo":
129
+ raise InvalidError(
130
+ f"Attempted to mount: `{name}` as a NetworkFileSystem " + "which already exists as a Volume"
131
+ )
132
+ raise
133
+
134
+ return _NetworkFileSystem._from_loader(_load, "NetworkFileSystem()", hydrate_lazily=True)
146
135
 
147
136
  @classmethod
148
137
  @asynccontextmanager
149
138
  async def ephemeral(
150
- cls: Type["_NetworkFileSystem"],
139
+ cls: type["_NetworkFileSystem"],
151
140
  client: Optional[_Client] = None,
152
141
  environment_name: Optional[str] = None,
153
142
  _heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
@@ -156,11 +145,13 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
156
145
 
157
146
  Usage:
158
147
  ```python
159
- with NetworkFileSystem.ephemeral() as nfs:
160
- assert nfs.listdir() == []
148
+ with modal.NetworkFileSystem.ephemeral() as nfs:
149
+ assert nfs.listdir("/") == []
150
+ ```
161
151
 
162
- async with NetworkFileSystem.ephemeral() as nfs:
163
- assert await nfs.listdir() == []
152
+ ```python notest
153
+ async with modal.NetworkFileSystem.ephemeral() as nfs:
154
+ assert await nfs.listdir("/") == []
164
155
  ```
165
156
  """
166
157
  if client is None:
@@ -176,44 +167,26 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
176
167
  yield cls._new_hydrated(response.shared_volume_id, client, None, is_another_app=True)
177
168
 
178
169
  @staticmethod
179
- def persisted(
180
- label: str,
181
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
182
- environment_name: Optional[str] = None,
183
- cloud: Optional[str] = None,
184
- ) -> "_NetworkFileSystem":
185
- """Deprecated! Use `NetworkFileSystem.from_name(name, create_if_missing=True)`."""
186
- deprecation_warning((2024, 3, 1), _NetworkFileSystem.persisted.__doc__)
187
- return _NetworkFileSystem.from_name(label, namespace, environment_name, create_if_missing=True)
188
-
189
- def persist(
190
- self,
191
- label: str,
192
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
193
- environment_name: Optional[str] = None,
194
- cloud: Optional[str] = None,
195
- ):
196
- """`NetworkFileSystem().persist("my-volume")` is deprecated. Use `NetworkFileSystem.from_name("my-volume", create_if_missing=True)` instead."""
197
- deprecation_warning((2024, 2, 29), _NetworkFileSystem.persist.__doc__)
198
- return self.persisted(label, namespace, environment_name, cloud)
199
-
200
- @staticmethod
170
+ @renamed_parameter((2024, 12, 18), "label", "name")
201
171
  async def lookup(
202
- label: str,
172
+ name: str,
203
173
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
204
174
  client: Optional[_Client] = None,
205
175
  environment_name: Optional[str] = None,
206
176
  create_if_missing: bool = False,
207
177
  ) -> "_NetworkFileSystem":
208
- """Lookup a network file system with a given name
178
+ """Lookup a named NetworkFileSystem.
209
179
 
210
- ```python
211
- n = modal.NetworkFileSystem.lookup("my-nfs")
212
- print(n.listdir("/"))
180
+ In contrast to `modal.NetworkFileSystem.from_name`, this is an eager method
181
+ that will hydrate the local object with metadata from Modal servers.
182
+
183
+ ```python notest
184
+ nfs = modal.NetworkFileSystem.lookup("my-nfs")
185
+ print(nfs.listdir("/"))
213
186
  ```
214
187
  """
215
188
  obj = _NetworkFileSystem.from_name(
216
- label, namespace=namespace, environment_name=environment_name, create_if_missing=create_if_missing
189
+ name, namespace=namespace, environment_name=environment_name, create_if_missing=create_if_missing
217
190
  )
218
191
  if client is None:
219
192
  client = await _Client.from_env()
@@ -229,6 +202,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
229
202
  environment_name: Optional[str] = None,
230
203
  ) -> str:
231
204
  """mdmd:hidden"""
205
+ check_object_name(deployment_name, "NetworkFileSystem")
232
206
  if client is None:
233
207
  client = await _Client.from_env()
234
208
  request = api_pb2.SharedVolumeGetOrCreateRequest(
@@ -240,8 +214,15 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
240
214
  resp = await retry_transient_errors(client.stub.SharedVolumeGetOrCreate, request)
241
215
  return resp.shared_volume_id
242
216
 
217
+ @staticmethod
218
+ @renamed_parameter((2024, 12, 18), "label", "name")
219
+ async def delete(name: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
220
+ obj = await _NetworkFileSystem.lookup(name, client=client, environment_name=environment_name)
221
+ req = api_pb2.SharedVolumeDeleteRequest(shared_volume_id=obj.object_id)
222
+ await retry_transient_errors(obj._client.stub.SharedVolumeDelete, req)
223
+
243
224
  @live_method
244
- async def write_file(self, remote_path: str, fp: BinaryIO) -> int:
225
+ async def write_file(self, remote_path: str, fp: BinaryIO, progress_cb: Optional[Callable[..., Any]] = None) -> int:
245
226
  """Write from a file object to a path on the network file system, atomically.
246
227
 
247
228
  Will create any needed parent directories automatically.
@@ -249,12 +230,20 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
249
230
  If remote_path ends with `/` it's assumed to be a directory and the
250
231
  file will be uploaded with its current name to that directory.
251
232
  """
233
+ progress_cb = progress_cb or (lambda *_, **__: None)
234
+
252
235
  sha_hash = get_sha256_hex(fp)
253
236
  fp.seek(0, os.SEEK_END)
254
237
  data_size = fp.tell()
255
238
  fp.seek(0)
256
239
  if data_size > LARGE_FILE_LIMIT:
257
- blob_id = await blob_upload_file(fp, self._client.stub)
240
+ progress_task_id = progress_cb(name=remote_path, size=data_size)
241
+ blob_id = await blob_upload_file(
242
+ fp,
243
+ self._client.stub,
244
+ progress_report_cb=functools.partial(progress_cb, progress_task_id),
245
+ sha256_hex=sha_hash,
246
+ )
258
247
  req = api_pb2.SharedVolumePutFileRequest(
259
248
  shared_volume_id=self.object_id,
260
249
  path=remote_path,
@@ -293,21 +282,25 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
293
282
  yield data
294
283
 
295
284
  @live_method_gen
296
- async def iterdir(self, path: str) -> AsyncIterator[api_pb2.SharedVolumeListFilesEntry]:
285
+ async def iterdir(self, path: str) -> AsyncIterator[FileEntry]:
297
286
  """Iterate over all files in a directory in the network file system.
298
287
 
299
288
  * Passing a directory path lists all files in the directory (names are relative to the directory)
300
289
  * Passing a file path returns a list containing only that file's listing description
301
- * Passing a glob path (including at least one * or ** sequence) returns all files matching that glob path (using absolute paths)
290
+ * Passing a glob path (including at least one * or ** sequence) returns all files matching
291
+ that glob path (using absolute paths)
302
292
  """
303
293
  req = api_pb2.SharedVolumeListFilesRequest(shared_volume_id=self.object_id, path=path)
304
- async for batch in unary_stream(self._client.stub.SharedVolumeListFilesStream, req):
294
+ async for batch in self._client.stub.SharedVolumeListFilesStream.unary_stream(req):
305
295
  for entry in batch.entries:
306
- yield entry
296
+ yield FileEntry._from_proto(entry)
307
297
 
308
298
  @live_method
309
299
  async def add_local_file(
310
- self, local_path: Union[Path, str], remote_path: Optional[Union[str, PurePosixPath, None]] = None
300
+ self,
301
+ local_path: Union[Path, str],
302
+ remote_path: Optional[Union[str, PurePosixPath, None]] = None,
303
+ progress_cb: Optional[Callable[..., Any]] = None,
311
304
  ):
312
305
  local_path = Path(local_path)
313
306
  if remote_path is None:
@@ -316,13 +309,14 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
316
309
  remote_path = PurePosixPath(remote_path).as_posix()
317
310
 
318
311
  with local_path.open("rb") as local_file:
319
- return await self.write_file(remote_path, local_file)
312
+ return await self.write_file(remote_path, local_file, progress_cb=progress_cb)
320
313
 
321
314
  @live_method
322
315
  async def add_local_dir(
323
316
  self,
324
317
  local_path: Union[Path, str],
325
318
  remote_path: Optional[Union[str, PurePosixPath, None]] = None,
319
+ progress_cb: Optional[Callable[..., Any]] = None,
326
320
  ):
327
321
  _local_path = Path(local_path)
328
322
  if remote_path is None:
@@ -337,17 +331,23 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
337
331
  if subpath.is_dir():
338
332
  continue
339
333
  relpath_str = subpath.relative_to(_local_path).as_posix()
340
- yield self.add_local_file(subpath, PurePosixPath(remote_path, relpath_str))
334
+ yield subpath, PurePosixPath(remote_path, relpath_str)
335
+
336
+ async def _add_local_file(paths: tuple[Path, PurePosixPath]) -> int:
337
+ return await self.add_local_file(paths[0], paths[1], progress_cb)
341
338
 
342
- await ConcurrencyPool(20).run_coros(gen_transfers(), return_exceptions=True)
339
+ async with aclosing(async_map(sync_or_async_iter(gen_transfers()), _add_local_file, concurrency=20)) as stream:
340
+ async for _ in stream: # consume/execute the map
341
+ pass
343
342
 
344
343
  @live_method
345
- async def listdir(self, path: str) -> List[api_pb2.SharedVolumeListFilesEntry]:
344
+ async def listdir(self, path: str) -> list[FileEntry]:
346
345
  """List all files in a directory in the network file system.
347
346
 
348
347
  * Passing a directory path lists all files in the directory (names are relative to the directory)
349
348
  * Passing a file path returns a list containing only that file's listing description
350
- * Passing a glob path (including at least one * or ** sequence) returns all files matching that glob path (using absolute paths)
349
+ * Passing a glob path (including at least one * or ** sequence) returns all files matching
350
+ that glob path (using absolute paths)
351
351
  """
352
352
  return [entry async for entry in self.iterdir(path)]
353
353