django-bookkeeper 0.2.1__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.
- django_bookkeeper-0.2.1/.claude/launch.json +23 -0
- django_bookkeeper-0.2.1/.github/workflows/ci.yml +65 -0
- django_bookkeeper-0.2.1/.github/workflows/publish-pypi.yml +63 -0
- django_bookkeeper-0.2.1/.github/workflows/publish-testpypi.yml +62 -0
- django_bookkeeper-0.2.1/.gitignore +23 -0
- django_bookkeeper-0.2.1/AGENTS.md +140 -0
- django_bookkeeper-0.2.1/CLAUDE.md +3 -0
- django_bookkeeper-0.2.1/Dockerfile.demo +16 -0
- django_bookkeeper-0.2.1/LICENSE +21 -0
- django_bookkeeper-0.2.1/Makefile +35 -0
- django_bookkeeper-0.2.1/PKG-INFO +191 -0
- django_bookkeeper-0.2.1/README.md +132 -0
- django_bookkeeper-0.2.1/demo/__init__.py +0 -0
- django_bookkeeper-0.2.1/demo/demo_project/__init__.py +0 -0
- django_bookkeeper-0.2.1/demo/demo_project/settings.py +67 -0
- django_bookkeeper-0.2.1/demo/demo_project/urls.py +10 -0
- django_bookkeeper-0.2.1/demo/manage.py +11 -0
- django_bookkeeper-0.2.1/demo/management/__init__.py +0 -0
- django_bookkeeper-0.2.1/demo/management/commands/__init__.py +0 -0
- django_bookkeeper-0.2.1/demo/management/commands/seed_demo.py +230 -0
- django_bookkeeper-0.2.1/demo/templates/registration/login.html +134 -0
- django_bookkeeper-0.2.1/docker-compose.yml +24 -0
- django_bookkeeper-0.2.1/pyproject.toml +73 -0
- django_bookkeeper-0.2.1/src/bookkeeper/__init__.py +1 -0
- django_bookkeeper-0.2.1/src/bookkeeper/admin.py +43 -0
- django_bookkeeper-0.2.1/src/bookkeeper/apps.py +10 -0
- django_bookkeeper-0.2.1/src/bookkeeper/forms.py +34 -0
- django_bookkeeper-0.2.1/src/bookkeeper/hooks.py +35 -0
- django_bookkeeper-0.2.1/src/bookkeeper/migrations/0001_initial.py +143 -0
- django_bookkeeper-0.2.1/src/bookkeeper/migrations/0002_chapter.py +30 -0
- django_bookkeeper-0.2.1/src/bookkeeper/migrations/__init__.py +0 -0
- django_bookkeeper-0.2.1/src/bookkeeper/models.py +243 -0
- django_bookkeeper-0.2.1/src/bookkeeper/readers/__init__.py +4 -0
- django_bookkeeper-0.2.1/src/bookkeeper/readers/base.py +28 -0
- django_bookkeeper-0.2.1/src/bookkeeper/readers/cbz.py +45 -0
- django_bookkeeper-0.2.1/src/bookkeeper/readers/epub.py +203 -0
- django_bookkeeper-0.2.1/src/bookkeeper/readers/pdf.py +37 -0
- django_bookkeeper-0.2.1/src/bookkeeper/readers/registry.py +16 -0
- django_bookkeeper-0.2.1/src/bookkeeper/signals.py +14 -0
- django_bookkeeper-0.2.1/src/bookkeeper/static/bookkeeper/css/bookkeeper.css +375 -0
- django_bookkeeper-0.2.1/src/bookkeeper/static/bookkeeper/css/reader.css +480 -0
- django_bookkeeper-0.2.1/src/bookkeeper/static/bookkeeper/js/bookkeeper.js +158 -0
- django_bookkeeper-0.2.1/src/bookkeeper/static/bookkeeper/js/reader.js +775 -0
- django_bookkeeper-0.2.1/src/bookkeeper/templates/bookkeeper/base.html +89 -0
- django_bookkeeper-0.2.1/src/bookkeeper/templates/bookkeeper/book_detail.html +140 -0
- django_bookkeeper-0.2.1/src/bookkeeper/templates/bookkeeper/chapter_eval.html +127 -0
- django_bookkeeper-0.2.1/src/bookkeeper/templates/bookkeeper/library.html +103 -0
- django_bookkeeper-0.2.1/src/bookkeeper/templates/bookkeeper/reader.html +225 -0
- django_bookkeeper-0.2.1/src/bookkeeper/templatetags/__init__.py +0 -0
- django_bookkeeper-0.2.1/src/bookkeeper/urls.py +41 -0
- django_bookkeeper-0.2.1/src/bookkeeper/views.py +435 -0
- django_bookkeeper-0.2.1/tests/__init__.py +0 -0
- django_bookkeeper-0.2.1/tests/settings.py +44 -0
- django_bookkeeper-0.2.1/tests/test_models.py +94 -0
- django_bookkeeper-0.2.1/tests/test_star_rating_url.py +106 -0
- django_bookkeeper-0.2.1/tests/urls.py +5 -0
- django_bookkeeper-0.2.1/uv.lock +1061 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.0.1",
|
|
3
|
+
"configurations": [
|
|
4
|
+
{
|
|
5
|
+
"name": "bookkeeper-demo",
|
|
6
|
+
"runtimeExecutable": "uv",
|
|
7
|
+
"runtimeArgs": [
|
|
8
|
+
"run",
|
|
9
|
+
"--with",
|
|
10
|
+
"django-bookkeeper",
|
|
11
|
+
"python",
|
|
12
|
+
"demo/manage.py",
|
|
13
|
+
"runserver",
|
|
14
|
+
"8765"
|
|
15
|
+
],
|
|
16
|
+
"port": 8765,
|
|
17
|
+
"env": {
|
|
18
|
+
"DJANGO_SETTINGS_MODULE": "demo_project.settings",
|
|
19
|
+
"PYTHONPATH": "src:demo:."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
workflow_call: {}
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
lint:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v3
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: uv sync --extra dev
|
|
22
|
+
|
|
23
|
+
- name: Ruff check
|
|
24
|
+
run: uv run ruff check src/ tests/ demo/
|
|
25
|
+
|
|
26
|
+
test:
|
|
27
|
+
needs: lint
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
33
|
+
django-version: ["4.2", "5.0", "5.1"]
|
|
34
|
+
exclude:
|
|
35
|
+
# Django 5.0 dropped support before Python 3.13 was released
|
|
36
|
+
- python-version: "3.13"
|
|
37
|
+
django-version: "5.0"
|
|
38
|
+
# Django 4.2 didn't officially support 3.13 until later point releases
|
|
39
|
+
- python-version: "3.13"
|
|
40
|
+
django-version: "4.2"
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- name: Install uv
|
|
45
|
+
uses: astral-sh/setup-uv@v3
|
|
46
|
+
|
|
47
|
+
- name: Install Python ${{ matrix.python-version }}
|
|
48
|
+
run: uv python install ${{ matrix.python-version }}
|
|
49
|
+
|
|
50
|
+
- name: Install dependencies
|
|
51
|
+
run: uv sync --extra dev --python ${{ matrix.python-version }}
|
|
52
|
+
|
|
53
|
+
- name: Run tests against Django ${{ matrix.django-version }}
|
|
54
|
+
# `uv run` re-syncs the venv to uv.lock before executing, which would
|
|
55
|
+
# silently revert a `uv pip install` override done in a separate step.
|
|
56
|
+
# `--with` overrides the dependency for this invocation only, and
|
|
57
|
+
# survives the sync.
|
|
58
|
+
run: uv run --with "django~=${{ matrix.django-version }}.0" pytest --cov --cov-report=xml
|
|
59
|
+
|
|
60
|
+
- name: Upload coverage
|
|
61
|
+
if: matrix.python-version == '3.12' && matrix.django-version == '5.1'
|
|
62
|
+
uses: codecov/codecov-action@v4
|
|
63
|
+
with:
|
|
64
|
+
files: ./coverage.xml
|
|
65
|
+
fail_ci_if_error: false
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Production releases publish on GitHub Release "published", not on tag
|
|
4
|
+
# push — tags get verified on TestPyPI first (see publish-testpypi.yml).
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
uses: ./.github/workflows/ci.yml
|
|
16
|
+
|
|
17
|
+
build:
|
|
18
|
+
needs: test
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
ref: ${{ github.event.release.tag_name }}
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v3
|
|
27
|
+
|
|
28
|
+
- name: Verify release tag matches package version
|
|
29
|
+
if: github.event_name == 'release'
|
|
30
|
+
env:
|
|
31
|
+
TAG_NAME: ${{ github.event.release.tag_name }}
|
|
32
|
+
run: |
|
|
33
|
+
PKG_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
34
|
+
TAG_VERSION="${TAG_NAME#v}"
|
|
35
|
+
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
|
36
|
+
echo "::error::Release tag $TAG_NAME does not match pyproject.toml version $PKG_VERSION"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
- name: Build sdist and wheel
|
|
41
|
+
run: uv build
|
|
42
|
+
|
|
43
|
+
- name: Upload build artifacts
|
|
44
|
+
uses: actions/upload-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
|
|
49
|
+
publish:
|
|
50
|
+
needs: build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
environment: pypi
|
|
53
|
+
permissions:
|
|
54
|
+
id-token: write # required for PyPI trusted publishing (OIDC)
|
|
55
|
+
steps:
|
|
56
|
+
- name: Download build artifacts
|
|
57
|
+
uses: actions/download-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
|
|
62
|
+
- name: Publish to PyPI
|
|
63
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Publish to TestPyPI
|
|
2
|
+
|
|
3
|
+
# Tag pushes (vX.Y.Z) publish to TestPyPI for pre-release verification.
|
|
4
|
+
# Pushing to pypi.org happens separately, on GitHub Release "published".
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags:
|
|
8
|
+
- "v*"
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
test:
|
|
16
|
+
uses: ./.github/workflows/ci.yml
|
|
17
|
+
|
|
18
|
+
build:
|
|
19
|
+
needs: test
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v3
|
|
26
|
+
|
|
27
|
+
- name: Verify tag matches package version
|
|
28
|
+
if: github.event_name == 'push'
|
|
29
|
+
run: |
|
|
30
|
+
PKG_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
31
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
32
|
+
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
|
33
|
+
echo "::error::Tag v$TAG_VERSION does not match pyproject.toml version $PKG_VERSION"
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
- name: Build sdist and wheel
|
|
38
|
+
run: uv build
|
|
39
|
+
|
|
40
|
+
- name: Upload build artifacts
|
|
41
|
+
uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist/
|
|
45
|
+
|
|
46
|
+
publish:
|
|
47
|
+
needs: build
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
environment: testpypi
|
|
50
|
+
permissions:
|
|
51
|
+
id-token: write # required for PyPI trusted publishing (OIDC)
|
|
52
|
+
steps:
|
|
53
|
+
- name: Download build artifacts
|
|
54
|
+
uses: actions/download-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: dist
|
|
57
|
+
path: dist/
|
|
58
|
+
|
|
59
|
+
- name: Publish to TestPyPI
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
61
|
+
with:
|
|
62
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.env
|
|
11
|
+
*.sqlite3
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
coverage.xml
|
|
15
|
+
htmlcov/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
*.mo
|
|
18
|
+
*.pot
|
|
19
|
+
/media/
|
|
20
|
+
/static-collected/
|
|
21
|
+
node_modules/
|
|
22
|
+
demo/media/
|
|
23
|
+
demo/demo.sqlite3
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Agent instructions for django-bookkeeper
|
|
2
|
+
|
|
3
|
+
This file documents how to work in this repo so AI coding agents follow the
|
|
4
|
+
same process humans do. Read this before making changes.
|
|
5
|
+
|
|
6
|
+
## What this project is
|
|
7
|
+
|
|
8
|
+
A reusable Django app (`src/bookkeeper/`) for storing, cataloguing, and
|
|
9
|
+
reading e-books (PDF, EPUB, CBZ), installable via pip/uv. `demo/` is a
|
|
10
|
+
throwaway Django project that exercises the app end-to-end — it is not
|
|
11
|
+
part of the published package.
|
|
12
|
+
|
|
13
|
+
## Branching (GitFlow) — never push directly to main
|
|
14
|
+
|
|
15
|
+
- `main` — stable releases only. Never commit or push directly here.
|
|
16
|
+
- `develop` — integration branch. Feature/fix branches merge here via PR.
|
|
17
|
+
- `feature/*` — new features, branched from `develop`.
|
|
18
|
+
- `fix/*` — bug fixes, branched from `develop`.
|
|
19
|
+
- `release/*` — release prep branches.
|
|
20
|
+
|
|
21
|
+
Always create a branch and open a PR. Do not push to `develop` or `main`
|
|
22
|
+
directly unless explicitly told to.
|
|
23
|
+
|
|
24
|
+
## Before every push
|
|
25
|
+
|
|
26
|
+
Run lint and tests. Both must pass:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv run ruff check src/ tests/ demo/
|
|
30
|
+
uv run pytest --cov
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
CI runs the same `ruff check` command and a pytest matrix across Python
|
|
34
|
+
3.11–3.13 and Django 4.2/5.0/5.1 — catch failures locally first.
|
|
35
|
+
|
|
36
|
+
Note: `ruff format` is not currently enforced (the existing codebase
|
|
37
|
+
hasn't been run through the formatter). Don't add a `ruff format --check`
|
|
38
|
+
CI gate without first reformatting the codebase as a deliberate, separate
|
|
39
|
+
change — otherwise it fails on ~11 pre-existing files immediately.
|
|
40
|
+
|
|
41
|
+
## Working with the demo project
|
|
42
|
+
|
|
43
|
+
The demo lives in `demo/` but imports `bookkeeper` from `../src`. Because of
|
|
44
|
+
this, commands run from inside `demo/` need an explicit `PYTHONPATH`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd demo
|
|
48
|
+
PYTHONPATH=../src:.. uv run python manage.py migrate
|
|
49
|
+
PYTHONPATH=../src:.. uv run python manage.py seed_demo
|
|
50
|
+
PYTHONPATH=../src:.. uv run python manage.py runserver
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
(`make demo` from the repo root wraps this correctly — prefer it when you
|
|
54
|
+
just want to run the demo, use the explicit `PYTHONPATH` form when you need
|
|
55
|
+
a one-off management command.)
|
|
56
|
+
|
|
57
|
+
### Re-seeding after model or extraction changes
|
|
58
|
+
|
|
59
|
+
`seed_demo --fast` skips books that already exist (matched by file hash),
|
|
60
|
+
so changing extraction logic (e.g. `EpubReader.extract_chapters()`) has
|
|
61
|
+
**no effect on already-seeded books** — they were extracted once at
|
|
62
|
+
upload time and the result is stored in the DB. To pick up extraction
|
|
63
|
+
changes:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
PYTHONPATH=../src:.. uv run python manage.py flush --no-input
|
|
67
|
+
PYTHONPATH=../src:.. uv run python manage.py seed_demo
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This applies to any manually-uploaded book too — re-upload it after
|
|
71
|
+
pulling a fix to extraction code.
|
|
72
|
+
|
|
73
|
+
## Architecture notes
|
|
74
|
+
|
|
75
|
+
- `src/bookkeeper/readers/` — one module per format (`epub.py`, `pdf.py`,
|
|
76
|
+
`cbz.py`), each implementing `BaseReader` (metadata/cover extraction).
|
|
77
|
+
`EpubReader` additionally extracts chapter HTML at upload time (see
|
|
78
|
+
below) — this is EPUB-specific, not part of the shared interface.
|
|
79
|
+
- **EPUB chapters are extracted once, at upload time**, not re-parsed on
|
|
80
|
+
every read. `EpubReader.extract_chapters()` walks the spine, sanitizes
|
|
81
|
+
each XHTML item with BeautifulSoup, and stores the result in `Chapter`
|
|
82
|
+
rows. The reader serves `Chapter.html` directly — no epub.js, no
|
|
83
|
+
runtime EPUB parsing. epub.js remains only as a fallback for
|
|
84
|
+
fixed-layout (pre-paginated) EPUBs, which aren't extracted.
|
|
85
|
+
- `src/bookkeeper/hooks.py` — Django signals (`book_opened`,
|
|
86
|
+
`progress_updated`, `book_finished`, `book_rated`, `book_uploaded`,
|
|
87
|
+
`highlight_created`, `bookmark_created`) are the extension points for
|
|
88
|
+
consuming projects. Don't add new functionality that should be
|
|
89
|
+
observable externally without firing a signal.
|
|
90
|
+
- URLs are namespaced (`bookkeeper:`) and never hardcode a mount prefix
|
|
91
|
+
in templates — always use `{% url %}`. The demo mounts the app at `/`,
|
|
92
|
+
but consuming projects can mount it anywhere (e.g. `/books/`).
|
|
93
|
+
**JavaScript must follow the same rule**: never hardcode an API path
|
|
94
|
+
like `/books/api/...` — read it from a `data-url-*` attribute injected
|
|
95
|
+
via `{% url %}` in the template. (See `reader.html`/`reader.js` for the
|
|
96
|
+
pattern. A hardcoded path in `bookkeeper.js`'s star-rating widget is a
|
|
97
|
+
known bug — see issue #4.)
|
|
98
|
+
|
|
99
|
+
## Known gotchas worth knowing before touching the reader
|
|
100
|
+
|
|
101
|
+
- **EPUB self-closing tags**: BeautifulSoup's `lxml-xml` parser (needed to
|
|
102
|
+
read well-formed XHTML) serializes empty elements as `<tag/>`, which is
|
|
103
|
+
valid XML but meaningless to a browser's HTML5 parser for non-void
|
|
104
|
+
elements like `<a>` — the tag stays open and swallows everything after
|
|
105
|
+
it. `EpubReader.extract_chapters()` works around this by forcing an
|
|
106
|
+
explicit empty text node on empty non-void, non-SVG elements before
|
|
107
|
+
serializing. Don't "simplify" this by reparsing the whole fragment
|
|
108
|
+
through an HTML parser — that lowercases SVG's case-sensitive
|
|
109
|
+
attributes (e.g. `viewBox` → `viewbox`), breaking SVG cover rendering.
|
|
110
|
+
- **Flexbox scroll containers need `min-height: 0`**: flex children
|
|
111
|
+
default to `min-height: auto`, meaning they won't shrink below their
|
|
112
|
+
content size. Any new scrollable panel inside the reader's flex layout
|
|
113
|
+
needs `min-height: 0` alongside `overflow-y: auto`, or it will grow to
|
|
114
|
+
fit all content instead of scrolling.
|
|
115
|
+
- **`[hidden]` can be overridden by author CSS**: if you give an element
|
|
116
|
+
with the `hidden` attribute its own `display: flex`/`block` rule, that
|
|
117
|
+
rule wins over the browser's `[hidden] { display: none }` UA rule. Any
|
|
118
|
+
element toggled via the `hidden` attribute needs an explicit
|
|
119
|
+
`#id[hidden] { display: none; }` rule alongside its own display rules
|
|
120
|
+
(see the four reader viewer divs in `reader.css` for the pattern).
|
|
121
|
+
|
|
122
|
+
## Release process
|
|
123
|
+
|
|
124
|
+
Versioning is currently manual (static `version` in `pyproject.toml`,
|
|
125
|
+
not git-tag-derived).
|
|
126
|
+
|
|
127
|
+
1. Bump `version` in `pyproject.toml`.
|
|
128
|
+
2. Commit, merge to `main` via the normal release-branch flow.
|
|
129
|
+
3. Tag and push `vX.Y.Z` — this triggers `publish-testpypi.yml`, which
|
|
130
|
+
verifies the tag matches the `pyproject.toml` version and publishes to
|
|
131
|
+
**test.pypi.org** via OIDC trusted publishing.
|
|
132
|
+
4. Verify the TestPyPI release installs and works correctly.
|
|
133
|
+
5. Cut a GitHub Release from that tag — this triggers `publish-pypi.yml`,
|
|
134
|
+
which publishes to **pypi.org**.
|
|
135
|
+
|
|
136
|
+
Both publish workflows gate on the CI workflow (lint + test matrix)
|
|
137
|
+
passing first, via `workflow_call`. Trusted publisher config (on both
|
|
138
|
+
PyPI and TestPyPI) must reference the exact workflow filename and
|
|
139
|
+
environment name — see `.github/workflows/publish-*.yml` for the
|
|
140
|
+
environment names (`testpypi` / `pypi`).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
4
|
+
libmagic1 \
|
|
5
|
+
libmupdf-dev \
|
|
6
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
7
|
+
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
|
|
10
|
+
COPY pyproject.toml uv.lock ./
|
|
11
|
+
RUN pip install uv && uv sync --no-dev
|
|
12
|
+
|
|
13
|
+
COPY src/ ./src/
|
|
14
|
+
COPY demo/ ./demo/
|
|
15
|
+
|
|
16
|
+
EXPOSE 8000
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eric Williams
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.PHONY: demo demo-reset test lint
|
|
2
|
+
|
|
3
|
+
# ── Demo ─────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
demo:
|
|
6
|
+
@echo "Setting up demo…"
|
|
7
|
+
cd demo && DJANGO_SETTINGS_MODULE=demo_project.settings PYTHONPATH=../src:.. \
|
|
8
|
+
uv run python manage.py migrate --run-syncdb
|
|
9
|
+
@echo "Seeding demo data (downloads ~5 public-domain EPUBs)…"
|
|
10
|
+
cd demo && DJANGO_SETTINGS_MODULE=demo_project.settings PYTHONPATH=../src:.. \
|
|
11
|
+
uv run python manage.py seed_demo
|
|
12
|
+
@echo ""
|
|
13
|
+
@echo "Starting server at http://127.0.0.1:8000/"
|
|
14
|
+
cd demo && DJANGO_SETTINGS_MODULE=demo_project.settings PYTHONPATH=../src:.. \
|
|
15
|
+
uv run python manage.py runserver
|
|
16
|
+
|
|
17
|
+
demo-reset:
|
|
18
|
+
rm -f demo/demo.sqlite3
|
|
19
|
+
rm -rf demo/media/bookkeeper
|
|
20
|
+
$(MAKE) demo
|
|
21
|
+
|
|
22
|
+
demo-run:
|
|
23
|
+
cd demo && DJANGO_SETTINGS_MODULE=demo_project.settings PYTHONPATH=../src:.. \
|
|
24
|
+
uv run python manage.py runserver
|
|
25
|
+
|
|
26
|
+
# ── Dev ──────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
test:
|
|
29
|
+
uv run pytest tests/ -v
|
|
30
|
+
|
|
31
|
+
lint:
|
|
32
|
+
uv run ruff check src/ tests/ demo/
|
|
33
|
+
|
|
34
|
+
lint-fix:
|
|
35
|
+
uv run ruff check --fix src/ tests/ demo/
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-bookkeeper
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: A Django app for storing, cataloguing, and reading e-Books (PDF, EPUB, CBZ)
|
|
5
|
+
Project-URL: Homepage, https://github.com/ehwio/django-bookkeeper
|
|
6
|
+
Project-URL: Repository, https://github.com/ehwio/django-bookkeeper
|
|
7
|
+
Project-URL: Issues, https://github.com/ehwio/django-bookkeeper/issues
|
|
8
|
+
Author-email: Eric Williams <eric@subcritical.org>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Eric Williams
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: cbz,django,ebook,epub,library,pdf,reader
|
|
32
|
+
Classifier: Framework :: Django
|
|
33
|
+
Classifier: Framework :: Django :: 4.2
|
|
34
|
+
Classifier: Framework :: Django :: 5.0
|
|
35
|
+
Classifier: Framework :: Django :: 5.1
|
|
36
|
+
Classifier: Intended Audience :: Developers
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
40
|
+
Requires-Python: >=3.11
|
|
41
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
42
|
+
Requires-Dist: django-storages>=1.14
|
|
43
|
+
Requires-Dist: django>=4.2
|
|
44
|
+
Requires-Dist: ebooklib>=0.18
|
|
45
|
+
Requires-Dist: lxml>=5.0
|
|
46
|
+
Requires-Dist: pillow>=10.0
|
|
47
|
+
Requires-Dist: pymupdf>=1.24
|
|
48
|
+
Requires-Dist: python-magic>=0.4.27
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: django-debug-toolbar>=4.3; extra == 'dev'
|
|
51
|
+
Requires-Dist: factory-boy>=3.3; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest-django>=4.8; extra == 'dev'
|
|
54
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
55
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
56
|
+
Provides-Extra: social
|
|
57
|
+
Requires-Dist: social-auth-app-django>=5.4; extra == 'social'
|
|
58
|
+
Description-Content-Type: text/markdown
|
|
59
|
+
|
|
60
|
+
# django-bookkeeper
|
|
61
|
+
|
|
62
|
+
A Django app for storing, cataloguing, and reading e-Books (PDF, EPUB, CBZ).
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- **Upload** PDF, EPUB, and CBZ files (drag-and-drop or browse)
|
|
67
|
+
- **Automatic metadata extraction** — title, author, publisher, cover image
|
|
68
|
+
- **Deduplication** by SHA-256 hash
|
|
69
|
+
- **Modern reader** with:
|
|
70
|
+
- EPUB rendering via [epub.js](https://github.com/futurepress/epub.js/)
|
|
71
|
+
- PDF rendering via [PDF.js](https://mozilla.github.io/pdf.js/)
|
|
72
|
+
- CBZ page-by-page comic reader
|
|
73
|
+
- Keyboard navigation (arrow keys)
|
|
74
|
+
- **Reader settings**: light/sepia/dark themes, font family & size, line height, column width
|
|
75
|
+
- **Highlights** in five colours with optional notes
|
|
76
|
+
- **Bookmarks** with titles and notes
|
|
77
|
+
- **Reading progress** — auto-saved position and percentage
|
|
78
|
+
- **5-star ratings** per user
|
|
79
|
+
- **Favourites** and finished-book tracking
|
|
80
|
+
- **Extensible hook signals** for recent-books lists, activity feeds, etc.
|
|
81
|
+
- Django best-practices: `django-storages` compatible, `AUTH_USER_MODEL` aware, namespaced URLs
|
|
82
|
+
|
|
83
|
+
## Try the demo
|
|
84
|
+
|
|
85
|
+
The fastest way to see Bookkeeper in action — one command downloads five
|
|
86
|
+
public-domain classics from Project Gutenberg and starts a local server:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git clone https://github.com/ehwio/django-bookkeeper
|
|
90
|
+
cd django-bookkeeper
|
|
91
|
+
uv sync
|
|
92
|
+
make demo
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then open **http://127.0.0.1:8000/** and sign in as `demo` / `demo`.
|
|
96
|
+
|
|
97
|
+
**Or with Docker:**
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
docker compose up
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The demo ships with:
|
|
104
|
+
- *Pride and Prejudice* — Jane Austen
|
|
105
|
+
- *Twenty Thousand Leagues Under the Seas* — Jules Verne
|
|
106
|
+
- *The Time Machine* — H.G. Wells
|
|
107
|
+
- *Alice's Adventures in Wonderland* — Lewis Carroll
|
|
108
|
+
- *Frankenstein* — Mary Wollstonecraft Shelley
|
|
109
|
+
|
|
110
|
+
> Books are downloaded from [Project Gutenberg](https://www.gutenberg.org/) on first run.
|
|
111
|
+
> They are public domain and freely distributable.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Installation
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install django-bookkeeper
|
|
119
|
+
# or
|
|
120
|
+
uv add django-bookkeeper
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Add to `INSTALLED_APPS`:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
INSTALLED_APPS = [
|
|
127
|
+
...
|
|
128
|
+
"bookkeeper",
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Include URLs:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
# urls.py
|
|
136
|
+
from django.urls import include, path
|
|
137
|
+
|
|
138
|
+
urlpatterns = [
|
|
139
|
+
path("books/", include("bookkeeper.urls", namespace="bookkeeper")),
|
|
140
|
+
]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Run migrations:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
python manage.py migrate
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Optional dependencies
|
|
150
|
+
|
|
151
|
+
| Feature | Package |
|
|
152
|
+
|---------|---------|
|
|
153
|
+
| Social login | `django-social-auth-app-django` |
|
|
154
|
+
| Cloud storage | `django-storages` |
|
|
155
|
+
|
|
156
|
+
## Hooks
|
|
157
|
+
|
|
158
|
+
Connect to Bookkeeper signals to extend behaviour:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from bookkeeper.hooks import book_opened, book_finished, progress_updated
|
|
162
|
+
|
|
163
|
+
@book_opened.connect
|
|
164
|
+
def track_recent(sender, user, book, **kwargs):
|
|
165
|
+
RecentBook.objects.update_or_create(user=user, defaults={"book": book})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Available signals: `book_opened`, `progress_updated`, `book_finished`, `book_rated`,
|
|
169
|
+
`book_uploaded`, `highlight_created`, `bookmark_created`.
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git clone https://github.com/ehwio/django-bookkeeper
|
|
175
|
+
cd django-bookkeeper
|
|
176
|
+
uv sync --extra dev
|
|
177
|
+
uv run pytest
|
|
178
|
+
uv run ruff check src/ tests/
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### GitFlow
|
|
182
|
+
|
|
183
|
+
- `main` — stable releases
|
|
184
|
+
- `develop` — integration branch
|
|
185
|
+
- `feature/*` — new features
|
|
186
|
+
- `fix/*` — bug fixes
|
|
187
|
+
- `release/*` — release prep
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|