modAL 1.2.5.dev27__tar.gz → 1.2.6__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 (197) hide show
  1. {modal-1.2.5.dev27 → modal-1.2.6}/PKG-INFO +1 -1
  2. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_output.py +2 -2
  3. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/container_io_manager.py +1 -1
  4. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/user_code_imports.py +1 -1
  5. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_traceback.py +1 -1
  6. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/task_command_router_client.py +59 -80
  7. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/network_file_system.py +1 -1
  8. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/run.py +1 -1
  9. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/volume.py +1 -1
  10. {modal-1.2.5.dev27 → modal-1.2.6}/modal/client.pyi +2 -10
  11. {modal-1.2.5.dev27 → modal-1.2.6}/modal/container_process.py +2 -2
  12. {modal-1.2.5.dev27 → modal-1.2.6}/modal/io_streams.py +1 -1
  13. {modal-1.2.5.dev27 → modal-1.2.6}/modal/mount.py +8 -8
  14. {modal-1.2.5.dev27 → modal-1.2.6}/modal.egg-info/PKG-INFO +1 -1
  15. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/task_command_router.proto +20 -0
  16. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/task_command_router_grpc.py +33 -0
  17. modal-1.2.6/modal_proto/task_command_router_pb2.py +180 -0
  18. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/task_command_router_pb2.pyi +51 -0
  19. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/task_command_router_pb2_grpc.py +69 -0
  20. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/task_command_router_pb2_grpc.pyi +25 -0
  21. {modal-1.2.5.dev27 → modal-1.2.6}/modal_version/__init__.py +1 -1
  22. {modal-1.2.5.dev27 → modal-1.2.6}/modal_version/__main__.py +1 -1
  23. {modal-1.2.5.dev27 → modal-1.2.6}/pyproject.toml +11 -1
  24. modal-1.2.5.dev27/modal_proto/task_command_router_pb2.py +0 -149
  25. {modal-1.2.5.dev27 → modal-1.2.6}/LICENSE +0 -0
  26. {modal-1.2.5.dev27 → modal-1.2.6}/README.md +0 -0
  27. {modal-1.2.5.dev27 → modal-1.2.6}/modal/__init__.py +0 -0
  28. {modal-1.2.5.dev27 → modal-1.2.6}/modal/__main__.py +0 -0
  29. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_billing.py +0 -0
  30. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_clustered_functions.py +0 -0
  31. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_clustered_functions.pyi +0 -0
  32. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_container_entrypoint.py +0 -0
  33. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_functions.py +0 -0
  34. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_grpc_client.py +0 -0
  35. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_ipython.py +0 -0
  36. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_load_context.py +0 -0
  37. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_location.py +0 -0
  38. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_object.py +0 -0
  39. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_partial_function.py +0 -0
  40. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_pty.py +0 -0
  41. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_resolver.py +0 -0
  42. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_resources.py +0 -0
  43. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/__init__.py +0 -0
  44. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/asgi.py +0 -0
  45. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/container_io_manager.pyi +0 -0
  46. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/execution_context.py +0 -0
  47. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/execution_context.pyi +0 -0
  48. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  49. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/telemetry.py +0 -0
  50. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_runtime/user_code_event_loop.py +0 -0
  51. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_serialization.py +0 -0
  52. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_tunnel.py +0 -0
  53. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_tunnel.pyi +0 -0
  54. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_type_manager.py +0 -0
  55. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/__init__.py +0 -0
  56. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/app_utils.py +0 -0
  57. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/async_utils.py +0 -0
  58. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/auth_token_manager.py +0 -0
  59. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/blob_utils.py +0 -0
  60. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/bytes_io_segment_payload.py +0 -0
  61. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/deprecation.py +0 -0
  62. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/docker_utils.py +0 -0
  63. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/function_utils.py +0 -0
  64. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/git_utils.py +0 -0
  65. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/grpc_testing.py +0 -0
  66. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/grpc_utils.py +0 -0
  67. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/hash_utils.py +0 -0
  68. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/http_utils.py +0 -0
  69. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/jwt_utils.py +0 -0
  70. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/logger.py +0 -0
  71. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/mount_utils.py +0 -0
  72. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/name_utils.py +0 -0
  73. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/package_utils.py +0 -0
  74. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/pattern_utils.py +0 -0
  75. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/rand_pb_testing.py +0 -0
  76. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/shell_utils.py +0 -0
  77. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_utils/time_utils.py +0 -0
  78. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_vendor/__init__.py +0 -0
  79. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  80. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_vendor/cloudpickle.py +0 -0
  81. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_vendor/tblib.py +0 -0
  82. {modal-1.2.5.dev27 → modal-1.2.6}/modal/_watcher.py +0 -0
  83. {modal-1.2.5.dev27 → modal-1.2.6}/modal/app.py +0 -0
  84. {modal-1.2.5.dev27 → modal-1.2.6}/modal/app.pyi +0 -0
  85. {modal-1.2.5.dev27 → modal-1.2.6}/modal/billing.py +0 -0
  86. {modal-1.2.5.dev27 → modal-1.2.6}/modal/builder/2023.12.312.txt +0 -0
  87. {modal-1.2.5.dev27 → modal-1.2.6}/modal/builder/2023.12.txt +0 -0
  88. {modal-1.2.5.dev27 → modal-1.2.6}/modal/builder/2024.04.txt +0 -0
  89. {modal-1.2.5.dev27 → modal-1.2.6}/modal/builder/2024.10.txt +0 -0
  90. {modal-1.2.5.dev27 → modal-1.2.6}/modal/builder/2025.06.txt +0 -0
  91. {modal-1.2.5.dev27 → modal-1.2.6}/modal/builder/PREVIEW.txt +0 -0
  92. {modal-1.2.5.dev27 → modal-1.2.6}/modal/builder/README.md +0 -0
  93. {modal-1.2.5.dev27 → modal-1.2.6}/modal/builder/base-images.json +0 -0
  94. {modal-1.2.5.dev27 → modal-1.2.6}/modal/call_graph.py +0 -0
  95. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/__init__.py +0 -0
  96. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/_download.py +0 -0
  97. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/_traceback.py +0 -0
  98. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/app.py +0 -0
  99. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/cluster.py +0 -0
  100. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/config.py +0 -0
  101. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/container.py +0 -0
  102. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/dict.py +0 -0
  103. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/entry_point.py +0 -0
  104. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/environment.py +0 -0
  105. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/import_refs.py +0 -0
  106. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/launch.py +0 -0
  107. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/profile.py +0 -0
  108. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/programs/__init__.py +0 -0
  109. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/programs/launch_instance_ssh.py +0 -0
  110. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/programs/run_jupyter.py +0 -0
  111. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/programs/run_marimo.py +0 -0
  112. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/programs/vscode.py +0 -0
  113. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/queues.py +0 -0
  114. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/secret.py +0 -0
  115. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/shell.py +0 -0
  116. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/token.py +0 -0
  117. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cli/utils.py +0 -0
  118. {modal-1.2.5.dev27 → modal-1.2.6}/modal/client.py +0 -0
  119. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cloud_bucket_mount.py +0 -0
  120. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cloud_bucket_mount.pyi +0 -0
  121. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cls.py +0 -0
  122. {modal-1.2.5.dev27 → modal-1.2.6}/modal/cls.pyi +0 -0
  123. {modal-1.2.5.dev27 → modal-1.2.6}/modal/config.py +0 -0
  124. {modal-1.2.5.dev27 → modal-1.2.6}/modal/container_process.pyi +0 -0
  125. {modal-1.2.5.dev27 → modal-1.2.6}/modal/dict.py +0 -0
  126. {modal-1.2.5.dev27 → modal-1.2.6}/modal/dict.pyi +0 -0
  127. {modal-1.2.5.dev27 → modal-1.2.6}/modal/environments.py +0 -0
  128. {modal-1.2.5.dev27 → modal-1.2.6}/modal/environments.pyi +0 -0
  129. {modal-1.2.5.dev27 → modal-1.2.6}/modal/exception.py +0 -0
  130. {modal-1.2.5.dev27 → modal-1.2.6}/modal/experimental/__init__.py +0 -0
  131. {modal-1.2.5.dev27 → modal-1.2.6}/modal/experimental/flash.py +0 -0
  132. {modal-1.2.5.dev27 → modal-1.2.6}/modal/experimental/flash.pyi +0 -0
  133. {modal-1.2.5.dev27 → modal-1.2.6}/modal/experimental/ipython.py +0 -0
  134. {modal-1.2.5.dev27 → modal-1.2.6}/modal/file_io.py +0 -0
  135. {modal-1.2.5.dev27 → modal-1.2.6}/modal/file_io.pyi +0 -0
  136. {modal-1.2.5.dev27 → modal-1.2.6}/modal/file_pattern_matcher.py +0 -0
  137. {modal-1.2.5.dev27 → modal-1.2.6}/modal/functions.py +0 -0
  138. {modal-1.2.5.dev27 → modal-1.2.6}/modal/functions.pyi +0 -0
  139. {modal-1.2.5.dev27 → modal-1.2.6}/modal/gpu.py +0 -0
  140. {modal-1.2.5.dev27 → modal-1.2.6}/modal/image.py +0 -0
  141. {modal-1.2.5.dev27 → modal-1.2.6}/modal/image.pyi +0 -0
  142. {modal-1.2.5.dev27 → modal-1.2.6}/modal/io_streams.pyi +0 -0
  143. {modal-1.2.5.dev27 → modal-1.2.6}/modal/mount.pyi +0 -0
  144. {modal-1.2.5.dev27 → modal-1.2.6}/modal/network_file_system.py +0 -0
  145. {modal-1.2.5.dev27 → modal-1.2.6}/modal/network_file_system.pyi +0 -0
  146. {modal-1.2.5.dev27 → modal-1.2.6}/modal/object.py +0 -0
  147. {modal-1.2.5.dev27 → modal-1.2.6}/modal/object.pyi +0 -0
  148. {modal-1.2.5.dev27 → modal-1.2.6}/modal/output.py +0 -0
  149. {modal-1.2.5.dev27 → modal-1.2.6}/modal/parallel_map.py +0 -0
  150. {modal-1.2.5.dev27 → modal-1.2.6}/modal/parallel_map.pyi +0 -0
  151. {modal-1.2.5.dev27 → modal-1.2.6}/modal/partial_function.py +0 -0
  152. {modal-1.2.5.dev27 → modal-1.2.6}/modal/partial_function.pyi +0 -0
  153. {modal-1.2.5.dev27 → modal-1.2.6}/modal/proxy.py +0 -0
  154. {modal-1.2.5.dev27 → modal-1.2.6}/modal/proxy.pyi +0 -0
  155. {modal-1.2.5.dev27 → modal-1.2.6}/modal/py.typed +0 -0
  156. {modal-1.2.5.dev27 → modal-1.2.6}/modal/queue.py +0 -0
  157. {modal-1.2.5.dev27 → modal-1.2.6}/modal/queue.pyi +0 -0
  158. {modal-1.2.5.dev27 → modal-1.2.6}/modal/retries.py +0 -0
  159. {modal-1.2.5.dev27 → modal-1.2.6}/modal/runner.py +0 -0
  160. {modal-1.2.5.dev27 → modal-1.2.6}/modal/runner.pyi +0 -0
  161. {modal-1.2.5.dev27 → modal-1.2.6}/modal/running_app.py +0 -0
  162. {modal-1.2.5.dev27 → modal-1.2.6}/modal/sandbox.py +0 -0
  163. {modal-1.2.5.dev27 → modal-1.2.6}/modal/sandbox.pyi +0 -0
  164. {modal-1.2.5.dev27 → modal-1.2.6}/modal/schedule.py +0 -0
  165. {modal-1.2.5.dev27 → modal-1.2.6}/modal/scheduler_placement.py +0 -0
  166. {modal-1.2.5.dev27 → modal-1.2.6}/modal/secret.py +0 -0
  167. {modal-1.2.5.dev27 → modal-1.2.6}/modal/secret.pyi +0 -0
  168. {modal-1.2.5.dev27 → modal-1.2.6}/modal/serving.py +0 -0
  169. {modal-1.2.5.dev27 → modal-1.2.6}/modal/serving.pyi +0 -0
  170. {modal-1.2.5.dev27 → modal-1.2.6}/modal/snapshot.py +0 -0
  171. {modal-1.2.5.dev27 → modal-1.2.6}/modal/snapshot.pyi +0 -0
  172. {modal-1.2.5.dev27 → modal-1.2.6}/modal/stream_type.py +0 -0
  173. {modal-1.2.5.dev27 → modal-1.2.6}/modal/token_flow.py +0 -0
  174. {modal-1.2.5.dev27 → modal-1.2.6}/modal/token_flow.pyi +0 -0
  175. {modal-1.2.5.dev27 → modal-1.2.6}/modal/volume.py +0 -0
  176. {modal-1.2.5.dev27 → modal-1.2.6}/modal/volume.pyi +0 -0
  177. {modal-1.2.5.dev27 → modal-1.2.6}/modal.egg-info/SOURCES.txt +0 -0
  178. {modal-1.2.5.dev27 → modal-1.2.6}/modal.egg-info/dependency_links.txt +0 -0
  179. {modal-1.2.5.dev27 → modal-1.2.6}/modal.egg-info/entry_points.txt +0 -0
  180. {modal-1.2.5.dev27 → modal-1.2.6}/modal.egg-info/requires.txt +0 -0
  181. {modal-1.2.5.dev27 → modal-1.2.6}/modal.egg-info/top_level.txt +0 -0
  182. {modal-1.2.5.dev27 → modal-1.2.6}/modal_docs/__init__.py +0 -0
  183. {modal-1.2.5.dev27 → modal-1.2.6}/modal_docs/gen_cli_docs.py +0 -0
  184. {modal-1.2.5.dev27 → modal-1.2.6}/modal_docs/gen_reference_docs.py +0 -0
  185. {modal-1.2.5.dev27 → modal-1.2.6}/modal_docs/mdmd/__init__.py +0 -0
  186. {modal-1.2.5.dev27 → modal-1.2.6}/modal_docs/mdmd/mdmd.py +0 -0
  187. {modal-1.2.5.dev27 → modal-1.2.6}/modal_docs/mdmd/signatures.py +0 -0
  188. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/__init__.py +0 -0
  189. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/api.proto +0 -0
  190. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/api_grpc.py +0 -0
  191. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/api_pb2.py +0 -0
  192. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/api_pb2.pyi +0 -0
  193. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/api_pb2_grpc.py +0 -0
  194. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/api_pb2_grpc.pyi +0 -0
  195. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/modal_api_grpc.py +0 -0
  196. {modal-1.2.5.dev27 → modal-1.2.6}/modal_proto/py.typed +0 -0
  197. {modal-1.2.5.dev27 → modal-1.2.6}/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.6
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -556,7 +556,7 @@ async def get_app_logs_loop(
556
556
  async def stop_pty_shell():
557
557
  nonlocal pty_shell_finish_event, pty_shell_input_task
558
558
  if pty_shell_finish_event:
559
- print("\r", end="") # move cursor to beginning of line
559
+ print("\r", end="") # move cursor to beginning of line # noqa: T201
560
560
  pty_shell_finish_event.set()
561
561
  pty_shell_finish_event = None
562
562
 
@@ -623,7 +623,7 @@ async def get_app_logs_loop(
623
623
  # This corresponds to the `modal run -i` use case where a breakpoint
624
624
  # triggers and the task drops into an interactive PTY mode
625
625
  if pty_shell_finish_event:
626
- print("ERROR: concurrent PTY shells are not supported.")
626
+ print("ERROR: concurrent PTY shells are not supported.") # noqa: T201
627
627
  else:
628
628
  pty_shell_stdout = output_mgr._stdout
629
629
  pty_shell_finish_event = asyncio.Event()
@@ -1007,7 +1007,7 @@ class _ContainerIOManager:
1007
1007
  # Start a debugger if the worker tells us to
1008
1008
  if int(restored_state.get("snapshot_debug", 0)):
1009
1009
  logger.debug("Entering snapshot debugger")
1010
- breakpoint()
1010
+ breakpoint() # noqa: T100
1011
1011
 
1012
1012
  # Local ContainerIOManager state.
1013
1013
  for key in ["task_id", "function_id"]:
@@ -243,7 +243,7 @@ def create_breakpoint_wrapper(container_io_manager: "modal._runtime.container_io
243
243
  def breakpoint_wrapper():
244
244
  # note: it would be nice to not have breakpoint_wrapper() included in the backtrace
245
245
  container_io_manager.interact(from_breakpoint=True)
246
- import pdb
246
+ import pdb # noqa: T100
247
247
 
248
248
  current_frame = inspect.currentframe()
249
249
  if current_frame is not None:
@@ -119,7 +119,7 @@ def print_exception(exc: Optional[type[BaseException]], value: Optional[BaseExce
119
119
  traceback.print_exception(exc, value, tb)
120
120
  if sys.version_info < (3, 11) and value is not None: # type: ignore
121
121
  notes = getattr(value, "__notes__", [])
122
- print(*notes, sep="\n", file=sys.stderr)
122
+ print(*notes, sep="\n", file=sys.stderr) # noqa: T201
123
123
 
124
124
 
125
125
  def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
@@ -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):
210
224
  return
225
+ self._closed = True
211
226
 
212
- self._jwt_refresh_task.cancel()
227
+ channel = getattr(self, "_channel", None)
228
+ if channel is None:
229
+ return
213
230
 
214
- try:
215
- self._channel.close()
216
- except Exception:
217
- pass
231
+ loop = getattr(self, "_loop", None)
232
+
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
@@ -389,13 +403,14 @@ class TaskCommandRouterClient:
389
403
  )
390
404
  return
391
405
 
406
+ logger.debug(f"Refreshing JWT for exec with task ID {self._task_id}")
392
407
  resp = await fetch_command_router_access(self._server_client, self._task_id)
408
+ logger.debug(f"Finished refreshing JWT for exec with task ID {self._task_id}")
409
+
393
410
  # Ensure the server URL remains stable for the lifetime of this client.
394
411
  assert resp.url == self._server_url, "Task router URL changed during session"
395
412
  self._jwt = resp.jwt
396
413
  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
414
 
400
415
  async def _call_with_auth_retry(self, func, *args, **kwargs):
401
416
  try:
@@ -407,47 +422,6 @@ class TaskCommandRouterClient:
407
422
  return await func(*args, **kwargs)
408
423
  raise
409
424
 
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
425
  async def _stream_stdio(
452
426
  self,
453
427
  task_id: str,
@@ -463,7 +437,9 @@ class TaskCommandRouterClient:
463
437
  delay_secs = self.stream_stdio_retry_delay_secs
464
438
  delay_factor = self.stream_stdio_retry_delay_factor
465
439
  num_retries_remaining = self.stream_stdio_max_retries
466
- num_auth_retries = 0
440
+ # Flag to prevent infinite auth retries in the event that the JWT
441
+ # refresh yields an invalid JWT somehow or that the JWT is otherwise invalid.
442
+ did_auth_retry = False
467
443
 
468
444
  async def sleep_and_update_delay_and_num_retries_remaining(e: Exception):
469
445
  nonlocal delay_secs, num_retries_remaining
@@ -487,25 +463,28 @@ class TaskCommandRouterClient:
487
463
  file_descriptor=file_descriptor,
488
464
  )
489
465
 
490
- # Scope auth retry strictly to the initial send (where headers/auth are sent).
466
+ # Auth retry is scoped to a single refresh per streaming attempt. While auth metadata is
467
+ # sent on request start, UNAUTHENTICATED may sometimes surface during iteration,
468
+ # so we handle it at both send and receive boundaries.
491
469
  try:
492
470
  await s.send_message(req, end=True)
471
+ async for item in s:
472
+ # We successfully authenticated after a JWT refresh, reset the auth retry flag.
473
+ if did_auth_retry:
474
+ did_auth_retry = False
475
+ # Reset retry backoff after any successful chunk.
476
+ delay_secs = self.stream_stdio_retry_delay_secs
477
+ offset += len(item.data)
478
+ yield item
493
479
  except GRPCError as exc:
494
- if exc.status == Status.UNAUTHENTICATED and num_auth_retries < 1:
480
+ if exc.status == Status.UNAUTHENTICATED and not did_auth_retry:
495
481
  await self._refresh_jwt()
496
- num_auth_retries += 1
482
+ # Mark that we've retried authentication for this streaming attempt, to
483
+ # prevent subsequent retries.
484
+ did_auth_retry = True
497
485
  continue
498
486
  raise
499
487
 
500
- # We successfully authenticated, reset the auth retry count.
501
- num_auth_retries = 0
502
-
503
- async for item in s:
504
- # Reset retry backoff after any successful chunk.
505
- delay_secs = self.stream_stdio_retry_delay_secs
506
- offset += len(item.data)
507
- yield item
508
-
509
488
  # We successfully streamed all output.
510
489
  return
511
490
  except GRPCError as e:
@@ -102,7 +102,7 @@ async def ls(
102
102
  console.print(table)
103
103
  else:
104
104
  for entry in entries:
105
- print(entry.path)
105
+ print(entry.path) # noqa: T201
106
106
 
107
107
 
108
108
  @nfs_cli.command(
@@ -292,7 +292,7 @@ def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoin
292
292
  @click.pass_context
293
293
  def f(ctx, *args, **kwargs):
294
294
  if ctx.obj["detach"]:
295
- print(
295
+ print( # noqa: T201
296
296
  "Note that running a local entrypoint in detached mode only keeps the last "
297
297
  "triggered Modal function alive after the parent process has been killed or disconnected."
298
298
  )
@@ -148,7 +148,7 @@ async def ls(
148
148
  if not json and not sys.stdout.isatty():
149
149
  # Legacy behavior -- I am not sure why exactly we did this originally but I don't want to break it
150
150
  for entry in entries:
151
- print(entry.path)
151
+ print(entry.path) # noqa: T201
152
152
  else:
153
153
  rows = []
154
154
  for entry in entries:
@@ -28,11 +28,7 @@ class _Client:
28
28
  _snapshotted: bool
29
29
 
30
30
  def __init__(
31
- self,
32
- server_url: str,
33
- client_type: int,
34
- credentials: typing.Optional[tuple[str, str]],
35
- version: str = "1.2.5.dev27",
31
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.2.6"
36
32
  ):
37
33
  """mdmd:hidden
38
34
  The Modal client object is not intended to be instantiated directly by users.
@@ -159,11 +155,7 @@ class Client:
159
155
  _snapshotted: bool
160
156
 
161
157
  def __init__(
162
- self,
163
- server_url: str,
164
- client_type: int,
165
- credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.2.5.dev27",
158
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.2.6"
167
159
  ):
168
160
  """mdmd:hidden
169
161
  The Modal client object is not intended to be instantiated directly by users.
@@ -146,7 +146,7 @@ class _ContainerProcessThroughServer(Generic[T]):
146
146
  async def attach(self):
147
147
  """mdmd:hidden"""
148
148
  if platform.system() == "Windows":
149
- print("interactive exec is not currently supported on Windows.")
149
+ print("interactive exec is not currently supported on Windows.") # noqa: T201
150
150
  return
151
151
 
152
152
  from ._output import make_console
@@ -347,7 +347,7 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
347
347
 
348
348
  async def attach(self):
349
349
  if platform.system() == "Windows":
350
- print("interactive exec is not currently supported on Windows.")
350
+ print("interactive exec is not currently supported on Windows.") # noqa: T201
351
351
  return
352
352
 
353
353
  from ._output import make_console
@@ -183,7 +183,7 @@ class _StreamReaderThroughServer(Generic[T]):
183
183
  async for message, batch_index in iterator:
184
184
  if self._stream_type == StreamType.STDOUT and message:
185
185
  # TODO: rearchitect this, since these bytes aren't necessarily decodable
186
- print(message.decode("utf-8"), end="")
186
+ print(message.decode("utf-8"), end="") # noqa: T201
187
187
  elif self._stream_type == StreamType.PIPE:
188
188
  self._container_process_buffer.append(message)
189
189
 
@@ -775,13 +775,13 @@ async def _create_single_client_dependency_mount(
775
775
  if check_if_exists:
776
776
  try:
777
777
  await Mount.from_name(mount_name, namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL).hydrate.aio(client)
778
- print(f"➖ Found existing mount {mount_name} in global namespace.")
778
+ print(f"➖ Found existing mount {mount_name} in global namespace.") # noqa: T201
779
779
  return
780
780
  except modal.exception.NotFoundError:
781
781
  pass
782
782
 
783
783
  with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpd:
784
- print(f"📦 Building {mount_name}.")
784
+ print(f"📦 Building {mount_name}.") # noqa: T201
785
785
  requirements = os.path.join(os.path.dirname(__file__), f"builder/{builder_version}.txt")
786
786
  cmd = " ".join(
787
787
  [
@@ -810,11 +810,11 @@ async def _create_single_client_dependency_mount(
810
810
  await proc.wait()
811
811
  if proc.returncode:
812
812
  stdout, stderr = await proc.communicate()
813
- print(stdout.decode("utf-8"))
814
- print(stderr.decode("utf-8"))
813
+ print(stdout.decode("utf-8")) # noqa: T201
814
+ print(stderr.decode("utf-8")) # noqa: T201
815
815
  raise RuntimeError(f"Subprocess failed with {proc.returncode}")
816
816
 
817
- print(f"🌐 Downloaded and unpacked {mount_name} packages to {tmpd}.")
817
+ print(f"🌐 Downloaded and unpacked {mount_name} packages to {tmpd}.") # noqa: T201
818
818
 
819
819
  python_mount = Mount._from_local_dir(tmpd, remote_path=REMOTE_PACKAGES_PATH)
820
820
 
@@ -838,11 +838,11 @@ async def _create_single_client_dependency_mount(
838
838
  allow_overwrite=allow_overwrite,
839
839
  client=client,
840
840
  )
841
- print(f"✅ Deployed mount {mount_name} to global namespace.")
841
+ print(f"✅ Deployed mount {mount_name} to global namespace.") # noqa: T201
842
842
  except GRPCError as e:
843
- print(f"⚠️ Mount creation failed with {e.status}: {e.message}")
843
+ print(f"⚠️ Mount creation failed with {e.status}: {e.message}") # noqa: T201
844
844
  else:
845
- print(f"Dry run - skipping deployment of mount {mount_name}")
845
+ print(f"Dry run - skipping deployment of mount {mount_name}") # noqa: T201
846
846
 
847
847
 
848
848
  async def _create_client_dependency_mounts(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.5.dev27
3
+ Version: 1.2.6
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -2,6 +2,7 @@ syntax = "proto3";
2
2
 
3
3
  option go_package = "github.com/modal-labs/modal/go/proto";
4
4
 
5
+ import "google/protobuf/empty.proto";
5
6
  import "modal_proto/api.proto";
6
7
 
7
8
  package modal.task_command_router;
@@ -130,6 +131,21 @@ message TaskExecWaitResponse {
130
131
  // and termination by Modal (due to task timeout, exec exceeded deadline, etc.)
131
132
  }
132
133
 
134
+ message TaskMountDirectoryRequest {
135
+ string task_id = 1;
136
+ bytes path = 2;
137
+ string image_id = 3;
138
+ }
139
+
140
+ message TaskSnapshotDirectoryRequest {
141
+ string task_id = 1;
142
+ bytes path = 2;
143
+ }
144
+
145
+ message TaskSnapshotDirectoryResponse {
146
+ string image_id = 1;
147
+ }
148
+
133
149
  service TaskCommandRouter {
134
150
  // Poll for the exit status of an exec'd command.
135
151
  rpc TaskExecPoll(TaskExecPollRequest) returns (TaskExecPollResponse);
@@ -141,4 +157,8 @@ service TaskCommandRouter {
141
157
  rpc TaskExecStdioRead(TaskExecStdioReadRequest) returns (stream TaskExecStdioReadResponse);
142
158
  // Wait for an exec'd command to exit and return the exit code.
143
159
  rpc TaskExecWait(TaskExecWaitRequest) returns (TaskExecWaitResponse);
160
+ // Mount an image at a directory in the container.
161
+ rpc TaskMountDirectory(TaskMountDirectoryRequest) returns (google.protobuf.Empty);
162
+ // Snapshot a directory with a mounted image, including any local changes, into a new image.
163
+ rpc TaskSnapshotDirectory(TaskSnapshotDirectoryRequest) returns (TaskSnapshotDirectoryResponse);
144
164
  }
@@ -9,6 +9,7 @@ import grpclib.client
9
9
  if typing.TYPE_CHECKING:
10
10
  import grpclib.server
11
11
 
12
+ import google.protobuf.empty_pb2
12
13
  import modal_proto.api_pb2
13
14
  import modal_proto.task_command_router_pb2
14
15
 
@@ -35,6 +36,14 @@ class TaskCommandRouterBase(abc.ABC):
35
36
  async def TaskExecWait(self, stream: 'grpclib.server.Stream[modal_proto.task_command_router_pb2.TaskExecWaitRequest, modal_proto.task_command_router_pb2.TaskExecWaitResponse]') -> None:
36
37
  pass
37
38
 
39
+ @abc.abstractmethod
40
+ async def TaskMountDirectory(self, stream: 'grpclib.server.Stream[modal_proto.task_command_router_pb2.TaskMountDirectoryRequest, google.protobuf.empty_pb2.Empty]') -> None:
41
+ pass
42
+
43
+ @abc.abstractmethod
44
+ async def TaskSnapshotDirectory(self, stream: 'grpclib.server.Stream[modal_proto.task_command_router_pb2.TaskSnapshotDirectoryRequest, modal_proto.task_command_router_pb2.TaskSnapshotDirectoryResponse]') -> None:
45
+ pass
46
+
38
47
  def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
39
48
  return {
40
49
  '/modal.task_command_router.TaskCommandRouter/TaskExecPoll': grpclib.const.Handler(
@@ -67,6 +76,18 @@ class TaskCommandRouterBase(abc.ABC):
67
76
  modal_proto.task_command_router_pb2.TaskExecWaitRequest,
68
77
  modal_proto.task_command_router_pb2.TaskExecWaitResponse,
69
78
  ),
79
+ '/modal.task_command_router.TaskCommandRouter/TaskMountDirectory': grpclib.const.Handler(
80
+ self.TaskMountDirectory,
81
+ grpclib.const.Cardinality.UNARY_UNARY,
82
+ modal_proto.task_command_router_pb2.TaskMountDirectoryRequest,
83
+ google.protobuf.empty_pb2.Empty,
84
+ ),
85
+ '/modal.task_command_router.TaskCommandRouter/TaskSnapshotDirectory': grpclib.const.Handler(
86
+ self.TaskSnapshotDirectory,
87
+ grpclib.const.Cardinality.UNARY_UNARY,
88
+ modal_proto.task_command_router_pb2.TaskSnapshotDirectoryRequest,
89
+ modal_proto.task_command_router_pb2.TaskSnapshotDirectoryResponse,
90
+ ),
70
91
  }
71
92
 
72
93
 
@@ -103,3 +124,15 @@ class TaskCommandRouterStub:
103
124
  modal_proto.task_command_router_pb2.TaskExecWaitRequest,
104
125
  modal_proto.task_command_router_pb2.TaskExecWaitResponse,
105
126
  )
127
+ self.TaskMountDirectory = grpclib.client.UnaryUnaryMethod(
128
+ channel,
129
+ '/modal.task_command_router.TaskCommandRouter/TaskMountDirectory',
130
+ modal_proto.task_command_router_pb2.TaskMountDirectoryRequest,
131
+ google.protobuf.empty_pb2.Empty,
132
+ )
133
+ self.TaskSnapshotDirectory = grpclib.client.UnaryUnaryMethod(
134
+ channel,
135
+ '/modal.task_command_router.TaskCommandRouter/TaskSnapshotDirectory',
136
+ modal_proto.task_command_router_pb2.TaskSnapshotDirectoryRequest,
137
+ modal_proto.task_command_router_pb2.TaskSnapshotDirectoryResponse,
138
+ )