modAL 1.2.1.dev22__tar.gz → 1.2.2__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 (201) hide show
  1. {modal-1.2.1.dev22 → modal-1.2.2}/PKG-INFO +2 -2
  2. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_clustered_functions.py +1 -3
  3. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_functions.py +106 -97
  4. modal-1.2.2/modal/_grpc_client.py +171 -0
  5. modal-1.2.2/modal/_load_context.py +105 -0
  6. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_object.py +47 -18
  7. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_output.py +3 -4
  8. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_resolver.py +21 -35
  9. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/container_io_manager.py +21 -22
  10. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/async_utils.py +12 -3
  11. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/auth_token_manager.py +1 -4
  12. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/blob_utils.py +3 -4
  13. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/function_utils.py +1 -2
  14. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/grpc_utils.py +80 -51
  15. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/mount_utils.py +26 -1
  16. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/task_command_router_client.py +3 -4
  17. {modal-1.2.1.dev22 → modal-1.2.2}/modal/app.py +10 -4
  18. {modal-1.2.1.dev22 → modal-1.2.2}/modal/app.pyi +3 -0
  19. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/config.py +3 -1
  20. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/container.py +1 -2
  21. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/dict.py +5 -2
  22. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/network_file_system.py +1 -4
  23. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/queues.py +5 -4
  24. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/secret.py +1 -2
  25. {modal-1.2.1.dev22 → modal-1.2.2}/modal/client.py +5 -115
  26. {modal-1.2.1.dev22 → modal-1.2.2}/modal/client.pyi +2 -99
  27. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cloud_bucket_mount.py +2 -0
  28. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cloud_bucket_mount.pyi +4 -0
  29. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cls.py +71 -33
  30. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cls.pyi +3 -0
  31. {modal-1.2.1.dev22 → modal-1.2.2}/modal/config.py +1 -1
  32. {modal-1.2.1.dev22 → modal-1.2.2}/modal/container_process.py +4 -8
  33. {modal-1.2.1.dev22 → modal-1.2.2}/modal/dict.py +26 -17
  34. {modal-1.2.1.dev22 → modal-1.2.2}/modal/dict.pyi +2 -0
  35. {modal-1.2.1.dev22 → modal-1.2.2}/modal/environments.py +16 -8
  36. {modal-1.2.1.dev22 → modal-1.2.2}/modal/environments.pyi +6 -2
  37. {modal-1.2.1.dev22 → modal-1.2.2}/modal/experimental/__init__.py +2 -3
  38. {modal-1.2.1.dev22 → modal-1.2.2}/modal/experimental/flash.py +7 -12
  39. {modal-1.2.1.dev22 → modal-1.2.2}/modal/file_io.py +13 -27
  40. {modal-1.2.1.dev22 → modal-1.2.2}/modal/functions.pyi +10 -4
  41. {modal-1.2.1.dev22 → modal-1.2.2}/modal/image.py +47 -26
  42. {modal-1.2.1.dev22 → modal-1.2.2}/modal/image.pyi +13 -9
  43. {modal-1.2.1.dev22 → modal-1.2.2}/modal/io_streams.py +61 -91
  44. {modal-1.2.1.dev22 → modal-1.2.2}/modal/io_streams.pyi +33 -95
  45. {modal-1.2.1.dev22 → modal-1.2.2}/modal/mount.py +36 -26
  46. {modal-1.2.1.dev22 → modal-1.2.2}/modal/mount.pyi +33 -7
  47. {modal-1.2.1.dev22 → modal-1.2.2}/modal/network_file_system.py +19 -11
  48. {modal-1.2.1.dev22 → modal-1.2.2}/modal/network_file_system.pyi +12 -2
  49. {modal-1.2.1.dev22 → modal-1.2.2}/modal/object.pyi +35 -8
  50. {modal-1.2.1.dev22 → modal-1.2.2}/modal/parallel_map.py +29 -31
  51. {modal-1.2.1.dev22 → modal-1.2.2}/modal/parallel_map.pyi +3 -9
  52. {modal-1.2.1.dev22 → modal-1.2.2}/modal/proxy.py +14 -6
  53. {modal-1.2.1.dev22 → modal-1.2.2}/modal/proxy.pyi +10 -2
  54. {modal-1.2.1.dev22 → modal-1.2.2}/modal/queue.py +31 -23
  55. {modal-1.2.1.dev22 → modal-1.2.2}/modal/queue.pyi +12 -2
  56. {modal-1.2.1.dev22 → modal-1.2.2}/modal/runner.py +62 -58
  57. {modal-1.2.1.dev22 → modal-1.2.2}/modal/runner.pyi +4 -3
  58. {modal-1.2.1.dev22 → modal-1.2.2}/modal/sandbox.py +46 -60
  59. {modal-1.2.1.dev22 → modal-1.2.2}/modal/sandbox.pyi +0 -1
  60. {modal-1.2.1.dev22 → modal-1.2.2}/modal/secret.py +61 -44
  61. {modal-1.2.1.dev22 → modal-1.2.2}/modal/secret.pyi +21 -4
  62. {modal-1.2.1.dev22 → modal-1.2.2}/modal/serving.py +7 -11
  63. {modal-1.2.1.dev22 → modal-1.2.2}/modal/serving.pyi +7 -8
  64. {modal-1.2.1.dev22 → modal-1.2.2}/modal/snapshot.py +11 -8
  65. {modal-1.2.1.dev22 → modal-1.2.2}/modal/token_flow.py +1 -1
  66. {modal-1.2.1.dev22 → modal-1.2.2}/modal/volume.py +45 -29
  67. {modal-1.2.1.dev22 → modal-1.2.2}/modal/volume.pyi +2 -0
  68. {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/PKG-INFO +2 -2
  69. {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/SOURCES.txt +2 -0
  70. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api.proto +23 -9
  71. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_pb2.py +1031 -1015
  72. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_pb2.pyi +90 -19
  73. modal-1.2.2/modal_proto/modal_api_grpc.py +194 -0
  74. {modal-1.2.1.dev22 → modal-1.2.2}/modal_version/__init__.py +1 -1
  75. {modal-1.2.1.dev22 → modal-1.2.2}/pyproject.toml +1 -1
  76. modal-1.2.1.dev22/modal_proto/modal_api_grpc.py +0 -194
  77. {modal-1.2.1.dev22 → modal-1.2.2}/LICENSE +0 -0
  78. {modal-1.2.1.dev22 → modal-1.2.2}/README.md +0 -0
  79. {modal-1.2.1.dev22 → modal-1.2.2}/modal/__init__.py +0 -0
  80. {modal-1.2.1.dev22 → modal-1.2.2}/modal/__main__.py +0 -0
  81. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_billing.py +0 -0
  82. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_clustered_functions.pyi +0 -0
  83. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_container_entrypoint.py +0 -0
  84. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_ipython.py +0 -0
  85. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_location.py +0 -0
  86. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_partial_function.py +0 -0
  87. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_pty.py +0 -0
  88. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_resources.py +0 -0
  89. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/__init__.py +0 -0
  90. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/asgi.py +0 -0
  91. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/container_io_manager.pyi +0 -0
  92. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/execution_context.py +0 -0
  93. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/execution_context.pyi +0 -0
  94. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  95. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/telemetry.py +0 -0
  96. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/user_code_imports.py +0 -0
  97. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_serialization.py +0 -0
  98. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_traceback.py +0 -0
  99. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_tunnel.py +0 -0
  100. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_tunnel.pyi +0 -0
  101. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_type_manager.py +0 -0
  102. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/__init__.py +0 -0
  103. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/app_utils.py +0 -0
  104. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/bytes_io_segment_payload.py +0 -0
  105. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/deprecation.py +0 -0
  106. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/docker_utils.py +0 -0
  107. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/git_utils.py +0 -0
  108. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/grpc_testing.py +0 -0
  109. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/hash_utils.py +0 -0
  110. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/http_utils.py +0 -0
  111. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/jwt_utils.py +0 -0
  112. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/logger.py +0 -0
  113. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/name_utils.py +0 -0
  114. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/package_utils.py +0 -0
  115. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/pattern_utils.py +0 -0
  116. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/rand_pb_testing.py +0 -0
  117. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/shell_utils.py +0 -0
  118. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/time_utils.py +0 -0
  119. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_vendor/__init__.py +0 -0
  120. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  121. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_vendor/cloudpickle.py +0 -0
  122. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_vendor/tblib.py +0 -0
  123. {modal-1.2.1.dev22 → modal-1.2.2}/modal/_watcher.py +0 -0
  124. {modal-1.2.1.dev22 → modal-1.2.2}/modal/billing.py +0 -0
  125. {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2023.12.312.txt +0 -0
  126. {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2023.12.txt +0 -0
  127. {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2024.04.txt +0 -0
  128. {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2024.10.txt +0 -0
  129. {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2025.06.txt +0 -0
  130. {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/PREVIEW.txt +0 -0
  131. {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/README.md +0 -0
  132. {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/base-images.json +0 -0
  133. {modal-1.2.1.dev22 → modal-1.2.2}/modal/call_graph.py +0 -0
  134. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/__init__.py +0 -0
  135. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/_download.py +0 -0
  136. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/_traceback.py +0 -0
  137. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/app.py +0 -0
  138. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/cluster.py +0 -0
  139. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/entry_point.py +0 -0
  140. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/environment.py +0 -0
  141. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/import_refs.py +0 -0
  142. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/launch.py +0 -0
  143. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/profile.py +0 -0
  144. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/__init__.py +0 -0
  145. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/launch_instance_ssh.py +0 -0
  146. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/run_jupyter.py +0 -0
  147. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/run_marimo.py +0 -0
  148. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/vscode.py +0 -0
  149. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/run.py +0 -0
  150. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/token.py +0 -0
  151. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/utils.py +0 -0
  152. {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/volume.py +0 -0
  153. {modal-1.2.1.dev22 → modal-1.2.2}/modal/container_process.pyi +0 -0
  154. {modal-1.2.1.dev22 → modal-1.2.2}/modal/exception.py +0 -0
  155. {modal-1.2.1.dev22 → modal-1.2.2}/modal/experimental/flash.pyi +0 -0
  156. {modal-1.2.1.dev22 → modal-1.2.2}/modal/experimental/ipython.py +0 -0
  157. {modal-1.2.1.dev22 → modal-1.2.2}/modal/file_io.pyi +0 -0
  158. {modal-1.2.1.dev22 → modal-1.2.2}/modal/file_pattern_matcher.py +0 -0
  159. {modal-1.2.1.dev22 → modal-1.2.2}/modal/functions.py +0 -0
  160. {modal-1.2.1.dev22 → modal-1.2.2}/modal/gpu.py +0 -0
  161. {modal-1.2.1.dev22 → modal-1.2.2}/modal/object.py +0 -0
  162. {modal-1.2.1.dev22 → modal-1.2.2}/modal/output.py +0 -0
  163. {modal-1.2.1.dev22 → modal-1.2.2}/modal/partial_function.py +0 -0
  164. {modal-1.2.1.dev22 → modal-1.2.2}/modal/partial_function.pyi +0 -0
  165. {modal-1.2.1.dev22 → modal-1.2.2}/modal/py.typed +0 -0
  166. {modal-1.2.1.dev22 → modal-1.2.2}/modal/retries.py +0 -0
  167. {modal-1.2.1.dev22 → modal-1.2.2}/modal/running_app.py +0 -0
  168. {modal-1.2.1.dev22 → modal-1.2.2}/modal/schedule.py +0 -0
  169. {modal-1.2.1.dev22 → modal-1.2.2}/modal/scheduler_placement.py +0 -0
  170. {modal-1.2.1.dev22 → modal-1.2.2}/modal/snapshot.pyi +0 -0
  171. {modal-1.2.1.dev22 → modal-1.2.2}/modal/stream_type.py +0 -0
  172. {modal-1.2.1.dev22 → modal-1.2.2}/modal/token_flow.pyi +0 -0
  173. {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/dependency_links.txt +0 -0
  174. {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/entry_points.txt +0 -0
  175. {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/requires.txt +0 -0
  176. {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/top_level.txt +0 -0
  177. {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/__init__.py +0 -0
  178. {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/gen_cli_docs.py +0 -0
  179. {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/gen_reference_docs.py +0 -0
  180. {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/mdmd/__init__.py +0 -0
  181. {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/mdmd/mdmd.py +0 -0
  182. {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/mdmd/signatures.py +0 -0
  183. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/__init__.py +0 -0
  184. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_grpc.py +0 -0
  185. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_pb2_grpc.py +0 -0
  186. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_pb2_grpc.pyi +0 -0
  187. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/py.typed +0 -0
  188. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router.proto +0 -0
  189. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_grpc.py +0 -0
  190. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_pb2.py +0 -0
  191. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_pb2.pyi +0 -0
  192. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
  193. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
  194. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router.proto +0 -0
  195. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_grpc.py +0 -0
  196. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_pb2.py +0 -0
  197. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_pb2.pyi +0 -0
  198. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  199. {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  200. {modal-1.2.1.dev22 → modal-1.2.2}/modal_version/__main__.py +0 -0
  201. {modal-1.2.1.dev22 → modal-1.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.1.dev22
3
+ Version: 1.2.2
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 @@ Classifier: Topic :: System :: Distributed Computing
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: License :: OSI Approved :: Apache Software License
15
15
  Classifier: Programming Language :: Python :: 3
16
- Requires-Python: >=3.9
16
+ Requires-Python: <3.14,>=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: aiohttp
@@ -5,7 +5,6 @@ from dataclasses import dataclass
5
5
  from typing import Optional
6
6
 
7
7
  from modal._utils.async_utils import synchronize_api
8
- from modal._utils.grpc_utils import retry_transient_errors
9
8
  from modal.client import _Client
10
9
  from modal.exception import InvalidError
11
10
  from modal_proto import api_pb2
@@ -61,8 +60,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
61
60
  os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
62
61
 
63
62
  if world_size > 1:
64
- resp: api_pb2.TaskClusterHelloResponse = await retry_transient_errors(
65
- client.stub.TaskClusterHello,
63
+ resp = await client.stub.TaskClusterHello(
66
64
  api_pb2.TaskClusterHelloRequest(
67
65
  task_id=task_id,
68
66
  container_ip=container_ip,
@@ -19,7 +19,8 @@ from synchronicity.combined_types import MethodWithAio
19
19
  from modal_proto import api_pb2
20
20
  from modal_proto.modal_api_grpc import ModalClientModal
21
21
 
22
- from ._object import _get_environment_name, _Object, live_method, live_method_gen
22
+ from ._load_context import LoadContext
23
+ from ._object import _Object, live_method, live_method_gen
23
24
  from ._pty import get_pty_info
24
25
  from ._resolver import Resolver
25
26
  from ._resources import convert_fn_config_to_resources_config
@@ -53,7 +54,7 @@ from ._utils.function_utils import (
53
54
  get_function_type,
54
55
  is_async,
55
56
  )
56
- from ._utils.grpc_utils import RetryWarningMessage, retry_transient_errors
57
+ from ._utils.grpc_utils import Retry, RetryWarningMessage
57
58
  from ._utils.mount_utils import validate_network_file_systems, validate_volumes
58
59
  from .call_graph import InputInfo, _reconstruct_call_graph
59
60
  from .client import _Client
@@ -164,21 +165,22 @@ class _Invocation:
164
165
 
165
166
  if from_spawn_map:
166
167
  request.from_spawn_map = True
167
- response = await retry_transient_errors(
168
- client.stub.FunctionMap,
168
+ response = await client.stub.FunctionMap(
169
169
  request,
170
- max_retries=None,
171
- max_delay=30.0,
172
- retry_warning_message=RetryWarningMessage(
173
- message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
174
- "more function calls. This may be due to hitting rate limits or function backlog limits.",
175
- warning_interval=10,
176
- errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
170
+ retry=Retry(
171
+ max_retries=None,
172
+ max_delay=30.0,
173
+ warning_message=RetryWarningMessage(
174
+ message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
175
+ "more function calls. This may be due to hitting rate limits or function backlog limits.",
176
+ warning_interval=10,
177
+ errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
178
+ ),
179
+ additional_status_codes=[Status.RESOURCE_EXHAUSTED],
177
180
  ),
178
- additional_status_codes=[Status.RESOURCE_EXHAUSTED],
179
181
  )
180
182
  else:
181
- response = await retry_transient_errors(client.stub.FunctionMap, request)
183
+ response = await client.stub.FunctionMap(request)
182
184
 
183
185
  function_call_id = response.function_call_id
184
186
  if response.pipelined_inputs:
@@ -198,10 +200,7 @@ class _Invocation:
198
200
  request_put = api_pb2.FunctionPutInputsRequest(
199
201
  function_id=function_id, inputs=[item], function_call_id=function_call_id
200
202
  )
201
- inputs_response: api_pb2.FunctionPutInputsResponse = await retry_transient_errors(
202
- client.stub.FunctionPutInputs,
203
- request_put,
204
- )
203
+ inputs_response: api_pb2.FunctionPutInputsResponse = await client.stub.FunctionPutInputs(request_put)
205
204
  processed_inputs = inputs_response.inputs
206
205
  if not processed_inputs:
207
206
  raise Exception("Could not create function call - the input queue seems to be full")
@@ -243,10 +242,9 @@ class _Invocation:
243
242
  start_idx=index,
244
243
  end_idx=index,
245
244
  )
246
- response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
247
- self.stub.FunctionGetOutputs,
245
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
248
246
  request,
249
- attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD,
247
+ retry=Retry(attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD),
250
248
  )
251
249
 
252
250
  if len(response.outputs) > 0:
@@ -266,10 +264,7 @@ class _Invocation:
266
264
 
267
265
  item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
268
266
  request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
269
- await retry_transient_errors(
270
- self.stub.FunctionRetryInputs,
271
- request,
272
- )
267
+ await self.stub.FunctionRetryInputs(request)
273
268
 
274
269
  async def _get_single_output(self, expected_jwt: Optional[str] = None) -> api_pb2.FunctionGetOutputsItem:
275
270
  # waits indefinitely for a single result for the function, and clear the outputs buffer after
@@ -373,10 +368,8 @@ class _Invocation:
373
368
  start_idx=current_index,
374
369
  end_idx=batch_end_index,
375
370
  )
376
- response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
377
- self.stub.FunctionGetOutputs,
378
- request,
379
- attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD,
371
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
372
+ request, retry=Retry(attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD)
380
373
  )
381
374
 
382
375
  outputs = list(response.outputs)
@@ -448,7 +441,7 @@ class _InputPlaneInvocation:
448
441
  )
449
442
 
450
443
  metadata = await client.get_input_plane_metadata(input_plane_region)
451
- response = await retry_transient_errors(stub.AttemptStart, request, metadata=metadata)
444
+ response = await stub.AttemptStart(request, metadata=metadata)
452
445
  attempt_token = response.attempt_token
453
446
 
454
447
  return _InputPlaneInvocation(
@@ -468,10 +461,9 @@ class _InputPlaneInvocation:
468
461
  requested_at=time.time(),
469
462
  )
470
463
  metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
471
- await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
472
- self.stub.AttemptAwait,
464
+ await_response: api_pb2.AttemptAwaitResponse = await self.stub.AttemptAwait(
473
465
  await_request,
474
- attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
466
+ retry=Retry(attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD),
475
467
  metadata=metadata,
476
468
  )
477
469
 
@@ -511,11 +503,7 @@ class _InputPlaneInvocation:
511
503
  input=self.input_item,
512
504
  attempt_token=self.attempt_token,
513
505
  )
514
- retry_response = await retry_transient_errors(
515
- self.stub.AttemptRetry,
516
- retry_request,
517
- metadata=metadata,
518
- )
506
+ retry_response = await self.stub.AttemptRetry(retry_request, metadata=metadata)
519
507
  return retry_response.attempt_token
520
508
 
521
509
  async def run_generator(self):
@@ -669,7 +657,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
669
657
  @staticmethod
670
658
  def from_local(
671
659
  info: FunctionInfo,
672
- app,
660
+ app: Optional["modal.app._App"], # App here should only be None in case of Image.run_function
673
661
  image: _Image,
674
662
  env: Optional[dict[str, Optional[str]]] = None,
675
663
  secrets: Optional[Collection[_Secret]] = None,
@@ -895,12 +883,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
895
883
  is_web_endpoint, is_generator, restrict_output
896
884
  )
897
885
 
898
- async def _preload(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
899
- assert resolver.client and resolver.client.stub
900
-
901
- assert resolver.app_id
886
+ async def _preload(
887
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
888
+ ):
889
+ assert load_context.app_id
902
890
  req = api_pb2.FunctionPrecreateRequest(
903
- app_id=resolver.app_id,
891
+ app_id=load_context.app_id,
904
892
  function_name=info.function_name,
905
893
  function_type=function_type,
906
894
  existing_function_id=existing_object_id or "",
@@ -916,11 +904,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
916
904
  elif webhook_config:
917
905
  req.webhook_config.CopyFrom(webhook_config)
918
906
 
919
- response = await retry_transient_errors(resolver.client.stub.FunctionPrecreate, req)
920
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
907
+ response = await load_context.client.stub.FunctionPrecreate(req)
908
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
921
909
 
922
- async def _load(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
923
- assert resolver.client and resolver.client.stub
910
+ async def _load(
911
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
912
+ ):
924
913
  with FunctionCreationStatus(resolver, tag) as function_creation_status:
925
914
  timeout_secs = timeout
926
915
 
@@ -1116,18 +1105,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1116
1105
  ),
1117
1106
  )
1118
1107
 
1119
- assert resolver.app_id
1108
+ assert load_context.app_id
1120
1109
  assert (function_definition is None) != (function_data is None) # xor
1121
1110
  request = api_pb2.FunctionCreateRequest(
1122
- app_id=resolver.app_id,
1111
+ app_id=load_context.app_id,
1123
1112
  function=function_definition,
1124
1113
  function_data=function_data,
1125
1114
  existing_function_id=existing_object_id or "",
1126
1115
  )
1127
1116
  try:
1128
- response: api_pb2.FunctionCreateResponse = await retry_transient_errors(
1129
- resolver.client.stub.FunctionCreate, request
1130
- )
1117
+ response: api_pb2.FunctionCreateResponse = await load_context.client.stub.FunctionCreate(request)
1131
1118
  except GRPCError as exc:
1132
1119
  if exc.status == Status.INVALID_ARGUMENT:
1133
1120
  raise InvalidError(exc.message)
@@ -1142,10 +1129,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1142
1129
  serve_mounts = {m for m in all_mounts if m.is_local()}
1143
1130
  serve_mounts |= image._serve_mounts
1144
1131
  obj._serve_mounts = frozenset(serve_mounts)
1145
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
1132
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
1146
1133
 
1147
1134
  rep = f"Function({tag})"
1148
- obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps)
1135
+ # Pass a *reference* to the App's LoadContext - this is important since the App is
1136
+ # the only way to infer a LoadContext for an `@app.function`, and the App doesn't
1137
+ # get its client until *after* the Function is created.
1138
+ load_context = app._root_load_context if app else LoadContext.empty()
1139
+ obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps, load_context_overrides=load_context)
1149
1140
 
1150
1141
  obj._raw_f = info.raw_f
1151
1142
  obj._info = info
@@ -1187,7 +1178,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1187
1178
 
1188
1179
  parent = self
1189
1180
 
1190
- async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1181
+ async def _load(
1182
+ param_bound_func: _Function,
1183
+ resolver: Resolver,
1184
+ load_context: LoadContext,
1185
+ existing_object_id: Optional[str],
1186
+ ):
1191
1187
  if not parent.is_hydrated:
1192
1188
  # While the base Object.hydrate() method appears to be idempotent, it's not always safe
1193
1189
  await parent.hydrate()
@@ -1220,7 +1216,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1220
1216
  param_bound_func._hydrate_from_other(parent)
1221
1217
  return
1222
1218
 
1223
- environment_name = _get_environment_name(None, resolver)
1224
1219
  assert parent is not None and parent.is_hydrated
1225
1220
 
1226
1221
  if options:
@@ -1260,11 +1255,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1260
1255
  function_id=parent.object_id,
1261
1256
  serialized_params=serialized_params,
1262
1257
  function_options=options_pb,
1263
- environment_name=environment_name
1258
+ environment_name=load_context.environment_name
1264
1259
  or "", # TODO: investigate shouldn't environment name always be specified here?
1265
1260
  )
1266
1261
 
1267
- response = await retry_transient_errors(parent._client.stub.FunctionBindParams, req)
1262
+ response = await parent._client.stub.FunctionBindParams(req)
1268
1263
  param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
1269
1264
 
1270
1265
  def _deps():
@@ -1277,7 +1272,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1277
1272
  return [dep for dep in all_deps if not dep.is_hydrated]
1278
1273
  return []
1279
1274
 
1280
- fun: _Function = _Function._from_loader(_load, "Function(parametrized)", hydrate_lazily=True, deps=_deps)
1275
+ fun: _Function = _Function._from_loader(
1276
+ _load,
1277
+ "Function(parametrized)",
1278
+ hydrate_lazily=True,
1279
+ deps=_deps,
1280
+ load_context_overrides=self._load_context_overrides,
1281
+ )
1281
1282
 
1282
1283
  fun._info = self._info
1283
1284
  fun._obj = obj
@@ -1328,7 +1329,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1328
1329
  scaledown_window=scaledown_window,
1329
1330
  )
1330
1331
  request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
1331
- await retry_transient_errors(self.client.stub.FunctionUpdateSchedulingParams, request)
1332
+ await self.client.stub.FunctionUpdateSchedulingParams(request)
1332
1333
 
1333
1334
  # One idea would be for FunctionUpdateScheduleParams to return the current (coalesced) settings
1334
1335
  # and then we could return them here (would need some ad hoc dataclass, which I don't love)
@@ -1375,34 +1376,43 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1375
1376
  cls,
1376
1377
  app_name: str,
1377
1378
  name: str,
1378
- namespace=None, # mdmd:line-hidden
1379
- environment_name: Optional[str] = None,
1379
+ *,
1380
+ load_context_overrides: LoadContext,
1380
1381
  ):
1381
1382
  # internal function lookup implementation that allows lookup of class "service functions"
1382
1383
  # in addition to non-class functions
1383
- async def _load_remote(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1384
- assert resolver.client and resolver.client.stub
1384
+ async def _load_remote(
1385
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
1386
+ ):
1385
1387
  request = api_pb2.FunctionGetRequest(
1386
1388
  app_name=app_name,
1387
1389
  object_tag=name,
1388
- environment_name=_get_environment_name(environment_name, resolver) or "",
1390
+ environment_name=load_context.environment_name,
1389
1391
  )
1390
1392
  try:
1391
- response = await retry_transient_errors(resolver.client.stub.FunctionGet, request)
1393
+ response = await load_context.client.stub.FunctionGet(request)
1392
1394
  except NotFoundError as exc:
1393
1395
  # refine the error message
1394
- env_context = f" (in the '{environment_name}' environment)" if environment_name else ""
1396
+ env_context = (
1397
+ f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
1398
+ )
1395
1399
  raise NotFoundError(
1396
1400
  f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc}."
1397
1401
  ) from None
1398
1402
 
1399
1403
  print_server_warnings(response.server_warnings)
1400
1404
 
1401
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
1405
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
1402
1406
 
1403
- environment_rep = f", environment_name={environment_name!r}" if environment_name else ""
1407
+ environment_rep = (
1408
+ f", environment_name={load_context_overrides.environment_name!r}"
1409
+ if load_context_overrides._environment_name # slightly ugly - checking if _environment_name is overridden
1410
+ else ""
1411
+ )
1404
1412
  rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})"
1405
- return cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
1413
+ return cls._from_loader(
1414
+ _load_remote, rep, is_another_app=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
1415
+ )
1406
1416
 
1407
1417
  @classmethod
1408
1418
  def from_name(
@@ -1412,6 +1422,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1412
1422
  *,
1413
1423
  namespace=None, # mdmd:line-hidden
1414
1424
  environment_name: Optional[str] = None,
1425
+ client: Optional[_Client] = None,
1415
1426
  ) -> "_Function":
1416
1427
  """Reference a Function from a deployed App by its name.
1417
1428
 
@@ -1435,7 +1446,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1435
1446
  )
1436
1447
 
1437
1448
  warn_if_passing_namespace(namespace, "modal.Function.from_name")
1438
- return cls._from_name(app_name, name, environment_name=environment_name)
1449
+ return cls._from_name(
1450
+ app_name, name, load_context_overrides=LoadContext(environment_name=environment_name, client=client)
1451
+ )
1439
1452
 
1440
1453
  @property
1441
1454
  def tag(self) -> str:
@@ -1658,8 +1671,8 @@ Use the `Function.get_web_url()` method instead.
1658
1671
  input_queue,
1659
1672
  self.client,
1660
1673
  )
1661
- metadata = api_pb2.FunctionCallFromIdResponse(function_call_id=function_call_id, num_inputs=num_inputs)
1662
- fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, metadata)
1674
+ fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, None)
1675
+ fc._num_inputs = num_inputs # set the cached value of num_inputs
1663
1676
  return fc
1664
1677
 
1665
1678
  async def _call_function(self, args, kwargs) -> ReturnType:
@@ -1888,10 +1901,9 @@ Use the `Function.get_web_url()` method instead.
1888
1901
  @live_method
1889
1902
  async def get_current_stats(self) -> FunctionStats:
1890
1903
  """Return a `FunctionStats` object describing the current function's queue and runner counts."""
1891
- resp = await retry_transient_errors(
1892
- self.client.stub.FunctionGetCurrentStats,
1904
+ resp = await self.client.stub.FunctionGetCurrentStats(
1893
1905
  api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
1894
- total_timeout=10.0,
1906
+ retry=Retry(total_timeout=10.0),
1895
1907
  )
1896
1908
  return FunctionStats(backlog=resp.backlog, num_total_runners=resp.num_total_tasks)
1897
1909
 
@@ -1929,19 +1941,16 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1929
1941
  def _invocation(self):
1930
1942
  return _Invocation(self.client.stub, self.object_id, self.client)
1931
1943
 
1932
- def _hydrate_metadata(self, metadata: Optional[Message]):
1933
- if not metadata:
1934
- return
1935
- assert isinstance(metadata, api_pb2.FunctionCallFromIdResponse)
1936
- self._num_inputs = metadata.num_inputs
1937
-
1938
1944
  @live_method
1939
1945
  async def num_inputs(self) -> int:
1940
1946
  """Get the number of inputs in the function call."""
1941
- # Should have been hydrated.
1942
- assert self._num_inputs is not None
1947
+ if self._num_inputs is None:
1948
+ request = api_pb2.FunctionCallFromIdRequest(function_call_id=self.object_id)
1949
+ resp = await self.client.stub.FunctionCallFromId(request)
1950
+ self._num_inputs = resp.num_inputs # cached
1943
1951
  return self._num_inputs
1944
1952
 
1953
+ @live_method
1945
1954
  async def get(self, timeout: Optional[float] = None, *, index: int = 0) -> ReturnType:
1946
1955
  """Get the result of the index-th input of the function call.
1947
1956
  `.spawn()` calls have a single output, so only specifying `index=0` is valid.
@@ -1985,6 +1994,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1985
1994
  async for _, item in self._invocation().enumerate(start_index=start, end_index=end):
1986
1995
  yield item
1987
1996
 
1997
+ @live_method
1988
1998
  async def get_call_graph(self) -> list[InputInfo]:
1989
1999
  """Returns a structure representing the call graph from a given root
1990
2000
  call ID, along with the status of execution for each node.
@@ -1994,9 +2004,10 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1994
2004
  """
1995
2005
  assert self._client and self._client.stub
1996
2006
  request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
1997
- response = await retry_transient_errors(self._client.stub.FunctionGetCallGraph, request)
2007
+ response = await self._client.stub.FunctionGetCallGraph(request)
1998
2008
  return _reconstruct_call_graph(response)
1999
2009
 
2010
+ @live_method
2000
2011
  async def cancel(
2001
2012
  self,
2002
2013
  # if true, containers running the inputs are forcibly terminated
@@ -2012,7 +2023,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2012
2023
  function_call_id=self.object_id, terminate_containers=terminate_containers
2013
2024
  )
2014
2025
  assert self._client and self._client.stub
2015
- await retry_transient_errors(self._client.stub.FunctionCallCancel, request)
2026
+ await self._client.stub.FunctionCallCancel(request)
2016
2027
 
2017
2028
  @staticmethod
2018
2029
  async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_FunctionCall[Any]":
@@ -2034,20 +2045,18 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2034
2045
  if you no longer have access to the original object returned from `Function.spawn`.
2035
2046
 
2036
2047
  """
2037
- if client is None:
2038
- client = await _Client.from_env()
2039
2048
 
2040
- async def _load(self: _FunctionCall, resolver: Resolver, existing_object_id: Optional[str]):
2041
- request = api_pb2.FunctionCallFromIdRequest(function_call_id=function_call_id)
2042
- resp = await retry_transient_errors(resolver.client.stub.FunctionCallFromId, request)
2043
- self._hydrate(function_call_id, resolver.client, resp)
2049
+ async def _load(
2050
+ self: _FunctionCall, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
2051
+ ):
2052
+ # this loader doesn't do anything in practice, but it will get the client from the load_context
2053
+ self._hydrate(function_call_id, load_context.client, None)
2044
2054
 
2045
2055
  rep = f"FunctionCall.from_id({function_call_id!r})"
2046
- fc: _FunctionCall[Any] = _FunctionCall._from_loader(_load, rep, hydrate_lazily=True)
2047
- # We already know the object ID, so we can set it directly
2048
- fc._object_id = function_call_id
2049
- fc._client = client
2050
- return fc
2056
+
2057
+ return _FunctionCall._from_loader(
2058
+ _load, rep, hydrate_lazily=True, load_context_overrides=LoadContext(client=client)
2059
+ )
2051
2060
 
2052
2061
  @staticmethod
2053
2062
  async def gather(*function_calls: "_FunctionCall[T]") -> typing.Sequence[T]:
@@ -0,0 +1,171 @@
1
+ # Copyright Modal Labs 2025
2
+ from typing import TYPE_CHECKING, Any, Collection, Generic, Literal, Mapping, Optional, TypeVar, Union, overload
3
+
4
+ import grpclib.client
5
+ from google.protobuf.message import Message
6
+ from grpclib import GRPCError, Status
7
+
8
+ from ._traceback import suppress_tb_frames
9
+ from ._utils.grpc_utils import Retry, _retry_transient_errors
10
+ from .config import config, logger
11
+ from .exception import InvalidError, NotFoundError
12
+
13
+ if TYPE_CHECKING:
14
+ from .client import _Client
15
+
16
+
17
+ _Value = Union[str, bytes]
18
+ _MetadataLike = Union[Mapping[str, _Value], Collection[tuple[str, _Value]]]
19
+ RequestType = TypeVar("RequestType", bound=Message)
20
+ ResponseType = TypeVar("ResponseType", bound=Message)
21
+
22
+
23
+ class grpc_error_converter:
24
+ def __enter__(self):
25
+ pass
26
+
27
+ def __exit__(self, exc_type, exc, traceback) -> Literal[False]:
28
+ # skip all internal frames from grpclib
29
+ use_full_traceback = config.get("traceback")
30
+ with suppress_tb_frames(1):
31
+ if isinstance(exc, GRPCError):
32
+ if exc.status == Status.NOT_FOUND:
33
+ if use_full_traceback:
34
+ raise NotFoundError(exc.message)
35
+ else:
36
+ raise NotFoundError(exc.message) from None # from None to skip the grpc-internal cause
37
+
38
+ if not use_full_traceback:
39
+ # just include the frame in grpclib that actually raises the GRPCError
40
+ tb = exc.__traceback__
41
+ while tb.tb_next:
42
+ tb = tb.tb_next
43
+ exc.with_traceback(tb)
44
+ raise exc from None # from None to skip the grpc-internal cause
45
+ raise exc
46
+
47
+ return False
48
+
49
+
50
+ _DEFAULT_RETRY = Retry()
51
+
52
+
53
+ class UnaryUnaryWrapper(Generic[RequestType, ResponseType]):
54
+ # Calls a grpclib.UnaryUnaryMethod using a specific Client instance, respecting
55
+ # if that client is closed etc. and possibly introducing Modal-specific retry logic
56
+ wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType]
57
+ client: "_Client"
58
+
59
+ def __init__(
60
+ self,
61
+ wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType],
62
+ client: "_Client",
63
+ server_url: str,
64
+ ):
65
+ self.wrapped_method = wrapped_method
66
+ self.client = client
67
+ self.server_url = server_url
68
+
69
+ @property
70
+ def name(self) -> str:
71
+ return self.wrapped_method.name
72
+
73
+ @overload
74
+ async def __call__(
75
+ self,
76
+ req: RequestType,
77
+ *,
78
+ retry: Retry = _DEFAULT_RETRY,
79
+ timeout: None = None,
80
+ metadata: Optional[list[tuple[str, str]]] = None,
81
+ ) -> ResponseType: ...
82
+
83
+ @overload
84
+ async def __call__(
85
+ self,
86
+ req: RequestType,
87
+ *,
88
+ retry: None,
89
+ timeout: Optional[float] = None,
90
+ metadata: Optional[list[tuple[str, str]]] = None,
91
+ ) -> ResponseType: ...
92
+
93
+ async def __call__(
94
+ self,
95
+ req: RequestType,
96
+ *,
97
+ retry: Optional[Retry] = _DEFAULT_RETRY,
98
+ timeout: Optional[float] = None,
99
+ metadata: Optional[list[tuple[str, str]]] = None,
100
+ ) -> ResponseType:
101
+ with suppress_tb_frames(1):
102
+ if timeout is not None and retry is not None:
103
+ raise InvalidError("Retry must be None when timeout is set")
104
+
105
+ if retry is None:
106
+ return await self.direct(req, timeout=timeout, metadata=metadata)
107
+
108
+ return await _retry_transient_errors(
109
+ self, # type: ignore
110
+ req,
111
+ retry=retry,
112
+ metadata=metadata,
113
+ )
114
+
115
+ async def direct(
116
+ self,
117
+ req: RequestType,
118
+ *,
119
+ timeout: Optional[float] = None,
120
+ metadata: Optional[_MetadataLike] = None,
121
+ ) -> ResponseType:
122
+ from .client import _Client
123
+
124
+ if self.client._snapshotted:
125
+ logger.debug(f"refreshing client after snapshot for {self.name.rsplit('/', 1)[1]}")
126
+ self.client = await _Client.from_env()
127
+
128
+ # Note: We override the grpclib method's channel (see grpclib's code [1]). I think this is fine
129
+ # since grpclib's code doesn't seem to change very much, but we could also recreate the
130
+ # grpclib stub if we aren't comfortable with this. The downside is then we need to cache
131
+ # the grpclib stub so the rest of our code becomes a bit more complicated.
132
+ #
133
+ # We need to override the channel because after the process is forked or the client is
134
+ # snapshotted, the existing channel may be stale / unusable.
135
+ #
136
+ # [1]: https://github.com/vmagamedov/grpclib/blob/62f968a4c84e3f64e6966097574ff0a59969ea9b/grpclib/client.py#L844
137
+ self.wrapped_method.channel = await self.client._get_channel(self.server_url)
138
+ with suppress_tb_frames(1), grpc_error_converter():
139
+ return await self.client._call_unary(self.wrapped_method, req, timeout=timeout, metadata=metadata)
140
+
141
+
142
+ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
143
+ wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType]
144
+
145
+ def __init__(
146
+ self,
147
+ wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType],
148
+ client: "_Client",
149
+ server_url: str,
150
+ ):
151
+ self.wrapped_method = wrapped_method
152
+ self.client = client
153
+ self.server_url = server_url
154
+
155
+ @property
156
+ def name(self) -> str:
157
+ return self.wrapped_method.name
158
+
159
+ async def unary_stream(
160
+ self,
161
+ request,
162
+ metadata: Optional[Any] = None,
163
+ ):
164
+ from .client import _Client
165
+
166
+ if self.client._snapshotted:
167
+ logger.debug(f"refreshing client after snapshot for {self.name.rsplit('/', 1)[1]}")
168
+ self.client = await _Client.from_env()
169
+ self.wrapped_method.channel = await self.client._get_channel(self.server_url)
170
+ async for response in self.client._call_stream(self.wrapped_method, request, metadata=metadata):
171
+ yield response