modal 0.68.9__py3-none-any.whl → 0.68.14__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/_ipython.py CHANGED
@@ -1,21 +1,11 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import sys
3
- import warnings
4
-
5
- ipy_outstream = None
6
- try:
7
- with warnings.catch_warnings():
8
- warnings.simplefilter("ignore")
9
- import ipykernel.iostream
10
-
11
- ipy_outstream = ipykernel.iostream.OutStream
12
- except ImportError:
13
- pass
14
3
 
15
4
 
16
5
  def is_notebook(stdout=None):
17
- if ipy_outstream is None:
6
+ ipykernel_iostream = sys.modules.get("ipykernel.iostream")
7
+ if ipykernel_iostream is None:
18
8
  return False
19
9
  if stdout is None:
20
10
  stdout = sys.stdout
21
- return isinstance(stdout, ipy_outstream)
11
+ return isinstance(stdout, ipykernel_iostream.OutStream)
@@ -269,10 +269,12 @@ def import_single_function_service(
269
269
  # The cls decorator is in global scope
270
270
  _cls = synchronizer._translate_in(cls)
271
271
  user_defined_callable = _cls._callables[fun_name]
272
- function = _cls._method_functions.get(fun_name)
272
+ function = _cls._method_functions.get(
273
+ fun_name
274
+ ) # bound to the class service function - there is no instance
273
275
  active_app = _cls._app
274
276
  else:
275
- # This is a raw class
277
+ # This is non-decorated class
276
278
  user_defined_callable = getattr(cls, fun_name)
277
279
  else:
278
280
  raise InvalidError(f"Invalid function qualname {qual_name}")
@@ -21,7 +21,7 @@ from modal_proto.modal_api_grpc import ModalClientModal
21
21
  from ..exception import ExecutionError
22
22
  from .async_utils import TaskContext, retry
23
23
  from .grpc_utils import retry_transient_errors
24
- from .hash_utils import UploadHashes, get_sha256_hex, get_upload_hashes
24
+ from .hash_utils import UploadHashes, get_upload_hashes
25
25
  from .http_utils import ClientSessionRegistry
26
26
  from .logger import logger
27
27
 
@@ -38,6 +38,11 @@ BLOB_MAX_PARALLELISM = 10
38
38
  # read ~16MiB chunks by default
39
39
  DEFAULT_SEGMENT_CHUNK_SIZE = 2**24
40
40
 
41
+ # Files larger than this will be multipart uploaded. The server might request multipart upload for smaller files as
42
+ # well, but the limit will never be raised.
43
+ # TODO(dano): remove this once we stop requiring md5 for blobs
44
+ MULTIPART_UPLOAD_THRESHOLD = 1024**3
45
+
41
46
 
42
47
  class BytesIOSegmentPayload(BytesIOPayload):
43
48
  """Modified bytes payload for concurrent sends of chunks from the same file.
@@ -305,9 +310,13 @@ async def blob_upload(payload: bytes, stub: ModalClientModal) -> str:
305
310
 
306
311
 
307
312
  async def blob_upload_file(
308
- file_obj: BinaryIO, stub: ModalClientModal, progress_report_cb: Optional[Callable] = None
313
+ file_obj: BinaryIO,
314
+ stub: ModalClientModal,
315
+ progress_report_cb: Optional[Callable] = None,
316
+ sha256_hex: Optional[str] = None,
317
+ md5_hex: Optional[str] = None,
309
318
  ) -> str:
310
- upload_hashes = get_upload_hashes(file_obj)
319
+ upload_hashes = get_upload_hashes(file_obj, sha256_hex=sha256_hex, md5_hex=md5_hex)
311
320
  return await _blob_upload(upload_hashes, file_obj, stub, progress_report_cb)
312
321
 
313
322
 
@@ -366,6 +375,7 @@ class FileUploadSpec:
366
375
  use_blob: bool
367
376
  content: Optional[bytes] # typically None if using blob, required otherwise
368
377
  sha256_hex: str
378
+ md5_hex: str
369
379
  mode: int # file permission bits (last 12 bits of st_mode)
370
380
  size: int
371
381
 
@@ -383,13 +393,15 @@ def _get_file_upload_spec(
383
393
  fp.seek(0)
384
394
 
385
395
  if size >= LARGE_FILE_LIMIT:
396
+ # TODO(dano): remove the placeholder md5 once we stop requiring md5 for blobs
397
+ md5_hex = "baadbaadbaadbaadbaadbaadbaadbaad" if size > MULTIPART_UPLOAD_THRESHOLD else None
386
398
  use_blob = True
387
399
  content = None
388
- sha256_hex = get_sha256_hex(fp)
400
+ hashes = get_upload_hashes(fp, md5_hex=md5_hex)
389
401
  else:
390
402
  use_blob = False
391
403
  content = fp.read()
392
- sha256_hex = get_sha256_hex(content)
404
+ hashes = get_upload_hashes(content)
393
405
 
394
406
  return FileUploadSpec(
395
407
  source=source,
@@ -397,7 +409,8 @@ def _get_file_upload_spec(
397
409
  mount_filename=mount_filename.as_posix(),
398
410
  use_blob=use_blob,
399
411
  content=content,
400
- sha256_hex=sha256_hex,
412
+ sha256_hex=hashes.sha256_hex(),
413
+ md5_hex=hashes.md5_hex(),
401
414
  mode=mode & 0o7777,
402
415
  size=size,
403
416
  )
@@ -99,7 +99,11 @@ def get_function_type(is_generator: Optional[bool]) -> "api_pb2.Function.Functio
99
99
 
100
100
 
101
101
  class FunctionInfo:
102
- """Class that helps us extract a bunch of information about a function."""
102
+ """Class that helps us extract a bunch of information about a locally defined function.
103
+
104
+ Used for populating the definition of a remote function, and for making .local() calls
105
+ on a host with the local definition available.
106
+ """
103
107
 
104
108
  raw_f: Optional[Callable[..., Any]] # if None - this is a "class service function"
105
109
  function_name: str
@@ -50,7 +50,7 @@ def patch_mock_servicer(cls):
50
50
 
51
51
  @contextlib.contextmanager
52
52
  def intercept(servicer):
53
- ctx = InterceptionContext()
53
+ ctx = InterceptionContext(servicer)
54
54
  servicer.interception_context = ctx
55
55
  yield ctx
56
56
  ctx._assert_responses_consumed()
@@ -101,7 +101,8 @@ class ResponseNotConsumed(Exception):
101
101
 
102
102
 
103
103
  class InterceptionContext:
104
- def __init__(self):
104
+ def __init__(self, servicer):
105
+ self._servicer = servicer
105
106
  self.calls: list[tuple[str, Any]] = [] # List[Tuple[method_name, message]]
106
107
  self.custom_responses: dict[str, list[tuple[Callable[[Any], bool], list[Any]]]] = defaultdict(list)
107
108
  self.custom_defaults: dict[str, Callable[["MockClientServicer", grpclib.server.Stream], Awaitable[None]]] = {}
@@ -149,6 +150,9 @@ class InterceptionContext:
149
150
  raise KeyError(f"No message of that type in call list: {self.calls}")
150
151
 
151
152
  def get_requests(self, method_name: str) -> list[Any]:
153
+ if not hasattr(self._servicer, method_name):
154
+ # we check this to prevent things like `assert ctx.get_requests("ASdfFunctionCreate") == 0` passing
155
+ raise ValueError(f"{method_name} not in MockServicer - did you spell it right?")
152
156
  return [msg for _method_name, msg in self.calls if _method_name == method_name]
153
157
 
154
158
  def _add_recv(self, method_name: str, msg):
@@ -3,14 +3,14 @@ import base64
3
3
  import dataclasses
4
4
  import hashlib
5
5
  import time
6
- from typing import BinaryIO, Callable, Union
6
+ from typing import BinaryIO, Callable, Optional, Sequence, Union
7
7
 
8
8
  from modal.config import logger
9
9
 
10
10
  HASH_CHUNK_SIZE = 65536
11
11
 
12
12
 
13
- def _update(hashers: list[Callable[[bytes], None]], data: Union[bytes, BinaryIO]) -> None:
13
+ def _update(hashers: Sequence[Callable[[bytes], None]], data: Union[bytes, BinaryIO]) -> None:
14
14
  if isinstance(data, bytes):
15
15
  for hasher in hashers:
16
16
  hasher(data)
@@ -57,15 +57,44 @@ class UploadHashes:
57
57
  md5_base64: str
58
58
  sha256_base64: str
59
59
 
60
+ def md5_hex(self) -> str:
61
+ return base64.b64decode(self.md5_base64).hex()
60
62
 
61
- def get_upload_hashes(data: Union[bytes, BinaryIO]) -> UploadHashes:
63
+ def sha256_hex(self) -> str:
64
+ return base64.b64decode(self.sha256_base64).hex()
65
+
66
+
67
+ def get_upload_hashes(
68
+ data: Union[bytes, BinaryIO], sha256_hex: Optional[str] = None, md5_hex: Optional[str] = None
69
+ ) -> UploadHashes:
62
70
  t0 = time.monotonic()
63
- md5 = hashlib.md5()
64
- sha256 = hashlib.sha256()
65
- _update([md5.update, sha256.update], data)
71
+ hashers = {}
72
+
73
+ if not sha256_hex:
74
+ sha256 = hashlib.sha256()
75
+ hashers["sha256"] = sha256
76
+ if not md5_hex:
77
+ md5 = hashlib.md5()
78
+ hashers["md5"] = md5
79
+
80
+ if hashers:
81
+ updaters = [h.update for h in hashers.values()]
82
+ _update(updaters, data)
83
+
84
+ if sha256_hex:
85
+ sha256_base64 = base64.b64encode(bytes.fromhex(sha256_hex)).decode("ascii")
86
+ else:
87
+ sha256_base64 = base64.b64encode(hashers["sha256"].digest()).decode("ascii")
88
+
89
+ if md5_hex:
90
+ md5_base64 = base64.b64encode(bytes.fromhex(md5_hex)).decode("ascii")
91
+ else:
92
+ md5_base64 = base64.b64encode(hashers["md5"].digest()).decode("ascii")
93
+
66
94
  hashes = UploadHashes(
67
- md5_base64=base64.b64encode(md5.digest()).decode("ascii"),
68
- sha256_base64=base64.b64encode(sha256.digest()).decode("ascii"),
95
+ md5_base64=md5_base64,
96
+ sha256_base64=sha256_base64,
69
97
  )
70
- logger.debug("get_upload_hashes took %.3fs", time.monotonic() - t0)
98
+
99
+ logger.debug("get_upload_hashes took %.3fs (%s)", time.monotonic() - t0, hashers.keys())
71
100
  return hashes
modal/client.pyi CHANGED
@@ -26,7 +26,7 @@ class _Client:
26
26
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
27
 
28
28
  def __init__(
29
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.9"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.14"
30
30
  ): ...
31
31
  def is_closed(self) -> bool: ...
32
32
  @property
@@ -81,7 +81,7 @@ class Client:
81
81
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
82
82
 
83
83
  def __init__(
84
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.9"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.14"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/cls.py CHANGED
@@ -19,7 +19,7 @@ from ._utils.async_utils import synchronize_api, synchronizer
19
19
  from ._utils.grpc_utils import retry_transient_errors
20
20
  from ._utils.mount_utils import validate_volumes
21
21
  from .client import _Client
22
- from .exception import InvalidError, NotFoundError, VersionError
22
+ from .exception import ExecutionError, InvalidError, NotFoundError, VersionError
23
23
  from .functions import _Function, _parse_retries
24
24
  from .gpu import GPU_T
25
25
  from .object import _get_environment_name, _Object
@@ -40,7 +40,7 @@ if typing.TYPE_CHECKING:
40
40
  import modal.app
41
41
 
42
42
 
43
- def _use_annotation_parameters(user_cls) -> bool:
43
+ def _use_annotation_parameters(user_cls: type) -> bool:
44
44
  has_parameters = any(is_parameter(cls_member) for cls_member in user_cls.__dict__.values())
45
45
  has_explicit_constructor = user_cls.__init__ != object.__init__
46
46
  return has_parameters and not has_explicit_constructor
@@ -73,7 +73,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
73
73
  def _bind_instance_method(service_function: _Function, class_bound_method: _Function):
74
74
  """mdmd:hidden
75
75
 
76
- Binds an "instance service function" to a specific method.
76
+ Binds an "instance service function" to a specific method name.
77
77
  This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
78
78
  it does it forward invocations to the underlying instance_service_function with the specified method,
79
79
  and we don't support web_config for parameterized methods at the moment.
@@ -84,7 +84,6 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
84
84
  # object itself doesn't need any "loading"
85
85
  assert service_function._obj
86
86
  method_name = class_bound_method._use_method_name
87
- full_function_name = f"{class_bound_method._function_name}[parameterized]"
88
87
 
89
88
  def hydrate_from_instance_service_function(method_placeholder_fun):
90
89
  method_placeholder_fun._hydrate_from_other(service_function)
@@ -92,7 +91,7 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
92
91
  method_placeholder_fun._web_url = (
93
92
  class_bound_method._web_url
94
93
  ) # TODO: this shouldn't be set when actual parameters are used
95
- method_placeholder_fun._function_name = full_function_name
94
+ method_placeholder_fun._function_name = f"{class_bound_method._function_name}[parameterized]"
96
95
  method_placeholder_fun._is_generator = class_bound_method._is_generator
97
96
  method_placeholder_fun._cluster_size = class_bound_method._cluster_size
98
97
  method_placeholder_fun._use_method_name = method_name
@@ -112,7 +111,7 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
112
111
  return []
113
112
  return [service_function]
114
113
 
115
- rep = f"Method({full_function_name})"
114
+ rep = f"Method({method_name})"
116
115
 
117
116
  fun = _Function._from_loader(
118
117
  _load,
@@ -137,22 +136,23 @@ class _Obj:
137
136
 
138
137
  All this class does is to return `Function` objects."""
139
138
 
139
+ _cls: "_Cls" # parent
140
140
  _functions: dict[str, _Function]
141
141
  _has_entered: bool
142
142
  _user_cls_instance: Optional[Any] = None
143
- _construction_args: tuple[tuple, dict[str, Any]]
143
+ _args: tuple[Any, ...]
144
+ _kwargs: dict[str, Any]
144
145
 
145
- _instance_service_function: Optional[_Function]
146
+ _instance_service_function: Optional[_Function] = None # this gets set lazily
146
147
 
147
148
  def _uses_common_service_function(self):
148
149
  # Used for backwards compatibility checks with pre v0.63 classes
149
- return self._instance_service_function is not None
150
+ return self._cls._class_service_function is not None
150
151
 
151
152
  def __init__(
152
153
  self,
154
+ cls: "_Cls",
153
155
  user_cls: Optional[type], # this would be None in case of lookups
154
- class_service_function: Optional[_Function], # only None for <v0.63 classes
155
- classbound_methods: dict[str, _Function],
156
156
  options: Optional[api_pb2.FunctionOptions],
157
157
  args,
158
158
  kwargs,
@@ -161,45 +161,60 @@ class _Obj:
161
161
  check_valid_cls_constructor_arg(i + 1, arg)
162
162
  for key, kwarg in kwargs.items():
163
163
  check_valid_cls_constructor_arg(key, kwarg)
164
-
165
- self._method_functions = {}
166
- if class_service_function:
167
- # >= v0.63 classes
168
- # first create the singular object function used by all methods on this parameterization
169
- self._instance_service_function = class_service_function._bind_parameters(self, options, args, kwargs)
170
- for method_name, class_bound_method in classbound_methods.items():
171
- method = _bind_instance_method(self._instance_service_function, class_bound_method)
172
- self._method_functions[method_name] = method
173
- else:
174
- # looked up <v0.63 classes - bind each individual method to the new parameters
175
- self._instance_service_function = None
176
- for method_name, class_bound_method in classbound_methods.items():
177
- method = class_bound_method._bind_parameters(self, options, args, kwargs)
178
- self._method_functions[method_name] = method
164
+ self._cls = cls
179
165
 
180
166
  # Used for construction local object lazily
181
167
  self._has_entered = False
182
168
  self._user_cls = user_cls
183
- self._construction_args = (args, kwargs) # used for lazy construction in case of explicit constructors
169
+
170
+ # used for lazy construction in case of explicit constructors
171
+ self._args = args
172
+ self._kwargs = kwargs
173
+ self._options = options
174
+
175
+ def _cached_service_function(self) -> "modal.functions._Function":
176
+ # Returns a service function for this _Obj, serving all its methods
177
+ # In case of methods without parameters or options, this is simply proxying to the class service function
178
+
179
+ # only safe to call for 0.63+ classes (before then, all methods had their own services)
180
+ if not self._instance_service_function:
181
+ assert self._cls._class_service_function
182
+ self._instance_service_function = self._cls._class_service_function._bind_parameters(
183
+ self, self._options, self._args, self._kwargs
184
+ )
185
+ return self._instance_service_function
186
+
187
+ def _get_parameter_values(self) -> dict[str, Any]:
188
+ # binds args and kwargs according to the class constructor signature
189
+ # (implicit by parameters or explicit)
190
+ sig = _get_class_constructor_signature(self._user_cls)
191
+ bound_vars = sig.bind(*self._args, **self._kwargs)
192
+ bound_vars.apply_defaults()
193
+ return bound_vars.arguments
184
194
 
185
195
  def _new_user_cls_instance(self):
186
- args, kwargs = self._construction_args
187
196
  if not _use_annotation_parameters(self._user_cls):
188
197
  # TODO(elias): deprecate this code path eventually
189
- user_cls_instance = self._user_cls(*args, **kwargs)
198
+ user_cls_instance = self._user_cls(*self._args, **self._kwargs)
190
199
  else:
191
200
  # ignore constructor (assumes there is no custom constructor,
192
201
  # which is guaranteed by _use_annotation_parameters)
193
202
  # set the attributes on the class corresponding to annotations
194
203
  # with = parameter() specifications
195
- sig = _get_class_constructor_signature(self._user_cls)
196
- bound_vars = sig.bind(*args, **kwargs)
197
- bound_vars.apply_defaults()
204
+ param_values = self._get_parameter_values()
198
205
  user_cls_instance = self._user_cls.__new__(self._user_cls) # new instance without running __init__
199
- user_cls_instance.__dict__.update(bound_vars.arguments)
206
+ user_cls_instance.__dict__.update(param_values)
200
207
 
201
208
  # TODO: always use Obj instances instead of making modifications to user cls
202
- user_cls_instance._modal_functions = self._method_functions # Needed for PartialFunction.__get__
209
+ # TODO: OR (if simpler for now) replace all the PartialFunctions on the user cls
210
+ # with getattr(self, method_name)
211
+
212
+ # user cls instances are only created locally, so we have all partial functions available
213
+ instance_methods = {}
214
+ for method_name in _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.FUNCTION):
215
+ instance_methods[method_name] = getattr(self, method_name)
216
+
217
+ user_cls_instance._modal_functions = instance_methods
203
218
  return user_cls_instance
204
219
 
205
220
  async def keep_warm(self, warm_pool_size: int) -> None:
@@ -221,7 +236,7 @@ class _Obj:
221
236
  raise VersionError(
222
237
  "Class instance `.keep_warm(...)` can't be used on classes deployed using client version <v0.63"
223
238
  )
224
- await self._instance_service_function.keep_warm(warm_pool_size)
239
+ await self._cached_service_function().keep_warm(warm_pool_size)
225
240
 
226
241
  def _cached_user_cls_instance(self):
227
242
  """Get or construct the local object
@@ -233,18 +248,20 @@ class _Obj:
233
248
  return self._user_cls_instance
234
249
 
235
250
  def _enter(self):
251
+ assert self._user_cls
236
252
  if not self._has_entered:
237
- if hasattr(self._user_cls_instance, "__enter__"):
238
- self._user_cls_instance.__enter__()
253
+ user_cls_instance = self._cached_user_cls_instance()
254
+ if hasattr(user_cls_instance, "__enter__"):
255
+ user_cls_instance.__enter__()
239
256
 
240
257
  for method_flag in (
241
258
  _PartialFunctionFlags.ENTER_PRE_SNAPSHOT,
242
259
  _PartialFunctionFlags.ENTER_POST_SNAPSHOT,
243
260
  ):
244
- for enter_method in _find_callables_for_obj(self._user_cls_instance, method_flag).values():
261
+ for enter_method in _find_callables_for_obj(user_cls_instance, method_flag).values():
245
262
  enter_method()
246
263
 
247
- self._has_entered = True
264
+ self._has_entered = True
248
265
 
249
266
  @property
250
267
  def _entered(self) -> bool:
@@ -266,23 +283,74 @@ class _Obj:
266
283
  self._has_entered = True
267
284
 
268
285
  def __getattr__(self, k):
269
- if k in self._method_functions:
270
- # If we know the user is accessing a *method* and not another attribute,
271
- # we don't have to create an instance of the user class yet.
272
- # This is because it might just be a call to `.remote()` on it which
273
- # doesn't require a local instance.
274
- # As long as we have the service function or params, we can do remote calls
275
- # without calling the constructor of the class in the calling context.
276
- return self._method_functions[k]
286
+ # This is a bit messy and branchy because:
287
+ # * Support for pre-0.63 lookups *and* newer classes
288
+ # * Support .remote() on both hydrated (local or remote classes) or unhydrated classes (remote classes only)
289
+ # * Support .local() on both hydrated and unhydrated classes (assuming local access to code)
290
+ # * Support attribute access (when local cls is available)
291
+
292
+ def _get_method_bound_function() -> Optional["_Function"]:
293
+ """Gets _Function object for method - either for a local or a hydrated remote class
294
+
295
+ * If class is neither local or hydrated - raise exception (should never happen)
296
+ * If attribute isn't a method - return None
297
+ """
298
+ if self._cls._method_functions is None:
299
+ raise ExecutionError("Method is not local and not hydrated")
300
+
301
+ if class_bound_method := self._cls._method_functions.get(k, None):
302
+ # If we know the user is accessing a *method* and not another attribute,
303
+ # we don't have to create an instance of the user class yet.
304
+ # This is because it might just be a call to `.remote()` on it which
305
+ # doesn't require a local instance.
306
+ # As long as we have the service function or params, we can do remote calls
307
+ # without calling the constructor of the class in the calling context.
308
+ if self._cls._class_service_function is None:
309
+ # a <v0.63 lookup
310
+ return class_bound_method._bind_parameters(self, self._options, self._args, self._kwargs)
311
+ else:
312
+ return _bind_instance_method(self._cached_service_function(), class_bound_method)
313
+
314
+ return None # The attribute isn't a method
315
+
316
+ if self._cls._method_functions is not None:
317
+ # We get here with either a hydrated Cls or an unhydrated one with local definition
318
+ if method := _get_method_bound_function():
319
+ return method
320
+ elif self._user_cls:
321
+ # We have the local definition, and the attribute isn't a method
322
+ # so we instantiate if we don't have an instance, and try to get the attribute
323
+ user_cls_instance = self._cached_user_cls_instance()
324
+ return getattr(user_cls_instance, k)
325
+ else:
326
+ # This is the case for a *hydrated* class without the local definition, i.e. a lookup
327
+ # where the attribute isn't a registered method of the class
328
+ raise NotFoundError(
329
+ f"Class has no method `{k}` and attributes (or undecorated methods) can't be accessed for"
330
+ f" remote classes (`Cls.from_name` instances)"
331
+ )
332
+
333
+ # Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
334
+ # has not yet been loaded. So use a special loader that loads it lazily:
277
335
 
278
- # if it's *not* a method, it *might* be an attribute of the class,
279
- # so we construct it and proxy the attribute
280
- # TODO: To get lazy loading (from_name) of classes to work, we need to avoid
281
- # this path, otherwise local initialization will happen regardless if user
282
- # only runs .remote(), since we don't know methods for the class until we
283
- # load it
284
- user_cls_instance = self._cached_user_cls_instance()
285
- return getattr(user_cls_instance, k)
336
+ async def method_loader(fun, resolver: Resolver, existing_object_id):
337
+ await resolver.load(self._cls) # load class so we get info about methods
338
+ method_function = _get_method_bound_function()
339
+ if method_function is None:
340
+ raise NotFoundError(
341
+ f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
342
+ )
343
+ await resolver.load(method_function) # get the appropriate method handle (lazy)
344
+ fun._hydrate_from_other(method_function)
345
+
346
+ # The reason we don't *always* use this lazy loader is because it precludes attribute access
347
+ # on local classes.
348
+ return _Function._from_loader(
349
+ method_loader,
350
+ repr,
351
+ deps=lambda: [], # TODO: use cls as dep instead of loading inside method_loader?
352
+ hydrate_lazily=True,
353
+ )
286
354
 
287
355
 
288
356
  Obj = synchronize_api(_Obj)
@@ -313,6 +381,7 @@ class _Cls(_Object, type_prefix="cs"):
313
381
  self._callables = {}
314
382
 
315
383
  def _initialize_from_other(self, other: "_Cls"):
384
+ super()._initialize_from_other(other)
316
385
  self._user_cls = other._user_cls
317
386
  self._class_service_function = other._class_service_function
318
387
  self._method_functions = other._method_functions
@@ -503,7 +572,8 @@ class _Cls(_Object, type_prefix="cs"):
503
572
  obj._hydrate(response.class_id, resolver.client, response.handle_metadata)
504
573
 
505
574
  rep = f"Ref({app_name})"
506
- cls = cls._from_loader(_load_remote, rep, is_another_app=True)
575
+ cls = cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
576
+ # TODO: when pre 0.63 is phased out, we can set class_service_function here instead
507
577
  return cls
508
578
 
509
579
  def with_options(
@@ -594,9 +664,8 @@ class _Cls(_Object, type_prefix="cs"):
594
664
  def __call__(self, *args, **kwargs) -> _Obj:
595
665
  """This acts as the class constructor."""
596
666
  return _Obj(
667
+ self,
597
668
  self._user_cls,
598
- self._class_service_function,
599
- self._method_functions,
600
669
  self._options,
601
670
  args,
602
671
  kwargs,
@@ -604,6 +673,7 @@ class _Cls(_Object, type_prefix="cs"):
604
673
 
605
674
  def __getattr__(self, k):
606
675
  # Used by CLI and container entrypoint
676
+ # TODO: remove this method - access to attributes on classes should be discouraged
607
677
  if k in self._method_functions:
608
678
  return self._method_functions[k]
609
679
  return getattr(self._user_cls, k)
modal/cls.pyi CHANGED
@@ -17,29 +17,32 @@ import typing_extensions
17
17
 
18
18
  T = typing.TypeVar("T")
19
19
 
20
- def _use_annotation_parameters(user_cls) -> bool: ...
20
+ def _use_annotation_parameters(user_cls: type) -> bool: ...
21
21
  def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
22
22
  def _bind_instance_method(
23
23
  service_function: modal.functions._Function, class_bound_method: modal.functions._Function
24
24
  ): ...
25
25
 
26
26
  class _Obj:
27
+ _cls: _Cls
27
28
  _functions: dict[str, modal.functions._Function]
28
29
  _has_entered: bool
29
30
  _user_cls_instance: typing.Optional[typing.Any]
30
- _construction_args: tuple[tuple, dict[str, typing.Any]]
31
+ _args: tuple[typing.Any, ...]
32
+ _kwargs: dict[str, typing.Any]
31
33
  _instance_service_function: typing.Optional[modal.functions._Function]
32
34
 
33
35
  def _uses_common_service_function(self): ...
34
36
  def __init__(
35
37
  self,
38
+ cls: _Cls,
36
39
  user_cls: typing.Optional[type],
37
- class_service_function: typing.Optional[modal.functions._Function],
38
- classbound_methods: dict[str, modal.functions._Function],
39
40
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
40
41
  args,
41
42
  kwargs,
42
43
  ): ...
44
+ def _cached_service_function(self) -> modal.functions._Function: ...
45
+ def _get_parameter_values(self) -> dict[str, typing.Any]: ...
43
46
  def _new_user_cls_instance(self): ...
44
47
  async def keep_warm(self, warm_pool_size: int) -> None: ...
45
48
  def _cached_user_cls_instance(self): ...
@@ -52,22 +55,25 @@ class _Obj:
52
55
  def __getattr__(self, k): ...
53
56
 
54
57
  class Obj:
58
+ _cls: Cls
55
59
  _functions: dict[str, modal.functions.Function]
56
60
  _has_entered: bool
57
61
  _user_cls_instance: typing.Optional[typing.Any]
58
- _construction_args: tuple[tuple, dict[str, typing.Any]]
62
+ _args: tuple[typing.Any, ...]
63
+ _kwargs: dict[str, typing.Any]
59
64
  _instance_service_function: typing.Optional[modal.functions.Function]
60
65
 
61
66
  def __init__(
62
67
  self,
68
+ cls: Cls,
63
69
  user_cls: typing.Optional[type],
64
- class_service_function: typing.Optional[modal.functions.Function],
65
- classbound_methods: dict[str, modal.functions.Function],
66
70
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
67
71
  args,
68
72
  kwargs,
69
73
  ): ...
70
74
  def _uses_common_service_function(self): ...
75
+ def _cached_service_function(self) -> modal.functions.Function: ...
76
+ def _get_parameter_values(self) -> dict[str, typing.Any]: ...
71
77
  def _new_user_cls_instance(self): ...
72
78
 
73
79
  class __keep_warm_spec(typing_extensions.Protocol):
modal/functions.py CHANGED
@@ -432,7 +432,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
432
432
  image: _Image,
433
433
  secrets: Sequence[_Secret] = (),
434
434
  schedule: Optional[Schedule] = None,
435
- is_generator=False,
435
+ is_generator: bool = False,
436
436
  gpu: Union[GPU_T, list[GPU_T]] = None,
437
437
  # TODO: maybe break this out into a separate decorator for notebooks.
438
438
  mounts: Collection[_Mount] = (),
@@ -628,7 +628,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
628
628
  raise InvalidError(f"Expected modal.Image object. Got {type(image)}.")
629
629
 
630
630
  method_definitions: Optional[dict[str, api_pb2.MethodDefinition]] = None
631
- partial_functions: dict[str, "modal.partial_function._PartialFunction"] = {}
631
+
632
632
  if info.user_cls:
633
633
  method_definitions = {}
634
634
  partial_functions = _find_partial_methods_for_user_cls(info.user_cls, _PartialFunctionFlags.FUNCTION)
@@ -1191,9 +1191,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1191
1191
  return self._web_url
1192
1192
 
1193
1193
  @property
1194
- def is_generator(self) -> bool:
1194
+ async def is_generator(self) -> bool:
1195
1195
  """mdmd:hidden"""
1196
- assert self._is_generator is not None
1196
+ # hacky: kind of like @live_method, but not hydrating if we have the value already from local source
1197
+ if self._is_generator is not None:
1198
+ # this is set if the function or class is local
1199
+ return self._is_generator
1200
+
1201
+ # not set - this is a from_name lookup - hydrate
1202
+ await self.resolve()
1203
+ assert self._is_generator is not None # should be set now
1197
1204
  return self._is_generator
1198
1205
 
1199
1206
  @property
@@ -1318,6 +1325,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1318
1325
  async for item in self._call_generator(args, kwargs): # type: ignore
1319
1326
  yield item
1320
1327
 
1328
+ def _is_local(self):
1329
+ return self._info is not None
1330
+
1321
1331
  def _get_info(self) -> FunctionInfo:
1322
1332
  if not self._info:
1323
1333
  raise ExecutionError("Can't get info for a function that isn't locally defined")
@@ -1342,19 +1352,24 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1342
1352
  """
1343
1353
  # TODO(erikbern): it would be nice to remove the nowrap thing, but right now that would cause
1344
1354
  # "user code" to run on the synchronicity thread, which seems bad
1355
+ if not self._is_local():
1356
+ msg = (
1357
+ "The definition for this function is missing here so it is not possible to invoke it locally. "
1358
+ "If this function was retrieved via `Function.lookup` you need to use `.remote()`."
1359
+ )
1360
+ raise ExecutionError(msg)
1361
+
1345
1362
  info = self._get_info()
1363
+ if not info.raw_f:
1364
+ # Here if calling .local on a service function itself which should never happen
1365
+ # TODO: check if we end up here in a container for a serialized function?
1366
+ raise ExecutionError("Can't call .local on service function")
1346
1367
 
1347
1368
  if is_local() and self.spec.volumes or self.spec.network_file_systems:
1348
1369
  warnings.warn(
1349
1370
  f"The {info.function_name} function is executing locally "
1350
1371
  + "and will not have access to the mounted Volume or NetworkFileSystem data"
1351
1372
  )
1352
- if not info or not info.raw_f:
1353
- msg = (
1354
- "The definition for this function is missing so it is not possible to invoke it locally. "
1355
- "If this function was retrieved via `Function.lookup` you need to use `.remote()`."
1356
- )
1357
- raise ExecutionError(msg)
1358
1373
 
1359
1374
  obj: Optional["modal.cls._Obj"] = self._get_obj()
1360
1375
 
@@ -1364,9 +1379,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1364
1379
  else:
1365
1380
  # This is a method on a class, so bind the self to the function
1366
1381
  user_cls_instance = obj._cached_user_cls_instance()
1367
-
1368
1382
  fun = info.raw_f.__get__(user_cls_instance)
1369
1383
 
1384
+ # TODO: replace implicit local enter/exit with a context manager
1370
1385
  if is_async(info.raw_f):
1371
1386
  # We want to run __aenter__ and fun in the same coroutine
1372
1387
  async def coro():
modal/functions.pyi CHANGED
@@ -157,7 +157,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
157
157
  image: modal.image._Image,
158
158
  secrets: collections.abc.Sequence[modal.secret._Secret] = (),
159
159
  schedule: typing.Optional[modal.schedule.Schedule] = None,
160
- is_generator=False,
160
+ is_generator: bool = False,
161
161
  gpu: typing.Union[
162
162
  None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]
163
163
  ] = None,
@@ -234,7 +234,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
234
234
  @property
235
235
  async def web_url(self) -> str: ...
236
236
  @property
237
- def is_generator(self) -> bool: ...
237
+ async def is_generator(self) -> bool: ...
238
238
  @property
239
239
  def cluster_size(self) -> int: ...
240
240
  def _map(
@@ -246,6 +246,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
246
246
  async def _call_generator_nowait(self, args, kwargs): ...
247
247
  async def remote(self, *args: P.args, **kwargs: P.kwargs) -> ReturnType: ...
248
248
  def remote_gen(self, *args, **kwargs) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
249
+ def _is_local(self): ...
249
250
  def _get_info(self) -> modal._utils.function_utils.FunctionInfo: ...
250
251
  def _get_obj(self) -> typing.Optional[modal.cls._Obj]: ...
251
252
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
@@ -325,7 +326,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
325
326
  image: modal.image.Image,
326
327
  secrets: collections.abc.Sequence[modal.secret.Secret] = (),
327
328
  schedule: typing.Optional[modal.schedule.Schedule] = None,
328
- is_generator=False,
329
+ is_generator: bool = False,
329
330
  gpu: typing.Union[
330
331
  None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]
331
332
  ] = None,
@@ -467,6 +468,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
467
468
 
468
469
  remote_gen: __remote_gen_spec
469
470
 
471
+ def _is_local(self): ...
470
472
  def _get_info(self) -> modal._utils.function_utils.FunctionInfo: ...
471
473
  def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
472
474
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
modal/mount.py CHANGED
@@ -483,7 +483,9 @@ class _Mount(_Object, type_prefix="mo"):
483
483
  logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
484
484
  async with blob_upload_concurrency:
485
485
  with file_spec.source() as fp:
486
- blob_id = await blob_upload_file(fp, resolver.client.stub)
486
+ blob_id = await blob_upload_file(
487
+ fp, resolver.client.stub, sha256_hex=file_spec.sha256_hex, md5_hex=file_spec.md5_hex
488
+ )
487
489
  logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
488
490
  request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
489
491
  else:
@@ -245,7 +245,10 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
245
245
  if data_size > LARGE_FILE_LIMIT:
246
246
  progress_task_id = progress_cb(name=remote_path, size=data_size)
247
247
  blob_id = await blob_upload_file(
248
- fp, self._client.stub, progress_report_cb=functools.partial(progress_cb, progress_task_id)
248
+ fp,
249
+ self._client.stub,
250
+ progress_report_cb=functools.partial(progress_cb, progress_task_id),
251
+ sha256_hex=sha_hash,
249
252
  )
250
253
  req = api_pb2.SharedVolumePutFileRequest(
251
254
  shared_volume_id=self.object_id,
modal/object.py CHANGED
@@ -96,7 +96,9 @@ class _Object:
96
96
 
97
97
  def _initialize_from_other(self, other):
98
98
  # default implementation, can be overriden in subclasses
99
- pass
99
+ self._object_id = other._object_id
100
+ self._is_hydrated = other._is_hydrated
101
+ self._client = other._client
100
102
 
101
103
  def _hydrate(self, object_id: str, client: _Client, metadata: Optional[Message]):
102
104
  assert isinstance(object_id, str)
@@ -139,7 +141,7 @@ class _Object:
139
141
 
140
142
  # Object to clone must already be hydrated, otherwise from_loader is more suitable.
141
143
  self._validate_is_hydrated()
142
- obj = _Object.__new__(type(self))
144
+ obj = type(self).__new__(type(self))
143
145
  obj._initialize_from_other(self)
144
146
  return obj
145
147
 
modal/volume.py CHANGED
@@ -632,7 +632,11 @@ class _VolumeUploadContextManager:
632
632
  logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
633
633
  with file_spec.source() as fp:
634
634
  blob_id = await blob_upload_file(
635
- fp, self._client.stub, functools.partial(self._progress_cb, progress_task_id)
635
+ fp,
636
+ self._client.stub,
637
+ functools.partial(self._progress_cb, progress_task_id),
638
+ sha256_hex=file_spec.sha256_hex,
639
+ md5_hex=file_spec.md5_hex,
636
640
  )
637
641
  logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
638
642
  request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.68.9
3
+ Version: 0.68.14
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -3,7 +3,7 @@ modal/__main__.py,sha256=scYhGFqh8OJcVDo-VOxIT6CCwxOgzgflYWMnIZiMRqE,2871
3
3
  modal/_clustered_functions.py,sha256=kTf-9YBXY88NutC1akI-gCbvf01RhMPCw-zoOI_YIUE,2700
4
4
  modal/_clustered_functions.pyi,sha256=vllkegc99A0jrUOWa8mdlSbdp6uz36TsHhGxysAOpaQ,771
5
5
  modal/_container_entrypoint.py,sha256=wk10vA5vRZZsVwQ0yINOLd0i-NwH7x6XbhTslumvGjo,28910
6
- modal/_ipython.py,sha256=HF_DYy0e0qM9WnGDmTY30s1RxzGya9GeORCauCEpRaE,450
6
+ modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
7
7
  modal/_location.py,sha256=S3lSxIU3h9HkWpkJ3Pwo0pqjIOSB1fjeSgUsY3x7eec,1202
8
8
  modal/_output.py,sha256=0fWX_KQwhER--U81ys16CL-pA5A-LN20C0EZjElKGJQ,25410
9
9
  modal/_proxy_tunnel.py,sha256=gnKyCfmVB7x2d1A6c-JDysNIP3kEFxmXzhcXhPrzPn0,1906
@@ -19,11 +19,11 @@ modal/app.py,sha256=EJ7FUN6rWnSwLJoYJh8nmKg_t-8hdN8_rt0OrkP7JvQ,46084
19
19
  modal/app.pyi,sha256=BE5SlR5tRECuc6-e2lUuOknDdov3zxgZ4N0AsLb5ZVQ,25270
20
20
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
21
21
  modal/client.py,sha256=nyPjfromWBeOyurexpFP2QLQNk822RPggMCLyX9j1jA,15247
22
- modal/client.pyi,sha256=pl58Y423t0X7ykjle0WhhFYcAirdwjSD4jGS2qqYW_Y,7278
22
+ modal/client.pyi,sha256=4x5j0yBlE_sZf8cS2eChs-pYuxr-_SsRiKrf1fSJmfA,7280
23
23
  modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
24
24
  modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
25
- modal/cls.py,sha256=pMirIOmb59YzaIK2rn7Vd756E1QKDDweYT90GYIHiMk,27472
26
- modal/cls.pyi,sha256=47jaIT06fz8PSUrs-MaNn6r03PHsAyUGsKuK5e9RMhQ,8140
25
+ modal/cls.py,sha256=ONnrfZ2vPcaY2JuKypPiBA9eTiyg8Qfg-Ull40nn9zs,30956
26
+ modal/cls.pyi,sha256=uoOEANXgCFT9Au3e-_bU98M6ZfAgQWF5ngj8f4c6qpY,8225
27
27
  modal/config.py,sha256=1KhNJkjYsJkX1V8RPPdRYPlM2HE-ZZs0JVSxbiXjmrw,11010
28
28
  modal/container_process.py,sha256=zDxCLk6KfJT1G9FfNtjom6gekBQ46op3TWepT7-Hkbg,6077
29
29
  modal/container_process.pyi,sha256=dqtqBmyRpXXpRrDooESL6WBVU_1Rh6OG-66P2Hk9E5U,2666
@@ -35,18 +35,18 @@ modal/exception.py,sha256=dRK789TD1HaB63kHhu1yZuvS2vP_Vua3iLMBtA6dgqk,7128
35
35
  modal/experimental.py,sha256=jFuNbwrNHos47viMB9q-cHJSvf2RDxDdoEcss9plaZE,2302
36
36
  modal/file_io.py,sha256=q8s872qf6Ntdw7dPogDlpYbixxGkwCA0BlQn2UUoVhY,14637
37
37
  modal/file_io.pyi,sha256=pfkmJiaBpMCZReE6-KCjYOzB1dVtyYDYokJoYX8ARK4,6932
38
- modal/functions.py,sha256=rtqs2Rq-Jg8_BniksWAqxId0aLU8obtRzw80--VD4zk,66960
39
- modal/functions.pyi,sha256=IyuM9TV79JfrtfTaJ4yq3EcWp3yHuxLavpxTOwSWEDw,24988
38
+ modal/functions.py,sha256=IIdHw0FNOdoMksG1b2zvkn8f-xskhJu07ZvHMey9iq4,67667
39
+ modal/functions.pyi,sha256=EYH4w4VgQtdbEWLGarnU5QtYVfuM2_tnovKFEbYyg2c,25068
40
40
  modal/gpu.py,sha256=r4rL6uH3UJIQthzYvfWauXNyh01WqCPtKZCmmSX1fd4,6881
41
41
  modal/image.py,sha256=cQ6WP1xHXZT_nY8z3aEFiGwKzrTV0yxi3Ab8JzF91eo,79653
42
42
  modal/image.pyi,sha256=PIKH6JBA4L5TfdJrQu3pm2ykyIITmiP920TpP8cdyQA,24585
43
43
  modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
44
44
  modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
45
- modal/mount.py,sha256=liaid5p42o0OKnzoocJJ_oCovDVderk3-JuCTa5pqtA,27656
45
+ modal/mount.py,sha256=7FJrS-QkRJGndKuvRnMz452wfUcbLpd_UEnmFgQCKQQ,27770
46
46
  modal/mount.pyi,sha256=3e4nkXUeeVmUmOyK8Tiyk_EQlHeWruN3yGJVnmDUVrI,9761
47
- modal/network_file_system.py,sha256=NKZgh_p8MyJyyJgP92lhRgTmwA3kOPw7m8AbYlchhCE,14530
47
+ modal/network_file_system.py,sha256=kwwQLCJVO086FTiAWSF_jz9BkqijZLpSbEYXpFvS0Ik,14600
48
48
  modal/network_file_system.pyi,sha256=8mHKXuRkxHPazF6ljIW7g4M5aVqLSl6eKUPLgDCug5c,7901
49
- modal/object.py,sha256=KmtWRDd5ntHGSO9ASHe9MJcIgjNRqaDXGc3rWOXwrmA,9646
49
+ modal/object.py,sha256=HZs3N59C6JxlMuPQWJYvrWV1FEEkH9txUovVDorVUbs,9763
50
50
  modal/object.pyi,sha256=MO78H9yFSE5i1gExPEwyyQzLdlshkcGHN1aQ0ylyvq0,8802
51
51
  modal/output.py,sha256=N0xf4qeudEaYrslzdAl35VKV8rapstgIM2e9wO8_iy0,1967
52
52
  modal/parallel_map.py,sha256=4aoMXIrlG3wl5Ifk2YDNOQkXsGRsm6Xbfm6WtJ2t3WY,16002
@@ -73,22 +73,22 @@ modal/serving.pyi,sha256=ncV-9jY_vZYFnGs5ZnMb3ffrX8LmcLdIMHBC56xRbtE,1711
73
73
  modal/stream_type.py,sha256=A6320qoAAWhEfwOCZfGtymQTu5AfLfJXXgARqooTPvY,417
74
74
  modal/token_flow.py,sha256=LcgSce_MSQ2p7j55DPwpVRpiAtCDe8GRSEwzO7muNR8,6774
75
75
  modal/token_flow.pyi,sha256=gOYtYujrWt_JFZeiI8EmfahXPx5GCR5Na-VaPQcWgEY,1937
76
- modal/volume.py,sha256=IISuMeXq9MoSkhXg8Q6JG0F-2n9NTkWk0xGuJB8l3d8,29159
76
+ modal/volume.py,sha256=PGzbninvRU-IhSwJgM2jZKzD8llRhZhadsOxZ-YNwaM,29316
77
77
  modal/volume.pyi,sha256=St0mDiaojfep6Bs4sBbkRJmeacYHF6lh6FKOWGmheHA,11182
78
78
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
79
79
  modal/_runtime/asgi.py,sha256=GvuxZqWnIHMIR-Bx5f7toCQlkERaJO8CHjTPNM9IFIw,21537
80
80
  modal/_runtime/container_io_manager.py,sha256=ctgyNFiHjq1brCrabXmlurkAXjnrCeWPRvTVa735vRw,44215
81
81
  modal/_runtime/execution_context.py,sha256=E6ofm6j1POXGPxS841X3V7JU6NheVb8OkQc7JpLq4Kg,2712
82
82
  modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
83
- modal/_runtime/user_code_imports.py,sha256=q_3JOYqCPDcVFZWCHEjyEqj8yzdFsQ49HzeqYmFDLbk,14521
83
+ modal/_runtime/user_code_imports.py,sha256=4fI0F9OIaNOcO_S4Tx2JcnYwZwZq6JdhMAkUzYN6je4,14629
84
84
  modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
85
85
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
86
86
  modal/_utils/async_utils.py,sha256=9ubwMkwiDB4gzOYG2jL9j7Fs-5dxHjcifZe3r7JRg-k,25091
87
- modal/_utils/blob_utils.py,sha256=0k_qUpO5GHnz538wjRhyRw4NdJ5O322N7QSilIu32jw,16601
88
- modal/_utils/function_utils.py,sha256=GV-mq6sSGXQIX5PcExYWJMaWY9YLjChjsiQjg-oPvm8,24902
89
- modal/_utils/grpc_testing.py,sha256=iqM9n5M0cWUUIIWNaEDer_pIfPnzXdZBO4L8FVbNepQ,8309
87
+ modal/_utils/blob_utils.py,sha256=1D_dXspFdsVxkW3gsYH-wJUUHiCpYvlwhmCgYZZgN9k,17237
88
+ modal/_utils/function_utils.py,sha256=LgcveUUb4XU_dWxtqgK_3ujZBvS3cGVzcDOkljyFZ2w,25066
89
+ modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
90
90
  modal/_utils/grpc_utils.py,sha256=PPB5ay-vXencXNIWPVw5modr3EH7gfq2QPcO5YJ1lMU,7737
91
- modal/_utils/hash_utils.py,sha256=LOWJ9U5Eaye2ZIGQOdEk8RN8ExPFovGQisyuGv1PXU0,2236
91
+ modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
92
92
  modal/_utils/http_utils.py,sha256=VKXYNPJtrSwZ1ttcXVGQUWmn8cLAXiOTv05g2ac3GbU,2179
93
93
  modal/_utils/logger.py,sha256=ePzdudrtx9jJCjuO6-bcL_kwUJfi4AwloUmIiNtqkY0,1330
94
94
  modal/_utils/mount_utils.py,sha256=J-FRZbPQv1i__Tob-FIpbB1oXWpFLAwZiB4OCiJpFG0,3206
@@ -161,10 +161,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
161
161
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
162
  modal_version/__init__.py,sha256=RT6zPoOdFO99u5Wcxxaoir4ZCuPTbQ22cvzFAXl3vUY,470
163
163
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
164
- modal_version/_version_generated.py,sha256=KGFfqnh_ipp1SMsIR7jjAwSJnOQ_TUXMsPTVZMuxRcs,148
165
- modal-0.68.9.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
166
- modal-0.68.9.dist-info/METADATA,sha256=Boz-j_G8TxxueTch5nd084dyB-x7_Ti0qNuYKjCGzFo,2328
167
- modal-0.68.9.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
168
- modal-0.68.9.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
169
- modal-0.68.9.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
170
- modal-0.68.9.dist-info/RECORD,,
164
+ modal_version/_version_generated.py,sha256=SWqJPFBaGp3gDChTb6Zx02oI_iQyIRtKpGgcw69Yegs,149
165
+ modal-0.68.14.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
166
+ modal-0.68.14.dist-info/METADATA,sha256=gHSGyvKb5gQiL1YbO3mRis3T8En0t19ug9mfOY4HYos,2329
167
+ modal-0.68.14.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
168
+ modal-0.68.14.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
169
+ modal-0.68.14.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
170
+ modal-0.68.14.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2024
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 9 # git: 02c0b6e
4
+ build_number = 14 # git: 45ed247