linux-mirror-bench 0.1.2__tar.gz → 0.3.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 (79) hide show
  1. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.github/workflows/ci.yml +12 -12
  2. linux_mirror_bench-0.3.0/.github/workflows/integration.yml +42 -0
  3. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.github/workflows/release.yml +4 -4
  4. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.pre-commit-config.yaml +1 -0
  5. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.release-please-config.json +1 -0
  6. linux_mirror_bench-0.3.0/.release-please-manifest.json +3 -0
  7. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/AGENTS.md +16 -4
  8. linux_mirror_bench-0.3.0/CHANGELOG.md +123 -0
  9. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/CONTRIBUTING.md +1 -1
  10. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/Dockerfile +15 -6
  11. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/PKG-INFO +124 -30
  12. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/README.md +120 -28
  13. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/pyproject.toml +6 -1
  14. linux_mirror_bench-0.3.0/src/mirror_bench/__init__.py +5 -0
  15. linux_mirror_bench-0.3.0/src/mirror_bench/benchmark/h3.py +88 -0
  16. linux_mirror_bench-0.3.0/src/mirror_bench/benchmark/h3_quic.py +193 -0
  17. linux_mirror_bench-0.3.0/src/mirror_bench/benchmark/probe.py +302 -0
  18. linux_mirror_bench-0.3.0/src/mirror_bench/benchmark/scorer.py +197 -0
  19. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/benchmark/transport.py +9 -4
  20. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/cli.py +141 -32
  21. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/discovery/__init__.py +2 -1
  22. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/discovery/arch.py +21 -9
  23. linux_mirror_bench-0.3.0/src/mirror_bench/discovery/base.py +178 -0
  24. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/discovery/debian.py +26 -25
  25. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/discovery/fedora.py +96 -29
  26. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/discovery/mint.py +13 -6
  27. linux_mirror_bench-0.3.0/src/mirror_bench/discovery/ubuntu.py +113 -0
  28. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/display.py +73 -9
  29. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/distro.py +20 -26
  30. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/models.py +8 -1
  31. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/tests/fixtures/arch_status.json +17 -0
  32. linux_mirror_bench-0.3.0/tests/fixtures/debian_masterlist.txt +31 -0
  33. linux_mirror_bench-0.3.0/tests/test_cli.py +447 -0
  34. linux_mirror_bench-0.3.0/tests/test_discovery.py +654 -0
  35. linux_mirror_bench-0.3.0/tests/test_display.py +161 -0
  36. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/tests/test_distro.py +67 -1
  37. linux_mirror_bench-0.3.0/tests/test_h3.py +238 -0
  38. linux_mirror_bench-0.3.0/tests/test_integration.py +94 -0
  39. linux_mirror_bench-0.3.0/tests/test_probe.py +456 -0
  40. linux_mirror_bench-0.3.0/tests/test_scorer.py +278 -0
  41. linux_mirror_bench-0.3.0/uv.lock +739 -0
  42. linux_mirror_bench-0.1.2/.release-please-manifest.json +0 -3
  43. linux_mirror_bench-0.1.2/CHANGELOG.md +0 -61
  44. linux_mirror_bench-0.1.2/src/mirror_bench/__init__.py +0 -3
  45. linux_mirror_bench-0.1.2/src/mirror_bench/benchmark/probe.py +0 -251
  46. linux_mirror_bench-0.1.2/src/mirror_bench/benchmark/scorer.py +0 -130
  47. linux_mirror_bench-0.1.2/src/mirror_bench/discovery/base.py +0 -43
  48. linux_mirror_bench-0.1.2/src/mirror_bench/discovery/ubuntu.py +0 -112
  49. linux_mirror_bench-0.1.2/tests/fixtures/debian_masterlist.txt +0 -19
  50. linux_mirror_bench-0.1.2/tests/test_cli.py +0 -189
  51. linux_mirror_bench-0.1.2/tests/test_discovery.py +0 -272
  52. linux_mirror_bench-0.1.2/tests/test_integration.py +0 -36
  53. linux_mirror_bench-0.1.2/tests/test_probe.py +0 -123
  54. linux_mirror_bench-0.1.2/tests/test_scorer.py +0 -138
  55. linux_mirror_bench-0.1.2/uv.lock +0 -424
  56. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.dockerignore +0 -0
  57. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.editorconfig +0 -0
  58. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  59. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  60. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  61. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  62. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.github/dependabot.yml +0 -0
  63. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.github/workflows/release-please.yml +0 -0
  64. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.gitignore +0 -0
  65. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.markdownlint.yaml +0 -0
  66. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.tool-versions +0 -0
  67. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/.yamllint.yaml +0 -0
  68. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/CODEOWNERS +0 -0
  69. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/LICENSE +0 -0
  70. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/Makefile +0 -0
  71. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/SECURITY.md +0 -0
  72. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/__main__.py +0 -0
  73. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/benchmark/__init__.py +0 -0
  74. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/src/mirror_bench/benchmark/tls.py +0 -0
  75. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/tests/__init__.py +0 -0
  76. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/tests/conftest.py +0 -0
  77. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/tests/fixtures/fedora_mirrorlist.txt +0 -0
  78. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/tests/fixtures/mint_mirrors.html +0 -0
  79. {linux_mirror_bench-0.1.2 → linux_mirror_bench-0.3.0}/tests/fixtures/ubuntu_mirrors.txt +0 -0
@@ -23,8 +23,8 @@ jobs:
23
23
  name: Pre-commit
24
24
  runs-on: ubuntu-latest
25
25
  steps:
26
- - uses: actions/checkout@v6
27
- - uses: actions/setup-python@v6
26
+ - uses: actions/checkout@v7
27
+ - uses: actions/setup-python@v7
28
28
  with:
29
29
  python-version: ${{ env.PYTHON_VERSION }}
30
30
  - uses: astral-sh/setup-uv@v7
@@ -38,13 +38,13 @@ jobs:
38
38
  name: Lint + format
39
39
  runs-on: ubuntu-latest
40
40
  steps:
41
- - uses: actions/checkout@v6
41
+ - uses: actions/checkout@v7
42
42
  - uses: astral-sh/setup-uv@v7
43
43
  with:
44
44
  version: ${{ env.UV_VERSION }}
45
45
  enable-cache: true
46
46
  cache-dependency-glob: pyproject.toml
47
- - run: uv sync --frozen --all-extras || uv sync --all-extras
47
+ - run: uv sync --frozen --all-extras
48
48
  - name: Ruff check
49
49
  run: uv run ruff check --output-format=github .
50
50
  - name: Ruff format check
@@ -54,13 +54,13 @@ jobs:
54
54
  name: Mypy (strict)
55
55
  runs-on: ubuntu-latest
56
56
  steps:
57
- - uses: actions/checkout@v6
57
+ - uses: actions/checkout@v7
58
58
  - uses: astral-sh/setup-uv@v7
59
59
  with:
60
60
  version: ${{ env.UV_VERSION }}
61
61
  enable-cache: true
62
62
  cache-dependency-glob: pyproject.toml
63
- - run: uv sync --frozen --all-extras || uv sync --all-extras
63
+ - run: uv sync --frozen --all-extras
64
64
  - name: mypy
65
65
  run: uv run mypy src tests
66
66
 
@@ -72,8 +72,8 @@ jobs:
72
72
  matrix:
73
73
  python: ["3.14"]
74
74
  steps:
75
- - uses: actions/checkout@v6
76
- - uses: actions/setup-python@v6
75
+ - uses: actions/checkout@v7
76
+ - uses: actions/setup-python@v7
77
77
  with:
78
78
  python-version: ${{ matrix.python }}
79
79
  - uses: astral-sh/setup-uv@v7
@@ -81,7 +81,7 @@ jobs:
81
81
  version: ${{ env.UV_VERSION }}
82
82
  enable-cache: true
83
83
  cache-dependency-glob: pyproject.toml
84
- - run: uv sync --frozen --all-extras || uv sync --all-extras
84
+ - run: uv sync --frozen --all-extras
85
85
  - name: pytest
86
86
  run: uv run pytest -v --tb=short
87
87
  - name: Smoke-test the built CLI
@@ -93,13 +93,13 @@ jobs:
93
93
  name: Security scan (bandit + pip-audit)
94
94
  runs-on: ubuntu-latest
95
95
  steps:
96
- - uses: actions/checkout@v6
96
+ - uses: actions/checkout@v7
97
97
  - uses: astral-sh/setup-uv@v7
98
98
  with:
99
99
  version: ${{ env.UV_VERSION }}
100
100
  enable-cache: true
101
101
  cache-dependency-glob: pyproject.toml
102
- - run: uv sync --frozen --all-extras || uv sync --all-extras
102
+ - run: uv sync --frozen --all-extras
103
103
  - name: Bandit
104
104
  run: uv run --with "bandit[toml]" bandit -c pyproject.toml -r src
105
105
  - name: pip-audit (runtime deps)
@@ -114,7 +114,7 @@ jobs:
114
114
  contents: read
115
115
  security-events: write
116
116
  steps:
117
- - uses: actions/checkout@v6
117
+ - uses: actions/checkout@v7
118
118
  - uses: docker/setup-buildx-action@v4
119
119
  - name: Build image
120
120
  uses: docker/build-push-action@v7
@@ -0,0 +1,42 @@
1
+ ---
2
+ # Runs the opt-in integration tests against real mirror infrastructure.
3
+ # Mirror lists, file paths, and release names change upstream without notice;
4
+ # this is the only job that sees it. It is not a PR gate, because a failure
5
+ # here is usually upstream drift, not a defect in the change under review.
6
+
7
+ name: Integration
8
+
9
+ on:
10
+ schedule:
11
+ - cron: "17 6 * * 1"
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ concurrency:
18
+ group: ${{ github.workflow }}
19
+ cancel-in-progress: true
20
+
21
+ env:
22
+ PYTHON_VERSION: "3.14"
23
+ UV_VERSION: "0.11.7"
24
+
25
+ jobs:
26
+ integration:
27
+ name: Live mirrors
28
+ runs-on: ubuntu-latest
29
+ timeout-minutes: 15
30
+ steps:
31
+ - uses: actions/checkout@v7
32
+ - uses: actions/setup-python@v7
33
+ with:
34
+ python-version: ${{ env.PYTHON_VERSION }}
35
+ - uses: astral-sh/setup-uv@v7
36
+ with:
37
+ version: ${{ env.UV_VERSION }}
38
+ enable-cache: true
39
+ cache-dependency-glob: pyproject.toml
40
+ - run: uv sync --frozen --all-extras
41
+ - name: pytest (integration)
42
+ run: make test-integration
@@ -31,8 +31,8 @@ jobs:
31
31
  outputs:
32
32
  version: ${{ steps.version.outputs.value }}
33
33
  steps:
34
- - uses: actions/checkout@v6
35
- - uses: actions/setup-python@v6
34
+ - uses: actions/checkout@v7
35
+ - uses: actions/setup-python@v7
36
36
  with:
37
37
  python-version: ${{ env.PYTHON_VERSION }}
38
38
  - uses: astral-sh/setup-uv@v7
@@ -94,7 +94,7 @@ jobs:
94
94
  id-token: write
95
95
  attestations: write
96
96
  steps:
97
- - uses: actions/checkout@v6
97
+ - uses: actions/checkout@v7
98
98
  - uses: docker/setup-qemu-action@v4
99
99
  - uses: docker/setup-buildx-action@v4
100
100
  - uses: docker/login-action@v4
@@ -143,7 +143,7 @@ jobs:
143
143
  permissions:
144
144
  contents: write
145
145
  steps:
146
- - uses: actions/checkout@v6
146
+ - uses: actions/checkout@v7
147
147
  - uses: actions/download-artifact@v8
148
148
  with:
149
149
  name: dist
@@ -44,6 +44,7 @@ repos:
44
44
  - id: mypy
45
45
  additional_dependencies:
46
46
  - httpx
47
+ - aioquic
47
48
  - typer
48
49
  - rich
49
50
  - pytest
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "release-type": "python",
3
+ "bump-minor-pre-major": true,
3
4
  "packages": {
4
5
  ".": {}
5
6
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.0"
3
+ }
@@ -9,21 +9,26 @@ this repo. Humans should read `README.md` + `CONTRIBUTING.md` instead.
9
9
  src/mirror_bench/
10
10
  cli.py typer app, subcommands `bench`, `list`, `completion`
11
11
  distro.py /etc/os-release parsing + derivative alias table +
12
- structured exceptions (UnsupportedDistroError, etc.)
12
+ default codenames + structured exceptions
13
+ (UnsupportedDistroError, etc.)
13
14
  models.py Mirror, ProbeResult, Score, Weights, BenchConfig, HostInfo
14
15
  discovery/ per-distro mirror fetchers (ABC + 5 impls +
15
- UnknownDiscovererError)
16
+ DiscoveryError, UnknownDiscovererError)
16
17
  benchmark/
17
18
  transport.py shared httpx.AsyncClient factory
18
19
  probe.py two-phase async orchestration (TaskGroup + semaphore)
19
20
  tls.py TLS version / cert inspection via response.extensions
21
+ h3.py HTTP/3 discovery from the Alt-Svc header + optional
22
+ QUIC probe orchestration (--http3)
23
+ h3_quic.py aioquic client; imported only through h3.py (optional
24
+ `http3` extra, not a runtime dependency)
20
25
  scorer.py weighted composite ranking
21
26
  display.py rich.Table + JSON/CSV emitters
22
27
 
23
28
  tests/
24
29
  fixtures/ captured mirror-list bodies per distro
25
30
  test_*.py respx-mocked units + @pytest.mark.integration opt-ins
26
- (45 tests, 2 integration-gated)
31
+ (integration.yml runs the opt-ins each week)
27
32
  ```
28
33
 
29
34
  ## Commands (use these, not ad-hoc ones)
@@ -87,6 +92,9 @@ Run a single test: `uv run pytest tests/test_scorer.py::test_weights_shift_ranki
87
92
  ## Adding a new discoverer
88
93
 
89
94
  1. `src/mirror_bench/discovery/<name>.py` implementing `MirrorDiscoverer`.
95
+ `probe_path` must name a small file that upstream rewrites as packages
96
+ change, and `sync_time` must read its generation time, or stale mirrors
97
+ cannot be detected.
90
98
  2. Register in `discovery/__init__.py::DISCOVERERS`.
91
99
  3. Add ID aliases in `distro.py::_ALIASES`.
92
100
  4. Respx-mocked test in `tests/test_discovery.py` + fixture file under
@@ -100,7 +108,8 @@ Run a single test: `uv run pytest tests/test_scorer.py::test_weights_shift_ranki
100
108
  - Don't skip hooks (`--no-verify`), nor add `# type: ignore` / `# noqa` /
101
109
  `# nosec` without a one-line reason at the call site.
102
110
  - Don't introduce new runtime dependencies without a strong reason — three
103
- (`httpx[http2]`, `typer`, `rich`) is the budget.
111
+ (`httpx[http2]`, `typer`, `rich`) is the budget. `aioquic` is an optional
112
+ extra (`http3`): import it only in `benchmark/h3_quic.py`.
104
113
  - Don't expand `[tool.ruff.lint] select` without a concrete bug class in mind.
105
114
  - Don't emit rich output to stdout when `--json` or `--csv` is set.
106
115
  - Don't rely on network access in unit tests. Network-touching tests must be
@@ -111,6 +120,9 @@ Run a single test: `uv run pytest tests/test_scorer.py::test_weights_shift_ranki
111
120
  - **Mypy error.** Fix the types. `# type: ignore` is not allowed in this
112
121
  codebase — if you think you need one, you're probably modelling something
113
122
  wrong.
123
+ - **Scheduled integration run fails.** First check for upstream drift: a
124
+ moved file, a renamed list field, an end-of-life default release. Fix the
125
+ path or default and refresh the fixture to match the live format.
114
126
  - **Integration test flaky.** Real mirrors come and go. If reproducible,
115
127
  tighten the assertion or bump tolerance. Don't add retries to runtime code
116
128
  just to paper over a test.
@@ -0,0 +1,123 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
+ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0](https://github.com/MysticRyuujin/mirror-bench/compare/v0.2.0...v0.3.0) (2026-11-13)
9
+
10
+
11
+ ### Features
12
+
13
+ * measure mirror freshness and drop stale mirrors ([#18](https://github.com/MysticRyuujin/mirror-bench/issues/18)) ([462ac42](https://github.com/MysticRyuujin/mirror-bench/commit/462ac42017f1026c6892701aaa0452b791a0c498))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * make completion work without click, and reject error pages as probes ([#19](https://github.com/MysticRyuujin/mirror-bench/issues/19)) ([0c2427d](https://github.com/MysticRyuujin/mirror-bench/commit/0c2427dcb949f394e0118a386407d215d732465b))
19
+ * try https for mirrors listed as http, and write plain output with no TTY ([#17](https://github.com/MysticRyuujin/mirror-bench/issues/17)) ([07d38ee](https://github.com/MysticRyuujin/mirror-bench/commit/07d38eec00a0c5669a728723a13a9d805f6113c8))
20
+
21
+
22
+ ### Dependencies
23
+
24
+ * **actions:** match action versions in integration.yml ([#15](https://github.com/MysticRyuujin/mirror-bench/issues/15)) ([35da73d](https://github.com/MysticRyuujin/mirror-bench/commit/35da73d052acf4f1ea67c2f14f4df0b09a8b8c98))
25
+
26
+ ## [0.2.0](https://github.com/MysticRyuujin/mirror-bench/compare/v0.1.2...v0.2.0) (2026-11-13)
27
+
28
+
29
+ ### ⚠ BREAKING CHANGES
30
+
31
+ * follow redirects in probes and require a 2xx response; reject an https mirror that redirects to http
32
+ * report throughput from phase 2 only; throughput_bps is null when phase 2 fails or is skipped
33
+ * remove the certificate point from the security score; the maximum is 2.5
34
+ * change default releases to Debian trixie, Mint zena, and Fedora 45
35
+ * reject --json together with --csv
36
+ * stop mapping distros with their own mirror network (rocky, almalinux, centos, rhel, ol, manjaro, kali, parrot, devuan, raspbian) to another distro
37
+
38
+ ### Features
39
+
40
+ * add --http3 to confirm HTTP/3 with a QUIC probe (optional http3 extra) ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
41
+ * add throughput_error to JSON and CSV output ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
42
+ * report mirrors that advertise HTTP/3 (HTTP/3 column, h3_advertised) ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
43
+
44
+
45
+ ### Bug Fixes
46
+
47
+ * add a time limit to both probe phases ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
48
+ * change default releases to Debian trixie, Mint zena, and Fedora 45 ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
49
+ * correct probe and throughput paths for Ubuntu, Mint, Arch, and Fedora ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
50
+ * **docker:** remove pip from the runtime image ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
51
+ * drop rsync entries from Arch discovery ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
52
+ * exit with code 1 and a message when discovery fails ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
53
+ * follow redirects in probes and require a 2xx response; reject an https mirror that redirects to http ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
54
+ * handle Debian records with no Country, and try https and http for each Debian site ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
55
+ * reject --concurrency 0 and --top 0 ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
56
+ * reject --json together with --csv ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
57
+ * remove the certificate point from the security score; the maximum is 2.5 ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
58
+ * report throughput from phase 2 only; throughput_bps is null when phase 2 fails or is skipped ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
59
+ * run bench when no subcommand is given ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
60
+ * stop mapping distros with their own mirror network (rocky, almalinux, centos, rhel, ol, manjaro, kali, parrot, devuan, raspbian) to another distro ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
61
+
62
+
63
+ ### Dependencies
64
+
65
+ * **actions:** bump actions/checkout ([#9](https://github.com/MysticRyuujin/mirror-bench/issues/9)) ([be49743](https://github.com/MysticRyuujin/mirror-bench/commit/be49743112ba6001b022bd3daec9c442a5925a66))
66
+ * **actions:** bump actions/setup-python in the actions group ([#12](https://github.com/MysticRyuujin/mirror-bench/issues/12)) ([930680d](https://github.com/MysticRyuujin/mirror-bench/commit/930680da12de59dc3171385bcb3672399586f66f))
67
+ * bump anyio, certifi, h2, hpack ([9977a7c](https://github.com/MysticRyuujin/mirror-bench/commit/9977a7c439590741123af2dec31548917d265186))
68
+ * bump idna to 3.18 (fixes PYSEC-2026-215) ([#10](https://github.com/MysticRyuujin/mirror-bench/issues/10)) ([690565c](https://github.com/MysticRyuujin/mirror-bench/commit/690565cc1669320eded4faaa175eb8ae85d739e4))
69
+
70
+ ## [0.1.2](https://github.com/MysticRyuujin/mirror-bench/compare/v0.1.1...v0.1.2) (2026-04-23)
71
+
72
+
73
+ ### Bug Fixes
74
+
75
+ * **docs:** lowercase ghcr.io references (docker pull requires lowercase) ([#8](https://github.com/MysticRyuujin/mirror-bench/issues/8)) ([31fcf57](https://github.com/MysticRyuujin/mirror-bench/commit/31fcf57239fc98eecb6df9fe8fed06400cb55dc7))
76
+
77
+
78
+ ### Documentation
79
+
80
+ * add shields.io badges at top of README ([#6](https://github.com/MysticRyuujin/mirror-bench/issues/6)) ([c25c12b](https://github.com/MysticRyuujin/mirror-bench/commit/c25c12bdc36aa6af67cbbb1b8b7ec65c000d7a53))
81
+
82
+ ## [0.1.1](https://github.com/MysticRyuujin/mirror-bench/compare/v0.1.0...v0.1.1) (2026-04-23)
83
+
84
+
85
+ ### Bug Fixes
86
+
87
+ * **ci:** pin aquasecurity/trivy-action to a version that exists ([#2](https://github.com/MysticRyuujin/mirror-bench/issues/2)) ([9984cd0](https://github.com/MysticRyuujin/mirror-bench/commit/9984cd018aaaa6de34b5cdf8261752f563187858))
88
+
89
+
90
+ ### Dependencies
91
+
92
+ * **actions:** bump the actions group across 1 directory with 11 updates ([#4](https://github.com/MysticRyuujin/mirror-bench/issues/4)) ([4aba2a6](https://github.com/MysticRyuujin/mirror-bench/commit/4aba2a68079d2029bf5cc387d9a6eae459d9ff2a))
93
+
94
+ ## [Unreleased]
95
+
96
+ ## [0.1.0] - 2026-04-23
97
+
98
+ ### Added
99
+
100
+ - Cross-distribution mirror benchmarking for **Ubuntu, Debian, Fedora,
101
+ Linux Mint, and Arch**.
102
+ - Two-phase probe: async latency screen (ranged GET on each distro's canonical
103
+ metadata file; median of 3 samples) followed by a throughput test on the
104
+ top-N candidates.
105
+ - Weighted composite scoring across latency, throughput, and security (HTTPS +
106
+ TLS 1.3 + valid cert + HTTP/2). Weights adjustable via
107
+ `--weights lat=…,thr=…,sec=…`.
108
+ - `bench` and `list` subcommands with `--json` / `--csv` / `--https-only` /
109
+ `--tls13-only` / `--no-throughput` / `--country` / `--top` / `--concurrency`.
110
+ - Host auto-detection via `/etc/os-release`, with derivative alias support
111
+ (Pop!\_OS, Kali, Rocky, etc.). `--distro` override for containers and macOS.
112
+ - Shell completion via typer (`--install-completion` / `--show-completion`).
113
+ - Multi-stage Dockerfile (`python:3.14-slim-bookworm`, non-root uid 65532).
114
+ - GitHub Actions CI + release pipelines with OIDC PyPI publishing, multi-arch
115
+ container image, SLSA build provenance, and Trivy image scanning.
116
+ - Pre-commit hooks: ruff, mypy, bandit, gitleaks, hadolint, shellcheck,
117
+ yamllint, markdownlint, commitizen.
118
+ - `SECURITY.md` with responsible-disclosure workflow and supply-chain notes.
119
+ - Licensed under **GNU AGPL-3.0-or-later** — users of hosted/network-facing
120
+ modified versions must receive the modified source.
121
+
122
+ [Unreleased]: https://github.com/MysticRyuujin/mirror-bench/compare/v0.1.0...HEAD
123
+ [0.1.0]: https://github.com/MysticRyuujin/mirror-bench/releases/tag/v0.1.0
@@ -66,7 +66,7 @@ walks you through the format interactively.
66
66
  1. Create `src/mirror_bench/discovery/<name>.py` implementing
67
67
  `MirrorDiscoverer` (`discover`, `probe_path`, `throughput_path`).
68
68
  2. Register it in `src/mirror_bench/discovery/__init__.py`'s `DISCOVERERS` map.
69
- 3. Add the distro's `ID` / `ID_LIKE` aliases to
69
+ 3. Add the distro's `ID` aliases to
70
70
  `src/mirror_bench/distro.py::_ALIASES`.
71
71
  4. Add a respx-mocked test with a realistic fixture under
72
72
  `tests/fixtures/<name>_*`.
@@ -8,7 +8,7 @@ ARG UV_VERSION=0.11.7
8
8
  FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv
9
9
 
10
10
  FROM python:${PYTHON_VERSION}-slim-bookworm AS builder
11
- ARG VERSION=0.1.0
11
+ ARG VERSION=dev
12
12
 
13
13
  # Minimal build deps; nothing compiled lands in the final image.
14
14
  ENV PYTHONDONTWRITEBYTECODE=1 \
@@ -20,20 +20,26 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
20
20
  COPY --from=uv /uv /usr/local/bin/uv
21
21
 
22
22
  WORKDIR /app
23
- COPY pyproject.toml README.md LICENSE ./
23
+ COPY pyproject.toml uv.lock README.md LICENSE ./
24
24
  COPY src ./src
25
25
 
26
- # Build the wheel, then install it (+ runtime deps) into a self-contained venv
27
- # under /opt/venv. No dev deps, no sources, no uv binary in the final image.
26
+ # Build the wheel, then install it into a self-contained venv under /opt/venv.
27
+ # Runtime deps come from uv.lock, so the image runs the same versions that CI
28
+ # tests; resolving at build time would pull whatever is newest that day.
29
+ # No dev deps, no sources, no uv binary in the final image.
28
30
  RUN --mount=type=cache,target=/root/.cache/uv \
29
31
  uv venv /opt/venv \
32
+ && uv export --frozen --no-dev --no-emit-project --format requirements.txt \
33
+ --output-file /tmp/requirements.txt \
34
+ && VIRTUAL_ENV=/opt/venv uv pip install --no-cache --require-hashes \
35
+ -r /tmp/requirements.txt \
30
36
  && uv build --wheel \
31
37
  && wheel=$(find dist -maxdepth 1 -name '*.whl' -print -quit) \
32
- && VIRTUAL_ENV=/opt/venv uv pip install --no-cache "${wheel}"
38
+ && VIRTUAL_ENV=/opt/venv uv pip install --no-cache --no-deps "${wheel}"
33
39
 
34
40
  # ---- Runtime stage -------------------------------------------------------
35
41
  FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime
36
- ARG VERSION=0.1.0
42
+ ARG VERSION=dev
37
43
 
38
44
  LABEL org.opencontainers.image.title="mirror-bench" \
39
45
  org.opencontainers.image.description="Cross-distribution Linux package mirror benchmarking." \
@@ -46,11 +52,14 @@ LABEL org.opencontainers.image.title="mirror-bench" \
46
52
  # We deliberately do not pin ca-certificates: the whole point of this package
47
53
  # is to ship the latest Debian-security-vetted CA list at build time. Pinning
48
54
  # a specific version would freeze trust material and defeat the upgrade path.
55
+ # The runtime never installs anything, so pip goes too: its vendored copies of
56
+ # urllib3, msgpack, and setuptools are what image scanners report.
49
57
  # hadolint ignore=DL3008
50
58
  RUN apt-get update \
51
59
  && apt-get install -y --no-install-recommends ca-certificates \
52
60
  && apt-get clean \
53
61
  && rm -rf /var/lib/apt/lists/* \
62
+ && python -m pip uninstall --yes --no-cache-dir pip \
54
63
  && groupadd --system --gid 65532 app \
55
64
  && useradd --system --uid 65532 --gid app \
56
65
  --home-dir /home/app --create-home --shell /usr/sbin/nologin app
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: linux-mirror-bench
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: Cross-distribution Linux package mirror benchmarking tool
5
5
  Project-URL: Homepage, https://github.com/MysticRyuujin/mirror-bench
6
6
  Project-URL: Issues, https://github.com/MysticRyuujin/mirror-bench/issues
@@ -21,6 +21,8 @@ Requires-Python: >=3.14
21
21
  Requires-Dist: httpx[http2]>=0.27
22
22
  Requires-Dist: rich>=13
23
23
  Requires-Dist: typer>=0.12
24
+ Provides-Extra: http3
25
+ Requires-Dist: aioquic>=1.2; extra == 'http3'
24
26
  Description-Content-Type: text/markdown
25
27
 
26
28
  # mirror-bench
@@ -35,7 +37,7 @@ Description-Content-Type: text/markdown
35
37
 
36
38
  A cross-distribution Linux package mirror benchmarking tool.
37
39
 
38
- Ranks mirrors by **latency**, **throughput**, and **security** (HTTPS / TLS 1.3 / cert validity / HTTP/2).
40
+ Ranks mirrors by **latency**, **throughput**, and **security** (HTTPS / TLS 1.3 / HTTP/2).
39
41
 
40
42
  `mirror-bench` is **read-only**: it discovers mirrors, probes them, prints a
41
43
  ranked table, and exits. It never touches `/etc/apt/sources.list`,
@@ -131,19 +133,21 @@ Running `mirror-bench` with no subcommand is equivalent to `mirror-bench bench`.
131
133
 
132
134
  ### Common flags
133
135
 
134
- | Flag | Description |
135
- | ----------------------------- | ------------------------------------------------------------------------------------------- |
136
- | `--distro, -d NAME` | One of `ubuntu`, `debian`, `fedora`, `mint`, `arch`. |
137
- | `--release, -r VALUE` | Release override: codename for apt (noble, bookworm, wilma) or numeric for Fedora (41, 42). |
138
- | `--country, -c US,CA,GB` | ISO 3166-1 alpha-2 country codes to filter on. |
139
- | `--top, -n N` | How many mirrors to include in phase 2 / display (default 15). |
140
- | `--concurrency N` | Max parallel HTTP probes (default 20). |
141
- | `--https-only` | Hard-exclude mirrors without HTTPS. |
142
- | `--tls13-only` | Hard-exclude mirrors that don't negotiate TLS 1.3. |
143
- | `--weights lat=…,thr=…,sec=…` | Override scoring weights (auto-normalized). |
144
- | `--no-throughput` | Skip phase 2. Phase-1 latency screen only. |
145
- | `--json` | Emit JSON to stdout. Suppresses the table + progress. |
146
- | `--csv` | Emit CSV to stdout. Suppresses the table + progress. |
136
+ | Flag | Description |
137
+ | ----------------------------- | ---------------------------------------------------------------------------------------- |
138
+ | `--distro, -d NAME` | One of `ubuntu`, `debian`, `fedora`, `mint`, `arch`. |
139
+ | `--release, -r VALUE` | Release override: codename for apt (noble, trixie, zena) or numeric for Fedora (44, 45). |
140
+ | `--country, -c US,CA,GB` | ISO 3166-1 alpha-2 country codes to filter on. |
141
+ | `--top, -n N` | How many mirrors to include in phase 2 / display (default 15). |
142
+ | `--concurrency N` | Max parallel HTTP probes (default 20). |
143
+ | `--https-only` | Hard-exclude mirrors without HTTPS. |
144
+ | `--tls13-only` | Hard-exclude mirrors that don't negotiate TLS 1.3. |
145
+ | `--weights lat=…,thr=…,sec=…` | Override scoring weights (auto-normalized). |
146
+ | `--no-throughput` | Skip phase 2. Phase-1 latency screen only. |
147
+ | `--max-age HOURS` | Drop mirrors more than this far behind the newest mirror (default 36). `0` keeps all. |
148
+ | `--http3` | Probe over QUIC the mirrors that advertise HTTP/3. Needs the `http3` extra. |
149
+ | `--json` | Emit JSON to stdout. Suppresses the table + progress. |
150
+ | `--csv` | Emit CSV to stdout. Suppresses the table + progress. |
147
151
 
148
152
  ## Running in Docker
149
153
 
@@ -161,8 +165,9 @@ directly.
161
165
  > The image renders a colored, properly-widened table even without `-it`.
162
166
  > If you want **rich to use your host terminal's exact width** (and react to
163
167
  > resizes), add `-it`; otherwise the image falls back to a sensible 120-col
164
- > table. Set `NO_COLOR=1` if you'd rather have plain text — e.g. when
165
- > redirecting to a file.
168
+ > table. Set `NO_COLOR=1` if you'd rather have plain text with no escape
169
+ > codes — e.g. when redirecting to a file. Progress bars show only when
170
+ > stderr is a terminal.
166
171
 
167
172
  ### Basic invocation
168
173
 
@@ -277,7 +282,7 @@ All arguments are optional; defaults match what CI ships.
277
282
  | ---------------- | -------- | ------------------------------------------------------------------- |
278
283
  | `PYTHON_VERSION` | `3.14` | Base image tag (`python:<PYTHON_VERSION>-slim-bookworm`). |
279
284
  | `UV_VERSION` | `0.11.7` | uv release pulled from `ghcr.io/astral-sh/uv:<UV_VERSION>`. |
280
- | `VERSION` | `0.1.0` | Value embedded in the `org.opencontainers.image.version` OCI label. |
285
+ | `VERSION` | `dev` | Value embedded in the `org.opencontainers.image.version` OCI label. |
281
286
 
282
287
  Example:
283
288
 
@@ -369,6 +374,26 @@ $ mirror-bench bench --<TAB>
369
374
  --distro --country --top --concurrency --https-only --tls13-only …
370
375
  ```
371
376
 
377
+ ## Supported distributions
378
+
379
+ | `--distro` | Auto-detected from `/etc/os-release` `ID` |
380
+ | ---------- | ---------------------------------------------- |
381
+ | `ubuntu` | `ubuntu`, `pop`, `elementary`, `zorin`, `neon` |
382
+ | `debian` | `debian` |
383
+ | `fedora` | `fedora`, `nobara` |
384
+ | `mint` | `linuxmint` |
385
+ | `arch` | `arch`, `archlinux`, `endeavouros` |
386
+
387
+ Another Ubuntu remix is detected when its `os-release` has `ID_LIKE=ubuntu`
388
+ and `UBUNTU_CODENAME`.
389
+
390
+ Distributions with their own mirror network (RHEL family, Kali, Devuan,
391
+ Raspberry Pi OS, Manjaro) are not supported. Ranking the mirrors of a
392
+ different distribution for them gives a wrong answer.
393
+
394
+ The Ubuntu, Mint, and Arch throughput tests always fetch the x86_64 file.
395
+ The mirror lists for those distributions cover x86_64 mirrors only.
396
+
372
397
  ## How it works
373
398
 
374
399
  1. **Discover** — fetch the canonical mirror list for the target distribution:
@@ -376,23 +401,92 @@ $ mirror-bench bench --<TAB>
376
401
  set (`mirrors.ubuntu.com/mirrors.txt` returns a geo-selected _single_
377
402
  mirror, not a full list — so we don't rely on it alone).
378
403
  - Debian: `mirror-master.debian.org/status/Mirrors.masterlist` (RFC822
379
- multi-record format).
404
+ multi-record format). The list does not say which sites serve HTTPS, so
405
+ each site is tried as an `https://` and an `http://` candidate.
380
406
  - Fedora: `mirrors.fedoraproject.org/mirrorlist?repo=&arch=&country=`.
407
+ - The Ubuntu and Fedora lists name one scheme for each mirror, often
408
+ `http://` for a host that also serves HTTPS. Each `http://` entry is
409
+ also tried as `https://`.
381
410
  - Linux Mint: scraped from `linuxmint.com/mirrors.php`.
382
- - Arch: `archlinux.org/mirrors/status/json/` (JSON, filtered to active).
383
- 2. **Phase 1 — latency screen.** Concurrent ranged GET (`bytes=0-1023`) on the
384
- distro's canonical small metadata file (`InRelease`, `repomd.xml`,
385
- `lastsync`). Three samples per mirror, **median TTFB** used for ranking.
386
- 3. **Phase 2 — throughput test.** Full streaming GET of a representative file
387
- (`Contents-<arch>.gz` for apt, `core.db` for Arch, etc.) on the top N from
388
- phase 1, capped at 5 MiB. `bytes_per_sec` is measured from first-byte to
389
- end-of-stream so steady-state bandwidth isn't polluted by connect/TLS
390
- overhead.
411
+ - Arch: `archlinux.org/mirrors/status/json/` (JSON, filtered to active
412
+ HTTP and HTTPS entries).
413
+
414
+ If the list cannot be fetched, the command prints the reason and exits
415
+ with code 1.
416
+
417
+ 2. **Phase 1 — latency screen.** Concurrent ranged GET (`bytes=0-1023`) on a
418
+ small metadata file that the distribution rewrites as packages change:
419
+ `InRelease` of the `-updates` suite for Ubuntu and Debian, `Release` for
420
+ Mint, `repomd.xml` of the updates repository for Fedora, `lastupdate` for
421
+ Arch. Three samples per mirror, **median TTFB** used for ranking.
422
+ 3. **Phase 2 — throughput test.** Streaming GET of a large file
423
+ (`Contents-<arch>.gz` for apt, `images/install.img` for Fedora, `extra.db`
424
+ for Arch) on the top N from phase 1, capped at 5 MiB and 30 seconds.
425
+ `bytes_per_sec` is measured from first-byte to last-byte so steady-state
426
+ bandwidth isn't polluted by connect/TLS overhead.
427
+
428
+ A mirror that answers a missing file with status 200 and an HTML error
429
+ page fails phase 1 with `soft_404`. A complete phase 2 body under 256 KiB
430
+ fails with `short_body`. If no mirror passes phase 1, a line on stderr
431
+ gives the most common failure; a wrong `--release` is the usual cause.
432
+
433
+ Both phases follow redirects, so the timing includes the extra hop that a
434
+ package manager also pays. Only a 2xx final response counts. An `https://`
435
+ mirror that redirects to `http://` fails with `https_downgrade`. An
436
+ `http://` mirror gets no TLS credit, even when it redirects to HTTPS.
437
+
391
438
  4. **Score.** Weighted composite of normalized latency (inverse), throughput,
392
- and security (HTTPS + TLS 1.3 + cert-valid + HTTP/2, max 3.5). Weights
439
+ and security (HTTPS 1 + TLS 1.3 1 + HTTP/2 0.5, max 2.5). Weights
393
440
  default to `lat=0.4, thr=0.4, sec=0.2` and are user-overridable.
441
+ - Throughput is the phase 2 measurement only. A mirror whose phase 2 probe
442
+ fails shows `-`, scores 0 for throughput, and reports the cause in the
443
+ `throughput_error` field of the JSON and CSV output.
444
+ - With `--no-throughput`, the throughput weight is 0 and the other weights
445
+ are normalized again.
446
+ - When a site answers on `https://` and `http://`, only the `https://` row
447
+ is kept.
394
448
  5. **Render.** A `rich.Table`, or JSON / CSV for scripting.
395
449
 
450
+ ### Freshness
451
+
452
+ A fast mirror that stopped syncing serves old packages. The phase 1 file
453
+ carries the time its content was generated, so each run also reads that
454
+ time, with no extra request.
455
+
456
+ - The `Age` column shows how far a mirror is behind the newest mirror in the
457
+ run: `0`, `23h`, `12d`, or `-` when the file gave no time. JSON and CSV
458
+ carry it as `sync_age_s`.
459
+ - Age is relative to the other mirrors, not to your clock. If the
460
+ distribution published nothing for a week, every current mirror still
461
+ shows `0`.
462
+ - Mirrors more than 36 hours behind are dropped before ranking, and a line on
463
+ stderr says how many. Change the limit with `--max-age HOURS`; `0` keeps
464
+ all mirrors.
465
+ - Age does not change the score. A stale mirror is wrong, not slow.
466
+
467
+ ### HTTP/3
468
+
469
+ Phase 1 reads the `Alt-Svc` response header. The `HTTP/3` column shows `adv`
470
+ when a mirror advertises HTTP/3 (`h3`) for its own host; JSON and CSV carry
471
+ the same fact as `h3_advertised`. This has no effect on the score: apt, dnf,
472
+ and pacman do not use HTTP/3 today.
473
+
474
+ To confirm the advertisement with a real QUIC request, install the optional
475
+ extra and add `--http3`:
476
+
477
+ ```sh
478
+ uv tool install "linux-mirror-bench[http3]"
479
+ mirror-bench bench --distro arch --http3
480
+ ```
481
+
482
+ - Each ranked mirror that advertises HTTP/3 gets one ranged GET over QUIC.
483
+ - The `HTTP/3` column shows `yes` when that request succeeds. JSON and CSV
484
+ gain `h3_latency_ms` and `h3_error`.
485
+ - If UDP is blocked on your network, the probes fail after about 8 seconds
486
+ and the column stays at `adv`.
487
+ - Without the extra, `--http3` exits with code 2 and names the extra.
488
+ - The container image does not include the extra.
489
+
396
490
  ## Development
397
491
 
398
492
  See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full dev loop.