withruntime 0.9.0__tar.gz → 0.11.0__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 (322) hide show
  1. {withruntime-0.9.0 → withruntime-0.11.0}/PKG-INFO +15 -6
  2. {withruntime-0.9.0 → withruntime-0.11.0}/README.md +14 -5
  3. {withruntime-0.9.0 → withruntime-0.11.0}/pyproject.toml +4 -1
  4. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_blaxel.py +427 -6
  5. withruntime-0.11.0/tests/test_blaxel_wait_deadline.py +57 -0
  6. withruntime-0.11.0/tests/test_browser.py +48 -0
  7. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_client.py +3 -1
  8. withruntime-0.11.0/tests/test_compat_modal_deadline_errors.py +73 -0
  9. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_new_providers.py +4 -4
  10. withruntime-0.11.0/tests/test_compat_prime_process_lifecycle.py +106 -0
  11. withruntime-0.11.0/tests/test_compat_prime_validation.py +274 -0
  12. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_e2b_deadline.py +35 -6
  13. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_interpreter_deadline.py +4 -1
  14. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_request_scope.py +5 -1
  15. withruntime-0.11.0/tests/test_customer_usage.py +127 -0
  16. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_daytona.py +4 -8
  17. withruntime-0.11.0/tests/test_daytona_create_cleanup.py +75 -0
  18. withruntime-0.11.0/tests/test_daytona_download_labels.py +153 -0
  19. withruntime-0.11.0/tests/test_daytona_refusals_lifted.py +335 -0
  20. withruntime-0.11.0/tests/test_e2b_fork_validation.py +78 -0
  21. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_e2b_sandbox.py +208 -13
  22. withruntime-0.11.0/tests/test_exec_stream_names.py +51 -0
  23. withruntime-0.11.0/tests/test_file_archives.py +174 -0
  24. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_harbor.py +56 -3
  25. withruntime-0.11.0/tests/test_image_delete_by_name.py +55 -0
  26. withruntime-0.11.0/tests/test_package_typing.py +142 -0
  27. withruntime-0.11.0/tests/test_private_network.py +71 -0
  28. withruntime-0.11.0/tests/test_process_object.py +49 -0
  29. withruntime-0.11.0/tests/test_process_write_backpressure.py +174 -0
  30. withruntime-0.11.0/tests/test_product_aliases.py +87 -0
  31. withruntime-0.11.0/tests/test_runloop_stream_cleanup.py +167 -0
  32. withruntime-0.11.0/tests/test_sandboxes_stop_all.py +49 -0
  33. withruntime-0.11.0/tests/test_saved_connection.py +37 -0
  34. withruntime-0.11.0/tests/test_snapshot_recovery.py +128 -0
  35. withruntime-0.11.0/tests/test_transport_body_recovery.py +229 -0
  36. withruntime-0.11.0/tests/test_transport_connection_limits.py +256 -0
  37. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_unpack.py +64 -0
  38. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_vercel.py +1 -1
  39. withruntime-0.11.0/tests/test_vercel_create_validation.py +158 -0
  40. withruntime-0.11.0/tests/test_vercel_disk_snapshot.py +391 -0
  41. withruntime-0.11.0/tests/test_vercel_refusals_lifted.py +134 -0
  42. withruntime-0.11.0/tests/test_volume_operations.py +52 -0
  43. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/__init__.py +5 -1
  44. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_client.py +220 -56
  45. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/__init__.py +2 -0
  46. withruntime-0.11.0/withruntime/_async_products/browser.py +52 -0
  47. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/desktop.py +2 -2
  48. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/images.py +14 -2
  49. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/network_products.py +3 -0
  50. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/previews.py +14 -4
  51. withruntime-0.11.0/withruntime/_async_products/private_network.py +31 -0
  52. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/volumes.py +24 -2
  53. withruntime-0.11.0/withruntime/_compat_lease.py +32 -0
  54. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_connection.py +5 -2
  55. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_http.py +109 -75
  56. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_proxy.py +17 -3
  57. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_client.py +220 -56
  58. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/__init__.py +2 -0
  59. withruntime-0.11.0/withruntime/_sync_products/browser.py +53 -0
  60. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/desktop.py +2 -2
  61. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/images.py +14 -2
  62. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/network_products.py +3 -0
  63. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/previews.py +14 -4
  64. withruntime-0.11.0/withruntime/_sync_products/private_network.py +21 -0
  65. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/volumes.py +24 -2
  66. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_unpack.py +67 -6
  67. withruntime-0.11.0/withruntime/_version.py +1 -0
  68. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/__init__.py +2 -2
  69. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/_async_io.py +6 -0
  70. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/_async_sandbox.py +71 -30
  71. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/_core.py +49 -3
  72. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/_sync_io.py +9 -0
  73. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/_sync_sandbox.py +71 -30
  74. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/models/__init__.py +1 -1
  75. withruntime-0.11.0/withruntime/blaxel/core/client/models/pagination_meta.py +2 -0
  76. withruntime-0.11.0/withruntime/blaxel/core/client/pagination.py +2 -0
  77. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/daytona/__init__.py +14 -9
  78. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/daytona/_async_daytona.py +472 -77
  79. withruntime-0.11.0/withruntime/daytona/_async_io.py +127 -0
  80. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/daytona/_core.py +361 -6
  81. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/daytona/_sync_daytona.py +472 -77
  82. withruntime-0.11.0/withruntime/daytona/_sync_io.py +150 -0
  83. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/__init__.py +3 -3
  84. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/_async_io.py +41 -0
  85. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/_async_sandbox.py +319 -45
  86. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/_core.py +177 -34
  87. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/_sync_io.py +44 -1
  88. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/_sync_sandbox.py +312 -42
  89. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/harbor.py +5 -30
  90. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/__init__.py +3 -3
  91. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/_async.py +2 -3
  92. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/prime/__init__.py +7 -8
  93. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/prime/_async.py +29 -6
  94. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/prime/_sync.py +29 -6
  95. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/prime/exceptions.py +5 -4
  96. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/prime/models.py +81 -7
  97. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/prime/process.py +10 -1
  98. withruntime-0.11.0/withruntime/py.typed +0 -0
  99. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/runloop/_async.py +20 -5
  100. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/runloop/_sync.py +20 -5
  101. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/vercel/_async_io.py +7 -0
  102. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/vercel/_async_sandbox.py +186 -33
  103. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/vercel/_core.py +59 -12
  104. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/vercel/_sync_io.py +10 -0
  105. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/vercel/_sync_sandbox.py +186 -33
  106. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/vercel/sandbox/__init__.py +4 -2
  107. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/vercel/sandbox/sync.py +4 -2
  108. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime.egg-info/PKG-INFO +15 -6
  109. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime.egg-info/SOURCES.txt +36 -0
  110. withruntime-0.9.0/withruntime/_version.py +0 -1
  111. withruntime-0.9.0/withruntime/daytona/_async_io.py +0 -34
  112. withruntime-0.9.0/withruntime/daytona/_sync_io.py +0 -43
  113. {withruntime-0.9.0 → withruntime-0.11.0}/LICENSE +0 -0
  114. {withruntime-0.9.0 → withruntime-0.11.0}/setup.cfg +0 -0
  115. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_account_errors.py +0 -0
  116. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_async_stream_cleanup.py +0 -0
  117. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_audit.py +0 -0
  118. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_billing.py +0 -0
  119. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_capacity_wait.py +0 -0
  120. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_live.py +0 -0
  121. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_disk_snapshots.py +0 -0
  122. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_e2b_pty.py +0 -0
  123. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_e2b_stream.py +0 -0
  124. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_e2b_watch.py +0 -0
  125. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_modal.py +0 -0
  126. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_official.py +0 -0
  127. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_pins.py +0 -0
  128. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_processes.py +0 -0
  129. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_runloop.py +0 -0
  130. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_shell.py +0 -0
  131. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_snapshot_capture.py +0 -0
  132. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_python_sprites.py +0 -0
  133. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_compat_router.py +0 -0
  134. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_deepagents.py +0 -0
  135. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_e2b_code_interpreter.py +0 -0
  136. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_e2b_watch.py +0 -0
  137. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_embed_and_webhook_watch.py +0 -0
  138. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_eval_sandbox.py +0 -0
  139. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_exec_stream.py +0 -0
  140. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_files_download.py +0 -0
  141. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_files_mode.py +0 -0
  142. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_files_upload_parallel.py +0 -0
  143. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_http_tls_close.py +0 -0
  144. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_identity_token.py +0 -0
  145. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_images_v2.py +0 -0
  146. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_images_volumes_interpreter.py +0 -0
  147. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_inspect_ai.py +0 -0
  148. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_interface_contract.py +0 -0
  149. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_jobs.py +0 -0
  150. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_late_answer.py +0 -0
  151. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_lifecycle_settings.py +0 -0
  152. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_limits.py +0 -0
  153. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_mounts.py +0 -0
  154. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_network_funding.py +0 -0
  155. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_network_products.py +0 -0
  156. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_observability.py +0 -0
  157. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_openai_agents.py +0 -0
  158. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_previews_network_desktop.py +0 -0
  159. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_process_deadline.py +0 -0
  160. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_process_input.py +0 -0
  161. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_process_output.py +0 -0
  162. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_proxy.py +0 -0
  163. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_referrals.py +0 -0
  164. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_sandbox_origin.py +0 -0
  165. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_sandbox_sessions.py +0 -0
  166. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_secrets.py +0 -0
  167. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_snapshot_after_fork.py +0 -0
  168. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_sso.py +0 -0
  169. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_start_report_kept.py +0 -0
  170. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_stream_exit_close.py +0 -0
  171. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_switch_image.py +0 -0
  172. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_switching.py +0 -0
  173. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_tailscale.py +0 -0
  174. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_tools.py +0 -0
  175. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_volume_backups.py +0 -0
  176. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_wake_awake.py +0 -0
  177. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_wallet.py +0 -0
  178. {withruntime-0.9.0 → withruntime-0.11.0}/tests/test_watch.py +0 -0
  179. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_api_defaults.py +0 -0
  180. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/audit.py +0 -0
  181. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/billing.py +0 -0
  182. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/interpreter.py +0 -0
  183. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/jobs.py +0 -0
  184. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/limits.py +0 -0
  185. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/mcp.py +0 -0
  186. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/mounts.py +0 -0
  187. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/network.py +0 -0
  188. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/observability.py +0 -0
  189. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/referrals.py +0 -0
  190. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/secrets.py +0 -0
  191. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/sso.py +0 -0
  192. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/switching.py +0 -0
  193. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/tailscale.py +0 -0
  194. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_async_products/watch.py +0 -0
  195. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_clock.py +0 -0
  196. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_compat.py +0 -0
  197. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_compat_lifecycle.py +0 -0
  198. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_compat_shell.py +0 -0
  199. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_errors.py +0 -0
  200. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_eval_sandbox.py +0 -0
  201. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_request_scope.py +0 -0
  202. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/audit.py +0 -0
  203. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/billing.py +0 -0
  204. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/interpreter.py +0 -0
  205. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/jobs.py +0 -0
  206. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/limits.py +0 -0
  207. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/mcp.py +0 -0
  208. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/mounts.py +0 -0
  209. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/network.py +0 -0
  210. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/observability.py +0 -0
  211. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/referrals.py +0 -0
  212. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/secrets.py +0 -0
  213. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/sso.py +0 -0
  214. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/switching.py +0 -0
  215. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/tailscale.py +0 -0
  216. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_sync_products/watch.py +0 -0
  217. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_tunnel.py +0 -0
  218. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/_ws.py +0 -0
  219. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/__init__.py +0 -0
  220. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/agents.py +0 -0
  221. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/__init__.py +0 -0
  222. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/__init__.py +0 -0
  223. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/applications/__init__.py +0 -0
  224. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/applications/list_application_revisions.py +0 -0
  225. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/compute/__init__.py +0 -0
  226. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/compute/get_sandbox.py +0 -0
  227. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/compute/update_sandbox.py +0 -0
  228. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/functions/__init__.py +0 -0
  229. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/functions/create_function.py +0 -0
  230. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/images.py +0 -0
  231. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/api/workspaces.py +0 -0
  232. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/client.py +0 -0
  233. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/models/create_job_execution_request.py +0 -0
  234. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/models/create_job_execution_request_env.py +0 -0
  235. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/models/env.py +0 -0
  236. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/models/function.py +0 -0
  237. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/models/function_runtime.py +0 -0
  238. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/models/function_spec.py +0 -0
  239. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/models/metadata.py +0 -0
  240. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/models/workspace.py +0 -0
  241. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/client/types.py +0 -0
  242. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/common.py +0 -0
  243. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/drive.py +0 -0
  244. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/jobs.py +0 -0
  245. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/sandbox/__init__.py +0 -0
  246. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/core/sandbox/types.py +0 -0
  247. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/crewai.py +0 -0
  248. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/googleadk.py +0 -0
  249. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/langgraph.py +0 -0
  250. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/livekit.py +0 -0
  251. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/llamaindex.py +0 -0
  252. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/openai.py +0 -0
  253. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/pydantic.py +0 -0
  254. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/blaxel/telemetry.py +0 -0
  255. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/deepagents.py +0 -0
  256. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/code_interpreter/__init__.py +0 -0
  257. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/code_interpreter/_async_ci.py +0 -0
  258. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/code_interpreter/_models.py +0 -0
  259. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/code_interpreter/_sync_ci.py +0 -0
  260. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/code_interpreter/charts.py +0 -0
  261. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox/__init__.py +0 -0
  262. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox/commands/__init__.py +0 -0
  263. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox/commands/command_handle.py +0 -0
  264. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox/filesystem/__init__.py +0 -0
  265. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox/filesystem/filesystem.py +0 -0
  266. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_async/__init__.py +0 -0
  267. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_async/commands/__init__.py +0 -0
  268. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_async/commands/command.py +0 -0
  269. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_async/commands/command_handle.py +0 -0
  270. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_async/commands/pty.py +0 -0
  271. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_async/filesystem/__init__.py +0 -0
  272. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_async/filesystem/filesystem.py +0 -0
  273. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_sync/__init__.py +0 -0
  274. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_sync/commands/__init__.py +0 -0
  275. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_sync/commands/command.py +0 -0
  276. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_sync/commands/command_handle.py +0 -0
  277. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_sync/commands/pty.py +0 -0
  278. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_sync/filesystem/__init__.py +0 -0
  279. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/e2b/sandbox_sync/filesystem/filesystem.py +0 -0
  280. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/inspect_ai.py +0 -0
  281. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/_dual.py +0 -0
  282. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/_fs_async.py +0 -0
  283. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/_fs_common.py +0 -0
  284. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/_fs_sync.py +0 -0
  285. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/_lookup.py +0 -0
  286. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/_snapshot.py +0 -0
  287. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/exception.py +0 -0
  288. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/sandbox_fs.py +0 -0
  289. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/modal/types.py +0 -0
  290. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/openai_agents.py +0 -0
  291. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/prime/core/__init__.py +0 -0
  292. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/prime/core/client.py +0 -0
  293. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/prime/sandbox.py +0 -0
  294. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/runloop/__init__.py +0 -0
  295. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/runloop/_exceptions.py +0 -0
  296. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/runloop/_snapshots.py +0 -0
  297. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/runloop/lib/__init__.py +0 -0
  298. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/runloop/lib/polling.py +0 -0
  299. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/runloop/sdk/__init__.py +0 -0
  300. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/__init__.py +0 -0
  301. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/_capture.py +0 -0
  302. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/_capture_async.py +0 -0
  303. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/_metadata.py +0 -0
  304. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/_stream.py +0 -0
  305. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/_stream_async.py +0 -0
  306. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/async_client.py +0 -0
  307. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/async_exec.py +0 -0
  308. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/client.py +0 -0
  309. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/exceptions.py +0 -0
  310. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/exec.py +0 -0
  311. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/filesystem.py +0 -0
  312. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/sprite.py +0 -0
  313. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/sprites/types.py +0 -0
  314. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/tools.py +0 -0
  315. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/vercel/__init__.py +0 -0
  316. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/vercel/api.py +0 -0
  317. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/wallet.py +0 -0
  318. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime/webhooks.py +0 -0
  319. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime.egg-info/dependency_links.txt +0 -0
  320. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime.egg-info/entry_points.txt +0 -0
  321. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime.egg-info/requires.txt +0 -0
  322. {withruntime-0.9.0 → withruntime-0.11.0}/withruntime.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: withruntime
3
- Version: 0.9.0
3
+ Version: 0.11.0
4
4
  Summary: Runtime Cloud: one client (sync and async) for every Runtime product. Sandboxes first.
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://withruntime.com
@@ -44,7 +44,8 @@ command-line argument.
44
44
  from withruntime import Sandbox
45
45
 
46
46
  with Sandbox.create() as sbx:
47
- result = sbx.exec("python3 -c 'print(6 * 7)'")
47
+ sbx.files.write("/workspace/invoice.py", "print(sum([125, 250, 375]))\n")
48
+ result = sbx.exec("python3 /workspace/invoice.py")
48
49
  print(result.exit_code, result.stdout)
49
50
  ```
50
51
 
@@ -53,6 +54,8 @@ leaving the `with` block stops it. With no arguments you get the free trial
53
54
  while it lasts: 100 free hours, no card, up to eight sandboxes running at once.
54
55
  The current default image includes NumPy, pandas and matplotlib; see the
55
56
  [sandbox environment](https://withruntime.com/docs/sandbox-environment).
57
+ Omitting `funding` can use prepaid credit after the trial is exhausted. Use
58
+ `Sandbox.create(funding="trial")` for free use only; it never falls back to paid.
56
59
  `AsyncRuntime` is the same client for asyncio, method for method:
57
60
 
58
61
  ```python
@@ -83,14 +86,20 @@ also wakes by itself on the next call, and `Sandbox.get_or_create(name)` returns
83
86
  the sandbox with that name or creates it. A sandbox pauses itself after 60 seconds
84
87
  with nothing happening in it, keeping its memory and processes, so an
85
88
  idle sandbox costs no compute; `idle_pause_seconds` sets 10 to 86,400, or 0 for
86
- never. The client has `sandboxes`, `images`, `volumes`, `snapshots`, `jobs`,
89
+ never. `runtime.sandbox`, `snapshot`, `image`, `volume`, `job`, `domain`,
90
+ `port` and `address` name the products as the CLI does. Their existing plural
91
+ names remain aliases of the same product clients. The client has `sandboxes`, `images`, `volumes`, `snapshots`, `jobs`,
87
92
  `secrets`, `limits`, `feedback` and `support`; `webhooks`, `events`, `otel` and
88
93
  `audit` to watch the account; `domains`, `ports`, `addresses`, `tunnel` and
89
94
  `network` to connect sandboxes to your own world (paid accounts); and `mcp`,
90
95
  `sso`, `billing`, `referrals` and `switching`. `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
91
96
  what its agent may still spend today. Every write carries an
92
97
  idempotency key, so retries never do anything twice; errors are typed and carry
93
- `code`, `hint` and `request_id`.
98
+ `code`, `hint` and `request_id`. `RuntimeAPIError` and
99
+ `RuntimeConnectionError` name the SDK errors without shadowing Python's built-in
100
+ errors; the previous `RuntimeError` and `ConnectionError` exports remain aliases.
101
+ `runtime.usage_requests("7d")` reads the account's API call counts and error rates;
102
+ counts are exact decimal strings, and an empty window has `errorPercent: None`.
94
103
 
95
104
  ## Behind a proxy
96
105
 
@@ -109,7 +118,7 @@ proxy http://proxy.internal:3128 (HTTPS_PROXY).`
109
118
 
110
119
  ## Code written for E2B
111
120
 
112
- `withruntime.e2b` runs code written for E2B's Python SDK on Runtime. Change
121
+ `withruntime.e2b` translates supported E2B Python sandbox calls. Change
113
122
  the import and set `RUNTIME_API_KEY`:
114
123
 
115
124
  ```python no-run
@@ -147,7 +156,7 @@ JavaScript package list every mapping and gap.
147
156
 
148
157
  ## Code written for Blaxel
149
158
 
150
- `withruntime.blaxel` runs code written for Blaxel's Python SDK (`blaxel`
159
+ `withruntime.blaxel` translates supported Blaxel Python sandbox calls (`blaxel`
151
160
  0.4.11) on Runtime. Change the import and set `RUNTIME_API_KEY`, or run
152
161
  `npx withruntime login` once:
153
162
 
@@ -19,7 +19,8 @@ command-line argument.
19
19
  from withruntime import Sandbox
20
20
 
21
21
  with Sandbox.create() as sbx:
22
- result = sbx.exec("python3 -c 'print(6 * 7)'")
22
+ sbx.files.write("/workspace/invoice.py", "print(sum([125, 250, 375]))\n")
23
+ result = sbx.exec("python3 /workspace/invoice.py")
23
24
  print(result.exit_code, result.stdout)
24
25
  ```
25
26
 
@@ -28,6 +29,8 @@ leaving the `with` block stops it. With no arguments you get the free trial
28
29
  while it lasts: 100 free hours, no card, up to eight sandboxes running at once.
29
30
  The current default image includes NumPy, pandas and matplotlib; see the
30
31
  [sandbox environment](https://withruntime.com/docs/sandbox-environment).
32
+ Omitting `funding` can use prepaid credit after the trial is exhausted. Use
33
+ `Sandbox.create(funding="trial")` for free use only; it never falls back to paid.
31
34
  `AsyncRuntime` is the same client for asyncio, method for method:
32
35
 
33
36
  ```python
@@ -58,14 +61,20 @@ also wakes by itself on the next call, and `Sandbox.get_or_create(name)` returns
58
61
  the sandbox with that name or creates it. A sandbox pauses itself after 60 seconds
59
62
  with nothing happening in it, keeping its memory and processes, so an
60
63
  idle sandbox costs no compute; `idle_pause_seconds` sets 10 to 86,400, or 0 for
61
- never. The client has `sandboxes`, `images`, `volumes`, `snapshots`, `jobs`,
64
+ never. `runtime.sandbox`, `snapshot`, `image`, `volume`, `job`, `domain`,
65
+ `port` and `address` name the products as the CLI does. Their existing plural
66
+ names remain aliases of the same product clients. The client has `sandboxes`, `images`, `volumes`, `snapshots`, `jobs`,
62
67
  `secrets`, `limits`, `feedback` and `support`; `webhooks`, `events`, `otel` and
63
68
  `audit` to watch the account; `domains`, `ports`, `addresses`, `tunnel` and
64
69
  `network` to connect sandboxes to your own world (paid accounts); and `mcp`,
65
70
  `sso`, `billing`, `referrals` and `switching`. `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
66
71
  what its agent may still spend today. Every write carries an
67
72
  idempotency key, so retries never do anything twice; errors are typed and carry
68
- `code`, `hint` and `request_id`.
73
+ `code`, `hint` and `request_id`. `RuntimeAPIError` and
74
+ `RuntimeConnectionError` name the SDK errors without shadowing Python's built-in
75
+ errors; the previous `RuntimeError` and `ConnectionError` exports remain aliases.
76
+ `runtime.usage_requests("7d")` reads the account's API call counts and error rates;
77
+ counts are exact decimal strings, and an empty window has `errorPercent: None`.
69
78
 
70
79
  ## Behind a proxy
71
80
 
@@ -84,7 +93,7 @@ proxy http://proxy.internal:3128 (HTTPS_PROXY).`
84
93
 
85
94
  ## Code written for E2B
86
95
 
87
- `withruntime.e2b` runs code written for E2B's Python SDK on Runtime. Change
96
+ `withruntime.e2b` translates supported E2B Python sandbox calls. Change
88
97
  the import and set `RUNTIME_API_KEY`:
89
98
 
90
99
  ```python no-run
@@ -122,7 +131,7 @@ JavaScript package list every mapping and gap.
122
131
 
123
132
  ## Code written for Blaxel
124
133
 
125
- `withruntime.blaxel` runs code written for Blaxel's Python SDK (`blaxel`
134
+ `withruntime.blaxel` translates supported Blaxel Python sandbox calls (`blaxel`
126
135
  0.4.11) on Runtime. Change the import and set `RUNTIME_API_KEY`, or run
127
136
  `npx withruntime login` once:
128
137
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "withruntime"
7
- version = "0.9.0"
7
+ version = "0.11.0"
8
8
  description = "Runtime Cloud: one client (sync and async) for every Runtime product. Sandboxes first."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -36,3 +36,6 @@ withruntime = "withruntime.inspect_ai"
36
36
  [tool.setuptools.packages.find]
37
37
  where = ["."]
38
38
  include = ["withruntime*"]
39
+
40
+ [tool.setuptools.package-data]
41
+ withruntime = ["py.typed"]
@@ -255,9 +255,9 @@ class Create(Base):
255
255
  "archive it (sandbox.archive()), then try again. Moving to paid credit is the account "
256
256
  "owner's decision.",
257
257
  "public_preview_not_allowed": 'On the trial, share the port privately: sandbox.previews.create({"metadata": '
258
- '{"name": ...}, "spec": {"port": ..., "public": False}}) and a token from '
259
- "preview.tokens.create(expires_at). A public preview needs a paid sandbox, "
260
- "which is the account owner's decision.",
258
+ '{"name": ...}, "spec": {"port": ..., "public": False}}) and a Runtime '
259
+ "duration-based token from sandbox.withruntime.previews.get(port, ttl_seconds=seconds). "
260
+ "A public preview needs a paid sandbox, which is the account owner's decision.",
261
261
  "busy": "Try again in a moment.", "guest_busy": "Try again in a moment.",
262
262
  "rate_limited": "Try again in a moment.",
263
263
  "unauthorized": "Set RUNTIME_API_KEY to a Runtime key (https://withruntime.com/account/keys), or run "
@@ -710,6 +710,24 @@ class Processes(Base):
710
710
 
711
711
 
712
712
  class Files(Base):
713
+ def test_failed_privileged_write_removes_its_staging_bytes(self):
714
+ box = self.create()
715
+ error = ResponseError('move failed', 422, 'permission_denied')
716
+ undo = self.refuse(box, 'write', {'/root/private'})
717
+ def fail(*args):
718
+ raise error
719
+ box.fs._sudo = fail
720
+ try:
721
+ with self.assertRaises(ResponseError) as caught:
722
+ box.fs.write('/root/private', 'private bytes')
723
+ finally:
724
+ undo()
725
+ self.assertIs(caught.exception, error)
726
+ self.assertEqual(self.runtime(box).file_map, {})
727
+ removed = self.world.called('files.remove')
728
+ self.assertEqual(len(removed), 1)
729
+ self.assertTrue(removed[0][0].startswith('/workspace/.runtime-blaxel-'))
730
+
713
731
  def test_paths_and_shapes(self):
714
732
  box = self.create()
715
733
  written = box.fs.write("/blaxel/app/config.json", "{}")
@@ -867,6 +885,20 @@ class Files(Base):
867
885
 
868
886
 
869
887
  class Previews(Base):
888
+ def test_invalid_token_expirations_refuse_before_lookup_or_native_requests(self):
889
+ box = SyncSandboxInstance({"metadata": {"name": "unresolved-source"}})
890
+ preview = _sync_sandbox.SandboxPreview(bl.Preview.from_dict({
891
+ "metadata": {"name": "app"}, "spec": {"port": 3000, "public": False}}), box)
892
+ now = datetime.now(timezone.utc)
893
+ for seconds in (-1, 0, 5, 59.999, 8 * 86400, 40 * 86400):
894
+ with self.subTest(seconds=seconds), self.assertRaises(ValueError):
895
+ preview.tokens.create(now + timedelta(seconds=seconds))
896
+ self.assertEqual(self.world.calls, [])
897
+ for invalid in (None, "not-a-date", object(), datetime.now() + timedelta(minutes=10)):
898
+ with self.assertRaises(ValueError):
899
+ preview.tokens.create(invalid)
900
+ self.assertEqual(self.world.calls, [])
901
+
870
902
  def test_create_get_tokens_delete(self):
871
903
  box = self.create({"name": "web"})
872
904
  preview = box.previews.create({"metadata": {"name": "app"}, "spec": {"port": 3000, "public": False}})
@@ -876,9 +908,15 @@ class Previews(Base):
876
908
  other = SyncSandboxInstance.get("web")
877
909
  self.assertEqual(other.previews.get("app").spec.port, 3000)
878
910
  self.assertEqual([one.name for one in other.previews.list()], ["app"])
879
- token = preview.tokens.create(datetime.now(timezone.utc) + timedelta(minutes=10))
880
- self.assertEqual(token.value, "tok-1")
881
- self.assertTrue(595 <= self.world.called("previews.get")[-1][1] <= 600)
911
+ deadline = datetime.now(timezone(timedelta(hours=5, minutes=30))) + timedelta(minutes=10, microseconds=123)
912
+ private = preview.tokens.create(deadline)
913
+ self.assertEqual(private.value, "tok-1")
914
+ self.assertLessEqual(private.expires_at, deadline)
915
+ self.assertEqual(self.world.called("previews.get")[-1],
916
+ (3000, None, deadline.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")))
917
+ token = box.withruntime.previews.get(3000, ttl_seconds=600)
918
+ self.assertEqual(token["token"], "tok-1")
919
+ self.assertEqual(self.world.called("previews.get")[-1][1], 600)
882
920
  with self.assertRaises(NotSupportedError):
883
921
  preview.tokens.list()
884
922
  with self.assertRaises(NotSupportedError) as caught:
@@ -893,6 +931,37 @@ class Previews(Base):
893
931
  box.previews.get("app")
894
932
  self.assertEqual(caught.exception.status_code, 404)
895
933
 
934
+ def test_exact_deadline_rejects_bad_responses_and_never_retries_an_old_server(self):
935
+ box = self.create({"name": "web"})
936
+ preview = box.previews.create({"metadata": {"name": "app"}, "spec": {"port": 3000}})
937
+ native = box.withruntime.previews
938
+ deadline = datetime.now(timezone.utc) + timedelta(minutes=10)
939
+ replies = ({"token": "must-not-escape"},
940
+ {"token": "must-not-escape", "tokenExpiresAt": "not-a-date"},
941
+ {"token": "must-not-escape", "tokenExpiresAt": (deadline + timedelta(seconds=1)).isoformat()},
942
+ {"token": "must-not-escape", "tokenExpiresAt": deadline.replace(tzinfo=None).isoformat()},
943
+ {"token": None, "tokenExpiresAt": None})
944
+ for reply in replies:
945
+ calls = []
946
+ def get(port, **options):
947
+ calls.append((port, options))
948
+ return reply
949
+ native.get = get
950
+ with self.subTest(reply=reply), self.assertRaises(SandboxAPIError):
951
+ preview.tokens.create(deadline)
952
+ self.assertEqual(len(calls), 1)
953
+ self.assertEqual(set(calls[0][1]), {"expires_at"})
954
+ calls = []
955
+ def old_server(port, **options):
956
+ calls.append((port, options))
957
+ raise withruntime.InvalidRequestError("Unknown expiresAt field", code="unknown_field", status=400)
958
+ native.get = old_server
959
+ with self.assertRaises(SandboxAPIError) as caught:
960
+ preview.tokens.create(deadline)
961
+ self.assertEqual((caught.exception.status_code, caught.exception.code), (400, "unknown_field"))
962
+ self.assertEqual(len(calls), 1)
963
+ self.assertNotIn("ttl_seconds", calls[0][1])
964
+
896
965
  def test_what_a_runtime_preview_cannot_do(self):
897
966
  box = self.create()
898
967
  for field in ("responseHeaders", "customDomain", "ttl"):
@@ -988,7 +1057,20 @@ class Lifecycle(Base):
988
1057
  self.create({"name": name})
989
1058
  page = SyncSandboxInstance.list(limit=2)
990
1059
  self.assertEqual(([one.metadata.name for one in page.data], page.has_more), (["a", "b"], True))
1060
+ self.assertTrue(page.meta.has_more)
1061
+ self.assertEqual(page.meta.next_cursor, page.next_cursor)
1062
+ self.assertIsNone(page.meta.total)
1063
+ self.assertIsNone(page.meta.total_is_partial)
1064
+ final = page.next_page()
1065
+ self.assertFalse(final.meta.has_more)
1066
+ self.assertIsNone(final.meta.next_cursor)
1067
+ self.assertNotIn('total', final.meta.to_dict())
991
1068
  self.assertEqual([one.metadata.name for one in page.auto_paging_iter()], ["a", "b", "c"])
1069
+ page.meta.next_cursor = "changed-native-cursor"
1070
+ before = list(self.world.calls)
1071
+ with self.assertRaises(NotSupportedError):
1072
+ page.next_page()
1073
+ self.assertEqual(self.world.calls, before)
992
1074
  with self.assertRaises(NotSupportedError):
993
1075
  SyncSandboxInstance.list(cursor="abc")
994
1076
 
@@ -1026,6 +1108,33 @@ class Lifecycle(Base):
1026
1108
  box.fork("x", **kwargs)
1027
1109
 
1028
1110
 
1111
+ class ForkLifecycle(Base):
1112
+ def test_sync_fork_lifecycle_override_refuses_before_lookup_or_mutation(self):
1113
+ box = SyncSandboxInstance({"metadata": {"name": "unresolved-source"}})
1114
+ for lifecycle in ({}, {"expirationPolicies": []}, bl.SandboxLifecycle.from_dict({})):
1115
+ for options in ({}, {"snapshot_id": "unresolved-snapshot"}):
1116
+ with self.subTest(lifecycle=lifecycle, options=options):
1117
+ with self.assertRaises(NotSupportedError) as caught:
1118
+ box.fork("copy", lifecycle=lifecycle, **options)
1119
+ self.assertIn("lifecycle", caught.exception.feature)
1120
+ self.assertEqual(caught.exception.alternative,
1121
+ "Omit lifecycle to use the existing Runtime fork behavior.")
1122
+ self.assertEqual(self.world.calls, [])
1123
+
1124
+ def test_sync_fork_default_and_explicit_none_pass_labels_without_override(self):
1125
+ lifecycle = {"expirationPolicies": [{"type": "ttl-idle", "value": "3d", "action": "delete"}]}
1126
+ box = self.create({"name": "lifecycle-source", "ttl": "5d", "lifecycle": lifecycle})
1127
+ labels = dict(self.runtime(box).info["labels"])
1128
+ default = box.fork("default-copy")
1129
+ explicit = box.fork("none-copy", lifecycle=None)
1130
+ self.assertEqual((default.name, explicit.name), ("default-copy", "none-copy"))
1131
+ forks = self.world.called("sandbox.fork")
1132
+ self.assertEqual(len(forks), 2)
1133
+ for _, fields in forks:
1134
+ self.assertEqual(fields["labels"], labels)
1135
+ self.assertNotIn("lifecycle", fields)
1136
+
1137
+
1029
1138
  class Interpreter(Base):
1030
1139
  def test_run_code_and_contexts(self):
1031
1140
  interpreter = SyncCodeInterpreter.create({"name": "ci", "envs": [{"name": "K", "value": "v"}]})
@@ -1086,6 +1195,31 @@ class Unsupported(Base):
1086
1195
 
1087
1196
 
1088
1197
  class Async(unittest.TestCase):
1198
+ def test_canceled_privileged_write_removes_its_staging_bytes(self):
1199
+ from withruntime.blaxel._async_sandbox import AsyncSandboxFileSystem
1200
+ async def scenario():
1201
+ paths = {}
1202
+ class Files:
1203
+ async def write(self, path, data):
1204
+ if path == '/root/private':
1205
+ raise withruntime.PermissionDeniedError('root only', code='permission_denied', status=403)
1206
+ paths[path] = data
1207
+ async def remove(self, path):
1208
+ paths.pop(path, None)
1209
+ files = Files()
1210
+ async def get_files():
1211
+ return files
1212
+ canceled = asyncio.CancelledError()
1213
+ async def fail(*args):
1214
+ raise canceled
1215
+ filesystem = AsyncSandboxFileSystem(None, None)
1216
+ filesystem._files, filesystem._sudo = get_files, fail
1217
+ with self.assertRaises(asyncio.CancelledError) as caught:
1218
+ await filesystem.write('/root/private', 'private bytes')
1219
+ self.assertIs(caught.exception, canceled)
1220
+ self.assertEqual(paths, {})
1221
+ asyncio.run(scenario())
1222
+
1089
1223
  def setUp(self) -> None:
1090
1224
  offline()
1091
1225
  self.world = BlaxelWorld()
@@ -1096,6 +1230,95 @@ class Async(unittest.TestCase):
1096
1230
  os.environ.clear()
1097
1231
  os.environ.update(SAVED)
1098
1232
 
1233
+ def test_async_native_list_metadata_tracks_each_page_without_invented_totals(self):
1234
+ async def scenario():
1235
+ for name in ("a", "b", "c"):
1236
+ await SandboxInstance.create({"name": name})
1237
+ page = await SandboxInstance.list(limit=2)
1238
+ self.assertEqual([one.metadata.name for one in page.data], ["a", "b"])
1239
+ self.assertTrue(page.meta.has_more)
1240
+ self.assertEqual(page.meta.next_cursor, page.next_cursor)
1241
+ self.assertIsNone(page.meta.total)
1242
+ self.assertIsNone(page.meta.total_is_partial)
1243
+ final = await page.next_page()
1244
+ self.assertEqual([one.metadata.name for one in final.data], ["c"])
1245
+ self.assertFalse(final.meta.has_more)
1246
+ self.assertIsNone(final.meta.next_cursor)
1247
+ self.assertNotIn("total", final.meta.to_dict())
1248
+ page.meta.next_cursor = "changed-native-cursor"
1249
+ before = list(self.world.calls)
1250
+ with self.assertRaises(NotSupportedError):
1251
+ await page.next_page()
1252
+ self.assertEqual(self.world.calls, before)
1253
+ asyncio.run(scenario())
1254
+
1255
+ def test_async_invalid_token_expirations_refuse_before_lookup_or_native_requests(self):
1256
+ from withruntime.blaxel._async_sandbox import AsyncSandboxPreview
1257
+ async def scenario():
1258
+ box = SandboxInstance({"metadata": {"name": "unresolved-source"}})
1259
+ preview = AsyncSandboxPreview(bl.Preview.from_dict({
1260
+ "metadata": {"name": "app"}, "spec": {"port": 3000, "public": False}}), box)
1261
+ now = datetime.now(timezone.utc)
1262
+ for seconds in (-1, 0, 5, 59.999, 8 * 86400, 40 * 86400):
1263
+ with self.subTest(seconds=seconds), self.assertRaises(ValueError):
1264
+ await preview.tokens.create(now + timedelta(seconds=seconds))
1265
+ self.assertEqual(self.world.calls, [])
1266
+ for invalid in (None, "not-a-date", object(), datetime.now() + timedelta(minutes=10)):
1267
+ with self.assertRaises(ValueError):
1268
+ await preview.tokens.create(invalid)
1269
+ self.assertEqual(self.world.calls, [])
1270
+ asyncio.run(scenario())
1271
+
1272
+ def test_async_token_forwards_exact_deadline_and_refuses_extension(self):
1273
+ async def scenario():
1274
+ box = await SandboxInstance.create({"name": "web"})
1275
+ preview = await box.previews.create({"metadata": {"name": "app"}, "spec": {"port": 3000}})
1276
+ deadline = datetime.now(timezone.utc) + timedelta(minutes=10, microseconds=123)
1277
+ token = await preview.tokens.create(deadline)
1278
+ self.assertEqual(token.value, "tok-1")
1279
+ self.assertTrue(token.expires_at.endswith("Z"))
1280
+ self.assertLessEqual(datetime.fromisoformat(token.expires_at.replace("Z", "+00:00")), deadline)
1281
+ self.assertEqual(self.world.called("previews.get")[-1],
1282
+ (3000, None, deadline.isoformat(timespec="milliseconds").replace("+00:00", "Z")))
1283
+ calls = []
1284
+ def get(port, **options):
1285
+ calls.append((port, options))
1286
+ return {"token": "must-not-escape", "tokenExpiresAt": (deadline + timedelta(seconds=1)).isoformat()}
1287
+ self.world.sandboxes[box.withruntime.id].previews.get = get
1288
+ with self.assertRaises(SandboxAPIError):
1289
+ await preview.tokens.create(deadline)
1290
+ self.assertEqual(len(calls), 1)
1291
+ asyncio.run(scenario())
1292
+
1293
+ def test_async_fork_lifecycle_override_refuses_before_lookup_or_mutation(self):
1294
+ async def scenario():
1295
+ box = SandboxInstance({"metadata": {"name": "unresolved-source"}})
1296
+ for lifecycle in ({}, {"expirationPolicies": []}, bl.SandboxLifecycle.from_dict({})):
1297
+ for options in ({}, {"snapshot_id": "unresolved-snapshot"}):
1298
+ with self.subTest(lifecycle=lifecycle, options=options):
1299
+ with self.assertRaises(NotSupportedError) as caught:
1300
+ await box.fork("copy", lifecycle=lifecycle, **options)
1301
+ self.assertIn("lifecycle", caught.exception.feature)
1302
+ self.assertEqual(caught.exception.alternative,
1303
+ "Omit lifecycle to use the existing Runtime fork behavior.")
1304
+ self.assertEqual(self.world.calls, [])
1305
+ asyncio.run(scenario())
1306
+
1307
+ def test_async_fork_default_and_explicit_none_pass_labels_without_override(self):
1308
+ async def scenario():
1309
+ lifecycle = {"expirationPolicies": [{"type": "ttl-idle", "value": "3d", "action": "delete"}]}
1310
+ box = await SandboxInstance.create({"name": "lifecycle-source", "ttl": "5d", "lifecycle": lifecycle})
1311
+ labels = dict(self.world.sandboxes[box.withruntime.id].info["labels"])
1312
+ default = await box.fork("default-copy")
1313
+ explicit = await box.fork("none-copy", lifecycle=None)
1314
+ self.assertEqual((default.name, explicit.name), ("default-copy", "none-copy"))
1315
+ forks = self.world.called("sandbox.fork")
1316
+ self.assertEqual(len(forks), 2)
1317
+ for _, fields in forks:
1318
+ self.assertEqual(fields["labels"], labels)
1319
+ self.assertNotIn("lifecycle", fields)
1320
+ asyncio.run(scenario())
1321
+
1099
1322
  def test_the_async_api(self):
1100
1323
  async def scenario():
1101
1324
  box = await SandboxInstance.create({"name": "a", "envs": [{"name": "A", "value": "1"}]})
@@ -1170,6 +1393,204 @@ class ImportPaths(unittest.TestCase):
1170
1393
  self.assertTrue(caught.exception.alternative)
1171
1394
 
1172
1395
 
1396
+ class PreviewTokenExpiryShape(unittest.TestCase):
1397
+ def test_sync_utc_expiry_supports_the_python_310_iso_parser(self):
1398
+ from unittest.mock import patch
1399
+ from withruntime.blaxel._async_sandbox import AsyncSandboxPreviewToken
1400
+ calls = []
1401
+ class Python310Datetime:
1402
+ @staticmethod
1403
+ def fromisoformat(text):
1404
+ calls.append(text)
1405
+ if text.endswith('Z'):
1406
+ raise ValueError('Python 3.10 does not parse terminal Z')
1407
+ return datetime.fromisoformat(text)
1408
+ expiry = '2026-10-01T00:00:00.123Z'
1409
+ model = bl.PreviewToken(spec=bl.PreviewTokenSpec(token='private', expires_at=expiry))
1410
+ with patch('withruntime.blaxel._sync_io.datetime', Python310Datetime):
1411
+ parsed = _sync_sandbox.SandboxPreviewToken(model).expires_at
1412
+ self.assertEqual(parsed, datetime(2026, 10, 1, 0, 0, 0, 123000, tzinfo=timezone.utc))
1413
+ self.assertEqual(calls, ['2026-10-01T00:00:00.123+00:00'])
1414
+ self.assertIs(AsyncSandboxPreviewToken(model).expires_at, expiry)
1415
+
1416
+ def test_async_expiry_text_and_sync_datetime_match_consumer_expectations(self):
1417
+ from withruntime.blaxel._async_sandbox import AsyncSandboxPreviewToken
1418
+ for expiry in ('2026-10-01T00:00:00.123Z', '2026-10-01T05:30:00.123+05:30'):
1419
+ model = bl.PreviewToken(spec=bl.PreviewTokenSpec(token='private', expires_at=expiry))
1420
+ asynchronous = AsyncSandboxPreviewToken(model)
1421
+ synchronous = _sync_sandbox.SandboxPreviewToken(model)
1422
+ self.assertIs(asynchronous.expires_at, model.spec.expires_at)
1423
+ self.assertEqual(asynchronous.expires_at.endswith('Z'), expiry.endswith('Z'))
1424
+ self.assertEqual(synchronous.expires_at, datetime.fromisoformat(expiry.replace('Z', '+00:00')))
1425
+ self.assertEqual(synchronous.expires_at.utcoffset(), datetime.fromisoformat(expiry.replace('Z', '+00:00')).utcoffset())
1426
+ empty = bl.PreviewToken(spec=bl.PreviewTokenSpec())
1427
+ self.assertIsNone(AsyncSandboxPreviewToken(empty).expires_at)
1428
+ self.assertIsNone(_sync_sandbox.SandboxPreviewToken(empty).expires_at.utcoffset())
1429
+ no_spec = bl.PreviewToken()
1430
+ self.assertIsNone(AsyncSandboxPreviewToken(no_spec).expires_at.utcoffset())
1431
+ self.assertIsNone(_sync_sandbox.SandboxPreviewToken(no_spec).expires_at.utcoffset())
1432
+
1433
+ @unittest.skipUnless(os.environ.get('RUNTIME_COMPAT_OFFICIAL') == '1', 'Pinned official SDK opt-in')
1434
+ def test_pinned_official_unchanged_expiry_property_consumers(self):
1435
+ import importlib.metadata
1436
+ from blaxel.core.sandbox.default.preview import SandboxPreviewToken as OfficialAsync
1437
+ from blaxel.core.sandbox.sync.preview import SyncSandboxPreviewToken as OfficialSync
1438
+ from blaxel.core.client.models.preview_token import PreviewToken as OfficialToken
1439
+ from blaxel.core.client.models.preview_token_spec import PreviewTokenSpec as OfficialSpec
1440
+ self.assertEqual(importlib.metadata.version('blaxel'), '0.4.13')
1441
+ expiry = '2026-10-01T00:00:00.123Z'
1442
+ def async_consumer(token):
1443
+ return token.value, token.expires_at.endswith('Z'), token.expires_at
1444
+ def sync_consumer(token):
1445
+ return token.value, token.expires_at.isoformat(), token.expires_at.utcoffset()
1446
+ official = OfficialToken(spec=OfficialSpec(token='private', expires_at=expiry))
1447
+ migrated = bl.PreviewToken(spec=bl.PreviewTokenSpec(token='private', expires_at=expiry))
1448
+ self.assertEqual(async_consumer(OfficialAsync(official)), async_consumer(bl.SandboxPreviewToken(migrated)))
1449
+ self.assertEqual(sync_consumer(OfficialSync(official)), sync_consumer(_sync_sandbox.SandboxPreviewToken(migrated)))
1450
+
1451
+
1452
+ class NativePreviewDeadlineTransport(unittest.TestCase):
1453
+ def test_native_sync_and_async_forward_deadline_fields_and_preserve_existing_options(self):
1454
+ from types import SimpleNamespace
1455
+ from withruntime._sync_products.previews import Previews
1456
+ from withruntime._async_products.previews import AsyncPreviews
1457
+ deadline = '2026-10-01T00:00:00.123Z'
1458
+ calls = []
1459
+ class Transport:
1460
+ def json(self, method, path, **options):
1461
+ calls.append((method, path, options))
1462
+ return {}
1463
+ sync = Previews(Transport(), SimpleNamespace(id='box'))
1464
+ sync.get(3000)
1465
+ sync.get(3000, expires_at=deadline)
1466
+ sync.get(3000, ttl_seconds=120, expires_at=deadline)
1467
+ sync.create(3000, visibility='private', ttl_seconds=120, expires_at=deadline,
1468
+ embed_origins=['https://app.test'], idempotency_key='request')
1469
+ expected = [('GET', '/v1/sandboxes/box/previews/3000', {'query': None}),
1470
+ ('GET', '/v1/sandboxes/box/previews/3000', {'query': {'expiresAt': deadline}}),
1471
+ ('GET', '/v1/sandboxes/box/previews/3000', {'query': {'ttlSeconds': 120, 'expiresAt': deadline}}),
1472
+ ('POST', '/v1/sandboxes/box/previews',
1473
+ {'body': {'port': 3000, 'visibility': 'private', 'ttlSeconds': 120, 'expiresAt': deadline,
1474
+ 'embedOrigins': ['https://app.test']}, 'idempotency_key': 'request'})]
1475
+ self.assertEqual(calls, expected)
1476
+ calls.clear()
1477
+ class AsyncTransport:
1478
+ async def json(self, method, path, **options):
1479
+ calls.append((method, path, options))
1480
+ return {}
1481
+ async def run():
1482
+ native = AsyncPreviews(AsyncTransport(), SimpleNamespace(id='box'))
1483
+ await native.get(3000)
1484
+ await native.get(3000, expires_at=deadline)
1485
+ await native.get(3000, ttl_seconds=120, expires_at=deadline)
1486
+ await native.create(3000, visibility='private', ttl_seconds=120, expires_at=deadline,
1487
+ embed_origins=['https://app.test'], idempotency_key='request')
1488
+ asyncio.run(run())
1489
+ self.assertEqual(calls, expected)
1490
+
1491
+
1492
+ class PaginationMetadata(unittest.TestCase):
1493
+ def test_official_metadata_imports_resolve_to_the_same_model(self):
1494
+ from withruntime.blaxel.core.client.models import PaginationMeta
1495
+ from withruntime.blaxel.core.client.models.pagination_meta import PaginationMeta as FromModule
1496
+ self.assertIs(PaginationMeta, bl.PaginationMeta)
1497
+ self.assertIs(FromModule, bl.PaginationMeta)
1498
+ from withruntime.blaxel.core.client.pagination import AsyncPaginatedList, PaginatedList
1499
+ self.assertIs(PaginatedList, bl.PaginatedList)
1500
+ self.assertIs(AsyncPaginatedList, bl.AsyncPaginatedList)
1501
+
1502
+ @unittest.skipUnless(os.environ.get('RUNTIME_COMPAT_OFFICIAL') == '1', 'Pinned official SDK opt-in')
1503
+ def test_pinned_official_unchanged_pagination_consumers(self):
1504
+ import importlib
1505
+ import importlib.metadata
1506
+ self.assertEqual(importlib.metadata.version('blaxel'), '0.4.13')
1507
+ def sync_consumer(pages, model):
1508
+ seen = []
1509
+ meta = model(has_more=True, next_cursor='before')
1510
+ final = pages.PaginatedList([2], meta=model(has_more=False))
1511
+ page = pages.PaginatedList([1], meta=meta, fetch_next=lambda cursor: seen.append(cursor) or final)
1512
+ self.assertIs(page.meta, meta)
1513
+ self.assertIs(page.data, page)
1514
+ meta.next_cursor = 'after'
1515
+ values = list(page.auto_paging_iter())
1516
+ return values, seen, meta.to_dict(), final.has_more, final.next_cursor
1517
+ async def async_consumer(pages, model):
1518
+ seen = []
1519
+ meta = model(has_more=True, next_cursor='before')
1520
+ final = pages.AsyncPaginatedList([2], meta=model(has_more=False))
1521
+ async def fetch(cursor):
1522
+ seen.append(cursor)
1523
+ return final
1524
+ page = pages.AsyncPaginatedList([1], meta=meta, fetch_next=fetch)
1525
+ self.assertIs(page.meta, meta)
1526
+ self.assertIs(page.data, page)
1527
+ meta.next_cursor = 'after'
1528
+ values = [value async for value in page.auto_paging_iter()]
1529
+ return values, seen, meta.to_dict(), final.has_more, final.next_cursor
1530
+ results = []
1531
+ for prefix in ('blaxel', 'withruntime.blaxel'):
1532
+ pages = importlib.import_module(prefix + '.core.client.pagination')
1533
+ model = importlib.import_module(prefix + '.core.client.models.pagination_meta').PaginationMeta
1534
+ results.append((sync_consumer(pages, model), asyncio.run(async_consumer(pages, model))))
1535
+ expected = ([1, 2], ['after'], {'hasMore': True, 'nextCursor': 'after'}, False, None)
1536
+ self.assertEqual(results, [(expected, expected), (expected, expected)])
1537
+
1538
+ def test_model_aliases_unknown_fields_and_independent_defaults(self):
1539
+ first = bl.PaginationMeta.from_dict({'hasMore': True, 'nextCursor': 'opaque', 'totalIsPartial': True, 'vendor': 1})
1540
+ second = bl.PaginationMeta.from_dict({'has_more': False, 'total': 3})
1541
+ self.assertEqual((first.has_more, first.next_cursor, first.total, first.total_is_partial), (True, 'opaque', None, True))
1542
+ self.assertEqual(first.to_dict(), {'hasMore': True, 'nextCursor': 'opaque', 'totalIsPartial': True, 'vendor': 1})
1543
+ self.assertEqual(second.to_dict(), {'hasMore': False, 'total': 3})
1544
+ self.assertEqual(first['vendor'], 1)
1545
+ first['changed'] = 2
1546
+ self.assertNotIn('changed', second)
1547
+ self.assertEqual(first.additional_keys, ['vendor', 'changed'])
1548
+ del first['changed']
1549
+ self.assertEqual(first.additional_keys, ['vendor'])
1550
+ self.assertIsNone(bl.PaginationMeta.from_dict({}))
1551
+ self.assertEqual(bl.PaginationMeta().to_dict(), {})
1552
+ self.assertEqual(bl.PaginationMeta(has_more=core.UNSET).to_dict(), {})
1553
+ aliases = bl.PaginationMeta.from_dict({'hasMore': False, 'has_more': True,
1554
+ 'nextCursor': 'first', 'next_cursor': 'second'})
1555
+ self.assertEqual(aliases.to_dict(), {'hasMore': False, 'nextCursor': 'first'})
1556
+
1557
+ def test_sync_custom_metadata_identity_mutable_cursor_and_dict_behavior(self):
1558
+ seen = []
1559
+ meta = bl.PaginationMeta(has_more=True, next_cursor='original')
1560
+ expected = bl.PaginatedList([2])
1561
+ page = bl.PaginatedList([1], meta=meta, fetch_next=lambda cursor: seen.append(cursor) or expected)
1562
+ self.assertIs(page.meta, meta)
1563
+ self.assertIs(page.data, page)
1564
+ meta.next_cursor = 'changed'
1565
+ self.assertIs(page.next_page(), expected)
1566
+ self.assertEqual(seen, ['changed'])
1567
+ meta.has_more = False
1568
+ self.assertFalse(page.has_more)
1569
+ self.assertEqual(page.next_page(), [])
1570
+ raw = {'has_more': True, 'next_cursor': 'ignored'}
1571
+ direct = bl.PaginatedList([1], meta=raw, fetch_next=lambda _: self.fail('dict metadata must not fetch'))
1572
+ self.assertIs(direct.meta, raw)
1573
+ self.assertFalse(direct.has_more)
1574
+ self.assertEqual(direct.next_page(), [])
1575
+
1576
+ def test_async_custom_metadata_identity_and_mutable_cursor(self):
1577
+ async def run():
1578
+ seen = []
1579
+ meta = bl.PaginationMeta(has_more=True, next_cursor='original')
1580
+ expected = bl.AsyncPaginatedList([2])
1581
+ async def fetch(cursor):
1582
+ seen.append(cursor)
1583
+ return expected
1584
+ page = bl.AsyncPaginatedList([1], meta=meta, fetch_next=fetch)
1585
+ self.assertIs(page.meta, meta)
1586
+ meta.next_cursor = 'changed'
1587
+ self.assertIs(await page.next_page(), expected)
1588
+ self.assertEqual(seen, ['changed'])
1589
+ meta.has_more = False
1590
+ self.assertEqual(await page.next_page(), [])
1591
+ asyncio.run(run())
1592
+
1593
+
1173
1594
  class Generated(unittest.TestCase):
1174
1595
  def test_the_sync_adapter_is_generated_from_the_async_one(self):
1175
1596
  result = subprocess.run([sys.executable, str(ROOT / "scripts" / "generate_dropin_sync.py"), "--check"],