modAL 1.3.1.dev34__tar.gz → 1.3.2__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 (199) hide show
  1. {modal-1.3.1.dev34 → modal-1.3.2}/PKG-INFO +1 -1
  2. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_object.py +40 -35
  3. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_output.py +2 -1
  4. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_server.py +1 -1
  5. modal-1.3.2/modal/_utils/browser_utils.py +28 -0
  6. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/task_command_router_client.py +3 -3
  7. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/app.py +34 -0
  8. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/container.py +9 -1
  9. modal-1.3.2/modal/cli/dashboard.py +30 -0
  10. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/entry_point.py +2 -0
  11. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/token.py +2 -0
  12. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/utils.py +4 -2
  13. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/volume.py +28 -0
  14. {modal-1.3.1.dev34 → modal-1.3.2}/modal/client.py +6 -0
  15. {modal-1.3.1.dev34 → modal-1.3.2}/modal/client.pyi +12 -10
  16. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cls.py +57 -44
  17. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cls.pyi +1 -0
  18. {modal-1.3.1.dev34 → modal-1.3.2}/modal/config.py +0 -2
  19. {modal-1.3.1.dev34 → modal-1.3.2}/modal/experimental/flash.py +84 -36
  20. {modal-1.3.1.dev34 → modal-1.3.2}/modal/experimental/flash.pyi +14 -0
  21. {modal-1.3.1.dev34 → modal-1.3.2}/modal/functions.pyi +6 -6
  22. {modal-1.3.1.dev34 → modal-1.3.2}/modal/object.pyi +11 -0
  23. {modal-1.3.1.dev34 → modal-1.3.2}/modal/queue.py +11 -1
  24. {modal-1.3.1.dev34 → modal-1.3.2}/modal/queue.pyi +33 -3
  25. {modal-1.3.1.dev34 → modal-1.3.2}/modal/runner.py +1 -5
  26. {modal-1.3.1.dev34 → modal-1.3.2}/modal/sandbox.py +3 -6
  27. {modal-1.3.1.dev34 → modal-1.3.2}/modal/token_flow.py +2 -17
  28. {modal-1.3.1.dev34 → modal-1.3.2}/modal/token_flow.pyi +0 -3
  29. {modal-1.3.1.dev34 → modal-1.3.2}/modal/volume.pyi +2 -2
  30. {modal-1.3.1.dev34 → modal-1.3.2}/modal.egg-info/PKG-INFO +1 -1
  31. {modal-1.3.1.dev34 → modal-1.3.2}/modal.egg-info/SOURCES.txt +2 -0
  32. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/api.proto +12 -0
  33. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/api_grpc.py +16 -0
  34. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/api_pb2.py +178 -158
  35. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/api_pb2.pyi +32 -1
  36. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/api_pb2_grpc.py +33 -0
  37. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/api_pb2_grpc.pyi +10 -0
  38. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/modal_api_grpc.py +1 -0
  39. {modal-1.3.1.dev34 → modal-1.3.2}/modal_version/__init__.py +1 -1
  40. {modal-1.3.1.dev34 → modal-1.3.2}/LICENSE +0 -0
  41. {modal-1.3.1.dev34 → modal-1.3.2}/README.md +0 -0
  42. {modal-1.3.1.dev34 → modal-1.3.2}/modal/__init__.py +0 -0
  43. {modal-1.3.1.dev34 → modal-1.3.2}/modal/__main__.py +0 -0
  44. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_billing.py +0 -0
  45. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_clustered_functions.py +0 -0
  46. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_clustered_functions.pyi +0 -0
  47. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_container_entrypoint.py +0 -0
  48. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_functions.py +0 -0
  49. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_grpc_client.py +0 -0
  50. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_ipython.py +0 -0
  51. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_load_context.py +0 -0
  52. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_location.py +0 -0
  53. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_partial_function.py +0 -0
  54. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_pty.py +0 -0
  55. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_resolver.py +0 -0
  56. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_resources.py +0 -0
  57. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/__init__.py +0 -0
  58. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/asgi.py +0 -0
  59. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/container_io_manager.py +0 -0
  60. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/container_io_manager.pyi +0 -0
  61. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/execution_context.py +0 -0
  62. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/execution_context.pyi +0 -0
  63. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  64. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/telemetry.py +0 -0
  65. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/user_code_event_loop.py +0 -0
  66. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_runtime/user_code_imports.py +0 -0
  67. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_serialization.py +0 -0
  68. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_traceback.py +0 -0
  69. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_tunnel.py +0 -0
  70. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_tunnel.pyi +0 -0
  71. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_type_manager.py +0 -0
  72. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/__init__.py +0 -0
  73. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/app_utils.py +0 -0
  74. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/async_utils.py +0 -0
  75. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/auth_token_manager.py +0 -0
  76. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/blob_utils.py +0 -0
  77. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/bytes_io_segment_payload.py +0 -0
  78. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/deprecation.py +0 -0
  79. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/docker_utils.py +0 -0
  80. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/function_utils.py +0 -0
  81. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/git_utils.py +0 -0
  82. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/grpc_testing.py +0 -0
  83. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/grpc_utils.py +0 -0
  84. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/hash_utils.py +0 -0
  85. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/http_utils.py +0 -0
  86. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/jwt_utils.py +0 -0
  87. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/logger.py +0 -0
  88. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/mount_utils.py +0 -0
  89. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/name_utils.py +0 -0
  90. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/package_utils.py +0 -0
  91. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/pattern_utils.py +0 -0
  92. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/rand_pb_testing.py +0 -0
  93. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/shell_utils.py +0 -0
  94. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_utils/time_utils.py +0 -0
  95. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_vendor/__init__.py +0 -0
  96. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  97. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_vendor/cloudpickle.py +0 -0
  98. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_vendor/tblib.py +0 -0
  99. {modal-1.3.1.dev34 → modal-1.3.2}/modal/_watcher.py +0 -0
  100. {modal-1.3.1.dev34 → modal-1.3.2}/modal/app.py +0 -0
  101. {modal-1.3.1.dev34 → modal-1.3.2}/modal/app.pyi +0 -0
  102. {modal-1.3.1.dev34 → modal-1.3.2}/modal/billing.py +0 -0
  103. {modal-1.3.1.dev34 → modal-1.3.2}/modal/builder/2023.12.312.txt +0 -0
  104. {modal-1.3.1.dev34 → modal-1.3.2}/modal/builder/2023.12.txt +0 -0
  105. {modal-1.3.1.dev34 → modal-1.3.2}/modal/builder/2024.04.txt +0 -0
  106. {modal-1.3.1.dev34 → modal-1.3.2}/modal/builder/2024.10.txt +0 -0
  107. {modal-1.3.1.dev34 → modal-1.3.2}/modal/builder/2025.06.txt +0 -0
  108. {modal-1.3.1.dev34 → modal-1.3.2}/modal/builder/PREVIEW.txt +0 -0
  109. {modal-1.3.1.dev34 → modal-1.3.2}/modal/builder/README.md +0 -0
  110. {modal-1.3.1.dev34 → modal-1.3.2}/modal/builder/base-images.json +0 -0
  111. {modal-1.3.1.dev34 → modal-1.3.2}/modal/call_graph.py +0 -0
  112. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/__init__.py +0 -0
  113. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/_download.py +0 -0
  114. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/_traceback.py +0 -0
  115. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/cluster.py +0 -0
  116. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/config.py +0 -0
  117. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/dict.py +0 -0
  118. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/environment.py +0 -0
  119. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/import_refs.py +0 -0
  120. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/launch.py +0 -0
  121. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/network_file_system.py +0 -0
  122. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/profile.py +0 -0
  123. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/programs/__init__.py +0 -0
  124. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/programs/run_jupyter.py +0 -0
  125. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/programs/vscode.py +0 -0
  126. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/queues.py +0 -0
  127. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/run.py +0 -0
  128. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/secret.py +0 -0
  129. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cli/shell.py +0 -0
  130. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cloud_bucket_mount.py +0 -0
  131. {modal-1.3.1.dev34 → modal-1.3.2}/modal/cloud_bucket_mount.pyi +0 -0
  132. {modal-1.3.1.dev34 → modal-1.3.2}/modal/container_process.py +0 -0
  133. {modal-1.3.1.dev34 → modal-1.3.2}/modal/container_process.pyi +0 -0
  134. {modal-1.3.1.dev34 → modal-1.3.2}/modal/dict.py +0 -0
  135. {modal-1.3.1.dev34 → modal-1.3.2}/modal/dict.pyi +0 -0
  136. {modal-1.3.1.dev34 → modal-1.3.2}/modal/environments.py +0 -0
  137. {modal-1.3.1.dev34 → modal-1.3.2}/modal/environments.pyi +0 -0
  138. {modal-1.3.1.dev34 → modal-1.3.2}/modal/exception.py +0 -0
  139. {modal-1.3.1.dev34 → modal-1.3.2}/modal/experimental/__init__.py +0 -0
  140. {modal-1.3.1.dev34 → modal-1.3.2}/modal/experimental/ipython.py +0 -0
  141. {modal-1.3.1.dev34 → modal-1.3.2}/modal/file_io.py +0 -0
  142. {modal-1.3.1.dev34 → modal-1.3.2}/modal/file_io.pyi +0 -0
  143. {modal-1.3.1.dev34 → modal-1.3.2}/modal/file_pattern_matcher.py +0 -0
  144. {modal-1.3.1.dev34 → modal-1.3.2}/modal/functions.py +0 -0
  145. {modal-1.3.1.dev34 → modal-1.3.2}/modal/gpu.py +0 -0
  146. {modal-1.3.1.dev34 → modal-1.3.2}/modal/image.py +0 -0
  147. {modal-1.3.1.dev34 → modal-1.3.2}/modal/image.pyi +0 -0
  148. {modal-1.3.1.dev34 → modal-1.3.2}/modal/io_streams.py +0 -0
  149. {modal-1.3.1.dev34 → modal-1.3.2}/modal/io_streams.pyi +0 -0
  150. {modal-1.3.1.dev34 → modal-1.3.2}/modal/mount.py +0 -0
  151. {modal-1.3.1.dev34 → modal-1.3.2}/modal/mount.pyi +0 -0
  152. {modal-1.3.1.dev34 → modal-1.3.2}/modal/network_file_system.py +0 -0
  153. {modal-1.3.1.dev34 → modal-1.3.2}/modal/network_file_system.pyi +0 -0
  154. {modal-1.3.1.dev34 → modal-1.3.2}/modal/object.py +0 -0
  155. {modal-1.3.1.dev34 → modal-1.3.2}/modal/output.py +0 -0
  156. {modal-1.3.1.dev34 → modal-1.3.2}/modal/parallel_map.py +0 -0
  157. {modal-1.3.1.dev34 → modal-1.3.2}/modal/parallel_map.pyi +0 -0
  158. {modal-1.3.1.dev34 → modal-1.3.2}/modal/partial_function.py +0 -0
  159. {modal-1.3.1.dev34 → modal-1.3.2}/modal/partial_function.pyi +0 -0
  160. {modal-1.3.1.dev34 → modal-1.3.2}/modal/proxy.py +0 -0
  161. {modal-1.3.1.dev34 → modal-1.3.2}/modal/proxy.pyi +0 -0
  162. {modal-1.3.1.dev34 → modal-1.3.2}/modal/py.typed +0 -0
  163. {modal-1.3.1.dev34 → modal-1.3.2}/modal/retries.py +0 -0
  164. {modal-1.3.1.dev34 → modal-1.3.2}/modal/runner.pyi +0 -0
  165. {modal-1.3.1.dev34 → modal-1.3.2}/modal/running_app.py +0 -0
  166. {modal-1.3.1.dev34 → modal-1.3.2}/modal/sandbox.pyi +3 -3
  167. {modal-1.3.1.dev34 → modal-1.3.2}/modal/schedule.py +0 -0
  168. {modal-1.3.1.dev34 → modal-1.3.2}/modal/scheduler_placement.py +0 -0
  169. {modal-1.3.1.dev34 → modal-1.3.2}/modal/secret.py +0 -0
  170. {modal-1.3.1.dev34 → modal-1.3.2}/modal/secret.pyi +0 -0
  171. {modal-1.3.1.dev34 → modal-1.3.2}/modal/server.py +0 -0
  172. {modal-1.3.1.dev34 → modal-1.3.2}/modal/server.pyi +0 -0
  173. {modal-1.3.1.dev34 → modal-1.3.2}/modal/serving.py +0 -0
  174. {modal-1.3.1.dev34 → modal-1.3.2}/modal/serving.pyi +0 -0
  175. {modal-1.3.1.dev34 → modal-1.3.2}/modal/snapshot.py +0 -0
  176. {modal-1.3.1.dev34 → modal-1.3.2}/modal/snapshot.pyi +0 -0
  177. {modal-1.3.1.dev34 → modal-1.3.2}/modal/stream_type.py +0 -0
  178. {modal-1.3.1.dev34 → modal-1.3.2}/modal/volume.py +0 -0
  179. {modal-1.3.1.dev34 → modal-1.3.2}/modal.egg-info/dependency_links.txt +0 -0
  180. {modal-1.3.1.dev34 → modal-1.3.2}/modal.egg-info/entry_points.txt +0 -0
  181. {modal-1.3.1.dev34 → modal-1.3.2}/modal.egg-info/requires.txt +0 -0
  182. {modal-1.3.1.dev34 → modal-1.3.2}/modal.egg-info/top_level.txt +0 -0
  183. {modal-1.3.1.dev34 → modal-1.3.2}/modal_docs/__init__.py +0 -0
  184. {modal-1.3.1.dev34 → modal-1.3.2}/modal_docs/gen_cli_docs.py +0 -0
  185. {modal-1.3.1.dev34 → modal-1.3.2}/modal_docs/gen_reference_docs.py +0 -0
  186. {modal-1.3.1.dev34 → modal-1.3.2}/modal_docs/mdmd/__init__.py +0 -0
  187. {modal-1.3.1.dev34 → modal-1.3.2}/modal_docs/mdmd/mdmd.py +0 -0
  188. {modal-1.3.1.dev34 → modal-1.3.2}/modal_docs/mdmd/signatures.py +0 -0
  189. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/__init__.py +0 -0
  190. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/py.typed +0 -0
  191. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/task_command_router.proto +0 -0
  192. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/task_command_router_grpc.py +0 -0
  193. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/task_command_router_pb2.py +0 -0
  194. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/task_command_router_pb2.pyi +0 -0
  195. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  196. {modal-1.3.1.dev34 → modal-1.3.2}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  197. {modal-1.3.1.dev34 → modal-1.3.2}/modal_version/__main__.py +0 -0
  198. {modal-1.3.1.dev34 → modal-1.3.2}/pyproject.toml +0 -0
  199. {modal-1.3.1.dev34 → modal-1.3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.3.1.dev34
3
+ Version: 1.3.2
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -39,6 +39,41 @@ def _get_environment_name(
39
39
  return config.get("environment")
40
40
 
41
41
 
42
+ def live_method(method):
43
+ @wraps(method)
44
+ async def wrapped(self, *args, **kwargs):
45
+ await self.hydrate()
46
+ return await method(self, *args, **kwargs)
47
+
48
+ return wrapped
49
+
50
+
51
+ def live_method_gen(method):
52
+ @wraps(method)
53
+ async def wrapped(self, *args, **kwargs):
54
+ await self.hydrate()
55
+ async with aclosing(method(self, *args, **kwargs)) as stream:
56
+ async for item in stream:
57
+ yield item
58
+
59
+ return wrapped
60
+
61
+
62
+ def live_method_contextmanager(method):
63
+ # make sure a wrapped function returning an async context manager
64
+ # will not require both an `await func.aio()` and `async with`
65
+ # which would have been the case if it was wrapped in live_method
66
+
67
+ @wraps(method)
68
+ @contextlib.asynccontextmanager
69
+ async def wrapped(self, *args, **kwargs):
70
+ await self.hydrate()
71
+ async with method(self, *args, **kwargs) as ctx:
72
+ yield ctx
73
+
74
+ return wrapped
75
+
76
+
42
77
  class _Object:
43
78
  _type_prefix: ClassVar[Optional[str]] = None
44
79
  _prefix_to_type: ClassVar[dict[str, type]] = {}
@@ -275,6 +310,11 @@ class _Object:
275
310
  raise AttributeError(f"Attempting to get object_id of unhydrated {self}")
276
311
  return self._object_id
277
312
 
313
+ @live_method
314
+ async def get_dashboard_url(self) -> str:
315
+ """mdmd:hidden"""
316
+ return f"https://modal.com/id/{self.object_id}"
317
+
278
318
  @property
279
319
  def client(self) -> _Client:
280
320
  """mdmd:hidden"""
@@ -336,38 +376,3 @@ class _Object:
336
376
  with suppress_tb_frame(): # skip this frame by default
337
377
  await resolver.load(self, root_load_context)
338
378
  return self
339
-
340
-
341
- def live_method(method):
342
- @wraps(method)
343
- async def wrapped(self, *args, **kwargs):
344
- await self.hydrate()
345
- return await method(self, *args, **kwargs)
346
-
347
- return wrapped
348
-
349
-
350
- def live_method_gen(method):
351
- @wraps(method)
352
- async def wrapped(self, *args, **kwargs):
353
- await self.hydrate()
354
- async with aclosing(method(self, *args, **kwargs)) as stream:
355
- async for item in stream:
356
- yield item
357
-
358
- return wrapped
359
-
360
-
361
- def live_method_contextmanager(method):
362
- # make sure a wrapped function returning an async context manager
363
- # will not require both an `await func.aio()` and `async with`
364
- # which would have been the case if it was wrapped in live_method
365
-
366
- @wraps(method)
367
- @contextlib.asynccontextmanager
368
- async def wrapped(self, *args, **kwargs):
369
- await self.hydrate()
370
- async with method(self, *args, **kwargs) as ctx:
371
- yield ctx
372
-
373
- return wrapped
@@ -545,7 +545,7 @@ async def get_app_logs_loop(
545
545
  output_mgr: OutputManager,
546
546
  app_id: str | None = None,
547
547
  task_id: str | None = None,
548
- app_logs_url: str | None = None,
548
+ sandbox_id: str | None = None,
549
549
  ):
550
550
  last_log_batch_entry_id = ""
551
551
 
@@ -600,6 +600,7 @@ async def get_app_logs_loop(
600
600
  request = api_pb2.AppGetLogsRequest(
601
601
  app_id=app_id or "",
602
602
  task_id=task_id or "",
603
+ sandbox_id=sandbox_id or "",
603
604
  timeout=55,
604
605
  last_entry_id=last_log_batch_entry_id,
605
606
  )
@@ -84,7 +84,7 @@ class _Server:
84
84
 
85
85
  return {
86
86
  _extract_region_from_url(url): url
87
- for url in await self._get_service_function()._experimental_get_flash_urls()
87
+ for url in await self._get_service_function()._experimental_get_flash_urls() or []
88
88
  }
89
89
 
90
90
  @live_method
@@ -0,0 +1,28 @@
1
+ # Copyright Modal Labs 2026
2
+ import webbrowser
3
+
4
+ from rich.console import Console
5
+
6
+
7
+ def open_url(url: str) -> bool:
8
+ """Opens url in web browser, making sure we use a modern one (not Lynx etc)"""
9
+ try:
10
+ browser = webbrowser.get()
11
+ # zpresto defines `BROWSER=open` by default on macOS, which causes `webbrowser` to return `GenericBrowser`.
12
+ if isinstance(browser, webbrowser.GenericBrowser) and browser.name != "open":
13
+ return False
14
+ else:
15
+ return browser.open_new_tab(url)
16
+ except webbrowser.Error:
17
+ return False
18
+
19
+
20
+ def open_url_and_display(url: str, target: str, console: Console) -> None:
21
+ """Open a URL in the web browser and display it to the user."""
22
+ if open_url(url):
23
+ console.print(f"Opening {target} in your web browser...")
24
+ console.print(f"[link={url}]{url}[/link]")
25
+ else:
26
+ console.print("[yellow]Could not open web browser automatically.[/yellow]")
27
+ console.print("Please open this URL in your browser:")
28
+ console.print(f"[link={url}]{url}[/link]")
@@ -15,7 +15,7 @@ import grpclib.events
15
15
  from grpclib import GRPCError, Status
16
16
  from grpclib.exceptions import StreamTerminatedError
17
17
 
18
- from modal.config import config, logger
18
+ from modal.config import logger
19
19
  from modal.exception import ConflictError, ExecTimeoutError
20
20
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
21
21
  from modal_proto.task_command_router_grpc import TaskCommandRouterStub
@@ -152,8 +152,8 @@ class TaskCommandRouterClient:
152
152
  ssl_context = ssl.create_default_context()
153
153
 
154
154
  # Allow insecure TLS when explicitly enabled via config.
155
- if config["task_command_router_insecure"]:
156
- logger.warning("Using insecure TLS for task command router due to MODAL_TASK_COMMAND_ROUTER_INSECURE")
155
+ if server_client._is_localhost:
156
+ logger.warning("Using insecure TLS for task command router because server client points to localhost")
157
157
  ssl_context.check_hostname = False
158
158
  ssl_context.verify_mode = ssl.CERT_NONE
159
159
 
@@ -10,7 +10,9 @@ from rich.text import Text
10
10
  from typer import Argument
11
11
 
12
12
  from modal._object import _get_environment_name
13
+ from modal._output import make_console
13
14
  from modal._utils.async_utils import synchronizer
15
+ from modal._utils.browser_utils import open_url_and_display
14
16
  from modal.client import _Client
15
17
  from modal.environments import ensure_env
16
18
  from modal_proto import api_pb2
@@ -248,3 +250,35 @@ async def history(
248
250
 
249
251
  if deployments_with_dirty_commit and not json:
250
252
  rich.print("* - repo had uncommitted changes")
253
+
254
+
255
+ @app_cli.command("dashboard", no_args_is_help=True)
256
+ @synchronizer.create_blocking
257
+ async def dashboard(
258
+ app_identifier: str = APP_IDENTIFIER,
259
+ *,
260
+ env: Optional[str] = ENV_OPTION,
261
+ ):
262
+ """Open an App's dashboard page in your web browser.
263
+
264
+ **Examples:**
265
+
266
+ Open dashboard for an app by name:
267
+
268
+ ```
269
+ modal app dashboard my-app
270
+ ```
271
+
272
+ Use a specified environment:
273
+
274
+ ```
275
+ modal app dashboard my-app --env dev
276
+ ```
277
+ """
278
+ client = await _Client.from_env()
279
+ app_id = await get_app_id.aio(app_identifier, env, client)
280
+
281
+ console = make_console()
282
+ url = f"https://modal.com/id/{app_id}"
283
+
284
+ open_url_and_display(url, "App dashboard", console)
@@ -13,6 +13,7 @@ from modal.client import _Client
13
13
  from modal.config import config
14
14
  from modal.container_process import _ContainerProcess
15
15
  from modal.environments import ensure_env
16
+ from modal.exception import InvalidError
16
17
  from modal.stream_type import StreamType
17
18
  from modal_proto import api_pb2
18
19
 
@@ -52,7 +53,14 @@ def logs(
52
53
  timestamps: bool = typer.Option(False, "--timestamps", help="Show timestamps for each log line"),
53
54
  ):
54
55
  """Show logs for a specific container, streaming while active."""
55
- stream_app_logs(task_id=container_id, show_timestamps=timestamps)
56
+ task_id, sandbox_id = None, None
57
+ if container_id.startswith("sb-"):
58
+ sandbox_id = container_id
59
+ elif container_id.startswith("ta-"):
60
+ task_id = container_id
61
+ else:
62
+ raise InvalidError(f"Invalid container ID: {container_id}")
63
+ stream_app_logs(task_id=task_id, sandbox_id=sandbox_id, show_timestamps=timestamps)
56
64
 
57
65
 
58
66
  @container_cli.command("exec")
@@ -0,0 +1,30 @@
1
+ # Copyright Modal Labs 2026
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from modal._output import make_console
7
+ from modal._utils.async_utils import synchronizer
8
+ from modal._utils.browser_utils import open_url_and_display
9
+ from modal.client import _Client
10
+ from modal.config import config
11
+ from modal_proto import api_pb2
12
+
13
+
14
+ @synchronizer.create_blocking
15
+ async def dashboard(
16
+ object_id: Optional[str] = typer.Argument(None, help="Open a view for a specific object."),
17
+ ):
18
+ """Open the Modal Dashboard in a web browser."""
19
+ console = make_console()
20
+
21
+ if object_id:
22
+ url = f"https://modal.com/id/{object_id}"
23
+ else:
24
+ env = config.get("environment")
25
+ request = api_pb2.WorkspaceDashboardUrlRequest(environment_name=env)
26
+ client = await _Client.from_env()
27
+ response = await client.stub.WorkspaceDashboardUrlGet(request)
28
+ url = response.url
29
+
30
+ open_url_and_display(url, "dashboard", console)
@@ -13,6 +13,7 @@ from .app import app_cli
13
13
  from .cluster import cluster_cli
14
14
  from .config import config_cli
15
15
  from .container import container_cli
16
+ from .dashboard import dashboard
16
17
  from .dict import dict_cli
17
18
  from .environment import environment_cli
18
19
  from .launch import launch_cli
@@ -118,6 +119,7 @@ async def setup(profile: Optional[str] = None):
118
119
  entrypoint_cli_typer.command("deploy", no_args_is_help=True)(run.deploy)
119
120
  entrypoint_cli_typer.command("serve", no_args_is_help=True)(run.serve)
120
121
  entrypoint_cli_typer.command("shell")(shell_module.shell)
122
+ entrypoint_cli_typer.command("dashboard")(dashboard)
121
123
  entrypoint_cli_typer.add_typer(launch_cli)
122
124
 
123
125
  # Deployments
@@ -87,6 +87,8 @@ async def info():
87
87
  console.print(f"[dim](Using {env_vars_str} environment variable{plural})[/dim]")
88
88
 
89
89
  console.print(f"[bold]Token:[/bold] {resp.token_id}")
90
+ if resp.token_name:
91
+ console.print(f"[bold]Name:[/bold] {resp.token_name}")
90
92
  console.print(f"[bold]Workspace:[/bold] {resp.workspace_name} [dim]({resp.workspace_id})[/dim]")
91
93
 
92
94
  if resp.HasField("user_identity"):
@@ -21,14 +21,16 @@ from ..exception import NotFoundError
21
21
  async def stream_app_logs(
22
22
  app_id: Optional[str] = None,
23
23
  task_id: Optional[str] = None,
24
+ sandbox_id: Optional[str] = None,
24
25
  app_logs_url: Optional[str] = None,
25
26
  show_timestamps: bool = False,
26
27
  ):
27
28
  client = await _Client.from_env()
28
- output_mgr = OutputManager(status_spinner_text=f"Tailing logs for {app_id}", show_timestamps=show_timestamps)
29
+ display_id = app_id or task_id or sandbox_id
30
+ output_mgr = OutputManager(status_spinner_text=f"Tailing logs for {display_id}", show_timestamps=show_timestamps)
29
31
  try:
30
32
  with output_mgr.show_status_spinner():
31
- await get_app_logs_loop(client, output_mgr, app_id=app_id, task_id=task_id, app_logs_url=app_logs_url)
33
+ await get_app_logs_loop(client, output_mgr, app_id=app_id, task_id=task_id, sandbox_id=sandbox_id)
32
34
  except asyncio.CancelledError:
33
35
  pass
34
36
  except KeyboardInterrupt:
@@ -12,6 +12,7 @@ from typer import Argument, Option, Typer
12
12
  import modal
13
13
  from modal._output import OutputManager, ProgressHandler, make_console
14
14
  from modal._utils.async_utils import synchronizer
15
+ from modal._utils.browser_utils import open_url_and_display
15
16
  from modal._utils.time_utils import timestamp_to_localized_str
16
17
  from modal.cli._download import _volume_download
17
18
  from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
@@ -310,3 +311,30 @@ async def rename(
310
311
  )
311
312
 
312
313
  await _Volume.rename(old_name, new_name, environment_name=env)
314
+
315
+
316
+ @volume_cli.command(
317
+ name="dashboard",
318
+ help="Open the Volume's dashboard page in your web browser.",
319
+ rich_help_panel="Management",
320
+ )
321
+ @synchronizer.create_blocking
322
+ async def dashboard(
323
+ volume_name: str = Argument(help="Name of the Volume to view in the dashboard."),
324
+ env: Optional[str] = ENV_OPTION,
325
+ ):
326
+ """Open a Volume's dashboard page in your web browser.
327
+
328
+ **Example:**
329
+
330
+ ```
331
+ modal volume dashboard my-volume
332
+ ```
333
+ """
334
+ env = ensure_env(env)
335
+ volume = await _Volume.from_name(volume_name, environment_name=env).hydrate()
336
+
337
+ console = make_console()
338
+ url = f"https://modal.com/id/{volume.object_id}"
339
+
340
+ open_url_and_display(url, "Volume dashboard", console)
@@ -100,6 +100,12 @@ class _Client:
100
100
  def is_closed(self) -> bool:
101
101
  return self._closed
102
102
 
103
+ @property
104
+ def _is_localhost(self) -> bool:
105
+ """Returns True if the server URL points to localhost."""
106
+ hostname = urllib.parse.urlparse(self.server_url).hostname
107
+ return hostname in {"localhost", "127.0.0.1", "::1", "172.21.0.1"}
108
+
103
109
  @property
104
110
  def stub(self) -> modal_api_grpc.ModalClientModal:
105
111
  """mdmd:hidden
@@ -31,11 +31,7 @@ class _Client:
31
31
  client_type: int
32
32
 
33
33
  def __init__(
34
- self,
35
- server_url: str,
36
- client_type: int,
37
- credentials: typing.Optional[tuple[str, str]],
38
- version: str = "1.3.1.dev34",
34
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.3.2"
39
35
  ):
40
36
  """mdmd:hidden
41
37
  The Modal client object is not intended to be instantiated directly by users.
@@ -43,6 +39,11 @@ class _Client:
43
39
  ...
44
40
 
45
41
  def is_closed(self) -> bool: ...
42
+ @property
43
+ def _is_localhost(self) -> bool:
44
+ """Returns True if the server URL points to localhost."""
45
+ ...
46
+
46
47
  @property
47
48
  def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal:
48
49
  """mdmd:hidden
@@ -162,11 +163,7 @@ class Client:
162
163
  client_type: int
163
164
 
164
165
  def __init__(
165
- self,
166
- server_url: str,
167
- client_type: int,
168
- credentials: typing.Optional[tuple[str, str]],
169
- version: str = "1.3.1.dev34",
166
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.3.2"
170
167
  ):
171
168
  """mdmd:hidden
172
169
  The Modal client object is not intended to be instantiated directly by users.
@@ -174,6 +171,11 @@ class Client:
174
171
  ...
175
172
 
176
173
  def is_closed(self) -> bool: ...
174
+ @property
175
+ def _is_localhost(self) -> bool:
176
+ """Returns True if the server URL points to localhost."""
177
+ ...
178
+
177
179
  @property
178
180
  def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal:
179
181
  """mdmd:hidden
@@ -99,21 +99,27 @@ class _ServiceOptions:
99
99
  """Implement protobuf-like MergeFrom semantics for this dataclass.
100
100
 
101
101
  This mostly exists to support "stacking" of `.with_options()` calls.
102
+ Returns a new _ServiceOptions instance without modifying self.
102
103
  """
104
+ # Create a shallow copy of self to start with
105
+ merged = dataclasses.replace(self)
106
+
103
107
  # Don't use dataclasses.asdict() because it does a deepcopy(), which chokes on a hydrated object
104
108
  new_options_dict = {k.name: getattr(new_options, k.name) for k in dataclasses.fields(new_options)}
105
109
 
106
110
  # Resources needs special merge handling because individual fields are parameters in the public API
107
111
  merged_resources = api_pb2.Resources()
108
- if self.resources:
109
- merged_resources.MergeFrom(self.resources)
112
+ if merged.resources:
113
+ merged_resources.MergeFrom(merged.resources)
110
114
  if new_resources := new_options_dict.pop("resources"):
111
115
  merged_resources.MergeFrom(new_resources)
112
- self.resources = merged_resources
116
+ merged.resources = merged_resources
113
117
 
114
118
  for key, value in new_options_dict.items():
115
119
  if value: # Only overwrite data when the value was set in the new options
116
- setattr(self, key, value)
120
+ setattr(merged, key, value)
121
+
122
+ return merged
117
123
 
118
124
 
119
125
  def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name: str):
@@ -743,32 +749,6 @@ More information on class parameterization can be found here: https://modal.com/
743
749
  " please use the `.with_concurrency` method instead.",
744
750
  )
745
751
 
746
- async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
747
- # this is a bit confusing, the cls will always have the same metadata
748
- # since it has the same *class* service function (i.e. "template")
749
- # But the (instance) service function for each Obj will be different
750
- # since it will rebind to whatever `_options` have been assigned on
751
- # the particular Cls parent
752
- if not self.is_hydrated:
753
- # this should only happen for Cls.from_name instances
754
- # other classes should already be hydrated!
755
- await resolver.load(self, load_context)
756
-
757
- new_cls._initialize_from_other(self)
758
-
759
- def _deps():
760
- return []
761
-
762
- cls = _Cls._from_loader(
763
- _load_from_base,
764
- rep=f"{self._name}.with_options(...)",
765
- is_another_app=True,
766
- deps=_deps,
767
- load_context_overrides=self._load_context_overrides,
768
- hydrate_lazily=True,
769
- )
770
- cls._initialize_from_other(self)
771
-
772
752
  # Validate volumes
773
753
  validated_volumes = validate_volumes(volumes)
774
754
  cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
@@ -801,8 +781,37 @@ More information on class parameterization can be found here: https://modal.com/
801
781
  target_concurrent_inputs=allow_concurrent_inputs,
802
782
  )
803
783
 
804
- cls._options.merge_options(new_options)
805
- return cls
784
+ combined_options = self._options.merge_options(new_options)
785
+
786
+ async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
787
+ # this is a bit confusing, the cls will always have the same metadata
788
+ # since it has the same *class* service function (i.e. "template")
789
+ # But the (instance) service function for each Obj will be different
790
+ # since it will rebind to whatever `_options` have been assigned on
791
+ # the particular Cls parent
792
+ if not self.is_hydrated:
793
+ # this should only happen for Cls.from_name instances
794
+ # other classes should already be hydrated!
795
+ await resolver.load(self, load_context)
796
+
797
+ new_cls._initialize_from_other(self)
798
+ # Restore the merged options after _initialize_from_other overwrites them
799
+ new_cls._options = combined_options
800
+
801
+ def _deps():
802
+ return []
803
+
804
+ new_cls = _Cls._from_loader(
805
+ _load_from_base,
806
+ rep=f"{self._name}.with_options(...)",
807
+ is_another_app=True,
808
+ deps=_deps,
809
+ load_context_overrides=self._load_context_overrides,
810
+ hydrate_lazily=True,
811
+ )
812
+ new_cls._initialize_from_other(self)
813
+ new_cls._options = combined_options
814
+ return new_cls
806
815
 
807
816
  def with_concurrency(self: "_Cls", *, max_inputs: int, target_inputs: Optional[int] = None) -> "_Cls":
808
817
  """Create an instance of the Cls with input concurrency enabled or overridden with new values.
@@ -815,16 +824,20 @@ More information on class parameterization can be found here: https://modal.com/
815
824
  ModelUsingGPU().generate.remote(42) # will run on an A100 GPU with input concurrency enabled
816
825
  ```
817
826
  """
827
+ concurrency_options = _ServiceOptions(max_concurrent_inputs=max_inputs, target_concurrent_inputs=target_inputs)
828
+ combined_options = self._options.merge_options(concurrency_options)
818
829
 
819
830
  async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
820
831
  if not self.is_hydrated:
821
832
  await resolver.load(self, load_context)
822
833
  new_cls._initialize_from_other(self)
834
+ # Restore the merged options after _initialize_from_other overwrites them
835
+ new_cls._options = combined_options
823
836
 
824
837
  def _deps():
825
838
  return []
826
839
 
827
- cls = _Cls._from_loader(
840
+ new_cls = _Cls._from_loader(
828
841
  _load_from_base,
829
842
  rep=f"{self._name}.with_concurrency(...)",
830
843
  is_another_app=True,
@@ -832,11 +845,9 @@ More information on class parameterization can be found here: https://modal.com/
832
845
  load_context_overrides=self._load_context_overrides,
833
846
  hydrate_lazily=True,
834
847
  )
835
- cls._initialize_from_other(self)
836
-
837
- concurrency_options = _ServiceOptions(max_concurrent_inputs=max_inputs, target_concurrent_inputs=target_inputs)
838
- cls._options.merge_options(concurrency_options)
839
- return cls
848
+ new_cls._initialize_from_other(self)
849
+ new_cls._options = combined_options
850
+ return new_cls
840
851
 
841
852
  def with_batching(self: "_Cls", *, max_batch_size: int, wait_ms: int) -> "_Cls":
842
853
  """Create an instance of the Cls with dynamic batching enabled or overridden with new values.
@@ -849,16 +860,20 @@ More information on class parameterization can be found here: https://modal.com/
849
860
  ModelUsingGPU().generate.remote(42) # will run on an A100 GPU with input concurrency enabled
850
861
  ```
851
862
  """
863
+ batching_options = _ServiceOptions(batch_max_size=max_batch_size, batch_wait_ms=wait_ms)
864
+ combined_options = self._options.merge_options(batching_options)
852
865
 
853
866
  async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
854
867
  if not self.is_hydrated:
855
868
  await resolver.load(self, load_context)
856
869
  new_cls._initialize_from_other(self)
870
+ # Restore the merged options after _initialize_from_other overwrites them
871
+ new_cls._options = combined_options
857
872
 
858
873
  def _deps():
859
874
  return []
860
875
 
861
- cls = _Cls._from_loader(
876
+ new_cls = _Cls._from_loader(
862
877
  _load_from_base,
863
878
  rep=f"{self._name}.with_batching(...)",
864
879
  is_another_app=True,
@@ -866,11 +881,9 @@ More information on class parameterization can be found here: https://modal.com/
866
881
  load_context_overrides=self._load_context_overrides,
867
882
  hydrate_lazily=True,
868
883
  )
869
- cls._initialize_from_other(self)
870
-
871
- batching_options = _ServiceOptions(batch_max_size=max_batch_size, batch_wait_ms=wait_ms)
872
- cls._options.merge_options(batching_options)
873
- return cls
884
+ new_cls._initialize_from_other(self)
885
+ new_cls._options = combined_options
886
+ return new_cls
874
887
 
875
888
  @synchronizer.no_input_translation
876
889
  def __call__(self, *args, **kwargs) -> _Obj:
@@ -47,6 +47,7 @@ class _ServiceOptions:
47
47
  """Implement protobuf-like MergeFrom semantics for this dataclass.
48
48
 
49
49
  This mostly exists to support "stacking" of `.with_options()` calls.
50
+ Returns a new _ServiceOptions instance without modifying self.
50
51
  """
51
52
  ...
52
53
 
@@ -251,8 +251,6 @@ _SETTINGS = {
251
251
  "traceback": _Setting(False, transform=_to_boolean),
252
252
  "image_builder_version": _Setting(),
253
253
  "strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
254
- # Allow insecure TLS for the task command router when running locally (testing/dev only)
255
- "task_command_router_insecure": _Setting(False, transform=_to_boolean),
256
254
  "snapshot_debug": _Setting(False, transform=_to_boolean),
257
255
  "cuda_checkpoint_path": _Setting("/__modal/.bin/cuda-checkpoint"), # Used for snapshotting GPU memory.
258
256
  "build_validation": _Setting("error", transform=_check_value(["error", "warn", "ignore"])),