All notable changes to da-cli are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
The version in dacli/constants.py is the single source of truth:
pyproject.toml reads it dynamically, the release workflow refuses a tag
that disagrees with it, and PyPI therefore always matches the tag.
- The README now shows that the project is on PyPI: a version badge as the
first badge, and
pipx install da-syncdirectly under the opening paragraph. The install instructions were already correct, but nothing above the fold said the package was installable, and the badge row — eight badges covering CI, linting and coverage — never mentioned it. - The static
Python 3.10+badge is nowpypi/pyversions, which reads the classifiers rather than restating them, so it cannot drift fromrequires-python. - Dropped the
Status: Betabadge. The blockquote two lines below says the same thing with more detail, and the badge row has a budget: the discoverability check requires "deviantart" within the first twelve lines, and a ninth badge pushed the opening paragraph past it.
Documentation only; no code changes.
The 0.1.0 project page on PyPI carried the pre-launch README, which told visitors "Option B — pip install. Not published yet. Use Option A for now." — on the page of the published package. A project description is frozen into the uploaded artifact and PyPI has no way to edit it, so correcting it requires a release. That is what this one is for.
- README documents the real
pipx install da-syncpath now that 0.1.0 is published, replacing the placeholder that said it was unavailable. - The getting-started guide now offers the PyPI install as well. It had
only ever documented
git clone+./install.sh, so the tutorial a new user is pointed at was the one place that never mentioned the published package. install_schedule.shships in the repository but not in the wheel, so the scheduling guide and the tutorial's scheduling step now say where to get it rather than assuming a checkout. Its "not found on PATH" error names both install paths instead of only./install.sh.
- The getting-started transcript of
./install.shomitted the(N modules)suffix the script actually prints.check_doc_referencesnow asserts any documented module count againstdacli/, so the number cannot drift again —docs/commands/examples are executed bytests/test_docs_examples.py, butgetting-started.mdis not, which is how this survived.
First public release.
da sync— three ways to walk DeviantArt and save what you have not got yet.sync feedfollows your watch feed from the top and stops at the checkpoint the previous run left, so a quiet day costs one API call.sync artistwalks one gallery newest-first.sync watcheddiscovers everyone you watch and runs the artist walk for each under one shared time budget.- Resumable walks. Each artist's position is checkpointed in
state.json, so a run cut short by its time budget resumes where it stopped rather than starting over. - A local SQLite index of what has been downloaded, so re-runs do not
re-fetch. Self-healing: a row whose folder has gone is dropped, and
da index rebuildreconstructs the whole index from disk without re-downloading anything. - OAuth 2.1 with PKCE, against a loopback HTTPS listener with a
self-signed certificate generated on first run. The
client_secretis optional — DeviantArt's own guidance for desktop apps is a public client with PKCE and no secret. Where one is used on macOS it lives in the Keychain, not on disk. - Scheduled syncs —
install_schedule.shwrites a launchd agent on macOS; a systemd user timer is documented for Linux. da search/da user/da deviation/da daily— read-only browse helpers for tags, topics, daily deviations, profiles and metadata. Thirteen commands accept--jsonfor scripting.da diagnose— one command that checks every layer that can quietly break an unattended run: config, destination writability and free space, token expiry and scope, index drift, and whether the launchd job is loaded.da auth status— a small JSON object plus an exit code, for cron and monitoring wrappers.- Zero runtime dependencies. The whole tool is the Python 3.10+
standard library. The CI
artifactjob installs the built wheel into a clean virtualenv and imports every submodule, so the claim is tested rather than asserted. - Typed. Ships
py.typed;mypyruns in CI.
- Credentials never land in a world-readable file: config, state, the
index and both lock files are created
0600, and the loopback TLS key is generated inside a0700directory so it is never briefly readable. - The OAuth flow generates and verifies a
stateparameter (RFC 6749 §10.12) and accepts a callback on the expected path only. - Debug output (
-v) passes every URL through a redactor coveringaccess_token,client_secret,codeandtoken— the last because the image CDN signs every content URL with a live JWT. da config showmasks secrets;--unmaskwrites to stderr so redirecting stdout cannot capture the value.
See SECURITY.md for the threat model, including what this explicitly does not protect against.