modal 1.6.4.dev9__tar.gz → 1.6.4.dev11__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 (254) hide show
  1. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/PKG-INFO +1 -1
  2. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/__init__.py +11 -0
  3. modal-1.6.4.dev11/modal/_audit_logs/__init__.py +1 -0
  4. modal-1.6.4.dev11/modal/_audit_logs/_rpcs.py +66 -0
  5. modal-1.6.4.dev11/modal/_audit_logs/count.py +17 -0
  6. modal-1.6.4.dev11/modal/_audit_logs/list.py +83 -0
  7. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_function_variants.py +1 -0
  8. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_functions.py +39 -4
  9. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_image.py +24 -6
  10. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_type_manager.py +7 -0
  11. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/name_utils.py +8 -1
  12. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/profile.py +40 -29
  13. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/shell.py +1 -1
  14. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/token.py +83 -14
  15. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/client.py +1 -0
  16. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/client.pyi +2 -2
  17. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/config.py +1 -0
  18. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/functions.pyi +7 -0
  19. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/token_flow.py +32 -2
  20. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/token_flow.pyi +2 -0
  21. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/PKG-INFO +1 -1
  22. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/SOURCES.txt +4 -0
  23. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_grpc.py +48 -0
  24. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_pb2.py +1832 -1633
  25. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_pb2.pyi +447 -14
  26. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_pb2_grpc.py +101 -0
  27. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_pb2_grpc.pyi +34 -0
  28. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/modal_api_grpc.py +3 -0
  29. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_version/__init__.py +1 -1
  30. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/pyproject.toml +1 -0
  31. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/LICENSE +0 -0
  32. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/README.md +0 -0
  33. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/__main__.py +0 -0
  34. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_billing.py +0 -0
  35. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_cluster.py +0 -0
  36. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_cluster.pyi +0 -0
  37. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_container_entrypoint.py +0 -0
  38. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_environments.py +0 -0
  39. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_grpc_client.py +0 -0
  40. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_ipython.py +0 -0
  41. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_load_context.py +0 -0
  42. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_location.py +0 -0
  43. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_logs.py +0 -0
  44. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_logs_manager.py +0 -0
  45. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_logs_manager.pyi +0 -0
  46. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_metrics.py +0 -0
  47. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_metrics.pyi +0 -0
  48. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_object.py +0 -0
  49. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_outbound_policy.py +0 -0
  50. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_outbound_policy.pyi +0 -0
  51. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/__init__.py +0 -0
  52. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/manager.py +0 -0
  53. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/pty.py +0 -0
  54. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/rich.py +0 -0
  55. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/status.py +0 -0
  56. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_partial_function.py +0 -0
  57. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_resolver.py +0 -0
  58. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_resources.py +0 -0
  59. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/__init__.py +0 -0
  60. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/asgi.py +0 -0
  61. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/container_io_manager.py +0 -0
  62. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/container_io_manager.pyi +0 -0
  63. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/execution_context.py +0 -0
  64. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/execution_context.pyi +0 -0
  65. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  66. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/task_lifecycle_manager.py +0 -0
  67. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  68. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/telemetry.py +0 -0
  69. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/user_code_event_loop.py +0 -0
  70. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/user_code_imports.py +0 -0
  71. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_serialization.py +0 -0
  72. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_server.py +0 -0
  73. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_server.pyi +0 -0
  74. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_supports_logs.py +0 -0
  75. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_traceback.py +0 -0
  76. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_tunnel.py +0 -0
  77. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_tunnel.pyi +0 -0
  78. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/__init__.py +0 -0
  79. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/app_utils.py +0 -0
  80. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/async_utils.py +0 -0
  81. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/auth_token_manager.py +0 -0
  82. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/blob_utils.py +0 -0
  83. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/browser_utils.py +0 -0
  84. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/bytes_io_segment_payload.py +0 -0
  85. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/curl_utils.py +0 -0
  86. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/deprecation.py +0 -0
  87. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/display_utils.py +0 -0
  88. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/docker_utils.py +0 -0
  89. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/function_utils.py +0 -0
  90. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/git_utils.py +0 -0
  91. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/grpc_testing.py +0 -0
  92. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/grpc_utils.py +0 -0
  93. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/hash_utils.py +0 -0
  94. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/http_utils.py +0 -0
  95. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/idle_countdown.py +0 -0
  96. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/jwt_utils.py +0 -0
  97. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/logger.py +0 -0
  98. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/mount_utils.py +0 -0
  99. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/oauth.py +0 -0
  100. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/package_utils.py +0 -0
  101. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/pattern_utils.py +0 -0
  102. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/proxy_support.py +0 -0
  103. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/rand_pb_testing.py +0 -0
  104. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/sandbox_fs_utils.py +0 -0
  105. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/shell_utils.py +0 -0
  106. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/time_utils.py +0 -0
  107. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/__init__.py +0 -0
  108. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  109. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/cloudpickle.py +0 -0
  110. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/tblib.py +0 -0
  111. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/version.py +0 -0
  112. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_watcher.py +0 -0
  113. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_workspace.py +0 -0
  114. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/app.py +0 -0
  115. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/app.pyi +0 -0
  116. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/billing.py +0 -0
  117. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/billing.pyi +0 -0
  118. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2023.12.312.txt +0 -0
  119. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2023.12.txt +0 -0
  120. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2024.04.txt +0 -0
  121. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2024.10.txt +0 -0
  122. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2025.06.txt +0 -0
  123. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2026.10.txt +0 -0
  124. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/PREVIEW.txt +0 -0
  125. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/README.md +0 -0
  126. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/base-images.json +0 -0
  127. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/call_graph.py +0 -0
  128. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/__init__.py +0 -0
  129. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_download.py +0 -0
  130. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_help.py +0 -0
  131. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_logs.py +0 -0
  132. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_stats.py +0 -0
  133. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_traceback.py +0 -0
  134. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/app.py +0 -0
  135. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/billing.py +0 -0
  136. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/changelog.py +0 -0
  137. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/config.py +0 -0
  138. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/container.py +0 -0
  139. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/curl.py +0 -0
  140. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/dashboard.py +0 -0
  141. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/dict.py +0 -0
  142. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/endpoint.py +0 -0
  143. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/entry_point.py +0 -0
  144. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/environment.py +0 -0
  145. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/function.py +0 -0
  146. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/image.py +0 -0
  147. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/import_refs.py +0 -0
  148. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/logo.py +0 -0
  149. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/network_file_system.py +0 -0
  150. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/queues.py +0 -0
  151. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/run.py +0 -0
  152. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/secret.py +0 -0
  153. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/selector.py +0 -0
  154. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/server.py +0 -0
  155. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/skills.py +0 -0
  156. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/utils.py +0 -0
  157. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/volume.py +0 -0
  158. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/workspace.py +0 -0
  159. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cloud_bucket_mount.py +0 -0
  160. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cloud_bucket_mount.pyi +0 -0
  161. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cls.py +0 -0
  162. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cls.pyi +0 -0
  163. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cluster.py +0 -0
  164. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cluster.pyi +0 -0
  165. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/container_process.py +0 -0
  166. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/container_process.pyi +0 -0
  167. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/dict.py +0 -0
  168. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/dict.pyi +0 -0
  169. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/environments.py +0 -0
  170. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/environments.pyi +0 -0
  171. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/exception.py +0 -0
  172. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/__init__.py +0 -0
  173. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/_flash_container_entry.py +0 -0
  174. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/flash.py +0 -0
  175. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/flash.pyi +0 -0
  176. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/flash_server.py +0 -0
  177. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/flash_server.pyi +0 -0
  178. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/ipython.py +0 -0
  179. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/file_pattern_matcher.py +0 -0
  180. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/functions.py +0 -0
  181. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/image.py +0 -0
  182. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/image.pyi +0 -0
  183. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/io_streams.py +0 -0
  184. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/io_streams.pyi +0 -0
  185. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/mount.py +0 -0
  186. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/mount.pyi +0 -0
  187. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/network_file_system.py +0 -0
  188. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/network_file_system.pyi +0 -0
  189. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/object.py +0 -0
  190. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/object.pyi +0 -0
  191. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/output.py +0 -0
  192. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/parallel_map.py +0 -0
  193. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/parallel_map.pyi +0 -0
  194. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/partial_function.py +0 -0
  195. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/partial_function.pyi +0 -0
  196. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/proxy.py +0 -0
  197. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/proxy.pyi +0 -0
  198. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/py.typed +0 -0
  199. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/queue.py +0 -0
  200. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/queue.pyi +0 -0
  201. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/retries.py +0 -0
  202. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/runner.py +0 -0
  203. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/runner.pyi +0 -0
  204. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/running_app.py +0 -0
  205. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/__init__.py +0 -0
  206. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_common.py +0 -0
  207. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_filesystem.py +0 -0
  208. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_impl_persistent.py +0 -0
  209. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_impl_v1.py +0 -0
  210. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_impl_v2.py +0 -0
  211. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_sandbox.py +0 -0
  212. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_sidecar.py +0 -0
  213. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_snapshot.py +0 -0
  214. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_task_command_router_client.py +0 -0
  215. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/sandbox.py +0 -0
  216. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/sandbox.pyi +0 -0
  217. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/schedule.py +0 -0
  218. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/scheduler_placement.py +0 -0
  219. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/secret.py +0 -0
  220. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/secret.pyi +0 -0
  221. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/server.py +0 -0
  222. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/server.pyi +0 -0
  223. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/serving.py +0 -0
  224. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/serving.pyi +0 -0
  225. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/skills/modal/SKILL.md +0 -0
  226. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/stream_type.py +0 -0
  227. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/types.py +0 -0
  228. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/volume.py +0 -0
  229. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/volume.pyi +0 -0
  230. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/workspace.py +0 -0
  231. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/workspace.pyi +0 -0
  232. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/dependency_links.txt +0 -0
  233. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/entry_points.txt +0 -0
  234. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/requires.txt +0 -0
  235. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/top_level.txt +0 -0
  236. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/__init__.py +0 -0
  237. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/gen_cli_docs.py +0 -0
  238. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/gen_cli_docs_main.py +0 -0
  239. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/gen_reference_docs.py +0 -0
  240. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/gen_reference_docs_main.py +0 -0
  241. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/mdmd/__init__.py +0 -0
  242. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/mdmd/mdmd.py +0 -0
  243. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/mdmd/signatures.py +0 -0
  244. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/mdmd/types.py +0 -0
  245. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/reference_intro.md +0 -0
  246. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/__init__.py +0 -0
  247. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/py.typed +0 -0
  248. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_grpc.py +0 -0
  249. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_pb2.py +0 -0
  250. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_pb2.pyi +0 -0
  251. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  252. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  253. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_version/__main__.py +0 -0
  254. {modal-1.6.4.dev9 → modal-1.6.4.dev11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.6.4.dev9
3
+ Version: 1.6.4.dev11
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -1,4 +1,5 @@
1
1
  # Copyright Modal Labs 2022
2
+ import os
2
3
  import sys
3
4
 
4
5
  if sys.version_info[:2] < (3, 10):
@@ -59,6 +60,16 @@ except Exception:
59
60
  print()
60
61
  raise
61
62
 
63
+ if sys.version_info[:2] == (3, 10) and os.environ.get("MODAL_IS_REMOTE") != "1":
64
+ from ._utils.deprecation import deprecation_warning
65
+
66
+ deprecation_warning(
67
+ (2026, 11, 4),
68
+ "Python 3.10 is EOL and Modal will not support it in version 1.7.0. "
69
+ "Please upgrade your local Python to 3.11 or later.",
70
+ show_source=False,
71
+ )
72
+
62
73
 
63
74
  __all__ = [
64
75
  "__version__",
@@ -0,0 +1 @@
1
+ # Copyright Modal Labs 2026
@@ -0,0 +1,66 @@
1
+ # Copyright Modal Labs 2026
2
+ """The audit log RPCs, one call each, retried with backoff. Ordering and batching live in list.py."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Awaitable, Callable
7
+ from typing import TYPE_CHECKING, TypeVar
8
+
9
+ from modal._utils.grpc_utils import Retry
10
+ from modal_proto import api_pb2
11
+
12
+ if TYPE_CHECKING:
13
+ from modal.client import _Client
14
+
15
+
16
+ _Resp = TypeVar("_Resp")
17
+
18
+
19
+ _MAX_RETRIES = 4
20
+ _PLAN_MAX_RETRIES = 2
21
+
22
+
23
+ async def _call(
24
+ rpc: Callable[..., Awaitable[_Resp]], req: object, *, timeout: float, max_retries: int = _MAX_RETRIES
25
+ ) -> _Resp:
26
+ """Call with backoff on timeouts and transient errors; rate and resource limits wait as the server says."""
27
+ retry = Retry(base_delay=1.0, delay_factor=2.0, max_delay=8.0, max_retries=max_retries, attempt_timeout=timeout)
28
+ return await rpc(req, retry=retry)
29
+
30
+
31
+ async def get_settings(client: _Client) -> api_pb2.AuditLogSettingsGetResponse:
32
+ return await client._stub.AuditLogSettingsGet(api_pb2.AuditLogSettingsGetRequest())
33
+
34
+
35
+ async def count_entries(
36
+ client: _Client, q: str, oldest_ms: int, newest_ms: int, config: api_pb2.AuditLogSettingsGetResponse
37
+ ) -> int:
38
+ req = api_pb2.AuditLogCountRequest(q=q, oldest_ms=oldest_ms, newest_ms=newest_ms)
39
+ resp = await _call(client._stub.AuditLogCount, req, timeout=config.request_timeout_secs)
40
+ return resp.count
41
+
42
+
43
+ async def list_page(
44
+ client: _Client,
45
+ q: str,
46
+ oldest_ms: int,
47
+ newest_ms: int,
48
+ limit: int,
49
+ config: api_pb2.AuditLogSettingsGetResponse,
50
+ newest_cursor: api_pb2.AuditLogCursor | None = None,
51
+ oldest_cursor: api_pb2.AuditLogCursor | None = None,
52
+ ) -> api_pb2.AuditLogListResponse:
53
+ req = api_pb2.AuditLogListRequest(q=q, oldest_ms=oldest_ms, newest_ms=newest_ms, limit=limit)
54
+ if newest_cursor is not None:
55
+ req.newest_cursor.CopyFrom(newest_cursor)
56
+ if oldest_cursor is not None:
57
+ req.oldest_cursor.CopyFrom(oldest_cursor)
58
+ return await _call(client._stub.AuditLogList, req, timeout=config.request_timeout_secs)
59
+
60
+
61
+ async def plan_buckets(
62
+ client: _Client, q: str, oldest_ms: int, newest_ms: int, config: api_pb2.AuditLogSettingsGetResponse
63
+ ) -> list[api_pb2.AuditLogCursor]:
64
+ req = api_pb2.AuditLogBucketsRequest(q=q, oldest_ms=oldest_ms, newest_ms=newest_ms, bucket_size=config.bucket_size)
65
+ resp = await _call(client._stub.AuditLogPlan, req, timeout=config.plan_timeout_secs, max_retries=_PLAN_MAX_RETRIES)
66
+ return list(resp.cursors)
@@ -0,0 +1,17 @@
1
+ # Copyright Modal Labs 2026
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ from modal._audit_logs import _rpcs
7
+ from modal_proto import api_pb2
8
+
9
+ if TYPE_CHECKING:
10
+ from modal.client import _Client
11
+
12
+
13
+ async def count_audit_logs(
14
+ client: _Client, q: str, oldest_ms: int, newest_ms: int, config: api_pb2.AuditLogSettingsGetResponse
15
+ ) -> int:
16
+ """Count the entries in [oldest_ms, newest_ms) that match the query."""
17
+ return await _rpcs.count_entries(client, q, oldest_ms, newest_ms, config)
@@ -0,0 +1,83 @@
1
+ # Copyright Modal Labs 2026
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import AsyncGenerator
5
+ from contextlib import aclosing
6
+
7
+ from modal._audit_logs import _rpcs
8
+ from modal._utils.async_utils import async_map_ordered
9
+ from modal.client import _Client
10
+ from modal_proto import api_pb2
11
+
12
+ # (newest_cursor, oldest_cursor); None where the bucket is open-ended.
13
+ Bucket = tuple[api_pb2.AuditLogCursor | None, api_pb2.AuditLogCursor | None]
14
+
15
+
16
+ async def get_bucket_cursors(
17
+ client: _Client, q: str, oldest_ms: int, newest_ms: int, config: api_pb2.AuditLogSettingsGetResponse
18
+ ) -> list[Bucket]:
19
+ """Split the range into buckets, as (newest_cursor, oldest_cursor) pairs ordered oldest to newest.
20
+
21
+ The first bucket has no oldest cursor and the last has no newest cursor.
22
+ """
23
+ cursors = await _rpcs.plan_buckets(client, q, oldest_ms, newest_ms, config)
24
+ return list(zip([None, *cursors], [*cursors, None]))[::-1]
25
+
26
+
27
+ async def _fetch_bucket(
28
+ client: _Client,
29
+ q: str,
30
+ oldest_ms: int,
31
+ newest_ms: int,
32
+ config: api_pb2.AuditLogSettingsGetResponse,
33
+ bucket: Bucket,
34
+ ) -> list[api_pb2.AuditLogEntry]:
35
+ """Read one bucket's entries, oldest to newest, using pagination"""
36
+ newest_cursor, oldest_cursor = bucket
37
+ entries: list[api_pb2.AuditLogEntry] = []
38
+ while True:
39
+ resp = await _rpcs.list_page(
40
+ client, q, oldest_ms, newest_ms, config.page_size, config, newest_cursor, oldest_cursor
41
+ )
42
+ entries.extend(resp.items)
43
+ # Skips the empty request a full last page would otherwise trigger; an unset next_cursor also ends the loop.
44
+ if oldest_cursor is not None and resp.items and resp.items[-1].id == oldest_cursor.id:
45
+ break
46
+ if not resp.HasField("next_cursor"):
47
+ break
48
+ newest_cursor = resp.next_cursor
49
+ entries.reverse()
50
+ return entries
51
+
52
+
53
+ async def fetch_audit_logs(
54
+ q: str,
55
+ oldest_ms: int,
56
+ newest_ms: int,
57
+ *,
58
+ client: _Client | None = None,
59
+ config: api_pb2.AuditLogSettingsGetResponse,
60
+ ) -> AsyncGenerator[api_pb2.AuditLogEntry]:
61
+ """Yield the range's entries from the oldest to the newest, one bucket at a time.
62
+
63
+ At most `max_concurrent_requests` buckets are fetched at once, and at most `max_buffered_buckets` are fetched
64
+ or held at once. A new bucket is admitted only after the consumer has taken all of a bucket's entries, so a slow
65
+ consumer or a stalled request cannot grow memory.
66
+ If any request fails after its attempts, the remaining requests are cancelled and the error is raised.
67
+ """
68
+ if client is None:
69
+ client = await _Client.from_env()
70
+ buckets = await get_bucket_cursors(client, q, oldest_ms, newest_ms, config)
71
+
72
+ async def bucket_stream() -> AsyncGenerator[Bucket]:
73
+ for bucket in buckets:
74
+ yield bucket
75
+
76
+ async def fetch(bucket: Bucket) -> list[api_pb2.AuditLogEntry]:
77
+ return await _fetch_bucket(client, q, oldest_ms, newest_ms, config, bucket)
78
+
79
+ fetched = async_map_ordered(bucket_stream(), fetch, config.max_concurrent_requests, config.max_buffered_buckets)
80
+ async with aclosing(fetched) as stream:
81
+ async for entries in stream:
82
+ for entry in entries:
83
+ yield entry
@@ -408,6 +408,7 @@ def _make_function_variant(
408
408
  if not base_function._is_hydrated:
409
409
  await base_function.hydrate(load_context.client)
410
410
 
411
+ base_function._check_not_observability_only()
411
412
  assert base_function._client and base_function._client._stub
412
413
 
413
414
  if parameter_schema is None:
@@ -119,6 +119,15 @@ if TYPE_CHECKING:
119
119
  import modal.functions
120
120
 
121
121
  MAX_INTERNAL_FAILURE_COUNT = 8
122
+ _OBSERVABILITY_ONLY_APIS = (
123
+ ".logs",
124
+ ".metrics",
125
+ ".info()",
126
+ ".stats()",
127
+ ".get_current_stats()",
128
+ ".get_web_url()",
129
+ ".autoscaler.settings()",
130
+ )
122
131
  TERMINAL_STATUSES = (
123
132
  api_pb2.GenericResult.GENERIC_STATUS_SUCCESS,
124
133
  api_pb2.GenericResult.GENERIC_STATUS_TERMINATED,
@@ -651,6 +660,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
651
660
  _base_function: "_Function | None" = None
652
661
  _app_id: str | None = None
653
662
  _function_info: FunctionInfo | None = None
663
+ _is_observability_only: bool = False
654
664
 
655
665
  async def _get_log_query_data(self) -> _LogQueryData:
656
666
  await self.hydrate()
@@ -1398,6 +1408,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1398
1408
 
1399
1409
  """
1400
1410
 
1411
+ self._check_not_observability_only()
1401
1412
  deprecation_warning(
1402
1413
  (2026, 10, 16),
1403
1414
  "`Function.update_autoscaler()` is deprecated and will be removed in a future version "
@@ -1485,16 +1496,15 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1485
1496
  # refine the error message
1486
1497
  msg = f"Lookup failed for {called_from} '{function_id}': {exc}."
1487
1498
  raise NotFoundError(msg) from None
1488
- if called_from == "Function" and (response.function.is_server or response.function.is_class):
1489
- if response.function.is_server:
1490
- raise InvalidError(f"{function_id} is a Server. Use \n ``modal.Server.from_id('{function_id}')``")
1491
- raise InvalidError(f"{function_id} is a Cls and cannot be loaded with Function.from_id().")
1499
+ if called_from == "Function" and response.function.is_server:
1500
+ raise InvalidError(f"{function_id} is a Server. Use \n ``modal.Server.from_id('{function_id}')``")
1492
1501
  if called_from == "Server" and not response.function.is_server:
1493
1502
  if response.function.is_class:
1494
1503
  raise InvalidError(f"{function_id} is a Cls and cannot be loaded with Server.from_id().")
1495
1504
  raise InvalidError(f"{function_id} is a Function. Use \n ``modal.Function.from_id('{function_id}')``")
1496
1505
  self._hydrate(function_id, load_context.client, response.handle_metadata)
1497
1506
  self._function_info = FunctionInfo._from_function_proto(response.function)
1507
+ self._is_observability_only = called_from == "Function" and response.function.is_class
1498
1508
 
1499
1509
  rep = f"modal.Function.from_id('{function_id}')"
1500
1510
  return cls._from_loader(
@@ -1514,6 +1524,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1514
1524
  object with metadata from Modal servers until the first
1515
1525
  time it is actually used.
1516
1526
 
1527
+ If the ID belongs to a Cls, the Function only supports the following APIs:
1528
+ `.logs`, `.metrics`, `.info()`, `.stats()`, `.get_current_stats()`,
1529
+ `.get_web_url()` and `.autoscaler.settings()`. Use `modal.Cls.from_name(...)`
1530
+ to invoke all other methods.
1531
+
1517
1532
  Args:
1518
1533
  function_id: ID of the function.
1519
1534
  client: Modal client to use; defaults to `Client.from_env()` when omitted.
@@ -1744,6 +1759,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1744
1759
  + f"or call it locally: {self._function_name}.local()"
1745
1760
  )
1746
1761
 
1762
+ def _check_not_observability_only(self):
1763
+ if self._is_observability_only:
1764
+ assert self._function_name
1765
+ class_name = self._function_name.removesuffix(".*")
1766
+ apis = [f"`{name}`" for name in _OBSERVABILITY_ONLY_APIS]
1767
+ allowed_apis = ", ".join(apis[:-1]) + " and " + apis[-1]
1768
+ raise InvalidError(
1769
+ f"`{self.object_id}` belongs to the Cls `{class_name}`. A Function loaded with "
1770
+ f"`Function.from_id` only supports {allowed_apis}. "
1771
+ "Use `modal.Cls.from_name(...)` to invoke all other methods."
1772
+ )
1773
+
1747
1774
  @live_method
1748
1775
  async def get_web_url(self) -> str | None:
1749
1776
  """URL for addressing a Web Function via HTTP.
@@ -1779,6 +1806,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1779
1806
  config_method_name: str,
1780
1807
  ) -> "_Function[P, ReturnType, OriginalReturnType]":
1781
1808
  base_function = self._base_function or self
1809
+ base_function._check_not_observability_only()
1782
1810
  combined_options = self._options.merge_options(new_options)
1783
1811
 
1784
1812
  # For "True Functions" (i.e. not the Function inside a Cls), we will use
@@ -1941,6 +1969,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1941
1969
  _SynchronizedQueue is used instead of asyncio.Queue so that the main thread can put
1942
1970
  items in the queue safely.
1943
1971
  """
1972
+ self._check_not_observability_only()
1944
1973
  self._check_no_web_url("map")
1945
1974
  if self._is_generator:
1946
1975
  raise InvalidError("A generator function cannot be called with `.map(...)`.")
@@ -1978,6 +2007,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1978
2007
 
1979
2008
  @live_method
1980
2009
  async def _spawn_map(self, input_queue: _SynchronizedQueue) -> "_FunctionCall[ReturnType]":
2010
+ self._check_not_observability_only()
1981
2011
  self._check_no_web_url("spawn_map")
1982
2012
  if self._is_generator:
1983
2013
  raise InvalidError("A generator function cannot be called with `.spawn_map(...)`.")
@@ -1995,6 +2025,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1995
2025
  return fc
1996
2026
 
1997
2027
  async def _call_function(self, args, kwargs) -> ReturnType:
2028
+ self._check_not_observability_only()
1998
2029
  invocation: _Invocation | _InputPlaneInvocation
1999
2030
  if self._input_plane_url:
2000
2031
  invocation = await _InputPlaneInvocation.create(
@@ -2024,6 +2055,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
2024
2055
  function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType",
2025
2056
  from_spawn_map: bool = False,
2026
2057
  ) -> _Invocation:
2058
+ self._check_not_observability_only()
2027
2059
  return await _Invocation.create(
2028
2060
  self,
2029
2061
  args,
@@ -2037,6 +2069,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
2037
2069
  @live_method_gen
2038
2070
  @synchronizer.no_input_translation
2039
2071
  async def _call_generator(self, args, kwargs):
2072
+ self._check_not_observability_only()
2040
2073
  invocation: _Invocation | _InputPlaneInvocation
2041
2074
  if self._input_plane_url:
2042
2075
  invocation = await _InputPlaneInvocation.create(
@@ -2093,6 +2126,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
2093
2126
  Values produced by the remote generator.
2094
2127
  """
2095
2128
  # TODO: Generics/TypeVars
2129
+ self._check_not_observability_only()
2096
2130
  self._check_no_web_url("remote_gen")
2097
2131
 
2098
2132
  if not self._is_generator:
@@ -2669,6 +2703,7 @@ class _FunctionAutoscalerManager:
2669
2703
  if not self._function._is_hydrated:
2670
2704
  await self._function.hydrate()
2671
2705
 
2706
+ self._function._check_not_observability_only()
2672
2707
  settings: api_pb2.AutoscalerSettings = api_pb2.AutoscalerSettings()
2673
2708
 
2674
2709
  if min_containers != UNSET_PARAMETER:
@@ -37,6 +37,7 @@ from ._serialization import get_preferred_payload_format, serialize
37
37
  from ._supports_logs import _ImageLogQueryData
38
38
  from ._utils.async_utils import TaskContext, deprecate_aio_usage, synchronizer
39
39
  from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
40
+ from ._utils.deprecation import deprecation_warning
40
41
  from ._utils.docker_utils import (
41
42
  extract_copy_command_patterns,
42
43
  find_dockerignore_file,
@@ -87,6 +88,23 @@ SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
87
88
  "2023.12": ["3.10", "3.11", "3.12"],
88
89
  }
89
90
 
91
+
92
+ def _uses_python_310(metadata: api_pb2.ImageMetadata | None) -> bool:
93
+ return metadata is not None and bool(re.match(r"Python 3\.10\.", metadata.python_version_info))
94
+
95
+
96
+ def _warn_if_python_310(image_id: str, metadata: api_pb2.ImageMetadata | None) -> None:
97
+ if _uses_python_310(metadata):
98
+ deprecation_warning(
99
+ (2026, 11, 4),
100
+ f"Image {image_id} uses Python 3.10, which is EOL. Modal 1.7.0 and later will not run or deploy apps "
101
+ "that use Images with Python 3.10; apps already deployed with older clients keep working. "
102
+ "Please update the Image to use Python 3.11 or later "
103
+ "(e.g. via `python_version=` or `add_python=`).",
104
+ show_source=False,
105
+ )
106
+
107
+
90
108
  LOCAL_REQUIREMENTS_DIR = Path(__file__).parent / "builder"
91
109
  CONTAINER_REQUIREMENTS_PATH = "/modal_requirements.txt"
92
110
 
@@ -877,6 +895,8 @@ class _Image(_Object, type_prefix="im"):
877
895
  raise ImageBuildError("Unknown status %s!" % result.status, image_id)
878
896
 
879
897
  self._hydrate(image_id, load_context.client, metadata)
898
+ if not any(_uses_python_310(base._metadata) for base in base_images.values()):
899
+ _warn_if_python_310(image_id, metadata)
880
900
  local_mounts: set[_Mount] = set()
881
901
  for base in base_images.values():
882
902
  local_mounts |= base._serve_mounts
@@ -1098,6 +1118,7 @@ class _Image(_Object, type_prefix="im"):
1098
1118
  async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
1099
1119
  resp = await load_context.client._stub.ImageFromId(api_pb2.ImageFromIdRequest(image_id=image_id))
1100
1120
  self._hydrate(resp.image_id, load_context.client, resp.metadata)
1121
+ _warn_if_python_310(resp.image_id, resp.metadata)
1101
1122
 
1102
1123
  rep = f"Image.from_id({image_id!r})"
1103
1124
 
@@ -2064,8 +2085,7 @@ class _Image(_Object, type_prefix="im"):
2064
2085
  if not isinstance(entrypoint_commands, list) or not all(isinstance(x, str) for x in entrypoint_commands):
2065
2086
  raise InvalidError("entrypoint_commands must be a list of strings.")
2066
2087
  args = _flatten_str_args("entrypoint", "entrypoint_commands", entrypoint_commands)
2067
- args_str = '"' + '", "'.join(args) + '"' if args else ""
2068
- dockerfile_cmd = f"ENTRYPOINT [{args_str}]"
2088
+ dockerfile_cmd = f"ENTRYPOINT {json.dumps(args, ensure_ascii=False)}"
2069
2089
 
2070
2090
  return self.dockerfile_commands(dockerfile_cmd)
2071
2091
 
@@ -2085,8 +2105,7 @@ class _Image(_Object, type_prefix="im"):
2085
2105
  if not isinstance(shell_commands, list) or not all(isinstance(x, str) for x in shell_commands):
2086
2106
  raise InvalidError("shell_commands must be a list of strings.")
2087
2107
  args = _flatten_str_args("shell", "shell_commands", shell_commands)
2088
- args_str = '"' + '", "'.join(args) + '"' if args else ""
2089
- dockerfile_cmd = f"SHELL [{args_str}]"
2108
+ dockerfile_cmd = f"SHELL {json.dumps(args, ensure_ascii=False)}"
2090
2109
 
2091
2110
  return self.dockerfile_commands(dockerfile_cmd)
2092
2111
 
@@ -3076,8 +3095,7 @@ class _Image(_Object, type_prefix="im"):
3076
3095
  raise InvalidError("Image CMD must be a list of strings.")
3077
3096
 
3078
3097
  cmd_args = _flatten_str_args("cmd", "cmd", cmd)
3079
- cmd_str = '"' + '", "'.join(cmd_args) + '"' if cmd_args else ""
3080
- dockerfile_cmd = f"CMD [{cmd_str}]"
3098
+ dockerfile_cmd = f"CMD {json.dumps(cmd_args, ensure_ascii=False)}"
3081
3099
 
3082
3100
  return self.dockerfile_commands(dockerfile_cmd)
3083
3101
 
@@ -1,4 +1,5 @@
1
1
  # Copyright Modal Labs 2025
2
+ import types
2
3
  import typing
3
4
  from typing import Any
4
5
 
@@ -186,6 +187,12 @@ class SchemaRegistry:
186
187
 
187
188
  def get_proto_generic_type(self, declared_type: type):
188
189
  if origin := typing_extensions.get_origin(declared_type):
190
+ if origin in (typing.Union, types.UnionType):
191
+ args = [arg for arg in typing_extensions.get_args(declared_type) if arg is not type(None)]
192
+ if len(args) == 1:
193
+ return self.get_proto_generic_type(args[0])
194
+ return unknown_type_schema(declared_type)
195
+
189
196
  base_type = origin
190
197
  else:
191
198
  base_type = declared_type
@@ -16,6 +16,13 @@ def replace_invalid_subdomain_chars(label: str) -> str:
16
16
  return re.sub("[^a-z0-9-]", "-", label.lower())
17
17
 
18
18
 
19
+ app_id_regex = re.compile("^ap-[a-zA-Z0-9]{22}$")
20
+
21
+
22
+ def is_app_id(identifier: str) -> bool:
23
+ return app_id_regex.match(identifier) is not None
24
+
25
+
19
26
  def is_valid_object_name(name: str) -> bool:
20
27
  return (
21
28
  # Limit object name length
@@ -23,7 +30,7 @@ def is_valid_object_name(name: str) -> bool:
23
30
  # Limit character set
24
31
  and re.match("^[a-zA-Z0-9-_.]+$", name) is not None
25
32
  # Avoid collisions with App IDs
26
- and re.match("^ap-[a-zA-Z0-9]{22}$", name) is None
33
+ and not is_app_id(name)
27
34
  )
28
35
 
29
36
 
@@ -5,11 +5,12 @@ import os
5
5
  from contextlib import suppress
6
6
 
7
7
  import click
8
- from rich.json import JSON
9
- from rich.table import Table
8
+ from rich.box import SIMPLE_HEAD
9
+ from rich.text import Text
10
10
 
11
11
  from modal import config as config_module
12
12
  from modal._utils.async_utils import synchronizer
13
+ from modal.cli.utils import display_table
13
14
  from modal.config import (
14
15
  Config,
15
16
  _lookup_workspace,
@@ -28,7 +29,7 @@ profile_cli = ModalGroup(name="profile", help="Switch between Modal profiles.")
28
29
  @profile_cli.command("list", help="Show all Modal profiles and highlight the active one.")
29
30
  @click.option("--json", is_flag=True, default=False)
30
31
  @synchronizer.create_blocking
31
- async def list_(json: bool | None = False):
32
+ async def list_(json: bool = False):
32
33
  config = Config()
33
34
  profiles = config_profiles()
34
35
  lookup_coros = [
@@ -44,22 +45,6 @@ async def list_(json: bool | None = False):
44
45
  ]
45
46
  responses = await asyncio.gather(*lookup_coros, return_exceptions=True)
46
47
 
47
- rows = []
48
- for profile, resp in zip(profiles, responses):
49
- active = profile == config_module._profile
50
- if isinstance(resp, AuthError):
51
- workspace = "Unknown (authentication failure)"
52
- elif isinstance(resp, TimeoutError):
53
- workspace = "Unknown (timed out)"
54
- elif isinstance(resp, Exception):
55
- # Catch-all for other exceptions, like incorrect server url
56
- workspace = "Unknown (profile misconfigured)"
57
- else:
58
- assert not isinstance(resp, BaseException)
59
- workspace = resp.username
60
- content = ["•" if active else "", profile, workspace]
61
- rows.append((active, content))
62
-
63
48
  env_based_workspace: str | None = None
64
49
  credential_env_vars = (
65
50
  "MODAL_TOKEN_ID",
@@ -87,18 +72,44 @@ async def list_(json: bool | None = False):
87
72
 
88
73
  output = OutputManager.get()
89
74
  highlight = "bold green" if env_based_workspace is None else "yellow"
90
- if json:
91
- json_data = []
92
- for active, content in rows:
93
- json_data.append({"name": content[1], "workspace": content[2], "active": active})
94
- output.print(JSON.from_data(json_data))
75
+
76
+ if not json:
77
+ table_columns = [" Profile", "Workspace"]
95
78
  else:
96
- table = Table(" ", "Profile", "Workspace")
97
- for active, content in rows:
98
- table.add_row(*content, style=highlight if active else "dim")
99
- output.print(table)
79
+ table_columns = ["name", "workspace", "active"]
80
+
81
+ table_rows: list[list[str | Text | bool]] = []
82
+ for profile, resp in zip(profiles, responses):
83
+ active = profile == config_module._profile
84
+
85
+ if isinstance(resp, AuthError):
86
+ workspace = "Unknown (authentication failure)"
87
+ elif isinstance(resp, TimeoutError):
88
+ workspace = "Unknown (timed out)"
89
+ elif isinstance(resp, Exception):
90
+ # Catch-all for other exceptions, like incorrect server url
91
+ workspace = "Unknown (profile misconfigured)"
92
+ else:
93
+ assert not isinstance(resp, BaseException)
94
+ workspace = resp.username
95
+
96
+ if json:
97
+ table_rows.append([profile, workspace, active])
98
+ continue
99
+
100
+ rendered_profile = f"• {profile}" if active else f" {profile}"
101
+ style = highlight if active else "dim"
102
+
103
+ table_rows.append(
104
+ [
105
+ Text(rendered_profile, style=style),
106
+ Text(workspace, style=style),
107
+ ]
108
+ )
109
+
110
+ display_table(table_columns, table_rows, json=json, table_box=SIMPLE_HEAD)
100
111
 
101
- if env_based_workspace is not None:
112
+ if not json and env_based_workspace is not None:
102
113
  output.print(
103
114
  f"[yellow]Using [bold]{env_based_workspace}[/bold] workspace based on environment variables[/yellow]"
104
115
  )
@@ -448,7 +448,7 @@ def shell(
448
448
  app = App("modal shell")
449
449
 
450
450
  # NB: invoking under bash makes --cmd a lot more flexible.
451
- cmds = shlex.split(f'/bin/bash -c "{cmd}"')
451
+ cmds = ["/bin/bash", "-c", cmd]
452
452
  timeout = 3600
453
453
  parsed_experimental_options = _parse_experimental_options(experimental_options)
454
454