qc-grader 2026.6.18__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 (53) hide show
  1. qc_grader-2026.6.18/.github/workflows/ci.yml +18 -0
  2. qc_grader-2026.6.18/.github/workflows/release.yml +147 -0
  3. qc_grader-2026.6.18/.gitignore +74 -0
  4. qc_grader-2026.6.18/AGENTS.md +17 -0
  5. qc_grader-2026.6.18/CLAUDE.md +1 -0
  6. qc_grader-2026.6.18/CODE_OF_CONDUCT.md +9 -0
  7. qc_grader-2026.6.18/CONTRIBUTING.md +279 -0
  8. qc_grader-2026.6.18/Justfile +10 -0
  9. qc_grader-2026.6.18/LICENSE.txt +203 -0
  10. qc_grader-2026.6.18/PKG-INFO +22 -0
  11. qc_grader-2026.6.18/README.md +76 -0
  12. qc_grader-2026.6.18/pyproject.toml +53 -0
  13. qc_grader-2026.6.18/qc_grader/__init__.py +15 -0
  14. qc_grader-2026.6.18/qc_grader/challenges/__init__.py +9 -0
  15. qc_grader-2026.6.18/qc_grader/challenges/common/__init__.py +9 -0
  16. qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/__init__.py +9 -0
  17. qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/lab_hadron.py +220 -0
  18. qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/lab_qmoo.py +154 -0
  19. qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/lab_skqd.py +172 -0
  20. qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/qmoo_files.py +65 -0
  21. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/__init__.py +127 -0
  22. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab0.py +105 -0
  23. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab1.py +76 -0
  24. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab2.py +200 -0
  25. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab3.py +272 -0
  26. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab4a.py +44 -0
  27. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab4b.py +408 -0
  28. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab4b_test.py +671 -0
  29. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab4c.py +174 -0
  30. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/utils/lab4c_test_occ.npy +0 -0
  31. qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/utils/lab4c_test_samples.npy +0 -0
  32. qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_canada/__init__.py +78 -0
  33. qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_canada/lab_hadron.py +39 -0
  34. qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_canada/lab_qmoo.py +25 -0
  35. qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_canada/lab_skqd.py +29 -0
  36. qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_us/__init__.py +78 -0
  37. qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_us/lab_hadron.py +39 -0
  38. qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_us/lab_qmoo.py +25 -0
  39. qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_us/lab_skqd.py +29 -0
  40. qc_grader-2026.6.18/qc_grader/challenges/test_challenges/__init__.py +9 -0
  41. qc_grader-2026.6.18/qc_grader/challenges/test_challenges/individual.py +51 -0
  42. qc_grader-2026.6.18/qc_grader/challenges/test_challenges/team.py +56 -0
  43. qc_grader-2026.6.18/qc_grader/custom_encoder/__init__.py +14 -0
  44. qc_grader-2026.6.18/qc_grader/custom_encoder/json_encoder.py +78 -0
  45. qc_grader-2026.6.18/qc_grader/custom_encoder/json_encoder_test.py +60 -0
  46. qc_grader-2026.6.18/qc_grader/custom_encoder/serializer.py +151 -0
  47. qc_grader-2026.6.18/qc_grader/grader/__init__.py +9 -0
  48. qc_grader-2026.6.18/qc_grader/grader/api.py +46 -0
  49. qc_grader-2026.6.18/qc_grader/grader/auth.py +77 -0
  50. qc_grader-2026.6.18/qc_grader/grader/env.py +24 -0
  51. qc_grader-2026.6.18/qc_grader/grader/grade.py +202 -0
  52. qc_grader-2026.6.18/qc_grader/grader/grade_test.py +185 -0
  53. qc_grader-2026.6.18/uv.lock +1510 -0
@@ -0,0 +1,18 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ jobs:
7
+ test:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: astral-sh/setup-uv@v5
12
+ with:
13
+ python-version: "3.10"
14
+ - uses: extractions/setup-just@v3
15
+ - name: install
16
+ run: uv sync
17
+ - run: just lint
18
+ - run: just test
@@ -0,0 +1,147 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - 'qc_grader/__init__.py'
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ validate:
13
+ runs-on: ubuntu-latest
14
+ outputs:
15
+ version: ${{ steps.get-version.outputs.version }}
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Get version from qc_grader/__init__.py
21
+ id: get-version
22
+ run: |
23
+ VERSION=$(python -c "import re; print(re.search(r'__version__\s*=\s*\"([^\"]+)\"', open('qc_grader/__init__.py').read()).group(1))")
24
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
25
+ echo "Version to release: $VERSION"
26
+
27
+ - name: Validate version format (YEAR.MONTH.DAY, no leading zeros)
28
+ run: |
29
+ VERSION="${{ steps.get-version.outputs.version }}"
30
+ if ! [[ "$VERSION" =~ ^[0-9]{4}\.(1[0-2]|[1-9])\.(3[01]|[12][0-9]|[1-9])(\.[0-9]+)?$ ]]; then
31
+ echo "Error: Invalid version format: $VERSION"
32
+ echo "Expected a date-based version YEAR.MONTH.DAY with no leading zeros, e.g. 2026.6.18"
33
+ exit 1
34
+ fi
35
+
36
+ - name: Check if tag exists
37
+ run: |
38
+ VERSION="${{ steps.get-version.outputs.version }}"
39
+ if git ls-remote --tags origin "v$VERSION" | grep -q .; then
40
+ echo "Error: Tag v$VERSION already exists"
41
+ exit 1
42
+ fi
43
+
44
+ test:
45
+ needs: validate
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+ - uses: astral-sh/setup-uv@v5
50
+ with:
51
+ python-version: "3.10"
52
+ - uses: extractions/setup-just@v3
53
+ - name: install
54
+ run: uv sync
55
+ - run: just lint
56
+ - run: just test
57
+
58
+ build:
59
+ needs: [validate, test]
60
+ runs-on: ubuntu-latest
61
+
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+ - uses: astral-sh/setup-uv@v5
65
+
66
+ - name: Build distribution
67
+ run: uv build
68
+
69
+ - name: Store the distribution packages
70
+ uses: actions/upload-artifact@v4
71
+ with:
72
+ name: python-package-distributions
73
+ path: dist/
74
+
75
+ publish-to-pypi:
76
+ needs: build
77
+ runs-on: ubuntu-latest
78
+ environment:
79
+ name: pypi
80
+ url: https://pypi.org/project/qc_grader/
81
+ permissions:
82
+ id-token: write
83
+
84
+ steps:
85
+ - name: Download all the dists
86
+ uses: actions/download-artifact@v4
87
+ with:
88
+ name: python-package-distributions
89
+ path: dist/
90
+
91
+ - name: Publish distribution to PyPI
92
+ uses: pypa/gh-action-pypi-publish@release/v1
93
+
94
+ create-release:
95
+ needs: [validate, publish-to-pypi]
96
+ runs-on: ubuntu-latest
97
+ permissions:
98
+ contents: write
99
+
100
+ steps:
101
+ - uses: actions/checkout@v4
102
+ with:
103
+ fetch-depth: 0
104
+
105
+ - name: Create and push tag
106
+ run: |
107
+ VERSION="${{ needs.validate.outputs.version }}"
108
+ git config user.name "github-actions[bot]"
109
+ git config user.email "github-actions[bot]@users.noreply.github.com"
110
+ git tag -a "v$VERSION" -m "Release version $VERSION"
111
+ git push origin "v$VERSION"
112
+
113
+ - name: Write release notes
114
+ env:
115
+ VERSION: ${{ needs.validate.outputs.version }}
116
+ REPO: ${{ github.repository }}
117
+ run: |
118
+ # The previous release tag, if any. HEAD is already tagged v$VERSION,
119
+ # so look at HEAD^ to find the prior release.
120
+ PREV=$(git describe --tags --abbrev=0 --match 'v*' "v$VERSION^" 2>/dev/null || true)
121
+ if [ -n "$PREV" ]; then
122
+ URL="https://github.com/$REPO/compare/$PREV...v$VERSION"
123
+ else
124
+ URL="https://github.com/$REPO/commits/v$VERSION"
125
+ fi
126
+ {
127
+ echo "**What changed:** see the commits included in this release:"
128
+ echo ""
129
+ echo "$URL"
130
+ } > release-notes.md
131
+ cat release-notes.md
132
+
133
+ - name: Download distribution artifacts
134
+ uses: actions/download-artifact@v4
135
+ with:
136
+ name: python-package-distributions
137
+ path: dist/
138
+
139
+ - name: Create GitHub Release
140
+ env:
141
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
142
+ VERSION: ${{ needs.validate.outputs.version }}
143
+ run: |
144
+ gh release create "v$VERSION" \
145
+ --title "v$VERSION" \
146
+ --notes-file release-notes.md \
147
+ dist/*
@@ -0,0 +1,74 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ pip-wheel-metadata/
19
+ share/python-wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ MANIFEST
24
+
25
+ *.manifest
26
+ *.spec
27
+
28
+ pip-log.txt
29
+ pip-delete-this-directory.txt
30
+
31
+ htmlcov/
32
+ .tox/
33
+ .nox/
34
+ .coverage
35
+ .coverage.*
36
+ .cache
37
+ nosetests.xml
38
+ coverage.xml
39
+ *.cover
40
+ *.py,cover
41
+ .hypothesis/
42
+ .pytest_cache/
43
+
44
+ .ipynb_checkpoints
45
+ profile_default/
46
+ ipython_config.py
47
+
48
+ .python-version
49
+
50
+ __pypackages__/
51
+
52
+ .mypy_cache/
53
+ .dmypy.json
54
+ dmypy.json
55
+
56
+ .pyre/
57
+
58
+ .DS_Store
59
+
60
+ .vscode
61
+ .idea/
62
+
63
+ setup.cfg
64
+ qc_archives/
65
+
66
+ # Environments
67
+ .env
68
+ .venv
69
+ env/
70
+ venv/
71
+ ENV/
72
+ env.bak/
73
+ venv.bak/
74
+ .claude/
@@ -0,0 +1,17 @@
1
+ # Agents.md
2
+
3
+ ## About the project
4
+
5
+ This project is a Python library for students to submit their answers to quantum computing challenges to a grader server. The server will return back whether the answer was correct or not.
6
+
7
+ Users use this Python library through a REPL or Jupyter notebook, rather than a traditional Python program.
8
+
9
+ Users are primarily students and are sometimes beginners to either programming or quantum computing. So, where reasonable, we try to make the program user-friendly, such as useful error messages.
10
+
11
+ ## Workflows
12
+
13
+ * Format: `just fmt`
14
+ * Linter and type checker: `just lint`
15
+ * Test: `just test`
16
+ * Add new labs and exercises: refer to `CONTRIBUTING.md`
17
+ * Releases: refer to `CONTRIBUTING.md`. All user-facing changes should have a new release.
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,9 @@
1
+ <!-- Copyright Contributors to the Qiskit project. -->
2
+
3
+ # Code of Conduct
4
+ All members of this project agree to adhere to the Qiskit Code of Conduct listed at [https://github.com/Qiskit/qiskit/blob/master/CODE_OF_CONDUCT.md](https://github.com/Qiskit/qiskit/blob/master/CODE_OF_CONDUCT.md)
5
+
6
+ ----
7
+
8
+ License: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/),
9
+ Copyright Contributors to Qiskit.
@@ -0,0 +1,279 @@
1
+ # Contributing
2
+
3
+ ## Prerequisites
4
+
5
+ * [uv](https://docs.astral.sh/uv/getting-started/installation/)
6
+ * [Just](https://just.systems/man/en/)
7
+
8
+ ## Installation
9
+
10
+ ```sh
11
+ uv sync
12
+ ```
13
+
14
+ (uv automatically creates and manages a virtual environment.)
15
+
16
+ ## Format code
17
+
18
+ ```
19
+ just fmt
20
+ ```
21
+
22
+ ## Lint
23
+
24
+ ```
25
+ just lint
26
+ ```
27
+
28
+ To run the individual linters:
29
+
30
+ * Ruff: `uv run ruff check`
31
+ * Ty (type check): `uv run ty check`
32
+
33
+ ## Tests
34
+
35
+ ```
36
+ just test
37
+ ```
38
+
39
+ ## Update dependencies
40
+
41
+ ([Original documentation](https://docs.astral.sh/uv/concepts/projects/dependencies/))
42
+
43
+ Add a dependency:
44
+
45
+ ```
46
+ uv add <dependency>
47
+ ```
48
+
49
+ Add a dev dependency:
50
+
51
+ ```
52
+ uv add <dependency> --dev
53
+ ```
54
+
55
+ ## Release a new version
56
+
57
+ You should release a new version any time you make user-facing changes.
58
+
59
+ Releasing is automated. To cut a release, update `__version__` in
60
+ [`qc_grader/__init__.py`](qc_grader/__init__.py) to **today's date** and merge to `main`.
61
+ A GitHub Actions workflow then builds the package, publishes it to PyPI, and creates a
62
+ GitHub Release whose notes link to the commits included in that release.
63
+
64
+ ### Version format
65
+
66
+ Versions are date-based, written as **`YEAR.MONTH.DAY`** with **no leading zeros**:
67
+
68
+ * The year comes first, then the month, then the day (`YEAR.MONTH.DAY`).
69
+ * Do not pad with zeros: use `6`, not `06`.
70
+
71
+ For example, a release made on **18 June 2026** is version `2026.6.18`.
72
+
73
+ | Date | Version |
74
+ | -------------- | ----------- |
75
+ | 18 June 2026 | `2026.6.18` |
76
+ | 1 December 2026 | `2026.12.1` |
77
+ | 5 January 2027 | `2027.1.5` |
78
+
79
+ If you need to release more than once on the same day, add a counter at the end, starting
80
+ at `.1`: the second release on 18 June 2026 is `2026.6.18.1`, the third is `2026.6.18.2`,
81
+ and so on.
82
+
83
+ Versions only ever move forward — we always release from the latest `main` and never
84
+ maintain older versions in parallel.
85
+
86
+ ## Run the client
87
+
88
+ Use this workflow to test the Python client against the Grader server.
89
+
90
+ ### Initial setup
91
+
92
+ You must create a Quantum API token for an account with at least one instance.
93
+
94
+ Prod server:
95
+
96
+ 1. Use https://quantum.cloud.ibm.com to create the API key
97
+ 2. Save the key by running `uv run python`, then this code:
98
+
99
+ ```python
100
+ from qiskit_ibm_runtime import QiskitRuntimeService
101
+
102
+ QiskitRuntimeService.save_account(
103
+ token="<your-api-key>",
104
+ instance="<CRN>",
105
+ )
106
+ ```
107
+ 3. Close the REPL.
108
+
109
+ Staging or local development server:
110
+
111
+ 1. Use https://quantum.test.cloud.ibm.com to create the API key.
112
+ 2. Save the key by running `uv run python`, then this code:
113
+
114
+ ```python
115
+ from qiskit_ibm_runtime import QiskitRuntimeService
116
+
117
+ QiskitRuntimeService.save_account(
118
+ token="<your-api-key>",
119
+ instance="<CRN>",
120
+ name="grader-staging",
121
+ )
122
+ ```
123
+ 3. Close the REPL.
124
+
125
+ ### How to run
126
+
127
+ 1. Launch a Python REPL:
128
+ - Prod server: `uv run python`
129
+ - Staging server: `STAGING=1 uv run python`
130
+ - Local development server: `DEV=1 uv run python`
131
+ 2. In the REPL, import and run your exercises. For example:
132
+
133
+ ```python
134
+ >>> from qc_grader.challenges.qgss_2026 import grade_lab0_ex1
135
+ >>> grade_lab0_ex1()
136
+ ```
137
+
138
+ For developers testing how the server behaves, you can use the files from `qc_grader.challenges.test_challenges`, such as `grade_success` from `qc_grader.challenges.test_challenges.individual`.
139
+
140
+ ## Adding a new challenge
141
+
142
+ Create a new folder under `qc_grader/challenges` with the name of the challenge. This folder should contain:
143
+
144
+ * A file for each lab (such as `lab0.py`, `lab2.py`)
145
+ * An `__init__.py`, which imports and re-exports the grading functions from your labs.
146
+
147
+ Every challenge must also export a `check_progress` function so users can see how far they've gotten:
148
+
149
+ ```python
150
+ from qc_grader.grader.grade import create_check_progress_function
151
+
152
+ # Replace the string with the name of your challenge
153
+ check_progress = create_check_progress_function("...")
154
+ ```
155
+
156
+ Users call `check_progress()` (no arguments) to print a per-lab and per-exercise breakdown of their submissions plus a challenge-wide aggregate.
157
+
158
+ If your challenge is a team challenge, you should also export a `join_team` function so users can register with a team when they start.
159
+
160
+ ```python
161
+ from qc_grader.grader.grade import create_join_team_function
162
+
163
+ # Replace the string with the name of your challenge
164
+ join_team = create_join_team_function("...")
165
+ ```
166
+
167
+ Users must call `join_team()` with their team name to participate in a team challenge. They can switch teams any time.
168
+
169
+ You may find it easier to copy an existing challenge and modify it.
170
+
171
+ ## Adding new labs
172
+
173
+ A *lab* is a single Python file corresponding to a Jupyter notebook that users receive. Each *challenge* has one or more labs. When you add new exercises to the server, add a matching Python file here so that users can call grading functions from their Jupyter notebooks.
174
+
175
+ Create `qc_grader/challenges/{challenge}/{lab}.py`, e.g. `qc_grader/challenges/qgss_2027/lab1.py`.
176
+
177
+ The `_CHALLENGE` and `_LAB` constants, and each exercise string (e.g., `"ex1"`), must exactly match the identifiers configured on the server. These are permanent: once a challenge is live, changing them breaks existing notebook submissions.
178
+
179
+ A minimal lab file:
180
+
181
+ ```python
182
+ # qc_grader/challenges/qgss_2027/lab1.py
183
+ from typing import Any
184
+
185
+ from typeguard import typechecked
186
+
187
+ from qc_grader.grader.grade import grade_answer
188
+
189
+ _CHALLENGE = "qgss_2027"
190
+ _LAB = "lab1"
191
+
192
+
193
+ def _grade(answer: Any, exercise: str) -> None:
194
+ grade_answer(answer, lab=_LAB, exercise=exercise, challenge=_CHALLENGE)
195
+
196
+
197
+ @typechecked
198
+ def grade_lab1_ex1(answer: str) -> None:
199
+ _grade(answer, "ex1")
200
+
201
+
202
+ @typechecked
203
+ def grade_lab1_ex2(answer: int) -> None:
204
+ _grade(answer, "ex2")
205
+ ```
206
+
207
+ Then, export every grading function from the challenge package's `__init__.py`:
208
+
209
+ ```python
210
+ # qc_grader/challenges/qgss_2027/__init__.py
211
+ from .lab1 import grade_lab1_ex1, grade_lab1_ex2
212
+
213
+ __all__ = ["grade_lab1_ex1", "grade_lab1_ex2"]
214
+ ```
215
+
216
+ Users can then import your functions like this:
217
+
218
+ ```python
219
+ from qc_grader.challenges.qgss_2027 import grade_lab1_ex1
220
+ ```
221
+
222
+ ### Type validation
223
+
224
+ All grading functions must use the `@typechecked` decorator from `typeguard` and precise type hints on the answer parameter. This lets the client reject submissions with the wrong data type before they reach the server.
225
+
226
+ Use the most specific type that describes what the user should submit — `QuantumCircuit`, `Statevector`, `int`, `float`, etc. Avoid `Any` or bare `dict` and bare `list`.
227
+
228
+ ```python
229
+ from typeguard import typechecked
230
+
231
+ @typechecked
232
+ def grade_lab1_ex1(arg1: str, arg2: list[int], arg3: QuantumCircuit) -> None:
233
+ ...
234
+ ```
235
+
236
+ #### Dictionaries with required keys
237
+
238
+ If the user submits a dictionary with specific keys, use `typing.TypedDict` rather than a generic `dict`. `TypedDict` allows `typechecked` to validate each key's name and type:
239
+
240
+ ```python
241
+ from typing import TypedDict
242
+
243
+ from typeguard import typechecked
244
+
245
+ Ex1Input = TypedDict("Ex1Input", {"0": int, "1": int})
246
+
247
+ @typechecked
248
+ def grade_lab0_ex1(counts: Ex1Input) -> None:
249
+ ...
250
+ ```
251
+
252
+ #### Multiple accepted types
253
+
254
+ Use a union (`|`) to accept more than one type:
255
+
256
+ ```python
257
+ @typechecked
258
+ def grade_lab0_ex1(answer: int | float) -> None:
259
+ ...
260
+ ```
261
+
262
+ #### Flexible types with transformation
263
+
264
+ It is often helpful to accept a more flexible data type and transform it before sending to the server. When doing so, anticipate likely user mistakes and raise a `ValueError` if they violate your assumptions. For example, this accepts either a `float` or an `ndarray` (useful when users are working with NumPy) and validates that the array is a scalar:
265
+
266
+ ```python
267
+ from typeguard import typechecked
268
+
269
+ @typechecked
270
+ def grade_lab0_ex1(exp_val: np.ndarray | float) -> None:
271
+ arr = np.asarray(exp_val)
272
+ if arr.ndim != 0 and arr.size != 1:
273
+ raise ValueError(
274
+ f"exp_val must be a scalar, got shape {arr.shape}. "
275
+ f"Use result[0].data.evs (not result.data.evs) for a single expectation value."
276
+ )
277
+ exp_val = float(arr.flat[0])
278
+ _grade(exp_val, "ex1")
279
+ ```
@@ -0,0 +1,10 @@
1
+ fmt:
2
+ uv run ruff format
3
+
4
+ lint:
5
+ uv run ruff format --check
6
+ uv run ruff check
7
+ uv run ty check --error-on-warning
8
+
9
+ test:
10
+ uv run pytest