notability-extractor 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.
- notability_extractor-0.1.0/.github/workflows/ci.yml +107 -0
- notability_extractor-0.1.0/.gitignore +35 -0
- notability_extractor-0.1.0/Makefile +63 -0
- notability_extractor-0.1.0/PKG-INFO +205 -0
- notability_extractor-0.1.0/README.md +182 -0
- notability_extractor-0.1.0/pyproject.toml +183 -0
- notability_extractor-0.1.0/src/notability_extractor/__init__.py +3 -0
- notability_extractor-0.1.0/src/notability_extractor/__main__.py +3 -0
- notability_extractor-0.1.0/src/notability_extractor/anki.py +297 -0
- notability_extractor-0.1.0/src/notability_extractor/archive/__init__.py +1 -0
- notability_extractor-0.1.0/src/notability_extractor/archive/backup.py +198 -0
- notability_extractor-0.1.0/src/notability_extractor/archive/config.py +109 -0
- notability_extractor-0.1.0/src/notability_extractor/archive/filter.py +44 -0
- notability_extractor-0.1.0/src/notability_extractor/archive/scheduler.py +65 -0
- notability_extractor-0.1.0/src/notability_extractor/archive/scheduler_install.py +186 -0
- notability_extractor-0.1.0/src/notability_extractor/archive/store.py +217 -0
- notability_extractor-0.1.0/src/notability_extractor/build/__init__.py +1 -0
- notability_extractor-0.1.0/src/notability_extractor/build/flashcards.py +91 -0
- notability_extractor-0.1.0/src/notability_extractor/build/notes.py +31 -0
- notability_extractor-0.1.0/src/notability_extractor/build/reader.py +108 -0
- notability_extractor-0.1.0/src/notability_extractor/build/summaries.py +38 -0
- notability_extractor-0.1.0/src/notability_extractor/cli.py +263 -0
- notability_extractor-0.1.0/src/notability_extractor/extract/__init__.py +1 -0
- notability_extractor-0.1.0/src/notability_extractor/extract/exporter.py +45 -0
- notability_extractor-0.1.0/src/notability_extractor/extract/http_cache.py +87 -0
- notability_extractor-0.1.0/src/notability_extractor/extract/nbn.py +78 -0
- notability_extractor-0.1.0/src/notability_extractor/extract/platform_check.py +35 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/__init__.py +0 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/app.py +68 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/main_window.py +119 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/pages/__init__.py +0 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/pages/export.py +123 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/pages/library.py +203 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/pages/notes.py +102 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/pages/settings.py +349 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/pages/summaries.py +101 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/theme.py +61 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/widgets/__init__.py +0 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/widgets/card_editor.py +180 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/widgets/tag_filter.py +101 -0
- notability_extractor-0.1.0/src/notability_extractor/gui/widgets/tag_input.py +161 -0
- notability_extractor-0.1.0/src/notability_extractor/model.py +76 -0
- notability_extractor-0.1.0/src/notability_extractor/utils.py +80 -0
- notability_extractor-0.1.0/tests/__init__.py +0 -0
- notability_extractor-0.1.0/tests/archive/__init__.py +0 -0
- notability_extractor-0.1.0/tests/archive/test_backup.py +208 -0
- notability_extractor-0.1.0/tests/archive/test_config.py +59 -0
- notability_extractor-0.1.0/tests/archive/test_filter.py +70 -0
- notability_extractor-0.1.0/tests/archive/test_scheduler.py +61 -0
- notability_extractor-0.1.0/tests/archive/test_scheduler_install.py +106 -0
- notability_extractor-0.1.0/tests/archive/test_store.py +222 -0
- notability_extractor-0.1.0/tests/build/__init__.py +0 -0
- notability_extractor-0.1.0/tests/build/test_flashcards.py +53 -0
- notability_extractor-0.1.0/tests/build/test_notes.py +26 -0
- notability_extractor-0.1.0/tests/build/test_summaries.py +27 -0
- notability_extractor-0.1.0/tests/conftest.py +1 -0
- notability_extractor-0.1.0/tests/gui/__init__.py +0 -0
- notability_extractor-0.1.0/tests/gui/test_app_smoke.py +33 -0
- notability_extractor-0.1.0/tests/gui/test_card_editor.py +54 -0
- notability_extractor-0.1.0/tests/gui/test_library_page.py +50 -0
- notability_extractor-0.1.0/tests/gui/test_tag_input.py +38 -0
- notability_extractor-0.1.0/tests/test_build_reader.py +76 -0
- notability_extractor-0.1.0/tests/test_cli.py +113 -0
- notability_extractor-0.1.0/tests/test_extract_exporter.py +57 -0
- notability_extractor-0.1.0/tests/test_extract_http_cache.py +109 -0
- notability_extractor-0.1.0/tests/test_extract_nbn.py +90 -0
- notability_extractor-0.1.0/tests/test_extract_platform_check.py +40 -0
- notability_extractor-0.1.0/tests/test_model.py +151 -0
- notability_extractor-0.1.0/upload-to-pypi.sh +70 -0
- notability_extractor-0.1.0/uv.lock +911 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
# Run tests on every push and PR
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: uv sync
|
|
19
|
+
- name: Run check gate
|
|
20
|
+
run: |
|
|
21
|
+
uv run ruff check src tests
|
|
22
|
+
uv run pylint src tests
|
|
23
|
+
uv run mypy src
|
|
24
|
+
uv run pyright
|
|
25
|
+
uv run black --check src tests
|
|
26
|
+
uv run pytest
|
|
27
|
+
|
|
28
|
+
# Auto-tag when pyproject.toml version is new (main pushes only)
|
|
29
|
+
autotag:
|
|
30
|
+
needs: test
|
|
31
|
+
if: github.ref == 'refs/heads/main'
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
permissions:
|
|
34
|
+
contents: write
|
|
35
|
+
outputs:
|
|
36
|
+
tag_created: ${{ steps.tag.outputs.tag_created }}
|
|
37
|
+
version: ${{ steps.tag.outputs.version }}
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
with:
|
|
41
|
+
fetch-depth: 0
|
|
42
|
+
- name: Create tag if version is new
|
|
43
|
+
id: tag
|
|
44
|
+
run: |
|
|
45
|
+
VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
|
|
46
|
+
TAG="v$VERSION"
|
|
47
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
48
|
+
if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then
|
|
49
|
+
echo "Tag $TAG already exists - skipping"
|
|
50
|
+
echo "tag_created=false" >> "$GITHUB_OUTPUT"
|
|
51
|
+
else
|
|
52
|
+
git config user.name "github-actions[bot]"
|
|
53
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
54
|
+
git tag "$TAG"
|
|
55
|
+
git push origin "$TAG"
|
|
56
|
+
echo "Created tag $TAG"
|
|
57
|
+
echo "tag_created=true" >> "$GITHUB_OUTPUT"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Build wheel + sdist on every push
|
|
61
|
+
build:
|
|
62
|
+
needs: [test, autotag]
|
|
63
|
+
if: always() && needs.test.result == 'success'
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
permissions:
|
|
66
|
+
contents: write
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
- uses: astral-sh/setup-uv@v5
|
|
70
|
+
- name: Build distributions
|
|
71
|
+
run: uv build
|
|
72
|
+
- name: Store dist for publish job
|
|
73
|
+
uses: actions/upload-artifact@v4
|
|
74
|
+
with:
|
|
75
|
+
name: dist
|
|
76
|
+
path: dist/
|
|
77
|
+
- name: Upload artifacts to GitHub Release
|
|
78
|
+
if: |
|
|
79
|
+
needs.autotag.outputs.tag_created == 'true' ||
|
|
80
|
+
(needs.autotag.result == 'skipped' && startsWith(github.ref, 'refs/tags/v'))
|
|
81
|
+
uses: softprops/action-gh-release@v2
|
|
82
|
+
with:
|
|
83
|
+
tag_name: ${{ needs.autotag.outputs.tag_created == 'true' && format('v{0}', needs.autotag.outputs.version) || github.ref_name }}
|
|
84
|
+
files: dist/*
|
|
85
|
+
|
|
86
|
+
# Publish to PyPI via OIDC trusted publishing (no API tokens needed)
|
|
87
|
+
publish:
|
|
88
|
+
needs: [build, autotag]
|
|
89
|
+
if: |
|
|
90
|
+
always() &&
|
|
91
|
+
needs.build.result == 'success' &&
|
|
92
|
+
(
|
|
93
|
+
needs.autotag.outputs.tag_created == 'true' ||
|
|
94
|
+
(needs.autotag.result == 'skipped' && startsWith(github.ref, 'refs/tags/v'))
|
|
95
|
+
)
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
environment: pypi
|
|
98
|
+
permissions:
|
|
99
|
+
id-token: write
|
|
100
|
+
steps:
|
|
101
|
+
- name: Download build artifacts
|
|
102
|
+
uses: actions/download-artifact@v4
|
|
103
|
+
with:
|
|
104
|
+
name: dist
|
|
105
|
+
path: dist/
|
|
106
|
+
- name: Publish to PyPI
|
|
107
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
.Python
|
|
6
|
+
/build/
|
|
7
|
+
/dist/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.eggs/
|
|
10
|
+
*.egg
|
|
11
|
+
|
|
12
|
+
# UV / virtualenv
|
|
13
|
+
.venv/
|
|
14
|
+
.uv/
|
|
15
|
+
|
|
16
|
+
# pytest / coverage
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
coverage.xml
|
|
21
|
+
|
|
22
|
+
# Anki output files
|
|
23
|
+
*.apkg
|
|
24
|
+
|
|
25
|
+
# macOS
|
|
26
|
+
.DS_Store
|
|
27
|
+
|
|
28
|
+
# editors
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
*.swp
|
|
32
|
+
|
|
33
|
+
docs/superpowers/
|
|
34
|
+
.claude/
|
|
35
|
+
.superpowers/
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Makefile for notability-extractor
|
|
2
|
+
# Run `make help` to list targets.
|
|
3
|
+
|
|
4
|
+
.DEFAULT_GOAL := check
|
|
5
|
+
SHELL := bash
|
|
6
|
+
|
|
7
|
+
# pass CLI args through: make run ARGS="--list-tables"
|
|
8
|
+
ARGS ?=
|
|
9
|
+
|
|
10
|
+
.PHONY: help install lock lint typecheck format format-check \
|
|
11
|
+
test test-cov build run clean check
|
|
12
|
+
|
|
13
|
+
help: ## show this help
|
|
14
|
+
@awk 'BEGIN {FS = ":.*##"; printf "Targets:\n"} \
|
|
15
|
+
/^[a-zA-Z_-]+:.*?##/ { printf " %-14s %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
|
|
16
|
+
|
|
17
|
+
install: ## install dev env + put notability-extractor on PATH (~/.local/bin)
|
|
18
|
+
uv sync
|
|
19
|
+
uv tool install --editable --force .
|
|
20
|
+
|
|
21
|
+
lock: ## regenerate uv.lock
|
|
22
|
+
uv lock
|
|
23
|
+
|
|
24
|
+
lint: ## run ruff check then pylint on src and tests
|
|
25
|
+
uv run ruff check src tests
|
|
26
|
+
uv run pylint src tests
|
|
27
|
+
|
|
28
|
+
typecheck: ## run mypy then pyright on src
|
|
29
|
+
uv run mypy src
|
|
30
|
+
uv run pyright
|
|
31
|
+
|
|
32
|
+
format: ## run black then autopep8 (writes changes in place)
|
|
33
|
+
uv run black src tests
|
|
34
|
+
uv run autopep8 --in-place --recursive src tests
|
|
35
|
+
|
|
36
|
+
format-check: ## verify formatting without writing
|
|
37
|
+
uv run black --check src tests
|
|
38
|
+
@diff_out=$$(uv run autopep8 --diff --recursive src tests); \
|
|
39
|
+
if [ -n "$$diff_out" ]; then \
|
|
40
|
+
echo "$$diff_out"; \
|
|
41
|
+
echo "autopep8 would make changes - run 'make format'"; \
|
|
42
|
+
exit 1; \
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
test: ## run pytest
|
|
46
|
+
uv run pytest
|
|
47
|
+
|
|
48
|
+
test-cov: ## run pytest with coverage report
|
|
49
|
+
uv run pytest --cov --cov-report=term-missing
|
|
50
|
+
|
|
51
|
+
build: ## build wheel and sdist into dist/
|
|
52
|
+
uv build
|
|
53
|
+
|
|
54
|
+
run: ## run the CLI - pass args via ARGS="..."
|
|
55
|
+
uv run notability-extractor $(ARGS)
|
|
56
|
+
|
|
57
|
+
clean: ## remove build artifacts and tool caches
|
|
58
|
+
rm -rf build/ dist/ *.egg-info src/*.egg-info \
|
|
59
|
+
.pytest_cache .coverage htmlcov/ \
|
|
60
|
+
.mypy_cache .ruff_cache
|
|
61
|
+
find . -type d -name __pycache__ -exec rm -rf {} +
|
|
62
|
+
|
|
63
|
+
check: lint typecheck format-check test ## full gate: lint + typecheck + format-check + test
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notability-extractor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extract flashcards from Notability's SQLite database and export to Anki .apkg
|
|
5
|
+
Project-URL: Homepage, https://github.com/mdeguzis/notability-extractor
|
|
6
|
+
Project-URL: Issues, https://github.com/mdeguzis/notability-extractor/issues
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: anki,flashcards,notability,spaced-repetition,sqlite
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Education
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: genanki>=0.13.1
|
|
21
|
+
Requires-Dist: pyside6>=6.6
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# notability-extractor
|
|
25
|
+
|
|
26
|
+
Extract Notability Learn content (AI-generated quizzes, summaries, and OCR'd
|
|
27
|
+
note text) and export it as an Anki `.apkg` deck, JSON, or Markdown for review.
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
Notability Learn generates quizzes via Claude Haiku and summaries via Gemini
|
|
32
|
+
2.5 Flash, server-side. Both get cached locally in the app's HTTP cache.
|
|
33
|
+
Handwriting OCR and PDF text live inside `.nbn` note bundles.
|
|
34
|
+
|
|
35
|
+
The tool runs in two phases:
|
|
36
|
+
|
|
37
|
+
1. **Extract** (macOS only): walks the iCloud Drive `.nbn` bundles and the
|
|
38
|
+
HTTP cache (`Cache.db` + `fsCachedData/`), writes a normalized export
|
|
39
|
+
directory at `~/notability_export/`.
|
|
40
|
+
2. **Build** (any OS): reads the export directory, merges flashcards into a
|
|
41
|
+
persistent JSONL archive at `~/.notability_extractor/cards.jsonl`, and
|
|
42
|
+
emits outputs: `.apkg` for Anki, `.json` for programmatic review, `.md`
|
|
43
|
+
for human reading.
|
|
44
|
+
|
|
45
|
+
Linux and Windows machines can skip phase 1 by pointing `--input-dir` at a
|
|
46
|
+
directory produced on a Mac. Useful if you want to do the Anki packaging on a
|
|
47
|
+
different machine than the one Notability runs on.
|
|
48
|
+
|
|
49
|
+
The JSONL archive is the source of truth for flashcards. Both the CLI and the
|
|
50
|
+
GUI read from and write to it, and the build always reconstructs `.apkg` from
|
|
51
|
+
the archive (not the input dir) so edits and tags persist across rebuilds.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
From PyPI:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install notability-extractor
|
|
59
|
+
# or with uv:
|
|
60
|
+
uv tool install notability-extractor
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
From source (for development):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git clone https://github.com/mdeguzis/notability-extractor.git
|
|
67
|
+
cd notability-extractor
|
|
68
|
+
make install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`make install` sets up `.venv/` for dev work and drops the
|
|
72
|
+
`notability-extractor` console script into `~/.local/bin/` so it's runnable
|
|
73
|
+
from any shell.
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# macOS: auto-extract and build all outputs in the current dir
|
|
79
|
+
notability-extractor
|
|
80
|
+
|
|
81
|
+
# Anywhere: build from a pre-extracted directory
|
|
82
|
+
notability-extractor --input-dir ~/notability_export
|
|
83
|
+
|
|
84
|
+
# Custom output directory
|
|
85
|
+
notability-extractor --input-dir ~/notability_export --out-dir ./decks
|
|
86
|
+
|
|
87
|
+
# macOS: just run phase 1 (produce export dir, no .apkg/json/md)
|
|
88
|
+
notability-extractor --extract-only
|
|
89
|
+
|
|
90
|
+
# Custom Anki deck name (only affects what shows up inside Anki)
|
|
91
|
+
notability-extractor --deck-name "Biology 101"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Output filenames are fixed:
|
|
95
|
+
|
|
96
|
+
| File | Contents |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `notability_flashcards.apkg` | Anki deck (quiz questions only) |
|
|
99
|
+
| `notability_flashcards.json` | Full structured dump for programmatic review |
|
|
100
|
+
| `notability_flashcards.md` | Human-readable flashcards |
|
|
101
|
+
| `notability_notes.{json,md}` | Note transcripts |
|
|
102
|
+
| `notability_summaries.{json,md}` | Generated summaries |
|
|
103
|
+
|
|
104
|
+
### Archive management CLI flags
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Open a prompt-driven editor against the archive
|
|
108
|
+
notability-extractor --edit-flashcards
|
|
109
|
+
|
|
110
|
+
# One-shot interactive add
|
|
111
|
+
notability-extractor --add-card
|
|
112
|
+
|
|
113
|
+
# Print archive contents (optionally filter by tag)
|
|
114
|
+
notability-extractor --list-cards
|
|
115
|
+
notability-extractor --list-cards --tag biology
|
|
116
|
+
|
|
117
|
+
# Backup / restore round-trip
|
|
118
|
+
notability-extractor --backup
|
|
119
|
+
notability-extractor --export ~/cards-backup.jsonl
|
|
120
|
+
notability-extractor --import ~/cards-backup.jsonl --mode merge
|
|
121
|
+
notability-extractor --import ~/cards-backup.jsonl --mode replace
|
|
122
|
+
|
|
123
|
+
# Launch the GUI
|
|
124
|
+
notability-extractor --gui
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## GUI
|
|
128
|
+
|
|
129
|
+
A PySide6 desktop companion ships in the same package. After install:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
notability-extractor-gui
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Pages: Library (browse / edit / add / delete cards with tag filter), Notes
|
|
136
|
+
(read-only), Summaries (read-only), Build (export apkg / json / md), Settings
|
|
137
|
+
(theme, paths, backups, schedule).
|
|
138
|
+
|
|
139
|
+
On Wayland with no `DISPLAY` set, the GUI auto-applies
|
|
140
|
+
`QT_QPA_PLATFORM=wayland` so SSH/login sessions don't need a manual export.
|
|
141
|
+
|
|
142
|
+
## Backups
|
|
143
|
+
|
|
144
|
+
The archive at `~/.notability_extractor/cards.jsonl` is snapshotted on every
|
|
145
|
+
save to `~/.notability_extractor/backups/cards-YYYYMMDD-HHMMSS.jsonl`,
|
|
146
|
+
hash-deduped so unchanged saves don't make redundant copies. Default retention
|
|
147
|
+
is the last 10 snapshots (configurable in Settings).
|
|
148
|
+
|
|
149
|
+
For a scheduled backup when the GUI is closed, run from cron:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
0 * * * * notability-extractor --backup
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The Settings page surfaces this exact line for convenience.
|
|
156
|
+
|
|
157
|
+
## Importing into Anki
|
|
158
|
+
|
|
159
|
+
1. Open Anki on your desktop.
|
|
160
|
+
2. `File > Import` and select the generated `.apkg` file.
|
|
161
|
+
3. The deck appears as "Notability Flashcards" (or whatever you passed to
|
|
162
|
+
`--deck-name`).
|
|
163
|
+
|
|
164
|
+
## Caveats
|
|
165
|
+
|
|
166
|
+
- The Learn cache only contains content from sessions you've actively opened
|
|
167
|
+
in Notability. If a note has never had Learn run on it, no quiz is cached.
|
|
168
|
+
- Notability does not provide a stable export API. The tool reads on-disk
|
|
169
|
+
formats that could change between app versions. If extraction breaks after
|
|
170
|
+
a Notability update, open an issue.
|
|
171
|
+
- iPadOS-only setups need iCloud Drive sync enabled so the `.nbn` bundles and
|
|
172
|
+
cache files are present on a Mac. Without sync, you'd need physical access
|
|
173
|
+
to the iPad's sandbox (not currently supported).
|
|
174
|
+
|
|
175
|
+
## Releasing
|
|
176
|
+
|
|
177
|
+
Releases are automated via GitHub Actions. To cut a new release:
|
|
178
|
+
|
|
179
|
+
1. Bump the `version = "X.Y.Z"` line in `pyproject.toml`
|
|
180
|
+
2. Commit and push to `main`
|
|
181
|
+
3. CI runs: tests pass -> autotag creates `vX.Y.Z` -> build produces wheel
|
|
182
|
+
and sdist -> GitHub Release is created -> PyPI publishes via OIDC
|
|
183
|
+
trusted publishing
|
|
184
|
+
|
|
185
|
+
No manual tag step. No API tokens in CI.
|
|
186
|
+
|
|
187
|
+
### One-time PyPI setup
|
|
188
|
+
|
|
189
|
+
(Skip this if the package is already on PyPI with OIDC configured.)
|
|
190
|
+
|
|
191
|
+
1. First upload manually with `./upload-to-pypi.sh` (needs `~/.pypirc` with
|
|
192
|
+
a PyPI API token) to claim the package name.
|
|
193
|
+
2. On PyPI: project settings -> Publishing -> add trusted publisher with
|
|
194
|
+
repo `mdeguzis/notability-extractor`, workflow `ci.yml`, environment
|
|
195
|
+
`pypi`.
|
|
196
|
+
3. On GitHub: repo settings -> Environments -> create `pypi` environment.
|
|
197
|
+
|
|
198
|
+
### Manual ad-hoc upload
|
|
199
|
+
|
|
200
|
+
Only needed if CI is broken:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
./upload-to-pypi.sh --test # TestPyPI dry-run first
|
|
204
|
+
./upload-to-pypi.sh # then prod PyPI
|
|
205
|
+
```
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# notability-extractor
|
|
2
|
+
|
|
3
|
+
Extract Notability Learn content (AI-generated quizzes, summaries, and OCR'd
|
|
4
|
+
note text) and export it as an Anki `.apkg` deck, JSON, or Markdown for review.
|
|
5
|
+
|
|
6
|
+
## How it works
|
|
7
|
+
|
|
8
|
+
Notability Learn generates quizzes via Claude Haiku and summaries via Gemini
|
|
9
|
+
2.5 Flash, server-side. Both get cached locally in the app's HTTP cache.
|
|
10
|
+
Handwriting OCR and PDF text live inside `.nbn` note bundles.
|
|
11
|
+
|
|
12
|
+
The tool runs in two phases:
|
|
13
|
+
|
|
14
|
+
1. **Extract** (macOS only): walks the iCloud Drive `.nbn` bundles and the
|
|
15
|
+
HTTP cache (`Cache.db` + `fsCachedData/`), writes a normalized export
|
|
16
|
+
directory at `~/notability_export/`.
|
|
17
|
+
2. **Build** (any OS): reads the export directory, merges flashcards into a
|
|
18
|
+
persistent JSONL archive at `~/.notability_extractor/cards.jsonl`, and
|
|
19
|
+
emits outputs: `.apkg` for Anki, `.json` for programmatic review, `.md`
|
|
20
|
+
for human reading.
|
|
21
|
+
|
|
22
|
+
Linux and Windows machines can skip phase 1 by pointing `--input-dir` at a
|
|
23
|
+
directory produced on a Mac. Useful if you want to do the Anki packaging on a
|
|
24
|
+
different machine than the one Notability runs on.
|
|
25
|
+
|
|
26
|
+
The JSONL archive is the source of truth for flashcards. Both the CLI and the
|
|
27
|
+
GUI read from and write to it, and the build always reconstructs `.apkg` from
|
|
28
|
+
the archive (not the input dir) so edits and tags persist across rebuilds.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
From PyPI:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install notability-extractor
|
|
36
|
+
# or with uv:
|
|
37
|
+
uv tool install notability-extractor
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
From source (for development):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/mdeguzis/notability-extractor.git
|
|
44
|
+
cd notability-extractor
|
|
45
|
+
make install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`make install` sets up `.venv/` for dev work and drops the
|
|
49
|
+
`notability-extractor` console script into `~/.local/bin/` so it's runnable
|
|
50
|
+
from any shell.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# macOS: auto-extract and build all outputs in the current dir
|
|
56
|
+
notability-extractor
|
|
57
|
+
|
|
58
|
+
# Anywhere: build from a pre-extracted directory
|
|
59
|
+
notability-extractor --input-dir ~/notability_export
|
|
60
|
+
|
|
61
|
+
# Custom output directory
|
|
62
|
+
notability-extractor --input-dir ~/notability_export --out-dir ./decks
|
|
63
|
+
|
|
64
|
+
# macOS: just run phase 1 (produce export dir, no .apkg/json/md)
|
|
65
|
+
notability-extractor --extract-only
|
|
66
|
+
|
|
67
|
+
# Custom Anki deck name (only affects what shows up inside Anki)
|
|
68
|
+
notability-extractor --deck-name "Biology 101"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Output filenames are fixed:
|
|
72
|
+
|
|
73
|
+
| File | Contents |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `notability_flashcards.apkg` | Anki deck (quiz questions only) |
|
|
76
|
+
| `notability_flashcards.json` | Full structured dump for programmatic review |
|
|
77
|
+
| `notability_flashcards.md` | Human-readable flashcards |
|
|
78
|
+
| `notability_notes.{json,md}` | Note transcripts |
|
|
79
|
+
| `notability_summaries.{json,md}` | Generated summaries |
|
|
80
|
+
|
|
81
|
+
### Archive management CLI flags
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Open a prompt-driven editor against the archive
|
|
85
|
+
notability-extractor --edit-flashcards
|
|
86
|
+
|
|
87
|
+
# One-shot interactive add
|
|
88
|
+
notability-extractor --add-card
|
|
89
|
+
|
|
90
|
+
# Print archive contents (optionally filter by tag)
|
|
91
|
+
notability-extractor --list-cards
|
|
92
|
+
notability-extractor --list-cards --tag biology
|
|
93
|
+
|
|
94
|
+
# Backup / restore round-trip
|
|
95
|
+
notability-extractor --backup
|
|
96
|
+
notability-extractor --export ~/cards-backup.jsonl
|
|
97
|
+
notability-extractor --import ~/cards-backup.jsonl --mode merge
|
|
98
|
+
notability-extractor --import ~/cards-backup.jsonl --mode replace
|
|
99
|
+
|
|
100
|
+
# Launch the GUI
|
|
101
|
+
notability-extractor --gui
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## GUI
|
|
105
|
+
|
|
106
|
+
A PySide6 desktop companion ships in the same package. After install:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
notability-extractor-gui
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Pages: Library (browse / edit / add / delete cards with tag filter), Notes
|
|
113
|
+
(read-only), Summaries (read-only), Build (export apkg / json / md), Settings
|
|
114
|
+
(theme, paths, backups, schedule).
|
|
115
|
+
|
|
116
|
+
On Wayland with no `DISPLAY` set, the GUI auto-applies
|
|
117
|
+
`QT_QPA_PLATFORM=wayland` so SSH/login sessions don't need a manual export.
|
|
118
|
+
|
|
119
|
+
## Backups
|
|
120
|
+
|
|
121
|
+
The archive at `~/.notability_extractor/cards.jsonl` is snapshotted on every
|
|
122
|
+
save to `~/.notability_extractor/backups/cards-YYYYMMDD-HHMMSS.jsonl`,
|
|
123
|
+
hash-deduped so unchanged saves don't make redundant copies. Default retention
|
|
124
|
+
is the last 10 snapshots (configurable in Settings).
|
|
125
|
+
|
|
126
|
+
For a scheduled backup when the GUI is closed, run from cron:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
0 * * * * notability-extractor --backup
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The Settings page surfaces this exact line for convenience.
|
|
133
|
+
|
|
134
|
+
## Importing into Anki
|
|
135
|
+
|
|
136
|
+
1. Open Anki on your desktop.
|
|
137
|
+
2. `File > Import` and select the generated `.apkg` file.
|
|
138
|
+
3. The deck appears as "Notability Flashcards" (or whatever you passed to
|
|
139
|
+
`--deck-name`).
|
|
140
|
+
|
|
141
|
+
## Caveats
|
|
142
|
+
|
|
143
|
+
- The Learn cache only contains content from sessions you've actively opened
|
|
144
|
+
in Notability. If a note has never had Learn run on it, no quiz is cached.
|
|
145
|
+
- Notability does not provide a stable export API. The tool reads on-disk
|
|
146
|
+
formats that could change between app versions. If extraction breaks after
|
|
147
|
+
a Notability update, open an issue.
|
|
148
|
+
- iPadOS-only setups need iCloud Drive sync enabled so the `.nbn` bundles and
|
|
149
|
+
cache files are present on a Mac. Without sync, you'd need physical access
|
|
150
|
+
to the iPad's sandbox (not currently supported).
|
|
151
|
+
|
|
152
|
+
## Releasing
|
|
153
|
+
|
|
154
|
+
Releases are automated via GitHub Actions. To cut a new release:
|
|
155
|
+
|
|
156
|
+
1. Bump the `version = "X.Y.Z"` line in `pyproject.toml`
|
|
157
|
+
2. Commit and push to `main`
|
|
158
|
+
3. CI runs: tests pass -> autotag creates `vX.Y.Z` -> build produces wheel
|
|
159
|
+
and sdist -> GitHub Release is created -> PyPI publishes via OIDC
|
|
160
|
+
trusted publishing
|
|
161
|
+
|
|
162
|
+
No manual tag step. No API tokens in CI.
|
|
163
|
+
|
|
164
|
+
### One-time PyPI setup
|
|
165
|
+
|
|
166
|
+
(Skip this if the package is already on PyPI with OIDC configured.)
|
|
167
|
+
|
|
168
|
+
1. First upload manually with `./upload-to-pypi.sh` (needs `~/.pypirc` with
|
|
169
|
+
a PyPI API token) to claim the package name.
|
|
170
|
+
2. On PyPI: project settings -> Publishing -> add trusted publisher with
|
|
171
|
+
repo `mdeguzis/notability-extractor`, workflow `ci.yml`, environment
|
|
172
|
+
`pypi`.
|
|
173
|
+
3. On GitHub: repo settings -> Environments -> create `pypi` environment.
|
|
174
|
+
|
|
175
|
+
### Manual ad-hoc upload
|
|
176
|
+
|
|
177
|
+
Only needed if CI is broken:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
./upload-to-pypi.sh --test # TestPyPI dry-run first
|
|
181
|
+
./upload-to-pypi.sh # then prod PyPI
|
|
182
|
+
```
|