modal 1.2.5.dev27__tar.gz → 1.2.5.dev28__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 (196) hide show
  1. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/PKG-INFO +1 -1
  2. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/task_command_router_client.py +38 -67
  3. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/client.pyi +2 -2
  4. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal.egg-info/PKG-INFO +1 -1
  5. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_version/__init__.py +1 -1
  6. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/LICENSE +0 -0
  7. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/README.md +0 -0
  8. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/__init__.py +0 -0
  9. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/__main__.py +0 -0
  10. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_billing.py +0 -0
  11. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_clustered_functions.py +0 -0
  12. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_clustered_functions.pyi +0 -0
  13. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_container_entrypoint.py +0 -0
  14. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_functions.py +0 -0
  15. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_grpc_client.py +0 -0
  16. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_ipython.py +0 -0
  17. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_load_context.py +0 -0
  18. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_location.py +0 -0
  19. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_object.py +0 -0
  20. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_output.py +0 -0
  21. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_partial_function.py +0 -0
  22. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_pty.py +0 -0
  23. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_resolver.py +0 -0
  24. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_resources.py +0 -0
  25. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/__init__.py +0 -0
  26. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/asgi.py +0 -0
  27. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/container_io_manager.py +0 -0
  28. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/container_io_manager.pyi +0 -0
  29. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/execution_context.py +0 -0
  30. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/execution_context.pyi +0 -0
  31. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  32. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/telemetry.py +0 -0
  33. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/user_code_event_loop.py +0 -0
  34. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_runtime/user_code_imports.py +0 -0
  35. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_serialization.py +0 -0
  36. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_traceback.py +0 -0
  37. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_tunnel.py +0 -0
  38. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_tunnel.pyi +0 -0
  39. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_type_manager.py +0 -0
  40. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/__init__.py +0 -0
  41. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/app_utils.py +0 -0
  42. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/async_utils.py +0 -0
  43. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/auth_token_manager.py +0 -0
  44. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/blob_utils.py +0 -0
  45. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/bytes_io_segment_payload.py +0 -0
  46. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/deprecation.py +0 -0
  47. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/docker_utils.py +0 -0
  48. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/function_utils.py +0 -0
  49. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/git_utils.py +0 -0
  50. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/grpc_testing.py +0 -0
  51. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/grpc_utils.py +0 -0
  52. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/hash_utils.py +0 -0
  53. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/http_utils.py +0 -0
  54. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/jwt_utils.py +0 -0
  55. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/logger.py +0 -0
  56. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/mount_utils.py +0 -0
  57. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/name_utils.py +0 -0
  58. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/package_utils.py +0 -0
  59. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/pattern_utils.py +0 -0
  60. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/rand_pb_testing.py +0 -0
  61. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/shell_utils.py +0 -0
  62. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_utils/time_utils.py +0 -0
  63. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_vendor/__init__.py +0 -0
  64. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  65. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_vendor/cloudpickle.py +0 -0
  66. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_vendor/tblib.py +0 -0
  67. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/_watcher.py +0 -0
  68. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/app.py +0 -0
  69. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/app.pyi +0 -0
  70. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/billing.py +0 -0
  71. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/builder/2023.12.312.txt +0 -0
  72. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/builder/2023.12.txt +0 -0
  73. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/builder/2024.04.txt +0 -0
  74. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/builder/2024.10.txt +0 -0
  75. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/builder/2025.06.txt +0 -0
  76. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/builder/PREVIEW.txt +0 -0
  77. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/builder/README.md +0 -0
  78. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/builder/base-images.json +0 -0
  79. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/call_graph.py +0 -0
  80. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/__init__.py +0 -0
  81. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/_download.py +0 -0
  82. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/_traceback.py +0 -0
  83. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/app.py +0 -0
  84. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/cluster.py +0 -0
  85. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/config.py +0 -0
  86. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/container.py +0 -0
  87. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/dict.py +0 -0
  88. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/entry_point.py +0 -0
  89. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/environment.py +0 -0
  90. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/import_refs.py +0 -0
  91. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/launch.py +0 -0
  92. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/network_file_system.py +0 -0
  93. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/profile.py +0 -0
  94. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/programs/__init__.py +0 -0
  95. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/programs/launch_instance_ssh.py +0 -0
  96. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/programs/run_jupyter.py +0 -0
  97. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/programs/run_marimo.py +0 -0
  98. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/programs/vscode.py +0 -0
  99. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/queues.py +0 -0
  100. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/run.py +0 -0
  101. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/secret.py +0 -0
  102. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/shell.py +0 -0
  103. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/token.py +0 -0
  104. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/utils.py +0 -0
  105. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cli/volume.py +0 -0
  106. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/client.py +0 -0
  107. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cloud_bucket_mount.py +0 -0
  108. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cloud_bucket_mount.pyi +0 -0
  109. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cls.py +0 -0
  110. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/cls.pyi +0 -0
  111. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/config.py +0 -0
  112. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/container_process.py +0 -0
  113. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/container_process.pyi +0 -0
  114. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/dict.py +0 -0
  115. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/dict.pyi +0 -0
  116. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/environments.py +0 -0
  117. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/environments.pyi +0 -0
  118. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/exception.py +0 -0
  119. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/experimental/__init__.py +0 -0
  120. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/experimental/flash.py +0 -0
  121. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/experimental/flash.pyi +0 -0
  122. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/experimental/ipython.py +0 -0
  123. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/file_io.py +0 -0
  124. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/file_io.pyi +0 -0
  125. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/file_pattern_matcher.py +0 -0
  126. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/functions.py +0 -0
  127. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/functions.pyi +0 -0
  128. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/gpu.py +0 -0
  129. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/image.py +0 -0
  130. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/image.pyi +0 -0
  131. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/io_streams.py +0 -0
  132. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/io_streams.pyi +0 -0
  133. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/mount.py +0 -0
  134. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/mount.pyi +0 -0
  135. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/network_file_system.py +0 -0
  136. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/network_file_system.pyi +0 -0
  137. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/object.py +0 -0
  138. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/object.pyi +0 -0
  139. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/output.py +0 -0
  140. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/parallel_map.py +0 -0
  141. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/parallel_map.pyi +0 -0
  142. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/partial_function.py +0 -0
  143. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/partial_function.pyi +0 -0
  144. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/proxy.py +0 -0
  145. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/proxy.pyi +0 -0
  146. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/py.typed +0 -0
  147. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/queue.py +0 -0
  148. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/queue.pyi +0 -0
  149. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/retries.py +0 -0
  150. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/runner.py +0 -0
  151. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/runner.pyi +0 -0
  152. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/running_app.py +0 -0
  153. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/sandbox.py +0 -0
  154. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/sandbox.pyi +0 -0
  155. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/schedule.py +0 -0
  156. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/scheduler_placement.py +0 -0
  157. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/secret.py +0 -0
  158. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/secret.pyi +0 -0
  159. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/serving.py +0 -0
  160. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/serving.pyi +0 -0
  161. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/snapshot.py +0 -0
  162. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/snapshot.pyi +0 -0
  163. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/stream_type.py +0 -0
  164. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/token_flow.py +0 -0
  165. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/token_flow.pyi +0 -0
  166. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/volume.py +0 -0
  167. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal/volume.pyi +0 -0
  168. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal.egg-info/SOURCES.txt +0 -0
  169. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal.egg-info/dependency_links.txt +0 -0
  170. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal.egg-info/entry_points.txt +0 -0
  171. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal.egg-info/requires.txt +0 -0
  172. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal.egg-info/top_level.txt +0 -0
  173. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_docs/__init__.py +0 -0
  174. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_docs/gen_cli_docs.py +0 -0
  175. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_docs/gen_reference_docs.py +0 -0
  176. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_docs/mdmd/__init__.py +0 -0
  177. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_docs/mdmd/mdmd.py +0 -0
  178. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_docs/mdmd/signatures.py +0 -0
  179. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/__init__.py +0 -0
  180. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/api.proto +0 -0
  181. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/api_grpc.py +0 -0
  182. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/api_pb2.py +0 -0
  183. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/api_pb2.pyi +0 -0
  184. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/api_pb2_grpc.py +0 -0
  185. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/api_pb2_grpc.pyi +0 -0
  186. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/modal_api_grpc.py +0 -0
  187. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/py.typed +0 -0
  188. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/task_command_router.proto +0 -0
  189. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/task_command_router_grpc.py +0 -0
  190. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2.py +0 -0
  191. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2.pyi +0 -0
  192. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  193. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  194. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/modal_version/__main__.py +0 -0
  195. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/pyproject.toml +0 -0
  196. {modal-1.2.5.dev27 → modal-1.2.5.dev28}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.5.dev27
3
+ Version: 1.2.5.dev28
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -158,8 +158,10 @@ class TaskCommandRouterClient:
158
158
  )
159
159
 
160
160
  await connect_channel(channel)
161
+ loop = asyncio.get_running_loop()
162
+ jwt_refresh_lock = asyncio.Lock()
161
163
 
162
- return cls(server_client, task_id, resp.url, resp.jwt, channel)
164
+ return cls(server_client, task_id, resp.url, resp.jwt, channel, loop, jwt_refresh_lock)
163
165
 
164
166
  def __init__(
165
167
  self,
@@ -168,12 +170,18 @@ class TaskCommandRouterClient:
168
170
  server_url: str,
169
171
  jwt: str,
170
172
  channel: grpclib.client.Channel,
173
+ loop: asyncio.AbstractEventLoop,
174
+ jwt_refresh_lock: asyncio.Lock,
171
175
  *,
172
176
  stream_stdio_retry_delay_secs: float = 0.01,
173
177
  stream_stdio_retry_delay_factor: float = 2,
174
178
  stream_stdio_max_retries: int = 10,
175
179
  ) -> None:
176
180
  """Callers should not use this directly. Use TaskCommandRouterClient.try_init() instead."""
181
+ # Record the loop this instance is bound to so __del__ can safely schedule cleanup
182
+ # even if finalization happens from a different thread (e.g. via synchronicity).
183
+ self._loop = loop
184
+
177
185
  # Attach bearer token on all requests to the worker-side router service.
178
186
  self._server_client = server_client
179
187
  self._task_id = task_id
@@ -187,12 +195,10 @@ class TaskCommandRouterClient:
187
195
 
188
196
  # JWT refresh coordination
189
197
  self._jwt_exp: Optional[float] = _parse_jwt_expiration(jwt)
190
- self._jwt_refresh_lock = asyncio.Lock()
191
- self._jwt_refresh_event = asyncio.Event()
192
- self._closed = False
198
+ # This is passed in as an argument to ensure it's created from within the correct event loop.
199
+ self._jwt_refresh_lock = jwt_refresh_lock
193
200
 
194
- # Start background task to eagerly refresh JWT 30s before expiration.
195
- self._jwt_refresh_task = asyncio.create_task(self._jwt_refresh_loop())
201
+ self._closed = False
196
202
 
197
203
  async def send_request(event: grpclib.events.SendRequest) -> None:
198
204
  # This will get the most recent JWT for every request. No need to
@@ -205,29 +211,40 @@ class TaskCommandRouterClient:
205
211
  self._stub = TaskCommandRouterStub(self._channel)
206
212
 
207
213
  def __del__(self) -> None:
208
- """Clean up the client when it's garbage collected."""
209
- if self._closed:
214
+ """Best-effort cleanup if the caller forgot to close().
215
+
216
+ This object is typically used through synchronicity wrappers, which means this finalizer
217
+ may run on a different thread than the event loop that owns the channel. Closing the
218
+ channel is therefore scheduled onto the owning loop using call_soon_threadsafe.
219
+
220
+ Use getattr in the event that attributes are not yet initialized or the
221
+ object is in a half-torn-down state.
222
+ """
223
+ if getattr(self, "_closed", False):
224
+ return
225
+ self._closed = True
226
+
227
+ channel = getattr(self, "_channel", None)
228
+ if channel is None:
210
229
  return
211
230
 
212
- self._jwt_refresh_task.cancel()
231
+ loop = getattr(self, "_loop", None)
213
232
 
214
- try:
215
- self._channel.close()
216
- except Exception:
217
- pass
233
+ if loop is not None and not loop.is_closed():
234
+ try:
235
+ loop.call_soon_threadsafe(channel.close)
236
+ except Exception:
237
+ # call_soon_threadsafe could throw if the loop is torn down
238
+ # after calling is_closed. This is safe to ignore, and we don't
239
+ # want to raise an exception from a destructor.
240
+ pass
218
241
 
219
242
  async def close(self) -> None:
220
- """Close the client and stop the background JWT refresh task."""
243
+ """Close the client."""
221
244
  if self._closed:
222
245
  return
223
246
 
224
247
  self._closed = True
225
- self._jwt_refresh_task.cancel()
226
- try:
227
- logger.debug(f"Waiting for JWT refresh task to complete for exec with task ID {self._task_id}")
228
- await self._jwt_refresh_task
229
- except asyncio.CancelledError:
230
- pass
231
248
  self._channel.close()
232
249
 
233
250
  async def exec_start(self, request: sr_pb2.TaskExecStartRequest) -> sr_pb2.TaskExecStartResponse:
@@ -370,10 +387,7 @@ class TaskCommandRouterClient:
370
387
  raise ExecTimeoutError(f"Deadline exceeded while waiting for exec {exec_id}")
371
388
 
372
389
  async def _refresh_jwt(self) -> None:
373
- """Refresh JWT from the server and update internal state.
374
-
375
- Concurrency-safe: only one refresh runs at a time.
376
- """
390
+ """Refresh JWT from the server and update internal state."""
377
391
  async with self._jwt_refresh_lock:
378
392
  if self._closed:
379
393
  return
@@ -394,8 +408,6 @@ class TaskCommandRouterClient:
394
408
  assert resp.url == self._server_url, "Task router URL changed during session"
395
409
  self._jwt = resp.jwt
396
410
  self._jwt_exp = _parse_jwt_expiration(resp.jwt)
397
- # Wake up the background loop to recompute its next sleep.
398
- self._jwt_refresh_event.set()
399
411
 
400
412
  async def _call_with_auth_retry(self, func, *args, **kwargs):
401
413
  try:
@@ -407,47 +419,6 @@ class TaskCommandRouterClient:
407
419
  return await func(*args, **kwargs)
408
420
  raise
409
421
 
410
- async def _jwt_refresh_loop(self) -> None:
411
- """Background task that refreshes JWT 30 seconds before expiration.
412
-
413
- Uses an event to wake early when a manual refresh happens or token changes.
414
- """
415
- while not self._closed:
416
- try:
417
- exp = self._jwt_exp
418
- now = time.time()
419
- if exp is None:
420
- # Unknown expiration: re-check periodically or until event wakes us.
421
- sleep_s = 60.0
422
- else:
423
- refresh_at = exp - 30.0
424
- sleep_s = max(refresh_at - now, 0.0)
425
-
426
- self._jwt_refresh_event.clear()
427
- if sleep_s > 0:
428
- try:
429
- logger.debug(f"Waiting for JWT refresh for {sleep_s}s for exec with task ID {self._task_id}")
430
- # Wait until it's time to refresh, unless woken early.
431
- await asyncio.wait_for(self._jwt_refresh_event.wait(), timeout=sleep_s)
432
- logger.debug(f"Stopped waiting for JWT refresh for exec with task ID {self._task_id}")
433
- # Event fired (e.g., token changed) -> recompute timings.
434
- continue
435
- except asyncio.TimeoutError:
436
- logger.debug(f"Done waiting for JWT refresh for exec with task ID {self._task_id}")
437
- pass
438
-
439
- # Time to refresh.
440
- logger.debug(f"Refreshing JWT for exec with task ID {self._task_id}")
441
- await self._refresh_jwt()
442
- except asyncio.CancelledError:
443
- logger.debug(f"Cancelled JWT refresh loop for exec with task ID {self._task_id}")
444
- break
445
- except Exception as e:
446
- # Exceptions here can stem from non-transient errors against the server sending
447
- # the TaskGetCommandRouterAccess RPC, for instance, if the task has finished.
448
- logger.debug(f"Background JWT refresh failed for exec with task ID {self._task_id}: {e}")
449
- break
450
-
451
422
  async def _stream_stdio(
452
423
  self,
453
424
  task_id: str,
@@ -32,7 +32,7 @@ class _Client:
32
32
  server_url: str,
33
33
  client_type: int,
34
34
  credentials: typing.Optional[tuple[str, str]],
35
- version: str = "1.2.5.dev27",
35
+ version: str = "1.2.5.dev28",
36
36
  ):
37
37
  """mdmd:hidden
38
38
  The Modal client object is not intended to be instantiated directly by users.
@@ -163,7 +163,7 @@ class Client:
163
163
  server_url: str,
164
164
  client_type: int,
165
165
  credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.2.5.dev27",
166
+ version: str = "1.2.5.dev28",
167
167
  ):
168
168
  """mdmd:hidden
169
169
  The Modal client object is not intended to be instantiated directly by users.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.5.dev27
3
+ Version: 1.2.5.dev28
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.2.5.dev27"
4
+ __version__ = "1.2.5.dev28"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes