modal 1.2.5.dev26__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.dev26 → modal-1.2.5.dev28}/PKG-INFO +1 -1
  2. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/blob_utils.py +18 -5
  3. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/task_command_router_client.py +38 -67
  4. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/client.pyi +2 -2
  5. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/mount.py +14 -8
  6. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/volume.py +5 -1
  7. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/PKG-INFO +1 -1
  8. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_version/__init__.py +1 -1
  9. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/LICENSE +0 -0
  10. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/README.md +0 -0
  11. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/__init__.py +0 -0
  12. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/__main__.py +0 -0
  13. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_billing.py +0 -0
  14. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_clustered_functions.py +0 -0
  15. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_clustered_functions.pyi +0 -0
  16. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_container_entrypoint.py +0 -0
  17. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_functions.py +0 -0
  18. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_grpc_client.py +0 -0
  19. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_ipython.py +0 -0
  20. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_load_context.py +0 -0
  21. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_location.py +0 -0
  22. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_object.py +0 -0
  23. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_output.py +0 -0
  24. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_partial_function.py +0 -0
  25. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_pty.py +0 -0
  26. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_resolver.py +0 -0
  27. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_resources.py +0 -0
  28. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/__init__.py +0 -0
  29. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/asgi.py +0 -0
  30. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/container_io_manager.py +0 -0
  31. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/container_io_manager.pyi +0 -0
  32. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/execution_context.py +0 -0
  33. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/execution_context.pyi +0 -0
  34. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  35. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/telemetry.py +0 -0
  36. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/user_code_event_loop.py +0 -0
  37. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_runtime/user_code_imports.py +0 -0
  38. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_serialization.py +0 -0
  39. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_traceback.py +0 -0
  40. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_tunnel.py +0 -0
  41. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_tunnel.pyi +0 -0
  42. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_type_manager.py +0 -0
  43. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/__init__.py +0 -0
  44. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/app_utils.py +0 -0
  45. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/async_utils.py +0 -0
  46. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/auth_token_manager.py +0 -0
  47. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/bytes_io_segment_payload.py +0 -0
  48. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/deprecation.py +0 -0
  49. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/docker_utils.py +0 -0
  50. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/function_utils.py +0 -0
  51. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/git_utils.py +0 -0
  52. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/grpc_testing.py +0 -0
  53. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/grpc_utils.py +0 -0
  54. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/hash_utils.py +0 -0
  55. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/http_utils.py +0 -0
  56. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/jwt_utils.py +0 -0
  57. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/logger.py +0 -0
  58. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/mount_utils.py +0 -0
  59. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/name_utils.py +0 -0
  60. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/package_utils.py +0 -0
  61. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/pattern_utils.py +0 -0
  62. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/rand_pb_testing.py +0 -0
  63. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/shell_utils.py +0 -0
  64. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_utils/time_utils.py +0 -0
  65. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_vendor/__init__.py +0 -0
  66. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  67. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_vendor/cloudpickle.py +0 -0
  68. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_vendor/tblib.py +0 -0
  69. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/_watcher.py +0 -0
  70. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/app.py +0 -0
  71. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/app.pyi +0 -0
  72. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/billing.py +0 -0
  73. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2023.12.312.txt +0 -0
  74. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2023.12.txt +0 -0
  75. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2024.04.txt +0 -0
  76. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2024.10.txt +0 -0
  77. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/2025.06.txt +0 -0
  78. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/PREVIEW.txt +0 -0
  79. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/README.md +0 -0
  80. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/builder/base-images.json +0 -0
  81. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/call_graph.py +0 -0
  82. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/__init__.py +0 -0
  83. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/_download.py +0 -0
  84. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/_traceback.py +0 -0
  85. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/app.py +0 -0
  86. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/cluster.py +0 -0
  87. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/config.py +0 -0
  88. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/container.py +0 -0
  89. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/dict.py +0 -0
  90. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/entry_point.py +0 -0
  91. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/environment.py +0 -0
  92. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/import_refs.py +0 -0
  93. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/launch.py +0 -0
  94. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/network_file_system.py +0 -0
  95. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/profile.py +0 -0
  96. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/__init__.py +0 -0
  97. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/launch_instance_ssh.py +0 -0
  98. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/run_jupyter.py +0 -0
  99. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/run_marimo.py +0 -0
  100. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/programs/vscode.py +0 -0
  101. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/queues.py +0 -0
  102. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/run.py +0 -0
  103. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/secret.py +0 -0
  104. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/shell.py +0 -0
  105. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/token.py +0 -0
  106. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/utils.py +0 -0
  107. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cli/volume.py +0 -0
  108. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/client.py +0 -0
  109. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cloud_bucket_mount.py +0 -0
  110. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cloud_bucket_mount.pyi +0 -0
  111. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cls.py +0 -0
  112. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/cls.pyi +0 -0
  113. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/config.py +0 -0
  114. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/container_process.py +0 -0
  115. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/container_process.pyi +0 -0
  116. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/dict.py +0 -0
  117. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/dict.pyi +0 -0
  118. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/environments.py +0 -0
  119. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/environments.pyi +0 -0
  120. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/exception.py +0 -0
  121. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/experimental/__init__.py +0 -0
  122. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/experimental/flash.py +0 -0
  123. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/experimental/flash.pyi +0 -0
  124. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/experimental/ipython.py +0 -0
  125. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/file_io.py +0 -0
  126. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/file_io.pyi +0 -0
  127. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/file_pattern_matcher.py +0 -0
  128. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/functions.py +0 -0
  129. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/functions.pyi +0 -0
  130. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/gpu.py +0 -0
  131. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/image.py +0 -0
  132. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/image.pyi +0 -0
  133. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/io_streams.py +0 -0
  134. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/io_streams.pyi +0 -0
  135. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/mount.pyi +0 -0
  136. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/network_file_system.py +0 -0
  137. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/network_file_system.pyi +0 -0
  138. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/object.py +0 -0
  139. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/object.pyi +0 -0
  140. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/output.py +0 -0
  141. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/parallel_map.py +0 -0
  142. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/parallel_map.pyi +0 -0
  143. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/partial_function.py +0 -0
  144. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/partial_function.pyi +0 -0
  145. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/proxy.py +0 -0
  146. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/proxy.pyi +0 -0
  147. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/py.typed +0 -0
  148. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/queue.py +0 -0
  149. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/queue.pyi +0 -0
  150. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/retries.py +0 -0
  151. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/runner.py +0 -0
  152. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/runner.pyi +0 -0
  153. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/running_app.py +0 -0
  154. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/sandbox.py +0 -0
  155. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/sandbox.pyi +0 -0
  156. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/schedule.py +0 -0
  157. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/scheduler_placement.py +0 -0
  158. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/secret.py +0 -0
  159. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/secret.pyi +0 -0
  160. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/serving.py +0 -0
  161. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/serving.pyi +0 -0
  162. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/snapshot.py +0 -0
  163. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/snapshot.pyi +0 -0
  164. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/stream_type.py +0 -0
  165. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/token_flow.py +0 -0
  166. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/token_flow.pyi +0 -0
  167. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal/volume.pyi +0 -0
  168. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/SOURCES.txt +0 -0
  169. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/dependency_links.txt +0 -0
  170. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/entry_points.txt +0 -0
  171. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/requires.txt +0 -0
  172. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal.egg-info/top_level.txt +0 -0
  173. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/__init__.py +0 -0
  174. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/gen_cli_docs.py +0 -0
  175. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/gen_reference_docs.py +0 -0
  176. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/mdmd/__init__.py +0 -0
  177. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/mdmd/mdmd.py +0 -0
  178. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_docs/mdmd/signatures.py +0 -0
  179. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/__init__.py +0 -0
  180. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api.proto +0 -0
  181. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_grpc.py +0 -0
  182. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_pb2.py +0 -0
  183. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_pb2.pyi +0 -0
  184. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_pb2_grpc.py +0 -0
  185. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/api_pb2_grpc.pyi +0 -0
  186. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/modal_api_grpc.py +0 -0
  187. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/py.typed +0 -0
  188. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router.proto +0 -0
  189. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_grpc.py +0 -0
  190. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2.py +0 -0
  191. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2.pyi +0 -0
  192. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  193. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  194. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/modal_version/__main__.py +0 -0
  195. {modal-1.2.5.dev26 → modal-1.2.5.dev28}/pyproject.toml +0 -0
  196. {modal-1.2.5.dev26 → 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.dev26
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
@@ -371,11 +371,17 @@ class FileUploadSpec:
371
371
  mount_filename: str
372
372
 
373
373
  use_blob: bool
374
- content: Optional[bytes] # typically None if using blob, required otherwise
375
374
  sha256_hex: str
376
375
  md5_hex: str
377
376
  mode: int # file permission bits (last 12 bits of st_mode)
378
377
  size: int
378
+ content: Optional[bytes] = None # Set for very small files to avoid double-read
379
+
380
+ def read_content(self) -> bytes:
381
+ """Read content from source."""
382
+ with self.source() as fp:
383
+ fp.seek(0)
384
+ return fp.read()
379
385
 
380
386
 
381
387
  def _get_file_upload_spec(
@@ -384,6 +390,7 @@ def _get_file_upload_spec(
384
390
  mount_filename: PurePosixPath,
385
391
  mode: int,
386
392
  ) -> FileUploadSpec:
393
+ content = None
387
394
  with source() as fp:
388
395
  # Current position is ignored - we always upload from position 0
389
396
  fp.seek(0, os.SEEK_END)
@@ -394,12 +401,18 @@ def _get_file_upload_spec(
394
401
  # TODO(dano): remove the placeholder md5 once we stop requiring md5 for blobs
395
402
  md5_hex = "baadbaadbaadbaadbaadbaadbaadbaad" if size > MULTIPART_UPLOAD_THRESHOLD else None
396
403
  use_blob = True
397
- content = None
398
404
  hashes = get_upload_hashes(fp, md5_hex=md5_hex)
399
405
  else:
400
406
  use_blob = False
401
- content = fp.read()
402
- hashes = get_upload_hashes(content)
407
+ # For very small files (< 256 KiB), read content once and cache it
408
+ # This avoids double-read penalty while limiting memory usage
409
+ if size < 256 * 1024: # 256 KiB threshold
410
+ fp.seek(0)
411
+ content = fp.read()
412
+ hashes = get_upload_hashes(content)
413
+ else:
414
+ # For medium files (256 KiB - 4 MiB), compute hashes without caching content
415
+ hashes = get_upload_hashes(fp)
403
416
 
404
417
  return FileUploadSpec(
405
418
  source=source,
@@ -407,11 +420,11 @@ def _get_file_upload_spec(
407
420
  source_is_path=isinstance(source_description, Path),
408
421
  mount_filename=mount_filename.as_posix(),
409
422
  use_blob=use_blob,
410
- content=content,
411
423
  sha256_hex=hashes.sha256_hex(),
412
424
  md5_hex=hashes.md5_hex(),
413
425
  mode=mode & 0o7777,
414
426
  size=size,
427
+ content=content,
415
428
  )
416
429
 
417
430
 
@@ -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.dev26",
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.dev26",
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.
@@ -466,16 +466,18 @@ class _Mount(_Object, type_prefix="mo"):
466
466
  loop = asyncio.get_event_loop()
467
467
  with concurrent.futures.ThreadPoolExecutor() as exe:
468
468
  all_files = await loop.run_in_executor(exe, _select_files, entries)
469
+ logger.debug(f"Computing checksums for {len(all_files)} files using {exe._max_workers} worker threads")
469
470
 
470
- futs = []
471
+ # Yield FileUploadSpec objects lazily as they're consumed by async_map downstream.
472
+ # async_map's concurrency limit provides natural backpressure, so we don't need
473
+ # a separate semaphore here. This keeps memory bounded without creating all tasks upfront.
471
474
  for local_filename, remote_filename in all_files:
472
- logger.debug(f"Mounting {local_filename} as {remote_filename}")
473
- futs.append(loop.run_in_executor(exe, get_file_upload_spec_from_path, local_filename, remote_filename))
474
-
475
- logger.debug(f"Computing checksums for {len(futs)} files using {exe._max_workers} worker threads")
476
- for fut in asyncio.as_completed(futs):
477
475
  try:
478
- yield await fut
476
+ logger.debug(f"Mounting {local_filename} as {remote_filename}")
477
+ file_spec = await loop.run_in_executor(
478
+ exe, get_file_upload_spec_from_path, local_filename, remote_filename
479
+ )
480
+ yield file_spec
479
481
  except FileNotFoundError as exc:
480
482
  # Can happen with temporary files (e.g. emacs will write temp files and delete them quickly)
481
483
  logger.info(f"Ignoring file not found: {exc}")
@@ -547,7 +549,11 @@ class _Mount(_Object, type_prefix="mo"):
547
549
  logger.debug(
548
550
  f"Uploading file {file_spec.source_description} to {remote_filename} ({file_spec.size} bytes)"
549
551
  )
550
- request2 = api_pb2.MountPutFileRequest(data=file_spec.content, sha256_hex=file_spec.sha256_hex)
552
+ if file_spec.content is None:
553
+ content = await asyncio.to_thread(file_spec.read_content)
554
+ else:
555
+ content = file_spec.content
556
+ request2 = api_pb2.MountPutFileRequest(data=content, sha256_hex=file_spec.sha256_hex)
551
557
 
552
558
  start_time = time.monotonic()
553
559
  while time.monotonic() - start_time < MOUNT_PUT_FILE_CLIENT_TIMEOUT:
@@ -1056,7 +1056,11 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
1056
1056
  logger.debug(
1057
1057
  f"Uploading file {file_spec.source_description} to {remote_filename} ({file_spec.size} bytes)"
1058
1058
  )
1059
- request2 = api_pb2.MountPutFileRequest(data=file_spec.content, sha256_hex=file_spec.sha256_hex)
1059
+ if file_spec.content is None:
1060
+ content = await asyncio.to_thread(file_spec.read_content)
1061
+ else:
1062
+ content = file_spec.content
1063
+ request2 = api_pb2.MountPutFileRequest(data=content, sha256_hex=file_spec.sha256_hex)
1060
1064
  self._progress_cb(task_id=progress_task_id, complete=True)
1061
1065
 
1062
1066
  while (time.monotonic() - start_time) < VOLUME_PUT_FILE_CLIENT_TIMEOUT:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.5.dev26
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.dev26"
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