raydriver 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 (57) hide show
  1. raydriver-0.1.0/.github/workflows/ci.yml +180 -0
  2. raydriver-0.1.0/.github/workflows/release.yml +85 -0
  3. raydriver-0.1.0/.gitignore +9 -0
  4. raydriver-0.1.0/AGENTS.md +111 -0
  5. raydriver-0.1.0/Cargo.lock +1426 -0
  6. raydriver-0.1.0/Cargo.toml +36 -0
  7. raydriver-0.1.0/Cargo.toml.bak +35 -0
  8. raydriver-0.1.0/LICENSE +21 -0
  9. raydriver-0.1.0/Makefile +42 -0
  10. raydriver-0.1.0/PKG-INFO +93 -0
  11. raydriver-0.1.0/README.md +76 -0
  12. raydriver-0.1.0/env.sh +5 -0
  13. raydriver-0.1.0/freeze-dump.txt +0 -0
  14. raydriver-0.1.0/pyproject.toml +57 -0
  15. raydriver-0.1.0/python/raydriver/__init__.py +22 -0
  16. raydriver-0.1.0/python/raydriver/__init__.pyi +141 -0
  17. raydriver-0.1.0/python/raydriver/cli/__init__.py +1 -0
  18. raydriver-0.1.0/python/raydriver/cli/main.py +290 -0
  19. raydriver-0.1.0/python/raydriver/emulator.py +871 -0
  20. raydriver-0.1.0/python/raydriver/grbl/__init__.py +12 -0
  21. raydriver-0.1.0/python/raydriver/grbl/__init__.pyi +190 -0
  22. raydriver-0.1.0/python/raydriver/grbl/parser/__init__.py +30 -0
  23. raydriver-0.1.0/python/raydriver/grbl/parser/__init__.pyi +8 -0
  24. raydriver-0.1.0/python/raydriver/grbl/parser/py.typed +0 -0
  25. raydriver-0.1.0/python/raydriver/grbl/py.typed +0 -0
  26. raydriver-0.1.0/python/raydriver/grbl/types/__init__.py +12 -0
  27. raydriver-0.1.0/python/raydriver/grbl/types/__init__.pyi +91 -0
  28. raydriver-0.1.0/python/raydriver/grbl/types/py.typed +0 -0
  29. raydriver-0.1.0/python/raydriver/py.typed +0 -0
  30. raydriver-0.1.0/rustfmt.toml +1 -0
  31. raydriver-0.1.0/src/bin/stub_gen.rs +4 -0
  32. raydriver-0.1.0/src/grbl/dialect.rs +218 -0
  33. raydriver-0.1.0/src/grbl/errors.rs +289 -0
  34. raydriver-0.1.0/src/grbl/flow.rs +435 -0
  35. raydriver-0.1.0/src/grbl/mod.rs +13 -0
  36. raydriver-0.1.0/src/grbl/parser.rs +510 -0
  37. raydriver-0.1.0/src/grbl/session.rs +2096 -0
  38. raydriver-0.1.0/src/grbl/transport/mock.rs +116 -0
  39. raydriver-0.1.0/src/grbl/transport/mod.rs +60 -0
  40. raydriver-0.1.0/src/grbl/transport/serial.rs +106 -0
  41. raydriver-0.1.0/src/grbl/transport/telnet.rs +106 -0
  42. raydriver-0.1.0/src/grbl/types.rs +181 -0
  43. raydriver-0.1.0/src/lib.rs +37 -0
  44. raydriver-0.1.0/src/python/events.rs +288 -0
  45. raydriver-0.1.0/src/python/mock.rs +79 -0
  46. raydriver-0.1.0/src/python/mod.rs +50 -0
  47. raydriver-0.1.0/src/python/parser.rs +267 -0
  48. raydriver-0.1.0/src/python/pyfuture.rs +70 -0
  49. raydriver-0.1.0/src/python/runtime.rs +45 -0
  50. raydriver-0.1.0/src/python/session.rs +587 -0
  51. raydriver-0.1.0/src/python/types.rs +238 -0
  52. raydriver-0.1.0/tests/conftest.py +155 -0
  53. raydriver-0.1.0/tests/test_cli.py +79 -0
  54. raydriver-0.1.0/tests/test_parser.py +347 -0
  55. raydriver-0.1.0/tests/test_session.py +362 -0
  56. raydriver-0.1.0/tests/test_streaming.py +250 -0
  57. raydriver-0.1.0/tests/test_stress.py +92 -0
@@ -0,0 +1,180 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ name: Lint and typecheck
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v7
15
+
16
+ - name: Build wheel
17
+ uses: PyO3/maturin-action@v1
18
+ with:
19
+ command: build
20
+ args: --release --out dist
21
+
22
+ - name: Install wheel
23
+ run: pip install dist/raydriver-*.whl
24
+
25
+ - name: Lint Python
26
+ run: pip install ruff && ruff check tests/ python/
27
+
28
+ - name: Install Rust toolchain
29
+ uses: dtolnay/rust-toolchain@1.98.1
30
+ with:
31
+ components: rustfmt, clippy
32
+
33
+ - name: Lint Rust
34
+ shell: bash
35
+ run: |
36
+ set -o pipefail
37
+ cargo fmt --check 2>&1 | tee fmt-output.txt
38
+ cargo clippy -- -D warnings 2>&1 | tee clippy-output.txt
39
+
40
+ - name: Persist diagnostics to ci-logs branch
41
+ if: failure()
42
+ shell: bash
43
+ run: |
44
+ mkdir -p logs
45
+ cp fmt-output.txt clippy-output.txt logs/ 2>/dev/null || true
46
+ git config user.name ci-bot
47
+ git config user.email ci-bot@users.noreply.github.com
48
+ git add logs
49
+ git diff --cached --quiet && echo "nothing to persist" && exit 0
50
+ git commit -qm "lint diagnostics @ ${{ github.sha }}"
51
+ git push -q -f origin "HEAD:refs/heads/ci-logs-lint"
52
+
53
+ test:
54
+ name: Tests (${{ matrix.os }})
55
+ runs-on: ${{ matrix.os }}
56
+ timeout-minutes: 40
57
+ strategy:
58
+ fail-fast: false
59
+ # Windows is covered by the sharded windows-shard job (its
60
+ # release build is slow and one hang must not hide the rest).
61
+ matrix:
62
+ os: [ubuntu-latest, macos-15-intel, macos-15]
63
+ steps:
64
+ - uses: actions/checkout@v7
65
+
66
+ - name: Build wheel
67
+ uses: PyO3/maturin-action@v1
68
+ with:
69
+ command: build
70
+ args: --release --out dist
71
+
72
+ - name: Install wheel
73
+ shell: bash
74
+ run: pip install dist/raydriver-*.whl
75
+
76
+ - name: Install test dependencies
77
+ shell: bash
78
+ run: pip install pytest pytest-asyncio pytest-timeout
79
+
80
+ - name: Run tests
81
+ shell: bash
82
+ run: |
83
+ set -o pipefail
84
+ rm -f pytest-output.txt
85
+ for attempt in 1 2 3; do
86
+ if pytest -v --timeout=60 --timeout-method=thread --tb=short -rA 2>&1 | tee -a pytest-output.txt; then
87
+ exit 0
88
+ fi
89
+ echo "attempt $attempt failed, retrying..." | tee -a pytest-output.txt
90
+ done
91
+ exit 1
92
+
93
+ - name: Persist diagnostics to ci-logs branch
94
+ if: failure() || cancelled()
95
+ shell: bash
96
+ run: |
97
+ mkdir -p logs
98
+ cp pytest-output.txt freeze-dump.txt logs/ 2>/dev/null || true
99
+ git config user.name ci-bot
100
+ git config user.email ci-bot@users.noreply.github.com
101
+ git add logs
102
+ git diff --cached --quiet && echo "nothing to persist" && exit 0
103
+ git commit -qm "test diagnostics: ${{ matrix.os }} @ ${{ github.sha }}"
104
+ git push -q -f origin "HEAD:refs/heads/ci-logs-main-${{ matrix.os }}"
105
+
106
+ windows-shard:
107
+ name: Windows shard (${{ matrix.file }})
108
+ runs-on: windows-latest
109
+ timeout-minutes: 60
110
+ strategy:
111
+ fail-fast: false
112
+ matrix:
113
+ file:
114
+ - tests/test_cli.py
115
+ - tests/test_parser.py
116
+ - tests/test_session.py
117
+ - tests/test_streaming.py
118
+ - tests/test_stress.py
119
+ steps:
120
+ - uses: actions/checkout@v7
121
+
122
+ - name: Install Rust toolchain
123
+ uses: dtolnay/rust-toolchain@1.98.1
124
+
125
+ - name: Install maturin
126
+ shell: bash
127
+ run: pip install maturin
128
+
129
+ - name: Build wheel
130
+ shell: bash
131
+ run: |
132
+ set -o pipefail
133
+ maturin build --release --out dist 2>&1 | tee build-output.txt
134
+
135
+ - name: Persist build diagnostics to ci-logs branch
136
+ if: failure()
137
+ shell: bash
138
+ run: |
139
+ mkdir -p logs
140
+ cp build-output.txt logs/ 2>/dev/null || true
141
+ git config user.name ci-bot
142
+ git config user.email ci-bot@users.noreply.github.com
143
+ git add logs
144
+ git diff --cached --quiet && echo "nothing to persist" && exit 0
145
+ git commit -qm "windows build diagnostics: ${{ matrix.file }} @ ${{ github.sha }}"
146
+ git push -q -f origin "HEAD:refs/heads/ci-logs-win-build-${{ matrix.file }}"
147
+
148
+ - name: Install wheel
149
+ shell: bash
150
+ run: pip install dist/raydriver-*.whl
151
+
152
+ - name: Install test dependencies
153
+ shell: bash
154
+ run: pip install pytest pytest-asyncio pytest-timeout
155
+
156
+ - name: Run shard
157
+ shell: bash
158
+ run: |
159
+ set -o pipefail
160
+ rm -f shard-output.txt
161
+ for attempt in 1 2 3; do
162
+ if python -u -m pytest ${{ matrix.file }} -v --timeout=30 --timeout-method=thread --tb=long -rA 2>&1 | tee -a shard-output.txt; then
163
+ exit 0
164
+ fi
165
+ echo "attempt $attempt failed, retrying..." | tee -a shard-output.txt
166
+ done
167
+ exit 1
168
+
169
+ - name: Persist shard diagnostics to ci-logs branch
170
+ if: failure() || cancelled()
171
+ shell: bash
172
+ run: |
173
+ mkdir -p logs
174
+ cp shard-output.txt freeze-dump.txt logs/ 2>/dev/null || true
175
+ git config user.name ci-bot
176
+ git config user.email ci-bot@users.noreply.github.com
177
+ git add logs
178
+ git diff --cached --quiet && echo "nothing to persist" && exit 0
179
+ git commit -qm "windows shard diagnostics: ${{ matrix.file }} @ ${{ github.sha }}"
180
+ git push -q -f origin "HEAD:refs/heads/ci-logs-win-shard"
@@ -0,0 +1,85 @@
1
+ name: Build and Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build-wheels:
10
+ name: Build wheels on ${{ matrix.os }}
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, windows-latest, macos-15-intel, macos-15]
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+
19
+ - name: Set version from tag
20
+ if: startsWith(github.ref, 'refs/tags/v')
21
+ shell: bash
22
+ run: |
23
+ VERSION="${GITHUB_REF#refs/tags/v}"
24
+ sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml
25
+
26
+ - uses: PyO3/maturin-action@v1
27
+ with:
28
+ command: build
29
+ args: --release --out dist
30
+
31
+ - name: Upload wheels as artifacts
32
+ uses: actions/upload-artifact@v4
33
+ with:
34
+ name: wheels-${{ matrix.os }}
35
+ path: dist/*.whl
36
+
37
+ build-sdist:
38
+ name: Build sdist
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v7
42
+
43
+ - name: Set version from tag
44
+ if: startsWith(github.ref, 'refs/tags/v')
45
+ shell: bash
46
+ run: |
47
+ VERSION="${GITHUB_REF#refs/tags/v}"
48
+ sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml
49
+
50
+ - uses: PyO3/maturin-action@v1
51
+ with:
52
+ command: sdist
53
+ args: --out dist
54
+
55
+ - name: Upload sdist as artifact
56
+ uses: actions/upload-artifact@v4
57
+ with:
58
+ name: sdist
59
+ path: dist/*.tar.gz
60
+
61
+ publish:
62
+ name: Publish to PyPI
63
+ needs: [build-wheels, build-sdist]
64
+ runs-on: ubuntu-latest
65
+ environment: pypi
66
+ permissions:
67
+ id-token: write
68
+ contents: write
69
+ steps:
70
+ - name: Download all artifacts
71
+ uses: actions/download-artifact@v4
72
+ with:
73
+ path: dist
74
+ merge-multiple: true
75
+
76
+ - name: Publish to PyPI
77
+ uses: pypa/gh-action-pypi-publish@release/v1
78
+ with:
79
+ packages-dir: dist/
80
+
81
+ - name: Upload artifacts to GitHub Release
82
+ uses: softprops/action-gh-release@v2
83
+ if: startsWith(github.ref, 'refs/tags/')
84
+ with:
85
+ files: dist/*
@@ -0,0 +1,9 @@
1
+ target/
2
+ __pycache__/
3
+ *.pyc
4
+ *.so
5
+ *.pyd
6
+ dist/
7
+ .venv/
8
+ .ruff_cache/
9
+ .pytest_cache/
@@ -0,0 +1,111 @@
1
+ # AGENTS.md
2
+
3
+ ## Available commands
4
+
5
+ - `make dev` — build and install into the active venv
6
+ - `make stubs` — re-generate `.pyi` type stubs (after changing `src/python/` bindings)
7
+ - `make lint` — lint all code (Rust + Python)
8
+ - `make lint-rust` — lint Rust only
9
+ - `make lint-python` — lint Python only
10
+ - `make format` — auto-format all code (Rust + Python, including PEP8 import ordering)
11
+ - `make format-rust` — format Rust only
12
+ - `make format-python` — format Python only (ruff handles formatting and import sorting)
13
+ - `make test` — run the full test suite. make sure to "make dev" before you test
14
+ - `make check` — lint + test
15
+ - `make build` — build the wheel (release)
16
+
17
+ ## CLI
18
+
19
+ - `raydriver run job.gcode --port /dev/ttyUSB0` — stream a G-code
20
+ file to a real device with live progress
21
+ - `raydriver run job.gcode --emulator` — same, against the built-in
22
+ firmware emulator (no hardware needed)
23
+ - `raydriver status --port /dev/ttyUSB0` — connect and print live
24
+ status reports
25
+
26
+ ## Testing
27
+
28
+ All tests are Python-based, under `tests/`, and run against
29
+ `raydriver.emulator.GrblEmulator` — a GRBL 1.1h *firmware emulator*
30
+ (not a mock): it models the character-counting RX buffer, the
31
+ 15-block planner with deferred acknowledgements, realtime command
32
+ interception, modal G-code state, feed-rate-timed motion, alarms,
33
+ homing, probing and the `$` system commands. Device-side code
34
+ consumes session writes via `MockTransport.take_new_sent()` (a
35
+ monotonic cursor — never index into `sent()`, which tests may clear
36
+ at any time).
37
+
38
+ ## Rules
39
+
40
+ - You are strictly forbidden from editing stubs manually. They are only
41
+ to be edited using "make stubs".
42
+ - Use make commands when available - avoid calling the underlying tools
43
+ directly.
44
+ - Never add Rust tests (`#[cfg(test)]` / `#[test]` blocks in `src/`).
45
+ All tests are Python-based, under `tests/`. Exercise new Rust code
46
+ through PyO3 bindings from Python test code.
47
+ - Keep the Grbl protocol behavior byte-for-byte compatible with the
48
+ Python driver in Rayforge
49
+ (`rayforge/machine/driver/grbl/grbl_serial.py`). Do not change
50
+ protocol quirks (ack extraction order, realtime bypass, buffer
51
+ accounting, stall/liveness rules) without an explicit request.
52
+
53
+ ## Layering Rules Specification
54
+
55
+ The crate is split into two layers that depend only downward:
56
+
57
+ ```
58
+ grbl (protocol + session core) → python (PyO3 bindings)
59
+ ```
60
+
61
+ - `src/grbl/` must not depend on PyO3 or any Python types. It is pure
62
+ Rust: protocol parsing, flow control, transports, and the session
63
+ state machines. Python-facing notifications go through the
64
+ `SessionEvents` trait; the `python` feature provides the
65
+ implementation that marshals events across the boundary.
66
+ - `src/python/` contains all PyO3 bindings and may use `src/grbl/`
67
+ freely, never the other way around.
68
+
69
+ ## Export Policy: Explicit Paths
70
+
71
+ Every item has exactly one canonical path — its leaf module. Parent
72
+ mod.rs must not re-export children's items (no namespace flattening).
73
+
74
+ Exceptions: Primitive types, errors, classes and constants that are
75
+ _public_ AND _shared_ within a submodule.
76
+
77
+ Python sub-modules mirror the Rust hierarchy - no aliases or re-exports
78
+ at higher levels. The compiled module is `raydriver.raydriver`; the
79
+ pure-Python packages under `python/raydriver/` serve as the importable
80
+ packages and delegate to the Rust module via module-level
81
+ `__getattr__`:
82
+
83
+ ```python
84
+ import raydriver.raydriver as _raydriver
85
+
86
+ def __getattr__(name):
87
+ return getattr(_raydriver.grbl, name)
88
+ ```
89
+
90
+ Rust submodules registered with `add_submodule` are intentionally NOT
91
+ registered in `sys.modules` — the Python `__init__.py` of the same
92
+ name is the package.
93
+
94
+ ## Python/Rust Boundary Rules
95
+
96
+ - No domain model objects cross the Python/Rust boundary — only
97
+ primitive types, typed pyclasses defined in this crate, and
98
+ JSON-serialisable dicts.
99
+ - Async Rust methods return asyncio futures that are completed from
100
+ the tokio side via `call_soon_threadsafe` on the caller's event
101
+ loop. Never block the loop inside a binding.
102
+ - Events emitted by the session are delivered to the Python
103
+ `event_callback(name: str, payload)` on the event loop thread.
104
+ Bindings must not call the callback from a foreign thread directly.
105
+
106
+ ## Dialects
107
+
108
+ Dialects remain data owned by Rayforge. This crate receives resolved
109
+ command templates (a plain dict of format strings) and only needs the
110
+ subset used for interactive commands. Formatting supports the Python
111
+ `str.format` subset `{name}` and `{name:.Nf}`.