modAL 1.2.3__tar.gz → 1.2.3.dev0__tar.gz

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 (200) hide show
  1. {modal-1.2.3 → modal-1.2.3.dev0}/PKG-INFO +1 -1
  2. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_functions.py +4 -10
  3. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_load_context.py +1 -2
  4. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_object.py +6 -12
  5. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_partial_function.py +1 -1
  6. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/async_utils.py +5 -15
  7. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/grpc_utils.py +24 -157
  8. {modal-1.2.3 → modal-1.2.3.dev0}/modal/app.py +21 -47
  9. {modal-1.2.3 → modal-1.2.3.dev0}/modal/app.pyi +4 -8
  10. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/run.py +1 -24
  11. {modal-1.2.3 → modal-1.2.3.dev0}/modal/client.py +9 -9
  12. {modal-1.2.3 → modal-1.2.3.dev0}/modal/client.pyi +17 -8
  13. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cloud_bucket_mount.py +1 -1
  14. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cls.py +4 -6
  15. {modal-1.2.3 → modal-1.2.3.dev0}/modal/config.py +0 -5
  16. {modal-1.2.3 → modal-1.2.3.dev0}/modal/functions.pyi +8 -8
  17. {modal-1.2.3 → modal-1.2.3.dev0}/modal/image.py +4 -1
  18. {modal-1.2.3 → modal-1.2.3.dev0}/modal/io_streams.py +32 -101
  19. {modal-1.2.3 → modal-1.2.3.dev0}/modal/io_streams.pyi +5 -30
  20. {modal-1.2.3 → modal-1.2.3.dev0}/modal/sandbox.py +21 -8
  21. {modal-1.2.3 → modal-1.2.3.dev0}/modal/sandbox.pyi +9 -0
  22. {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/PKG-INFO +1 -1
  23. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api.proto +0 -29
  24. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_grpc.py +0 -1
  25. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_pb2.py +1021 -1052
  26. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_pb2.pyi +6 -96
  27. {modal-1.2.3 → modal-1.2.3.dev0}/modal_version/__init__.py +1 -1
  28. {modal-1.2.3 → modal-1.2.3.dev0}/LICENSE +0 -0
  29. {modal-1.2.3 → modal-1.2.3.dev0}/README.md +0 -0
  30. {modal-1.2.3 → modal-1.2.3.dev0}/modal/__init__.py +0 -0
  31. {modal-1.2.3 → modal-1.2.3.dev0}/modal/__main__.py +0 -0
  32. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_billing.py +0 -0
  33. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_clustered_functions.py +0 -0
  34. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_clustered_functions.pyi +0 -0
  35. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_container_entrypoint.py +0 -0
  36. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_grpc_client.py +0 -0
  37. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_ipython.py +0 -0
  38. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_location.py +0 -0
  39. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_output.py +0 -0
  40. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_pty.py +0 -0
  41. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_resolver.py +0 -0
  42. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_resources.py +0 -0
  43. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/__init__.py +0 -0
  44. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/asgi.py +0 -0
  45. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/container_io_manager.py +0 -0
  46. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/container_io_manager.pyi +0 -0
  47. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/execution_context.py +0 -0
  48. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/execution_context.pyi +0 -0
  49. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  50. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/telemetry.py +0 -0
  51. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/user_code_imports.py +0 -0
  52. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_serialization.py +0 -0
  53. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_traceback.py +0 -0
  54. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_tunnel.py +0 -0
  55. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_tunnel.pyi +0 -0
  56. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_type_manager.py +0 -0
  57. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/__init__.py +0 -0
  58. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/app_utils.py +0 -0
  59. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/auth_token_manager.py +0 -0
  60. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/blob_utils.py +0 -0
  61. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/bytes_io_segment_payload.py +0 -0
  62. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/deprecation.py +0 -0
  63. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/docker_utils.py +0 -0
  64. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/function_utils.py +0 -0
  65. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/git_utils.py +0 -0
  66. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/grpc_testing.py +0 -0
  67. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/hash_utils.py +0 -0
  68. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/http_utils.py +0 -0
  69. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/jwt_utils.py +0 -0
  70. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/logger.py +0 -0
  71. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/mount_utils.py +0 -0
  72. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/name_utils.py +0 -0
  73. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/package_utils.py +0 -0
  74. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/pattern_utils.py +0 -0
  75. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/rand_pb_testing.py +0 -0
  76. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/shell_utils.py +0 -0
  77. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/task_command_router_client.py +0 -0
  78. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/time_utils.py +0 -0
  79. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_vendor/__init__.py +0 -0
  80. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  81. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_vendor/cloudpickle.py +0 -0
  82. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_vendor/tblib.py +0 -0
  83. {modal-1.2.3 → modal-1.2.3.dev0}/modal/_watcher.py +0 -0
  84. {modal-1.2.3 → modal-1.2.3.dev0}/modal/billing.py +0 -0
  85. {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2023.12.312.txt +0 -0
  86. {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2023.12.txt +0 -0
  87. {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2024.04.txt +0 -0
  88. {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2024.10.txt +0 -0
  89. {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2025.06.txt +0 -0
  90. {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/PREVIEW.txt +0 -0
  91. {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/README.md +0 -0
  92. {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/base-images.json +0 -0
  93. {modal-1.2.3 → modal-1.2.3.dev0}/modal/call_graph.py +0 -0
  94. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/__init__.py +0 -0
  95. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/_download.py +0 -0
  96. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/_traceback.py +0 -0
  97. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/app.py +0 -0
  98. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/cluster.py +0 -0
  99. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/config.py +0 -0
  100. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/container.py +0 -0
  101. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/dict.py +0 -0
  102. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/entry_point.py +0 -0
  103. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/environment.py +0 -0
  104. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/import_refs.py +0 -0
  105. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/launch.py +0 -0
  106. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/network_file_system.py +0 -0
  107. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/profile.py +0 -0
  108. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/__init__.py +0 -0
  109. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/launch_instance_ssh.py +0 -0
  110. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/run_jupyter.py +0 -0
  111. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/run_marimo.py +0 -0
  112. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/vscode.py +0 -0
  113. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/queues.py +0 -0
  114. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/secret.py +0 -0
  115. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/token.py +0 -0
  116. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/utils.py +0 -0
  117. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/volume.py +0 -0
  118. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cloud_bucket_mount.pyi +0 -0
  119. {modal-1.2.3 → modal-1.2.3.dev0}/modal/cls.pyi +0 -0
  120. {modal-1.2.3 → modal-1.2.3.dev0}/modal/container_process.py +0 -0
  121. {modal-1.2.3 → modal-1.2.3.dev0}/modal/container_process.pyi +0 -0
  122. {modal-1.2.3 → modal-1.2.3.dev0}/modal/dict.py +0 -0
  123. {modal-1.2.3 → modal-1.2.3.dev0}/modal/dict.pyi +0 -0
  124. {modal-1.2.3 → modal-1.2.3.dev0}/modal/environments.py +0 -0
  125. {modal-1.2.3 → modal-1.2.3.dev0}/modal/environments.pyi +0 -0
  126. {modal-1.2.3 → modal-1.2.3.dev0}/modal/exception.py +0 -0
  127. {modal-1.2.3 → modal-1.2.3.dev0}/modal/experimental/__init__.py +0 -0
  128. {modal-1.2.3 → modal-1.2.3.dev0}/modal/experimental/flash.py +0 -0
  129. {modal-1.2.3 → modal-1.2.3.dev0}/modal/experimental/flash.pyi +0 -0
  130. {modal-1.2.3 → modal-1.2.3.dev0}/modal/experimental/ipython.py +0 -0
  131. {modal-1.2.3 → modal-1.2.3.dev0}/modal/file_io.py +0 -0
  132. {modal-1.2.3 → modal-1.2.3.dev0}/modal/file_io.pyi +0 -0
  133. {modal-1.2.3 → modal-1.2.3.dev0}/modal/file_pattern_matcher.py +0 -0
  134. {modal-1.2.3 → modal-1.2.3.dev0}/modal/functions.py +0 -0
  135. {modal-1.2.3 → modal-1.2.3.dev0}/modal/gpu.py +0 -0
  136. {modal-1.2.3 → modal-1.2.3.dev0}/modal/image.pyi +0 -0
  137. {modal-1.2.3 → modal-1.2.3.dev0}/modal/mount.py +0 -0
  138. {modal-1.2.3 → modal-1.2.3.dev0}/modal/mount.pyi +0 -0
  139. {modal-1.2.3 → modal-1.2.3.dev0}/modal/network_file_system.py +0 -0
  140. {modal-1.2.3 → modal-1.2.3.dev0}/modal/network_file_system.pyi +0 -0
  141. {modal-1.2.3 → modal-1.2.3.dev0}/modal/object.py +0 -0
  142. {modal-1.2.3 → modal-1.2.3.dev0}/modal/object.pyi +0 -0
  143. {modal-1.2.3 → modal-1.2.3.dev0}/modal/output.py +0 -0
  144. {modal-1.2.3 → modal-1.2.3.dev0}/modal/parallel_map.py +0 -0
  145. {modal-1.2.3 → modal-1.2.3.dev0}/modal/parallel_map.pyi +0 -0
  146. {modal-1.2.3 → modal-1.2.3.dev0}/modal/partial_function.py +0 -0
  147. {modal-1.2.3 → modal-1.2.3.dev0}/modal/partial_function.pyi +0 -0
  148. {modal-1.2.3 → modal-1.2.3.dev0}/modal/proxy.py +0 -0
  149. {modal-1.2.3 → modal-1.2.3.dev0}/modal/proxy.pyi +0 -0
  150. {modal-1.2.3 → modal-1.2.3.dev0}/modal/py.typed +0 -0
  151. {modal-1.2.3 → modal-1.2.3.dev0}/modal/queue.py +0 -0
  152. {modal-1.2.3 → modal-1.2.3.dev0}/modal/queue.pyi +0 -0
  153. {modal-1.2.3 → modal-1.2.3.dev0}/modal/retries.py +0 -0
  154. {modal-1.2.3 → modal-1.2.3.dev0}/modal/runner.py +0 -0
  155. {modal-1.2.3 → modal-1.2.3.dev0}/modal/runner.pyi +0 -0
  156. {modal-1.2.3 → modal-1.2.3.dev0}/modal/running_app.py +0 -0
  157. {modal-1.2.3 → modal-1.2.3.dev0}/modal/schedule.py +0 -0
  158. {modal-1.2.3 → modal-1.2.3.dev0}/modal/scheduler_placement.py +0 -0
  159. {modal-1.2.3 → modal-1.2.3.dev0}/modal/secret.py +0 -0
  160. {modal-1.2.3 → modal-1.2.3.dev0}/modal/secret.pyi +0 -0
  161. {modal-1.2.3 → modal-1.2.3.dev0}/modal/serving.py +0 -0
  162. {modal-1.2.3 → modal-1.2.3.dev0}/modal/serving.pyi +0 -0
  163. {modal-1.2.3 → modal-1.2.3.dev0}/modal/snapshot.py +0 -0
  164. {modal-1.2.3 → modal-1.2.3.dev0}/modal/snapshot.pyi +0 -0
  165. {modal-1.2.3 → modal-1.2.3.dev0}/modal/stream_type.py +0 -0
  166. {modal-1.2.3 → modal-1.2.3.dev0}/modal/token_flow.py +0 -0
  167. {modal-1.2.3 → modal-1.2.3.dev0}/modal/token_flow.pyi +0 -0
  168. {modal-1.2.3 → modal-1.2.3.dev0}/modal/volume.py +0 -0
  169. {modal-1.2.3 → modal-1.2.3.dev0}/modal/volume.pyi +0 -0
  170. {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/SOURCES.txt +0 -0
  171. {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/dependency_links.txt +0 -0
  172. {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/entry_points.txt +0 -0
  173. {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/requires.txt +0 -0
  174. {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/top_level.txt +0 -0
  175. {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/__init__.py +0 -0
  176. {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/gen_cli_docs.py +0 -0
  177. {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/gen_reference_docs.py +0 -0
  178. {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/mdmd/__init__.py +0 -0
  179. {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/mdmd/mdmd.py +0 -0
  180. {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/mdmd/signatures.py +0 -0
  181. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/__init__.py +0 -0
  182. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_pb2_grpc.py +0 -0
  183. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_pb2_grpc.pyi +0 -0
  184. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/modal_api_grpc.py +0 -0
  185. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/py.typed +0 -0
  186. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router.proto +0 -0
  187. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_grpc.py +0 -0
  188. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_pb2.py +0 -0
  189. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_pb2.pyi +0 -0
  190. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
  191. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
  192. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router.proto +0 -0
  193. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_grpc.py +0 -0
  194. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_pb2.py +0 -0
  195. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_pb2.pyi +0 -0
  196. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  197. {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  198. {modal-1.2.3 → modal-1.2.3.dev0}/modal_version/__main__.py +0 -0
  199. {modal-1.2.3 → modal-1.2.3.dev0}/pyproject.toml +0 -0
  200. {modal-1.2.3 → modal-1.2.3.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.3
3
+ Version: 1.2.3.dev0
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -90,6 +90,7 @@ from .parallel_map import (
90
90
  from .proxy import _Proxy
91
91
  from .retries import Retries, RetryManager
92
92
  from .schedule import Schedule
93
+ from .scheduler_placement import SchedulerPlacement
93
94
  from .secret import _Secret
94
95
  from .volume import _Volume
95
96
 
@@ -536,7 +537,6 @@ class _InputPlaneInvocation:
536
537
  async def _get_metadata(input_plane_region: str, client: _Client) -> list[tuple[str, str]]:
537
538
  if not input_plane_region:
538
539
  return []
539
- assert client._auth_token_manager, "Client is not open"
540
540
  token = await client._auth_token_manager.get_token()
541
541
  return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
542
542
 
@@ -589,7 +589,7 @@ class _FunctionSpec:
589
589
  cpu: Optional[Union[float, tuple[float, float]]]
590
590
  memory: Optional[Union[int, tuple[int, int]]]
591
591
  ephemeral_disk: Optional[int]
592
- scheduler_placement: Optional[api_pb2.SchedulerPlacement]
592
+ scheduler_placement: Optional[SchedulerPlacement]
593
593
  proxy: Optional[_Proxy]
594
594
 
595
595
 
@@ -683,8 +683,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
683
683
  batch_max_size: Optional[int] = None,
684
684
  batch_wait_ms: Optional[int] = None,
685
685
  cloud: Optional[str] = None,
686
- region: Optional[Union[str, Sequence[str]]] = None,
687
- nonpreemptible: bool = False,
686
+ scheduler_placement: Optional[SchedulerPlacement] = None,
688
687
  is_builder_function: bool = False,
689
688
  is_auto_snapshot: bool = False,
690
689
  enable_memory_snapshot: bool = False,
@@ -747,11 +746,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
747
746
  if env:
748
747
  secrets = [*secrets, _Secret.from_dict(env)]
749
748
 
750
- scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
751
- if region or nonpreemptible:
752
- regions = [region] if isinstance(region, str) else (list(region) if region else None)
753
- scheduler_placement = api_pb2.SchedulerPlacement(regions=regions, nonpreemptible=nonpreemptible)
754
-
755
749
  function_spec = _FunctionSpec(
756
750
  mounts=all_mounts,
757
751
  secrets=secrets,
@@ -1024,7 +1018,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1024
1018
  untrusted=restrict_modal_access,
1025
1019
  max_inputs=max_inputs or 0,
1026
1020
  cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
1027
- scheduler_placement=scheduler_placement,
1021
+ scheduler_placement=scheduler_placement.proto if scheduler_placement else None,
1028
1022
  is_class=info.is_service_class(),
1029
1023
  class_parameter_info=info.class_parameter_info(),
1030
1024
  i6pn_enabled=i6pn_enabled,
@@ -66,9 +66,8 @@ class LoadContext:
66
66
 
67
67
  Returns a new instance (no in place mutation)"""
68
68
 
69
- is_valid_client = self._client is not None and not self._client._snapshotted
70
69
  return LoadContext(
71
- client=self.client if is_valid_client else await _Client.from_env(),
70
+ client=await _Client.from_env() if self._client is None else self.client,
72
71
  environment_name=self._environment_name or config.get("environment") or "",
73
72
  app_id=self._app_id,
74
73
  )
@@ -43,7 +43,7 @@ class _Object:
43
43
  _prefix_to_type: ClassVar[dict[str, type]] = {}
44
44
 
45
45
  # For constructors
46
- _load: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]] = None
46
+ _load: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]]
47
47
  _preload: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]]
48
48
  _rep: str
49
49
  _is_another_app: bool
@@ -308,17 +308,11 @@ class _Object:
308
308
  # memory snapshots capture references which must be rehydrated
309
309
  # on restore to handle staleness.
310
310
  logger.debug(f"rehydrating {self} after snapshot")
311
- if self._hydrate_lazily:
312
- logger.debug(f"reloading lazy {self} from server")
313
- self._is_hydrated = False # un-hydrate and re-resolve
314
- # we don't set an explicit Client here, relying on the default
315
- # env client to be applied by LoadContext.apply_default
316
- root_load_context = LoadContext.empty()
317
- resolver = Resolver()
318
- await resolver.load(typing.cast(_Object, self), root_load_context)
319
- else:
320
- logger.debug(f"reloading non-lazy {self} by replacing client")
321
- self._client = client or await _Client.from_env()
311
+ self._is_hydrated = False # un-hydrate and re-resolve
312
+ # Set the client on LoadContext before loading
313
+ root_load_context = LoadContext(client=client)
314
+ resolver = Resolver()
315
+ await resolver.load(typing.cast(_Object, self), root_load_context)
322
316
  self._is_rehydrated = True
323
317
  logger.debug(f"rehydrated {self} with client {id(self.client)}")
324
318
  elif not self._hydrate_lazily:
@@ -219,7 +219,7 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
219
219
  # of the type PartialFunction and this descriptor would be triggered when accessing it,
220
220
  #
221
221
  # However, modal classes are *actually* Cls instances (which isn't reflected in type checkers
222
- # due to Python's lack of type checking intersection types), so at runtime the Cls instance would
222
+ # due to Python's lack of type chekcing intersection types), so at runtime the Cls instance would
223
223
  # use its __getattr__ rather than this descriptor.
224
224
  assert self.raw_f is not None # Should only be relevant in a method context
225
225
  k = self.raw_f.__name__
@@ -1,14 +1,12 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import asyncio
3
3
  import concurrent.futures
4
- import contextlib
5
4
  import functools
6
5
  import inspect
7
6
  import itertools
8
7
  import sys
9
8
  import time
10
9
  import typing
11
- import warnings
12
10
  from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Iterable, Iterator
13
11
  from contextlib import asynccontextmanager
14
12
  from dataclasses import dataclass
@@ -136,8 +134,7 @@ class TaskContext:
136
134
  _loops: set[asyncio.Task]
137
135
 
138
136
  def __init__(self, grace: Optional[float] = None):
139
- self._grace = grace # grace is the time we want for tasks to finish before cancelling them
140
- self._cancellation_grace: float = 1.0 # extra graceperiod for the cancellation itself to "bubble up"
137
+ self._grace = grace
141
138
  self._loops = set()
142
139
 
143
140
  async def start(self):
@@ -169,29 +166,22 @@ class TaskContext:
169
166
  # still needs to be handled
170
167
  # (https://stackoverflow.com/a/63356323/2475114)
171
168
  if gather_future:
172
- with contextlib.suppress(asyncio.CancelledError):
169
+ try:
173
170
  await gather_future
171
+ except asyncio.CancelledError:
172
+ pass
174
173
 
175
- cancelled_tasks: list[asyncio.Task] = []
176
174
  for task in self._tasks:
177
175
  if task.done() and not task.cancelled():
178
176
  # Raise any exceptions if they happened.
179
177
  # Only tasks without a done_callback will still be present in self._tasks
180
178
  task.result()
181
179
 
182
- if task.done():
180
+ if task.done() or task in self._loops: # Note: Legacy code, we can probably cancel loops.
183
181
  continue
184
182
 
185
183
  # Cancel any remaining unfinished tasks.
186
184
  task.cancel()
187
- cancelled_tasks.append(task)
188
-
189
- cancellation_gather = asyncio.gather(*cancelled_tasks, return_exceptions=True)
190
- try:
191
- await asyncio.wait_for(cancellation_gather, timeout=self._cancellation_grace)
192
- except asyncio.TimeoutError:
193
- warnings.warn(f"Internal warning: Tasks did not cancel in a timely manner: {cancelled_tasks}")
194
-
195
185
  await asyncio.sleep(0) # wake up coroutines waiting for cancellations
196
186
 
197
187
  async def __aexit__(self, exc_type, value, tb):
@@ -9,8 +9,7 @@ import urllib.parse
9
9
  import uuid
10
10
  from collections.abc import AsyncIterator
11
11
  from dataclasses import dataclass, field
12
- from functools import cache
13
- from typing import Any, Optional, Sequence, TypeVar
12
+ from typing import Any, Optional, TypeVar
14
13
 
15
14
  import grpclib.client
16
15
  import grpclib.config
@@ -18,18 +17,14 @@ import grpclib.events
18
17
  import grpclib.protocol
19
18
  import grpclib.stream
20
19
  from google.protobuf.message import Message
21
- from google.protobuf.symbol_database import SymbolDatabase
22
20
  from grpclib import GRPCError, Status
23
- from grpclib.encoding.base import StatusDetailsCodecBase
24
21
  from grpclib.exceptions import StreamTerminatedError
25
22
  from grpclib.protocol import H2Protocol
26
23
 
27
24
  from modal.exception import AuthError, ConnectionError
28
- from modal_proto import api_pb2
29
25
  from modal_version import __version__
30
26
 
31
27
  from .._traceback import suppress_tb_frames
32
- from ..config import config
33
28
  from .async_utils import retry
34
29
  from .logger import logger
35
30
 
@@ -73,7 +68,6 @@ RETRYABLE_GRPC_STATUS_CODES = [
73
68
  Status.INTERNAL,
74
69
  Status.UNKNOWN,
75
70
  ]
76
- SERVER_RETRY_WARNING_TIME_INTERVAL = 30.0
77
71
 
78
72
 
79
73
  @dataclass
@@ -113,56 +107,6 @@ class ConnectionManager:
113
107
  self._channels.clear()
114
108
 
115
109
 
116
- @cache
117
- def _sym_db() -> SymbolDatabase:
118
- from google.protobuf.symbol_database import Default
119
-
120
- return Default()
121
-
122
-
123
- class CustomProtoStatusDetailsCodec(StatusDetailsCodecBase):
124
- """grpclib compatible details codec.
125
-
126
- The server can encode the details using `google.rpc.Status` using grpclib's default codec and this custom codec
127
- can decode it into a `api_pb2.RPCStatus`.
128
- """
129
-
130
- def encode(
131
- self,
132
- status: Status,
133
- message: Optional[str],
134
- details: Optional[Sequence[Message]],
135
- ) -> bytes:
136
- details_proto = api_pb2.RPCStatus(code=status.value, message=message or "")
137
- if details is not None:
138
- for detail in details:
139
- detail_container = details_proto.details.add()
140
- detail_container.Pack(detail)
141
- return details_proto.SerializeToString()
142
-
143
- def decode(
144
- self,
145
- status: Status,
146
- message: Optional[str],
147
- data: bytes,
148
- ) -> Any:
149
- sym_db = _sym_db()
150
- details_proto = api_pb2.RPCStatus.FromString(data)
151
-
152
- details = []
153
- for detail_container in details_proto.details:
154
- # If we do not know how to decode an emssage, we'll ignore it.
155
- with contextlib.suppress(Exception):
156
- msg_type = sym_db.GetSymbol(detail_container.TypeName())
157
- detail = msg_type()
158
- detail_container.Unpack(detail)
159
- details.append(detail)
160
- return details
161
-
162
-
163
- custom_detail_codec = CustomProtoStatusDetailsCodec()
164
-
165
-
166
110
  def create_channel(
167
111
  server_url: str,
168
112
  metadata: dict[str, str] = {},
@@ -181,7 +125,7 @@ def create_channel(
181
125
  )
182
126
 
183
127
  if o.scheme == "unix":
184
- channel = grpclib.client.Channel(path=o.path, config=config, status_details_codec=custom_detail_codec)
128
+ channel = grpclib.client.Channel(path=o.path, config=config) # probably pointless to use a pool ever
185
129
  elif o.scheme in ("http", "https"):
186
130
  target = o.netloc
187
131
  parts = target.split(":")
@@ -189,7 +133,7 @@ def create_channel(
189
133
  ssl = o.scheme.endswith("s")
190
134
  host = parts[0]
191
135
  port = int(parts[1]) if len(parts) == 2 else 443 if ssl else 80
192
- channel = grpclib.client.Channel(host, port, ssl=ssl, config=config, status_details_codec=custom_detail_codec)
136
+ channel = grpclib.client.Channel(host, port, ssl=ssl, config=config)
193
137
  else:
194
138
  raise Exception(f"Unknown scheme: {o.scheme}")
195
139
 
@@ -253,50 +197,6 @@ async def retry_transient_errors(
253
197
  return await _retry_transient_errors(fn, req, retry=Retry(max_retries=max_retries))
254
198
 
255
199
 
256
- def get_server_retry_policy(exc: Exception) -> Optional[api_pb2.RPCRetryPolicy]:
257
- """Get server retry policy."""
258
- if not isinstance(exc, GRPCError) or not exc.details:
259
- return None
260
-
261
- # Server should not set multiple retry instructions, but if there is more than one, pick the first one
262
- for entry in exc.details:
263
- if isinstance(entry, api_pb2.RPCRetryPolicy):
264
- return entry
265
- return None
266
-
267
-
268
- def process_exception_before_retry(
269
- exc: Exception,
270
- final_attempt: bool,
271
- fn_name: str,
272
- n_retries: int,
273
- delay: float,
274
- idempotency_key: str,
275
- ):
276
- """Process exception before retry, used by `_retry_transient_errors`."""
277
- with suppress_tb_frames(1):
278
- if final_attempt:
279
- logger.debug(
280
- f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})"
281
- )
282
- if isinstance(exc, OSError):
283
- raise ConnectionError(str(exc))
284
- elif isinstance(exc, asyncio.TimeoutError):
285
- raise ConnectionError(str(exc))
286
- else:
287
- raise exc
288
-
289
- if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
290
- # StreamTerminatedError are not properly raised in grpclib<=0.4.7
291
- # fixed in https://github.com/vmagamedov/grpclib/issues/185
292
- # TODO: update to newer version (>=0.4.8) once stable
293
- # Also be sure to remove the AttributeError from the set of exceptions
294
- # we handle in the retry logic once we drop this check!
295
- raise exc
296
-
297
- logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})")
298
-
299
-
300
200
  async def _retry_transient_errors(
301
201
  fn: typing.Union[
302
202
  "modal._grpc_client.UnaryUnaryWrapper[RequestType, ResponseType]",
@@ -319,15 +219,12 @@ async def _retry_transient_errors(
319
219
 
320
220
  delay = retry.base_delay
321
221
  n_retries = 0
322
- n_throttled_retries = 0
323
222
 
324
223
  status_codes = [*RETRYABLE_GRPC_STATUS_CODES, *retry.additional_status_codes]
325
224
 
326
225
  idempotency_key = str(uuid.uuid4())
327
226
 
328
227
  t0 = time.time()
329
- last_server_retry_warning_time = None
330
-
331
228
  if retry.total_timeout is not None:
332
229
  total_deadline = t0 + retry.total_timeout
333
230
  else:
@@ -339,77 +236,29 @@ async def _retry_transient_errors(
339
236
  attempt_metadata = [
340
237
  ("x-idempotency-key", idempotency_key),
341
238
  ("x-retry-attempt", str(n_retries)),
342
- ("x-throttle-retry-attempt", str(n_throttled_retries)),
343
239
  *metadata,
344
240
  ]
345
241
  if n_retries > 0:
346
242
  attempt_metadata.append(("x-retry-delay", str(time.time() - t0)))
347
- if n_throttled_retries > 0:
348
- attempt_metadata.append(("x-throttle-retry-delay", str(time.time() - t0)))
349
-
350
243
  timeouts = []
351
244
  if retry.attempt_timeout is not None:
352
245
  timeouts.append(retry.attempt_timeout)
353
- if total_deadline is not None:
246
+ if retry.total_timeout is not None and total_deadline is not None:
354
247
  timeouts.append(max(total_deadline - time.time(), retry.attempt_timeout_floor))
355
248
  if timeouts:
356
249
  timeout = min(timeouts) # In case the function provided both types of timeouts
357
250
  else:
358
251
  timeout = None
359
-
360
252
  try:
361
253
  with suppress_tb_frames(1):
362
254
  return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
363
255
  except (StreamTerminatedError, GRPCError, OSError, asyncio.TimeoutError, AttributeError) as exc:
364
- # Note that we only catch AttributeError to handle a specific case that works around a bug
365
- # in grpclib<=0.4.7. See above (search for `write_appdata`).
366
-
367
- # Server side instruction for retries
368
- max_throttle_wait: Optional[int] = config.get("max_throttle_wait")
369
- if (
370
- max_throttle_wait != 0
371
- and isinstance(exc, GRPCError)
372
- and (server_retry_policy := get_server_retry_policy(exc))
373
- ):
374
- server_delay = server_retry_policy.retry_after_secs
375
-
376
- now = time.time()
377
-
378
- # We check if the timeout will be reached **after** the sleep, so we can raise an error early
379
- # without needing to actually sleep.
380
- total_timeout_will_be_reached = (
381
- retry.total_timeout is not None and (now + server_delay - t0) >= retry.total_timeout
382
- )
383
- max_throttle_will_be_reached = (
384
- max_throttle_wait is not None and (now + server_delay - t0) >= max_throttle_wait
385
- )
386
- final_attempt = total_timeout_will_be_reached or max_throttle_will_be_reached
387
-
388
- with suppress_tb_frames(1):
389
- process_exception_before_retry(
390
- exc, final_attempt, fn.name, n_retries, server_delay, idempotency_key
391
- )
392
-
393
- now = time.time()
394
- if last_server_retry_warning_time is None or (
395
- now - last_server_retry_warning_time >= SERVER_RETRY_WARNING_TIME_INTERVAL
396
- ):
397
- last_server_retry_warning_time = now
398
- logger.warning(
399
- f"Warning: Received {exc.status} status: {exc.message}. "
400
- f"Will retry in {server_delay:0.2f} seconds."
401
- )
402
-
403
- n_throttled_retries += 1
404
- await asyncio.sleep(server_delay)
405
- continue
406
-
407
- # Client handles retry
408
256
  if isinstance(exc, GRPCError) and exc.status not in status_codes:
409
257
  if exc.status == Status.UNAUTHENTICATED:
410
258
  raise AuthError(exc.message)
411
259
  else:
412
260
  raise exc
261
+
413
262
  if retry.max_retries is not None and n_retries >= retry.max_retries:
414
263
  final_attempt = True
415
264
  elif total_deadline is not None and time.time() + delay + retry.attempt_timeout_floor >= total_deadline:
@@ -418,7 +267,25 @@ async def _retry_transient_errors(
418
267
  final_attempt = False
419
268
 
420
269
  with suppress_tb_frames(1):
421
- process_exception_before_retry(exc, final_attempt, fn.name, n_retries, delay, idempotency_key)
270
+ if final_attempt:
271
+ logger.debug(
272
+ f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
273
+ f"{total_deadline=} for {fn.name} ({idempotency_key[:8]})"
274
+ )
275
+ if isinstance(exc, OSError):
276
+ raise ConnectionError(str(exc))
277
+ elif isinstance(exc, asyncio.TimeoutError):
278
+ raise ConnectionError(str(exc))
279
+ else:
280
+ raise exc
281
+
282
+ if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
283
+ # StreamTerminatedError are not properly raised in grpclib<=0.4.7
284
+ # fixed in https://github.com/vmagamedov/grpclib/issues/185
285
+ # TODO: update to newer version (>=0.4.8) once stable
286
+ raise exc
287
+
288
+ logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn.name} ({idempotency_key[:8]})")
422
289
 
423
290
  n_retries += 1
424
291
 
@@ -719,7 +719,6 @@ class _App:
719
719
  ] = None, # Set this to True if it's a non-generator function returning a [sync/async] generator object
720
720
  cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
721
721
  region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
722
- nonpreemptible: bool = False, # Whether to run the function on a nonpreemptible instance.
723
722
  enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
724
723
  block_network: bool = False, # Whether to block network access
725
724
  restrict_modal_access: bool = False, # Whether to allow this function access to other Modal resources
@@ -732,6 +731,9 @@ class _App:
732
731
  include_source: Optional[bool] = None,
733
732
  experimental_options: Optional[dict[str, Any]] = None,
734
733
  # Parameters below here are experimental. Use with caution!
734
+ _experimental_scheduler_placement: Optional[
735
+ SchedulerPlacement
736
+ ] = None, # Experimental controls over fine-grained scheduling (alpha).
735
737
  _experimental_proxy_ip: Optional[str] = None, # IP address of proxy
736
738
  _experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
737
739
  _experimental_restrict_output: bool = False, # Don't use pickle for return values
@@ -741,8 +743,6 @@ class _App:
741
743
  container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
742
744
  allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
743
745
  _experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
744
- _experimental_scheduler_placement: Optional[SchedulerPlacement] = None, # Replaced in favor of
745
- # using `region` and `nonpreemptible`
746
746
  ) -> _FunctionDecoratorType:
747
747
  """Decorator to register a new Modal Function with this App."""
748
748
  if isinstance(_warn_parentheses_missing, _Image):
@@ -762,24 +762,6 @@ class _App:
762
762
  "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
763
763
  )
764
764
 
765
- if _experimental_scheduler_placement is not None:
766
- deprecation_warning(
767
- (2025, 11, 17),
768
- "The `_experimental_scheduler_placement` parameter is deprecated."
769
- " Please use the `region` and `nonpreemptible` parameters instead.",
770
- )
771
- if region is not None or nonpreemptible:
772
- raise InvalidError(
773
- "Cannot use `_experimental_scheduler_placement` together with "
774
- "`region` or `nonpreemptible` parameters."
775
- )
776
- # Extract regions and lifecycle from scheduler placement
777
- if _experimental_scheduler_placement.proto.regions:
778
- region = list(_experimental_scheduler_placement.proto.regions)
779
- if _experimental_scheduler_placement.proto._lifecycle:
780
- # Convert lifecycle to nonpreemptible: "on-demand" -> True, "spot" -> False
781
- nonpreemptible = _experimental_scheduler_placement.proto._lifecycle == "on-demand"
782
-
783
765
  secrets = secrets or []
784
766
  if env:
785
767
  secrets = [*secrets, _Secret.from_dict(env)]
@@ -789,7 +771,7 @@ class _App:
789
771
  def wrapped(
790
772
  f: Union[_PartialFunction, Callable[..., Any], None],
791
773
  ) -> _Function:
792
- nonlocal is_generator, cloud, serialized, region, nonpreemptible
774
+ nonlocal is_generator, cloud, serialized
793
775
 
794
776
  # Check if the decorated object is a class
795
777
  if inspect.isclass(f):
@@ -878,6 +860,12 @@ class _App:
878
860
  if is_generator is None:
879
861
  is_generator = inspect.isgeneratorfunction(raw_f) or inspect.isasyncgenfunction(raw_f)
880
862
 
863
+ scheduler_placement: Optional[SchedulerPlacement] = _experimental_scheduler_placement
864
+ if region:
865
+ if scheduler_placement:
866
+ raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
867
+ scheduler_placement = SchedulerPlacement(region=region)
868
+
881
869
  function = _Function.from_local(
882
870
  info,
883
871
  app=self,
@@ -904,13 +892,12 @@ class _App:
904
892
  timeout=timeout,
905
893
  startup_timeout=startup_timeout or timeout,
906
894
  cloud=cloud,
907
- region=region,
908
- nonpreemptible=nonpreemptible,
909
895
  webhook_config=webhook_config,
910
896
  enable_memory_snapshot=enable_memory_snapshot,
911
897
  block_network=block_network,
912
898
  restrict_modal_access=restrict_modal_access,
913
899
  max_inputs=max_inputs,
900
+ scheduler_placement=scheduler_placement,
914
901
  i6pn_enabled=i6pn_enabled,
915
902
  cluster_size=cluster_size, # Experimental: Clustered functions
916
903
  rdma=rdma,
@@ -963,7 +950,6 @@ class _App:
963
950
  startup_timeout: Optional[int] = None, # Maximum startup time in seconds with higher precedence than `timeout`.
964
951
  cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
965
952
  region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
966
- nonpreemptible: bool = False, # Whether to run the function on a non-preemptible instance.
967
953
  enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
968
954
  block_network: bool = False, # Whether to block network access
969
955
  restrict_modal_access: bool = False, # Whether to allow this class access to other Modal resources
@@ -974,6 +960,9 @@ class _App:
974
960
  include_source: Optional[bool] = None, # When `False`, don't automatically add the App source to the container.
975
961
  experimental_options: Optional[dict[str, Any]] = None,
976
962
  # Parameters below here are experimental. Use with caution!
963
+ _experimental_scheduler_placement: Optional[
964
+ SchedulerPlacement
965
+ ] = None, # Experimental controls over fine-grained scheduling (alpha).
977
966
  _experimental_proxy_ip: Optional[str] = None, # IP address of proxy
978
967
  _experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
979
968
  _experimental_restrict_output: bool = False, # Don't use pickle for return values
@@ -983,8 +972,6 @@ class _App:
983
972
  container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
984
973
  allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
985
974
  _experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
986
- _experimental_scheduler_placement: Optional[SchedulerPlacement] = None, # Replaced in favor of
987
- # using `region` and `nonpreemptible`
988
975
  ) -> Callable[[Union[CLS_T, _PartialFunction]], CLS_T]:
989
976
  """
990
977
  Decorator to register a new Modal [Cls](https://modal.com/docs/reference/modal.Cls) with this App.
@@ -992,6 +979,12 @@ class _App:
992
979
  if _warn_parentheses_missing:
993
980
  raise InvalidError("Did you forget parentheses? Suggestion: `@app.cls()`.")
994
981
 
982
+ scheduler_placement = _experimental_scheduler_placement
983
+ if region:
984
+ if scheduler_placement:
985
+ raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
986
+ scheduler_placement = SchedulerPlacement(region=region)
987
+
995
988
  if allow_concurrent_inputs is not None:
996
989
  deprecation_warning(
997
990
  (2025, 4, 9),
@@ -1000,24 +993,6 @@ class _App:
1000
993
  "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
1001
994
  )
1002
995
 
1003
- if _experimental_scheduler_placement is not None:
1004
- deprecation_warning(
1005
- (2025, 11, 17),
1006
- "The `_experimental_scheduler_placement` parameter is deprecated."
1007
- " Please use the `region` and `nonpreemptible` parameters instead.",
1008
- )
1009
- if region is not None or nonpreemptible:
1010
- raise InvalidError(
1011
- "Cannot use `_experimental_scheduler_placement` together with "
1012
- "`region` or `nonpreemptible` parameters."
1013
- )
1014
- # Extract regions and lifecycle from scheduler placement
1015
- if _experimental_scheduler_placement.proto.regions:
1016
- region = list(_experimental_scheduler_placement.proto.regions)
1017
- if _experimental_scheduler_placement.proto._lifecycle:
1018
- # Convert lifecycle to nonpreemptible: "on-demand" -> True, "spot" -> False
1019
- nonpreemptible = _experimental_scheduler_placement.proto._lifecycle == "on-demand"
1020
-
1021
996
  secrets = secrets or []
1022
997
  if env:
1023
998
  secrets = [*secrets, _Secret.from_dict(env)]
@@ -1111,12 +1086,11 @@ class _App:
1111
1086
  timeout=timeout,
1112
1087
  startup_timeout=startup_timeout or timeout,
1113
1088
  cloud=cloud,
1114
- region=region,
1115
- nonpreemptible=nonpreemptible,
1116
1089
  enable_memory_snapshot=enable_memory_snapshot,
1117
1090
  block_network=block_network,
1118
1091
  restrict_modal_access=restrict_modal_access,
1119
1092
  max_inputs=max_inputs,
1093
+ scheduler_placement=scheduler_placement,
1120
1094
  i6pn_enabled=i6pn_enabled,
1121
1095
  cluster_size=cluster_size,
1122
1096
  rdma=rdma,