modal 1.5.6.dev25__tar.gz → 1.5.6.dev27__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 (245) hide show
  1. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/PKG-INFO +1 -1
  2. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/__init__.py +6 -2
  3. modal-1.5.6.dev27/modal/_cluster.py +192 -0
  4. modal-1.5.6.dev27/modal/_cluster.pyi +108 -0
  5. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_container_entrypoint.py +3 -11
  6. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_environments.py +72 -15
  7. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_function_variants.py +75 -23
  8. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_functions.py +120 -97
  9. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_image.py +10 -7
  10. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_logs.py +5 -5
  11. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_logs_manager.py +2 -2
  12. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_object.py +0 -31
  13. modal-1.5.6.dev25/modal/outbound_policy.py → modal-1.5.6.dev27/modal/_outbound_policy.py +10 -4
  14. modal-1.5.6.dev25/modal/outbound_policy.pyi → modal-1.5.6.dev27/modal/_outbound_policy.pyi +10 -4
  15. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/pty.py +2 -2
  16. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_partial_function.py +116 -26
  17. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/container_io_manager.py +15 -15
  18. modal-1.5.6.dev27/modal/_runtime/gpu_memory_snapshot.py +56 -0
  19. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/task_lifecycle_manager.py +20 -32
  20. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/task_lifecycle_manager.pyi +0 -5
  21. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_server.py +189 -2
  22. modal-1.5.6.dev27/modal/_server.pyi +411 -0
  23. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_tunnel.py +2 -2
  24. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/function_utils.py +3 -3
  25. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/grpc_utils.py +10 -6
  26. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/task_command_router_client.py +16 -6
  27. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_workspace.py +34 -14
  28. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/app.py +55 -59
  29. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/app.pyi +29 -56
  30. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/app.py +18 -14
  31. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/container.py +5 -5
  32. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/curl.py +1 -1
  33. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/dashboard.py +1 -1
  34. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/endpoint.py +5 -5
  35. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/entry_point.py +0 -3
  36. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/function.py +9 -7
  37. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/image.py +2 -2
  38. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/network_file_system.py +1 -1
  39. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/queues.py +1 -1
  40. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/secret.py +1 -1
  41. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/server.py +5 -5
  42. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/shell.py +27 -10
  43. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/token.py +1 -1
  44. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/utils.py +4 -2
  45. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/workspace.py +24 -0
  46. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/client.py +44 -20
  47. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/client.pyi +61 -29
  48. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cloud_bucket_mount.py +16 -9
  49. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cloud_bucket_mount.pyi +32 -0
  50. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cls.py +14 -34
  51. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cls.pyi +0 -20
  52. modal-1.5.6.dev27/modal/cluster.py +7 -0
  53. modal-1.5.6.dev27/modal/cluster.pyi +108 -0
  54. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/config.py +2 -3
  55. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/dict.py +17 -17
  56. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/environments.py +2 -0
  57. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/environments.pyi +53 -0
  58. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/exception.py +4 -0
  59. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/__init__.py +39 -14
  60. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/_flash_container_entry.py +2 -2
  61. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/flash.py +5 -5
  62. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/flash_server.py +1 -1
  63. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/functions.pyi +24 -98
  64. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/io_streams.py +166 -75
  65. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/io_streams.pyi +65 -40
  66. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/mount.py +5 -5
  67. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/network_file_system.py +11 -11
  68. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/object.pyi +0 -15
  69. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/parallel_map.py +17 -17
  70. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/partial_function.py +4 -0
  71. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/partial_function.pyi +91 -0
  72. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/proxy.py +1 -1
  73. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/queue.py +14 -14
  74. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/runner.py +12 -12
  75. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/sandbox.py +116 -94
  76. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/sandbox.pyi +69 -35
  77. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/secret.py +9 -9
  78. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/server.pyi +77 -0
  79. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/snapshot.py +1 -1
  80. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/token_flow.py +1 -1
  81. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/types.py +259 -91
  82. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/volume.py +24 -24
  83. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/workspace.pyi +37 -0
  84. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/PKG-INFO +1 -1
  85. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/SOURCES.txt +7 -5
  86. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/mdmd/mdmd.py +24 -3
  87. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_grpc.py +16 -0
  88. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_pb2.py +1423 -1413
  89. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_pb2.pyi +28 -3
  90. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_pb2_grpc.py +33 -0
  91. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_pb2_grpc.pyi +10 -0
  92. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/modal_api_grpc.py +1 -0
  93. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_version/__init__.py +1 -1
  94. modal-1.5.6.dev25/modal/_clustered_functions.py +0 -97
  95. modal-1.5.6.dev25/modal/_clustered_functions.pyi +0 -38
  96. modal-1.5.6.dev25/modal/_runtime/gpu_memory_snapshot.py +0 -281
  97. modal-1.5.6.dev25/modal/cli/cluster.py +0 -117
  98. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/LICENSE +0 -0
  99. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/README.md +0 -0
  100. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/__main__.py +0 -0
  101. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_billing.py +0 -0
  102. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_grpc_client.py +0 -0
  103. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_ipython.py +0 -0
  104. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_load_context.py +0 -0
  105. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_location.py +0 -0
  106. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_logs_manager.pyi +0 -0
  107. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/__init__.py +0 -0
  108. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/manager.py +0 -0
  109. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/rich.py +0 -0
  110. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/status.py +0 -0
  111. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_resolver.py +0 -0
  112. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_resources.py +0 -0
  113. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/__init__.py +0 -0
  114. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/asgi.py +0 -0
  115. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/container_io_manager.pyi +0 -0
  116. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/execution_context.py +0 -0
  117. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/execution_context.pyi +0 -0
  118. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/telemetry.py +0 -0
  119. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/user_code_event_loop.py +0 -0
  120. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/user_code_imports.py +0 -0
  121. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_serialization.py +0 -0
  122. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_supports_logs.py +0 -0
  123. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_traceback.py +0 -0
  124. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_tunnel.pyi +0 -0
  125. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_type_manager.py +0 -0
  126. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/__init__.py +0 -0
  127. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/app_utils.py +0 -0
  128. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/async_utils.py +0 -0
  129. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/auth_token_manager.py +0 -0
  130. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/blob_utils.py +0 -0
  131. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/browser_utils.py +0 -0
  132. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/bytes_io_segment_payload.py +0 -0
  133. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/curl_utils.py +0 -0
  134. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/deprecation.py +0 -0
  135. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/display_utils.py +0 -0
  136. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/docker_utils.py +0 -0
  137. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/git_utils.py +0 -0
  138. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/grpc_testing.py +0 -0
  139. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/hash_utils.py +0 -0
  140. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/http_utils.py +0 -0
  141. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/idle_countdown.py +0 -0
  142. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/jwt_utils.py +0 -0
  143. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/logger.py +0 -0
  144. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/mount_utils.py +0 -0
  145. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/name_utils.py +0 -0
  146. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/package_utils.py +0 -0
  147. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/pattern_utils.py +0 -0
  148. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/proxy_support.py +0 -0
  149. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/rand_pb_testing.py +0 -0
  150. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/sandbox_fs_utils.py +0 -0
  151. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/shell_utils.py +0 -0
  152. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/time_utils.py +0 -0
  153. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/__init__.py +0 -0
  154. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  155. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/cloudpickle.py +0 -0
  156. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/tblib.py +0 -0
  157. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/version.py +0 -0
  158. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_watcher.py +0 -0
  159. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/billing.py +0 -0
  160. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/billing.pyi +0 -0
  161. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2023.12.312.txt +0 -0
  162. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2023.12.txt +0 -0
  163. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2024.04.txt +0 -0
  164. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2024.10.txt +0 -0
  165. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2025.06.txt +0 -0
  166. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/PREVIEW.txt +0 -0
  167. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/README.md +0 -0
  168. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/base-images.json +0 -0
  169. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/call_graph.py +0 -0
  170. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/__init__.py +0 -0
  171. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_download.py +0 -0
  172. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_help.py +0 -0
  173. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_logs.py +0 -0
  174. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_stats.py +0 -0
  175. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_traceback.py +0 -0
  176. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/billing.py +0 -0
  177. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/changelog.py +0 -0
  178. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/config.py +0 -0
  179. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/dict.py +0 -0
  180. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/environment.py +0 -0
  181. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/import_refs.py +0 -0
  182. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/logo.py +0 -0
  183. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/profile.py +0 -0
  184. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/run.py +0 -0
  185. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/selector.py +0 -0
  186. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/skills.py +0 -0
  187. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/volume.py +0 -0
  188. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/container_process.py +0 -0
  189. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/container_process.pyi +0 -0
  190. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/dict.pyi +0 -0
  191. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/flash.pyi +0 -0
  192. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/flash_server.pyi +0 -0
  193. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/ipython.py +0 -0
  194. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/file_pattern_matcher.py +0 -0
  195. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/functions.py +0 -0
  196. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/image.py +0 -0
  197. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/image.pyi +0 -0
  198. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/mount.pyi +0 -0
  199. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/network_file_system.pyi +0 -0
  200. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/object.py +0 -0
  201. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/output.py +0 -0
  202. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/parallel_map.pyi +0 -0
  203. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/proxy.pyi +0 -0
  204. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/py.typed +0 -0
  205. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/queue.pyi +0 -0
  206. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/retries.py +0 -0
  207. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/runner.pyi +0 -0
  208. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/running_app.py +0 -0
  209. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/sandbox_fs.py +0 -0
  210. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/sandbox_fs.pyi +0 -0
  211. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/schedule.py +0 -0
  212. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/scheduler_placement.py +0 -0
  213. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/secret.pyi +0 -0
  214. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/server.py +0 -0
  215. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/serving.py +0 -0
  216. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/serving.pyi +0 -0
  217. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/skills/modal/SKILL.md +0 -0
  218. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/snapshot.pyi +0 -0
  219. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/stream_type.py +0 -0
  220. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/token_flow.pyi +0 -0
  221. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/volume.pyi +0 -0
  222. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/workspace.py +0 -0
  223. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/dependency_links.txt +0 -0
  224. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/entry_points.txt +0 -0
  225. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/requires.txt +0 -0
  226. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/top_level.txt +0 -0
  227. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/__init__.py +0 -0
  228. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/gen_cli_docs.py +0 -0
  229. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/gen_cli_docs_main.py +0 -0
  230. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/gen_reference_docs.py +0 -0
  231. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/gen_reference_docs_main.py +0 -0
  232. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/mdmd/__init__.py +0 -0
  233. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/mdmd/signatures.py +0 -0
  234. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/mdmd/types.py +0 -0
  235. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/reference_intro.md +0 -0
  236. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/__init__.py +0 -0
  237. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/py.typed +0 -0
  238. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_grpc.py +0 -0
  239. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_pb2.py +0 -0
  240. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_pb2.pyi +0 -0
  241. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  242. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  243. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_version/__main__.py +0 -0
  244. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/pyproject.toml +0 -0
  245. {modal-1.5.6.dev25 → modal-1.5.6.dev27}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.6.dev25
3
+ Version: 1.5.6.dev27
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -16,6 +16,7 @@ try:
16
16
  from .client import Client
17
17
  from .cloud_bucket_mount import CloudBucketMount
18
18
  from .cls import Cls, parameter
19
+ from .cluster import Cluster
19
20
  from .dict import Dict
20
21
  from .environments import Environment
21
22
  from .exception import Error
@@ -23,16 +24,17 @@ try:
23
24
  from .functions import Function, FunctionCall
24
25
  from .image import Image
25
26
  from .network_file_system import NetworkFileSystem
26
- from .outbound_policy import OutboundPolicy
27
27
  from .output import enable_output
28
28
  from .partial_function import (
29
29
  asgi_app,
30
30
  batched,
31
+ clustered,
31
32
  concurrent,
32
33
  enter,
33
34
  exit,
34
35
  fastapi_endpoint,
35
36
  method,
37
+ sessioned,
36
38
  web_endpoint,
37
39
  web_server,
38
40
  wsgi_app,
@@ -62,6 +64,7 @@ __all__ = [
62
64
  "__version__",
63
65
  "App",
64
66
  "Client",
67
+ "Cluster",
65
68
  "Cls",
66
69
  "Cron",
67
70
  "Dict",
@@ -72,7 +75,6 @@ __all__ = [
72
75
  "FunctionCall",
73
76
  "Image",
74
77
  "NetworkFileSystem",
75
- "OutboundPolicy",
76
78
  "Period",
77
79
  "Probe",
78
80
  "Proxy",
@@ -90,6 +92,7 @@ __all__ = [
90
92
  "asgi_app",
91
93
  "batched",
92
94
  "billing",
95
+ "clustered",
93
96
  "concurrent",
94
97
  "current_function_call_id",
95
98
  "current_input_id",
@@ -103,6 +106,7 @@ __all__ = [
103
106
  "interact",
104
107
  "method",
105
108
  "parameter",
109
+ "sessioned",
106
110
  "types",
107
111
  "web_endpoint",
108
112
  "web_server",
@@ -0,0 +1,192 @@
1
+ # Copyright Modal Labs 2026
2
+ import os
3
+ import socket
4
+ from dataclasses import dataclass
5
+ from typing import Literal, cast
6
+
7
+ from google.protobuf.message import Message
8
+
9
+ from modal_proto import api_pb2
10
+
11
+ from ._load_context import LoadContext
12
+ from ._object import _Object, live_method
13
+ from ._resolver import Resolver
14
+ from ._utils.async_utils import synchronize_api
15
+ from .client import _Client
16
+ from .exception import InvalidError
17
+
18
+
19
+ @dataclass
20
+ class ClusterContext:
21
+ """Cluster metadata and the executing container's rank, initialized at startup."""
22
+
23
+ rank: int
24
+ cluster_id: str
25
+ container_ips: list[str]
26
+ container_ipv4_ips: list[str]
27
+ fabric_ids: list[str]
28
+
29
+
30
+ current_cluster_context: ClusterContext | None = None
31
+
32
+
33
+ def get_current_cluster_context() -> ClusterContext:
34
+ """Return the executing container's initialized cluster context without an RPC."""
35
+ if current_cluster_context is None:
36
+ raise InvalidError(
37
+ "Cluster context is not initialized. This operation requires execution "
38
+ "inside a clustered Function or Server."
39
+ )
40
+ return current_cluster_context
41
+
42
+
43
+ class _Cluster(_Object, type_prefix="cu"):
44
+ """A group of containers scheduled together for a clustered Function or Server.
45
+
46
+ Use `Cluster.from_context()` inside a cluster, or `Cluster.from_id()` to
47
+ inspect a cluster remotely. Containers are ordered by cluster rank.
48
+ """
49
+
50
+ _metadata: api_pb2.ClusterStats | None = None
51
+
52
+ def _hydrate_metadata(self, metadata: Message | None):
53
+ if metadata is not None:
54
+ assert isinstance(metadata, api_pb2.ClusterStats)
55
+ self._metadata = metadata
56
+
57
+ def _get_metadata(self) -> api_pb2.ClusterStats:
58
+ assert self._metadata is not None
59
+ return self._metadata
60
+
61
+ @staticmethod
62
+ def from_context() -> "_Cluster":
63
+ """Reference a Cluster from within one of its containers.
64
+
65
+ Raises `InvalidError` outside an initialized clustered execution.
66
+ """
67
+ context = get_current_cluster_context()
68
+ if not context.cluster_id:
69
+ raise InvalidError("The executing container does not have an initialized cluster ID.")
70
+ return _Cluster.from_id(context.cluster_id)
71
+
72
+ @staticmethod
73
+ def from_id(cluster_id: str, *, client: _Client | None = None) -> "_Cluster":
74
+ """Reference a Cluster by its ID.
75
+
76
+ Args:
77
+ cluster_id: ID of the cluster.
78
+ client: Modal client to use; defaults to `Client.from_env()` when omitted.
79
+
80
+ Examples:
81
+ ```python notest
82
+ cluster = modal.Cluster.from_id("cu-123")
83
+ ```
84
+ """
85
+
86
+ async def _load(self: _Cluster, resolver: Resolver, context: LoadContext, existing_object_id: str | None):
87
+ response = await context.client._stub.ClusterGet(api_pb2.ClusterGetRequest(cluster_id=cluster_id))
88
+ self._hydrate(cluster_id, context.client, response.cluster)
89
+
90
+ obj = _Cluster._from_loader(
91
+ _load,
92
+ f"modal.Cluster.from_id({cluster_id!r})",
93
+ hydrate_lazily=True,
94
+ load_context_overrides=LoadContext(client=client),
95
+ )
96
+ obj._object_id = cluster_id
97
+ return obj
98
+
99
+ @property
100
+ def object_id(self) -> str:
101
+ """The cluster's unique `cu-` object ID."""
102
+ return super().object_id
103
+
104
+ @live_method
105
+ async def container_ids(self) -> list[str]:
106
+ """Return container IDs ordered by cluster rank."""
107
+ return list(self._get_metadata().task_ids)
108
+
109
+ async def container_ips(self, family: Literal["ipv4", "ipv6"] = "ipv6") -> list[str]:
110
+ """Return container IP addresses ordered by cluster rank.
111
+
112
+ Returns IPv6 addresses by default; pass `family="ipv4"` for IPv4.
113
+ These addresses are for intra-cluster communication.
114
+
115
+ Must be called from a container in this cluster; otherwise raises `InvalidError`.
116
+ """
117
+ if family not in ("ipv4", "ipv6"):
118
+ raise InvalidError("family must be 'ipv4' or 'ipv6'.")
119
+ context = get_current_cluster_context()
120
+ if context.cluster_id != self.object_id:
121
+ raise InvalidError("container_ips() must be called from a container in the referenced cluster.")
122
+ return list(context.container_ipv4_ips if family == "ipv4" else context.container_ips)
123
+
124
+ async def container_rank(self, container_id: str | None = None) -> int:
125
+ """Return a container's rank within this cluster.
126
+
127
+ With no argument, return the executing container's rank without a network
128
+ request. Raises `InvalidError` if it is not a member of this cluster.
129
+
130
+ With an explicit container ID, look up its rank in the cluster's membership.
131
+ Raises `InvalidError` for nonmembers.
132
+ """
133
+ if container_id is None:
134
+ context = get_current_cluster_context()
135
+ if context.cluster_id != self.object_id:
136
+ raise InvalidError("The executing container is not a member of the referenced cluster.")
137
+ return context.rank
138
+
139
+ container_ids = await self.container_ids()
140
+ try:
141
+ return container_ids.index(container_id)
142
+ except ValueError:
143
+ raise InvalidError(f"Container {container_id!r} is not a member of cluster {self.object_id!r}.") from None
144
+
145
+
146
+ async def _initialize_clustered_function(client: _Client, task_id: str):
147
+ global current_cluster_context
148
+
149
+ def get_i6pn() -> str:
150
+ """Returns the ipv6 address assigned to this container."""
151
+ # An AF_INET6 result always carries a (host, port, flowinfo, scope_id) sockaddr,
152
+ # but getaddrinfo is typed for every address family, some of which lead with an int
153
+ return cast(str, socket.getaddrinfo("i6pn.modal.local", None, socket.AF_INET6)[0][4][0])
154
+
155
+ hostname = socket.gethostname()
156
+ container_ip = get_i6pn()
157
+
158
+ # nccl's default host ID is $(hostname)$(cat /proc/sys/kernel/random/boot_id).
159
+ # on runc, if two i6pn-linked containers get scheduled on the same worker,
160
+ # their boot ID and hostname will both be identical, causing nccl to break.
161
+ # As a workaround, we can explicitly specify a unique host ID here.
162
+ # See MOD-4067.
163
+ os.environ["NCCL_HOSTID"] = f"{hostname}{container_ip}"
164
+
165
+ # We found these settings to work well in most cases. You may be able to achieve
166
+ # better performance by tuning these settings.
167
+ if os.environ["MODAL_CLOUD_PROVIDER"] in ("CLOUD_PROVIDER_GCP", "CLOUD_PROVIDER_OCI"):
168
+ os.environ["NCCL_SOCKET_NTHREADS"] = "4"
169
+ os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
170
+ elif os.environ["MODAL_CLOUD_PROVIDER"] == "CLOUD_PROVIDER_AWS":
171
+ os.environ["NCCL_SOCKET_NTHREADS"] = "2"
172
+ os.environ["NCCL_NSOCKS_PERTHREAD"] = "8"
173
+ else:
174
+ os.environ["NCCL_SOCKET_NTHREADS"] = "1"
175
+ os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
176
+
177
+ resp = await client._stub.TaskClusterHello(
178
+ api_pb2.TaskClusterHelloRequest(
179
+ task_id=task_id,
180
+ container_ip=container_ip,
181
+ ),
182
+ )
183
+ current_cluster_context = ClusterContext(
184
+ rank=resp.cluster_rank,
185
+ cluster_id=resp.cluster_id,
186
+ container_ips=list(resp.container_ips),
187
+ container_ipv4_ips=list(resp.container_ipv4_ips),
188
+ fabric_ids=list(resp.fabric_ids),
189
+ )
190
+
191
+
192
+ initialize_clustered_function = synchronize_api(_initialize_clustered_function)
@@ -0,0 +1,108 @@
1
+ import google.protobuf.message
2
+ import modal._object
3
+ import modal.client
4
+ import modal_proto.api_pb2
5
+ import types
6
+ import typing
7
+ import typing_extensions
8
+
9
+ class ClusterContext:
10
+ """Cluster metadata and the executing container's rank, initialized at startup."""
11
+
12
+ rank: int
13
+ cluster_id: str
14
+ container_ips: list[str]
15
+ container_ipv4_ips: list[str]
16
+ fabric_ids: list[str]
17
+
18
+ def __init__(
19
+ self, rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str], fabric_ids: list[str]
20
+ ) -> None:
21
+ """Initialize self. See help(type(self)) for accurate signature."""
22
+ ...
23
+
24
+ def __repr__(self):
25
+ """Return repr(self)."""
26
+ ...
27
+
28
+ def __eq__(self, other):
29
+ """Return self==value."""
30
+ ...
31
+
32
+ def get_current_cluster_context() -> ClusterContext:
33
+ """Return the executing container's initialized cluster context without an RPC."""
34
+ ...
35
+
36
+ class _Cluster(modal._object._Object):
37
+ """A group of containers scheduled together for a clustered Function or Server.
38
+
39
+ Use `Cluster.from_context()` inside a cluster, or `Cluster.from_id()` to
40
+ inspect a cluster remotely. Containers are ordered by cluster rank.
41
+ """
42
+
43
+ _metadata: typing.Optional[modal_proto.api_pb2.ClusterStats]
44
+
45
+ def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
46
+ def _get_metadata(self) -> modal_proto.api_pb2.ClusterStats: ...
47
+ @staticmethod
48
+ def from_context() -> _Cluster:
49
+ """Reference a Cluster from within one of its containers.
50
+
51
+ Raises `InvalidError` outside an initialized clustered execution.
52
+ """
53
+ ...
54
+
55
+ @staticmethod
56
+ def from_id(cluster_id: str, *, client: typing.Optional[modal.client._Client] = None) -> _Cluster:
57
+ """Reference a Cluster by its ID.
58
+
59
+ Args:
60
+ cluster_id: ID of the cluster.
61
+ client: Modal client to use; defaults to `Client.from_env()` when omitted.
62
+
63
+ Examples:
64
+ ```python notest
65
+ cluster = modal.Cluster.from_id("cu-123")
66
+ ```
67
+ """
68
+ ...
69
+
70
+ @property
71
+ def object_id(self) -> str:
72
+ """The cluster's unique `cu-` object ID."""
73
+ ...
74
+
75
+ async def container_ids(self) -> list[str]:
76
+ """Return container IDs ordered by cluster rank."""
77
+ ...
78
+
79
+ async def container_ips(self, family: typing.Literal["ipv4", "ipv6"] = "ipv6") -> list[str]:
80
+ """Return container IP addresses ordered by cluster rank.
81
+
82
+ Returns IPv6 addresses by default; pass `family="ipv4"` for IPv4.
83
+ These addresses are for intra-cluster communication.
84
+
85
+ Must be called from a container in this cluster; otherwise raises `InvalidError`.
86
+ """
87
+ ...
88
+
89
+ async def container_rank(self, container_id: typing.Optional[str] = None) -> int:
90
+ """Return a container's rank within this cluster.
91
+
92
+ With no argument, return the executing container's rank without a network
93
+ request. Raises `InvalidError` if it is not a member of this cluster.
94
+
95
+ With an explicit container ID, look up its rank in the cluster's membership.
96
+ Raises `InvalidError` for nonmembers.
97
+ """
98
+ ...
99
+
100
+ async def _initialize_clustered_function(client: modal.client._Client, task_id: str): ...
101
+
102
+ class __initialize_clustered_function_spec(typing_extensions.Protocol):
103
+ def __call__(self, /, client: modal.client.Client, task_id: str): ...
104
+ async def aio(self, /, client: modal.client.Client, task_id: str): ...
105
+
106
+ initialize_clustered_function: __initialize_clustered_function_spec
107
+
108
+ current_cluster_context: ClusterContext | None
@@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Any, cast
24
24
 
25
25
  from google.protobuf.message import Message
26
26
 
27
- from modal._clustered_functions import initialize_clustered_function
27
+ from modal._cluster import initialize_clustered_function
28
28
  from modal._runtime.user_code_event_loop import UserCodeEventLoop
29
29
  from modal._serialization import deserialize, deserialize_params
30
30
  from modal._utils.async_utils import TaskContext, aclosing, synchronizer
@@ -407,11 +407,7 @@ def run_server(container_args: api_pb2.ContainerArguments, client: Client):
407
407
  service: Service = hydrate_function(container_args, task_lifecycle_manager, function_def, _client)
408
408
  # Initialize clustered functions.
409
409
  if function_def._experimental_group_size > 0:
410
- initialize_clustered_function(
411
- client,
412
- container_args.task_id,
413
- function_def._experimental_group_size,
414
- )
410
+ initialize_clustered_function(client, container_args.task_id)
415
411
 
416
412
  with UserCodeEventLoop() as event_loop:
417
413
  with service.lifecycle_context(
@@ -452,11 +448,7 @@ def run_function(container_args: api_pb2.ContainerArguments, client: Client):
452
448
 
453
449
  # Initialize clustered functions.
454
450
  if function_def._experimental_group_size > 0:
455
- initialize_clustered_function(
456
- client,
457
- container_args.task_id,
458
- function_def._experimental_group_size,
459
- )
451
+ initialize_clustered_function(client, container_args.task_id)
460
452
 
461
453
  with service.function_execution_context(
462
454
  event_loop=event_loop,
@@ -4,7 +4,7 @@ import builtins
4
4
  from collections.abc import Iterable, Mapping
5
5
  from dataclasses import dataclass
6
6
  from datetime import datetime, timezone
7
- from typing import Any, Literal
7
+ from typing import TYPE_CHECKING, Any, Literal
8
8
 
9
9
  from google.protobuf.empty_pb2 import Empty
10
10
  from google.protobuf.message import Message
@@ -24,6 +24,9 @@ from .config import config, logger
24
24
  from .exception import InvalidError, WorkspaceManagementError
25
25
  from .types import BillingReportItem, EnvironmentBillingSummary
26
26
 
27
+ if TYPE_CHECKING:
28
+ import modal
29
+
27
30
 
28
31
  @dataclass(frozen=True)
29
32
  class EnvironmentSettings:
@@ -59,7 +62,7 @@ class _EnvironmentManager:
59
62
  request.default_member_role_str = default_role
60
63
  if (experimental_options or {}).get("is_public", False):
61
64
  request.environment_type = api_pb2.ENVIRONMENT_TYPE_PUBLIC
62
- await client.stub.EnvironmentCreate(request)
65
+ await client._stub.EnvironmentCreate(request)
63
66
 
64
67
  async def list(
65
68
  self,
@@ -76,7 +79,7 @@ class _EnvironmentManager:
76
79
  ```
77
80
  """
78
81
  client = await _Client.from_env() if client is None else client
79
- resp = await client.stub.EnvironmentList(Empty())
82
+ resp = await client._stub.EnvironmentList(Empty())
80
83
  environments = []
81
84
  for item in resp.items:
82
85
  metadata = api_pb2.EnvironmentMetadata(
@@ -110,7 +113,7 @@ class _EnvironmentManager:
110
113
  ```
111
114
  """
112
115
  client = await _Client.from_env() if client is None else client
113
- await client.stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
116
+ await client._stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
114
117
 
115
118
 
116
119
  class _EnvironmentRolesManager:
@@ -139,7 +142,7 @@ class _EnvironmentRolesManager:
139
142
  """
140
143
  await self._environment.hydrate()
141
144
  req = api_pb2.EnvironmentGetRolesRequest(environment_id=self._environment.object_id)
142
- resp = await self._environment.client.stub.EnvironmentGetRoles(req)
145
+ resp = await self._environment.client._stub.EnvironmentGetRoles(req)
143
146
 
144
147
  users: dict[str, str] = {}
145
148
  service_users: dict[str, str] = {}
@@ -180,7 +183,7 @@ class _EnvironmentRolesManager:
180
183
  service_users = service_users or {}
181
184
 
182
185
  req = api_pb2.EnvironmentGetRolesRequest(environment_id=self._environment.object_id)
183
- resp = await self._environment.client.stub.EnvironmentGetRoles(req)
186
+ resp = await self._environment.client._stub.EnvironmentGetRoles(req)
184
187
 
185
188
  # EnvironmentGetRoles returns every workspace principal
186
189
  user_name_to_id: dict[str, str] = {}
@@ -214,7 +217,7 @@ class _EnvironmentRolesManager:
214
217
  async def _dispatch_role_updates(self, requests: dict[str, api_pb2.EnvironmentRoleSetRequest]) -> None:
215
218
  """Send batched EnvironmentRoleSet RPCs and report all errors encountered."""
216
219
  results = await asyncio.gather(
217
- *(self._environment.client.stub.EnvironmentRoleSet(req) for req in requests.values()),
220
+ *(self._environment.client._stub.EnvironmentRoleSet(req) for req in requests.values()),
218
221
  return_exceptions=True,
219
222
  )
220
223
  errors = [(label, result) for label, result in zip(requests.keys(), results) if isinstance(result, Exception)]
@@ -269,7 +272,7 @@ class _EnvironmentMembersManager:
269
272
  service_users = service_users or []
270
273
 
271
274
  req = api_pb2.EnvironmentGetRolesRequest(environment_id=self._environment.object_id)
272
- resp = await self._environment.client.stub.EnvironmentGetRoles(req)
275
+ resp = await self._environment.client._stub.EnvironmentGetRoles(req)
273
276
 
274
277
  user_name_to_id: dict[str, str] = {}
275
278
  service_user_name_to_id: dict[str, str] = {}
@@ -300,6 +303,55 @@ class _EnvironmentMembersManager:
300
303
  await _EnvironmentRolesManager(self._environment)._dispatch_role_updates(requests)
301
304
 
302
305
 
306
+ class _EnvironmentAppsManager:
307
+ """mdmd:namespace"""
308
+
309
+ def __init__(self, environment: "_Environment"):
310
+ """mdmd:hidden"""
311
+ self._environment = environment
312
+
313
+ async def list(self) -> builtins.list["modal.app._App"]:
314
+ """Return handles for live Apps in this Environment.
315
+
316
+ The returned handles reference existing remote Apps and can be used with APIs such as
317
+ [`App.info()`](https://modal.com/docs/sdk/py/latest/App#info),
318
+ [`App.logs`](https://modal.com/docs/sdk/py/latest/App#logs), and
319
+ [`Sandbox.create()`](https://modal.com/docs/sdk/py/latest/Sandbox#create). Listing Apps does not create
320
+ Apps.
321
+
322
+ Examples:
323
+
324
+ ```python notest
325
+ environment = modal.Environment.from_name("prod")
326
+ apps = environment.apps.list()
327
+ print([app.name for app in apps])
328
+ ```
329
+ """
330
+ from .app import _App
331
+
332
+ await self._environment.hydrate()
333
+ assert self._environment.name is not None
334
+
335
+ response = await self._environment.client._stub.AppList(
336
+ api_pb2.AppListRequest(environment_name=self._environment.name)
337
+ )
338
+ stopped_app_states = (
339
+ api_pb2.APP_STATE_STOPPED,
340
+ api_pb2.APP_STATE_DISABLED,
341
+ )
342
+ return [
343
+ _App._new_remote(
344
+ item.name or None,
345
+ item.app_id,
346
+ self._environment.name,
347
+ self._environment.client,
348
+ description=item.description or None,
349
+ )
350
+ for item in response.apps
351
+ if item.state not in stopped_app_states
352
+ ]
353
+
354
+
303
355
  class _Environment(_Object, type_prefix="en"):
304
356
  _name: str | None = None
305
357
  _settings: EnvironmentSettings
@@ -334,6 +386,11 @@ class _Environment(_Object, type_prefix="en"):
334
386
  # Deprecated alias for `Environment.roles`.
335
387
  return _EnvironmentMembersManager(self)
336
388
 
389
+ @property
390
+ def apps(self) -> "_EnvironmentAppsManager":
391
+ """Namespace for accessing Apps deployed in this Environment."""
392
+ return _EnvironmentAppsManager(self)
393
+
337
394
  # TODO(michael) Keeping this private for now until we decide what else should be in it
338
395
  # And what the rules should be about updates / mutability
339
396
  # @property
@@ -366,7 +423,7 @@ class _Environment(_Object, type_prefix="en"):
366
423
  else api_pb2.OBJECT_CREATION_TYPE_UNSPECIFIED
367
424
  ),
368
425
  )
369
- response = await load_context.client.stub.EnvironmentGetOrCreate(request)
426
+ response = await load_context.client._stub.EnvironmentGetOrCreate(request)
370
427
  logger.debug(f"Created environment with id {response.environment_id}")
371
428
  self._hydrate(response.environment_id, load_context.client, response.metadata)
372
429
 
@@ -491,7 +548,7 @@ class _EnvironmentBillingManager:
491
548
 
492
549
  return [
493
550
  BillingReportItem._from_proto(pb_item)
494
- async for pb_item in self._environment.client.stub.WorkspaceBillingReport.unary_stream(request)
551
+ async for pb_item in self._environment.client._stub.WorkspaceBillingReport.unary_stream(request)
495
552
  ]
496
553
 
497
554
  async def summary(
@@ -556,7 +613,7 @@ class _EnvironmentBillingManager:
556
613
  request.start_timestamp.FromDatetime(cycle)
557
614
 
558
615
  return EnvironmentBillingSummary._from_proto(
559
- await self._environment.client.stub.EnvironmentBillingSummary(request)
616
+ await self._environment.client._stub.EnvironmentBillingSummary(request)
560
617
  )
561
618
 
562
619
 
@@ -582,7 +639,7 @@ async def _get_environment_cached(name: str, client: _Client) -> _Environment:
582
639
  async def _delete_environment(name: str, client: _Client | None = None):
583
640
  if client is None:
584
641
  client = await _Client.from_env()
585
- await client.stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
642
+ await client._stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
586
643
 
587
644
 
588
645
  async def _update_environment(
@@ -608,19 +665,19 @@ async def _update_environment(
608
665
  )
609
666
  if client is None:
610
667
  client = await _Client.from_env()
611
- await client.stub.EnvironmentUpdate(update_payload)
668
+ await client._stub.EnvironmentUpdate(update_payload)
612
669
 
613
670
 
614
671
  async def _create_environment(name: str, client: _Client | None = None):
615
672
  if client is None:
616
673
  client = await _Client.from_env()
617
- await client.stub.EnvironmentCreate(api_pb2.EnvironmentCreateRequest(name=name))
674
+ await client._stub.EnvironmentCreate(api_pb2.EnvironmentCreateRequest(name=name))
618
675
 
619
676
 
620
677
  async def _list_environments(client: _Client | None = None) -> list[api_pb2.EnvironmentListItem]:
621
678
  if client is None:
622
679
  client = await _Client.from_env()
623
- resp = await client.stub.EnvironmentList(Empty())
680
+ resp = await client._stub.EnvironmentList(Empty())
624
681
  return list(resp.items)
625
682
 
626
683