modal 0.62.115__py3-none-any.whl → 0.72.13__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 +13 -9
  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 +402 -398
  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 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  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 +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  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 +3 -3
  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 +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  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 +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/cls.pyi CHANGED
@@ -1,5 +1,6 @@
1
+ import collections.abc
1
2
  import google.protobuf.message
2
- import modal._output
3
+ import inspect
3
4
  import modal.app
4
5
  import modal.client
5
6
  import modal.functions
@@ -16,167 +17,210 @@ import typing_extensions
16
17
 
17
18
  T = typing.TypeVar("T")
18
19
 
19
- class _Obj:
20
- _functions: typing.Dict[str, modal.functions._Function]
21
- _inited: bool
22
- _entered: bool
23
- _local_obj: typing.Any
24
- _local_obj_constr: typing.Union[typing.Callable[[], typing.Any], None]
25
-
26
- def __init__(self, user_cls: type, output_mgr: typing.Union[modal._output.OutputManager, None], base_functions: typing.Dict[str, modal.functions._Function], from_other_workspace: bool, options: typing.Union[modal_proto.api_pb2.FunctionOptions, None], args, kwargs):
27
- ...
28
-
29
- def get_obj(self):
30
- ...
31
-
32
- def get_local_obj(self):
33
- ...
34
-
35
- def enter(self):
36
- ...
20
+ def _use_annotation_parameters(user_cls: type) -> bool: ...
21
+ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
22
+ def _bind_instance_method(
23
+ service_function: modal.functions._Function, class_bound_method: modal.functions._Function
24
+ ): ...
37
25
 
26
+ class _Obj:
27
+ _cls: _Cls
28
+ _functions: dict[str, modal.functions._Function]
29
+ _has_entered: bool
30
+ _user_cls_instance: typing.Optional[typing.Any]
31
+ _args: tuple[typing.Any, ...]
32
+ _kwargs: dict[str, typing.Any]
33
+ _instance_service_function: typing.Optional[modal.functions._Function]
34
+
35
+ def _uses_common_service_function(self): ...
36
+ def __init__(
37
+ self,
38
+ cls: _Cls,
39
+ user_cls: typing.Optional[type],
40
+ options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
41
+ args,
42
+ kwargs,
43
+ ): ...
44
+ def _cached_service_function(self) -> modal.functions._Function: ...
45
+ def _get_parameter_values(self) -> dict[str, typing.Any]: ...
46
+ def _new_user_cls_instance(self): ...
47
+ async def keep_warm(self, warm_pool_size: int) -> None: ...
48
+ def _cached_user_cls_instance(self): ...
49
+ def _enter(self): ...
38
50
  @property
39
- def entered(self):
40
- ...
41
-
42
- @entered.setter
43
- def entered(self, val):
44
- ...
45
-
46
- async def aenter(self):
47
- ...
48
-
49
- def __getattr__(self, k):
50
- ...
51
-
51
+ def _entered(self) -> bool: ...
52
+ @_entered.setter
53
+ def _entered(self, val: bool): ...
54
+ async def _aenter(self): ...
55
+ def __getattr__(self, k): ...
52
56
 
53
57
  class Obj:
54
- _functions: typing.Dict[str, modal.functions.Function]
55
- _inited: bool
56
- _entered: bool
57
- _local_obj: typing.Any
58
- _local_obj_constr: typing.Union[typing.Callable[[], typing.Any], None]
59
-
60
- def __init__(self, user_cls: type, output_mgr: typing.Union[modal._output.OutputManager, None], base_functions: typing.Dict[str, modal.functions.Function], from_other_workspace: bool, options: typing.Union[modal_proto.api_pb2.FunctionOptions, None], args, kwargs):
61
- ...
62
-
63
- def get_obj(self):
64
- ...
65
-
66
- def get_local_obj(self):
67
- ...
68
-
69
- def enter(self):
70
- ...
71
-
58
+ _cls: Cls
59
+ _functions: dict[str, modal.functions.Function]
60
+ _has_entered: bool
61
+ _user_cls_instance: typing.Optional[typing.Any]
62
+ _args: tuple[typing.Any, ...]
63
+ _kwargs: dict[str, typing.Any]
64
+ _instance_service_function: typing.Optional[modal.functions.Function]
65
+
66
+ def __init__(
67
+ self,
68
+ cls: Cls,
69
+ user_cls: typing.Optional[type],
70
+ options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
71
+ args,
72
+ kwargs,
73
+ ): ...
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]: ...
77
+ def _new_user_cls_instance(self): ...
78
+
79
+ class __keep_warm_spec(typing_extensions.Protocol):
80
+ def __call__(self, warm_pool_size: int) -> None: ...
81
+ async def aio(self, warm_pool_size: int) -> None: ...
82
+
83
+ keep_warm: __keep_warm_spec
84
+
85
+ def _cached_user_cls_instance(self): ...
86
+ def _enter(self): ...
72
87
  @property
73
- def entered(self):
74
- ...
75
-
76
- @entered.setter
77
- def entered(self, val):
78
- ...
79
-
80
- async def aenter(self):
81
- ...
82
-
83
- def __getattr__(self, k):
84
- ...
85
-
88
+ def _entered(self) -> bool: ...
89
+ @_entered.setter
90
+ def _entered(self, val: bool): ...
91
+ async def _aenter(self): ...
92
+ def __getattr__(self, k): ...
86
93
 
87
94
  class _Cls(modal.object._Object):
88
- _user_cls: typing.Union[type, None]
89
- _functions: typing.Dict[str, modal.functions._Function]
90
- _options: typing.Union[modal_proto.api_pb2.FunctionOptions, None]
91
- _callables: typing.Dict[str, typing.Callable]
92
- _from_other_workspace: typing.Union[bool, None]
93
- _app: typing.Union[modal.app._App, None]
94
-
95
- def _initialize_from_empty(self):
96
- ...
97
-
98
- def _initialize_from_other(self, other: _Cls):
99
- ...
100
-
101
- def _set_output_mgr(self, output_mgr: modal._output.OutputManager):
102
- ...
103
-
104
- def _hydrate_metadata(self, metadata: google.protobuf.message.Message):
105
- ...
106
-
107
- def _get_metadata(self) -> modal_proto.api_pb2.ClassHandleMetadata:
108
- ...
109
-
95
+ _user_cls: typing.Optional[type]
96
+ _class_service_function: typing.Optional[modal.functions._Function]
97
+ _method_functions: typing.Optional[dict[str, modal.functions._Function]]
98
+ _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
99
+ _callables: dict[str, typing.Callable[..., typing.Any]]
100
+ _app: typing.Optional[modal.app._App]
101
+ _name: typing.Optional[str]
102
+
103
+ def _initialize_from_empty(self): ...
104
+ def _initialize_from_other(self, other: _Cls): ...
105
+ def _get_partial_functions(self) -> dict[str, modal.partial_function._PartialFunction]: ...
106
+ def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
110
107
  @staticmethod
111
- def from_local(user_cls, app, decorator: typing.Callable[[modal.partial_function.PartialFunction, type], modal.functions._Function]) -> _Cls:
112
- ...
113
-
108
+ def validate_construction_mechanism(user_cls): ...
109
+ @staticmethod
110
+ def from_local(user_cls, app: modal.app._App, class_service_function: modal.functions._Function) -> _Cls: ...
111
+ def _uses_common_service_function(self): ...
114
112
  @classmethod
115
- def from_name(cls: typing.Type[_Cls], app_name: str, tag: typing.Union[str, None] = None, namespace=1, environment_name: typing.Union[str, None] = None, workspace: typing.Union[str, None] = None) -> _Cls:
116
- ...
117
-
118
- def with_options(self: _Cls, cpu: typing.Union[float, None] = None, memory: typing.Union[int, typing.Tuple[int, int], None] = None, gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None, secrets: typing.Collection[modal.secret._Secret] = (), volumes: typing.Dict[typing.Union[str, os.PathLike], modal.volume._Volume] = {}, retries: typing.Union[int, modal.retries.Retries, None] = None, timeout: typing.Union[int, None] = None, concurrency_limit: typing.Union[int, None] = None, allow_concurrent_inputs: typing.Union[int, None] = None, container_idle_timeout: typing.Union[int, None] = None, allow_background_volume_commits: bool = False) -> _Cls:
119
- ...
120
-
113
+ def from_name(
114
+ cls: type[_Cls],
115
+ app_name: str,
116
+ name: str,
117
+ namespace=1,
118
+ environment_name: typing.Optional[str] = None,
119
+ workspace: typing.Optional[str] = None,
120
+ ) -> _Cls: ...
121
+ def with_options(
122
+ self: _Cls,
123
+ cpu: typing.Union[float, tuple[float, float], None] = None,
124
+ memory: typing.Union[int, tuple[int, int], None] = None,
125
+ gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
126
+ secrets: collections.abc.Collection[modal.secret._Secret] = (),
127
+ volumes: dict[typing.Union[str, os.PathLike], modal.volume._Volume] = {},
128
+ retries: typing.Union[int, modal.retries.Retries, None] = None,
129
+ timeout: typing.Optional[int] = None,
130
+ concurrency_limit: typing.Optional[int] = None,
131
+ allow_concurrent_inputs: typing.Optional[int] = None,
132
+ container_idle_timeout: typing.Optional[int] = None,
133
+ ) -> _Cls: ...
121
134
  @staticmethod
122
- async def lookup(app_name: str, tag: typing.Union[str, None] = None, namespace=1, client: typing.Union[modal.client._Client, None] = None, environment_name: typing.Union[str, None] = None, workspace: typing.Union[str, None] = None) -> _Cls:
123
- ...
124
-
125
- def __call__(self, *args, **kwargs) -> _Obj:
126
- ...
127
-
128
- def __getattr__(self, k):
129
- ...
130
-
135
+ async def lookup(
136
+ app_name: str,
137
+ name: str,
138
+ namespace=1,
139
+ client: typing.Optional[modal.client._Client] = None,
140
+ environment_name: typing.Optional[str] = None,
141
+ workspace: typing.Optional[str] = None,
142
+ ) -> _Cls: ...
143
+ def __call__(self, *args, **kwargs) -> _Obj: ...
144
+ def __getattr__(self, k): ...
131
145
 
132
146
  class Cls(modal.object.Object):
133
- _user_cls: typing.Union[type, None]
134
- _functions: typing.Dict[str, modal.functions.Function]
135
- _options: typing.Union[modal_proto.api_pb2.FunctionOptions, None]
136
- _callables: typing.Dict[str, typing.Callable]
137
- _from_other_workspace: typing.Union[bool, None]
138
- _app: typing.Union[modal.app.App, None]
139
-
140
- def __init__(self, *args, **kwargs):
141
- ...
142
-
143
- def _initialize_from_empty(self):
144
- ...
145
-
146
- def _initialize_from_other(self, other: Cls):
147
- ...
148
-
149
- def _set_output_mgr(self, output_mgr: modal._output.OutputManager):
150
- ...
151
-
152
- def _hydrate_metadata(self, metadata: google.protobuf.message.Message):
153
- ...
154
-
155
- def _get_metadata(self) -> modal_proto.api_pb2.ClassHandleMetadata:
156
- ...
157
-
147
+ _user_cls: typing.Optional[type]
148
+ _class_service_function: typing.Optional[modal.functions.Function]
149
+ _method_functions: typing.Optional[dict[str, modal.functions.Function]]
150
+ _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
151
+ _callables: dict[str, typing.Callable[..., typing.Any]]
152
+ _app: typing.Optional[modal.app.App]
153
+ _name: typing.Optional[str]
154
+
155
+ def __init__(self, *args, **kwargs): ...
156
+ def _initialize_from_empty(self): ...
157
+ def _initialize_from_other(self, other: Cls): ...
158
+ def _get_partial_functions(self) -> dict[str, modal.partial_function.PartialFunction]: ...
159
+ def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
158
160
  @staticmethod
159
- def from_local(user_cls, app, decorator: typing.Callable[[modal.partial_function.PartialFunction, type], modal.functions.Function]) -> Cls:
160
- ...
161
-
161
+ def validate_construction_mechanism(user_cls): ...
162
+ @staticmethod
163
+ def from_local(user_cls, app: modal.app.App, class_service_function: modal.functions.Function) -> Cls: ...
164
+ def _uses_common_service_function(self): ...
162
165
  @classmethod
163
- def from_name(cls: typing.Type[Cls], app_name: str, tag: typing.Union[str, None] = None, namespace=1, environment_name: typing.Union[str, None] = None, workspace: typing.Union[str, None] = None) -> Cls:
164
- ...
165
-
166
- def with_options(self: Cls, cpu: typing.Union[float, None] = None, memory: typing.Union[int, typing.Tuple[int, int], None] = None, gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None, secrets: typing.Collection[modal.secret.Secret] = (), volumes: typing.Dict[typing.Union[str, os.PathLike], modal.volume.Volume] = {}, retries: typing.Union[int, modal.retries.Retries, None] = None, timeout: typing.Union[int, None] = None, concurrency_limit: typing.Union[int, None] = None, allow_concurrent_inputs: typing.Union[int, None] = None, container_idle_timeout: typing.Union[int, None] = None, allow_background_volume_commits: bool = False) -> Cls:
167
- ...
166
+ def from_name(
167
+ cls: type[Cls],
168
+ app_name: str,
169
+ name: str,
170
+ namespace=1,
171
+ environment_name: typing.Optional[str] = None,
172
+ workspace: typing.Optional[str] = None,
173
+ ) -> Cls: ...
174
+ def with_options(
175
+ self: Cls,
176
+ cpu: typing.Union[float, tuple[float, float], None] = None,
177
+ memory: typing.Union[int, tuple[int, int], None] = None,
178
+ gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
179
+ secrets: collections.abc.Collection[modal.secret.Secret] = (),
180
+ volumes: dict[typing.Union[str, os.PathLike], modal.volume.Volume] = {},
181
+ retries: typing.Union[int, modal.retries.Retries, None] = None,
182
+ timeout: typing.Optional[int] = None,
183
+ concurrency_limit: typing.Optional[int] = None,
184
+ allow_concurrent_inputs: typing.Optional[int] = None,
185
+ container_idle_timeout: typing.Optional[int] = None,
186
+ ) -> Cls: ...
168
187
 
169
188
  class __lookup_spec(typing_extensions.Protocol):
170
- def __call__(self, app_name: str, tag: typing.Union[str, None] = None, namespace=1, client: typing.Union[modal.client.Client, None] = None, environment_name: typing.Union[str, None] = None, workspace: typing.Union[str, None] = None) -> Cls:
171
- ...
172
-
173
- async def aio(self, *args, **kwargs) -> Cls:
174
- ...
189
+ def __call__(
190
+ self,
191
+ app_name: str,
192
+ name: str,
193
+ namespace=1,
194
+ client: typing.Optional[modal.client.Client] = None,
195
+ environment_name: typing.Optional[str] = None,
196
+ workspace: typing.Optional[str] = None,
197
+ ) -> Cls: ...
198
+ async def aio(
199
+ self,
200
+ app_name: str,
201
+ name: str,
202
+ namespace=1,
203
+ client: typing.Optional[modal.client.Client] = None,
204
+ environment_name: typing.Optional[str] = None,
205
+ workspace: typing.Optional[str] = None,
206
+ ) -> Cls: ...
175
207
 
176
208
  lookup: __lookup_spec
177
209
 
178
- def __call__(self, *args, **kwargs) -> Obj:
179
- ...
210
+ def __call__(self, *args, **kwargs) -> Obj: ...
211
+ def __getattr__(self, k): ...
212
+
213
+ class _NO_DEFAULT:
214
+ def __repr__(self): ...
215
+
216
+ _no_default: _NO_DEFAULT
217
+
218
+ class _Parameter:
219
+ default: typing.Any
220
+ init: bool
221
+
222
+ def __init__(self, default: typing.Any, init: bool): ...
223
+ def __get__(self, obj, obj_type=None) -> typing.Any: ...
180
224
 
181
- def __getattr__(self, k):
182
- ...
225
+ def is_parameter(p: typing.Any) -> bool: ...
226
+ def parameter(*, default: typing.Any = modal.cls._NO_DEFAULT(), init: bool = True) -> typing.Any: ...
modal/config.py CHANGED
@@ -29,7 +29,7 @@ Setting tokens using the CLI
29
29
 
30
30
  You can set a token by running the command::
31
31
 
32
- ```bash
32
+ ```
33
33
  modal token set \
34
34
  --token-id <token id> \
35
35
  --token-secret <token secret>
@@ -55,6 +55,11 @@ Other possible configuration options are:
55
55
  Defaults to True.
56
56
  By default, Modal automatically mounts modules imported in the current scope, that
57
57
  are deemed to be "local". This can be turned off by setting this to False.
58
+ * `force_build` (in the .toml file) / `MODAL_FORCE_BUILD` (as an env var).
59
+ Defaults to False.
60
+ When set, ignores the Image cache and builds all Image layers. Note that this
61
+ will break the cache for all images based on the rebuilt layers, so other images
62
+ may rebuild on subsequent runs / deploys even if the config is reverted.
58
63
  * `traceback` (in the .toml file) / `MODAL_TRACEBACK` (as an env var).
59
64
  Defaults to False. Enables printing full tracebacks on unexpected CLI
60
65
  errors, which can be useful for debugging client issues.
@@ -75,14 +80,15 @@ import os
75
80
  import typing
76
81
  import warnings
77
82
  from textwrap import dedent
78
- from typing import Any, Dict, Optional
83
+ from typing import Any, Optional
79
84
 
80
85
  from google.protobuf.empty_pb2 import Empty
81
86
 
82
87
  from modal_proto import api_pb2
83
88
 
89
+ from ._utils.deprecation import deprecation_error
84
90
  from ._utils.logger import configure_logger
85
- from .exception import InvalidError, deprecation_warning
91
+ from .exception import InvalidError
86
92
 
87
93
  # Locate config file and read it
88
94
 
@@ -98,14 +104,25 @@ def _is_remote() -> bool:
98
104
 
99
105
 
100
106
  def _read_user_config():
107
+ config_data = {}
101
108
  if not _is_remote() and os.path.exists(user_config_path):
102
109
  # Defer toml import so we don't need it in the container runtime environment
103
110
  import toml
104
111
 
105
- with open(user_config_path) as f:
106
- return toml.load(f)
107
- else:
108
- return {}
112
+ try:
113
+ with open(user_config_path) as f:
114
+ config_data = toml.load(f)
115
+ except Exception as exc:
116
+ config_problem = str(exc)
117
+ else:
118
+ if not all(isinstance(e, dict) for e in config_data.values()):
119
+ config_problem = "TOML file must contain table sections for each profile."
120
+ else:
121
+ config_problem = ""
122
+ if config_problem:
123
+ message = f"\nError when reading the modal configuration from `{user_config_path}`.\n\n{config_problem}"
124
+ raise InvalidError(message)
125
+ return config_data
109
126
 
110
127
 
111
128
  _user_config = _read_user_config()
@@ -161,11 +178,9 @@ def _check_config() -> None:
161
178
  Support for using an implicit 'default' profile is deprecated.
162
179
  Please use `modal profile activate` to activate one of your profiles.
163
180
  (Use `modal profile list` to see the options.)
164
-
165
- This will become an error in a future update.
166
181
  """
167
182
  )
168
- deprecation_warning((2024, 2, 6), message, show_source=False)
183
+ deprecation_error((2024, 2, 6), message)
169
184
 
170
185
 
171
186
  _profile = os.environ.get("MODAL_PROFILE") or _config_active_profile()
@@ -189,16 +204,15 @@ _SETTINGS = {
189
204
  "token_id": _Setting(),
190
205
  "token_secret": _Setting(),
191
206
  "task_id": _Setting(),
192
- "task_secret": _Setting(),
193
207
  "serve_timeout": _Setting(transform=float),
194
208
  "sync_entrypoint": _Setting(),
195
209
  "logs_timeout": _Setting(10, float),
196
210
  "image_id": _Setting(),
197
211
  "automount": _Setting(True, transform=_to_boolean),
198
- "profiling_enabled": _Setting(False, transform=_to_boolean),
199
212
  "heartbeat_interval": _Setting(15, float),
200
213
  "function_runtime": _Setting(),
201
214
  "function_runtime_debug": _Setting(False, transform=_to_boolean), # For internal debugging use.
215
+ "runtime_perf_record": _Setting(False, transform=_to_boolean), # For internal debugging use.
202
216
  "environment": _Setting(),
203
217
  "default_cloud": _Setting(None, transform=lambda x: x if x else None),
204
218
  "worker_id": _Setting(), # For internal debugging use.
@@ -206,6 +220,9 @@ _SETTINGS = {
206
220
  "force_build": _Setting(False, transform=_to_boolean),
207
221
  "traceback": _Setting(False, transform=_to_boolean),
208
222
  "image_builder_version": _Setting(),
223
+ "strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
224
+ "snapshot_debug": _Setting(False, transform=_to_boolean),
225
+ "client_retries": _Setting(False, transform=_to_boolean), # For internal testing.
209
226
  }
210
227
 
211
228
 
@@ -243,7 +260,7 @@ class Config:
243
260
  os.environ["MODAL_" + key.upper()] = value
244
261
  except KeyError:
245
262
  # Override env vars not available in config, e.g. NVIDIA_VISIBLE_DEVICES.
246
- # This is used for restoring env vars from a checkpoint.
263
+ # This is used for restoring env vars from a memory snapshot.
247
264
  os.environ[key.upper()] = value
248
265
 
249
266
  def __getitem__(self, key):
@@ -253,7 +270,7 @@ class Config:
253
270
  return repr(self.to_dict())
254
271
 
255
272
  def to_dict(self):
256
- return {key: self.get(key) for key in _SETTINGS.keys()}
273
+ return {key: self.get(key) for key in sorted(_SETTINGS)}
257
274
 
258
275
 
259
276
  config = Config()
@@ -267,7 +284,7 @@ configure_logger(logger, config["loglevel"], config["log_format"])
267
284
 
268
285
 
269
286
  def _store_user_config(
270
- new_settings: Dict[str, Any], profile: Optional[str] = None, active_profile: Optional[str] = None
287
+ new_settings: dict[str, Any], profile: Optional[str] = None, active_profile: Optional[str] = None
271
288
  ):
272
289
  """Internal method, used by the CLI to set tokens."""
273
290
  if profile is None:
@@ -0,0 +1,177 @@
1
+ # Copyright Modal Labs 2024
2
+ import asyncio
3
+ import platform
4
+ from typing import Generic, Optional, TypeVar
5
+
6
+ from modal_proto import api_pb2
7
+
8
+ from ._utils.async_utils import TaskContext, synchronize_api
9
+ from ._utils.deprecation import deprecation_error
10
+ from ._utils.grpc_utils import retry_transient_errors
11
+ from ._utils.shell_utils import stream_from_stdin, write_to_fd
12
+ from .client import _Client
13
+ from .exception import InteractiveTimeoutError, InvalidError
14
+ from .io_streams import _StreamReader, _StreamWriter
15
+ from .stream_type import StreamType
16
+
17
+ T = TypeVar("T", str, bytes)
18
+
19
+
20
+ class _ContainerProcess(Generic[T]):
21
+ _process_id: Optional[str] = None
22
+ _stdout: _StreamReader[T]
23
+ _stderr: _StreamReader[T]
24
+ _stdin: _StreamWriter
25
+ _text: bool
26
+ _by_line: bool
27
+ _returncode: Optional[int] = None
28
+
29
+ def __init__(
30
+ self,
31
+ process_id: str,
32
+ client: _Client,
33
+ stdout: StreamType = StreamType.PIPE,
34
+ stderr: StreamType = StreamType.PIPE,
35
+ text: bool = True,
36
+ by_line: bool = False,
37
+ ) -> None:
38
+ self._process_id = process_id
39
+ self._client = client
40
+ self._text = text
41
+ self._by_line = by_line
42
+ self._stdout = _StreamReader[T](
43
+ api_pb2.FILE_DESCRIPTOR_STDOUT,
44
+ process_id,
45
+ "container_process",
46
+ self._client,
47
+ stream_type=stdout,
48
+ text=text,
49
+ by_line=by_line,
50
+ )
51
+ self._stderr = _StreamReader[T](
52
+ api_pb2.FILE_DESCRIPTOR_STDERR,
53
+ process_id,
54
+ "container_process",
55
+ self._client,
56
+ stream_type=stderr,
57
+ text=text,
58
+ by_line=by_line,
59
+ )
60
+ self._stdin = _StreamWriter(process_id, "container_process", self._client)
61
+
62
+ @property
63
+ def stdout(self) -> _StreamReader[T]:
64
+ """StreamReader for the container process's stdout stream."""
65
+ return self._stdout
66
+
67
+ @property
68
+ def stderr(self) -> _StreamReader[T]:
69
+ """StreamReader for the container process's stderr stream."""
70
+ return self._stderr
71
+
72
+ @property
73
+ def stdin(self) -> _StreamWriter:
74
+ """StreamWriter for the container process's stdin stream."""
75
+ return self._stdin
76
+
77
+ @property
78
+ def returncode(self) -> int:
79
+ if self._returncode is None:
80
+ raise InvalidError(
81
+ "You must call wait() before accessing the returncode. "
82
+ "To poll for the status of a running process, use poll() instead."
83
+ )
84
+ return self._returncode
85
+
86
+ async def poll(self) -> Optional[int]:
87
+ """Check if the container process has finished running.
88
+
89
+ Returns `None` if the process is still running, else returns the exit code.
90
+ """
91
+ if self._returncode is not None:
92
+ return self._returncode
93
+
94
+ req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=0)
95
+ resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(self._client.stub.ContainerExecWait, req)
96
+
97
+ if resp.completed:
98
+ self._returncode = resp.exit_code
99
+ return self._returncode
100
+
101
+ return None
102
+
103
+ async def wait(self) -> int:
104
+ """Wait for the container process to finish running. Returns the exit code."""
105
+
106
+ if self._returncode is not None:
107
+ return self._returncode
108
+
109
+ while True:
110
+ req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=50)
111
+ resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(
112
+ self._client.stub.ContainerExecWait, req
113
+ )
114
+ if resp.completed:
115
+ self._returncode = resp.exit_code
116
+ return self._returncode
117
+
118
+ async def attach(self, *, pty: Optional[bool] = None):
119
+ if platform.system() == "Windows":
120
+ print("interactive exec is not currently supported on Windows.")
121
+ return
122
+
123
+ if pty is not None:
124
+ deprecation_error(
125
+ (2024, 12, 9),
126
+ "The `pty` argument to `modal.container_process.attach(pty=...)` is deprecated, "
127
+ "as only PTY mode is supported. Please remove the argument.",
128
+ )
129
+
130
+ from rich.console import Console
131
+
132
+ console = Console()
133
+
134
+ connecting_status = console.status("Connecting...")
135
+ connecting_status.start()
136
+ on_connect = asyncio.Event()
137
+
138
+ async def _write_to_fd_loop(stream: _StreamReader):
139
+ # Don't skip empty messages so we can detect when the process has booted.
140
+ async for chunk in stream._get_logs(skip_empty_messages=False):
141
+ if chunk is None:
142
+ break
143
+
144
+ if not on_connect.is_set():
145
+ connecting_status.stop()
146
+ on_connect.set()
147
+
148
+ await write_to_fd(stream.file_descriptor, chunk)
149
+
150
+ async def _handle_input(data: bytes, message_index: int):
151
+ self.stdin.write(data)
152
+ await self.stdin.drain()
153
+
154
+ async with TaskContext() as tc:
155
+ stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
156
+ stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
157
+
158
+ try:
159
+ # time out if we can't connect to the server fast enough
160
+ await asyncio.wait_for(on_connect.wait(), timeout=60)
161
+
162
+ async with stream_from_stdin(_handle_input, use_raw_terminal=True):
163
+ await stdout_task
164
+ await stderr_task
165
+
166
+ # TODO: this doesn't work right now.
167
+ # if exit_status != 0:
168
+ # raise ExecutionError(f"Process exited with status code {exit_status}")
169
+
170
+ except (asyncio.TimeoutError, TimeoutError):
171
+ connecting_status.stop()
172
+ stdout_task.cancel()
173
+ stderr_task.cancel()
174
+ raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
175
+
176
+
177
+ ContainerProcess = synchronize_api(_ContainerProcess)