goaly-looper 0.1.0b1__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 (57) hide show
  1. goaly_looper-0.1.0b1/.gitignore +16 -0
  2. goaly_looper-0.1.0b1/PKG-INFO +197 -0
  3. goaly_looper-0.1.0b1/README.md +188 -0
  4. goaly_looper-0.1.0b1/looper_sdk/__init__.py +132 -0
  5. goaly_looper-0.1.0b1/looper_sdk/_version.py +3 -0
  6. goaly_looper-0.1.0b1/looper_sdk/checkpoints.py +64 -0
  7. goaly_looper-0.1.0b1/looper_sdk/errors.py +380 -0
  8. goaly_looper-0.1.0b1/looper_sdk/losses.py +159 -0
  9. goaly_looper-0.1.0b1/looper_sdk/operations.py +109 -0
  10. goaly_looper-0.1.0b1/looper_sdk/proto/README.md +20 -0
  11. goaly_looper-0.1.0b1/looper_sdk/proto/__init__.py +2 -0
  12. goaly_looper-0.1.0b1/looper_sdk/proto/v1/__init__.py +0 -0
  13. goaly_looper-0.1.0b1/looper_sdk/proto/v1/checkpoints.proto +45 -0
  14. goaly_looper-0.1.0b1/looper_sdk/proto/v1/checkpoints_pb2.py +56 -0
  15. goaly_looper-0.1.0b1/looper_sdk/proto/v1/checkpoints_pb2.pyi +108 -0
  16. goaly_looper-0.1.0b1/looper_sdk/proto/v1/errors.proto +70 -0
  17. goaly_looper-0.1.0b1/looper_sdk/proto/v1/errors_pb2.py +42 -0
  18. goaly_looper-0.1.0b1/looper_sdk/proto/v1/errors_pb2.pyi +101 -0
  19. goaly_looper-0.1.0b1/looper_sdk/proto/v1/losses.proto +38 -0
  20. goaly_looper-0.1.0b1/looper_sdk/proto/v1/losses_pb2.py +40 -0
  21. goaly_looper-0.1.0b1/looper_sdk/proto/v1/losses_pb2.pyi +30 -0
  22. goaly_looper-0.1.0b1/looper_sdk/proto/v1/operations.proto +160 -0
  23. goaly_looper-0.1.0b1/looper_sdk/proto/v1/operations_pb2.py +70 -0
  24. goaly_looper-0.1.0b1/looper_sdk/proto/v1/operations_pb2.pyi +195 -0
  25. goaly_looper-0.1.0b1/looper_sdk/proto/v1/runs.proto +112 -0
  26. goaly_looper-0.1.0b1/looper_sdk/proto/v1/runs_pb2.py +66 -0
  27. goaly_looper-0.1.0b1/looper_sdk/proto/v1/runs_pb2.pyi +146 -0
  28. goaly_looper-0.1.0b1/looper_sdk/proto/v1/sampling.proto +111 -0
  29. goaly_looper-0.1.0b1/looper_sdk/proto/v1/sampling_pb2.py +77 -0
  30. goaly_looper-0.1.0b1/looper_sdk/proto/v1/sampling_pb2.pyi +209 -0
  31. goaly_looper-0.1.0b1/looper_sdk/proto/v1/sessions.proto +54 -0
  32. goaly_looper-0.1.0b1/looper_sdk/proto/v1/sessions_pb2.py +51 -0
  33. goaly_looper-0.1.0b1/looper_sdk/proto/v1/sessions_pb2.pyi +70 -0
  34. goaly_looper-0.1.0b1/looper_sdk/proto/v1/values.proto +89 -0
  35. goaly_looper-0.1.0b1/looper_sdk/proto/v1/values_pb2.py +52 -0
  36. goaly_looper-0.1.0b1/looper_sdk/proto/v1/values_pb2.pyi +88 -0
  37. goaly_looper-0.1.0b1/looper_sdk/results.py +72 -0
  38. goaly_looper-0.1.0b1/looper_sdk/sampling.py +247 -0
  39. goaly_looper-0.1.0b1/looper_sdk/sampling_client.py +196 -0
  40. goaly_looper-0.1.0b1/looper_sdk/service.py +316 -0
  41. goaly_looper-0.1.0b1/looper_sdk/training.py +270 -0
  42. goaly_looper-0.1.0b1/looper_sdk/transport.py +259 -0
  43. goaly_looper-0.1.0b1/looper_sdk/unit_tests/__init__.py +0 -0
  44. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_credentials.py +105 -0
  45. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_lifecycle.py +260 -0
  46. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_losses.py +117 -0
  47. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_package.py +26 -0
  48. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_sampling.py +154 -0
  49. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_sampling_client.py +280 -0
  50. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_shutdown.py +525 -0
  51. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_transport.py +234 -0
  52. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_values.py +186 -0
  53. goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_wire.py +406 -0
  54. goaly_looper-0.1.0b1/looper_sdk/values.py +294 -0
  55. goaly_looper-0.1.0b1/looper_sdk/wire.py +479 -0
  56. goaly_looper-0.1.0b1/publish.sh +98 -0
  57. goaly_looper-0.1.0b1/pyproject.toml +30 -0
@@ -0,0 +1,16 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+
6
+ # Environments
7
+ .venv/
8
+
9
+ # Job directories kept in the checkout (goaly_rl/run.sh RECIPE --set dump_dir=runs/...)
10
+ runs/
11
+
12
+ # Claude Code session state (settings, worktrees)
13
+ .claude/
14
+
15
+ # Prepared benchmark bundles and images manifest
16
+ .cache/
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.5
2
+ Name: goaly-looper
3
+ Version: 0.1.0b1
4
+ Summary: Looper's SDK: the client and the typed values of Looper's API, looper.v1.
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: httpx<1,>=0.28.1
7
+ Requires-Dist: protobuf<7,>=6.33.5
8
+ Description-Content-Type: text/markdown
9
+
10
+ # goaly-looper
11
+
12
+ Looper's SDK: the client and the typed values of Looper's API, `looper.v1`.
13
+ Looper is Goaly's hosted post-training service: a user's own program holds the
14
+ data, the rewards and the loop, and calls a few primitives that Looper runs on
15
+ GPUs it manages ([the design](../docs/README.md), [the API and the
16
+ SDK](../docs/sdk.md)).
17
+
18
+ Its import package is `looper_sdk`. It imports nothing of ours, so it installs
19
+ and runs on its own. It is a project of its own, with this `pyproject.toml`,
20
+ to be published as `goaly-looper` and open-sourced
21
+ ([monorepo.md](../docs/monorepo.md) §4, §9).
22
+
23
+ Status: `looper.v1`'s messages for P1's calls; the values, losses, results
24
+ and errors, with the module that maps them onto the wire; and the clients
25
+ over httpx: `ServiceClient`, `TrainingClient` and `Operation` (sdk.md §3,
26
+ §6). Publishing, sampling and suspension join with their verbs
27
+ (server.md §14).
28
+
29
+ | Path | What lives there |
30
+ |---|---|
31
+ | `pyproject.toml` | The project: its version, read from `looper_sdk/_version.py`, and its dependency ranges |
32
+ | `looper_sdk/` | The import package |
33
+ | `looper_sdk/values.py`, `losses.py`, `results.py` | What a client sends and what an operation computed: frozen values that validate themselves |
34
+ | `looper_sdk/checkpoints.py` | `StateCheckpoint`: a named registry value with expiry, identity and source-version provenance, and its `CheckpointState`: `uploading`, `ready_to_use` or `failed` ([sdk.md §4.6](../docs/sdk.md#46-checkpoints-and-exports)) |
35
+ | `looper_sdk/errors.py` | A class for each of §4.9's codes; retry semantics come with each error, as data |
36
+ | `looper_sdk/wire.py` | The values, results and errors as `looper.v1`'s messages, and back |
37
+ | `looper_sdk/proto/` | `looper.v1`'s protos and their generated modules |
38
+ | `looper_sdk/service.py`, `training.py`, `operations.py` | The clients: a program's session, a run's writer, and an accepted operation |
39
+ | `looper_sdk/transport.py` | httpx, the retries every call takes, and the session's heartbeats and closure |
40
+ | `looper_sdk/unit_tests/` | The SDK's unit tests, which import only what the SDK declares |
41
+
42
+ ## Client lifetime
43
+
44
+ Use an async context manager when the program's work has a bounded scope:
45
+
46
+ ```python
47
+ from looper_sdk import ServiceClient
48
+
49
+ config = ServiceClient.Config(project="research")
50
+ async with await config.build() as service:
51
+ training = await service.create_training_client(training_config)
52
+ status = await training.status()
53
+ ```
54
+
55
+ The default API root is `https://api.looper.goaly.ai`, exported as
56
+ `looper_sdk.DEFAULT_BASE_URL`. Pass `base_url="http://127.0.0.1:8080"` for a
57
+ local service, or another root for an alternate deployment. Omit `/v1` from
58
+ the root: the SDK adds it to requests.
59
+
60
+ Exit closes the session and HTTP client and stops heartbeats, including when
61
+ the body raises or is cancelled. It invalidates local clients and records
62
+ termination of session-owned runs; the coordinator retires workers
63
+ asynchronously. Checkpoints and historical results retain their storage policy.
64
+
65
+ For an application-managed lifetime, keep the client and explicitly call
66
+ `await service.close()` in the application's shutdown path.
67
+
68
+ ## API key
69
+
70
+ Every request sends `Authorization: Bearer <key>`: the config's `api_key`,
71
+ or `$LOOPER_API_KEY` when that is None. With neither, the client sends no
72
+ credential, which only local mode accepts (looper/docs/sdk.md §3). The key
73
+ never appears in the config's repr.
74
+
75
+ ## Rules
76
+
77
+ - **Ranges, never pins.** Each dependency runs from the oldest version we
78
+ test to the next major. An exact pin in a published wheel would conflict
79
+ with the rest of a user's environment. protobuf's floor is the version of
80
+ the generator that wrote `looper_sdk/proto/` (monorepo.md §4).
81
+ - **It imports nothing of ours.** `looper_server`'s import-rules test fails
82
+ on any import of `goaly_rl`, `looper_server` or `looper_cookbook`, or
83
+ through `looper.`.
84
+ - **Its tests import only what it declares,** so they pass outside this
85
+ repository.
86
+
87
+ ## Alone
88
+
89
+ CI's `looper-isolated` job builds the SDK's sdist, and a wheel from it. It
90
+ installs the wheel in a fresh environment, with the declared dependencies at
91
+ the lowest versions their ranges allow. It then runs the unit tests copied out
92
+ of the checkout, so `import looper_sdk` finds only the wheel (monorepo.md §7).
93
+ From the repository's root:
94
+
95
+ ```bash
96
+ uv build looper/sdk --out-dir /tmp/looper-sdk
97
+ uv venv /tmp/looper-sdk/env --python 3.12
98
+ uv pip install --python /tmp/looper-sdk/env --resolution lowest-direct -r looper/sdk/pyproject.toml /tmp/looper-sdk/goaly_looper-*.whl pytest==9.1.1
99
+ cp -r looper/sdk/looper_sdk/unit_tests /tmp/looper-sdk/unit_tests
100
+ cd /tmp/looper-sdk && env/bin/python -m pytest -q unit_tests
101
+ ```
102
+
103
+ ## Publish a beta release to PyPI
104
+
105
+ [`publish.sh`](publish.sh) builds the SDK's sdist and a wheel from it, tests the
106
+ installed wheel in isolation at its dependency floors, then publishes both
107
+ artifacts to public PyPI as `goaly-looper`. It publishes only this standalone
108
+ SDK, never the server or the root project. PyPI distributions are publicly
109
+ accessible and discoverable; hosted API access still requires a Looper API key.
110
+
111
+ The version comes from `looper_sdk/_version.py`. Use beta versions such as
112
+ `0.1.0b1`, then `0.1.0b2`; never replace a release with different contents.
113
+
114
+ One-time setup: create a [PyPI account](https://pypi.org/account/register/), enable
115
+ 2FA, and create an [API token](https://pypi.org/manage/account/token/). The first
116
+ upload creates the project, so its token initially needs account-wide scope.
117
+ Once the project exists, replace it with a token scoped to `goaly-looper`.
118
+ The name must be available, or the account must already have publishing access.
119
+
120
+ With `uv` and Python 3 installed, run from the repository root:
121
+
122
+ ```bash
123
+ # Read the token without echoing it or storing its literal value in shell history.
124
+ read -r -s -p "PyPI API token: " UV_PUBLISH_TOKEN
125
+ printf '\n'
126
+ export UV_PUBLISH_TOKEN
127
+ bash looper/sdk/publish.sh
128
+ unset UV_PUBLISH_TOKEN
129
+ ```
130
+
131
+ The hidden-prompt example uses Bash; in zsh use
132
+ `read -r -s 'UV_PUBLISH_TOKEN?PyPI API token: '` instead of its `read` line.
133
+ Never put the token in a notebook or commit it. `uv` uses a temporary Python 3.12
134
+ virtual environment for the SDK tests. No GCS credentials or bucket settings
135
+ are needed. The script works from any directory when called by its path.
136
+
137
+ Uploads use PyPI's check URL so retrying an unchanged release can complete a
138
+ partial upload without replacing existing files. An upload failure exits
139
+ nonzero; completed uploads may remain on PyPI. After uploading, the script
140
+ checks PyPI's release metadata, downloads both public artifacts and verifies
141
+ their SHA-256 against the local build. Only then does it print `SUCCESS`, the
142
+ project URL and an exact-version Colab/Jupyter install command. Verification
143
+ failure exits nonzero and leaves the release in place for investigation.
144
+ Temporary local artifacts are removed on exit. These checks prove SDK unit
145
+ behavior and artifact availability, not compatibility with a deployed service.
146
+
147
+ Users install the beta in Colab or local Jupyter with:
148
+
149
+ ```python
150
+ %pip install "goaly-looper==0.1.0b1"
151
+ ```
152
+
153
+ An exact prerelease pin does not require `--pre`. Future automation can use
154
+ PyPI Trusted Publishing; the local release script needs no CI configuration.
155
+
156
+ ## Readiness and explicit closure
157
+
158
+ `create_training_client(config, key=..., timeout_s=...)` returns after the
159
+ training node is ready. The default wait has no overall deadline.
160
+ `CreationTimeout` carries the originating session ID, creation key and run ID
161
+ when known; accepted work remains intact. Service failures raise `CreationError`
162
+ with the original typed error in `.cause` and `__cause__`, preserving its
163
+ traceback and retry semantics.
164
+ An existing handle can `await training.wait_until_ready(timeout_s=...)`.
165
+
166
+ Explicit service closure, including context exit, rejects new calls with
167
+ `SessionClosed`. Pending calls have up to 60 seconds to finish while heartbeats
168
+ continue. Cleanup then stops heartbeats, cancels remaining calls and closes the
169
+ session and HTTP client. A call interrupted by shutdown raises `SessionClosed`;
170
+ caller cancellation remains `CancelledError`. The default close wait is 70
171
+ seconds; cancellation or a caller deadline leaves shared cleanup running while
172
+ the event loop remains alive. A later `close()` can wait again. Backend runs
173
+ end on session closure or expiry; workers retire asynchronously. Continuation
174
+ uses a new service and run, optionally from a saved checkpoint. Session identity
175
+ is immutable and expiry never renews it. Sampling clients, once added, must
176
+ borrow the same session lifetime.
177
+
178
+ `await training.save_state(name)` accepts a save; `await operation.result()`
179
+ answers once the step is on the worker group's local storage, with the
180
+ checkpoint `uploading`, while the run trains on. `await
181
+ service.wait_checkpoint(checkpoint.id)` waits until it is `ready_to_use`:
182
+ uploaded, named and loadable; it raises `CheckpointFailed` if the upload
183
+ never finished ([sdk.md §4.6](../docs/sdk.md#46-checkpoints-and-exports)).
184
+ Registry lookup, list and delete methods use `ServiceClient`.
185
+
186
+ ## Checkpoint model metadata
187
+
188
+ Every `StateCheckpoint` includes `model_spec: ModelSpec`, exported from
189
+ `looper_sdk`: the source run's `model` name and exact `Full()` or `Lora(...)`
190
+ configuration, including rank and target switches. It excludes workload sizing
191
+ and does not resolve a Hugging Face revision. Save results, registry lookups and
192
+ lists return this metadata; historical save results are completed by the server
193
+ from the immutable source run.
194
+
195
+ The updated SDK requires an updated server: missing or malformed model metadata
196
+ raises `InvalidArgument`. Older SDKs can ignore the additive binary protobuf
197
+ field. Clients using strict JSON schemas must allow the new response field.
@@ -0,0 +1,188 @@
1
+ # goaly-looper
2
+
3
+ Looper's SDK: the client and the typed values of Looper's API, `looper.v1`.
4
+ Looper is Goaly's hosted post-training service: a user's own program holds the
5
+ data, the rewards and the loop, and calls a few primitives that Looper runs on
6
+ GPUs it manages ([the design](../docs/README.md), [the API and the
7
+ SDK](../docs/sdk.md)).
8
+
9
+ Its import package is `looper_sdk`. It imports nothing of ours, so it installs
10
+ and runs on its own. It is a project of its own, with this `pyproject.toml`,
11
+ to be published as `goaly-looper` and open-sourced
12
+ ([monorepo.md](../docs/monorepo.md) §4, §9).
13
+
14
+ Status: `looper.v1`'s messages for P1's calls; the values, losses, results
15
+ and errors, with the module that maps them onto the wire; and the clients
16
+ over httpx: `ServiceClient`, `TrainingClient` and `Operation` (sdk.md §3,
17
+ §6). Publishing, sampling and suspension join with their verbs
18
+ (server.md §14).
19
+
20
+ | Path | What lives there |
21
+ |---|---|
22
+ | `pyproject.toml` | The project: its version, read from `looper_sdk/_version.py`, and its dependency ranges |
23
+ | `looper_sdk/` | The import package |
24
+ | `looper_sdk/values.py`, `losses.py`, `results.py` | What a client sends and what an operation computed: frozen values that validate themselves |
25
+ | `looper_sdk/checkpoints.py` | `StateCheckpoint`: a named registry value with expiry, identity and source-version provenance, and its `CheckpointState`: `uploading`, `ready_to_use` or `failed` ([sdk.md §4.6](../docs/sdk.md#46-checkpoints-and-exports)) |
26
+ | `looper_sdk/errors.py` | A class for each of §4.9's codes; retry semantics come with each error, as data |
27
+ | `looper_sdk/wire.py` | The values, results and errors as `looper.v1`'s messages, and back |
28
+ | `looper_sdk/proto/` | `looper.v1`'s protos and their generated modules |
29
+ | `looper_sdk/service.py`, `training.py`, `operations.py` | The clients: a program's session, a run's writer, and an accepted operation |
30
+ | `looper_sdk/transport.py` | httpx, the retries every call takes, and the session's heartbeats and closure |
31
+ | `looper_sdk/unit_tests/` | The SDK's unit tests, which import only what the SDK declares |
32
+
33
+ ## Client lifetime
34
+
35
+ Use an async context manager when the program's work has a bounded scope:
36
+
37
+ ```python
38
+ from looper_sdk import ServiceClient
39
+
40
+ config = ServiceClient.Config(project="research")
41
+ async with await config.build() as service:
42
+ training = await service.create_training_client(training_config)
43
+ status = await training.status()
44
+ ```
45
+
46
+ The default API root is `https://api.looper.goaly.ai`, exported as
47
+ `looper_sdk.DEFAULT_BASE_URL`. Pass `base_url="http://127.0.0.1:8080"` for a
48
+ local service, or another root for an alternate deployment. Omit `/v1` from
49
+ the root: the SDK adds it to requests.
50
+
51
+ Exit closes the session and HTTP client and stops heartbeats, including when
52
+ the body raises or is cancelled. It invalidates local clients and records
53
+ termination of session-owned runs; the coordinator retires workers
54
+ asynchronously. Checkpoints and historical results retain their storage policy.
55
+
56
+ For an application-managed lifetime, keep the client and explicitly call
57
+ `await service.close()` in the application's shutdown path.
58
+
59
+ ## API key
60
+
61
+ Every request sends `Authorization: Bearer <key>`: the config's `api_key`,
62
+ or `$LOOPER_API_KEY` when that is None. With neither, the client sends no
63
+ credential, which only local mode accepts (looper/docs/sdk.md §3). The key
64
+ never appears in the config's repr.
65
+
66
+ ## Rules
67
+
68
+ - **Ranges, never pins.** Each dependency runs from the oldest version we
69
+ test to the next major. An exact pin in a published wheel would conflict
70
+ with the rest of a user's environment. protobuf's floor is the version of
71
+ the generator that wrote `looper_sdk/proto/` (monorepo.md §4).
72
+ - **It imports nothing of ours.** `looper_server`'s import-rules test fails
73
+ on any import of `goaly_rl`, `looper_server` or `looper_cookbook`, or
74
+ through `looper.`.
75
+ - **Its tests import only what it declares,** so they pass outside this
76
+ repository.
77
+
78
+ ## Alone
79
+
80
+ CI's `looper-isolated` job builds the SDK's sdist, and a wheel from it. It
81
+ installs the wheel in a fresh environment, with the declared dependencies at
82
+ the lowest versions their ranges allow. It then runs the unit tests copied out
83
+ of the checkout, so `import looper_sdk` finds only the wheel (monorepo.md §7).
84
+ From the repository's root:
85
+
86
+ ```bash
87
+ uv build looper/sdk --out-dir /tmp/looper-sdk
88
+ uv venv /tmp/looper-sdk/env --python 3.12
89
+ uv pip install --python /tmp/looper-sdk/env --resolution lowest-direct -r looper/sdk/pyproject.toml /tmp/looper-sdk/goaly_looper-*.whl pytest==9.1.1
90
+ cp -r looper/sdk/looper_sdk/unit_tests /tmp/looper-sdk/unit_tests
91
+ cd /tmp/looper-sdk && env/bin/python -m pytest -q unit_tests
92
+ ```
93
+
94
+ ## Publish a beta release to PyPI
95
+
96
+ [`publish.sh`](publish.sh) builds the SDK's sdist and a wheel from it, tests the
97
+ installed wheel in isolation at its dependency floors, then publishes both
98
+ artifacts to public PyPI as `goaly-looper`. It publishes only this standalone
99
+ SDK, never the server or the root project. PyPI distributions are publicly
100
+ accessible and discoverable; hosted API access still requires a Looper API key.
101
+
102
+ The version comes from `looper_sdk/_version.py`. Use beta versions such as
103
+ `0.1.0b1`, then `0.1.0b2`; never replace a release with different contents.
104
+
105
+ One-time setup: create a [PyPI account](https://pypi.org/account/register/), enable
106
+ 2FA, and create an [API token](https://pypi.org/manage/account/token/). The first
107
+ upload creates the project, so its token initially needs account-wide scope.
108
+ Once the project exists, replace it with a token scoped to `goaly-looper`.
109
+ The name must be available, or the account must already have publishing access.
110
+
111
+ With `uv` and Python 3 installed, run from the repository root:
112
+
113
+ ```bash
114
+ # Read the token without echoing it or storing its literal value in shell history.
115
+ read -r -s -p "PyPI API token: " UV_PUBLISH_TOKEN
116
+ printf '\n'
117
+ export UV_PUBLISH_TOKEN
118
+ bash looper/sdk/publish.sh
119
+ unset UV_PUBLISH_TOKEN
120
+ ```
121
+
122
+ The hidden-prompt example uses Bash; in zsh use
123
+ `read -r -s 'UV_PUBLISH_TOKEN?PyPI API token: '` instead of its `read` line.
124
+ Never put the token in a notebook or commit it. `uv` uses a temporary Python 3.12
125
+ virtual environment for the SDK tests. No GCS credentials or bucket settings
126
+ are needed. The script works from any directory when called by its path.
127
+
128
+ Uploads use PyPI's check URL so retrying an unchanged release can complete a
129
+ partial upload without replacing existing files. An upload failure exits
130
+ nonzero; completed uploads may remain on PyPI. After uploading, the script
131
+ checks PyPI's release metadata, downloads both public artifacts and verifies
132
+ their SHA-256 against the local build. Only then does it print `SUCCESS`, the
133
+ project URL and an exact-version Colab/Jupyter install command. Verification
134
+ failure exits nonzero and leaves the release in place for investigation.
135
+ Temporary local artifacts are removed on exit. These checks prove SDK unit
136
+ behavior and artifact availability, not compatibility with a deployed service.
137
+
138
+ Users install the beta in Colab or local Jupyter with:
139
+
140
+ ```python
141
+ %pip install "goaly-looper==0.1.0b1"
142
+ ```
143
+
144
+ An exact prerelease pin does not require `--pre`. Future automation can use
145
+ PyPI Trusted Publishing; the local release script needs no CI configuration.
146
+
147
+ ## Readiness and explicit closure
148
+
149
+ `create_training_client(config, key=..., timeout_s=...)` returns after the
150
+ training node is ready. The default wait has no overall deadline.
151
+ `CreationTimeout` carries the originating session ID, creation key and run ID
152
+ when known; accepted work remains intact. Service failures raise `CreationError`
153
+ with the original typed error in `.cause` and `__cause__`, preserving its
154
+ traceback and retry semantics.
155
+ An existing handle can `await training.wait_until_ready(timeout_s=...)`.
156
+
157
+ Explicit service closure, including context exit, rejects new calls with
158
+ `SessionClosed`. Pending calls have up to 60 seconds to finish while heartbeats
159
+ continue. Cleanup then stops heartbeats, cancels remaining calls and closes the
160
+ session and HTTP client. A call interrupted by shutdown raises `SessionClosed`;
161
+ caller cancellation remains `CancelledError`. The default close wait is 70
162
+ seconds; cancellation or a caller deadline leaves shared cleanup running while
163
+ the event loop remains alive. A later `close()` can wait again. Backend runs
164
+ end on session closure or expiry; workers retire asynchronously. Continuation
165
+ uses a new service and run, optionally from a saved checkpoint. Session identity
166
+ is immutable and expiry never renews it. Sampling clients, once added, must
167
+ borrow the same session lifetime.
168
+
169
+ `await training.save_state(name)` accepts a save; `await operation.result()`
170
+ answers once the step is on the worker group's local storage, with the
171
+ checkpoint `uploading`, while the run trains on. `await
172
+ service.wait_checkpoint(checkpoint.id)` waits until it is `ready_to_use`:
173
+ uploaded, named and loadable; it raises `CheckpointFailed` if the upload
174
+ never finished ([sdk.md §4.6](../docs/sdk.md#46-checkpoints-and-exports)).
175
+ Registry lookup, list and delete methods use `ServiceClient`.
176
+
177
+ ## Checkpoint model metadata
178
+
179
+ Every `StateCheckpoint` includes `model_spec: ModelSpec`, exported from
180
+ `looper_sdk`: the source run's `model` name and exact `Full()` or `Lora(...)`
181
+ configuration, including rank and target switches. It excludes workload sizing
182
+ and does not resolve a Hugging Face revision. Save results, registry lookups and
183
+ lists return this metadata; historical save results are completed by the server
184
+ from the immutable source run.
185
+
186
+ The updated SDK requires an updated server: missing or malformed model metadata
187
+ raises `InvalidArgument`. Older SDKs can ignore the additive binary protobuf
188
+ field. Clients using strict JSON schemas must allow the new response field.
@@ -0,0 +1,132 @@
1
+ """Looper's SDK: the client and the typed values of Looper's API, ``looper.v1``
2
+ (looper/docs/sdk.md).
3
+
4
+ It imports nothing of ours, so it installs and runs on its own
5
+ (looper/docs/README.md, "Who imports what"), with httpx and protobuf.
6
+ ``ServiceClient`` opens the program's session; each run it creates has
7
+ a ``TrainingClient``, and each operation it submits an
8
+ ``Operation`` (sdk.md §3, §6).
9
+ """
10
+
11
+ from looper_sdk._version import __version__
12
+ from looper_sdk.checkpoints import CheckpointState, StateCheckpoint
13
+ from looper_sdk.errors import (
14
+ Cancelled,
15
+ CapacityExhausted,
16
+ Category,
17
+ CheckpointFailed,
18
+ CheckpointNotReady,
19
+ CreationError,
20
+ CreationTimeout,
21
+ DependencyFailed,
22
+ EmptyStep,
23
+ ErrorCode,
24
+ GradientsPending,
25
+ IdempotencyConflict,
26
+ IncompleteAccumulation,
27
+ Internal,
28
+ InvalidArgument,
29
+ LooperError,
30
+ NotFound,
31
+ PayloadTooLarge,
32
+ PermissionDenied,
33
+ QueueTimeout,
34
+ QuotaExceeded,
35
+ RunNotReady,
36
+ RunRolledBack,
37
+ RunStateLost,
38
+ RunSuspended,
39
+ SamplerFailed,
40
+ SessionClosed,
41
+ SessionCloseTimeout,
42
+ SessionExpired,
43
+ SpendRefused,
44
+ Unauthenticated,
45
+ Unavailable,
46
+ VersionSuperseded,
47
+ WriterSuperseded,
48
+ )
49
+ from looper_sdk.losses import CrossEntropy, Dapo, Grpo, Loss
50
+ from looper_sdk.operations import Operation, OperationState
51
+ from looper_sdk.results import DatumResult, ForwardBackwardResult, StepResult
52
+ from looper_sdk.sampling import (
53
+ FinishReason,
54
+ SamplerConfig,
55
+ SampleRequest,
56
+ SampleResult,
57
+ SamplerState,
58
+ SamplerStatus,
59
+ SampleSequence,
60
+ SamplingParams,
61
+ )
62
+ from looper_sdk.service import DEFAULT_BASE_URL, ServiceClient
63
+ from looper_sdk.training import RunState, RunStatus, TrainingClient
64
+ from looper_sdk.values import Datum, Full, Lora, ModelSpec, TrainingConfig, Version
65
+
66
+ __all__ = [
67
+ "DEFAULT_BASE_URL",
68
+ "StateCheckpoint",
69
+ "CheckpointState",
70
+ "Cancelled",
71
+ "CapacityExhausted",
72
+ "Category",
73
+ "CheckpointFailed",
74
+ "CheckpointNotReady",
75
+ "CreationError",
76
+ "CreationTimeout",
77
+ "SessionClosed",
78
+ "SessionCloseTimeout",
79
+ "CrossEntropy",
80
+ "Dapo",
81
+ "Datum",
82
+ "DatumResult",
83
+ "DependencyFailed",
84
+ "EmptyStep",
85
+ "ErrorCode",
86
+ "ForwardBackwardResult",
87
+ "Grpo",
88
+ "Full",
89
+ "GradientsPending",
90
+ "IdempotencyConflict",
91
+ "IncompleteAccumulation",
92
+ "Internal",
93
+ "InvalidArgument",
94
+ "Lora",
95
+ "ModelSpec",
96
+ "Loss",
97
+ "LooperError",
98
+ "NotFound",
99
+ "Operation",
100
+ "OperationState",
101
+ "PayloadTooLarge",
102
+ "PermissionDenied",
103
+ "QueueTimeout",
104
+ "QuotaExceeded",
105
+ "RunNotReady",
106
+ "RunRolledBack",
107
+ "RunState",
108
+ "RunStateLost",
109
+ "RunStatus",
110
+ "RunSuspended",
111
+ "FinishReason",
112
+ "SampleRequest",
113
+ "SampleResult",
114
+ "SampleSequence",
115
+ "SamplerConfig",
116
+ "SamplerFailed",
117
+ "SamplerState",
118
+ "SamplerStatus",
119
+ "SamplingParams",
120
+ "ServiceClient",
121
+ "SessionExpired",
122
+ "SpendRefused",
123
+ "StepResult",
124
+ "TrainingClient",
125
+ "TrainingConfig",
126
+ "Unauthenticated",
127
+ "Unavailable",
128
+ "Version",
129
+ "VersionSuperseded",
130
+ "WriterSuperseded",
131
+ "__version__",
132
+ ]
@@ -0,0 +1,3 @@
1
+ """The SDK's version, which its wheel's metadata reads (looper/docs/monorepo.md §4)."""
2
+
3
+ __version__ = "0.1.0b1"
@@ -0,0 +1,64 @@
1
+ """Named checkpoint values returned by the registry (sdk.md §4.6)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import enum
7
+ from datetime import datetime
8
+ from typing import ClassVar
9
+
10
+ from looper_sdk.errors import InvalidArgument
11
+ from looper_sdk.values import ModelSpec, Version
12
+
13
+
14
+ class CheckpointState(enum.StrEnum):
15
+ """Where a checkpoint is (sdk.md §4.6). A save succeeds once its step is
16
+ on the worker group's local SSD, and its checkpoint is ``UPLOADING``; only
17
+ a ``READY_TO_USE`` one is on the bucket, named and loadable."""
18
+
19
+ UPLOADING = "uploading"
20
+ READY_TO_USE = "ready_to_use"
21
+ FAILED = "failed"
22
+
23
+
24
+ @dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
25
+ class StateCheckpoint:
26
+ """A checkpoint observed at save or registry lookup time.
27
+
28
+ Attributes:
29
+ name: The name it binds once ``READY_TO_USE``; until then, the name its
30
+ save asked for, which still resolves to its previous target.
31
+ expires_at: Set once ``READY_TO_USE``, when its TTL starts.
32
+ model_spec: Source run model name and exact trainable parameters.
33
+ principal: Creating session's user or service identity.
34
+ version: Exact source run, epoch and committed optimizer step.
35
+ size_bytes: Physical directory size, known once ``READY_TO_USE``, and 0
36
+ before; aliases do not duplicate bytes.
37
+ created_at: Time of registry publication once ``READY_TO_USE``; before,
38
+ the time its save succeeded. With timezone.
39
+ state: Where it is; see ``CheckpointState``.
40
+ failure: Why a ``FAILED`` checkpoint failed, for people; None otherwise.
41
+ """
42
+
43
+ name: str
44
+ expires_at: datetime | None
45
+ id: str
46
+ operation_id: str
47
+ project: str
48
+ principal: str
49
+ source_run_id: str
50
+ version: Version
51
+ model_spec: ModelSpec
52
+ size_bytes: int
53
+ created_at: datetime
54
+ state: CheckpointState
55
+ failure: str | None = None
56
+ kind: ClassVar[str] = "state"
57
+
58
+ def __post_init__(self) -> None:
59
+ if not isinstance(self.model_spec, ModelSpec):
60
+ raise InvalidArgument("Checkpoint.model_spec must be a ModelSpec")
61
+ if not isinstance(self.state, CheckpointState):
62
+ raise InvalidArgument("Checkpoint.state must be a CheckpointState")
63
+ if (self.failure is not None) != (self.state is CheckpointState.FAILED):
64
+ raise InvalidArgument("Checkpoint.failure is set exactly when its state is FAILED")