crivo 0.1.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.
- crivo-0.1.0/.claude/agents/changelog-updater.md +98 -0
- crivo-0.1.0/.claude/agents/commit.md +99 -0
- crivo-0.1.0/.claude/agents/test-runner.md +51 -0
- crivo-0.1.0/.env.example +6 -0
- crivo-0.1.0/.gitattributes +16 -0
- crivo-0.1.0/.githooks/commit-msg +15 -0
- crivo-0.1.0/.github/workflows/ci.yml +91 -0
- crivo-0.1.0/.github/workflows/publish.yml +52 -0
- crivo-0.1.0/.gitignore +30 -0
- crivo-0.1.0/CHANGELOG.md +137 -0
- crivo-0.1.0/CLAUDE.md +253 -0
- crivo-0.1.0/CONTRIBUTING.md +112 -0
- crivo-0.1.0/LICENSE +25 -0
- crivo-0.1.0/PKG-INFO +235 -0
- crivo-0.1.0/README.md +216 -0
- crivo-0.1.0/TASKS.md +85 -0
- crivo-0.1.0/TODO.md +41 -0
- crivo-0.1.0/docs/requirements.md +185 -0
- crivo-0.1.0/docs/screenshots/selection-after.png +0 -0
- crivo-0.1.0/docs/screenshots/selection-before.png +0 -0
- crivo-0.1.0/examples/keywords_sample.txt +19 -0
- crivo-0.1.0/pyproject.toml +57 -0
- crivo-0.1.0/scripts/__init__.py +0 -0
- crivo-0.1.0/scripts/commit_lint.py +221 -0
- crivo-0.1.0/scripts/release.py +168 -0
- crivo-0.1.0/scripts/test_commit_lint.py +139 -0
- crivo-0.1.0/scripts/test_release.py +111 -0
- crivo-0.1.0/scripts/test_version.py +92 -0
- crivo-0.1.0/scripts/version.py +131 -0
- crivo-0.1.0/setup.cfg +4 -0
- crivo-0.1.0/src/crivo/__init__.py +9 -0
- crivo-0.1.0/src/crivo/__main__.py +5 -0
- crivo-0.1.0/src/crivo/cli.py +497 -0
- crivo-0.1.0/src/crivo/config.py +210 -0
- crivo-0.1.0/src/crivo/downloader.py +242 -0
- crivo-0.1.0/src/crivo/keywords.py +186 -0
- crivo-0.1.0/src/crivo/packager.py +166 -0
- crivo-0.1.0/src/crivo/pixabay_client.py +544 -0
- crivo-0.1.0/src/crivo/resizer.py +148 -0
- crivo-0.1.0/src/crivo/search_runner.py +149 -0
- crivo-0.1.0/src/crivo/selection.py +434 -0
- crivo-0.1.0/src/crivo/selector_ui.py +236 -0
- crivo-0.1.0/src/crivo/session_store.py +145 -0
- crivo-0.1.0/src/crivo/static/selector.css +93 -0
- crivo-0.1.0/src/crivo/static/selector.html +48 -0
- crivo-0.1.0/src/crivo/static/selector.js +274 -0
- crivo-0.1.0/src/crivo.egg-info/PKG-INFO +235 -0
- crivo-0.1.0/src/crivo.egg-info/SOURCES.txt +64 -0
- crivo-0.1.0/src/crivo.egg-info/dependency_links.txt +1 -0
- crivo-0.1.0/src/crivo.egg-info/entry_points.txt +2 -0
- crivo-0.1.0/src/crivo.egg-info/requires.txt +6 -0
- crivo-0.1.0/src/crivo.egg-info/scm_file_list.json +60 -0
- crivo-0.1.0/src/crivo.egg-info/scm_version.json +8 -0
- crivo-0.1.0/src/crivo.egg-info/top_level.txt +1 -0
- crivo-0.1.0/tests/fakes.py +129 -0
- crivo-0.1.0/tests/test_cli.py +760 -0
- crivo-0.1.0/tests/test_config.py +160 -0
- crivo-0.1.0/tests/test_downloader.py +205 -0
- crivo-0.1.0/tests/test_keywords.py +163 -0
- crivo-0.1.0/tests/test_packager.py +211 -0
- crivo-0.1.0/tests/test_pixabay_client.py +431 -0
- crivo-0.1.0/tests/test_resizer.py +247 -0
- crivo-0.1.0/tests/test_search_runner.py +154 -0
- crivo-0.1.0/tests/test_selection.py +537 -0
- crivo-0.1.0/tests/test_selector_ui.py +465 -0
- crivo-0.1.0/tests/test_session_store.py +146 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: changelog-updater
|
|
3
|
+
description: Adds an entry to CHANGELOG.md for a feature or fix that was just built. Use after finishing a change, when asked to update/record something in the changelog, or to backfill changelog entries for recent commits.
|
|
4
|
+
tools: Bash, Read, Edit
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You maintain `CHANGELOG.md` at the repo root. Your job is to add accurate entries for work that has actually happened — nothing else. You do not write code, do not fix bugs, and do not commit or push; you edit exactly one file.
|
|
9
|
+
|
|
10
|
+
## Figure out what changed
|
|
11
|
+
|
|
12
|
+
Start by establishing what you're describing. Run in parallel:
|
|
13
|
+
|
|
14
|
+
- `git status` and `git diff HEAD` — uncommitted work in the tree
|
|
15
|
+
- `git log --format='%h|%ad|%s' --date=short -15` — recent commits
|
|
16
|
+
- `sed -n '1,60p' CHANGELOG.md` — the current top of the file, so you see what's already recorded
|
|
17
|
+
|
|
18
|
+
Then decide the scope:
|
|
19
|
+
|
|
20
|
+
- **Uncommitted changes present** → describe those. That's the normal case: you're invoked right after a change is finished, before it's committed.
|
|
21
|
+
- **Clean tree** → describe the commits since the last one already covered by the changelog. Compare commit subjects against the entries under the most recent date heading; anything already there stays there, don't duplicate it.
|
|
22
|
+
- **The user named a specific commit, PR, or feature** → describe that, and ignore everything else.
|
|
23
|
+
|
|
24
|
+
Read the actual diff, not just the commit subject. The subject says what someone called the change; the diff says what it does. If they disagree, trust the diff and say so in your report.
|
|
25
|
+
|
|
26
|
+
## Where the entry goes
|
|
27
|
+
|
|
28
|
+
The file is grouped by **date**, newest first — versions are git tags derived from commit types (`python -m scripts.release`), so **never invent a version number or a `## [1.2.0]`-style heading.** Retitling `## [Unreleased]` is a release step the release script reminds a person about, not yours.
|
|
29
|
+
|
|
30
|
+
Structure, top to bottom:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
# Changelog
|
|
34
|
+
<intro paragraph>
|
|
35
|
+
|
|
36
|
+
## [Unreleased] <- points at TODO.md; leave it alone
|
|
37
|
+
|
|
38
|
+
## 2026-08-22 <- most recent day of work
|
|
39
|
+
### Added
|
|
40
|
+
### Changed
|
|
41
|
+
### Fixed
|
|
42
|
+
### Security <- only when relevant
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To place a new entry:
|
|
46
|
+
|
|
47
|
+
1. Get today's date with `date +%F`.
|
|
48
|
+
2. If a `## <today>` heading already exists, add your bullet under the right subsection there (creating the subsection if it's missing).
|
|
49
|
+
3. If not, insert a new `## <today>` heading directly *below* the `## [Unreleased]` block and above the previous most-recent date.
|
|
50
|
+
4. Subsection order within a date is always: Added, Changed, Fixed, Security, Removed. Only include the ones you actually have bullets for.
|
|
51
|
+
|
|
52
|
+
Backdating is only correct when you're backfilling older commits — use the commit's own author date (`%ad`) for those, not today's.
|
|
53
|
+
|
|
54
|
+
## Classifying
|
|
55
|
+
|
|
56
|
+
- **Added** — a capability that didn't exist before (new page, new endpoint, new action).
|
|
57
|
+
- **Changed** — existing behaviour now works differently, including UI/wording changes and refactors with a user-visible effect.
|
|
58
|
+
- **Fixed** — something was broken and now isn't. A fix for a bug that was never released still goes here.
|
|
59
|
+
- **Security** — auth, sessions, secrets, access control, dependency CVEs. These get their own bullets even if they'd otherwise read as a fix.
|
|
60
|
+
- **Removed** — a capability deliberately taken away.
|
|
61
|
+
|
|
62
|
+
Judgment calls that matter here:
|
|
63
|
+
|
|
64
|
+
- A change that's purely internal with **no observable effect** (formatting, comments, TODO.md edits, test-only changes, tooling that doesn't ship) usually does **not** belong in the changelog. Say so in your report rather than padding the file. Exceptions worth an entry: a new test suite or dev tooling that other people will use, and anything that changes how the project is run or configured.
|
|
65
|
+
- An investigation that concluded "no defect found" is not a Fixed entry.
|
|
66
|
+
|
|
67
|
+
## Writing the bullet
|
|
68
|
+
|
|
69
|
+
Match the voice already in the file: plain language, present tense, describing the software's behaviour rather than the commit.
|
|
70
|
+
|
|
71
|
+
- Good: *"'Behind' is measured against aired episodes, so unaired episodes no longer make a caught-up show look behind."*
|
|
72
|
+
- Bad: *"Fixed bug #23 in status.util.ts"* — a reader shouldn't need the repo open to understand it.
|
|
73
|
+
|
|
74
|
+
Rules:
|
|
75
|
+
|
|
76
|
+
- One bullet per user-facing change. A single commit can produce two bullets; two commits doing one thing produce one.
|
|
77
|
+
- No conventional-commit prefixes (`feat:`, `fix:`), no task IDs (`(CRIVO-12)`), no bug numbers as the entire bullet, no commit hashes. Commit *subjects* carry the type prefix and the ID by convention (`CLAUDE.md` → Commit messages) — strip both when the subject becomes a changelog bullet. The changelog is read by users, who have neither the type taxonomy nor the ledger.
|
|
78
|
+
- Name files or symbols only when they're genuinely the clearest way to say it (e.g. an env var, an endpoint path, a config key). Otherwise describe behaviour.
|
|
79
|
+
- Include the *why* when the change would otherwise look arbitrary, in the same sentence — don't add a separate rationale line.
|
|
80
|
+
- Wrap prose at roughly 80 columns, matching the rest of the file.
|
|
81
|
+
|
|
82
|
+
## Editing
|
|
83
|
+
|
|
84
|
+
Use `Edit` for surgical insertions. Never rewrite `CHANGELOG.md` wholesale, and never reorder, reword, or delete entries that are already there — earlier entries are a record, not a draft. The one exception: if you find a demonstrably wrong existing entry (it describes behaviour the code doesn't have), fix it and call that out explicitly in your report.
|
|
85
|
+
|
|
86
|
+
Leave the `## [Unreleased]` section as-is unless the user asks you to change it. Open work lives in `TODO.md`, which that section links to — it is not your job to sync the two.
|
|
87
|
+
|
|
88
|
+
## If there's nothing to record
|
|
89
|
+
|
|
90
|
+
If everything you found is internal-only, or already covered by existing entries, make no edit and say that plainly. An unchanged changelog is a valid outcome; a manufactured entry is not.
|
|
91
|
+
|
|
92
|
+
## Do not commit
|
|
93
|
+
|
|
94
|
+
You never run `git add`, `git commit`, or `git push`. Leave the edited file in the working tree for whoever invoked you — they'll bundle it with the change it describes.
|
|
95
|
+
|
|
96
|
+
## Report
|
|
97
|
+
|
|
98
|
+
State: which change(s) you described (source: working tree, or commit hashes), the exact bullet text you added and under which date/subsection heading, and anything you deliberately left out with the reason. If you had to guess at intent from an ambiguous diff, say where.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commit
|
|
3
|
+
description: Commits the current working-tree changes on a fresh branch, pushes it, merges it into main, and deletes the branch locally and on the remote. Use when explicitly asked to commit and push, ship the current changes, or save and push this work.
|
|
4
|
+
tools: Bash
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You commit the current changes, push them, land them on `main`, and clean up after yourself. Being invoked at all is the user's authorization to do all of it — don't stop partway to ask "should I push?" or "should I merge?"; that's the whole point of asking for you by name. That said, still exercise judgment: this pushes to a real remote and moves `main`, so follow the safety rules below exactly.
|
|
9
|
+
|
|
10
|
+
The shape of the whole job, in order: **branch → commit → push branch → land on `main` → push `main` → delete the branch both places.** Never commit directly on `main`, even when the change is one line, and never leave the feature branch behind once it has landed.
|
|
11
|
+
|
|
12
|
+
**No remote yet.** Crivo began life without an `origin`. Run `git remote` first: if it prints nothing, every step below that mentions the remote — `git push`, `git pull`, `git fetch --prune`, `git push origin --delete` — is skipped, the rest of the flow is unchanged (branch → commit → land on `main` with `--ff-only` → delete the local branch), and the report says the push steps were skipped for want of a remote. Do not add a remote, and do not treat the missing one as a failure.
|
|
13
|
+
|
|
14
|
+
## 1. Branch
|
|
15
|
+
|
|
16
|
+
1. Run in parallel: `git status` (never `-uall`), `git diff` (staged + unstaged), `git log --oneline -10` (to see this repo's message and branch-name style), and `git branch --show-current`.
|
|
17
|
+
2. If the tree is clean, stop — see "If there's nothing to commit" below. Do this before creating a branch, so you don't leave an empty one lying around.
|
|
18
|
+
3. Create a new branch off the current one and switch to it: `git checkout -b <name>`. Derive `<name>` from the change itself as a plain `kebab-case-summary` of what the work does.
|
|
19
|
+
|
|
20
|
+
**Never put a TODO priority in the branch name**, even when the change came straight off a prioritised TODO entry (`TODO.md` tags its items `[P1]`–`[P4]`). The priority is a fact about the backlog at one moment, not about the change, and it goes stale the instant the entry is re-prioritised or removed. So: `crivo-9-resize-to-common-size`, not `p1-crivo-9-resize-to-common-size`.
|
|
21
|
+
|
|
22
|
+
**Do put the task ID in the branch name.** Every task carries a `CRIVO-<n>` ID registered in `TASKS.md` (see `CLAUDE.md`) — `WINNOWER-<n>` is the retired prefix a handful of tasks opened before the rename still carry, permanently — and the branch leads with it lowercased: `crivo-12-show-candidate-tags`. If the work came off a `TODO.md` entry, that entry already has an ID — reuse it, don't take a new one. If it has none, take the next free number from the top of `TASKS.md`. The ID prefix and the banned priority prefix are different things: `crivo-12-...` is right, `p1-...` is wrong, and `p1-crivo-12-...` is wrong twice.
|
|
23
|
+
4. If you are *already* on a non-`main` branch when invoked, stay on it rather than branching off a branch — commit there and treat that as the feature branch for the rest of the flow. Don't rename it to strip a priority prefix — leave the branch as the user made it. Nothing carries that name onto `main` anyway — step 4 never records the branch name.
|
|
24
|
+
|
|
25
|
+
## 2. Commit
|
|
26
|
+
|
|
27
|
+
1. Look at what's actually changed and draft a commit message:
|
|
28
|
+
- Focus on *why*, not a restatement of the diff. The body carries the real context — what was wrong, why this approach, what was decided and rejected, how it was verified. This repo's commits are substantial; a one-line commit for a real change is under-written here.
|
|
29
|
+
- **Use Conventional Commits** (`CLAUDE.md` → Commit messages has the full rules): `<type>[optional scope][!]: <description> (CRIVO-<n>)`. Type is required and lowercase — `feat` and `fix` first, then `docs`, `refactor`, `perf`, `test`, `build`, `ci`, `chore` for changes that ship no behaviour. Optional scope names the area of the code, never the task. The description stays a capitalised, plain-language imperative summary with no trailing full stop.
|
|
30
|
+
|
|
31
|
+
This repo *used* to ban `feat:`/`fix:` prefixes. That rule is gone — do not reinstate it.
|
|
32
|
+
- **The task ID goes last, in parentheses**: `feat: Show each candidate's tags under its thumbnail (CRIVO-12)`. Same ID as the branch, and the same ID the `TODO.md` entry already carried if the work came off one. A commit closing two tasks names both: `(CRIVO-6, CRIVO-7)`. `WINNOWER-<n>` is never renumbered, so closing one of the tasks still open under that prefix still names it that way. Pure bookkeeping — a typo fix, a formatting pass, a `TODO.md` tidy — may go unnumbered rather than inflating the counter; when in doubt, number it.
|
|
33
|
+
- **Breaking changes** take a `!` before the colon *and* a `BREAKING CHANGE:` footer saying what to migrate.
|
|
34
|
+
- **No TODO priority marker** (`[P1]`, `p1-`) in the subject or the body. When the work came off a TODO entry, refer to that entry by what it says, not by how it was ranked.
|
|
35
|
+
- If the changes clearly span unrelated concerns (e.g. an unrelated leftover edit sitting alongside the real work), say so in your final report rather than silently bundling everything into one commit — but default to one commit unless the split is obvious and cheap.
|
|
36
|
+
2. **Update `TASKS.md` in the same commit.** If you took a new ID, increment the "Next ID to assign" line at the top of that file. If the work closes an open item, move its row from the Open table to the Done table with today's date and status `done`, and delete the corresponding `TODO.md` entry. Leave the Commit column as `—`: a commit cannot contain its own SHA, and it does not need to, because the ID is in the subject and `git log --grep='CRIVO-<n>'` finds it. The ledger and the commit that changes it always land together — a bumped counter with no commit behind it is how IDs get handed out twice.
|
|
37
|
+
3. **Stage deliberately** — add the specific files that make up this change by name, not `git add -A`/`git add .`. Before staging anything, check for files that look like secrets or personal data (`.env`, `credentials.*`, unexpected `.csv`/DB dumps) and leave them out; if something suspicious is already staged, unstage it and flag it in your report instead of committing it.
|
|
38
|
+
4. Create the commit with a HEREDOC-authored message ending in:
|
|
39
|
+
```
|
|
40
|
+
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
41
|
+
```
|
|
42
|
+
5. If a pre-commit hook fails, fix the underlying issue, re-stage, and make a **new** commit — never `--amend` a commit that a failed hook prevented from being created, and never `--no-verify` to route around a hook.
|
|
43
|
+
6. Never use `git commit --amend` here at all unless the user's request in this invocation explicitly said "amend" — default to a new commit.
|
|
44
|
+
|
|
45
|
+
## 3. Push the branch
|
|
46
|
+
|
|
47
|
+
1. Push with upstream tracking: `git push -u origin <branch>`.
|
|
48
|
+
2. **Never force-push** (`--force`, `--force-with-lease`). If a push is rejected because the remote has diverged, stop and report that clearly instead of forcing — reconciling divergent history is a judgment call for the user, not something to do silently here.
|
|
49
|
+
3. Never skip hooks or bypass signing (`--no-verify`, `--no-gpg-sign`, `-c commit.gpgsign=false`).
|
|
50
|
+
|
|
51
|
+
## 4. Land it on `main`
|
|
52
|
+
|
|
53
|
+
1. `git checkout main`, then bring it up to date: `git pull --ff-only origin main`. If that fails because local `main` has diverged from the remote, **stop** and report it — do not land onto a `main` you couldn't fast-forward, and do not reset or force anything to fix it.
|
|
54
|
+
2. Count what the branch adds — `git rev-list --count main..<branch>` — and let that number pick the method:
|
|
55
|
+
|
|
56
|
+
**One commit → fast-forward.** The normal case here.
|
|
57
|
+
```
|
|
58
|
+
git merge --ff-only <branch>
|
|
59
|
+
```
|
|
60
|
+
`main` simply moves onto the commit you already wrote. No new commit, no merge commit, no second subject to compose — the message from step 2 is the record on `main`, and the only one.
|
|
61
|
+
|
|
62
|
+
**Two or more → squash.**
|
|
63
|
+
```
|
|
64
|
+
git merge --squash <branch>
|
|
65
|
+
git commit
|
|
66
|
+
```
|
|
67
|
+
The branch's commits collapse into a single new commit on `main`. Note that `--squash` stages the changes but does **not** commit — the second command is required, and skipping it means nothing landed. Author that message with a HEREDOC under the same rules as step 2, summarising the branch as a whole, ending with the same `Co-Authored-By:` line.
|
|
68
|
+
|
|
69
|
+
**Never `--no-ff`.** An explicit merge commit here would duplicate the work commit's subject and add nothing to `main` but a second line in `git log` — that is the exact outcome this step exists to prevent.
|
|
70
|
+
3. If `--ff-only` is refused because `main` has moved ahead of where the branch was cut, the branch needs replaying — not a merge commit:
|
|
71
|
+
```
|
|
72
|
+
git checkout <branch>
|
|
73
|
+
git rebase main
|
|
74
|
+
git checkout main
|
|
75
|
+
git merge --ff-only <branch>
|
|
76
|
+
```
|
|
77
|
+
Do **not** force-push the rebased branch to reconcile it with its stale remote copy; step 5 deletes both copies regardless, so the divergence never outlives this run.
|
|
78
|
+
4. If the rebase or the squash conflicts, **stop**: leave the conflict in place, report which files conflict, and let the user resolve it. Do not guess at a resolution, and do not `git rebase --abort` or `git merge --abort` without saying you did.
|
|
79
|
+
5. Push: `git push origin main`.
|
|
80
|
+
|
|
81
|
+
## 5. Delete the branch
|
|
82
|
+
|
|
83
|
+
Only once `main` is pushed and the remote actually contains the change:
|
|
84
|
+
|
|
85
|
+
1. Delete the local branch. Which flag is correct depends on how step 4 landed it:
|
|
86
|
+
- **After a fast-forward:** `git branch -d <branch>` — the lowercase `-d`, which refuses to delete anything not fully merged. **Never `-D`** here: if `-d` refuses, that's a real signal the change didn't land, so stop and report instead of overriding it.
|
|
87
|
+
- **After a squash:** `-d` will refuse, and that refusal is expected rather than a warning — a squash writes a new commit and leaves no link back to the branch, so git genuinely cannot see it as merged. Verify the change landed before overriding: `git log --oneline -1 main` shows your squash commit, and `git diff main <branch>` prints nothing. With **both** confirmed, use `git branch -D <branch>`. If that `git diff` is not empty, something didn't land — stop and report.
|
|
88
|
+
2. `git push origin --delete <branch>`.
|
|
89
|
+
3. `git fetch --prune`, then confirm with `git branch -a` that neither the local branch nor `origin/<branch>` remains.
|
|
90
|
+
|
|
91
|
+
If either deletion fails, say so explicitly — a branch left behind is a small problem, but silently leaving one is a confusing one.
|
|
92
|
+
|
|
93
|
+
## If there's nothing to commit
|
|
94
|
+
|
|
95
|
+
If `git status` shows a clean tree, say so and stop — don't manufacture an empty commit, and don't create a branch you'd then have to delete.
|
|
96
|
+
|
|
97
|
+
## Report
|
|
98
|
+
|
|
99
|
+
State plainly, in order: the branch you created, what was committed (files + one-line summary of the message) and its hash, how it reached `main` (fast-forwarded, or squashed — with the squash commit's hash), that `main` was pushed, and that both copies of the branch were deleted. If you left anything out (suspicious files, unrelated changes) or stopped early (rejected push, failed fast-forward, rebase or squash conflict, refused branch delete), say exactly what and why, and what state the repo is in as a result — which branch is checked out, what has and hasn't been pushed.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-runner
|
|
3
|
+
description: Runs Crivo's automated checks (ruff lint and format, pytest for the package and the repo tooling) and reports a pass/fail summary. Use when asked to run the tests, verify tests pass, or check for regressions before a commit.
|
|
4
|
+
tools: Bash
|
|
5
|
+
model: haiku
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You run Crivo's checks and report results. You do not fix failures yourself unless explicitly asked — your job is to run everything and report clearly what passed and what didn't, with enough detail that whoever reads the report can act on it without re-running anything.
|
|
9
|
+
|
|
10
|
+
## Layers, in order
|
|
11
|
+
|
|
12
|
+
Run from the repo root, using the project's virtualenv if `.venv/` exists (`.venv/Scripts/python` on Windows, `.venv/bin/python` elsewhere), otherwise whatever `python` is on the path.
|
|
13
|
+
|
|
14
|
+
1. **Lint** — `python -m ruff check .`
|
|
15
|
+
2. **Format** — `python -m ruff format --check .`
|
|
16
|
+
3. **Crivo** — `python -m pytest tests` (the package: keywords, client, runner, downloader, resizer, packager, the selection UI over real HTTP on an ephemeral port)
|
|
17
|
+
4. **Repo tooling** — `python -m pytest scripts` (the commit-message linter and the release derivation)
|
|
18
|
+
|
|
19
|
+
Run them sequentially. Each is a few seconds; use a reasonable timeout anyway.
|
|
20
|
+
|
|
21
|
+
## Isolation
|
|
22
|
+
|
|
23
|
+
- Nothing here touches the network or needs a Pixabay API key: the client takes an injectable session, clock and sleep, and images are generated with Pillow. If a test appears to be reaching the real API, that is a bug in the test — report it, don't work around it.
|
|
24
|
+
- Do not read, print or copy `.env`. It holds a real API key.
|
|
25
|
+
- The selection-UI tests bind an ephemeral localhost port. If one fails on a bind error, report that distinctly from an assertion failure.
|
|
26
|
+
- Do not delete `.crivo/`: it may hold a user's unfinished session.
|
|
27
|
+
|
|
28
|
+
## If something fails
|
|
29
|
+
|
|
30
|
+
Capture the actual failing test name(s) and the relevant error output (assertion diff, stack trace excerpt) — not just "N tests failed". If a layer fails before any test runs (an import error, a missing dependency, a lint error), report that distinctly from a failed assertion, since the fix is different. If `ruff` or `pytest` is not installed, say so and suggest `pip install -e ".[dev]"` rather than installing anything.
|
|
31
|
+
|
|
32
|
+
## Report format
|
|
33
|
+
|
|
34
|
+
End with a concise summary, e.g.:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Lint: clean
|
|
38
|
+
Format: clean
|
|
39
|
+
Crivo: 84/84 passed
|
|
40
|
+
Repo tooling: 31/31 passed
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
or, on failure:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Crivo: 83/84 passed — FAILED: tests/test_resizer.py::test_pad_keeps_alpha_for_png
|
|
47
|
+
Expected: (0, 0, 0, 0), Received: (255, 255, 255, 255)
|
|
48
|
+
at tests/test_resizer.py:57
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If everything passes, "all N tests passed, lint and format clean" is enough.
|
crivo-0.1.0/.env.example
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Copy this file to `.env` (which is gitignored) and paste your own key.
|
|
2
|
+
#
|
|
3
|
+
# Every user needs their own free Pixabay API key; this project does not ship or
|
|
4
|
+
# share one. Log in at https://pixabay.com/api/docs/ and the key is shown in the
|
|
5
|
+
# parameters table. Never commit it.
|
|
6
|
+
PIXABAY_API_KEY=your_key_here
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Line endings are settled here rather than per-machine, so a checkout on
|
|
2
|
+
# Windows, macOS and Linux produces byte-identical files. Without this, a
|
|
3
|
+
# Windows clone with core.autocrlf=true rewrites every file to CRLF and Git
|
|
4
|
+
# then announces it on every command, and the .githooks/commit-msg shell script
|
|
5
|
+
# would not even run there (`sh` rejects a CRLF shebang line).
|
|
6
|
+
* text=auto eol=lf
|
|
7
|
+
|
|
8
|
+
# Never normalize and never diff as text. Crivo handles images all day, and
|
|
9
|
+
# a test fixture or README screenshot corrupted by line-ending conversion is a
|
|
10
|
+
# confusing way to lose an afternoon.
|
|
11
|
+
*.png binary
|
|
12
|
+
*.jpg binary
|
|
13
|
+
*.jpeg binary
|
|
14
|
+
*.gif binary
|
|
15
|
+
*.webp binary
|
|
16
|
+
*.zip binary
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Lints the commit message against CLAUDE.md -> Commit messages.
|
|
3
|
+
# Enabled per clone with: git config core.hooksPath .githooks
|
|
4
|
+
#
|
|
5
|
+
# Tries the interpreters in the order a contributor is likeliest to have a working
|
|
6
|
+
# one. `python3` is tried first on Linux/macOS, but on Windows it can be the
|
|
7
|
+
# Microsoft Store stub, which exists on PATH and fails when run; running
|
|
8
|
+
# `-c pass` first tells a real interpreter from the stub.
|
|
9
|
+
for py in python3 python py; do
|
|
10
|
+
if "$py" -c pass >/dev/null 2>&1; then
|
|
11
|
+
exec "$py" -m scripts.commit_lint --edit "$1"
|
|
12
|
+
fi
|
|
13
|
+
done
|
|
14
|
+
echo "commit-msg hook: no working Python found on PATH (tried python3, python, py)." >&2
|
|
15
|
+
exit 1
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Runs the checks that previously only ran when someone remembered to ask.
|
|
2
|
+
#
|
|
3
|
+
# Work lands on main by fast-forward rather than through pull requests, so `push` is
|
|
4
|
+
# the trigger that actually fires. `pull_request` is kept so the workflow still works
|
|
5
|
+
# if that ever changes.
|
|
6
|
+
name: CI
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
pull_request:
|
|
12
|
+
|
|
13
|
+
# A newer push to the same ref makes the older run pointless.
|
|
14
|
+
concurrency:
|
|
15
|
+
group: ci-${{ github.ref }}
|
|
16
|
+
cancel-in-progress: true
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
test:
|
|
20
|
+
name: Tests — ${{ matrix.os }}, Python ${{ matrix.python }}
|
|
21
|
+
runs-on: ${{ matrix.os }}
|
|
22
|
+
strategy:
|
|
23
|
+
fail-fast: false
|
|
24
|
+
# Portability across Windows, macOS and Linux is a stated requirement, so all
|
|
25
|
+
# three run on every push. 3.10 is the floor `pyproject.toml` promises and
|
|
26
|
+
# 3.13 the newest that is widely installed; the versions between them have
|
|
27
|
+
# never broken anything this project does, so they are not each paid for.
|
|
28
|
+
matrix:
|
|
29
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
30
|
+
python: ['3.10', '3.13']
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
# Full history: setuptools-scm derives the version from tags, and the tooling
|
|
34
|
+
# tests build their own repositories but the install reads this one.
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
with:
|
|
37
|
+
fetch-depth: 0
|
|
38
|
+
|
|
39
|
+
- uses: actions/setup-python@v5
|
|
40
|
+
with:
|
|
41
|
+
python-version: ${{ matrix.python }}
|
|
42
|
+
cache: pip
|
|
43
|
+
cache-dependency-path: pyproject.toml
|
|
44
|
+
|
|
45
|
+
- name: Install
|
|
46
|
+
run: python -m pip install -e ".[dev]"
|
|
47
|
+
|
|
48
|
+
# Once is enough: formatting does not differ by platform.
|
|
49
|
+
- name: Lint
|
|
50
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.13'
|
|
51
|
+
run: python -m ruff check .
|
|
52
|
+
|
|
53
|
+
- name: Format
|
|
54
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.13'
|
|
55
|
+
run: python -m ruff format --check .
|
|
56
|
+
|
|
57
|
+
# Both suites: the package (tests/) and the repo tooling (scripts/). No test
|
|
58
|
+
# needs a Pixabay key or the network.
|
|
59
|
+
- name: Tests
|
|
60
|
+
run: python -m pytest
|
|
61
|
+
|
|
62
|
+
commits:
|
|
63
|
+
name: Commit messages
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
|
|
66
|
+
steps:
|
|
67
|
+
# The linter needs the commits themselves, not just the tree.
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
with:
|
|
70
|
+
fetch-depth: 0
|
|
71
|
+
|
|
72
|
+
- uses: actions/setup-python@v5
|
|
73
|
+
with:
|
|
74
|
+
python-version: '3.13'
|
|
75
|
+
|
|
76
|
+
# The commit-msg hook already checks these locally. This catches what the hook
|
|
77
|
+
# cannot: --no-verify, and a clone that never ran
|
|
78
|
+
# `git config core.hooksPath .githooks`.
|
|
79
|
+
- name: Lint the pushed commits
|
|
80
|
+
run: |
|
|
81
|
+
BEFORE='${{ github.event.before }}'
|
|
82
|
+
if [ "${{ github.event_name }}" = 'pull_request' ]; then
|
|
83
|
+
python -m scripts.commit_lint \
|
|
84
|
+
--range '${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}'
|
|
85
|
+
elif [ -z "$BEFORE" ] || [ "$BEFORE" = '0000000000000000000000000000000000000000' ]; then
|
|
86
|
+
# A new branch, or the first push: there is no "before" to diff against,
|
|
87
|
+
# so check the tip and no more.
|
|
88
|
+
python -m scripts.commit_lint --last
|
|
89
|
+
else
|
|
90
|
+
python -m scripts.commit_lint --range "$BEFORE..${{ github.sha }}"
|
|
91
|
+
fi
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Publishes to PyPI when a release tag is pushed.
|
|
2
|
+
#
|
|
3
|
+
# `python -m scripts.release --write` (CONTRIBUTING.md -> Releases) creates the tag locally
|
|
4
|
+
# and deliberately never pushes it, so the tag push remains the one manual, on-purpose step
|
|
5
|
+
# that ships a release. This workflow is what that push now triggers: build the sdist and
|
|
6
|
+
# wheel setuptools-scm versions from the tag, and hand them to PyPI.
|
|
7
|
+
#
|
|
8
|
+
# Uses PyPI's trusted publishing (OIDC), not a token: nothing to rotate or leak in a repo
|
|
9
|
+
# secret. Set it up once at https://pypi.org/manage/account/publishing/ with this repo,
|
|
10
|
+
# workflow filename (publish.yml) and the `pypi` environment below.
|
|
11
|
+
name: Publish
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
tags: ['v*']
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
name: Build sdist and wheel
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0 # setuptools-scm needs the tag history, not just this commit
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: '3.13'
|
|
29
|
+
|
|
30
|
+
- run: python -m pip install build
|
|
31
|
+
- run: python -m build
|
|
32
|
+
|
|
33
|
+
- uses: actions/upload-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
publish:
|
|
39
|
+
name: Publish to PyPI
|
|
40
|
+
needs: build
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
environment: pypi # matches the trusted publisher config on PyPI; add required
|
|
43
|
+
# reviewers here (Settings -> Environments -> pypi) for a manual approval gate.
|
|
44
|
+
permissions:
|
|
45
|
+
id-token: write # required for trusted publishing; no API token stored anywhere
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/download-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: dist
|
|
50
|
+
path: dist/
|
|
51
|
+
|
|
52
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
crivo-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Secrets. Each user brings their own Pixabay key (README -> Setup); the repo
|
|
2
|
+
# ships .env.example and nothing else. See CLAUDE.md -> Pixabay rules.
|
|
3
|
+
.env
|
|
4
|
+
.env.*
|
|
5
|
+
!.env.example
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
.venv/
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
|
|
16
|
+
# Crivo's own working data: cached search responses, downloaded originals
|
|
17
|
+
# and the resumable session. Pixabay images must never be committed
|
|
18
|
+
# (CLAUDE.md -> Pixabay rules), so this is ignored wholesale rather than by
|
|
19
|
+
# file type.
|
|
20
|
+
.crivo/
|
|
21
|
+
crivo-output/
|
|
22
|
+
*.zip
|
|
23
|
+
# Pre-rename (CRIVO-1) default work-dir name; ignored so a checkout that still
|
|
24
|
+
# has one lying around from before the rename doesn't show up as untracked.
|
|
25
|
+
.winnower/
|
|
26
|
+
winnower-output/
|
|
27
|
+
|
|
28
|
+
# Claude Code: per-machine state, not project configuration.
|
|
29
|
+
.claude/settings.local.json
|
|
30
|
+
.claude/scheduled_tasks.lock
|
crivo-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Crivo are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
Entries below are grouped by date of the work rather than by version number.
|
|
7
|
+
Versions are git tags derived from commit types (see `CLAUDE.md`), and a release
|
|
8
|
+
is cut by retitling the `## [Unreleased]` section, not by generating this file.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
Open work is tracked in [TODO.md](TODO.md), and registered by ID in
|
|
13
|
+
[TASKS.md](TASKS.md).
|
|
14
|
+
|
|
15
|
+
## 2026-09-27
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `winnower run` (also `python -m winnower`) takes a list of keywords, searches
|
|
20
|
+
Pixabay for each, opens a page in your browser to pick images, and writes the
|
|
21
|
+
picks, resized to a common size, to a zip. This is the first version that can
|
|
22
|
+
be used from start to finish. It needs your own Pixabay API key.
|
|
23
|
+
- Keywords come from a `.txt` file (one per line), a `.csv` file with a header
|
|
24
|
+
row, standard input (`-`), or `-k` on the command line, repeated for several.
|
|
25
|
+
Blank lines and lines starting with `#` are ignored. Write `label | search
|
|
26
|
+
term` to name the result after one thing and search for another. For a CSV,
|
|
27
|
+
the label is read from a column headed `keyword` or `label` (else the first
|
|
28
|
+
column) and the search term from one headed `term` or `search` (else the
|
|
29
|
+
label), or name the columns with `--column` and `--term-column`. Comma,
|
|
30
|
+
semicolon and tab separators are all understood. A label repeated in a
|
|
31
|
+
different case is dropped and reported, because result files are named after
|
|
32
|
+
labels.
|
|
33
|
+
- Searching shows progress as `n / total`. A keyword with no results, or whose
|
|
34
|
+
search failed, is flagged and shown on the page for you to retry with another
|
|
35
|
+
term, instead of stopping the run. The run pauses, and says why, when Pixabay
|
|
36
|
+
refuses the API key, when the rate limit is used up, or when Pixabay cannot be
|
|
37
|
+
reached three times in a row. Everything found so far is kept, and it never
|
|
38
|
+
loops on its own.
|
|
39
|
+
- Search filters: `--image-type`, `--orientation`, `--category`, `--colors`,
|
|
40
|
+
`--min-width`, `--min-height`, `--safesearch`, `--editors-choice`, `--order`
|
|
41
|
+
and `--lang`, plus `--term-template` (for example `'{term} icon'`) to wrap
|
|
42
|
+
every search term. Options are checked before the first request is made, so a
|
|
43
|
+
typo does not cost a search.
|
|
44
|
+
- Search results are cached for 24 hours, so running the same list again does
|
|
45
|
+
not spend requests, and requests are held to Pixabay's limit of 100 per
|
|
46
|
+
minute.
|
|
47
|
+
- The API key is read only from the `PIXABAY_API_KEY` environment variable or a
|
|
48
|
+
`.env` file, never from a command-line flag, where it would end up in shell
|
|
49
|
+
history and the process list. It is not written to the cache and does not
|
|
50
|
+
appear in error messages.
|
|
51
|
+
- The selection page shows each candidate with its contributor's name and a link
|
|
52
|
+
to its Pixabay page. For each keyword you can pick an image, skip it, search
|
|
53
|
+
again with a different term, or load more results without losing your picks.
|
|
54
|
+
`--multiple` allows more than one pick per keyword, and `--no-browser` prints
|
|
55
|
+
the address instead of opening it. The page is served on this computer only
|
|
56
|
+
and needs a random token that is different on every run, so other programs and
|
|
57
|
+
other websites cannot use it.
|
|
58
|
+
- Picked images are downloaded once the page is finished, and each one is
|
|
59
|
+
checked to be a complete, readable image before it is used, so a dropped
|
|
60
|
+
connection or an error page never ends up as a result. Downloads are kept in
|
|
61
|
+
the work directory (`.winnower` by default, or `--work-dir`) and reused.
|
|
62
|
+
- Picks are resized to `--size` (default 512x512) by `--mode`: `crop` fills the
|
|
63
|
+
size and cuts the overflow, `pad` fits inside and fills the rest, and `fit`
|
|
64
|
+
fits inside without padding, so sizes are not uniform. Output is PNG, JPEG or
|
|
65
|
+
WebP (`--format`). `--background #RRGGBB` sets the fill for `pad`, and what a
|
|
66
|
+
JPEG is flattened onto; the default is transparent, or white for JPEG.
|
|
67
|
+
- The result is a zip (`winnower.zip`, or `-o`) of the resized images, named
|
|
68
|
+
after each keyword's label, with a `CREDITS.txt` naming each image's
|
|
69
|
+
contributor and Pixabay page. An existing zip is not replaced unless you pass
|
|
70
|
+
`--overwrite`, and this is checked before searching, so you are not asked to
|
|
71
|
+
pick images and then told the file exists.
|
|
72
|
+
- Exit codes: 0 when done; 1 for something you can fix, such as a bad option,
|
|
73
|
+
a missing keyword file or API key, an existing output file, or no image being
|
|
74
|
+
produced; 3 when the zip was written but some picked images are missing from
|
|
75
|
+
it, which are listed above the result; 130 when interrupted with Ctrl-C.
|
|
76
|
+
Searches and downloads stay cached after an interruption, and your picks are
|
|
77
|
+
saved too (see the session entries below).
|
|
78
|
+
- Your searches and picks are saved to `session.json` in the work directory
|
|
79
|
+
after every click, so a long run can be put down and picked up later. `winnower
|
|
80
|
+
run --resume` continues it: it needs no keyword file, does not search again
|
|
81
|
+
for anything already searched, and brings the page back as you left it. The
|
|
82
|
+
options that shape the output (`--size`, `--mode`, `--format`, `-o`) are taken
|
|
83
|
+
from the new command, so you can change your mind about them. The file holds
|
|
84
|
+
Pixabay's image addresses and the keywords, and never the API key.
|
|
85
|
+
- Starting a new run while an unfinished session is saved is refused, so a long
|
|
86
|
+
afternoon of picking is not thrown away by accident. This is checked before
|
|
87
|
+
any request is spent. Pass `--restart` to discard it and start over. A session
|
|
88
|
+
whose zip has been written is finished with, and no longer blocks the next
|
|
89
|
+
run.
|
|
90
|
+
- Resuming a session more than a day old still works, but warns that Pixabay's
|
|
91
|
+
image addresses are meant for short-term use, so some thumbnails or downloads
|
|
92
|
+
may no longer load; a keyword can be searched again from the page. A session
|
|
93
|
+
file that cannot be read is reported with `--restart` as the way out, and
|
|
94
|
+
`--resume` is refused, with a reason, when there is nothing saved or the
|
|
95
|
+
saved session is already complete.
|
|
96
|
+
- If the session file cannot be saved (a full disk, a read-only folder), this is
|
|
97
|
+
reported once at the end and the run carries on, since the picks are still
|
|
98
|
+
held in memory.
|
|
99
|
+
- `winnower run` with no keywords at all opens the page with a box to type or
|
|
100
|
+
paste them into, in the same format as a `.txt` file (`label | search term`,
|
|
101
|
+
`#` comments). The search then runs behind the page: it shows how far it has
|
|
102
|
+
got, each keyword appears as soon as it is searched, and you can start
|
|
103
|
+
choosing before the rest are done. A list that cannot be used is refused with
|
|
104
|
+
the reason next to the box, and you can try again. If you close the terminal
|
|
105
|
+
mid-search, `winnower run --resume` brings back everything found so far, with
|
|
106
|
+
the keywords not yet reached marked as not searched.
|
|
107
|
+
- Crivo can now be installed with `pipx install crivo` (or `pip install
|
|
108
|
+
--user crivo`), instead of only from a git clone with a hand-built virtual
|
|
109
|
+
environment — the previous route only made sense for a developer. Pushing a
|
|
110
|
+
release tag (`python -m scripts.release --write`, then `git push --tags`)
|
|
111
|
+
now builds the package and publishes it to PyPI through PyPI's trusted
|
|
112
|
+
publishing, so no API token is stored in the repository; creating the tag
|
|
113
|
+
still never pushes it, so that remains the one deliberate step, now the one
|
|
114
|
+
that also ships the release. Crivo's PyPI page also links back to this
|
|
115
|
+
repository, its issue tracker and this changelog.
|
|
116
|
+
|
|
117
|
+
### Changed
|
|
118
|
+
|
|
119
|
+
- The number of images shown per keyword (`-n`, `--candidates`) must be between
|
|
120
|
+
3 and 200, and defaults to 5, not the 1 to 10 of the original idea. Pixabay
|
|
121
|
+
refuses fewer than 3 results per page, and showing fewer than were fetched
|
|
122
|
+
would make "more results" skip some.
|
|
123
|
+
- Pressing Ctrl-C during `winnower run` now says that your picks are saved and
|
|
124
|
+
that `winnower run --resume` continues them, instead of only mentioning the
|
|
125
|
+
cache.
|
|
126
|
+
|
|
127
|
+
### Fixed
|
|
128
|
+
|
|
129
|
+
- Cropping (`--mode crop`, the default) no longer fails on some real photographs
|
|
130
|
+
with "box offset can't be negative". A rounding error far too small to see, in
|
|
131
|
+
working out which part of the picture to keep, made two of the three
|
|
132
|
+
photographs in the first run against Pixabay fail to resize, so their picks
|
|
133
|
+
were left out of the zip.
|
|
134
|
+
- The selection page no longer occasionally shows a network error instead of a
|
|
135
|
+
clear message when a request is refused (a wrong token or address). On a busy
|
|
136
|
+
computer the refusal could be lost when the connection was closed with the
|
|
137
|
+
request's body still unread; the body is now always read first.
|