fcloud-sdk 0.2.0__tar.gz → 0.3.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 (165) hide show
  1. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/CHANGELOG.md +101 -0
  2. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/PKG-INFO +89 -11
  3. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/README.md +85 -9
  4. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/SKILL.md +23 -2
  5. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/pyproject.toml +7 -4
  6. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/SKILL.md +23 -2
  7. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/__init__.py +21 -0
  8. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/__main__.py +2 -0
  9. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/_direct_bridge.py +92 -19
  10. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/_legacy_env.py +5 -0
  11. fcloud_sdk-0.3.0/src/fcloud/_modal_runner.py +101 -0
  12. fcloud_sdk-0.3.0/src/fcloud/billing_types.py +241 -0
  13. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/__init__.py +2 -0
  14. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/attach.py +43 -6
  15. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/common.py +2 -0
  16. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/console.py +3 -1
  17. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/context.py +49 -0
  18. fcloud_sdk-0.3.0/src/fcloud/cli/credits.py +215 -0
  19. fcloud_sdk-0.3.0/src/fcloud/cli/credits_buy.py +97 -0
  20. fcloud_sdk-0.3.0/src/fcloud/cli/credits_render.py +129 -0
  21. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/exec_cmd.py +4 -0
  22. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/files.py +23 -10
  23. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/help.py +7 -3
  24. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/interactive.py +7 -1
  25. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/job.py +63 -4
  26. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/main.py +18 -1
  27. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/migration.py +14 -1
  28. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/processes.py +42 -2
  29. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/run.py +16 -0
  30. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/sessions.py +128 -10
  31. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/setup.py +33 -34
  32. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/ssh.py +2 -0
  33. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/sweep.py +41 -4
  34. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/sweep_harvest.py +10 -1
  35. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/sweep_watch.py +12 -2
  36. fcloud_sdk-0.3.0/src/fcloud/cli/usage.py +69 -0
  37. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/volume.py +7 -1
  38. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/wait.py +9 -0
  39. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli_args.py +11 -2
  40. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/client.py +48 -2
  41. fcloud_sdk-0.3.0/src/fcloud/client_billing.py +105 -0
  42. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/config.py +24 -0
  43. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/direct.py +13 -5
  44. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/errors.py +15 -0
  45. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/image.py +60 -12
  46. fcloud_sdk-0.3.0/src/fcloud/login.py +236 -0
  47. fcloud_sdk-0.3.0/src/fcloud/modal_compat.py +306 -0
  48. fcloud_sdk-0.3.0/src/fcloud/modal_objects.py +137 -0
  49. fcloud_sdk-0.3.0/src/fcloud/modal_run.py +93 -0
  50. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/providers/requests_http.py +9 -0
  51. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/setup_cmd.py +15 -3
  52. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/sweeps.py +2 -2
  53. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/types.py +1 -0
  54. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/v2_connect.py +10 -2
  55. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud_sdk.egg-info/PKG-INFO +89 -11
  56. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud_sdk.egg-info/SOURCES.txt +22 -0
  57. fcloud_sdk-0.3.0/tests/test_credit_preflight.py +103 -0
  58. fcloud_sdk-0.3.0/tests/test_credits_cli.py +237 -0
  59. fcloud_sdk-0.3.0/tests/test_credits_errors.py +106 -0
  60. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_direct_bridge.py +152 -4
  61. fcloud_sdk-0.3.0/tests/test_download_promotion_retry.py +92 -0
  62. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_exec_attach_retry.py +23 -1
  63. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_first_run_login.py +18 -15
  64. fcloud_sdk-0.3.0/tests/test_job_ls.py +99 -0
  65. fcloud_sdk-0.3.0/tests/test_login.py +202 -0
  66. fcloud_sdk-0.3.0/tests/test_logs_unknown_pid.py +84 -0
  67. fcloud_sdk-0.3.0/tests/test_map_bake_flag.py +26 -0
  68. fcloud_sdk-0.3.0/tests/test_modal_compat.py +171 -0
  69. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_rate_limit_ride.py +2 -0
  70. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_ride_cap.py +1 -1
  71. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_sessions_live_statuses.py +17 -0
  72. fcloud_sdk-0.3.0/tests/test_spend.py +203 -0
  73. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_sweep_harvest.py +27 -0
  74. fcloud_sdk-0.3.0/tests/test_v2_connect_billing.py +52 -0
  75. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_volume_cli_hardening.py +9 -0
  76. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_wait.py +26 -0
  77. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/CONTRIBUTING.md +0 -0
  78. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/LICENSE +0 -0
  79. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/MANIFEST.in +0 -0
  80. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/NOTICE +0 -0
  81. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/setup.cfg +0 -0
  82. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/autoresearch/SETUP.md +0 -0
  83. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/autoresearch/program.template.md +0 -0
  84. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/autoresearch.py +0 -0
  85. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/config_cmd.py +0 -0
  86. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/hardware.py +0 -0
  87. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/mount.py +0 -0
  88. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/output.py +0 -0
  89. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/cli/registry.py +0 -0
  90. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/client_projects.py +0 -0
  91. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/client_sessions.py +0 -0
  92. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/client_volumes.py +0 -0
  93. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/fileset.py +0 -0
  94. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/job.py +0 -0
  95. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/providers/__init__.py +0 -0
  96. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/py.typed +0 -0
  97. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/session.py +0 -0
  98. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/shell.py +0 -0
  99. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/tunnel.py +0 -0
  100. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/version.py +0 -0
  101. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/volume_wait.py +0 -0
  102. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud/volumes.py +0 -0
  103. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud_sdk.egg-info/dependency_links.txt +0 -0
  104. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud_sdk.egg-info/entry_points.txt +0 -0
  105. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud_sdk.egg-info/requires.txt +0 -0
  106. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/fcloud_sdk.egg-info/top_level.txt +0 -0
  107. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/src/foom/__init__.py +0 -0
  108. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/conftest.py +0 -0
  109. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_autoresearch.py +0 -0
  110. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_checkpoint_option.py +0 -0
  111. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_cli_args.py +0 -0
  112. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_cli_dispatch.py +0 -0
  113. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_cli_guards.py +0 -0
  114. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_cli_help.py +0 -0
  115. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_client.py +0 -0
  116. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_client_contracts_c.py +0 -0
  117. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_client_host_frames_contract.py +0 -0
  118. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_client_http_contract.py +0 -0
  119. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_client_telemetry.py +0 -0
  120. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_config.py +0 -0
  121. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_config_cmd.py +0 -0
  122. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_default_image.py +0 -0
  123. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_direct_fake_host.py +0 -0
  124. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_dotenv_precedence.py +0 -0
  125. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_download_volume_hint.py +0 -0
  126. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_emit_pid.py +0 -0
  127. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_fileset.py +0 -0
  128. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_job.py +0 -0
  129. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_kill_records_first.py +0 -0
  130. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_legacy_shim.py +0 -0
  131. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_map_dag.py +0 -0
  132. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_map_default_image.py +0 -0
  133. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_map_window_flags.py +0 -0
  134. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_no_resume_gate.py +0 -0
  135. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_progress_narration.py +0 -0
  136. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_provider_env_default.py +0 -0
  137. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_queued_reason_render.py +0 -0
  138. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_rebuild_respawn.py +0 -0
  139. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_resume_volume_validation.py +0 -0
  140. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_run_migration.py +0 -0
  141. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_run_volume_collision.py +0 -0
  142. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_run_wait_flags.py +0 -0
  143. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_secret_resolution.py +0 -0
  144. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_session_truth.py +0 -0
  145. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_sessions_point_lookup.py +0 -0
  146. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_setup_cmd.py +0 -0
  147. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_shell_env.py +0 -0
  148. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_shell_interrupt.py +0 -0
  149. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_shell_volumes.py +0 -0
  150. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_skus.py +0 -0
  151. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_stop_refused_classification.py +0 -0
  152. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_sweep_boost.py +0 -0
  153. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_sweep_cost_optin.py +0 -0
  154. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_sweep_lifecycle_ux.py +0 -0
  155. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_sweep_retry_and_kill_truth.py +0 -0
  156. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_sweep_status_view.py +0 -0
  157. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_sweeps_binding.py +0 -0
  158. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_tunnel.py +0 -0
  159. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_types.py +0 -0
  160. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_upload_s3_path.py +0 -0
  161. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_v2_connect.py +0 -0
  162. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_volume_download_delete.py +0 -0
  163. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_volume_preflight.py +0 -0
  164. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_volume_resolution.py +0 -0
  165. {fcloud_sdk-0.2.0 → fcloud_sdk-0.3.0}/tests/test_volume_wait.py +0 -0
@@ -4,6 +4,107 @@ All notable changes to the `fcloud` Python SDK and CLI are recorded here.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
5
5
  versions follow [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [0.3.0] - 2026-09-16
10
+
11
+ ### Fixed
12
+ - `exec` no longer returns empty output for a command whose last write lands
13
+ at process exit. The host mirrors a process's durable log on a background
14
+ goroutine, so a read issued the instant the process reports "exited" can
15
+ beat that write and come back empty; the client was answering with that
16
+ empty read even though it had already drained the output off the live
17
+ stream. It now keeps whichever of the two saw more bytes. The symptom was
18
+ `stdout: ""` alongside `returncode: 0` — most often for a block-buffered
19
+ program, which flushes everything during interpreter teardown
20
+ microseconds before it dies. A training run's final `print(metrics)` is
21
+ exactly that shape.
22
+
23
+ ### Added
24
+ - `fcloud map --no-bake` opts a sweep out of canary auto-bake. By default,
25
+ packages the canary pip-installs are baked into the sweep's image before
26
+ the fan-out; the canary pass line now names them and points at
27
+ `image.pip` in `fcloud.json` as the permanent home.
28
+ - `fcloud credits`: the prepaid wallet from the terminal. `fcloud credits`
29
+ shows the posted balance, what is available after unbilled usage,
30
+ auto-refill state and the saved card; `fcloud credits buy <usd>` opens a
31
+ hosted checkout and waits for the purchase to post; `fcloud credits
32
+ autorefill on|off|set` changes the top-up rule (turning it off warns that
33
+ work stops at $0 and needs a confirmation); `fcloud credits statement`
34
+ lists ledger rows with a running balance.
35
+ - `InsufficientCreditError` (HTTP 402, `code: insufficient_credit`) — raised
36
+ when the prepaid balance is exhausted, and mapped to **exit code 4** in
37
+ the CLI with the top-up command named. It is a sibling of `FcloudError`,
38
+ not of `PaymentOverdueError`: nothing is owed, so the remedy is to add
39
+ credit, not to settle a bill. Code that catches `PaymentOverdueError`
40
+ alone will not catch it.
41
+ - `Client.credit_balance()`, `credit_ledger()`, `set_autorefill()` and
42
+ `start_credit_checkout()`, with typed payloads in `fcloud.billing_types`.
43
+ - A low-balance warning before `exec`/`run`/`shell`/`create`/`job run`/`map`
44
+ provision anything, shown once per process and only when nothing will top
45
+ the account up. Best-effort: a failed billing read never blocks a run.
46
+
47
+ ### Changed
48
+ - An out-of-credit refusal is no longer reported as a missed payment. Both
49
+ error paths now branch on the dispatcher's `code` before falling back on
50
+ the bare 402 status, so `payment_overdue`, `insufficient_credit` and an
51
+ older dispatcher's codeless 402 are told apart.
52
+ - A session closed for want of credit says so ("stopped: out of credits")
53
+ and exits 4, instead of exiting 75 as if it were briefly unreachable.
54
+ - `fcloud sweep` offers the fast lane only for waits more capacity can fix.
55
+ Tasks blocked on billing get a top-up hint — boosting past that block
56
+ buys nothing.
57
+ - Python 3.9 is now supported (`requires-python >= 3.9`), and 3.14 is
58
+ tested. The README has a support grid; CI runs the suite on 3.9 through
59
+ 3.14. The installer script falls back to `uv` instead of `pipx`/`pip`
60
+ when the system Python is older than 3.9.
61
+ - The CLI no longer prints urllib3's LibreSSL warning on macOS's bundled
62
+ Python.
63
+
64
+ ## [0.2.1] - 2026-09-14
65
+
66
+ ### Added
67
+ - `fcloud login`: sign in through the browser (device authorization). Shows a
68
+ short code, opens the approval page, and saves a key minted for this
69
+ machine (named `cli:<hostname>`); then installs the fcloud skill for the
70
+ coding agents present on the machine (`--agents auto`, the default; also
71
+ `all`, a list, or `none`). `--no-browser` prints the URL and code instead
72
+ of opening a browser. A fresh install's first command starts the same
73
+ login, and `fcloud setup` with no key delegates to it.
74
+ - Spend visibility: `fcloud sessions` shows `$/HR` (the rate billed on the
75
+ session's open usage interval while a host is held, 0 when cold), `SPENT`
76
+ (compute charged so far) and `AGE` (time on its current host) per row plus
77
+ a "Spending now" footer; `--json` rows carry `usd_per_hour`, `spent_usd`
78
+ and `host_age_seconds` (SDK: `Client.session_cost`). New `fcloud usage [--since DUR|TS] [--until TS]`
79
+ reports what the account was charged over a window (default 30 days) by
80
+ resource and session (SDK: `Client.billing_usage`).
81
+
82
+ ### Changed
83
+ - `fcloud job ls` shows each job's COMMAND and EXIT code (from its process
84
+ index; `--json` rows carry `command` and `exit_code`), so rows are
85
+ tellable apart and failures visible without opening each job.
86
+
87
+ ### Fixed
88
+ - `fcloud logs <sid> <pid>` with a pid the dispatcher has no record of now
89
+ exits 1 (`process_not_found`) like `wait` and `kill`, instead of printing
90
+ an empty stream with exit 0 (and `status: ""` in `--json`) that a poller
91
+ read as a finished, silent process. A just-spawned pid gets a few seconds
92
+ to reach the durable index before it is called unknown.
93
+ - `fcloud wait` with a `--timeout` shorter than the unknown-pid grace window
94
+ exits 1 for an unknown pid, not 75 (retry), so scripted callers stop
95
+ retrying a typo forever.
96
+ - Cold starts of `run`/`exec`/`job run` no longer dump every host status
97
+ frame (`⟵ session_status: building (phase_change) setup_timing ...`);
98
+ the fresh-allocation and reconnect paths now use the same quiet printer
99
+ as warm attaches.
100
+ - `fcloud sweep harvest` no longer pulls the session's virtual `_logs/`
101
+ directory (`_logs/processes.json`) for a results glob like `*.json`; name
102
+ `_logs/` in the glob to fetch logs.
103
+ - `fcloud sessions` sizes the HOST and SKU columns to the data (27-char AWS
104
+ host ids overflowed the fixed width on every row).
105
+ - `fcloud volume list` shows each volume's last update time.
106
+ - SKILL.md documents `fcloud sessions --limit N`.
107
+
7
108
  ## [0.2.0] - 2026-09-08
8
109
 
9
110
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fcloud-sdk
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python SDK and CLI for the fcloud GPU compute platform
5
5
  Author: fcloud
6
6
  License-Expression: Apache-2.0
@@ -11,14 +11,16 @@ Classifier: Intended Audience :: Developers
11
11
  Classifier: Intended Audience :: Science/Research
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
14
15
  Classifier: Programming Language :: Python :: 3.10
15
16
  Classifier: Programming Language :: Python :: 3.11
16
17
  Classifier: Programming Language :: Python :: 3.12
17
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
18
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
21
  Classifier: Topic :: System :: Distributed Computing
20
22
  Classifier: Typing :: Typed
21
- Requires-Python: >=3.10
23
+ Requires-Python: >=3.9
22
24
  Description-Content-Type: text/markdown
23
25
  License-File: LICENSE
24
26
  License-File: NOTICE
@@ -39,29 +41,61 @@ hosts, run commands and scripts on them, keep a persistent workspace between
39
41
  runs, move files in and out, and drive long-running or batch jobs — from a
40
42
  terminal, from Python, or from an AI coding agent.
41
43
 
42
- Requires Python 3.10+.
44
+ Requires Python 3.9+ (see [Python versions](#python-versions)).
43
45
 
44
46
  ## Install
45
47
 
48
+ ```bash
49
+ curl -fsSL https://fcloud-home.vercel.app/install.sh | sh
50
+ ```
51
+
52
+ That installs the CLI from PyPI (via `uv` or `pipx`), signs you in through
53
+ the browser, and installs the fcloud skill for coding agents on your machine.
54
+ With your own Python:
55
+
46
56
  ```bash
47
57
  pip install fcloud-sdk
58
+ fcloud login
48
59
  ```
49
60
 
61
+ fcloud is prepaid — `fcloud credits buy 25` before your first run, and
62
+ `fcloud credits` to see the balance.
63
+
64
+ ### Python versions
65
+
66
+ Every row runs the full test suite in CI on every push (the matrix in
67
+ `.github/workflows/unit-tests.yml` is the source of truth; a CI check fails
68
+ if `pyproject.toml`'s classifiers drift from it).
69
+
70
+ | Python | Status |
71
+ |--------|--------|
72
+ | 3.14 | supported |
73
+ | 3.13 | supported |
74
+ | 3.12 | supported |
75
+ | 3.11 | supported |
76
+ | 3.10 | supported |
77
+ | 3.9 | supported (macOS command-line-tools Python, Debian 11) |
78
+ | 3.8 and older | not supported; the installer script bootstraps `uv`, which fetches a supported Python |
79
+
80
+ The installer only uses your system Python (`pipx`/`pip`) when it is 3.9 or
81
+ newer; otherwise it installs `uv`, which brings its own.
82
+
50
83
  ## Setup
51
84
 
52
- Create an account on https://fcloud-home.vercel.app.
53
- Enter a credit card, and copy your API key.
54
- Or just run any `fcloud` command (or `fcloud login`): a fresh install
55
- walks you through sign-up and saves the key you paste.
85
+ `fcloud login` opens https://fcloud-home.vercel.app to sign in (Google or
86
+ email) and add a card, then saves a key minted for this machine. Running any
87
+ `fcloud` command on a fresh install starts the same login.
56
88
 
57
89
  ```bash
58
- fcloud setup # prompts for the key; optionally installs the agent skill
90
+ fcloud login [--no-browser] [--agents auto|all|cursor|claude|codex|none]
91
+ fcloud set_token <key> # already have a key (CI, agents, a second machine)
92
+ fcloud setup # save a key you already have; install agent skills
59
93
  fcloud health # verify connectivity
60
94
  ```
61
95
 
62
- `fcloud setup` also installs (by default) the fcloud skill file for supported
63
- coding agents (Cursor, Claude Code, Codex) so an agent can drive fcloud for you.
64
- Use `--agents none` to skip that.
96
+ `fcloud login` installs the fcloud skill file for the coding agents it finds
97
+ (Cursor, Claude Code, Codex — `--agents all` for every one) so an agent can
98
+ drive fcloud for you. `fcloud setup --agents none` skips that.
65
99
 
66
100
  ## Quick start
67
101
 
@@ -141,6 +175,7 @@ Batch
141
175
  fcloud sweeps / fcloud sweep <status|logs|retry|cancel|wait> <name>
142
176
 
143
177
  Setup
178
+ fcloud login [--no-browser] [--agents auto|all|cursor|claude|codex|none]
144
179
  fcloud setup [--token KEY] [--agents all|cursor|claude|codex|none]
145
180
  fcloud set_token <api-key>
146
181
  fcloud --version
@@ -177,6 +212,49 @@ with project.session(sku="gpu_1x_l4") as s:
177
212
 
178
213
  Errors raise `fcloud.FcloudError` (or a subclass such as `PaymentOverdueError`).
179
214
 
215
+ ## Porting from Modal
216
+
217
+ `fcloud` exposes a Modal-compatible surface, so most Modal scripts port with an
218
+ import rename:
219
+
220
+ ```python
221
+ import fcloud as modal # was: import modal
222
+
223
+ app = modal.App("demo")
224
+ image = modal.Image.debian_slim().pip_install("torch", "numpy")
225
+
226
+ @app.function(image=image, gpu="H100", timeout=600)
227
+ def train(steps: int) -> float:
228
+ ...
229
+
230
+ @app.cls(gpu="L4", volumes={"/data": modal.Volume.from_name("weights", create_if_missing=True)})
231
+ class Model:
232
+ @modal.enter()
233
+ def load(self): ...
234
+ @modal.method()
235
+ def predict(self, x): ...
236
+
237
+ @app.local_entrypoint()
238
+ def main(steps: int = 100):
239
+ print(train.remote(steps))
240
+ print(Model().predict.remote(1))
241
+ ```
242
+
243
+ Run it with `fcloud run demo.py [--steps 500]` (the Modal-style `demo.py::name`
244
+ picks an entrypoint or function). Each `(gpu, image, volumes)` combination gets
245
+ one warm session; `.remote()` pickles the args, runs the function on the host
246
+ and returns the pickled result, streaming stdout back live.
247
+
248
+ Supported: `App`, `@app.function` / `@app.cls` / `@app.local_entrypoint`,
249
+ `.remote()` / `.spawn()` / `.map()`, `Image.*` (varargs or list),
250
+ `Volume.from_name`, `Secret.from_dict` / `from_dotenv` / `from_local_environ`,
251
+ `gpu="H100"`, `"A100-80GB:8"`, etc. `cpu=`, `memory=`, `retries=` and similar
252
+ options are accepted and ignored with a warning.
253
+
254
+ Not supported: web endpoints, `Dict` / `Queue`, schedules, sandboxes,
255
+ `Secret.from_name` (no hosted secret store), `modal deploy`. `.map()` runs
256
+ inputs sequentially on one session; use `fcloud map` for real fan-out.
257
+
180
258
  ## Configuration
181
259
 
182
260
  API key, in order of precedence:
@@ -5,29 +5,61 @@ hosts, run commands and scripts on them, keep a persistent workspace between
5
5
  runs, move files in and out, and drive long-running or batch jobs — from a
6
6
  terminal, from Python, or from an AI coding agent.
7
7
 
8
- Requires Python 3.10+.
8
+ Requires Python 3.9+ (see [Python versions](#python-versions)).
9
9
 
10
10
  ## Install
11
11
 
12
+ ```bash
13
+ curl -fsSL https://fcloud-home.vercel.app/install.sh | sh
14
+ ```
15
+
16
+ That installs the CLI from PyPI (via `uv` or `pipx`), signs you in through
17
+ the browser, and installs the fcloud skill for coding agents on your machine.
18
+ With your own Python:
19
+
12
20
  ```bash
13
21
  pip install fcloud-sdk
22
+ fcloud login
14
23
  ```
15
24
 
25
+ fcloud is prepaid — `fcloud credits buy 25` before your first run, and
26
+ `fcloud credits` to see the balance.
27
+
28
+ ### Python versions
29
+
30
+ Every row runs the full test suite in CI on every push (the matrix in
31
+ `.github/workflows/unit-tests.yml` is the source of truth; a CI check fails
32
+ if `pyproject.toml`'s classifiers drift from it).
33
+
34
+ | Python | Status |
35
+ |--------|--------|
36
+ | 3.14 | supported |
37
+ | 3.13 | supported |
38
+ | 3.12 | supported |
39
+ | 3.11 | supported |
40
+ | 3.10 | supported |
41
+ | 3.9 | supported (macOS command-line-tools Python, Debian 11) |
42
+ | 3.8 and older | not supported; the installer script bootstraps `uv`, which fetches a supported Python |
43
+
44
+ The installer only uses your system Python (`pipx`/`pip`) when it is 3.9 or
45
+ newer; otherwise it installs `uv`, which brings its own.
46
+
16
47
  ## Setup
17
48
 
18
- Create an account on https://fcloud-home.vercel.app.
19
- Enter a credit card, and copy your API key.
20
- Or just run any `fcloud` command (or `fcloud login`): a fresh install
21
- walks you through sign-up and saves the key you paste.
49
+ `fcloud login` opens https://fcloud-home.vercel.app to sign in (Google or
50
+ email) and add a card, then saves a key minted for this machine. Running any
51
+ `fcloud` command on a fresh install starts the same login.
22
52
 
23
53
  ```bash
24
- fcloud setup # prompts for the key; optionally installs the agent skill
54
+ fcloud login [--no-browser] [--agents auto|all|cursor|claude|codex|none]
55
+ fcloud set_token <key> # already have a key (CI, agents, a second machine)
56
+ fcloud setup # save a key you already have; install agent skills
25
57
  fcloud health # verify connectivity
26
58
  ```
27
59
 
28
- `fcloud setup` also installs (by default) the fcloud skill file for supported
29
- coding agents (Cursor, Claude Code, Codex) so an agent can drive fcloud for you.
30
- Use `--agents none` to skip that.
60
+ `fcloud login` installs the fcloud skill file for the coding agents it finds
61
+ (Cursor, Claude Code, Codex — `--agents all` for every one) so an agent can
62
+ drive fcloud for you. `fcloud setup --agents none` skips that.
31
63
 
32
64
  ## Quick start
33
65
 
@@ -107,6 +139,7 @@ Batch
107
139
  fcloud sweeps / fcloud sweep <status|logs|retry|cancel|wait> <name>
108
140
 
109
141
  Setup
142
+ fcloud login [--no-browser] [--agents auto|all|cursor|claude|codex|none]
110
143
  fcloud setup [--token KEY] [--agents all|cursor|claude|codex|none]
111
144
  fcloud set_token <api-key>
112
145
  fcloud --version
@@ -143,6 +176,49 @@ with project.session(sku="gpu_1x_l4") as s:
143
176
 
144
177
  Errors raise `fcloud.FcloudError` (or a subclass such as `PaymentOverdueError`).
145
178
 
179
+ ## Porting from Modal
180
+
181
+ `fcloud` exposes a Modal-compatible surface, so most Modal scripts port with an
182
+ import rename:
183
+
184
+ ```python
185
+ import fcloud as modal # was: import modal
186
+
187
+ app = modal.App("demo")
188
+ image = modal.Image.debian_slim().pip_install("torch", "numpy")
189
+
190
+ @app.function(image=image, gpu="H100", timeout=600)
191
+ def train(steps: int) -> float:
192
+ ...
193
+
194
+ @app.cls(gpu="L4", volumes={"/data": modal.Volume.from_name("weights", create_if_missing=True)})
195
+ class Model:
196
+ @modal.enter()
197
+ def load(self): ...
198
+ @modal.method()
199
+ def predict(self, x): ...
200
+
201
+ @app.local_entrypoint()
202
+ def main(steps: int = 100):
203
+ print(train.remote(steps))
204
+ print(Model().predict.remote(1))
205
+ ```
206
+
207
+ Run it with `fcloud run demo.py [--steps 500]` (the Modal-style `demo.py::name`
208
+ picks an entrypoint or function). Each `(gpu, image, volumes)` combination gets
209
+ one warm session; `.remote()` pickles the args, runs the function on the host
210
+ and returns the pickled result, streaming stdout back live.
211
+
212
+ Supported: `App`, `@app.function` / `@app.cls` / `@app.local_entrypoint`,
213
+ `.remote()` / `.spawn()` / `.map()`, `Image.*` (varargs or list),
214
+ `Volume.from_name`, `Secret.from_dict` / `from_dotenv` / `from_local_environ`,
215
+ `gpu="H100"`, `"A100-80GB:8"`, etc. `cpu=`, `memory=`, `retries=` and similar
216
+ options are accepted and ignored with a warning.
217
+
218
+ Not supported: web endpoints, `Dict` / `Queue`, schedules, sandboxes,
219
+ `Secret.from_name` (no hosted secret store), `modal deploy`. `.map()` runs
220
+ inputs sequentially on one session; use `fcloud map` for real fan-out.
221
+
146
222
  ## Configuration
147
223
 
148
224
  API key, in order of precedence:
@@ -25,6 +25,9 @@ fcloud set_token <api_key> # one-time; saves to ~/.fcloud/token
25
25
  fcloud health # verify connectivity
26
26
  ```
27
27
 
28
+ (A human sets up with `fcloud login`, which signs in through the browser and
29
+ mints a key for the machine; agents use `set_token` with a key they were given.)
30
+
28
31
  Token resolution order:
29
32
 
30
33
  1. `api_key=` parameter to `Client()`
@@ -85,6 +88,8 @@ ignored.
85
88
  | ---------------------- | ----------------------------------------- |
86
89
  | `fcloud health` | Check API connectivity |
87
90
  | `fcloud set_token <key>` | Save API key |
91
+ | `fcloud credits` | Prepaid balance, auto-refill, card |
92
+ | `fcloud credits buy <usd>` | Buy credit (opens hosted checkout) |
88
93
 
89
94
 
90
95
  ### Execution
@@ -303,7 +308,8 @@ filesystem later; a **job** for batch/CI-style runs with declared outputs.
303
308
  | Command | Description |
304
309
  | ----------------------------- | ---------------------------------------------------- |
305
310
  | `fcloud create [--sku SKU]` | Create a session (cold — $0, no host, until first used) |
306
- | `fcloud sessions [--all]` | List sessions |
311
+ | `fcloud sessions [--all] [--limit N]` | List sessions with `$/HR` (billed rate), `SPENT` (compute so far) and `AGE` (time on its host) per row and a spending-now total (`--all --json` can be large; bound it with `--limit`) |
312
+ | `fcloud usage [--since 24h]` | What the account has been charged over a window (default 30 days), by resource and session |
307
313
  | `fcloud stop <session-id> [--wait]` | Stop a session now: halt GPU spend (files kept). `--wait` blocks until the workspace manifest is durable |
308
314
 
309
315
 
@@ -983,6 +989,16 @@ fcloud wait s-abc123 "$PID" --timeout 0
983
989
  > alive (`fcloud logs <sid>` lists it as `running`) rather than assuming `rc=75`
984
990
  > means your job is gone.
985
991
  >
992
+ > **Out of credit (`rc=4`).** A 402 with `code: insufficient_credit` means the
993
+ > prepaid balance is exhausted: sessions are closed and queued jobs canceled.
994
+ > **This is not retryable** — retrying returns the same refusal until credit
995
+ > lands. Distinguish it from `rc=3` (`payment_overdue`, a failed payment on a
996
+ > bill) and from `rc=75` (transient unreachability, which *is* retryable). The
997
+ > remedy is `fcloud credits buy 25` or `fcloud credits autorefill on`; the block
998
+ > clears about a minute after the credit posts. In the SDK it raises
999
+ > `fcloud.InsufficientCreditError`, a sibling of `PaymentOverdueError` — code
1000
+ > that catches only `PaymentOverdueError` will not catch it.
1001
+ >
986
1002
  > **Migration rides honor `--wait`.** When a foreground `exec` rides a
987
1003
  > checkpoint/restore, the ride is bounded by your `--wait` budget — it never
988
1004
  > blocks past it (`--wait 0` disables the bound; the ride then uses the full
@@ -1076,7 +1092,10 @@ fcloud exec --on s-abc123 /workspace/venv/bin/python train.py
1076
1092
  ```
1077
1093
 
1078
1094
  Better yet, put packages in the image spec (`pip_install([...])`) so every
1079
- resume rebuilds them from the content-addressed image cache.
1095
+ resume rebuilds them from the content-addressed image cache. A sweep whose
1096
+ canary pip-installs packages gets them baked into its image automatically
1097
+ before the fan-out (the pass line names them; `--no-bake` disables) — but
1098
+ the pins still belong in `fcloud.json` so the *next* sweep never pays them.
1080
1099
 
1081
1100
  ---
1082
1101
 
@@ -1215,6 +1234,8 @@ host (files restored from cloud storage) the next time you use it. You never run
1215
1234
  **Warmth**: `fcloud sessions` reports a session's state — `hot` (running, spending),
1216
1235
  `warm` (idle host held, spending), `stopping` (finishing teardown), `cold`
1217
1236
  (stopped, $0, resumes on use), or `preparing` (coming online).
1237
+ The `$/HR` column is the rate billed on the session's open usage interval while a
1238
+ host is held (0 when cold); `SPENT` is compute charged so far; the footer sums `$/HR`. `fcloud usage` reports what was actually charged.
1218
1239
 
1219
1240
  **Image**: Declarative container spec. Layers are cached by content hash. First run
1220
1241
  on a host triggers a build (30–120s); subsequent runs with the same image are instant.
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fcloud-sdk"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Python SDK and CLI for the fcloud GPU compute platform"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
11
11
  license-files = ["LICENSE", "NOTICE"]
12
12
  authors = [{ name = "fcloud" }]
13
- requires-python = ">=3.10"
13
+ requires-python = ">=3.9"
14
14
  keywords = ["gpu", "cloud", "compute", "ml", "training", "cli"]
15
15
  classifiers = [
16
16
  "Development Status :: 3 - Alpha",
@@ -19,10 +19,12 @@ classifiers = [
19
19
  "Intended Audience :: Science/Research",
20
20
  "Operating System :: OS Independent",
21
21
  "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
22
23
  "Programming Language :: Python :: 3.10",
23
24
  "Programming Language :: Python :: 3.11",
24
25
  "Programming Language :: Python :: 3.12",
25
26
  "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
26
28
  "Topic :: Software Development :: Libraries :: Python Modules",
27
29
  "Topic :: System :: Distributed Computing",
28
30
  "Typing :: Typed",
@@ -61,13 +63,14 @@ testpaths = ["tests"]
61
63
 
62
64
  [tool.ruff]
63
65
  line-length = 100
64
- target-version = "py310"
66
+ target-version = "py39"
65
67
 
66
68
  [tool.ruff.lint]
67
- select = ["E", "F", "W", "I", "UP", "B"]
69
+ select = ["E", "F", "W", "I", "UP", "B", "FA"]
68
70
  ignore = ["E741"]
69
71
 
70
72
  [tool.mypy]
73
+ # mypy >= 1.19 cannot target 3.9; ruff (py39) and the CI 3.9 job cover the floor.
71
74
  python_version = "3.10"
72
75
  files = ["src/fcloud"]
73
76
  ignore_missing_imports = true
@@ -25,6 +25,9 @@ fcloud set_token <api_key> # one-time; saves to ~/.fcloud/token
25
25
  fcloud health # verify connectivity
26
26
  ```
27
27
 
28
+ (A human sets up with `fcloud login`, which signs in through the browser and
29
+ mints a key for the machine; agents use `set_token` with a key they were given.)
30
+
28
31
  Token resolution order:
29
32
 
30
33
  1. `api_key=` parameter to `Client()`
@@ -85,6 +88,8 @@ ignored.
85
88
  | ---------------------- | ----------------------------------------- |
86
89
  | `fcloud health` | Check API connectivity |
87
90
  | `fcloud set_token <key>` | Save API key |
91
+ | `fcloud credits` | Prepaid balance, auto-refill, card |
92
+ | `fcloud credits buy <usd>` | Buy credit (opens hosted checkout) |
88
93
 
89
94
 
90
95
  ### Execution
@@ -303,7 +308,8 @@ filesystem later; a **job** for batch/CI-style runs with declared outputs.
303
308
  | Command | Description |
304
309
  | ----------------------------- | ---------------------------------------------------- |
305
310
  | `fcloud create [--sku SKU]` | Create a session (cold — $0, no host, until first used) |
306
- | `fcloud sessions [--all]` | List sessions |
311
+ | `fcloud sessions [--all] [--limit N]` | List sessions with `$/HR` (billed rate), `SPENT` (compute so far) and `AGE` (time on its host) per row and a spending-now total (`--all --json` can be large; bound it with `--limit`) |
312
+ | `fcloud usage [--since 24h]` | What the account has been charged over a window (default 30 days), by resource and session |
307
313
  | `fcloud stop <session-id> [--wait]` | Stop a session now: halt GPU spend (files kept). `--wait` blocks until the workspace manifest is durable |
308
314
 
309
315
 
@@ -983,6 +989,16 @@ fcloud wait s-abc123 "$PID" --timeout 0
983
989
  > alive (`fcloud logs <sid>` lists it as `running`) rather than assuming `rc=75`
984
990
  > means your job is gone.
985
991
  >
992
+ > **Out of credit (`rc=4`).** A 402 with `code: insufficient_credit` means the
993
+ > prepaid balance is exhausted: sessions are closed and queued jobs canceled.
994
+ > **This is not retryable** — retrying returns the same refusal until credit
995
+ > lands. Distinguish it from `rc=3` (`payment_overdue`, a failed payment on a
996
+ > bill) and from `rc=75` (transient unreachability, which *is* retryable). The
997
+ > remedy is `fcloud credits buy 25` or `fcloud credits autorefill on`; the block
998
+ > clears about a minute after the credit posts. In the SDK it raises
999
+ > `fcloud.InsufficientCreditError`, a sibling of `PaymentOverdueError` — code
1000
+ > that catches only `PaymentOverdueError` will not catch it.
1001
+ >
986
1002
  > **Migration rides honor `--wait`.** When a foreground `exec` rides a
987
1003
  > checkpoint/restore, the ride is bounded by your `--wait` budget — it never
988
1004
  > blocks past it (`--wait 0` disables the bound; the ride then uses the full
@@ -1076,7 +1092,10 @@ fcloud exec --on s-abc123 /workspace/venv/bin/python train.py
1076
1092
  ```
1077
1093
 
1078
1094
  Better yet, put packages in the image spec (`pip_install([...])`) so every
1079
- resume rebuilds them from the content-addressed image cache.
1095
+ resume rebuilds them from the content-addressed image cache. A sweep whose
1096
+ canary pip-installs packages gets them baked into its image automatically
1097
+ before the fan-out (the pass line names them; `--no-bake` disables) — but
1098
+ the pins still belong in `fcloud.json` so the *next* sweep never pays them.
1080
1099
 
1081
1100
  ---
1082
1101
 
@@ -1215,6 +1234,8 @@ host (files restored from cloud storage) the next time you use it. You never run
1215
1234
  **Warmth**: `fcloud sessions` reports a session's state — `hot` (running, spending),
1216
1235
  `warm` (idle host held, spending), `stopping` (finishing teardown), `cold`
1217
1236
  (stopped, $0, resumes on use), or `preparing` (coming online).
1237
+ The `$/HR` column is the rate billed on the session's open usage interval while a
1238
+ host is held (0 when cold); `SPENT` is compute charged so far; the footer sums `$/HR`. `fcloud usage` reports what was actually charged.
1218
1239
 
1219
1240
  **Image**: Declarative container spec. Layers are cached by content hash. First run
1220
1241
  on a host triggers a build (30–120s); subsequent runs with the same image are instant.
@@ -22,6 +22,7 @@ import fcloud._legacy_env # noqa: F401 — must run before any env read below
22
22
  from fcloud.client import (
23
23
  Client,
24
24
  FcloudError,
25
+ InsufficientCreditError,
25
26
  NoApiKeyError,
26
27
  PaymentOverdueError,
27
28
  SessionNotFoundError,
@@ -37,6 +38,16 @@ from fcloud.errors import (
37
38
  )
38
39
  from fcloud.image import Image
39
40
  from fcloud.job import Job # noqa: F401
41
+ from fcloud.modal_compat import ( # Modal-compatible surface: `import fcloud as modal`
42
+ App,
43
+ RemoteError,
44
+ Secret,
45
+ Volume,
46
+ enter,
47
+ exit, # noqa: A004 — mirrors modal.exit
48
+ method,
49
+ parameter,
50
+ )
40
51
  from fcloud.session import Project, Session
41
52
  from fcloud.sweeps import MapSpecError, SweepHandle # noqa: F401
42
53
  from fcloud.types import (
@@ -61,6 +72,15 @@ __all__ = [
61
72
  "Image",
62
73
  "set_token",
63
74
  "get_token",
75
+ # Modal-compatible surface
76
+ "App",
77
+ "Secret",
78
+ "Volume",
79
+ "enter",
80
+ "exit",
81
+ "method",
82
+ "parameter",
83
+ "RemoteError",
64
84
  # Handles
65
85
  "Project",
66
86
  "Session",
@@ -76,6 +96,7 @@ __all__ = [
76
96
  "ClientTooOldError",
77
97
  "FcloudError",
78
98
  "FcloudRuntimeError",
99
+ "InsufficientCreditError",
79
100
  "MapSpecError",
80
101
  "NoApiKeyError",
81
102
  "PaymentOverdueError",
@@ -50,6 +50,7 @@ from fcloud.cli.context import ( # noqa: F401
50
50
  _resolve_project,
51
51
  _resolve_volume_mounts,
52
52
  )
53
+ from fcloud.cli.credits import cmd_credits # noqa: F401
53
54
  from fcloud.cli.exec_cmd import _parse_exec_args, cmd_exec # noqa: F401
54
55
  from fcloud.cli.files import ( # noqa: F401
55
56
  _download_file,
@@ -119,6 +120,7 @@ from fcloud.cli.sweep import ( # noqa: F401
119
120
  cmd_sweep,
120
121
  cmd_sweeps,
121
122
  )
123
+ from fcloud.cli.usage import cmd_usage # noqa: F401
122
124
  from fcloud.cli.volume import ( # noqa: F401
123
125
  _is_within,
124
126
  _volume_cat,