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.
- quantum_exercises-0.8.0/.github/dependabot.yml +53 -0
- quantum_exercises-0.8.0/.github/workflows/ci.yml +172 -0
- quantum_exercises-0.8.0/.github/workflows/publish-test.yml +68 -0
- quantum_exercises-0.8.0/.github/workflows/publish.yml +59 -0
- quantum_exercises-0.8.0/.github/workflows/rotate-notes.yml +159 -0
- quantum_exercises-0.8.0/.github/workflows/verify.yml +189 -0
- quantum_exercises-0.8.0/.gitignore +245 -0
- quantum_exercises-0.8.0/.gitleaks.toml +32 -0
- quantum_exercises-0.8.0/.pre-commit-config.yaml +39 -0
- quantum_exercises-0.8.0/.python-version +1 -0
- quantum_exercises-0.8.0/.vscode/extensions.json +3 -0
- quantum_exercises-0.8.0/.vscode/settings.json +22 -0
- quantum_exercises-0.8.0/CITATION.cff +42 -0
- quantum_exercises-0.8.0/CODE_OF_CONDUCT.md +43 -0
- quantum_exercises-0.8.0/CONTRIBUTING.md +361 -0
- quantum_exercises-0.8.0/LICENSE +21 -0
- quantum_exercises-0.8.0/PKG-INFO +542 -0
- quantum_exercises-0.8.0/README.md +509 -0
- quantum_exercises-0.8.0/SECURITY.md +165 -0
- quantum_exercises-0.8.0/exercises/01_environment/README.md +34 -0
- quantum_exercises-0.8.0/exercises/01_environment/check.py +63 -0
- quantum_exercises-0.8.0/exercises/01_environment/exercise.py +11 -0
- quantum_exercises-0.8.0/exercises/01_environment/hints.md +30 -0
- quantum_exercises-0.8.0/exercises/01_environment/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/01_environment/solution.py +5 -0
- quantum_exercises-0.8.0/exercises/01_environment/template.py +11 -0
- quantum_exercises-0.8.0/exercises/02_dictionaries/README.md +41 -0
- quantum_exercises-0.8.0/exercises/02_dictionaries/check.py +91 -0
- quantum_exercises-0.8.0/exercises/02_dictionaries/exercise.py +40 -0
- quantum_exercises-0.8.0/exercises/02_dictionaries/hints.md +40 -0
- quantum_exercises-0.8.0/exercises/02_dictionaries/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/02_dictionaries/solution.py +15 -0
- quantum_exercises-0.8.0/exercises/02_dictionaries/template.py +40 -0
- quantum_exercises-0.8.0/exercises/03_first_circuit/README.md +39 -0
- quantum_exercises-0.8.0/exercises/03_first_circuit/check.py +49 -0
- quantum_exercises-0.8.0/exercises/03_first_circuit/exercise.py +13 -0
- quantum_exercises-0.8.0/exercises/03_first_circuit/hints.md +28 -0
- quantum_exercises-0.8.0/exercises/03_first_circuit/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/03_first_circuit/solution.py +6 -0
- quantum_exercises-0.8.0/exercises/03_first_circuit/template.py +13 -0
- quantum_exercises-0.8.0/exercises/04_measurement/README.md +48 -0
- quantum_exercises-0.8.0/exercises/04_measurement/check.py +88 -0
- quantum_exercises-0.8.0/exercises/04_measurement/exercise.py +25 -0
- quantum_exercises-0.8.0/exercises/04_measurement/hints.md +31 -0
- quantum_exercises-0.8.0/exercises/04_measurement/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/04_measurement/solution.py +13 -0
- quantum_exercises-0.8.0/exercises/04_measurement/template.py +25 -0
- quantum_exercises-0.8.0/exercises/05_reading_counts/README.md +64 -0
- quantum_exercises-0.8.0/exercises/05_reading_counts/check.py +74 -0
- quantum_exercises-0.8.0/exercises/05_reading_counts/exercise.py +28 -0
- quantum_exercises-0.8.0/exercises/05_reading_counts/hints.md +38 -0
- quantum_exercises-0.8.0/exercises/05_reading_counts/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/05_reading_counts/solution.py +9 -0
- quantum_exercises-0.8.0/exercises/05_reading_counts/template.py +28 -0
- quantum_exercises-0.8.0/exercises/06_born_rule/README.md +68 -0
- quantum_exercises-0.8.0/exercises/06_born_rule/check.py +93 -0
- quantum_exercises-0.8.0/exercises/06_born_rule/exercise.py +29 -0
- quantum_exercises-0.8.0/exercises/06_born_rule/hints.md +33 -0
- quantum_exercises-0.8.0/exercises/06_born_rule/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/06_born_rule/solution.py +17 -0
- quantum_exercises-0.8.0/exercises/06_born_rule/template.py +29 -0
- quantum_exercises-0.8.0/exercises/07_statevector/README.md +51 -0
- quantum_exercises-0.8.0/exercises/07_statevector/check.py +73 -0
- quantum_exercises-0.8.0/exercises/07_statevector/exercise.py +15 -0
- quantum_exercises-0.8.0/exercises/07_statevector/hints.md +45 -0
- quantum_exercises-0.8.0/exercises/07_statevector/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/07_statevector/solution.py +13 -0
- quantum_exercises-0.8.0/exercises/07_statevector/template.py +15 -0
- quantum_exercises-0.8.0/exercises/08_gates_as_matrices/README.md +51 -0
- quantum_exercises-0.8.0/exercises/08_gates_as_matrices/check.py +58 -0
- quantum_exercises-0.8.0/exercises/08_gates_as_matrices/exercise.py +15 -0
- quantum_exercises-0.8.0/exercises/08_gates_as_matrices/hints.md +43 -0
- quantum_exercises-0.8.0/exercises/08_gates_as_matrices/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/08_gates_as_matrices/solution.py +9 -0
- quantum_exercises-0.8.0/exercises/08_gates_as_matrices/template.py +15 -0
- quantum_exercises-0.8.0/exercises/09_interference/README.md +69 -0
- quantum_exercises-0.8.0/exercises/09_interference/check.py +157 -0
- quantum_exercises-0.8.0/exercises/09_interference/exercise.py +24 -0
- quantum_exercises-0.8.0/exercises/09_interference/hints.md +43 -0
- quantum_exercises-0.8.0/exercises/09_interference/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/09_interference/solution.py +17 -0
- quantum_exercises-0.8.0/exercises/09_interference/template.py +24 -0
- quantum_exercises-0.8.0/exercises/10_deutsch/README.md +91 -0
- quantum_exercises-0.8.0/exercises/10_deutsch/check.py +156 -0
- quantum_exercises-0.8.0/exercises/10_deutsch/exercise.py +62 -0
- quantum_exercises-0.8.0/exercises/10_deutsch/hints.md +55 -0
- quantum_exercises-0.8.0/exercises/10_deutsch/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/10_deutsch/solution.py +43 -0
- quantum_exercises-0.8.0/exercises/10_deutsch/template.py +62 -0
- quantum_exercises-0.8.0/exercises/11_bell_entanglement/README.md +62 -0
- quantum_exercises-0.8.0/exercises/11_bell_entanglement/check.py +121 -0
- quantum_exercises-0.8.0/exercises/11_bell_entanglement/exercise.py +23 -0
- quantum_exercises-0.8.0/exercises/11_bell_entanglement/hints.md +45 -0
- quantum_exercises-0.8.0/exercises/11_bell_entanglement/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/11_bell_entanglement/solution.py +13 -0
- quantum_exercises-0.8.0/exercises/11_bell_entanglement/template.py +23 -0
- quantum_exercises-0.8.0/exercises/12_exponential_wall/README.md +114 -0
- quantum_exercises-0.8.0/exercises/12_exponential_wall/check.py +276 -0
- quantum_exercises-0.8.0/exercises/12_exponential_wall/exercise.py +47 -0
- quantum_exercises-0.8.0/exercises/12_exponential_wall/hints.md +52 -0
- quantum_exercises-0.8.0/exercises/12_exponential_wall/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/12_exponential_wall/solution.py +22 -0
- quantum_exercises-0.8.0/exercises/12_exponential_wall/template.py +47 -0
- quantum_exercises-0.8.0/exercises/13_migration/README.md +62 -0
- quantum_exercises-0.8.0/exercises/13_migration/check.py +102 -0
- quantum_exercises-0.8.0/exercises/13_migration/exercise.py +30 -0
- quantum_exercises-0.8.0/exercises/13_migration/hints.md +54 -0
- quantum_exercises-0.8.0/exercises/13_migration/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/13_migration/solution.py +18 -0
- quantum_exercises-0.8.0/exercises/13_migration/template.py +30 -0
- quantum_exercises-0.8.0/exercises/14_real_hardware/README.md +77 -0
- quantum_exercises-0.8.0/exercises/14_real_hardware/check.py +195 -0
- quantum_exercises-0.8.0/exercises/14_real_hardware/exercise.py +31 -0
- quantum_exercises-0.8.0/exercises/14_real_hardware/hints.md +43 -0
- quantum_exercises-0.8.0/exercises/14_real_hardware/meta.toml +5 -0
- quantum_exercises-0.8.0/exercises/14_real_hardware/solution.py +17 -0
- quantum_exercises-0.8.0/exercises/14_real_hardware/template.py +31 -0
- quantum_exercises-0.8.0/exercises/15_honest_reading/README.md +71 -0
- quantum_exercises-0.8.0/exercises/15_honest_reading/check.py +134 -0
- quantum_exercises-0.8.0/exercises/15_honest_reading/exercise.py +43 -0
- quantum_exercises-0.8.0/exercises/15_honest_reading/hints.md +45 -0
- quantum_exercises-0.8.0/exercises/15_honest_reading/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/15_honest_reading/solution.py +20 -0
- quantum_exercises-0.8.0/exercises/15_honest_reading/template.py +43 -0
- quantum_exercises-0.8.0/exercises/16_readout_repair/README.md +113 -0
- quantum_exercises-0.8.0/exercises/16_readout_repair/check.py +304 -0
- quantum_exercises-0.8.0/exercises/16_readout_repair/exercise.py +55 -0
- quantum_exercises-0.8.0/exercises/16_readout_repair/hints.md +54 -0
- quantum_exercises-0.8.0/exercises/16_readout_repair/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/16_readout_repair/solution.py +39 -0
- quantum_exercises-0.8.0/exercises/16_readout_repair/template.py +55 -0
- quantum_exercises-0.8.0/exercises/17_estimator/README.md +79 -0
- quantum_exercises-0.8.0/exercises/17_estimator/check.py +162 -0
- quantum_exercises-0.8.0/exercises/17_estimator/exercise.py +31 -0
- quantum_exercises-0.8.0/exercises/17_estimator/hints.md +47 -0
- quantum_exercises-0.8.0/exercises/17_estimator/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/17_estimator/solution.py +17 -0
- quantum_exercises-0.8.0/exercises/17_estimator/template.py +31 -0
- quantum_exercises-0.8.0/exercises/18_measurement_bases/README.md +74 -0
- quantum_exercises-0.8.0/exercises/18_measurement_bases/check.py +180 -0
- quantum_exercises-0.8.0/exercises/18_measurement_bases/exercise.py +33 -0
- quantum_exercises-0.8.0/exercises/18_measurement_bases/hints.md +55 -0
- quantum_exercises-0.8.0/exercises/18_measurement_bases/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/18_measurement_bases/solution.py +19 -0
- quantum_exercises-0.8.0/exercises/18_measurement_bases/template.py +33 -0
- quantum_exercises-0.8.0/exercises/19_bloch_sphere/README.md +106 -0
- quantum_exercises-0.8.0/exercises/19_bloch_sphere/check.py +446 -0
- quantum_exercises-0.8.0/exercises/19_bloch_sphere/exercise.py +53 -0
- quantum_exercises-0.8.0/exercises/19_bloch_sphere/hints.md +63 -0
- quantum_exercises-0.8.0/exercises/19_bloch_sphere/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/19_bloch_sphere/solution.py +36 -0
- quantum_exercises-0.8.0/exercises/19_bloch_sphere/template.py +53 -0
- quantum_exercises-0.8.0/exercises/20_chsh/README.md +124 -0
- quantum_exercises-0.8.0/exercises/20_chsh/check.py +216 -0
- quantum_exercises-0.8.0/exercises/20_chsh/exercise.py +48 -0
- quantum_exercises-0.8.0/exercises/20_chsh/hints.md +50 -0
- quantum_exercises-0.8.0/exercises/20_chsh/meta.toml +4 -0
- quantum_exercises-0.8.0/exercises/20_chsh/solution.py +33 -0
- quantum_exercises-0.8.0/exercises/20_chsh/template.py +48 -0
- quantum_exercises-0.8.0/notebooks/lab-1-qiskit-patterns.ipynb +344 -0
- quantum_exercises-0.8.0/notebooks/lab-2-noise.ipynb +327 -0
- quantum_exercises-0.8.0/notebooks/lab-3-dynamic-circuits.ipynb +350 -0
- quantum_exercises-0.8.0/notebooks/playground.ipynb +235 -0
- quantum_exercises-0.8.0/pyproject.toml +126 -0
- quantum_exercises-0.8.0/readme-assets/01-qx-list.png +0 -0
- quantum_exercises-0.8.0/readme-assets/02-qx-run-fail.png +0 -0
- quantum_exercises-0.8.0/readme-assets/03-qx-run-pass.png +0 -0
- quantum_exercises-0.8.0/readme-assets/04-qx-doctor.png +0 -0
- quantum_exercises-0.8.0/readme-assets/05-vscode-split.png +0 -0
- quantum_exercises-0.8.0/readme-assets/06-vscode-notebook.png +0 -0
- quantum_exercises-0.8.0/readme-assets/07-qx-watch.png +0 -0
- quantum_exercises-0.8.0/readme-assets/08-real-hardware.png +0 -0
- quantum_exercises-0.8.0/readme-assets/banner-conduct.svg +92 -0
- quantum_exercises-0.8.0/readme-assets/banner-contributing.svg +173 -0
- quantum_exercises-0.8.0/readme-assets/banner-security.svg +68 -0
- quantum_exercises-0.8.0/readme-assets/hero.svg +189 -0
- quantum_exercises-0.8.0/src/quantum_exercises/__init__.py +44 -0
- quantum_exercises-0.8.0/src/quantum_exercises/backends.py +270 -0
- quantum_exercises-0.8.0/src/quantum_exercises/checks.py +451 -0
- quantum_exercises-0.8.0/src/quantum_exercises/cli.py +831 -0
- quantum_exercises-0.8.0/src/quantum_exercises/doctor.py +342 -0
- quantum_exercises-0.8.0/src/quantum_exercises/errors.py +395 -0
- quantum_exercises-0.8.0/src/quantum_exercises/registry.py +303 -0
- quantum_exercises-0.8.0/src/quantum_exercises/runner.py +341 -0
- quantum_exercises-0.8.0/src/quantum_exercises/state.py +310 -0
- quantum_exercises-0.8.0/src/quantum_exercises/theme.py +189 -0
- quantum_exercises-0.8.0/src/quantum_exercises/ui.py +497 -0
- quantum_exercises-0.8.0/src/quantum_exercises/watch.py +115 -0
- quantum_exercises-0.8.0/src/quantum_exercises/worker.py +365 -0
- quantum_exercises-0.8.0/tests/conftest.py +66 -0
- quantum_exercises-0.8.0/tests/test_backends.py +474 -0
- quantum_exercises-0.8.0/tests/test_checks.py +380 -0
- quantum_exercises-0.8.0/tests/test_cli.py +192 -0
- quantum_exercises-0.8.0/tests/test_cli_paths.py +890 -0
- quantum_exercises-0.8.0/tests/test_diagnostics.py +1404 -0
- quantum_exercises-0.8.0/tests/test_doctor.py +529 -0
- quantum_exercises-0.8.0/tests/test_errors.py +266 -0
- quantum_exercises-0.8.0/tests/test_exercises.py +294 -0
- quantum_exercises-0.8.0/tests/test_init.py +244 -0
- quantum_exercises-0.8.0/tests/test_notebook.py +95 -0
- quantum_exercises-0.8.0/tests/test_packaging.py +542 -0
- quantum_exercises-0.8.0/tests/test_registry.py +313 -0
- quantum_exercises-0.8.0/tests/test_runner.py +429 -0
- quantum_exercises-0.8.0/tests/test_state.py +338 -0
- quantum_exercises-0.8.0/tests/test_theme.py +331 -0
- quantum_exercises-0.8.0/tests/test_translations.py +188 -0
- quantum_exercises-0.8.0/tests/test_ui.py +305 -0
- quantum_exercises-0.8.0/tests/test_watch.py +256 -0
- quantum_exercises-0.8.0/tests/test_worker.py +262 -0
- 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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
59
|
+
.replace(/"/g, '"').replace(/'|'/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
|