etude-sdk 0.1.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 (97) hide show
  1. etude_sdk-0.1.0/.github/dependabot.yml +21 -0
  2. etude_sdk-0.1.0/.github/workflows/ci.yml +95 -0
  3. etude_sdk-0.1.0/.github/workflows/release.yml +39 -0
  4. etude_sdk-0.1.0/.gitignore +10 -0
  5. etude_sdk-0.1.0/.python-version +1 -0
  6. etude_sdk-0.1.0/AGENTS.md +172 -0
  7. etude_sdk-0.1.0/LICENSE +21 -0
  8. etude_sdk-0.1.0/PKG-INFO +83 -0
  9. etude_sdk-0.1.0/README.md +68 -0
  10. etude_sdk-0.1.0/docs/launcher.md +807 -0
  11. etude_sdk-0.1.0/docs/tracking.md +165 -0
  12. etude_sdk-0.1.0/pyproject.toml +62 -0
  13. etude_sdk-0.1.0/scratch/.gitkeep +0 -0
  14. etude_sdk-0.1.0/scripts/test_network.py +79 -0
  15. etude_sdk-0.1.0/src/etude/__init__.py +0 -0
  16. etude_sdk-0.1.0/src/etude/__main__.py +5 -0
  17. etude_sdk-0.1.0/src/etude/cli.py +85 -0
  18. etude_sdk-0.1.0/src/etude/launch/__init__.py +0 -0
  19. etude_sdk-0.1.0/src/etude/launch/cli.py +591 -0
  20. etude_sdk-0.1.0/src/etude/launch/client.py +178 -0
  21. etude_sdk-0.1.0/src/etude/launch/commands.py +25 -0
  22. etude_sdk-0.1.0/src/etude/launch/controller.py +488 -0
  23. etude_sdk-0.1.0/src/etude/launch/database.py +777 -0
  24. etude_sdk-0.1.0/src/etude/launch/environment.py +50 -0
  25. etude_sdk-0.1.0/src/etude/launch/errors.py +2 -0
  26. etude_sdk-0.1.0/src/etude/launch/executor.py +168 -0
  27. etude_sdk-0.1.0/src/etude/launch/job_file.py +107 -0
  28. etude_sdk-0.1.0/src/etude/launch/listing.py +92 -0
  29. etude_sdk-0.1.0/src/etude/launch/machines.py +83 -0
  30. etude_sdk-0.1.0/src/etude/launch/measurement.py +49 -0
  31. etude_sdk-0.1.0/src/etude/launch/providers/__init__.py +0 -0
  32. etude_sdk-0.1.0/src/etude/launch/providers/cleanup.py +10 -0
  33. etude_sdk-0.1.0/src/etude/launch/providers/package.py +49 -0
  34. etude_sdk-0.1.0/src/etude/launch/providers/runpod/__init__.py +0 -0
  35. etude_sdk-0.1.0/src/etude/launch/providers/runpod/api.py +189 -0
  36. etude_sdk-0.1.0/src/etude/launch/providers/runpod/http.py +78 -0
  37. etude_sdk-0.1.0/src/etude/launch/providers/runpod/machines.py +160 -0
  38. etude_sdk-0.1.0/src/etude/launch/providers/runpod/pod.py +179 -0
  39. etude_sdk-0.1.0/src/etude/launch/records.py +378 -0
  40. etude_sdk-0.1.0/src/etude/launch/scheduler.py +1455 -0
  41. etude_sdk-0.1.0/src/etude/launch/schemas.py +143 -0
  42. etude_sdk-0.1.0/src/etude/launch/source.py +321 -0
  43. etude_sdk-0.1.0/src/etude/launch/ssh.py +283 -0
  44. etude_sdk-0.1.0/src/etude/launch/startup.py +47 -0
  45. etude_sdk-0.1.0/src/etude/launch/storage/__init__.py +0 -0
  46. etude_sdk-0.1.0/src/etude/launch/storage/bucket.py +234 -0
  47. etude_sdk-0.1.0/src/etude/launch/storage/disk.py +186 -0
  48. etude_sdk-0.1.0/src/etude/launch/storage/files.py +32 -0
  49. etude_sdk-0.1.0/src/etude/launch/storage/s3.py +433 -0
  50. etude_sdk-0.1.0/src/etude/launch/worker/__init__.py +0 -0
  51. etude_sdk-0.1.0/src/etude/launch/worker/attempts.py +248 -0
  52. etude_sdk-0.1.0/src/etude/launch/worker/courier.py +319 -0
  53. etude_sdk-0.1.0/src/etude/launch/worker/main.py +26 -0
  54. etude_sdk-0.1.0/src/etude/launch/worker/remote.py +112 -0
  55. etude_sdk-0.1.0/src/etude/py.typed +0 -0
  56. etude_sdk-0.1.0/src/etude/track/__init__.py +0 -0
  57. etude_sdk-0.1.0/src/etude/track/block.py +462 -0
  58. etude_sdk-0.1.0/src/etude/track/block_file.py +59 -0
  59. etude_sdk-0.1.0/src/etude/track/cli.py +135 -0
  60. etude_sdk-0.1.0/src/etude/track/errors.py +10 -0
  61. etude_sdk-0.1.0/src/etude/track/manifest.py +185 -0
  62. etude_sdk-0.1.0/src/etude/track/read.py +658 -0
  63. etude_sdk-0.1.0/src/etude/track/records.py +297 -0
  64. etude_sdk-0.1.0/src/etude/track/report.html +363 -0
  65. etude_sdk-0.1.0/src/etude/track/report.py +224 -0
  66. etude_sdk-0.1.0/src/etude/track/run.py +1101 -0
  67. etude_sdk-0.1.0/src/etude/track/serve.py +73 -0
  68. etude_sdk-0.1.0/src/etude/track/storage.py +191 -0
  69. etude_sdk-0.1.0/src/etude/track/vendor/UPLOT_LICENSE +21 -0
  70. etude_sdk-0.1.0/src/etude/track/vendor/uPlot.iife.min.js +2 -0
  71. etude_sdk-0.1.0/src/etude/track/vendor/uPlot.min.css +1 -0
  72. etude_sdk-0.1.0/tests/__init__.py +0 -0
  73. etude_sdk-0.1.0/tests/launch/__init__.py +0 -0
  74. etude_sdk-0.1.0/tests/launch/counting.py +81 -0
  75. etude_sdk-0.1.0/tests/launch/fake.py +281 -0
  76. etude_sdk-0.1.0/tests/launch/test_accounting.py +128 -0
  77. etude_sdk-0.1.0/tests/launch/test_cli.py +403 -0
  78. etude_sdk-0.1.0/tests/launch/test_controller.py +363 -0
  79. etude_sdk-0.1.0/tests/launch/test_layout.py +80 -0
  80. etude_sdk-0.1.0/tests/launch/test_measurement.py +284 -0
  81. etude_sdk-0.1.0/tests/launch/test_package.py +202 -0
  82. etude_sdk-0.1.0/tests/launch/test_queue.py +371 -0
  83. etude_sdk-0.1.0/tests/launch/test_rentals.py +128 -0
  84. etude_sdk-0.1.0/tests/launch/test_runpod_api.py +250 -0
  85. etude_sdk-0.1.0/tests/launch/test_runpod_machines.py +685 -0
  86. etude_sdk-0.1.0/tests/launch/test_scheduler.py +633 -0
  87. etude_sdk-0.1.0/tests/launch/test_source.py +226 -0
  88. etude_sdk-0.1.0/tests/launch/test_ssh.py +82 -0
  89. etude_sdk-0.1.0/tests/launch/test_storage_s3.py +206 -0
  90. etude_sdk-0.1.0/tests/launch/test_worker_attempts.py +207 -0
  91. etude_sdk-0.1.0/tests/track/__init__.py +0 -0
  92. etude_sdk-0.1.0/tests/track/storage.py +49 -0
  93. etude_sdk-0.1.0/tests/track/test_cli.py +79 -0
  94. etude_sdk-0.1.0/tests/track/test_measure.py +195 -0
  95. etude_sdk-0.1.0/tests/track/test_store.py +723 -0
  96. etude_sdk-0.1.0/tests/track/test_track.py +469 -0
  97. etude_sdk-0.1.0/uv.lock +1213 -0
@@ -0,0 +1,21 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: uv
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ groups:
8
+ python:
9
+ update-types:
10
+ - minor
11
+ - patch
12
+
13
+ - package-ecosystem: github-actions
14
+ directory: /
15
+ schedule:
16
+ interval: weekly
17
+ groups:
18
+ actions:
19
+ update-types:
20
+ - minor
21
+ - patch
@@ -0,0 +1,95 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ merge_group:
8
+
9
+ # A new PR head supersedes its old proof. Main and merge-group runs keep their
10
+ # own identity so independent integration proofs do not cancel each other.
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
13
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
14
+
15
+ jobs:
16
+ # Scratch belongs to work in progress. Delivery clears it before publishing a
17
+ # landing candidate; queue and main runs never accept that exemption.
18
+ scratch-clear:
19
+ runs-on: ubuntu-latest
20
+ outputs:
21
+ candidate: ${{ steps.scratch.outputs.candidate }}
22
+ steps:
23
+ - uses: actions/checkout@v5
24
+ - name: Select landing candidates and enforce scratch cleanup
25
+ id: scratch
26
+ env:
27
+ EVENT_NAME: ${{ github.event_name }}
28
+ run: |
29
+ if [ ! -d scratch ]; then
30
+ echo "scratch/ is missing"
31
+ exit 1
32
+ fi
33
+ EXTRA=$(find scratch -mindepth 1 ! -path 'scratch/.gitkeep' -print)
34
+ if [ -n "$EXTRA" ]; then
35
+ if [ "$EVENT_NAME" = "pull_request" ]; then
36
+ echo "Checkpoint: checks are deferred until scratch/ is clear." | tee -a "$GITHUB_STEP_SUMMARY"
37
+ echo "candidate=false" >> "$GITHUB_OUTPUT"
38
+ exit 0
39
+ fi
40
+ echo "scratch/ must be clear before landing"
41
+ echo "$EXTRA"
42
+ exit 1
43
+ fi
44
+ echo "candidate=true" >> "$GITHUB_OUTPUT"
45
+
46
+ client-lint:
47
+ needs: [scratch-clear]
48
+ if: ${{ !cancelled() && needs.scratch-clear.outputs.candidate == 'true' }}
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v5
52
+ - uses: astral-sh/setup-uv@v6
53
+ - run: uv sync --locked
54
+ - run: uv run --no-sync ruff check .
55
+ - run: uv run --no-sync ruff format --check .
56
+ - run: uv run --no-sync mypy
57
+
58
+ client-test:
59
+ needs: [scratch-clear]
60
+ if: ${{ !cancelled() && needs.scratch-clear.outputs.candidate == 'true' }}
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - uses: actions/checkout@v5
64
+ - uses: astral-sh/setup-uv@v6
65
+ - run: uv sync --locked
66
+ - name: Run the client tests with the outside network denied
67
+ run: uv run --no-sync python scripts/test_network.py uv run --no-sync pytest
68
+ timeout-minutes: 15
69
+
70
+ tests-result:
71
+ # A skipped check is not proof. Only PR checkpoints may skip the aggregate;
72
+ # a failed classifier must still produce a failing required check.
73
+ if: ${{ always() && (github.event_name != 'pull_request' || needs.scratch-clear.result != 'success' || needs.scratch-clear.outputs.candidate != 'false') }}
74
+ runs-on: ubuntu-latest
75
+ needs: [scratch-clear, client-lint, client-test]
76
+ steps:
77
+ - name: Aggregate check results
78
+ env:
79
+ SCRATCH_CLEAR: ${{ needs.scratch-clear.result }}
80
+ CLIENT_LINT: ${{ needs.client-lint.result }}
81
+ CLIENT_TEST: ${{ needs.client-test.result }}
82
+ run: |
83
+ failures=0
84
+ check_result() {
85
+ if [ "$2" = success ]; then
86
+ echo "PASS $1"
87
+ else
88
+ echo "FAIL $1 ($2)"
89
+ failures=1
90
+ fi
91
+ }
92
+ check_result scratch-clear "$SCRATCH_CLEAR"
93
+ check_result client-lint "$CLIENT_LINT"
94
+ check_result client-test "$CLIENT_TEST"
95
+ exit "$failures"
@@ -0,0 +1,39 @@
1
+ name: Release
2
+
3
+ # Pushing a tag vX.Y.Z publishes that version of etude-sdk to PyPI. PyPI trusts
4
+ # this workflow by name (trusted publishing), so no token is stored anywhere.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v5
14
+ - uses: astral-sh/setup-uv@v6
15
+ - name: Refuse a tag that does not match the package version
16
+ run: |
17
+ version=$(uv version --short)
18
+ if [ "v$version" != "$GITHUB_REF_NAME" ]; then
19
+ echo "tag $GITHUB_REF_NAME does not match version $version in pyproject.toml"
20
+ exit 1
21
+ fi
22
+ - run: uv build
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment: pypi
32
+ permissions:
33
+ id-token: write
34
+ steps:
35
+ - uses: actions/download-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ *.egg-info/
7
+ dist/
8
+ .etude/
9
+ .lf/prompts/
10
+ .lf/tmp/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,172 @@
1
+ # Etude
2
+
3
+ Conventions for people and coding agents working on this repository.
4
+
5
+ ## Units
6
+
7
+ Use steps for scientific cadence and preserve absolute continuation coordinates.
8
+ Use hours and dollars for machine rental windows, credentials, watchdogs and budgets.
9
+ Distinguish training steps, learner transitions and optimizer updates/exposures.
10
+
11
+ ## Design
12
+
13
+ Design around the domain's data structures and public APIs, with one code
14
+ concept per real concept. Keep core types and entry points easy to find. Start
15
+ with the smallest types and APIs that are right; every line must earn its place.
16
+
17
+ A redesign deletes what it replaces, in the same change. Keep one
18
+ implementation and one owner for each fact: no `_old`, `_new`, `v2_` or backup
19
+ variants, and no compatibility shims unless a released database or a published
20
+ API with outside users requires one. Git holds old versions.
21
+
22
+ When code fails because it assumed a fixed context, prefer adapting to the
23
+ context over adding a guard, allowlist or precondition that refuses.
24
+
25
+ Name things for what they are (`Checkpoint`, not `CheckpointHelper`). Action
26
+ functions start with a verb. Private helpers start with an underscore.
27
+
28
+ ## Python
29
+
30
+ Use Python 3.12 and uv for every Python command (`uv run pytest`, never bare
31
+ `python` or `pip`), in docs and scripts as well.
32
+
33
+ Write fully typed Python: annotate parameters and returns on every function
34
+ and method, including private helpers and tests, with `-> None` where it
35
+ applies. Use 3.12 forms (`list[T]`, `T | None`). Represent structured domain
36
+ data with named types; do not pass untyped dictionaries or positional tuples
37
+ across application boundaries. Prefer precise unions and literals to `Any`. At
38
+ untyped library or JSON boundaries, validate once and expose a typed
39
+ interface; keep a necessary cast or suppression local, with its reason. Do not
40
+ weaken a type to silence the checker.
41
+
42
+ A nullable annotation means `None` is a real value; a default decides whether
43
+ an argument may be omitted. Return `None` for expected absence only where the
44
+ API defines it. Raise for violated invariants and impossible states, and never
45
+ disguise a failure as an empty result.
46
+
47
+ Put imports at the top and import explicitly from the defining module. There
48
+ are two exceptions, each with a comment saying why. An optional dependency is
49
+ imported where it is used. A command line or other top-level entry point
50
+ imports what one command needs inside that command, so starting the program
51
+ loads only what that command runs; keep these entry points as fast to start
52
+ as possible, and keep the rule everywhere below them. Declare dependencies in
53
+ `pyproject.toml`.
54
+
55
+ ## Comments and docs
56
+
57
+ Give a substantial module a short opening note: its purpose, principal types
58
+ and entry points, and any non-obvious ownership or data flow. Explain intent,
59
+ invariants, units and algorithmic choices beside the code that depends on
60
+ them. Keep docstrings short; include non-obvious contracts, side effects and
61
+ failure behaviour. No `Args:`/`Returns:` sections that repeat a signature, no
62
+ comments that restate code, and no comments that narrate a change.
63
+
64
+ Wrong documentation is worse than none: update it with the behaviour or delete
65
+ it. In user-facing docs, lead with an example and keep prose terse around it;
66
+ tables are for lookup, not teaching.
67
+
68
+ ## Tests
69
+
70
+ Test behaviour a user would care about, and assert on results. Mock side
71
+ effects (network, subprocesses, paid services) but never assert on how a mock
72
+ was called. Do not reshape production code for a test: no interface, factory
73
+ or extra parameter that exists only for test doubles. Delete a flaky test
74
+ instead of adding retries.
75
+
76
+ ## Development
77
+
78
+ Before calling work done, review it as a sceptical maintainer would: can it be
79
+ explained in one screen, does the API map to the real thing or to an accident
80
+ of implementation, what breaks at 2 a.m. and will the logs say why, is each
81
+ dependency and knob earning its keep, and would deleting code make it more
82
+ true. Fix what that finds or record it.
83
+
84
+ Make small coherent pull requests with useful descriptions and focused checks.
85
+ Linux account/permission tests use a disposable container’s native filesystem,
86
+ not macOS bind mounts (which can present different permission behavior). Keep
87
+ fixture inputs below its private root home, then copy journals out after execution.
88
+ Worker deadline tests resolve runner dependencies from the fixture image's local
89
+ wheelhouse with networking disabled; downloading packages must not consume the
90
+ short execution window used to test partial-output retention.
91
+
92
+ Commit messages say what changed and why in a sentence to a paragraph, not a
93
+ list of edits. Never add Co-Authored-By or other AI attribution footers.
94
+
95
+ Keep secrets out of code, output and artifacts. Prefer running a command with
96
+ the secret injected to reading the value. A raw value must never reach a
97
+ terminal, log or chat: consume it inline or redirect it. If one leaks, say so
98
+ and flag it for rotation without quoting it again.
99
+
100
+ Launcher records use frozen Pydantic models with named constructor fields and
101
+ model-owned JSON serialization. Keep `providers/runpod/http.py`,
102
+ `providers/runpod/pod.py`, `providers/package.py` and `environment.py`
103
+ standard-library-only: the RunPod guardian must start before
104
+ client dependency installation. The client and each job use separate environments.
105
+
106
+ Check before pushing: `uv run pytest`, `uv run mypy`, `uv run ruff check .` and
107
+ `uv run ruff format --check .`. CI runs the tests with the outside network
108
+ denied (`scripts/test_network.py`).
109
+
110
+ ## Code style
111
+
112
+ Readability matters more now that code is cheap to write: optimise for the
113
+ person reading it.
114
+
115
+ **Names.** Prefer clarity over cleverness. Use one word per concept and use it
116
+ everywhere. Every word in a path or name should be different from its
117
+ neighbours and tell the reader something: `etude.launch.scheduler`, not
118
+ `etude.launch.launcher`. Avoid words that say nothing, such as engine, service,
119
+ contract, api, util or manager. Before choosing a name, an interface or a
120
+ layout, look at what established tools call the same thing and say what you
121
+ found.
122
+
123
+ **Imports.** Write every import as a full path from the module that defines the
124
+ name. Relative imports are a lint error. Put every import at the top of its
125
+ module: an import inside a function is a lint error too. If that makes two
126
+ modules import each other, move the shared part or pass it in; do not hide the
127
+ cycle inside a function. Keep every `__init__.py` empty, so importing one
128
+ module never loads the rest.
129
+
130
+ **Layout.** Add a folder only when it has a rule a test can enforce, such as
131
+ "nothing in here imports the scheduler". A theme is not a reason for a folder.
132
+
133
+ **Records.** Anything stored or sent between machines is a frozen Pydantic
134
+ model. Do not keep a second kind of record beside them.
135
+
136
+ **Command line.** Use Typer. Every input is a `--flag`; do not add positional
137
+ conventions a person has to remember, such as a bare `--` or `name=value`.
138
+ Keep output plain, because agents and logs read it.
139
+
140
+ **Python is the interface.** Work is described in Python, not in a config
141
+ format. A command-line command is a thin way to call the same functions.
142
+
143
+ **Dependencies.** Manage them with uv. The launcher and tracking use one set of
144
+ versions for every dependency, from one lock file, whether they ship as one
145
+ package or several. Each must work without the other being used. Code Etude runs on someone else's
146
+ machine gets its own environment, separate from the job's. The one exception
147
+ is anything that must start before an install can finish, such as the process
148
+ that deletes a rented machine at its deadline: that stays standard library
149
+ only.
150
+
151
+ **Consequential actions are explicit.** Spending money, deleting something or
152
+ publishing something is never a side effect of a command that reads as doing
153
+ something else.
154
+
155
+ **What a command may wait on.** Every command is one of three kinds and says
156
+ which. A read answers from the database, at once, and says how fresh its answer
157
+ is. An ask records what is wanted and returns with an id. Only a wait may
158
+ depend on another machine, and it has a time limit. A command needs only what
159
+ its kind implies: a read must work with no provider key and no network.
160
+
161
+ **Measure by counting.** Machine load makes timings unreliable, so tests count
162
+ what matters to a user: calls to a provider, a bucket, SSH and git, database
163
+ queries, and modules imported at start-up. Pin those numbers in tests. A change
164
+ to a path that reaches another machine comes with a counting test.
165
+
166
+ Launcher vocabulary: Machine owns acquisition and provider facts; Rental owns
167
+ only a paid window, keyed by its machine; RunAttempt is the allocation on any
168
+ machine. Session is internal: one start of Etude's process on a machine, and its
169
+ journal folder. The process itself is the worker (`etude worker`). Use rent and
170
+ terminate for making and deleting paid machines. A record refers to another by
171
+ its name or id as a plain `str`. While the launcher is a prototype its database
172
+ has no migrations: change the schema version and refuse older files.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Loopflow Studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: etude-sdk
3
+ Version: 0.1.0
4
+ Summary: Launch training runs on your own or rented machines and track what they record
5
+ Project-URL: Repository, https://github.com/loopflowstudio/etude
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: numpy<3,>=1.26
10
+ Requires-Dist: pydantic<3,>=2.12
11
+ Requires-Dist: typer<1,>=0.27
12
+ Provides-Extra: s3
13
+ Requires-Dist: boto3<2,>=1.35; extra == 's3'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Etude
17
+
18
+ Launch training runs on your own or rented machines, and track what they record.
19
+
20
+ ```python
21
+ # job.py
22
+ from etude.launch.records import Job
23
+
24
+
25
+ def runs() -> Job:
26
+ return Job(command="uv run python train.py", max_hours=2)
27
+ ```
28
+
29
+ ```python
30
+ # train.py
31
+ from etude.track.records import Step
32
+ from etude.track.run import init
33
+
34
+ run = init(experiment="capacity-pilot", clocks=[Step("updates", "optimizer updates")])
35
+ for update in range(1, 1001):
36
+ loss = train_one_update()
37
+ run.log({"loss": loss}, updates=update)
38
+ if update % 500 == 0:
39
+ run.checkpoint(save_model(), updates=update)
40
+ run.finish()
41
+ ```
42
+
43
+ ```sh
44
+ etude launch job.py # run it on this computer, an SSH host or a rented GPU
45
+ etude status # where runs stand
46
+ etude logs RUN # what a run wrote
47
+ etude reports write --open # charts of every run, as one HTML file per experiment
48
+ ```
49
+
50
+ Everything a run records goes to your own directory or S3 bucket. There is no
51
+ server to run and no account to make. A run that loses its machine resumes from
52
+ its last checkpoint on another, and reads back as one run.
53
+
54
+ ## Install
55
+
56
+ Etude needs Python 3.12 and [uv](https://docs.astral.sh/uv/).
57
+
58
+ ```sh
59
+ uv tool install etude-sdk # the `etude` command
60
+ uv add etude-sdk # the library, in a training project
61
+ uv add 'etude-sdk[s3]' # with S3 storage
62
+ ```
63
+
64
+ ## Read more
65
+
66
+ - [Tracking](docs/tracking.md): runs, clocks, checkpoints, evaluations, reports.
67
+ - [The launcher](docs/launcher.md): pools, SSH hosts, rented machines, cohorts,
68
+ limits and recovery.
69
+
70
+ ## Develop
71
+
72
+ ```sh
73
+ uv sync
74
+ uv run pytest
75
+ uv run mypy
76
+ uv run ruff check . && uv run ruff format --check .
77
+ ```
78
+
79
+ [AGENTS.md](AGENTS.md) holds the conventions, for people and coding agents.
80
+
81
+ ## Licence
82
+
83
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,68 @@
1
+ # Etude
2
+
3
+ Launch training runs on your own or rented machines, and track what they record.
4
+
5
+ ```python
6
+ # job.py
7
+ from etude.launch.records import Job
8
+
9
+
10
+ def runs() -> Job:
11
+ return Job(command="uv run python train.py", max_hours=2)
12
+ ```
13
+
14
+ ```python
15
+ # train.py
16
+ from etude.track.records import Step
17
+ from etude.track.run import init
18
+
19
+ run = init(experiment="capacity-pilot", clocks=[Step("updates", "optimizer updates")])
20
+ for update in range(1, 1001):
21
+ loss = train_one_update()
22
+ run.log({"loss": loss}, updates=update)
23
+ if update % 500 == 0:
24
+ run.checkpoint(save_model(), updates=update)
25
+ run.finish()
26
+ ```
27
+
28
+ ```sh
29
+ etude launch job.py # run it on this computer, an SSH host or a rented GPU
30
+ etude status # where runs stand
31
+ etude logs RUN # what a run wrote
32
+ etude reports write --open # charts of every run, as one HTML file per experiment
33
+ ```
34
+
35
+ Everything a run records goes to your own directory or S3 bucket. There is no
36
+ server to run and no account to make. A run that loses its machine resumes from
37
+ its last checkpoint on another, and reads back as one run.
38
+
39
+ ## Install
40
+
41
+ Etude needs Python 3.12 and [uv](https://docs.astral.sh/uv/).
42
+
43
+ ```sh
44
+ uv tool install etude-sdk # the `etude` command
45
+ uv add etude-sdk # the library, in a training project
46
+ uv add 'etude-sdk[s3]' # with S3 storage
47
+ ```
48
+
49
+ ## Read more
50
+
51
+ - [Tracking](docs/tracking.md): runs, clocks, checkpoints, evaluations, reports.
52
+ - [The launcher](docs/launcher.md): pools, SSH hosts, rented machines, cohorts,
53
+ limits and recovery.
54
+
55
+ ## Develop
56
+
57
+ ```sh
58
+ uv sync
59
+ uv run pytest
60
+ uv run mypy
61
+ uv run ruff check . && uv run ruff format --check .
62
+ ```
63
+
64
+ [AGENTS.md](AGENTS.md) holds the conventions, for people and coding agents.
65
+
66
+ ## Licence
67
+
68
+ MIT. See [LICENSE](LICENSE).