modAL 1.3.4__tar.gz → 1.3.5.dev0__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 (206) hide show
  1. {modal-1.3.4 → modal-1.3.5.dev0}/PKG-INFO +1 -3
  2. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_billing.py +7 -3
  3. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/async_utils.py +71 -67
  4. modal-1.3.5.dev0/modal/_vendor/version.py +844 -0
  5. {modal-1.3.4 → modal-1.3.5.dev0}/modal/billing.py +2 -1
  6. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/billing.py +12 -7
  7. modal-1.3.5.dev0/modal/cli/changelog.py +157 -0
  8. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/entry_point.py +2 -0
  9. modal-1.3.5.dev0/modal/cli/selector.py +124 -0
  10. {modal-1.3.4 → modal-1.3.5.dev0}/modal/client.pyi +10 -2
  11. {modal-1.3.4 → modal-1.3.5.dev0}/modal/queue.py +3 -3
  12. {modal-1.3.4 → modal-1.3.5.dev0}/modal/queue.pyi +7 -7
  13. {modal-1.3.4 → modal-1.3.5.dev0}/modal/secret.py +18 -0
  14. {modal-1.3.4 → modal-1.3.5.dev0}/modal/secret.pyi +27 -0
  15. {modal-1.3.4 → modal-1.3.5.dev0}/modal.egg-info/PKG-INFO +1 -3
  16. {modal-1.3.4 → modal-1.3.5.dev0}/modal.egg-info/SOURCES.txt +3 -3
  17. {modal-1.3.4 → modal-1.3.5.dev0}/modal_docs/mdmd/mdmd.py +41 -0
  18. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/api_grpc.py +16 -0
  19. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/api_pb2.py +375 -354
  20. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/api_pb2.pyi +44 -1
  21. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/api_pb2_grpc.py +33 -0
  22. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/api_pb2_grpc.pyi +10 -0
  23. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/modal_api_grpc.py +1 -0
  24. {modal-1.3.4 → modal-1.3.5.dev0}/modal_version/__init__.py +1 -1
  25. modal-1.3.4/LICENSE +0 -176
  26. modal-1.3.4/modal_proto/api.proto +0 -3968
  27. modal-1.3.4/modal_proto/task_command_router.proto +0 -164
  28. {modal-1.3.4 → modal-1.3.5.dev0}/README.md +0 -0
  29. {modal-1.3.4 → modal-1.3.5.dev0}/modal/__init__.py +0 -0
  30. {modal-1.3.4 → modal-1.3.5.dev0}/modal/__main__.py +0 -0
  31. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_clustered_functions.py +0 -0
  32. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_clustered_functions.pyi +0 -0
  33. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_container_entrypoint.py +0 -0
  34. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_functions.py +0 -0
  35. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_grpc_client.py +0 -0
  36. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_ipython.py +0 -0
  37. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_load_context.py +0 -0
  38. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_location.py +0 -0
  39. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_object.py +0 -0
  40. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_output/__init__.py +0 -0
  41. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_output/manager.py +0 -0
  42. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_output/pty.py +0 -0
  43. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_output/rich.py +0 -0
  44. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_output/status.py +0 -0
  45. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_partial_function.py +0 -0
  46. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_resolver.py +0 -0
  47. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_resources.py +0 -0
  48. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/__init__.py +0 -0
  49. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/asgi.py +0 -0
  50. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/container_io_manager.py +0 -0
  51. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/container_io_manager.pyi +0 -0
  52. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/execution_context.py +0 -0
  53. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/execution_context.pyi +0 -0
  54. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  55. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/telemetry.py +0 -0
  56. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/user_code_event_loop.py +0 -0
  57. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_runtime/user_code_imports.py +0 -0
  58. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_serialization.py +0 -0
  59. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_server.py +0 -0
  60. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_traceback.py +0 -0
  61. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_tunnel.py +0 -0
  62. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_tunnel.pyi +0 -0
  63. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_type_manager.py +0 -0
  64. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/__init__.py +0 -0
  65. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/app_utils.py +0 -0
  66. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/auth_token_manager.py +0 -0
  67. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/blob_utils.py +0 -0
  68. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/browser_utils.py +0 -0
  69. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/bytes_io_segment_payload.py +0 -0
  70. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/deprecation.py +0 -0
  71. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/docker_utils.py +0 -0
  72. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/function_utils.py +0 -0
  73. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/git_utils.py +0 -0
  74. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/grpc_testing.py +0 -0
  75. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/grpc_utils.py +0 -0
  76. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/hash_utils.py +0 -0
  77. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/http_utils.py +0 -0
  78. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/jwt_utils.py +0 -0
  79. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/logger.py +0 -0
  80. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/mount_utils.py +0 -0
  81. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/name_utils.py +0 -0
  82. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/package_utils.py +0 -0
  83. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/pattern_utils.py +0 -0
  84. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/rand_pb_testing.py +0 -0
  85. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/shell_utils.py +0 -0
  86. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/task_command_router_client.py +0 -0
  87. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_utils/time_utils.py +0 -0
  88. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_vendor/__init__.py +0 -0
  89. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  90. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_vendor/cloudpickle.py +0 -0
  91. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_vendor/tblib.py +0 -0
  92. {modal-1.3.4 → modal-1.3.5.dev0}/modal/_watcher.py +0 -0
  93. {modal-1.3.4 → modal-1.3.5.dev0}/modal/app.py +0 -0
  94. {modal-1.3.4 → modal-1.3.5.dev0}/modal/app.pyi +0 -0
  95. {modal-1.3.4 → modal-1.3.5.dev0}/modal/builder/2023.12.312.txt +0 -0
  96. {modal-1.3.4 → modal-1.3.5.dev0}/modal/builder/2023.12.txt +0 -0
  97. {modal-1.3.4 → modal-1.3.5.dev0}/modal/builder/2024.04.txt +0 -0
  98. {modal-1.3.4 → modal-1.3.5.dev0}/modal/builder/2024.10.txt +0 -0
  99. {modal-1.3.4 → modal-1.3.5.dev0}/modal/builder/2025.06.txt +0 -0
  100. {modal-1.3.4 → modal-1.3.5.dev0}/modal/builder/PREVIEW.txt +0 -0
  101. {modal-1.3.4 → modal-1.3.5.dev0}/modal/builder/README.md +0 -0
  102. {modal-1.3.4 → modal-1.3.5.dev0}/modal/builder/base-images.json +0 -0
  103. {modal-1.3.4 → modal-1.3.5.dev0}/modal/call_graph.py +0 -0
  104. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/__init__.py +0 -0
  105. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/_download.py +0 -0
  106. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/_traceback.py +0 -0
  107. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/app.py +0 -0
  108. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/cluster.py +0 -0
  109. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/config.py +0 -0
  110. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/container.py +0 -0
  111. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/dashboard.py +0 -0
  112. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/dict.py +0 -0
  113. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/environment.py +0 -0
  114. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/import_refs.py +0 -0
  115. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/launch.py +0 -0
  116. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/network_file_system.py +0 -0
  117. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/profile.py +0 -0
  118. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/programs/__init__.py +0 -0
  119. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/programs/run_jupyter.py +0 -0
  120. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/programs/vscode.py +0 -0
  121. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/queues.py +0 -0
  122. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/run.py +0 -0
  123. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/secret.py +0 -0
  124. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/shell.py +0 -0
  125. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/token.py +0 -0
  126. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/utils.py +0 -0
  127. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cli/volume.py +0 -0
  128. {modal-1.3.4 → modal-1.3.5.dev0}/modal/client.py +0 -0
  129. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cloud_bucket_mount.py +0 -0
  130. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cloud_bucket_mount.pyi +0 -0
  131. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cls.py +0 -0
  132. {modal-1.3.4 → modal-1.3.5.dev0}/modal/cls.pyi +0 -0
  133. {modal-1.3.4 → modal-1.3.5.dev0}/modal/config.py +0 -0
  134. {modal-1.3.4 → modal-1.3.5.dev0}/modal/container_process.py +0 -0
  135. {modal-1.3.4 → modal-1.3.5.dev0}/modal/container_process.pyi +0 -0
  136. {modal-1.3.4 → modal-1.3.5.dev0}/modal/dict.py +0 -0
  137. {modal-1.3.4 → modal-1.3.5.dev0}/modal/dict.pyi +0 -0
  138. {modal-1.3.4 → modal-1.3.5.dev0}/modal/environments.py +0 -0
  139. {modal-1.3.4 → modal-1.3.5.dev0}/modal/environments.pyi +0 -0
  140. {modal-1.3.4 → modal-1.3.5.dev0}/modal/exception.py +0 -0
  141. {modal-1.3.4 → modal-1.3.5.dev0}/modal/experimental/__init__.py +0 -0
  142. {modal-1.3.4 → modal-1.3.5.dev0}/modal/experimental/flash.py +0 -0
  143. {modal-1.3.4 → modal-1.3.5.dev0}/modal/experimental/flash.pyi +0 -0
  144. {modal-1.3.4 → modal-1.3.5.dev0}/modal/experimental/ipython.py +0 -0
  145. {modal-1.3.4 → modal-1.3.5.dev0}/modal/file_io.py +0 -0
  146. {modal-1.3.4 → modal-1.3.5.dev0}/modal/file_io.pyi +0 -0
  147. {modal-1.3.4 → modal-1.3.5.dev0}/modal/file_pattern_matcher.py +0 -0
  148. {modal-1.3.4 → modal-1.3.5.dev0}/modal/functions.py +0 -0
  149. {modal-1.3.4 → modal-1.3.5.dev0}/modal/functions.pyi +0 -0
  150. {modal-1.3.4 → modal-1.3.5.dev0}/modal/gpu.py +0 -0
  151. {modal-1.3.4 → modal-1.3.5.dev0}/modal/image.py +0 -0
  152. {modal-1.3.4 → modal-1.3.5.dev0}/modal/image.pyi +0 -0
  153. {modal-1.3.4 → modal-1.3.5.dev0}/modal/io_streams.py +0 -0
  154. {modal-1.3.4 → modal-1.3.5.dev0}/modal/io_streams.pyi +0 -0
  155. {modal-1.3.4 → modal-1.3.5.dev0}/modal/mount.py +0 -0
  156. {modal-1.3.4 → modal-1.3.5.dev0}/modal/mount.pyi +0 -0
  157. {modal-1.3.4 → modal-1.3.5.dev0}/modal/network_file_system.py +0 -0
  158. {modal-1.3.4 → modal-1.3.5.dev0}/modal/network_file_system.pyi +0 -0
  159. {modal-1.3.4 → modal-1.3.5.dev0}/modal/object.py +0 -0
  160. {modal-1.3.4 → modal-1.3.5.dev0}/modal/object.pyi +0 -0
  161. {modal-1.3.4 → modal-1.3.5.dev0}/modal/output.py +0 -0
  162. {modal-1.3.4 → modal-1.3.5.dev0}/modal/parallel_map.py +0 -0
  163. {modal-1.3.4 → modal-1.3.5.dev0}/modal/parallel_map.pyi +0 -0
  164. {modal-1.3.4 → modal-1.3.5.dev0}/modal/partial_function.py +0 -0
  165. {modal-1.3.4 → modal-1.3.5.dev0}/modal/partial_function.pyi +0 -0
  166. {modal-1.3.4 → modal-1.3.5.dev0}/modal/proxy.py +0 -0
  167. {modal-1.3.4 → modal-1.3.5.dev0}/modal/proxy.pyi +0 -0
  168. {modal-1.3.4 → modal-1.3.5.dev0}/modal/py.typed +0 -0
  169. {modal-1.3.4 → modal-1.3.5.dev0}/modal/retries.py +0 -0
  170. {modal-1.3.4 → modal-1.3.5.dev0}/modal/runner.py +0 -0
  171. {modal-1.3.4 → modal-1.3.5.dev0}/modal/runner.pyi +0 -0
  172. {modal-1.3.4 → modal-1.3.5.dev0}/modal/running_app.py +0 -0
  173. {modal-1.3.4 → modal-1.3.5.dev0}/modal/sandbox.py +0 -0
  174. {modal-1.3.4 → modal-1.3.5.dev0}/modal/sandbox.pyi +0 -0
  175. {modal-1.3.4 → modal-1.3.5.dev0}/modal/schedule.py +0 -0
  176. {modal-1.3.4 → modal-1.3.5.dev0}/modal/scheduler_placement.py +0 -0
  177. {modal-1.3.4 → modal-1.3.5.dev0}/modal/server.py +0 -0
  178. {modal-1.3.4 → modal-1.3.5.dev0}/modal/server.pyi +0 -0
  179. {modal-1.3.4 → modal-1.3.5.dev0}/modal/serving.py +0 -0
  180. {modal-1.3.4 → modal-1.3.5.dev0}/modal/serving.pyi +0 -0
  181. {modal-1.3.4 → modal-1.3.5.dev0}/modal/snapshot.py +0 -0
  182. {modal-1.3.4 → modal-1.3.5.dev0}/modal/snapshot.pyi +0 -0
  183. {modal-1.3.4 → modal-1.3.5.dev0}/modal/stream_type.py +0 -0
  184. {modal-1.3.4 → modal-1.3.5.dev0}/modal/token_flow.py +0 -0
  185. {modal-1.3.4 → modal-1.3.5.dev0}/modal/token_flow.pyi +0 -0
  186. {modal-1.3.4 → modal-1.3.5.dev0}/modal/volume.py +0 -0
  187. {modal-1.3.4 → modal-1.3.5.dev0}/modal/volume.pyi +0 -0
  188. {modal-1.3.4 → modal-1.3.5.dev0}/modal.egg-info/dependency_links.txt +0 -0
  189. {modal-1.3.4 → modal-1.3.5.dev0}/modal.egg-info/entry_points.txt +0 -0
  190. {modal-1.3.4 → modal-1.3.5.dev0}/modal.egg-info/requires.txt +0 -0
  191. {modal-1.3.4 → modal-1.3.5.dev0}/modal.egg-info/top_level.txt +0 -0
  192. {modal-1.3.4 → modal-1.3.5.dev0}/modal_docs/__init__.py +0 -0
  193. {modal-1.3.4 → modal-1.3.5.dev0}/modal_docs/gen_cli_docs.py +0 -0
  194. {modal-1.3.4 → modal-1.3.5.dev0}/modal_docs/gen_reference_docs.py +0 -0
  195. {modal-1.3.4 → modal-1.3.5.dev0}/modal_docs/mdmd/__init__.py +0 -0
  196. {modal-1.3.4 → modal-1.3.5.dev0}/modal_docs/mdmd/signatures.py +0 -0
  197. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/__init__.py +0 -0
  198. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/py.typed +0 -0
  199. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/task_command_router_grpc.py +0 -0
  200. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/task_command_router_pb2.py +0 -0
  201. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/task_command_router_pb2.pyi +0 -0
  202. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  203. {modal-1.3.4 → modal-1.3.5.dev0}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  204. {modal-1.3.4 → modal-1.3.5.dev0}/modal_version/__main__.py +0 -0
  205. {modal-1.3.4 → modal-1.3.5.dev0}/pyproject.toml +0 -0
  206. {modal-1.3.4 → modal-1.3.5.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.3.4
3
+ Version: 1.3.5.dev0
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -15,7 +15,6 @@ Classifier: License :: OSI Approved :: Apache Software License
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Requires-Python: <3.15,>=3.10
17
17
  Description-Content-Type: text/markdown
18
- License-File: LICENSE
19
18
  Requires-Dist: aiohttp
20
19
  Requires-Dist: cbor2
21
20
  Requires-Dist: certifi
@@ -31,7 +30,6 @@ Requires-Dist: types-certifi
31
30
  Requires-Dist: types-toml
32
31
  Requires-Dist: watchfiles
33
32
  Requires-Dist: typing_extensions~=4.6
34
- Dynamic: license-file
35
33
 
36
34
  # Modal Python SDK
37
35
 
@@ -1,7 +1,7 @@
1
1
  # Copyright Modal Labs 2025
2
2
  from datetime import datetime, timezone
3
3
  from decimal import Decimal
4
- from typing import Any, Optional, TypedDict
4
+ from typing import Optional, TypedDict
5
5
 
6
6
  from modal_proto import api_pb2
7
7
 
@@ -25,7 +25,7 @@ async def _workspace_billing_report(
25
25
  resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
26
26
  tag_names: Optional[list[str]] = None, # Optional additional metadata to include
27
27
  client: Optional[_Client] = None,
28
- ) -> list[dict[str, Any]]:
28
+ ) -> list[WorkspaceBillingReportItem]:
29
29
  """Generate a tabular report of workspace usage by object and time.
30
30
 
31
31
  The result will be a list of dictionaries for each interval (determined by `resolution`)
@@ -49,6 +49,10 @@ async def _workspace_billing_report(
49
49
  minutes, although there may be collection delays. If completeness is important for your use
50
50
  case, we recommend leaving a buffer after the end of the query interval.
51
51
 
52
+ It's also possible to generate reports using the
53
+ [`modal billing report`](https://modal.com/docs/reference/cli/billing) CLI command. The CLI
54
+ has a few convenience features for generating reports across relative time ranges.
55
+
52
56
  """
53
57
  if client is None:
54
58
  client = await _Client.from_env()
@@ -77,7 +81,7 @@ async def _workspace_billing_report(
77
81
 
78
82
  rows = []
79
83
  async for pb_item in client.stub.WorkspaceBillingReport.unary_stream(request):
80
- item = {
84
+ item: WorkspaceBillingReportItem = {
81
85
  "object_id": pb_item.object_id,
82
86
  "description": pb_item.description,
83
87
  "environment_name": pb_item.environment_name,
@@ -49,6 +49,10 @@ def rewrite_sync_to_async(code_line: str, original_func: Callable) -> tuple[bool
49
49
  """
50
50
  Rewrite a blocking call to use async/await syntax.
51
51
 
52
+ Uses Python's ast module for method calls and property access to reliably
53
+ parse complex expressions (e.g. chained calls like SomeClass().method.spawn(q))
54
+ and determine exact positions for inserting `await` and `.aio`.
55
+
52
56
  Handles four patterns:
53
57
  1. __aiter__: for x in obj -> async for x in obj
54
58
  2. __aenter__: with obj as x -> async with obj as x
@@ -64,6 +68,7 @@ def rewrite_sync_to_async(code_line: str, original_func: Callable) -> tuple[bool
64
68
  - success: True if the pattern was found and rewritten, False if falling back to generic
65
69
  - rewritten_code: The rewritten code or a generic suggestion
66
70
  """
71
+ import ast
67
72
  import re
68
73
 
69
74
  func_name = original_func.__name__ # type: ignore
@@ -118,83 +123,82 @@ def rewrite_sync_to_async(code_line: str, original_func: Callable) -> tuple[bool
118
123
  suggestion = code_line.replace("for ", "async for ", 1)
119
124
  return (True, suggestion)
120
125
 
121
- # Handle regular method calls and property access
122
- # First check if it's a property access (no parentheses after the name)
123
- property_pattern = rf"\.{re.escape(func_name)}(?!\s*\()"
124
- property_match = re.search(property_pattern, code_line)
125
-
126
- if property_match:
127
- # This is a property access, rewrite to use await without .aio()
128
- # Find the start of the expression (skip statement keywords and assignments)
129
- statement_start = 0
130
- prefix_match = re.match(r"^(\s*(?:\w+\s*=|return|yield|raise)\s+)", code_line)
131
- if prefix_match:
132
- statement_start = len(prefix_match.group(1))
126
+ # Use Python's ast module for method calls and property access.
127
+ # This correctly handles complex expressions like SomeClass().method.spawn(q)
128
+ # without needing fragile backward-walking or regex matching.
129
+ stripped = code_line.lstrip()
130
+ indent = code_line[: len(code_line) - len(stripped)]
133
131
 
134
- before_expr = code_line[:statement_start]
135
- after_prefix = code_line[statement_start:]
132
+ try:
133
+ tree = ast.parse(stripped, mode="exec")
134
+ except SyntaxError:
135
+ return (False, f"await ...{func_name}.aio(...)")
136
136
 
137
- # Just add await before the expression for properties
138
- suggestion = before_expr + "await " + after_prefix.lstrip()
139
- return (True, suggestion)
137
+ if not tree.body:
138
+ return (False, f"await ...{func_name}.aio(...)")
140
139
 
141
- # Try to find a method call (with parentheses)
142
- method_pattern = rf"\.{re.escape(func_name)}\s*\("
143
- method_match = re.search(method_pattern, code_line)
140
+ stmt = tree.body[0]
144
141
 
145
- if not method_match:
146
- # Can't find the function call or property
142
+ # Bail out for control flow statements that are too complex to rewrite safely
143
+ if isinstance(stmt, (ast.If, ast.While, ast.For)):
147
144
  return (False, f"await ...{func_name}.aio(...)")
148
145
 
149
- # Safety check: don't attempt rewrite for complex expressions
150
- unsafe_keywords = ["if", "elif", "while", "and", "or", "not", "in", "is", "for"]
146
+ # Walk the AST to find Call nodes and Attribute nodes matching func_name
147
+ target_call = None
148
+ call_func_ids: set[int] = set()
149
+
150
+ for node in ast.walk(tree):
151
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
152
+ call_func_ids.add(id(node.func))
153
+ if node.func.attr == func_name and target_call is None:
154
+ target_call = node
155
+
156
+ # If no matching Call found, check for property access (Attribute not used as a Call's func)
157
+ if target_call is None:
158
+ target_attr = None
159
+ for node in ast.walk(tree):
160
+ if isinstance(node, ast.Attribute) and node.attr == func_name and id(node) not in call_func_ids:
161
+ target_attr = node
162
+ break
151
163
 
152
- # Check if line contains control flow keywords (might be too complex)
153
- for keyword in unsafe_keywords:
154
- if re.search(rf"\b{keyword}\b", code_line):
155
- # Fall back to generic suggestion for complex expressions
164
+ if target_attr is None:
156
165
  return (False, f"await ...{func_name}.aio(...)")
157
166
 
158
- # Find the start of the object expression that leads to the method call
159
- # We need to find where the object/chain starts, e.g., in "2 * foo.bar.method()" we want "foo"
160
- # Work backwards from the method match to find the start of the identifier chain
161
- method_start = method_match.start()
162
-
163
- # Find the start of the identifier chain (the object being called)
164
- # Walk backwards to find identifiers and dots that form the chain
165
- expr_start = method_start
166
- i = method_start - 1
167
- while i >= 0:
168
- c = code_line[i]
169
- if c.isalnum() or c == "_" or c == ".":
170
- expr_start = i
171
- i -= 1
172
- elif c.isspace():
173
- # Skip whitespace within the chain (though unusual)
174
- i -= 1
175
- else:
176
- # Found a non-identifier character, stop
177
- break
167
+ # Property access: insert "await" before the start of the attribute expression
168
+ expr_start = target_attr.col_offset
169
+ result = stripped[:expr_start] + "await " + stripped[expr_start:]
170
+ return (True, indent + result)
171
+
172
+ # Check if the target Call is inside a BoolOp (and/or) - bail out for safety
173
+ for node in ast.walk(tree):
174
+ if isinstance(node, ast.BoolOp):
175
+ for descendant in ast.walk(node):
176
+ if descendant is target_call:
177
+ return (False, f"await ...{func_name}.aio(...)")
178
+
179
+ # Method call: insert .aio after the method name and await before the expression
180
+ func_attr = target_call.func
181
+ attr_end = func_attr.end_col_offset
182
+
183
+ if attr_end is None:
184
+ return (False, f"await ...{func_name}.aio(...)")
185
+
186
+ # Find the opening parenthesis (skip any whitespace between method name and paren)
187
+ paren_pos = attr_end
188
+ while paren_pos < len(stripped) and stripped[paren_pos] != "(":
189
+ paren_pos += 1
190
+
191
+ if paren_pos >= len(stripped):
192
+ return (False, f"await ...{func_name}.aio(...)")
193
+
194
+ # Insert .aio, collapsing any whitespace between method name and (
195
+ modified = stripped[:attr_end] + ".aio" + stripped[paren_pos:]
196
+
197
+ # Insert await before the call expression (ast gives us the exact start position)
198
+ call_start = target_call.col_offset
199
+ modified = modified[:call_start] + "await " + modified[call_start:]
178
200
 
179
- # Now expr_start points to the start of the object chain (e.g., "foo" in "foo.method()")
180
- # But we need to check if the identifier we found is actually a keyword like return/yield/raise
181
- # In that case, skip over it and find the actual object
182
- before_obj = code_line[:expr_start]
183
- obj_and_rest = code_line[expr_start:]
184
-
185
- # Check if what we found starts with a statement keyword
186
- keyword_match = re.match(r"^(return|yield|raise)\s+", obj_and_rest)
187
- if keyword_match:
188
- # The "object" we found is actually a keyword, adjust to skip it
189
- keyword_len = len(keyword_match.group(0))
190
- before_obj = code_line[: expr_start + keyword_len]
191
- obj_and_rest = code_line[expr_start + keyword_len :]
192
-
193
- # Add .aio() after the method name and await before the object
194
- rewritten_expr = re.sub(rf"(\.{re.escape(func_name)})\s*\(", r"\1.aio(", obj_and_rest, count=1)
195
- suggestion = before_obj + "await " + rewritten_expr
196
-
197
- return (True, suggestion)
201
+ return (True, indent + modified)
198
202
 
199
203
 
200
204
  @dataclass