Back to README
git clone https://github.com/per2jensen/dar-backup.git
cd dar-backup/v2
ln ../README.md README.md # ignored by git, is here to put README in package
./build.shThis script:
- Creates a Python virtual environment called
venv - pip install
hatch - pip install the development environment as setup in pyproject.toml
Example:
dev = [
"pytest",
"wheel>=0.45.1",
"requests>=2.32.2",
"coverage>=7.8.2",
"pytest>=8.4.0",
"pytest-cov>=6.1.1",
"psutil>=7.0.0",
"pytest-timeout>=2.4.0",
"httpcore>=0.17.3",
"h11>=0.16.0",
"zipp>=3.19.1",
"anyio>=4.4.0",
"black>=25.1.0"]
src/dar_backup/__about__.py carries a .devN suffix between releases — for
example "1.1.10.dev5" — where N is the number of commits since the last
v2-X.Y.Z release tag. The counter is kept current automatically by a
pre-commit hook so every committed snapshot has a meaningful, unique version
string.
Install once (from the repo root):
ln -sf ../../v2/scripts/pre-commit-version-bump.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commitWhat the hook does on every commit:
- Reads
__version__from__about__.py. - Skips silently if there is no
.devsuffix (release.showns that state). - Counts commits since the last
v2-X.Y.Ztag, adds 1 for the commit in progress, and writesX.Y.Z.dev<N>back to__about__.py. - Stages the file so the updated counter is part of the commit.
Fallback: if no v2-X.Y.Z tag exists yet (fresh clone before the first
release), it counts all commits in history instead.
Release workflow:
- Strip
.devNfrom__about__.py→"1.1.10". - Commit, tag, run
./release.sh. - After the release, bump to the next dev version →
"1.1.11.dev0". - The hook takes it from there.
The hook lives at v2/scripts/pre-commit-version-bump.sh and is tracked in
git; only the symlink in .git/hooks/ is local.
source venv/bin/activate # activate the virtual env
pytest # run the test suiteMake sure __about__.py has the correct version number
VERSION=$(grep -Po '(?<=__version__ = ")[^"]+' src/dar_backup/__about__.py)
python3 -m build && pip install --force-reinstall dist/dar_backup-${VERSION}-py3-none-any.whlThe test suite is annotated with these markers:
unit(fast, pure logic)component(subprocess boundary with mocks/lightweight commands)integration(end-to-end workflows; external tools)slow(long-running/heavier integration)live_discord(sends real webhook messages; opt-in only)smokecritical integration tests that must pass on every commit (used with unit+component as the fast'ish (!) CI gate)debug: temporary mark for focused local debugging runs (never used in CI)
# Fast local loop (unit + component)
pytest -m "unit or component"
# Integration (exclude slow + live webhook)
pytest -m "integration and not slow and not live_discord"
# Slow-only
pytest -m slow
# Full suite (default pytest.ini already excludes live_discord)
pytest -m "not live_discord"
# Live webhook (requires DAR_BACKUP_DISCORD_WEBHOOK_URL)
pytest -m live_discordpytestThe simplest way to run the test suite:
# assumes the venv is activated, see above
pytestA pytest.ini is located in the v2 directory, so that pytest writes out captures to console.
That is useful when working with a single test and is the default:
PYTHONPATH=src pytest -c pytest-minimal.ini tests/test_verbose.py::test_verbose_error_reportingUse to get the minimal info on successful test cases:
PYTHONPATH=src pytest -c pytest-minimal.inior for specific file with test cases:
PYTHONPATH=src pytest -c pytest-minimal.ini tests/test_verbose.pyBy default, local pytest enables subprocess coverage to match the GitHub workflow.
This is done via COVERAGE_PROCESS_START=pyproject.toml set in tests/conftest.py.
Disable subprocess coverage for a single run:
DAR_BACKUP_NO_SUBPROCESS_COVERAGE=1 pytestOr run the full CI-equivalent report flow (same markers + coverage artifacts):
./scripts/pytest_report.sh fullscripts/pytest_report.sh runs mypy src/ as its first step and aborts
before pytest starts if mypy reports an error (same gating as ruff in
release.sh). This applies to every mode (fast, smoke, integration,
full), and runs identically in the GitHub workflow, since the workflow
calls this same script.
Plain pytest / pytest -m ... invocations do not run mypy — only
scripts/pytest_report.sh does. To check types on their own:
mypy src/Config lives in [tool.mypy] in pyproject.toml.
The repo root includes track_downloads.py which fetches daily download counts
from the PyPI Stats API and writes downloads.json in a time-series format
(similar to clonepulse/fetch_clones.json).
Default behavior:
- Re-fetches the last 31 days on each run to catch PyPI corrections.
- Preserves existing
annotationsindownloads.json. - Adds
rollups(last 7/30 days and averages). - Adds spike annotations using a rolling median + MAD rule (Hampel-style) with a
30‑day window, threshold
> 5 * MAD, andmin_count >= 50. The MAD is scaled by1.4826to match the standard deviation for normally distributed data.- Auto spike annotations use the label prefix
Spike:and are regenerated on each run. - Manual annotations (any label not starting with
Spike:) are preserved. - If too many spikes appear, increase the MAD threshold or minimum count.
- Auto spike annotations use the label prefix
Manual run:
cd <path/to/dar-backup>
python track_downloads.pyOverride the correction window:
cd <path/to/dar-backup>
python track_downloads.py --days-back 60The GitHub workflow update_downloads.yml runs this on a daily schedule.
References:
https://en.wikipedia.org/wiki/Median_absolute_deviation
https://en.wikipedia.org/wiki/Hampel_test
https://en.wikipedia.org/wiki/Moving_average
The release.sh script checks that the repo is clean with no changed files and that tag for the --tag option exists AND is at HEAD.
Two environment variables must be present for the release.sh scripts to upload to PyPI.
The developer must be ready to provide gpg passphrase when the gpg asks for it (after the user has tapped a key to procede to that step).
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=<the token>
cd <path/to/dar-backup/v2>
./release.sh --tag <the tag> --upload-to-pypi # provide password to GPG to sign the built artifactsgpg --import <key>
Verify source code has not been tampered with:
gpg --verify dar-2.7.18.tar.gz.sig dar-2.7.18.tar.gz
gpg: Signature made tir 20 maj 2025 18:02:15 CEST
gpg: using RSA key 55E484A6A5C5BC7F53F7F72EA8B14160D36B3BA7
gpg: Good signature from "Denis Corbin (http://dar.linux.free.fr/) <dar.linux@free.fr>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: 1BE4 7606 A74F 178C 7328 43B0 5F64 5B19 16D5 6546
Subkey fingerprint: 55E4 84A6 A5C5 BC7F 53F7 F72E A8B1 4160 D36B 3BA7export DAR_VERSION=2.7.21.RC1 This worked for dar version 2.7.21.RC1 on ubuntu 24.04
export SRC_CODE=/some/dir export DAR_DIR=$HOME/.local/dar-${DAR_VERSION}
apt-get update && apt-get install -y --no-install-recommends \
python3 python3-venv python3-pip gettext-base ca-certificates tzdata file gnupg \
build-essential autoconf automake libtool pkg-config binutils \
libkrb5-dev libgcrypt-dev libgpgme-dev libext2fs-dev libthreadar-dev \
librsync-dev libcurl4-gnutls-dev libargon2-dev \
bzip2 zlib1g-dev libbz2-dev liblzo2-dev liblzma-dev libzstd-dev liblz4-dev \
groff doxygen graphviz upx
sudo apt-get install libkrb5-dev
sudo apt-get install libgcrypt-dev libgpgme-dev libext2fs-dev libthreadar-dev librsync-dev libcurl4-gnutls-dev
cd "$SRC_CODE"
CXXFLAGS=-O
export CXXFLAGS
make clean distclean
./configure --prefix="$DAR_DIR" LDFLAGS="-lgssapi_krb5"
make
make install-strip
rm $HOME/.local/dar # remove link
ln -s $HOME/.local/dar-${DAR_VERSION} $HOME/.local/darThe metrics database (configured via METRICS_DB_PATH) uses WAL journal mode,
so Datasette, DB Browser for SQLite, and the sqlite3 CLI can all read it
concurrently without blocking backup writes.
sqlite3 /path/to/metrics.db ".mode box" ".headers on" "
SELECT s.archive_name,
s.file_path,
s.file_size_bytes,
s.result,
COALESCE(r.code, '') AS fail_reason,
COALESCE(s.fail_detail, '') AS detail,
s.tested_at
FROM restore_test_samples s
LEFT JOIN restore_test_fail_reasons r ON s.fail_reason_id = r.id
ORDER BY s.tested_at DESC;
"sqlite3 /mnt/dar/dar-backup-metrics.db ".mode box" ".headers on" "
SELECT archive_name, backup_type, status, dar_exit_code,
run_started_at as 'started (UTC)', run_finished_at as 'finished (UTC)'
FROM backup_runs
ORDER BY run_started_at DESC
LIMIT 20;
"cd ~/git/dar-backup/v2
source venv/bin/activate
datasette <path-to-metrics.db>That starts it on the default port 8001 at http://127.0.0.1:8001.
Common flags you might want:
Open browser automatically datasette ~/dar-backup/dar-backup-metrics.db -o
With CORS enabled (same as what dashboard.py uses) datasette ~/dar-backup/dar-backup-metrics.db --cors -p 8001
git log --pretty=format:"%ad - %an: %s %d" --date=shorttar --exclude='*/__pycache__' \
-czvf dar-backup.tgz \
tests/ \
doc/ \
src/ \
README.md \
Changelog.md \
pyproject.toml \
testall.sh \
build.sh \
release.sh \
pytest.ini \
MANIFEST.in \
pytest.ini