modal 0.73.114__py3-none-any.whl → 0.73.116__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/_serialization.py CHANGED
@@ -389,6 +389,12 @@ def check_valid_cls_constructor_arg(key, obj):
389
389
  )
390
390
 
391
391
 
392
+ def assert_bytes(obj: Any):
393
+ if not isinstance(obj, bytes):
394
+ raise TypeError(f"Expected bytes, got {type(obj)}")
395
+ return obj
396
+
397
+
392
398
  @dataclass
393
399
  class ParamTypeInfo:
394
400
  default_field: str
@@ -396,16 +402,27 @@ class ParamTypeInfo:
396
402
  converter: typing.Callable[[str], typing.Any]
397
403
 
398
404
 
399
- PARAM_TYPE_MAPPING = {
405
+ PYTHON_TO_PROTO_TYPE: dict[type, "api_pb2.ParameterType.ValueType"] = {
406
+ # python type -> protobuf type enum
407
+ str: api_pb2.PARAM_TYPE_STRING,
408
+ int: api_pb2.PARAM_TYPE_INT,
409
+ bytes: api_pb2.PARAM_TYPE_BYTES,
410
+ }
411
+
412
+ PROTO_TYPE_INFO = {
413
+ # Protobuf type enum -> encode/decode helper metadata
400
414
  api_pb2.PARAM_TYPE_STRING: ParamTypeInfo(default_field="string_default", proto_field="string_value", converter=str),
401
415
  api_pb2.PARAM_TYPE_INT: ParamTypeInfo(default_field="int_default", proto_field="int_value", converter=int),
416
+ api_pb2.PARAM_TYPE_BYTES: ParamTypeInfo(
417
+ default_field="bytes_default", proto_field="bytes_value", converter=assert_bytes
418
+ ),
402
419
  }
403
420
 
404
421
 
405
422
  def serialize_proto_params(python_params: dict[str, Any], schema: typing.Sequence[api_pb2.ClassParameterSpec]) -> bytes:
406
423
  proto_params: list[api_pb2.ClassParameterValue] = []
407
424
  for schema_param in schema:
408
- type_info = PARAM_TYPE_MAPPING.get(schema_param.type)
425
+ type_info = PROTO_TYPE_INFO.get(schema_param.type)
409
426
  if not type_info:
410
427
  raise ValueError(f"Unsupported parameter type: {schema_param.type}")
411
428
  proto_param = api_pb2.ClassParameterValue(
@@ -429,6 +446,8 @@ def serialize_proto_params(python_params: dict[str, Any], schema: typing.Sequenc
429
446
 
430
447
 
431
448
  def deserialize_proto_params(serialized_params: bytes, schema: list[api_pb2.ClassParameterSpec]) -> dict[str, Any]:
449
+ # TODO: this currently requires the schema to decode a payload, but we should make the validation
450
+ # distinct from the deserialization
432
451
  proto_struct = api_pb2.ClassParameterSet()
433
452
  proto_struct.ParseFromString(serialized_params)
434
453
  value_by_name = {p.name: p for p in proto_struct.parameters}
@@ -449,6 +468,8 @@ def deserialize_proto_params(serialized_params: bytes, schema: list[api_pb2.Clas
449
468
  python_value = param_value.string_value
450
469
  elif schema_param.type == api_pb2.PARAM_TYPE_INT:
451
470
  python_value = param_value.int_value
471
+ elif schema_param.type == api_pb2.PARAM_TYPE_BYTES:
472
+ python_value = param_value.bytes_value
452
473
  else:
453
474
  # TODO(elias): based on `parameters` declared types, we could add support for
454
475
  # custom non proto types encoded as bytes in the proto, e.g. PARAM_TYPE_PYTHON_PICKLE
@@ -15,7 +15,7 @@ from synchronicity.exceptions import UserCodeException
15
15
  import modal_proto
16
16
  from modal_proto import api_pb2
17
17
 
18
- from .._serialization import deserialize, deserialize_data_format, serialize
18
+ from .._serialization import PROTO_TYPE_INFO, PYTHON_TO_PROTO_TYPE, deserialize, deserialize_data_format, serialize
19
19
  from .._traceback import append_modal_tb
20
20
  from ..config import config, logger
21
21
  from ..exception import (
@@ -38,13 +38,6 @@ class FunctionInfoType(Enum):
38
38
  NOTEBOOK = "notebook"
39
39
 
40
40
 
41
- # TODO(elias): Add support for quoted/str annotations
42
- CLASS_PARAM_TYPE_MAP: dict[type, tuple["api_pb2.ParameterType.ValueType", str]] = {
43
- str: (api_pb2.PARAM_TYPE_STRING, "string_default"),
44
- int: (api_pb2.PARAM_TYPE_INT, "int_default"),
45
- }
46
-
47
-
48
41
  class LocalFunctionError(InvalidError):
49
42
  """Raised if a function declared in a non-global scope is used in an impermissible way"""
50
43
 
@@ -284,7 +277,7 @@ class FunctionInfo:
284
277
  return api_pb2.ClassParameterInfo()
285
278
 
286
279
  # TODO(elias): Resolve circular dependencies... maybe we'll need some cls_utils module
287
- from modal.cls import _get_class_constructor_signature, _use_annotation_parameters
280
+ from modal.cls import _get_class_constructor_signature, _use_annotation_parameters, _validate_parameter_type
288
281
 
289
282
  if not _use_annotation_parameters(self.user_cls):
290
283
  return api_pb2.ClassParameterInfo(format=api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PICKLE)
@@ -296,12 +289,12 @@ class FunctionInfo:
296
289
  signature = _get_class_constructor_signature(self.user_cls)
297
290
  for param in signature.parameters.values():
298
291
  has_default = param.default is not param.empty
299
- if param.annotation not in CLASS_PARAM_TYPE_MAP:
300
- raise InvalidError("modal.parameter() currently only support str or int types")
301
- param_type, default_field = CLASS_PARAM_TYPE_MAP[param.annotation]
302
- class_param_spec = api_pb2.ClassParameterSpec(name=param.name, has_default=has_default, type=param_type)
292
+ _validate_parameter_type(self.user_cls.__name__, param.name, param.annotation)
293
+ proto_type = PYTHON_TO_PROTO_TYPE[param.annotation]
294
+ proto_type_info = PROTO_TYPE_INFO[proto_type]
295
+ class_param_spec = api_pb2.ClassParameterSpec(name=param.name, has_default=has_default, type=proto_type)
303
296
  if has_default:
304
- setattr(class_param_spec, default_field, param.default)
297
+ setattr(class_param_spec, proto_type_info.default_field, param.default)
305
298
  modal_parameters.append(class_param_spec)
306
299
 
307
300
  return api_pb2.ClassParameterInfo(
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 = "0.73.114",
34
+ version: str = "0.73.116",
35
35
  ): ...
36
36
  def is_closed(self) -> bool: ...
37
37
  @property
@@ -93,7 +93,7 @@ class Client:
93
93
  server_url: str,
94
94
  client_type: int,
95
95
  credentials: typing.Optional[tuple[str, str]],
96
- version: str = "0.73.114",
96
+ version: str = "0.73.116",
97
97
  ): ...
98
98
  def is_closed(self) -> bool: ...
99
99
  @property
modal/cls.py CHANGED
@@ -9,7 +9,6 @@ from typing import Any, Callable, Optional, TypeVar, Union
9
9
  from google.protobuf.message import Message
10
10
  from grpclib import GRPCError, Status
11
11
 
12
- from modal._utils.function_utils import CLASS_PARAM_TYPE_MAP, FunctionInfo
13
12
  from modal_proto import api_pb2
14
13
 
15
14
  from ._functions import _Function, _parse_retries
@@ -22,7 +21,7 @@ from ._partial_function import (
22
21
  )
23
22
  from ._resolver import Resolver
24
23
  from ._resources import convert_fn_config_to_resources_config
25
- from ._serialization import check_valid_cls_constructor_arg
24
+ from ._serialization import PYTHON_TO_PROTO_TYPE, check_valid_cls_constructor_arg
26
25
  from ._traceback import print_server_warnings
27
26
  from ._utils.async_utils import synchronize_api, synchronizer
28
27
  from ._utils.deprecation import deprecation_warning, renamed_parameter, warn_on_renamed_autoscaler_settings
@@ -133,6 +132,8 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
133
132
 
134
133
  if cls._is_local():
135
134
  partial_function = cls._method_partials[method_name]
135
+ from modal._utils.function_utils import FunctionInfo
136
+
136
137
  fun._info = FunctionInfo(
137
138
  # ugly - needed for .local() TODO (elias): Clean up!
138
139
  partial_function.raw_f,
@@ -361,6 +362,15 @@ class _Obj:
361
362
  Obj = synchronize_api(_Obj)
362
363
 
363
364
 
365
+ def _validate_parameter_type(cls_name: str, parameter_name: str, parameter_type: type):
366
+ if parameter_type not in PYTHON_TO_PROTO_TYPE:
367
+ type_name = getattr(parameter_type, "__name__", repr(parameter_type))
368
+ supported = ", ".join(parameter_type.__name__ for parameter_type in PYTHON_TO_PROTO_TYPE.keys())
369
+ raise InvalidError(
370
+ f"{cls_name}.{parameter_name}: {type_name} is not a supported parameter type. Use one of: {supported}"
371
+ )
372
+
373
+
364
374
  class _Cls(_Object, type_prefix="cs"):
365
375
  """
366
376
  Cls adds method pooling and [lifecycle hook](/docs/guide/lifecycle-functions) behavior
@@ -461,12 +471,8 @@ class _Cls(_Object, type_prefix="cs"):
461
471
 
462
472
  annotated_params = {k: t for k, t in annotations.items() if k in params}
463
473
  for k, t in annotated_params.items():
464
- if t not in CLASS_PARAM_TYPE_MAP:
465
- t_name = getattr(t, "__name__", repr(t))
466
- supported = ", ".join(t.__name__ for t in CLASS_PARAM_TYPE_MAP.keys())
467
- raise InvalidError(
468
- f"{user_cls.__name__}.{k}: {t_name} is not a supported parameter type. Use one of: {supported}"
469
- )
474
+ if t not in PYTHON_TO_PROTO_TYPE:
475
+ _validate_parameter_type(user_cls.__name__, k, t)
470
476
 
471
477
  @staticmethod
472
478
  def from_local(user_cls, app: "modal.app._App", class_service_function: _Function) -> "_Cls":
modal/cls.pyi CHANGED
@@ -109,6 +109,8 @@ class Obj:
109
109
  async def _aenter(self): ...
110
110
  def __getattr__(self, k): ...
111
111
 
112
+ def _validate_parameter_type(cls_name: str, parameter_name: str, parameter_type: type): ...
113
+
112
114
  class _Cls(modal._object._Object):
113
115
  _class_service_function: typing.Optional[modal._functions._Function]
114
116
  _options: typing.Optional[_ServiceOptions]
modal/image.py CHANGED
@@ -861,6 +861,8 @@ class _Image(_Object, type_prefix="im"):
861
861
 
862
862
  *Added in v0.67.28*: This method replaces the deprecated `modal.Mount.from_local_python_packages` pattern.
863
863
  """
864
+ if not all(isinstance(module, str) for module in modules):
865
+ raise InvalidError("Local Python modules must be specified as strings.")
864
866
  mount = _Mount._from_local_python_packages(*modules, ignore=ignore)
865
867
  img = self._add_mount_layer_or_copy(mount, copy=copy)
866
868
  img._added_python_source_set |= set(modules)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: modal
3
- Version: 0.73.114
3
+ Version: 0.73.116
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -13,7 +13,7 @@ modal/_proxy_tunnel.py,sha256=gnKyCfmVB7x2d1A6c-JDysNIP3kEFxmXzhcXhPrzPn0,1906
13
13
  modal/_pty.py,sha256=JZfPDDpzqICZqtyPI_oMJf_9w-p_lLNuzHhwhodUXio,1329
14
14
  modal/_resolver.py,sha256=RtoXoYzSllPlFu0D1vel_FWiEmDO7RyToiC2bxeN8ZY,6917
15
15
  modal/_resources.py,sha256=5qmcirXUI8dSH926nwkUaeX9H25mqYu9mXD_KuT79-o,1733
16
- modal/_serialization.py,sha256=NYSjM9FnbLXULuzpboVvPcFFHRyh3hn_AcSFXQCGPYc,19741
16
+ modal/_serialization.py,sha256=xqBzCNvzDofh3DSmXFbK3nFvpz6gi4JDEnsLhcW21uI,20554
17
17
  modal/_traceback.py,sha256=IZQzB3fVlUfMHOSyKUgw0H6qv4yHnpyq-XVCNZKfUdA,5023
18
18
  modal/_tunnel.py,sha256=zTBxBiuH1O22tS1OliAJdIsSmaZS8PlnifS_6S5z-mk,6320
19
19
  modal/_tunnel.pyi,sha256=JmmDYAy9F1FpgJ_hWx0xkom2nTOFQjn4mTPYlU3PFo4,1245
@@ -22,11 +22,11 @@ modal/app.py,sha256=ojhuLZuNZAQ1OsbDH0k6G4pm1W7bOIvZfXbaKlvQ-Ao,45622
22
22
  modal/app.pyi,sha256=tZFbcsu20SuvfB2puxCyuXLFNJ9bQulzag55rVpgZmc,26827
23
23
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
24
24
  modal/client.py,sha256=j9D3hNis1lfhnz9lVFGgJgowbH3PaGUzNKgHPWYG778,15372
25
- modal/client.pyi,sha256=h-8XDhigD3KSUiMGL2K0ORIWrVgvRzWxIgOsmXD4t9o,7661
25
+ modal/client.pyi,sha256=U8G30XH2gEmB8-0JSMqRSu_BKopRwvnPHXnNWfpSWtk,7661
26
26
  modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
27
27
  modal/cloud_bucket_mount.pyi,sha256=30T3K1a89l6wzmEJ_J9iWv9SknoGqaZDx59Xs-ZQcmk,1607
28
- modal/cls.py,sha256=JhDbaZZHN52lqA_roY1BCbcN9BvbkUcdXiM2Kg9lIc0,31717
29
- modal/cls.pyi,sha256=ZJUwtRaQBGlM6tphvnv49FHBVDSgttMdD_LnYyRSKJM,10302
28
+ modal/cls.py,sha256=jjhzmnGdmLaBTJzVfUM81B-DWU2Mm2Rdw-m2LCGYbE0,31955
29
+ modal/cls.pyi,sha256=odrnYkqtQRiVwrAc6Ly23_zMYojIDYPo2bwMeElz-Ms,10395
30
30
  modal/config.py,sha256=Zx7YsllgIJzMRKeIkaGSLLtMFV4kTUvGxpptnmqlP1U,11623
31
31
  modal/container_process.py,sha256=WTqLn01dJPVkPpwR_0w_JH96ceN5mV4TGtiu1ZR2RRA,6108
32
32
  modal/container_process.pyi,sha256=Hf0J5JyDdCCXBJSKx6gvkPOo0XrztCm78xzxamtzUjQ,2828
@@ -43,7 +43,7 @@ modal/file_pattern_matcher.py,sha256=trosX-Bp7dOubudN1bLLhRAoidWy1TcoaR4Pv8CedWw
43
43
  modal/functions.py,sha256=kcNHvqeGBxPI7Cgd57NIBBghkfbeFJzXO44WW0jSmao,325
44
44
  modal/functions.pyi,sha256=D-PDJfSbwqMDXdq7Bxu2ErZRENo-tRgu_zPoB-jl0OU,14377
45
45
  modal/gpu.py,sha256=Kbhs_u49FaC2Zi0TjCdrpstpRtT5eZgecynmQi5IZVE,6752
46
- modal/image.py,sha256=Q-zNKdDmn7HHRv8YW7cIVYD1lbsOiiRuWfTZqLjGu2U,92189
46
+ modal/image.py,sha256=6hDizLZvRIjRy2cMWGT06Kj1uqW_L5B9GR6x3AKr4v4,92341
47
47
  modal/image.pyi,sha256=DQ4DLOCPr6_yV7z4LS0bTY0rOyvQP9-dQOrzaW7pPG8,25260
48
48
  modal/io_streams.py,sha256=h5O2LmbRoT9l777z3TQhCAm-JF1r7avZ2ykXlejztDs,15163
49
49
  modal/io_streams.pyi,sha256=bJ7ZLmSmJ0nKoa6r4FJpbqvzdUVa0lEe0Fa-MMpMezU,5071
@@ -98,7 +98,7 @@ modal/_utils/blob_utils.py,sha256=RB1G6T7eC1Poe-O45qYLaxwCr2jkM-Q6Nexk1J3wk_w,14
98
98
  modal/_utils/bytes_io_segment_payload.py,sha256=uunxVJS4PE1LojF_UpURMzVK9GuvmYWRqQo_bxEj5TU,3385
99
99
  modal/_utils/deprecation.py,sha256=EXP1beU4pmEqEzWMLw6E3kUfNfpmNA_VOp6i0EHi93g,4856
100
100
  modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
101
- modal/_utils/function_utils.py,sha256=OqsmLKOqSenxkXiNSPUWpKqD5KotySa_Omw1tmt97K0,27380
101
+ modal/_utils/function_utils.py,sha256=Un9WXZCmLb0itbDDOn4q6hF6n1r5ayhxPDb6oMCkucE,27190
102
102
  modal/_utils/git_utils.py,sha256=qtUU6JAttF55ZxYq51y55OR58B0tDPZsZWK5dJe6W5g,3182
103
103
  modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
104
104
  modal/_utils/grpc_utils.py,sha256=wmMydVKN9YbugTwUXuOuzxbpzYvxkTDaFRxlBtIDE_0,8526
@@ -170,10 +170,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
170
170
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
171
171
  modal_version/__init__.py,sha256=wiJQ53c-OMs0Xf1UeXOxQ7FwlV1VzIjnX6o-pRYZ_Pk,470
172
172
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
173
- modal_version/_version_generated.py,sha256=ROjaB34WYOCoibKCJBxkpTX-8P-A4xP5WrbxQiinFKw,150
174
- modal-0.73.114.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
175
- modal-0.73.114.dist-info/METADATA,sha256=TNknT3Jat8Rfo28q8ua9i7qsXa4ZAXyjpsKU8jD7gl8,2453
176
- modal-0.73.114.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
177
- modal-0.73.114.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
178
- modal-0.73.114.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
179
- modal-0.73.114.dist-info/RECORD,,
173
+ modal_version/_version_generated.py,sha256=Dt0kKCL30ILeQYfIk21dDhOoQb1oTOnpgKttcNXZovw,150
174
+ modal-0.73.116.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
175
+ modal-0.73.116.dist-info/METADATA,sha256=vzU2LT0158plcQnfBuJbbg23vAPW1qNT7PL8Xhy5Ohc,2453
176
+ modal-0.73.116.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
177
+ modal-0.73.116.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
178
+ modal-0.73.116.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
179
+ modal-0.73.116.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 114 # git: 3e3f684
4
+ build_number = 116 # git: 829b239