Sync DeviantArt galleries to local folders — zero-dependency Python CLI with OAuth 2.1 PKCE, a SQLite index, and scheduled macOS syncs.
Project description
da-cli
Sync your DeviantArt gallery to a local folder from the command line — a backup of the art you watch, kept current. Zero runtime dependencies: the whole tool is the Python 3.10+ standard library. A local SQLite index means a re-run costs one API call when nothing new was posted, and launchd (macOS) or a systemd timer (Linux) keeps it running unattended. Plus search and browse helpers.
New to da-cli? Follow the Setup Guide — it walks you through everything from install to first sync in about 10 minutes, with screenshots. Status: Beta — the sync and search flows are stable and covered by 868 tests. macOS Keychain integration is production-ready; Linux Secret Service support is planned. See CHANGELOG.md.
Documentation
| I want to… | Page |
|---|---|
| install it and download my first art | Getting started |
| understand a command properly | Command guides |
| look up a command or a flag | Command reference |
| fix an error I just got | Troubleshooting |
| sync automatically every day | Scheduling |
| change a setting | Configuration |
| script it or monitor it | Scripting |
| know what it does with my OAuth secret | Security model |
| contribute | CONTRIBUTING · ARCHITECTURE |
Full index: docs/
Quick tour
Each group below has a page documenting every flag, what it writes and how it behaves on a second run — linked after the block.
da --version # print version
da auth # one-time browser-based login
da auth logout # forget local tokens (DA-side authorisation persists; revoke at deviantart.com)
da whoami # confirm token + identity
da refresh # force-refresh the access token
da sync feed # incremental: pull new content from your watch feed
da sync feed --no-mature # ...excluding mature content (included by default)
da sync artist <username> # walk one artist's full gallery
da sync watched # walk every watched user's gallery
da sync watched --via-feed # discover artists via watch feed (works with `browse` scope alone)
da sync feed --jitter 0.4 # randomise sleeps by ±40% — avoids burst patterns
da search tag nature # browse by tag
da search topic digitalart # browse a curated DA topic
da search topics # list all valid topics
da search user deviantart # resolve a username
da daily 2026-01-15 # daily-deviation picks for a date
da user profile <username> # who is this person
da deviation show <deviationid> # full metadata for one deviation
da deviation morelikethis <id> # related deviations via DA's recommender
da watch list # show who you watch (needs `user` scope)
da config show # inspect config + resolved paths
da config path # just the file locations
da config set <key> <value> # store; secrets go to Keychain on macOS
da config get <key> [--unmask] # read back (secrets masked unless --unmask)
da config unset <key> # remove
da index show # synced-index stats (rows, top artists)
da index rebuild # walk dest, re-import (idempotent)
da diagnose # end-to-end health check
da bench # synthetic sync benchmark (no network)
da search popularandda search newestwere retired (DA dropped the underlying endpoints). Useda search topic <name>,da search tag <tag>, orda dailyinstead — both subcommands now exit 2 with an actionable hint.
Read more, by area:
- Authentication —
da auth,auth logout,auth status,whoami,refresh. The PKCE flow, scopes, and the 90-day refresh-token ceiling. - Syncing art —
sync feed,sync artist,sync watched. What each mode walks, the checkpoint, resuming an interrupted backfill, pacing, and time budgets. - Searching and browsing —
search tag,search topic,daily. Read-only; downloads nothing. - Inspecting users and deviations —
user profile,deviation show,watch list. How to find the id or username a sync command needs. - Configuration commands —
config show,set,get,unset. Where each setting lives and which source wins. - Index, health and benchmarking —
index show,index rebuild,diagnose,bench. How to tell whether a scheduled run is quietly failing.
Install
da-cli is a stdlib-only Python package with no runtime dependencies. Two install paths:
Option A — git clone + shim (zero global state, recommended for developers):
git clone https://github.com/FZ2000/da-cli.git ~/Documents/da-cli
cd ~/Documents/da-cli
./install.sh # copies da + the dacli package to ~/.local/share/da-cli/ and symlinks ~/.local/bin/da → that copy
Option B — pip install. Not published yet. Use Option A for now.
When it is published the distribution will be named da-sync, not
da-cli:
pipx install da-sync # the command is still `da`
Two names on PyPI are close to this project and are not it: dacli
(an unrelated data-engineering tool) and da-cli (unregistered). Since
this package installs a da command onto your PATH and handles
DeviantArt OAuth tokens, check the name before installing anything.
Python 3.10+ required (uses argparse.BooleanOptionalAction and X | None syntax). No third-party runtime dependencies.
First-time setup
New? Follow the Setup Guide instead — it has screenshots and walks through every step from zero.
Quick version for experienced users:
-
Create a Confidential OAuth app at https://www.deviantart.com/developers/ → Register Your Application. Set the Redirect URI Whitelist to
https://localhost:8765/(exactly — trailing slash matters). Manage your apps at https://www.deviantart.com/studio/apps. -
Configure da-cli:
da config set client_id 12345 da config set client_secret <YOUR_SECRET> da config set destination ~/Pictures/DA da auth da whoami
The CLI auto-refreshes the access token before each call; you only
repeat da auth if you revoke the app or the refresh token expires
(every 90 days — da diagnose warns 14 days before).
Scheduled sync
./install_schedule.sh # macOS: daily at 03:00 via launchd
On macOS this installs a launchd user agent; the script writes the plist
and loads it. On Linux, use a systemd user timer or cron. Both are covered, with the
Full Disk Access step macOS needs and the enable-linger step systemd
needs, in docs/guides/scheduling.md.
Troubleshooting
da diagnose checks configuration, credentials, the destination
folder, the index, and the scheduled job, and tells you what is wrong.
For specific errors — a redirect-URI mismatch, an expired refresh token, a scheduled run that silently does nothing — see docs/guides/troubleshooting.md.
Security model
This is documented separately in docs/explanation/security.md; to report a vulnerability see SECURITY.md. Short version:
- Secrets (
client_secret) live in macOS Keychain (or, on other platforms, in a 0600-permissioned config file). Never in this repo, never in git history, never in command-line arguments visible tops. - Tokens (access + refresh) live in
~/.local/state/da-cli/state.json(0600). Refreshing happens automatically before every API call. - Network: all DA traffic is HTTPS. Image downloads from wixmp CDN are also HTTPS.
- PKCE is mandatory: the CLI generates a fresh PKCE
code_verifier/code_challengepair for everyda authrun. The verifier never leaves your machine; only the SHA-256 hash is sent in the auth URL. - No telemetry, no third-party code at runtime. Stdlib only —
urllib,argparse,json,hashlib,subprocess(for the macOSsecuritykeychain helper). .gitignoreexcludesconfig.json,state.json,sync.log,*.log, and*.tmp— secrets-bearing files cannot be committed by accident.
Examples
The examples/ directory has runnable recipes: a
post-sync webhook, a cron health check, a CSV export, and a topic
browser that lists what a curated DA topic holds. Each is a short shell
or Python script you can copy and edit.
Contributing
Style: stdlib only at runtime. If a feature would need a third-party library, open an issue first. See CONTRIBUTING.md for the lint/type/test bar (every change must pass ruff check, ruff format --check, mypy dacli, and pytest with ≥92 % coverage). See ARCHITECTURE.md for a map of the package.
The Makefile wraps the four-command pipeline:
make dev-setup # one-time venv + dev-deps install
make check # ruff + ruff format --check + mypy + pytest (what CI runs)
Pre-commit hooks mirror CI locally — install once with
pip install pre-commit && pre-commit install, then every git commit
runs ruff / mypy / markdownlint / gitleaks. Config in
.pre-commit-config.yaml.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file da_sync-0.1.0.tar.gz.
File metadata
- Download URL: da_sync-0.1.0.tar.gz
- Upload date:
- Size: 224.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21ed37465900ad32d382a2448d5ef11eac2b8266ffebc5ee167db179652f4f18
|
|
| MD5 |
b8c2ec9ca28c1fa165d49382444a706a
|
|
| BLAKE2b-256 |
8c469f0ffd1310c7efba3ce3a2510dac182ae53ca624d13a5533e58c1b211a36
|
Provenance
The following attestation bundles were made for da_sync-0.1.0.tar.gz:
Publisher:
release.yml on FZ2000/da-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
da_sync-0.1.0.tar.gz -
Subject digest:
21ed37465900ad32d382a2448d5ef11eac2b8266ffebc5ee167db179652f4f18 - Sigstore transparency entry: 2299009609
- Sigstore integration time:
-
Permalink:
FZ2000/da-cli@64606958e9550007d7470febcc987b3227cd17bf -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/FZ2000
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@64606958e9550007d7470febcc987b3227cd17bf -
Trigger Event:
push
-
Statement type:
File details
Details for the file da_sync-0.1.0-py3-none-any.whl.
File metadata
- Download URL: da_sync-0.1.0-py3-none-any.whl
- Upload date:
- Size: 92.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74a359616948ee5c6bbd21de48f31df975124acf2384a2f9317b6158674711b2
|
|
| MD5 |
169686fc5f6be2b3755caf2ae8c51c49
|
|
| BLAKE2b-256 |
52e53c883c53811131abd516d8d68c70e27b9c9f217d1be68408350d7737d92a
|
Provenance
The following attestation bundles were made for da_sync-0.1.0-py3-none-any.whl:
Publisher:
release.yml on FZ2000/da-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
da_sync-0.1.0-py3-none-any.whl -
Subject digest:
74a359616948ee5c6bbd21de48f31df975124acf2384a2f9317b6158674711b2 - Sigstore transparency entry: 2299009639
- Sigstore integration time:
-
Permalink:
FZ2000/da-cli@64606958e9550007d7470febcc987b3227cd17bf -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/FZ2000
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@64606958e9550007d7470febcc987b3227cd17bf -
Trigger Event:
push
-
Statement type: