modal 1.0.6.dev32__tar.gz → 1.0.6.dev35__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (186) hide show
  1. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/PKG-INFO +1 -1
  2. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_functions.py +8 -6
  3. modal-1.0.6.dev35/modal/_utils/auth_token_manager.py +114 -0
  4. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/grpc_utils.py +0 -14
  5. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/app.py +20 -4
  6. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/app.pyi +40 -8
  7. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/client.py +5 -1
  8. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/client.pyi +5 -2
  9. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/experimental/__init__.py +47 -1
  10. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/sandbox.py +3 -1
  11. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal.egg-info/PKG-INFO +1 -1
  12. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal.egg-info/SOURCES.txt +1 -0
  13. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_version/__init__.py +1 -1
  14. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/LICENSE +0 -0
  15. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/README.md +0 -0
  16. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/__init__.py +0 -0
  17. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/__main__.py +0 -0
  18. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_clustered_functions.py +0 -0
  19. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_clustered_functions.pyi +0 -0
  20. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_container_entrypoint.py +0 -0
  21. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_ipython.py +0 -0
  22. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_location.py +0 -0
  23. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_object.py +0 -0
  24. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_output.py +0 -0
  25. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_partial_function.py +0 -0
  26. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_pty.py +0 -0
  27. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_resolver.py +0 -0
  28. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_resources.py +0 -0
  29. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_runtime/__init__.py +0 -0
  30. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_runtime/asgi.py +0 -0
  31. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_runtime/container_io_manager.py +0 -0
  32. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_runtime/container_io_manager.pyi +0 -0
  33. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_runtime/execution_context.py +0 -0
  34. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_runtime/execution_context.pyi +0 -0
  35. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  36. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_runtime/telemetry.py +0 -0
  37. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_runtime/user_code_imports.py +0 -0
  38. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_serialization.py +0 -0
  39. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_traceback.py +0 -0
  40. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_tunnel.py +0 -0
  41. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_tunnel.pyi +0 -0
  42. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_type_manager.py +0 -0
  43. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/__init__.py +0 -0
  44. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/app_utils.py +0 -0
  45. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/async_utils.py +0 -0
  46. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/blob_utils.py +0 -0
  47. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/bytes_io_segment_payload.py +0 -0
  48. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/deprecation.py +0 -0
  49. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/docker_utils.py +0 -0
  50. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/function_utils.py +0 -0
  51. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/git_utils.py +0 -0
  52. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/grpc_testing.py +0 -0
  53. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/hash_utils.py +0 -0
  54. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/http_utils.py +0 -0
  55. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/jwt_utils.py +0 -0
  56. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/logger.py +0 -0
  57. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/mount_utils.py +0 -0
  58. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/name_utils.py +0 -0
  59. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/package_utils.py +0 -0
  60. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/pattern_utils.py +0 -0
  61. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/rand_pb_testing.py +0 -0
  62. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/shell_utils.py +0 -0
  63. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_utils/time_utils.py +0 -0
  64. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_vendor/__init__.py +0 -0
  65. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  66. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_vendor/cloudpickle.py +0 -0
  67. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_vendor/tblib.py +0 -0
  68. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/_watcher.py +0 -0
  69. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/call_graph.py +0 -0
  70. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/__init__.py +0 -0
  71. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/_download.py +0 -0
  72. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/_traceback.py +0 -0
  73. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/app.py +0 -0
  74. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/cluster.py +0 -0
  75. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/config.py +0 -0
  76. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/container.py +0 -0
  77. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/dict.py +0 -0
  78. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/entry_point.py +0 -0
  79. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/environment.py +0 -0
  80. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/import_refs.py +0 -0
  81. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/launch.py +0 -0
  82. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/network_file_system.py +0 -0
  83. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/profile.py +0 -0
  84. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/programs/__init__.py +0 -0
  85. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/programs/run_jupyter.py +0 -0
  86. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/programs/vscode.py +0 -0
  87. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/queues.py +0 -0
  88. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/run.py +0 -0
  89. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/secret.py +0 -0
  90. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/token.py +0 -0
  91. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/utils.py +0 -0
  92. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cli/volume.py +0 -0
  93. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cloud_bucket_mount.py +0 -0
  94. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cloud_bucket_mount.pyi +0 -0
  95. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cls.py +0 -0
  96. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/cls.pyi +0 -0
  97. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/config.py +0 -0
  98. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/container_process.py +0 -0
  99. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/container_process.pyi +0 -0
  100. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/dict.py +0 -0
  101. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/dict.pyi +0 -0
  102. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/environments.py +0 -0
  103. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/environments.pyi +0 -0
  104. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/exception.py +0 -0
  105. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/experimental/ipython.py +0 -0
  106. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/file_io.py +0 -0
  107. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/file_io.pyi +0 -0
  108. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/file_pattern_matcher.py +0 -0
  109. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/functions.py +0 -0
  110. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/functions.pyi +0 -0
  111. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/gpu.py +0 -0
  112. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/image.py +0 -0
  113. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/image.pyi +0 -0
  114. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/io_streams.py +0 -0
  115. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/io_streams.pyi +0 -0
  116. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/mount.py +0 -0
  117. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/mount.pyi +0 -0
  118. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/network_file_system.py +0 -0
  119. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/network_file_system.pyi +0 -0
  120. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/object.py +0 -0
  121. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/object.pyi +0 -0
  122. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/output.py +0 -0
  123. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/parallel_map.py +0 -0
  124. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/parallel_map.pyi +0 -0
  125. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/partial_function.py +0 -0
  126. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/partial_function.pyi +0 -0
  127. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/proxy.py +0 -0
  128. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/proxy.pyi +0 -0
  129. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/py.typed +0 -0
  130. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/queue.py +0 -0
  131. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/queue.pyi +0 -0
  132. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/requirements/2023.12.312.txt +0 -0
  133. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/requirements/2023.12.txt +0 -0
  134. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/requirements/2024.04.txt +0 -0
  135. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/requirements/2024.10.txt +0 -0
  136. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/requirements/2025.06.txt +0 -0
  137. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/requirements/PREVIEW.txt +0 -0
  138. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/requirements/README.md +0 -0
  139. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/requirements/base-images.json +0 -0
  140. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/retries.py +0 -0
  141. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/runner.py +0 -0
  142. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/runner.pyi +0 -0
  143. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/running_app.py +0 -0
  144. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/sandbox.pyi +0 -0
  145. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/schedule.py +0 -0
  146. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/scheduler_placement.py +0 -0
  147. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/secret.py +0 -0
  148. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/secret.pyi +0 -0
  149. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/serving.py +0 -0
  150. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/serving.pyi +0 -0
  151. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/snapshot.py +0 -0
  152. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/snapshot.pyi +0 -0
  153. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/stream_type.py +0 -0
  154. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/token_flow.py +0 -0
  155. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/token_flow.pyi +0 -0
  156. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/volume.py +0 -0
  157. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal/volume.pyi +0 -0
  158. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal.egg-info/dependency_links.txt +0 -0
  159. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal.egg-info/entry_points.txt +0 -0
  160. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal.egg-info/requires.txt +0 -0
  161. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal.egg-info/top_level.txt +0 -0
  162. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_docs/__init__.py +0 -0
  163. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_docs/gen_cli_docs.py +0 -0
  164. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_docs/gen_reference_docs.py +0 -0
  165. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_docs/mdmd/__init__.py +0 -0
  166. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_docs/mdmd/mdmd.py +0 -0
  167. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_docs/mdmd/signatures.py +0 -0
  168. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/__init__.py +0 -0
  169. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/api.proto +0 -0
  170. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/api_grpc.py +0 -0
  171. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/api_pb2.py +0 -0
  172. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/api_pb2.pyi +0 -0
  173. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/api_pb2_grpc.py +0 -0
  174. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/api_pb2_grpc.pyi +0 -0
  175. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/modal_api_grpc.py +0 -0
  176. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/modal_options_grpc.py +0 -0
  177. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/options.proto +0 -0
  178. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/options_grpc.py +0 -0
  179. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/options_pb2.py +0 -0
  180. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/options_pb2.pyi +0 -0
  181. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/options_pb2_grpc.py +0 -0
  182. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/options_pb2_grpc.pyi +0 -0
  183. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_proto/py.typed +0 -0
  184. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/modal_version/__main__.py +0 -0
  185. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/pyproject.toml +0 -0
  186. {modal-1.0.6.dev32 → modal-1.0.6.dev35}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.0.6.dev32
3
+ Version: 1.0.6.dev35
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -401,9 +401,7 @@ class _InputPlaneInvocation:
401
401
  parent_input_id=current_input_id() or "",
402
402
  input=input_item,
403
403
  )
404
- metadata: list[tuple[str, str]] = []
405
- if input_plane_region and input_plane_region != "":
406
- metadata.append(("x-modal-input-plane-region", input_plane_region))
404
+ metadata = await _InputPlaneInvocation._get_metadata(input_plane_region, client)
407
405
  response = await retry_transient_errors(stub.AttemptStart, request, metadata=metadata)
408
406
  attempt_token = response.attempt_token
409
407
 
@@ -419,9 +417,7 @@ class _InputPlaneInvocation:
419
417
  timeout_secs=OUTPUTS_TIMEOUT,
420
418
  requested_at=time.time(),
421
419
  )
422
- metadata: list[tuple[str, str]] = []
423
- if self.input_plane_region and self.input_plane_region != "":
424
- metadata.append(("x-modal-input-plane-region", self.input_plane_region))
420
+ metadata = await self._get_metadata(self.input_plane_region, self.client)
425
421
  await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
426
422
  self.stub.AttemptAwait,
427
423
  await_request,
@@ -457,6 +453,12 @@ class _InputPlaneInvocation:
457
453
  await_response.output.result, await_response.output.data_format, control_plane_stub, self.client
458
454
  )
459
455
 
456
+ @staticmethod
457
+ async def _get_metadata(input_plane_region: str, client: _Client) -> list[tuple[str, str]]:
458
+ if not input_plane_region:
459
+ return []
460
+ token = await client._auth_token_manager.get_token()
461
+ return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
460
462
 
461
463
  # Wrapper type for api_pb2.FunctionStats
462
464
  @dataclass(frozen=True)
@@ -0,0 +1,114 @@
1
+ # Copyright Modal Labs 2025
2
+ import asyncio
3
+ import base64
4
+ import json
5
+ import time
6
+ import typing
7
+ from typing import Any
8
+
9
+ from modal.exception import ExecutionError
10
+ from modal_proto import api_pb2, modal_api_grpc
11
+
12
+ from .grpc_utils import retry_transient_errors
13
+ from .logger import logger
14
+
15
+
16
+ class _AuthTokenManager:
17
+ """Handles fetching and refreshing of the input plane auth token."""
18
+
19
+ # Start refreshing this many seconds before the token expires
20
+ REFRESH_WINDOW = 5 * 60
21
+ # If the token doesn't have an expiry field, default to current time plus this value (not expected).
22
+ DEFAULT_EXPIRY_OFFSET = 20 * 60
23
+
24
+ def __init__(self, stub: "modal_api_grpc.ModalClientModal"):
25
+ self._stub = stub
26
+ self._token = ""
27
+ self._expiry = 0.0
28
+ self._lock: typing.Union[asyncio.Lock, None] = None
29
+
30
+ async def get_token(self):
31
+ """
32
+ When called, the AuthTokenManager can be in one of three states:
33
+ 1. Has a valid cached token. It is returned to the caller.
34
+ 2. Has no cached token, or the token is expired. We fetch a new one and cache it. If `get_token` is called
35
+ concurrently by multiple coroutines, all requests will block until the token has been fetched. But only one
36
+ coroutine will actually make a request to the control plane to fetch the new token. This ensures we do not hit
37
+ the control plane with more requests than needed.
38
+ 3. Has a valid cached token, but it is going to expire in the next 5 minutes. In this case we fetch a new token
39
+ and cache it. If `get_token` is called concurrently, only one request will fetch the new token, and the others
40
+ will be given the old (but still valid) token - i.e. they will not block.
41
+ """
42
+ if not self._token or self._is_expired():
43
+ # We either have no token or it is expired - block everyone until we get a new token
44
+ await self._refresh_token()
45
+ elif self._needs_refresh():
46
+ # The token hasn't expired yet, but will soon, so it needs a refresh.
47
+ lock = await self._get_lock()
48
+ if lock.locked():
49
+ # The lock is taken, so someone else is refreshing. Continue to use the old token.
50
+ return self._token
51
+ else:
52
+ # The lock is not taken, so we need to fetch a new token.
53
+ await self._refresh_token()
54
+
55
+ return self._token
56
+
57
+ async def _refresh_token(self):
58
+ """
59
+ Fetch a new token from the control plane. If called concurrently, only one coroutine will make a request for a
60
+ new token. The others will block on a lock, until the first coroutine has fetched the new token.
61
+ """
62
+ lock = await self._get_lock()
63
+ async with lock:
64
+ # Double check inside lock - maybe another coroutine refreshed already. This happens the first time we fetch
65
+ # the token. The first coroutine will fetch the token, while the others block on the lock, waiting for the
66
+ # new token. Once we have a new token, the other coroutines will unblock and return from here.
67
+ if self._token and not self._needs_refresh():
68
+ return
69
+ resp: api_pb2.AuthTokenGetResponse = await retry_transient_errors(
70
+ self._stub.AuthTokenGet, api_pb2.AuthTokenGetRequest()
71
+ )
72
+ if not resp.token:
73
+ # Not expected
74
+ raise ExecutionError(
75
+ "Internal error: Did not receive auth token from server. Please contact Modal support."
76
+ )
77
+
78
+ self._token = resp.token
79
+ if exp := self._decode_jwt(resp.token).get("exp"):
80
+ self._expiry = float(exp)
81
+ else:
82
+ # This should never happen.
83
+ logger.warning("x-modal-auth-token does not contain exp field")
84
+ # We'll use the token, and set the expiry to 20 min from now.
85
+ self._expiry = time.time() + self.DEFAULT_EXPIRY_OFFSET
86
+
87
+ async def _get_lock(self) -> asyncio.Lock:
88
+ # Note: this function runs no async code but is marked as async to ensure it's
89
+ # being run inside the synchronicity event loop and binds the lock to the
90
+ # correct event loop on Python 3.9 which eagerly assigns event loops on
91
+ # constructions of locks
92
+ if self._lock is None:
93
+ self._lock = asyncio.Lock()
94
+ return self._lock
95
+
96
+ @staticmethod
97
+ def _decode_jwt(token: str) -> dict[str, Any]:
98
+ """
99
+ Decodes a JWT into a dict without verifying signature. We do this manually instead of using a library to avoid
100
+ adding another dependency to the client.
101
+ """
102
+ try:
103
+ payload = token.split(".")[1]
104
+ padding = "=" * (-len(payload) % 4)
105
+ decoded_bytes = base64.urlsafe_b64decode(payload + padding)
106
+ return json.loads(decoded_bytes)
107
+ except Exception as e:
108
+ raise ValueError("Internal error: Cannot parse auth token. Please contact Modal support.") from e
109
+
110
+ def _needs_refresh(self):
111
+ return time.time() >= (self._expiry - self.REFRESH_WINDOW)
112
+
113
+ def _is_expired(self):
114
+ return time.time() >= self._expiry
@@ -149,21 +149,7 @@ def create_channel(
149
149
 
150
150
  logger.debug(f"Sending request to {event.method_name}")
151
151
 
152
- async def recv_initial_metadata(initial_metadata: grpclib.events.RecvInitialMetadata) -> None:
153
- # If we receive an auth token from the server, include it in all future requests.
154
- # TODO(nathan): This isn't perfect because the metadata isn't propagated when the
155
- # process is forked and a new channel is created. This is OK for now since this
156
- # token is only used by the experimental input plane
157
- if token := initial_metadata.metadata.get("x-modal-auth-token"):
158
- metadata["x-modal-auth-token"] = str(token)
159
-
160
- async def recv_trailing_metadata(trailing_metadata: grpclib.events.RecvTrailingMetadata) -> None:
161
- if token := trailing_metadata.metadata.get("x-modal-auth-token"):
162
- metadata["x-modal-auth-token"] = str(token)
163
-
164
152
  grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
165
- grpclib.events.listen(channel, grpclib.events.RecvInitialMetadata, recv_initial_metadata)
166
- grpclib.events.listen(channel, grpclib.events.RecvTrailingMetadata, recv_trailing_metadata)
167
153
 
168
154
  return channel
169
155
 
@@ -508,22 +508,38 @@ class _App:
508
508
 
509
509
  @property
510
510
  def registered_functions(self) -> dict[str, _Function]:
511
- """All modal.Function objects registered on the app."""
511
+ """All modal.Function objects registered on the app.
512
+
513
+ Note: this property is populated only during the build phase, and it is not
514
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
515
+ """
512
516
  return self._functions
513
517
 
514
518
  @property
515
519
  def registered_classes(self) -> dict[str, _Cls]:
516
- """All modal.Cls objects registered on the app."""
520
+ """All modal.Cls objects registered on the app.
521
+
522
+ Note: this property is populated only during the build phase, and it is not
523
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
524
+ """
517
525
  return self._classes
518
526
 
519
527
  @property
520
528
  def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
521
- """All local CLI entrypoints registered on the app."""
529
+ """All local CLI entrypoints registered on the app.
530
+
531
+ Note: this property is populated only during the build phase, and it is not
532
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
533
+ """
522
534
  return self._local_entrypoints
523
535
 
524
536
  @property
525
537
  def registered_web_endpoints(self) -> list[str]:
526
- """Names of web endpoint (ie. webhook) functions registered on the app."""
538
+ """Names of web endpoint (ie. webhook) functions registered on the app.
539
+
540
+ Note: this property is populated only during the build phase, and it is not
541
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
542
+ """
527
543
  return self._web_endpoints
528
544
 
529
545
  def local_entrypoint(
@@ -299,22 +299,38 @@ class _App:
299
299
  def _init_container(self, client: modal.client._Client, running_app: modal.running_app.RunningApp): ...
300
300
  @property
301
301
  def registered_functions(self) -> dict[str, modal._functions._Function]:
302
- """All modal.Function objects registered on the app."""
302
+ """All modal.Function objects registered on the app.
303
+
304
+ Note: this property is populated only during the build phase, and it is not
305
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
306
+ """
303
307
  ...
304
308
 
305
309
  @property
306
310
  def registered_classes(self) -> dict[str, modal.cls._Cls]:
307
- """All modal.Cls objects registered on the app."""
311
+ """All modal.Cls objects registered on the app.
312
+
313
+ Note: this property is populated only during the build phase, and it is not
314
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
315
+ """
308
316
  ...
309
317
 
310
318
  @property
311
319
  def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
312
- """All local CLI entrypoints registered on the app."""
320
+ """All local CLI entrypoints registered on the app.
321
+
322
+ Note: this property is populated only during the build phase, and it is not
323
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
324
+ """
313
325
  ...
314
326
 
315
327
  @property
316
328
  def registered_web_endpoints(self) -> list[str]:
317
- """Names of web endpoint (ie. webhook) functions registered on the app."""
329
+ """Names of web endpoint (ie. webhook) functions registered on the app.
330
+
331
+ Note: this property is populated only during the build phase, and it is not
332
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
333
+ """
318
334
  ...
319
335
 
320
336
  def local_entrypoint(
@@ -888,22 +904,38 @@ class App:
888
904
  def _init_container(self, client: modal.client.Client, running_app: modal.running_app.RunningApp): ...
889
905
  @property
890
906
  def registered_functions(self) -> dict[str, modal.functions.Function]:
891
- """All modal.Function objects registered on the app."""
907
+ """All modal.Function objects registered on the app.
908
+
909
+ Note: this property is populated only during the build phase, and it is not
910
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
911
+ """
892
912
  ...
893
913
 
894
914
  @property
895
915
  def registered_classes(self) -> dict[str, modal.cls.Cls]:
896
- """All modal.Cls objects registered on the app."""
916
+ """All modal.Cls objects registered on the app.
917
+
918
+ Note: this property is populated only during the build phase, and it is not
919
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
920
+ """
897
921
  ...
898
922
 
899
923
  @property
900
924
  def registered_entrypoints(self) -> dict[str, LocalEntrypoint]:
901
- """All local CLI entrypoints registered on the app."""
925
+ """All local CLI entrypoints registered on the app.
926
+
927
+ Note: this property is populated only during the build phase, and it is not
928
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
929
+ """
902
930
  ...
903
931
 
904
932
  @property
905
933
  def registered_web_endpoints(self) -> list[str]:
906
- """Names of web endpoint (ie. webhook) functions registered on the app."""
934
+ """Names of web endpoint (ie. webhook) functions registered on the app.
935
+
936
+ Note: this property is populated only during the build phase, and it is not
937
+ expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
938
+ """
907
939
  ...
908
940
 
909
941
  def local_entrypoint(
@@ -27,6 +27,7 @@ from modal_version import __version__
27
27
  from ._traceback import print_server_warnings
28
28
  from ._utils import async_utils
29
29
  from ._utils.async_utils import TaskContext, synchronize_api
30
+ from ._utils.auth_token_manager import _AuthTokenManager
30
31
  from ._utils.grpc_utils import ConnectionManager, retry_transient_errors
31
32
  from .config import _check_config, _is_remote, config, logger
32
33
  from .exception import AuthError, ClientClosed
@@ -78,6 +79,7 @@ class _Client:
78
79
  _cancellation_context: TaskContext
79
80
  _cancellation_context_event_loop: asyncio.AbstractEventLoop = None
80
81
  _stub: Optional[api_grpc.ModalClientStub]
82
+ _auth_token_manager: _AuthTokenManager = None
81
83
  _snapshotted: bool
82
84
 
83
85
  def __init__(
@@ -96,6 +98,7 @@ class _Client:
96
98
  self.version = version
97
99
  self._closed = False
98
100
  self._stub: Optional[modal_api_grpc.ModalClientModal] = None
101
+ self._auth_token_manager: Optional[_AuthTokenManager] = None
99
102
  self._snapshotted = False
100
103
  self._owner_pid = None
101
104
 
@@ -133,9 +136,9 @@ class _Client:
133
136
  self._cancellation_context = TaskContext(grace=0.5) # allow running rpcs to finish in 0.5s when closing client
134
137
  self._cancellation_context_event_loop = asyncio.get_running_loop()
135
138
  await self._cancellation_context.__aenter__()
136
-
137
139
  self._connection_manager = ConnectionManager(client=self, metadata=metadata)
138
140
  self._stub = await self.get_stub(self.server_url)
141
+ self._auth_token_manager = _AuthTokenManager(self.stub)
139
142
  self._owner_pid = os.getpid()
140
143
 
141
144
  async def _close(self, prep_for_restore: bool = False):
@@ -424,3 +427,4 @@ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
424
427
  self.wrapped_method.channel = await self.client._get_channel(self.server_url)
425
428
  async for response in self.client._call_stream(self.wrapped_method, request, metadata=metadata):
426
429
  yield response
430
+
@@ -4,6 +4,7 @@ import collections.abc
4
4
  import google.protobuf.message
5
5
  import grpclib.client
6
6
  import modal._utils.async_utils
7
+ import modal._utils.auth_token_manager
7
8
  import modal_proto.api_grpc
8
9
  import modal_proto.modal_api_grpc
9
10
  import synchronicity.combined_types
@@ -24,6 +25,7 @@ class _Client:
24
25
  _cancellation_context: modal._utils.async_utils.TaskContext
25
26
  _cancellation_context_event_loop: asyncio.events.AbstractEventLoop
26
27
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
28
+ _auth_token_manager: modal._utils.auth_token_manager._AuthTokenManager
27
29
  _snapshotted: bool
28
30
 
29
31
  def __init__(
@@ -31,7 +33,7 @@ class _Client:
31
33
  server_url: str,
32
34
  client_type: int,
33
35
  credentials: typing.Optional[tuple[str, str]],
34
- version: str = "1.0.6.dev32",
36
+ version: str = "1.0.6.dev35",
35
37
  ):
36
38
  """mdmd:hidden
37
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -153,6 +155,7 @@ class Client:
153
155
  _cancellation_context: modal._utils.async_utils.TaskContext
154
156
  _cancellation_context_event_loop: asyncio.events.AbstractEventLoop
155
157
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
158
+ _auth_token_manager: modal._utils.auth_token_manager._AuthTokenManager
156
159
  _snapshotted: bool
157
160
 
158
161
  def __init__(
@@ -160,7 +163,7 @@ class Client:
160
163
  server_url: str,
161
164
  client_type: int,
162
165
  credentials: typing.Optional[tuple[str, str]],
163
- version: str = "1.0.6.dev32",
166
+ version: str = "1.0.6.dev35",
164
167
  ):
165
168
  """mdmd:hidden
166
169
  The Modal client object is not intended to be instantiated directly by users.
@@ -17,8 +17,9 @@ from .._tunnel import _forward as _forward_tunnel
17
17
  from .._utils.async_utils import synchronize_api, synchronizer
18
18
  from .._utils.deprecation import deprecation_warning
19
19
  from .._utils.grpc_utils import retry_transient_errors
20
+ from ..app import _App
20
21
  from ..client import _Client
21
- from ..cls import _Obj
22
+ from ..cls import _Cls, _Obj
22
23
  from ..config import logger
23
24
  from ..exception import InvalidError
24
25
  from ..image import DockerfileSpec, ImageBuilderVersion, _Image, _ImageRegistryConfig
@@ -88,6 +89,51 @@ async def list_deployed_apps(environment_name: str = "", client: Optional[_Clien
88
89
  return app_infos
89
90
 
90
91
 
92
+ @synchronizer.create_blocking
93
+ async def get_app_objects(
94
+ app_name: str, *, environment_name: Optional[str] = None, client: Optional[_Client] = None
95
+ ) -> dict[str, Union[_Function, _Cls]]:
96
+ """Experimental interface for retrieving a dictionary of the Functions / Clses in an App.
97
+
98
+ The return value is a dictionary mapping names to unhydrated Function or Cls objects.
99
+
100
+ We plan to support this functionality through a stable API in the future. It's likely that
101
+ the stable API will look different (it will probably be a method on the App object itself).
102
+
103
+ """
104
+ # This is implemented through a somewhat odd mixture of internal RPCs and public APIs.
105
+ # While AppGetLayout provides the object ID and metadata for each object in the App, it's
106
+ # currently somewhere between very awkward and impossible to hydrate a modal.Cls with just
107
+ # that information, since the "class service function" needs to be loaded first
108
+ # (and it's not always possible to do that without knowledge of the parameterization).
109
+ # So instead we just use AppGetLayout to retrieve the names of the Functions / Clsices on
110
+ # the App and then use the public .from_name constructors to return unhydrated handles.
111
+
112
+ # Additionally, since we need to know the environment name to use `.from_name`, and the App's
113
+ # environment name isn't stored anywhere on the App (and cannot be retrieved via an RPC), the
114
+ # experimental function is parameterized by an App name while the stable API would instead
115
+ # be a method on the App itself.
116
+
117
+ if client is None:
118
+ client = await _Client.from_env()
119
+
120
+ app = await _App.lookup(app_name, environment_name=environment_name, client=client)
121
+ req = api_pb2.AppGetLayoutRequest(app_id=app.app_id)
122
+ app_layout_resp = await retry_transient_errors(client.stub.AppGetLayout, req)
123
+
124
+ app_objects: dict[str, Union[_Function, _Cls]] = {}
125
+
126
+ for cls_name in app_layout_resp.app_layout.class_ids:
127
+ app_objects[cls_name] = _Cls.from_name(app_name, cls_name, environment_name=environment_name)
128
+
129
+ for func_name in app_layout_resp.app_layout.function_ids:
130
+ if func_name.endswith(".*"):
131
+ continue # TODO explain
132
+ app_objects[func_name] = _Function.from_name(app_name, func_name, environment_name=environment_name)
133
+
134
+ return app_objects
135
+
136
+
91
137
  @synchronizer.create_blocking
92
138
  async def raw_dockerfile_image(
93
139
  path: Union[str, Path],
@@ -578,7 +578,9 @@ class _Sandbox(_Object, type_prefix="sb"):
578
578
 
579
579
  async def _get_task_id(self) -> str:
580
580
  while not self._task_id:
581
- resp = await self._client.stub.SandboxGetTaskId(api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id))
581
+ resp = await retry_transient_errors(
582
+ self._client.stub.SandboxGetTaskId, api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id)
583
+ )
582
584
  self._task_id = resp.task_id
583
585
  if not self._task_id:
584
586
  await asyncio.sleep(0.5)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.0.6.dev32
3
+ Version: 1.0.6.dev35
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -101,6 +101,7 @@ modal/_runtime/user_code_imports.py
101
101
  modal/_utils/__init__.py
102
102
  modal/_utils/app_utils.py
103
103
  modal/_utils/async_utils.py
104
+ modal/_utils/auth_token_manager.py
104
105
  modal/_utils/blob_utils.py
105
106
  modal/_utils/bytes_io_segment_payload.py
106
107
  modal/_utils/deprecation.py
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.0.6.dev32"
4
+ __version__ = "1.0.6.dev35"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes