quantum-exercises 0.8.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 (210) hide show
  1. quantum_exercises-0.8.0/.github/dependabot.yml +53 -0
  2. quantum_exercises-0.8.0/.github/workflows/ci.yml +172 -0
  3. quantum_exercises-0.8.0/.github/workflows/publish-test.yml +68 -0
  4. quantum_exercises-0.8.0/.github/workflows/publish.yml +59 -0
  5. quantum_exercises-0.8.0/.github/workflows/rotate-notes.yml +159 -0
  6. quantum_exercises-0.8.0/.github/workflows/verify.yml +189 -0
  7. quantum_exercises-0.8.0/.gitignore +245 -0
  8. quantum_exercises-0.8.0/.gitleaks.toml +32 -0
  9. quantum_exercises-0.8.0/.pre-commit-config.yaml +39 -0
  10. quantum_exercises-0.8.0/.python-version +1 -0
  11. quantum_exercises-0.8.0/.vscode/extensions.json +3 -0
  12. quantum_exercises-0.8.0/.vscode/settings.json +22 -0
  13. quantum_exercises-0.8.0/CITATION.cff +42 -0
  14. quantum_exercises-0.8.0/CODE_OF_CONDUCT.md +43 -0
  15. quantum_exercises-0.8.0/CONTRIBUTING.md +361 -0
  16. quantum_exercises-0.8.0/LICENSE +21 -0
  17. quantum_exercises-0.8.0/PKG-INFO +542 -0
  18. quantum_exercises-0.8.0/README.md +509 -0
  19. quantum_exercises-0.8.0/SECURITY.md +165 -0
  20. quantum_exercises-0.8.0/exercises/01_environment/README.md +34 -0
  21. quantum_exercises-0.8.0/exercises/01_environment/check.py +63 -0
  22. quantum_exercises-0.8.0/exercises/01_environment/exercise.py +11 -0
  23. quantum_exercises-0.8.0/exercises/01_environment/hints.md +30 -0
  24. quantum_exercises-0.8.0/exercises/01_environment/meta.toml +4 -0
  25. quantum_exercises-0.8.0/exercises/01_environment/solution.py +5 -0
  26. quantum_exercises-0.8.0/exercises/01_environment/template.py +11 -0
  27. quantum_exercises-0.8.0/exercises/02_dictionaries/README.md +41 -0
  28. quantum_exercises-0.8.0/exercises/02_dictionaries/check.py +91 -0
  29. quantum_exercises-0.8.0/exercises/02_dictionaries/exercise.py +40 -0
  30. quantum_exercises-0.8.0/exercises/02_dictionaries/hints.md +40 -0
  31. quantum_exercises-0.8.0/exercises/02_dictionaries/meta.toml +4 -0
  32. quantum_exercises-0.8.0/exercises/02_dictionaries/solution.py +15 -0
  33. quantum_exercises-0.8.0/exercises/02_dictionaries/template.py +40 -0
  34. quantum_exercises-0.8.0/exercises/03_first_circuit/README.md +39 -0
  35. quantum_exercises-0.8.0/exercises/03_first_circuit/check.py +49 -0
  36. quantum_exercises-0.8.0/exercises/03_first_circuit/exercise.py +13 -0
  37. quantum_exercises-0.8.0/exercises/03_first_circuit/hints.md +28 -0
  38. quantum_exercises-0.8.0/exercises/03_first_circuit/meta.toml +4 -0
  39. quantum_exercises-0.8.0/exercises/03_first_circuit/solution.py +6 -0
  40. quantum_exercises-0.8.0/exercises/03_first_circuit/template.py +13 -0
  41. quantum_exercises-0.8.0/exercises/04_measurement/README.md +48 -0
  42. quantum_exercises-0.8.0/exercises/04_measurement/check.py +88 -0
  43. quantum_exercises-0.8.0/exercises/04_measurement/exercise.py +25 -0
  44. quantum_exercises-0.8.0/exercises/04_measurement/hints.md +31 -0
  45. quantum_exercises-0.8.0/exercises/04_measurement/meta.toml +4 -0
  46. quantum_exercises-0.8.0/exercises/04_measurement/solution.py +13 -0
  47. quantum_exercises-0.8.0/exercises/04_measurement/template.py +25 -0
  48. quantum_exercises-0.8.0/exercises/05_reading_counts/README.md +64 -0
  49. quantum_exercises-0.8.0/exercises/05_reading_counts/check.py +74 -0
  50. quantum_exercises-0.8.0/exercises/05_reading_counts/exercise.py +28 -0
  51. quantum_exercises-0.8.0/exercises/05_reading_counts/hints.md +38 -0
  52. quantum_exercises-0.8.0/exercises/05_reading_counts/meta.toml +4 -0
  53. quantum_exercises-0.8.0/exercises/05_reading_counts/solution.py +9 -0
  54. quantum_exercises-0.8.0/exercises/05_reading_counts/template.py +28 -0
  55. quantum_exercises-0.8.0/exercises/06_born_rule/README.md +68 -0
  56. quantum_exercises-0.8.0/exercises/06_born_rule/check.py +93 -0
  57. quantum_exercises-0.8.0/exercises/06_born_rule/exercise.py +29 -0
  58. quantum_exercises-0.8.0/exercises/06_born_rule/hints.md +33 -0
  59. quantum_exercises-0.8.0/exercises/06_born_rule/meta.toml +4 -0
  60. quantum_exercises-0.8.0/exercises/06_born_rule/solution.py +17 -0
  61. quantum_exercises-0.8.0/exercises/06_born_rule/template.py +29 -0
  62. quantum_exercises-0.8.0/exercises/07_statevector/README.md +51 -0
  63. quantum_exercises-0.8.0/exercises/07_statevector/check.py +73 -0
  64. quantum_exercises-0.8.0/exercises/07_statevector/exercise.py +15 -0
  65. quantum_exercises-0.8.0/exercises/07_statevector/hints.md +45 -0
  66. quantum_exercises-0.8.0/exercises/07_statevector/meta.toml +4 -0
  67. quantum_exercises-0.8.0/exercises/07_statevector/solution.py +13 -0
  68. quantum_exercises-0.8.0/exercises/07_statevector/template.py +15 -0
  69. quantum_exercises-0.8.0/exercises/08_gates_as_matrices/README.md +51 -0
  70. quantum_exercises-0.8.0/exercises/08_gates_as_matrices/check.py +58 -0
  71. quantum_exercises-0.8.0/exercises/08_gates_as_matrices/exercise.py +15 -0
  72. quantum_exercises-0.8.0/exercises/08_gates_as_matrices/hints.md +43 -0
  73. quantum_exercises-0.8.0/exercises/08_gates_as_matrices/meta.toml +4 -0
  74. quantum_exercises-0.8.0/exercises/08_gates_as_matrices/solution.py +9 -0
  75. quantum_exercises-0.8.0/exercises/08_gates_as_matrices/template.py +15 -0
  76. quantum_exercises-0.8.0/exercises/09_interference/README.md +69 -0
  77. quantum_exercises-0.8.0/exercises/09_interference/check.py +157 -0
  78. quantum_exercises-0.8.0/exercises/09_interference/exercise.py +24 -0
  79. quantum_exercises-0.8.0/exercises/09_interference/hints.md +43 -0
  80. quantum_exercises-0.8.0/exercises/09_interference/meta.toml +4 -0
  81. quantum_exercises-0.8.0/exercises/09_interference/solution.py +17 -0
  82. quantum_exercises-0.8.0/exercises/09_interference/template.py +24 -0
  83. quantum_exercises-0.8.0/exercises/10_deutsch/README.md +91 -0
  84. quantum_exercises-0.8.0/exercises/10_deutsch/check.py +156 -0
  85. quantum_exercises-0.8.0/exercises/10_deutsch/exercise.py +62 -0
  86. quantum_exercises-0.8.0/exercises/10_deutsch/hints.md +55 -0
  87. quantum_exercises-0.8.0/exercises/10_deutsch/meta.toml +4 -0
  88. quantum_exercises-0.8.0/exercises/10_deutsch/solution.py +43 -0
  89. quantum_exercises-0.8.0/exercises/10_deutsch/template.py +62 -0
  90. quantum_exercises-0.8.0/exercises/11_bell_entanglement/README.md +62 -0
  91. quantum_exercises-0.8.0/exercises/11_bell_entanglement/check.py +121 -0
  92. quantum_exercises-0.8.0/exercises/11_bell_entanglement/exercise.py +23 -0
  93. quantum_exercises-0.8.0/exercises/11_bell_entanglement/hints.md +45 -0
  94. quantum_exercises-0.8.0/exercises/11_bell_entanglement/meta.toml +4 -0
  95. quantum_exercises-0.8.0/exercises/11_bell_entanglement/solution.py +13 -0
  96. quantum_exercises-0.8.0/exercises/11_bell_entanglement/template.py +23 -0
  97. quantum_exercises-0.8.0/exercises/12_exponential_wall/README.md +114 -0
  98. quantum_exercises-0.8.0/exercises/12_exponential_wall/check.py +276 -0
  99. quantum_exercises-0.8.0/exercises/12_exponential_wall/exercise.py +47 -0
  100. quantum_exercises-0.8.0/exercises/12_exponential_wall/hints.md +52 -0
  101. quantum_exercises-0.8.0/exercises/12_exponential_wall/meta.toml +4 -0
  102. quantum_exercises-0.8.0/exercises/12_exponential_wall/solution.py +22 -0
  103. quantum_exercises-0.8.0/exercises/12_exponential_wall/template.py +47 -0
  104. quantum_exercises-0.8.0/exercises/13_migration/README.md +62 -0
  105. quantum_exercises-0.8.0/exercises/13_migration/check.py +102 -0
  106. quantum_exercises-0.8.0/exercises/13_migration/exercise.py +30 -0
  107. quantum_exercises-0.8.0/exercises/13_migration/hints.md +54 -0
  108. quantum_exercises-0.8.0/exercises/13_migration/meta.toml +4 -0
  109. quantum_exercises-0.8.0/exercises/13_migration/solution.py +18 -0
  110. quantum_exercises-0.8.0/exercises/13_migration/template.py +30 -0
  111. quantum_exercises-0.8.0/exercises/14_real_hardware/README.md +77 -0
  112. quantum_exercises-0.8.0/exercises/14_real_hardware/check.py +195 -0
  113. quantum_exercises-0.8.0/exercises/14_real_hardware/exercise.py +31 -0
  114. quantum_exercises-0.8.0/exercises/14_real_hardware/hints.md +43 -0
  115. quantum_exercises-0.8.0/exercises/14_real_hardware/meta.toml +5 -0
  116. quantum_exercises-0.8.0/exercises/14_real_hardware/solution.py +17 -0
  117. quantum_exercises-0.8.0/exercises/14_real_hardware/template.py +31 -0
  118. quantum_exercises-0.8.0/exercises/15_honest_reading/README.md +71 -0
  119. quantum_exercises-0.8.0/exercises/15_honest_reading/check.py +134 -0
  120. quantum_exercises-0.8.0/exercises/15_honest_reading/exercise.py +43 -0
  121. quantum_exercises-0.8.0/exercises/15_honest_reading/hints.md +45 -0
  122. quantum_exercises-0.8.0/exercises/15_honest_reading/meta.toml +4 -0
  123. quantum_exercises-0.8.0/exercises/15_honest_reading/solution.py +20 -0
  124. quantum_exercises-0.8.0/exercises/15_honest_reading/template.py +43 -0
  125. quantum_exercises-0.8.0/exercises/16_readout_repair/README.md +113 -0
  126. quantum_exercises-0.8.0/exercises/16_readout_repair/check.py +304 -0
  127. quantum_exercises-0.8.0/exercises/16_readout_repair/exercise.py +55 -0
  128. quantum_exercises-0.8.0/exercises/16_readout_repair/hints.md +54 -0
  129. quantum_exercises-0.8.0/exercises/16_readout_repair/meta.toml +4 -0
  130. quantum_exercises-0.8.0/exercises/16_readout_repair/solution.py +39 -0
  131. quantum_exercises-0.8.0/exercises/16_readout_repair/template.py +55 -0
  132. quantum_exercises-0.8.0/exercises/17_estimator/README.md +79 -0
  133. quantum_exercises-0.8.0/exercises/17_estimator/check.py +162 -0
  134. quantum_exercises-0.8.0/exercises/17_estimator/exercise.py +31 -0
  135. quantum_exercises-0.8.0/exercises/17_estimator/hints.md +47 -0
  136. quantum_exercises-0.8.0/exercises/17_estimator/meta.toml +4 -0
  137. quantum_exercises-0.8.0/exercises/17_estimator/solution.py +17 -0
  138. quantum_exercises-0.8.0/exercises/17_estimator/template.py +31 -0
  139. quantum_exercises-0.8.0/exercises/18_measurement_bases/README.md +74 -0
  140. quantum_exercises-0.8.0/exercises/18_measurement_bases/check.py +180 -0
  141. quantum_exercises-0.8.0/exercises/18_measurement_bases/exercise.py +33 -0
  142. quantum_exercises-0.8.0/exercises/18_measurement_bases/hints.md +55 -0
  143. quantum_exercises-0.8.0/exercises/18_measurement_bases/meta.toml +4 -0
  144. quantum_exercises-0.8.0/exercises/18_measurement_bases/solution.py +19 -0
  145. quantum_exercises-0.8.0/exercises/18_measurement_bases/template.py +33 -0
  146. quantum_exercises-0.8.0/exercises/19_bloch_sphere/README.md +106 -0
  147. quantum_exercises-0.8.0/exercises/19_bloch_sphere/check.py +446 -0
  148. quantum_exercises-0.8.0/exercises/19_bloch_sphere/exercise.py +53 -0
  149. quantum_exercises-0.8.0/exercises/19_bloch_sphere/hints.md +63 -0
  150. quantum_exercises-0.8.0/exercises/19_bloch_sphere/meta.toml +4 -0
  151. quantum_exercises-0.8.0/exercises/19_bloch_sphere/solution.py +36 -0
  152. quantum_exercises-0.8.0/exercises/19_bloch_sphere/template.py +53 -0
  153. quantum_exercises-0.8.0/exercises/20_chsh/README.md +124 -0
  154. quantum_exercises-0.8.0/exercises/20_chsh/check.py +216 -0
  155. quantum_exercises-0.8.0/exercises/20_chsh/exercise.py +48 -0
  156. quantum_exercises-0.8.0/exercises/20_chsh/hints.md +50 -0
  157. quantum_exercises-0.8.0/exercises/20_chsh/meta.toml +4 -0
  158. quantum_exercises-0.8.0/exercises/20_chsh/solution.py +33 -0
  159. quantum_exercises-0.8.0/exercises/20_chsh/template.py +48 -0
  160. quantum_exercises-0.8.0/notebooks/lab-1-qiskit-patterns.ipynb +344 -0
  161. quantum_exercises-0.8.0/notebooks/lab-2-noise.ipynb +327 -0
  162. quantum_exercises-0.8.0/notebooks/lab-3-dynamic-circuits.ipynb +350 -0
  163. quantum_exercises-0.8.0/notebooks/playground.ipynb +235 -0
  164. quantum_exercises-0.8.0/pyproject.toml +126 -0
  165. quantum_exercises-0.8.0/readme-assets/01-qx-list.png +0 -0
  166. quantum_exercises-0.8.0/readme-assets/02-qx-run-fail.png +0 -0
  167. quantum_exercises-0.8.0/readme-assets/03-qx-run-pass.png +0 -0
  168. quantum_exercises-0.8.0/readme-assets/04-qx-doctor.png +0 -0
  169. quantum_exercises-0.8.0/readme-assets/05-vscode-split.png +0 -0
  170. quantum_exercises-0.8.0/readme-assets/06-vscode-notebook.png +0 -0
  171. quantum_exercises-0.8.0/readme-assets/07-qx-watch.png +0 -0
  172. quantum_exercises-0.8.0/readme-assets/08-real-hardware.png +0 -0
  173. quantum_exercises-0.8.0/readme-assets/banner-conduct.svg +92 -0
  174. quantum_exercises-0.8.0/readme-assets/banner-contributing.svg +173 -0
  175. quantum_exercises-0.8.0/readme-assets/banner-security.svg +68 -0
  176. quantum_exercises-0.8.0/readme-assets/hero.svg +189 -0
  177. quantum_exercises-0.8.0/src/quantum_exercises/__init__.py +44 -0
  178. quantum_exercises-0.8.0/src/quantum_exercises/backends.py +270 -0
  179. quantum_exercises-0.8.0/src/quantum_exercises/checks.py +451 -0
  180. quantum_exercises-0.8.0/src/quantum_exercises/cli.py +831 -0
  181. quantum_exercises-0.8.0/src/quantum_exercises/doctor.py +342 -0
  182. quantum_exercises-0.8.0/src/quantum_exercises/errors.py +395 -0
  183. quantum_exercises-0.8.0/src/quantum_exercises/registry.py +303 -0
  184. quantum_exercises-0.8.0/src/quantum_exercises/runner.py +341 -0
  185. quantum_exercises-0.8.0/src/quantum_exercises/state.py +310 -0
  186. quantum_exercises-0.8.0/src/quantum_exercises/theme.py +189 -0
  187. quantum_exercises-0.8.0/src/quantum_exercises/ui.py +497 -0
  188. quantum_exercises-0.8.0/src/quantum_exercises/watch.py +115 -0
  189. quantum_exercises-0.8.0/src/quantum_exercises/worker.py +365 -0
  190. quantum_exercises-0.8.0/tests/conftest.py +66 -0
  191. quantum_exercises-0.8.0/tests/test_backends.py +474 -0
  192. quantum_exercises-0.8.0/tests/test_checks.py +380 -0
  193. quantum_exercises-0.8.0/tests/test_cli.py +192 -0
  194. quantum_exercises-0.8.0/tests/test_cli_paths.py +890 -0
  195. quantum_exercises-0.8.0/tests/test_diagnostics.py +1404 -0
  196. quantum_exercises-0.8.0/tests/test_doctor.py +529 -0
  197. quantum_exercises-0.8.0/tests/test_errors.py +266 -0
  198. quantum_exercises-0.8.0/tests/test_exercises.py +294 -0
  199. quantum_exercises-0.8.0/tests/test_init.py +244 -0
  200. quantum_exercises-0.8.0/tests/test_notebook.py +95 -0
  201. quantum_exercises-0.8.0/tests/test_packaging.py +542 -0
  202. quantum_exercises-0.8.0/tests/test_registry.py +313 -0
  203. quantum_exercises-0.8.0/tests/test_runner.py +429 -0
  204. quantum_exercises-0.8.0/tests/test_state.py +338 -0
  205. quantum_exercises-0.8.0/tests/test_theme.py +331 -0
  206. quantum_exercises-0.8.0/tests/test_translations.py +188 -0
  207. quantum_exercises-0.8.0/tests/test_ui.py +305 -0
  208. quantum_exercises-0.8.0/tests/test_watch.py +256 -0
  209. quantum_exercises-0.8.0/tests/test_worker.py +262 -0
  210. quantum_exercises-0.8.0/uv.lock +3668 -0
@@ -0,0 +1,53 @@
1
+ version: 2
2
+
3
+ # This file does two jobs.
4
+ #
5
+ # The obvious one: the actions in .github/workflows are pinned to commit SHAs,
6
+ # which is the right call for supply-chain reasons and also means nothing ever
7
+ # moves them. A pull request carrying the upstream release notes is how they get
8
+ # read before they get merged.
9
+ #
10
+ # The less obvious one, and the reason the interval is monthly rather than off:
11
+ # GitHub disables a scheduled workflow in a public repository after 60 days with
12
+ # no activity. This project is meant to reach a state where nobody commits to it
13
+ # for months, which is exactly the state that silently stops verify.yml. A
14
+ # monthly pull request is what keeps it running.
15
+
16
+ updates:
17
+ # Everything scheduled in this repository lands on the 1st and the 15th, and
18
+ # that includes this. `cronjob` is the field the interval requires, confirmed
19
+ # against the published Dependabot schema rather than guessed; if a Dependabot
20
+ # that predates the cron interval ever rejects it, `interval: "monthly"` is the
21
+ # fallback and already falls on the 1st.
22
+ - package-ecosystem: "github-actions"
23
+ directory: "/"
24
+ schedule:
25
+ interval: "cron"
26
+ cronjob: "23 6 1,15 * *"
27
+ timezone: "Etc/UTC"
28
+ # One pull request for all of them, not one per action.
29
+ groups:
30
+ actions:
31
+ patterns: ["*"]
32
+ commit-message:
33
+ prefix: "ci"
34
+
35
+ # Keeps uv.lock from drifting years behind the ranges in pyproject.toml.
36
+ #
37
+ # Expect these to arrive red. Bumping a pinned version makes the README badges
38
+ # and the CONTRIBUTING table disagree with what is installed, and three tests
39
+ # exist to say so. That is the tests working: in this context the docs really
40
+ # are stale, and the failing run is the list of files to update before merging.
41
+ # If the monthly touch-up is not worth it, delete this block. The one above
42
+ # already keeps the repository active on its own.
43
+ - package-ecosystem: "uv"
44
+ directory: "/"
45
+ schedule:
46
+ interval: "cron"
47
+ cronjob: "23 6 1,15 * *"
48
+ timezone: "Etc/UTC"
49
+ groups:
50
+ dependencies:
51
+ patterns: ["*"]
52
+ commit-message:
53
+ prefix: "deps"
@@ -0,0 +1,172 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions: {}
10
+
11
+ concurrency:
12
+ group: ci-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ env:
16
+ # Never let CI submit a job to a real QPU, whatever credentials exist.
17
+ QX_OFFLINE: "1"
18
+
19
+ jobs:
20
+ lint:
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: read
24
+ steps:
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
27
+ with:
28
+ enable-cache: true
29
+ python-version: "3.13"
30
+ - run: uv sync --locked --all-extras --dev
31
+ - run: uv run ruff check .
32
+ - run: uv run ruff format --check .
33
+
34
+ # pre-commit is opt-in per clone, so the secret scan has to run here too.
35
+ # The pre-commit hook runs `gitleaks git --staged`, which only sees what is
36
+ # about to be committed; on a fresh CI checkout nothing is staged, so it would
37
+ # scan nothing at all. `gitleaks dir` scans the tree, which is what is wanted.
38
+ secrets:
39
+ runs-on: ubuntu-latest
40
+ permissions:
41
+ contents: read
42
+ env:
43
+ GITLEAKS_VERSION: 8.30.1
44
+ steps:
45
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
+ - name: Install gitleaks
47
+ run: |
48
+ set -euo pipefail
49
+ curl -sSfL -o /tmp/gitleaks.tar.gz \
50
+ "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
51
+ tar -xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks
52
+ - name: Scan for hardcoded secrets
53
+ run: gitleaks dir . --config .gitleaks.toml --redact --no-banner --verbose
54
+
55
+ test:
56
+ runs-on: ubuntu-latest
57
+ permissions:
58
+ contents: read
59
+ strategy:
60
+ fail-fast: false
61
+ matrix:
62
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
63
+ steps:
64
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
65
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
66
+ with:
67
+ enable-cache: true
68
+ python-version: ${{ matrix.python-version }}
69
+ - run: uv sync --locked --all-extras --dev
70
+ - run: uv run pytest -q
71
+
72
+ # The suite covers every line of src/, and only a gate keeps it that way.
73
+ # coverage is pulled in for these commands alone, so it never reaches
74
+ # pyproject.toml, uv.lock, or the package counts CONTRIBUTING documents.
75
+ # The threshold and the subprocess setting live in [tool.coverage] there.
76
+ coverage:
77
+ runs-on: ubuntu-latest
78
+ permissions:
79
+ contents: read
80
+ steps:
81
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
82
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
83
+ with:
84
+ enable-cache: true
85
+ python-version: "3.13"
86
+ - run: uv sync --locked --all-extras --dev
87
+ - run: uv run --with coverage python -m coverage run -m pytest -q
88
+ # parallel mode writes one file per process, including every worker.
89
+ - run: uv run --with coverage python -m coverage combine
90
+ - run: uv run --with coverage python -m coverage report
91
+
92
+ # The README tells a learner to install with `uv tool install --editable .`, and
93
+ # nothing else here runs that path. It is also what proves the claim that qx
94
+ # finds the exercises from any directory.
95
+ install:
96
+ runs-on: ubuntu-latest
97
+ permissions:
98
+ contents: read
99
+ steps:
100
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
101
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
102
+ with:
103
+ enable-cache: true
104
+ python-version: "3.13"
105
+ - name: Install the way the README says
106
+ run: |
107
+ set -euo pipefail
108
+ uv tool install --editable .
109
+ # Asked for rather than assumed: the location is uv's to decide.
110
+ uv tool dir --bin >> "$GITHUB_PATH"
111
+ - name: Run it from outside the repository
112
+ run: |
113
+ set -euo pipefail
114
+ cd "$HOME"
115
+ qx version
116
+ qx doctor
117
+ qx list
118
+
119
+ # The other way in, and the one nothing else covers: installing the built
120
+ # wheel, with no repository anywhere. That path only works if the wheel really
121
+ # carries the course and `qx init` really copies it somewhere writable, so this
122
+ # job takes a learner all the way to a passing exercise rather than stopping at
123
+ # an exit code.
124
+ wheel:
125
+ runs-on: ubuntu-latest
126
+ permissions:
127
+ contents: read
128
+ steps:
129
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
130
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
131
+ with:
132
+ enable-cache: true
133
+ python-version: "3.13"
134
+ - name: Build the wheel
135
+ run: uv build --wheel --out-dir dist
136
+ - name: Install it into an environment of its own
137
+ run: |
138
+ set -euo pipefail
139
+ uv venv "$HOME/wheel-env" --python 3.13
140
+ VIRTUAL_ENV="$HOME/wheel-env" uv pip install dist/*.whl
141
+ - name: Take a learner through it, with no repository in sight
142
+ run: |
143
+ set -euo pipefail
144
+ qx="$HOME/wheel-env/bin/qx"
145
+ mkdir -p "$HOME/elsewhere"
146
+ cd "$HOME/elsewhere"
147
+
148
+ # Before anything is copied there is no course, and the message has to
149
+ # name the command that makes one rather than a repository to go and find.
150
+ if "$qx" list; then echo "expected qx list to fail before init"; exit 1; fi
151
+
152
+ # Captured, not piped into grep. `qx list` exits 2 here by design, and
153
+ # under pipefail that status is what the whole pipeline reports even
154
+ # though grep found what it was looking for. The check passed and the
155
+ # step failed anyway.
156
+ said=$("$qx" list 2>&1 || true)
157
+ case "$said" in
158
+ *init*) ;;
159
+ *) echo "the pre-init message does not name init: $said"; exit 1 ;;
160
+ esac
161
+
162
+ "$qx" init course
163
+ cd course
164
+ "$qx" doctor
165
+ "$qx" list
166
+ cp exercises/01_environment/solution.py exercises/01_environment/exercise.py
167
+ "$qx" run 1
168
+
169
+ # Running it again is the upgrade path, and it may not touch an answer.
170
+ cd ..
171
+ "$qx" init course
172
+ grep -q "__version__" course/exercises/01_environment/exercise.py
@@ -0,0 +1,68 @@
1
+ name: publish-test
2
+
3
+ # A rehearsal of publish.yml against TestPyPI, which is a throwaway copy of PyPI.
4
+ #
5
+ # Same build, same guard, same trusted publishing, different endpoint. The point
6
+ # is to find out whether the upload works before the version number is spent:
7
+ # an index accepts a given version exactly once, so a first attempt on the real
8
+ # PyPI is also the last one.
9
+ #
10
+ # Run by hand only. There is no release trigger here, so nothing reaches TestPyPI
11
+ # unless someone asks for it from the Actions tab.
12
+ #
13
+ # The PyPI side is a one time thing, under the project's Publishing settings on
14
+ # test.pypi.org: owner `TuguiDragos`, repository `quantum-exercises`, workflow
15
+ # `publish-test.yml`, environment `testpypi`. Until that exists the job fails at
16
+ # the upload step and changes nothing.
17
+ #
18
+ # Installing the result needs the real PyPI as well, because qiskit and the rest
19
+ # are not on TestPyPI:
20
+ #
21
+ # uv pip install quantum-exercises \
22
+ # --index-url https://test.pypi.org/simple/ \
23
+ # --extra-index-url https://pypi.org/simple/ \
24
+ # --index-strategy unsafe-best-match
25
+
26
+ on:
27
+ workflow_dispatch:
28
+
29
+ permissions: {}
30
+
31
+ jobs:
32
+ publish-test:
33
+ runs-on: ubuntu-latest
34
+ environment: testpypi
35
+ permissions:
36
+ id-token: write
37
+ steps:
38
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
39
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
40
+ with:
41
+ enable-cache: true
42
+ python-version: "3.13"
43
+
44
+ - name: Build the wheel and the sdist
45
+ run: uv build
46
+
47
+ # Kept identical to publish.yml on purpose. A rehearsal that skips the
48
+ # guard does not rehearse the thing that matters.
49
+ - name: Refuse to publish a wheel with no course in it
50
+ run: |
51
+ set -euo pipefail
52
+ python - <<'PY'
53
+ import sys, zipfile, pathlib
54
+ wheel = next(pathlib.Path("dist").glob("*.whl"))
55
+ names = zipfile.ZipFile(wheel).namelist()
56
+ exercises = {n.split("/")[3] for n in names if "/_course/exercises/" in n}
57
+ if len(exercises) < 20:
58
+ sys.exit(f"{wheel.name} carries {len(exercises)} exercises, expected 20")
59
+ if not any("/_course/notebooks/" in n for n in names):
60
+ sys.exit(f"{wheel.name} carries no notebooks")
61
+ print(f"{wheel.name}: {len(exercises)} exercises and the notebooks are in")
62
+ PY
63
+
64
+ - name: Upload to TestPyPI
65
+ run: |
66
+ uv publish --trusted-publishing always \
67
+ --publish-url https://test.pypi.org/legacy/ \
68
+ --check-url https://test.pypi.org/simple/
@@ -0,0 +1,59 @@
1
+ name: publish
2
+
3
+ # Uploads the distributions to PyPI when a release is published.
4
+ #
5
+ # No token lives anywhere. PyPI is told, once, to trust releases coming from this
6
+ # workflow in this repository, and the job asks GitHub for a short lived identity
7
+ # at run time. That is why the job needs `id-token: write` and nothing else, and
8
+ # why nobody has to rotate a secret.
9
+ #
10
+ # Setting it up on the PyPI side is a one time thing, under the project's
11
+ # Publishing settings: owner `TuguiDragos`, repository `quantum-exercises`,
12
+ # workflow `publish.yml`, environment `pypi`. Until that exists the job fails at
13
+ # the upload step and changes nothing.
14
+
15
+ on:
16
+ release:
17
+ types: [published]
18
+ workflow_dispatch:
19
+
20
+ permissions: {}
21
+
22
+ jobs:
23
+ publish:
24
+ runs-on: ubuntu-latest
25
+ # A named environment, so the upload can be held behind a review rule if that
26
+ # is ever wanted. It costs nothing while no rule is set.
27
+ environment: pypi
28
+ permissions:
29
+ id-token: write
30
+ steps:
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
33
+ with:
34
+ enable-cache: true
35
+ python-version: "3.13"
36
+
37
+ - name: Build the wheel and the sdist
38
+ run: uv build
39
+
40
+ # A wheel without the course installs cleanly and then has nothing to give
41
+ # `qx init`, which is the one packaging mistake that would reach a reader
42
+ # rather than a maintainer. Cheaper to check here than to yank a release.
43
+ - name: Refuse to publish a wheel with no course in it
44
+ run: |
45
+ set -euo pipefail
46
+ python - <<'PY'
47
+ import sys, zipfile, pathlib
48
+ wheel = next(pathlib.Path("dist").glob("*.whl"))
49
+ names = zipfile.ZipFile(wheel).namelist()
50
+ exercises = {n.split("/")[3] for n in names if "/_course/exercises/" in n}
51
+ if len(exercises) < 20:
52
+ sys.exit(f"{wheel.name} carries {len(exercises)} exercises, expected 20")
53
+ if not any("/_course/notebooks/" in n for n in names):
54
+ sys.exit(f"{wheel.name} carries no notebooks")
55
+ print(f"{wheel.name}: {len(exercises)} exercises and the notebooks are in")
56
+ PY
57
+
58
+ - name: Upload to PyPI
59
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,159 @@
1
+ name: Rotate notes
2
+
3
+ # Rewrites the block between the NOTES markers in README.md with five posts from
4
+ # the author's feed, chosen by a seeded shuffle so the set changes without ever
5
+ # repeating inside a cycle.
6
+ #
7
+ # It is also the only thing in this repository that commits on a schedule, and
8
+ # that is deliberate. GitHub disables a scheduled workflow in a public repository
9
+ # after 60 days with no activity, and this project is meant to reach a state
10
+ # where nobody touches it for months, which is exactly the state that would stop
11
+ # verify.yml without a word. A commit twice a month keeps the clock from ever
12
+ # getting close.
13
+ #
14
+ # Twice a month rather than daily, and the reason is margin rather than cost: a
15
+ # run is about a minute. Sixty days is the limit, and the widest gap between the
16
+ # 1st and the 15th is seventeen days, so two consecutive runs can fail outright
17
+ # before the clock gets anywhere near it. Runs do fail; a GitHub incident took
18
+ # out every job in this repository on the day this was written.
19
+ #
20
+ # Monthly would look tidier and halve that margin twice over: one skipped run
21
+ # puts the gap at sixty-two days, past the limit, from a single bad morning.
22
+ #
23
+ # Cron has no notion of a week of the year, so "every two weeks" is not
24
+ # expressible. Fixed days of the month are, and land close enough.
25
+
26
+ on:
27
+ schedule:
28
+ - cron: "17 4 1,15 * *" # the 1st and the 15th, 04:17 UTC
29
+ workflow_dispatch:
30
+
31
+ permissions: {}
32
+
33
+ jobs:
34
+ rotate:
35
+ runs-on: ubuntu-latest
36
+ permissions:
37
+ # The one workflow here that writes. Everything else in this repository
38
+ # runs read-only, so this is the job to look at first if the question is
39
+ # ever "what can CI change".
40
+ contents: write
41
+ steps:
42
+ # Pinned by commit, like every other action in this repository. It matters
43
+ # most here: this is the workflow holding write access, so a moved tag
44
+ # would be a moved tag on the one job that can push.
45
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
47
+ with:
48
+ node-version: "24"
49
+
50
+ - name: Pick this round's five
51
+ run: |
52
+ cat > /tmp/rotate.mjs <<'JS'
53
+ import { readFileSync, writeFileSync } from "node:fs";
54
+
55
+ const FEED = "https://tuguidragos.com/rss/";
56
+ const decode = (s) => s
57
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(+n))
58
+ .replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">")
59
+ .replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'");
60
+
61
+ const parse = (xml) => [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)].map((m) => {
62
+ const grab = (tag) => {
63
+ const re = new RegExp(`<${tag}>(?:<!\\[CDATA\\[)?([\\s\\S]*?)(?:\\]\\]>)?</${tag}>`);
64
+ return (m[1].match(re) || [])[1] || "";
65
+ };
66
+ return { title: decode(grab("title").trim()), link: grab("link").trim() };
67
+ }).filter((i) => i.title && i.link);
68
+
69
+ // Ghost paginates the feed at 15 posts, so walk pages until one runs dry.
70
+ const items = [];
71
+ const links = new Set();
72
+ for (let page = 1; page <= 30; page++) {
73
+ const url = page === 1 ? FEED : `${FEED}${page}/`;
74
+ let res;
75
+ try {
76
+ res = await fetch(url);
77
+ } catch {
78
+ break;
79
+ }
80
+ if (!res.ok) break;
81
+ const batch = parse(await res.text());
82
+ if (batch.length === 0) break;
83
+ let added = 0;
84
+ for (const it of batch) {
85
+ if (links.has(it.link)) continue;
86
+ links.add(it.link);
87
+ items.push(it);
88
+ added++;
89
+ }
90
+ if (added === 0) break;
91
+ }
92
+
93
+ if (items.length === 0) {
94
+ console.log("Feed returned no items — leaving README untouched.");
95
+ process.exit(0);
96
+ }
97
+
98
+ const PER_DAY = 5;
99
+
100
+ const shuffle = (arr, seedStr) => {
101
+ const a = arr.slice();
102
+ let seed = 2166136261;
103
+ for (const ch of seedStr) {
104
+ seed ^= ch.charCodeAt(0);
105
+ seed = Math.imul(seed, 16777619) >>> 0;
106
+ }
107
+ const rand = () => (seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0) / 4294967296;
108
+ for (let i = a.length - 1; i > 0; i--) {
109
+ const j = Math.floor(rand() * (i + 1));
110
+ [a[i], a[j]] = [a[j], a[i]];
111
+ }
112
+ return a;
113
+ };
114
+
115
+ const dayNo = Math.floor(Date.now() / 86400000);
116
+ const perCycle = Math.ceil(items.length / PER_DAY);
117
+ const cycle = Math.floor(dayNo / perCycle);
118
+ const slot = ((dayNo % perCycle) + perCycle) % perCycle;
119
+ const deck = shuffle(items, "cycle-" + cycle);
120
+
121
+ const today = [];
122
+ for (let k = 0; k < PER_DAY && k < deck.length; k++) {
123
+ today.push(deck[(slot * PER_DAY + k) % deck.length]);
124
+ }
125
+ const seen = new Set();
126
+ const picked = today.filter((i) => !seen.has(i.link) && seen.add(i.link));
127
+
128
+ // A square bracket in a post title would end the markdown link early and
129
+ // leave the rest of the title loose in the README. The titles come from a
130
+ // feed rather than from this repository, so they are escaped rather than
131
+ // trusted.
132
+ const esc = (s) => s.replace(/[[\]]/g, "\\$&");
133
+
134
+ const list = picked.map((i) => `- [${esc(i.title)}](${i.link})`).join("\n");
135
+ const file = "README.md";
136
+ const md = readFileSync(file, "utf8");
137
+ const next = md.replace(
138
+ /<!-- NOTES:START -->[\s\S]*?<!-- NOTES:END -->/,
139
+ `<!-- NOTES:START -->\n${list}\n<!-- NOTES:END -->`
140
+ );
141
+ if (next === md) {
142
+ console.log("No change.");
143
+ } else {
144
+ writeFileSync(file, next);
145
+ console.log(`Wrote ${picked.length} of ${items.length} posts (deck ${cycle}, slot ${slot + 1}/${perCycle}).`);
146
+ }
147
+ JS
148
+ node /tmp/rotate.mjs
149
+
150
+ - name: Commit if changed
151
+ run: |
152
+ git config user.name "github-actions[bot]"
153
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
154
+ git add README.md
155
+ if ! git diff --staged --quiet; then
156
+ git commit -m "chore: rotate notes" \
157
+ -m "Co-authored-by: Țugui Dragoș <contact@tuguidragos.com>"
158
+ git push
159
+ fi