modal 1.0.6.dev32__tar.gz → 1.0.6.dev34__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.dev34}/PKG-INFO +1 -1
  2. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_functions.py +8 -6
  3. modal-1.0.6.dev34/modal/_utils/auth_token_manager.py +114 -0
  4. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/grpc_utils.py +0 -14
  5. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/client.py +5 -1
  6. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/client.pyi +5 -2
  7. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/sandbox.py +3 -1
  8. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal.egg-info/PKG-INFO +1 -1
  9. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal.egg-info/SOURCES.txt +1 -0
  10. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_version/__init__.py +1 -1
  11. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/LICENSE +0 -0
  12. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/README.md +0 -0
  13. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/__init__.py +0 -0
  14. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/__main__.py +0 -0
  15. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_clustered_functions.py +0 -0
  16. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_clustered_functions.pyi +0 -0
  17. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_container_entrypoint.py +0 -0
  18. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_ipython.py +0 -0
  19. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_location.py +0 -0
  20. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_object.py +0 -0
  21. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_output.py +0 -0
  22. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_partial_function.py +0 -0
  23. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_pty.py +0 -0
  24. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_resolver.py +0 -0
  25. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_resources.py +0 -0
  26. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_runtime/__init__.py +0 -0
  27. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_runtime/asgi.py +0 -0
  28. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_runtime/container_io_manager.py +0 -0
  29. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_runtime/container_io_manager.pyi +0 -0
  30. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_runtime/execution_context.py +0 -0
  31. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_runtime/execution_context.pyi +0 -0
  32. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  33. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_runtime/telemetry.py +0 -0
  34. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_runtime/user_code_imports.py +0 -0
  35. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_serialization.py +0 -0
  36. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_traceback.py +0 -0
  37. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_tunnel.py +0 -0
  38. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_tunnel.pyi +0 -0
  39. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_type_manager.py +0 -0
  40. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/__init__.py +0 -0
  41. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/app_utils.py +0 -0
  42. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/async_utils.py +0 -0
  43. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/blob_utils.py +0 -0
  44. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/bytes_io_segment_payload.py +0 -0
  45. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/deprecation.py +0 -0
  46. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/docker_utils.py +0 -0
  47. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/function_utils.py +0 -0
  48. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/git_utils.py +0 -0
  49. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/grpc_testing.py +0 -0
  50. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/hash_utils.py +0 -0
  51. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/http_utils.py +0 -0
  52. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/jwt_utils.py +0 -0
  53. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/logger.py +0 -0
  54. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/mount_utils.py +0 -0
  55. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/name_utils.py +0 -0
  56. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/package_utils.py +0 -0
  57. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/pattern_utils.py +0 -0
  58. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/rand_pb_testing.py +0 -0
  59. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/shell_utils.py +0 -0
  60. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_utils/time_utils.py +0 -0
  61. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_vendor/__init__.py +0 -0
  62. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  63. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_vendor/cloudpickle.py +0 -0
  64. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_vendor/tblib.py +0 -0
  65. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/_watcher.py +0 -0
  66. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/app.py +0 -0
  67. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/app.pyi +0 -0
  68. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/call_graph.py +0 -0
  69. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/__init__.py +0 -0
  70. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/_download.py +0 -0
  71. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/_traceback.py +0 -0
  72. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/app.py +0 -0
  73. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/cluster.py +0 -0
  74. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/config.py +0 -0
  75. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/container.py +0 -0
  76. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/dict.py +0 -0
  77. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/entry_point.py +0 -0
  78. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/environment.py +0 -0
  79. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/import_refs.py +0 -0
  80. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/launch.py +0 -0
  81. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/network_file_system.py +0 -0
  82. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/profile.py +0 -0
  83. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/programs/__init__.py +0 -0
  84. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/programs/run_jupyter.py +0 -0
  85. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/programs/vscode.py +0 -0
  86. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/queues.py +0 -0
  87. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/run.py +0 -0
  88. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/secret.py +0 -0
  89. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/token.py +0 -0
  90. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/utils.py +0 -0
  91. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cli/volume.py +0 -0
  92. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cloud_bucket_mount.py +0 -0
  93. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cloud_bucket_mount.pyi +0 -0
  94. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cls.py +0 -0
  95. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/cls.pyi +0 -0
  96. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/config.py +0 -0
  97. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/container_process.py +0 -0
  98. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/container_process.pyi +0 -0
  99. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/dict.py +0 -0
  100. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/dict.pyi +0 -0
  101. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/environments.py +0 -0
  102. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/environments.pyi +0 -0
  103. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/exception.py +0 -0
  104. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/experimental/__init__.py +0 -0
  105. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/experimental/ipython.py +0 -0
  106. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/file_io.py +0 -0
  107. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/file_io.pyi +0 -0
  108. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/file_pattern_matcher.py +0 -0
  109. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/functions.py +0 -0
  110. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/functions.pyi +0 -0
  111. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/gpu.py +0 -0
  112. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/image.py +0 -0
  113. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/image.pyi +0 -0
  114. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/io_streams.py +0 -0
  115. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/io_streams.pyi +0 -0
  116. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/mount.py +0 -0
  117. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/mount.pyi +0 -0
  118. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/network_file_system.py +0 -0
  119. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/network_file_system.pyi +0 -0
  120. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/object.py +0 -0
  121. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/object.pyi +0 -0
  122. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/output.py +0 -0
  123. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/parallel_map.py +0 -0
  124. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/parallel_map.pyi +0 -0
  125. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/partial_function.py +0 -0
  126. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/partial_function.pyi +0 -0
  127. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/proxy.py +0 -0
  128. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/proxy.pyi +0 -0
  129. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/py.typed +0 -0
  130. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/queue.py +0 -0
  131. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/queue.pyi +0 -0
  132. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/requirements/2023.12.312.txt +0 -0
  133. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/requirements/2023.12.txt +0 -0
  134. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/requirements/2024.04.txt +0 -0
  135. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/requirements/2024.10.txt +0 -0
  136. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/requirements/2025.06.txt +0 -0
  137. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/requirements/PREVIEW.txt +0 -0
  138. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/requirements/README.md +0 -0
  139. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/requirements/base-images.json +0 -0
  140. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/retries.py +0 -0
  141. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/runner.py +0 -0
  142. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/runner.pyi +0 -0
  143. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/running_app.py +0 -0
  144. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/sandbox.pyi +0 -0
  145. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/schedule.py +0 -0
  146. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/scheduler_placement.py +0 -0
  147. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/secret.py +0 -0
  148. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/secret.pyi +0 -0
  149. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/serving.py +0 -0
  150. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/serving.pyi +0 -0
  151. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/snapshot.py +0 -0
  152. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/snapshot.pyi +0 -0
  153. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/stream_type.py +0 -0
  154. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/token_flow.py +0 -0
  155. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/token_flow.pyi +0 -0
  156. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/volume.py +0 -0
  157. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal/volume.pyi +0 -0
  158. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal.egg-info/dependency_links.txt +0 -0
  159. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal.egg-info/entry_points.txt +0 -0
  160. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal.egg-info/requires.txt +0 -0
  161. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal.egg-info/top_level.txt +0 -0
  162. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_docs/__init__.py +0 -0
  163. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_docs/gen_cli_docs.py +0 -0
  164. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_docs/gen_reference_docs.py +0 -0
  165. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_docs/mdmd/__init__.py +0 -0
  166. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_docs/mdmd/mdmd.py +0 -0
  167. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_docs/mdmd/signatures.py +0 -0
  168. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/__init__.py +0 -0
  169. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/api.proto +0 -0
  170. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/api_grpc.py +0 -0
  171. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/api_pb2.py +0 -0
  172. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/api_pb2.pyi +0 -0
  173. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/api_pb2_grpc.py +0 -0
  174. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/api_pb2_grpc.pyi +0 -0
  175. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/modal_api_grpc.py +0 -0
  176. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/modal_options_grpc.py +0 -0
  177. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/options.proto +0 -0
  178. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/options_grpc.py +0 -0
  179. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/options_pb2.py +0 -0
  180. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/options_pb2.pyi +0 -0
  181. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/options_pb2_grpc.py +0 -0
  182. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/options_pb2_grpc.pyi +0 -0
  183. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_proto/py.typed +0 -0
  184. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/modal_version/__main__.py +0 -0
  185. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/pyproject.toml +0 -0
  186. {modal-1.0.6.dev32 → modal-1.0.6.dev34}/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.dev34
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
 
@@ -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.dev34",
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.dev34",
164
167
  ):
165
168
  """mdmd:hidden
166
169
  The Modal client object is not intended to be instantiated directly by users.
@@ -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.dev34
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.dev34"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes