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.
Files changed (70) hide show
  1. notability_extractor-0.1.0/.github/workflows/ci.yml +107 -0
  2. notability_extractor-0.1.0/.gitignore +35 -0
  3. notability_extractor-0.1.0/Makefile +63 -0
  4. notability_extractor-0.1.0/PKG-INFO +205 -0
  5. notability_extractor-0.1.0/README.md +182 -0
  6. notability_extractor-0.1.0/pyproject.toml +183 -0
  7. notability_extractor-0.1.0/src/notability_extractor/__init__.py +3 -0
  8. notability_extractor-0.1.0/src/notability_extractor/__main__.py +3 -0
  9. notability_extractor-0.1.0/src/notability_extractor/anki.py +297 -0
  10. notability_extractor-0.1.0/src/notability_extractor/archive/__init__.py +1 -0
  11. notability_extractor-0.1.0/src/notability_extractor/archive/backup.py +198 -0
  12. notability_extractor-0.1.0/src/notability_extractor/archive/config.py +109 -0
  13. notability_extractor-0.1.0/src/notability_extractor/archive/filter.py +44 -0
  14. notability_extractor-0.1.0/src/notability_extractor/archive/scheduler.py +65 -0
  15. notability_extractor-0.1.0/src/notability_extractor/archive/scheduler_install.py +186 -0
  16. notability_extractor-0.1.0/src/notability_extractor/archive/store.py +217 -0
  17. notability_extractor-0.1.0/src/notability_extractor/build/__init__.py +1 -0
  18. notability_extractor-0.1.0/src/notability_extractor/build/flashcards.py +91 -0
  19. notability_extractor-0.1.0/src/notability_extractor/build/notes.py +31 -0
  20. notability_extractor-0.1.0/src/notability_extractor/build/reader.py +108 -0
  21. notability_extractor-0.1.0/src/notability_extractor/build/summaries.py +38 -0
  22. notability_extractor-0.1.0/src/notability_extractor/cli.py +263 -0
  23. notability_extractor-0.1.0/src/notability_extractor/extract/__init__.py +1 -0
  24. notability_extractor-0.1.0/src/notability_extractor/extract/exporter.py +45 -0
  25. notability_extractor-0.1.0/src/notability_extractor/extract/http_cache.py +87 -0
  26. notability_extractor-0.1.0/src/notability_extractor/extract/nbn.py +78 -0
  27. notability_extractor-0.1.0/src/notability_extractor/extract/platform_check.py +35 -0
  28. notability_extractor-0.1.0/src/notability_extractor/gui/__init__.py +0 -0
  29. notability_extractor-0.1.0/src/notability_extractor/gui/app.py +68 -0
  30. notability_extractor-0.1.0/src/notability_extractor/gui/main_window.py +119 -0
  31. notability_extractor-0.1.0/src/notability_extractor/gui/pages/__init__.py +0 -0
  32. notability_extractor-0.1.0/src/notability_extractor/gui/pages/export.py +123 -0
  33. notability_extractor-0.1.0/src/notability_extractor/gui/pages/library.py +203 -0
  34. notability_extractor-0.1.0/src/notability_extractor/gui/pages/notes.py +102 -0
  35. notability_extractor-0.1.0/src/notability_extractor/gui/pages/settings.py +349 -0
  36. notability_extractor-0.1.0/src/notability_extractor/gui/pages/summaries.py +101 -0
  37. notability_extractor-0.1.0/src/notability_extractor/gui/theme.py +61 -0
  38. notability_extractor-0.1.0/src/notability_extractor/gui/widgets/__init__.py +0 -0
  39. notability_extractor-0.1.0/src/notability_extractor/gui/widgets/card_editor.py +180 -0
  40. notability_extractor-0.1.0/src/notability_extractor/gui/widgets/tag_filter.py +101 -0
  41. notability_extractor-0.1.0/src/notability_extractor/gui/widgets/tag_input.py +161 -0
  42. notability_extractor-0.1.0/src/notability_extractor/model.py +76 -0
  43. notability_extractor-0.1.0/src/notability_extractor/utils.py +80 -0
  44. notability_extractor-0.1.0/tests/__init__.py +0 -0
  45. notability_extractor-0.1.0/tests/archive/__init__.py +0 -0
  46. notability_extractor-0.1.0/tests/archive/test_backup.py +208 -0
  47. notability_extractor-0.1.0/tests/archive/test_config.py +59 -0
  48. notability_extractor-0.1.0/tests/archive/test_filter.py +70 -0
  49. notability_extractor-0.1.0/tests/archive/test_scheduler.py +61 -0
  50. notability_extractor-0.1.0/tests/archive/test_scheduler_install.py +106 -0
  51. notability_extractor-0.1.0/tests/archive/test_store.py +222 -0
  52. notability_extractor-0.1.0/tests/build/__init__.py +0 -0
  53. notability_extractor-0.1.0/tests/build/test_flashcards.py +53 -0
  54. notability_extractor-0.1.0/tests/build/test_notes.py +26 -0
  55. notability_extractor-0.1.0/tests/build/test_summaries.py +27 -0
  56. notability_extractor-0.1.0/tests/conftest.py +1 -0
  57. notability_extractor-0.1.0/tests/gui/__init__.py +0 -0
  58. notability_extractor-0.1.0/tests/gui/test_app_smoke.py +33 -0
  59. notability_extractor-0.1.0/tests/gui/test_card_editor.py +54 -0
  60. notability_extractor-0.1.0/tests/gui/test_library_page.py +50 -0
  61. notability_extractor-0.1.0/tests/gui/test_tag_input.py +38 -0
  62. notability_extractor-0.1.0/tests/test_build_reader.py +76 -0
  63. notability_extractor-0.1.0/tests/test_cli.py +113 -0
  64. notability_extractor-0.1.0/tests/test_extract_exporter.py +57 -0
  65. notability_extractor-0.1.0/tests/test_extract_http_cache.py +109 -0
  66. notability_extractor-0.1.0/tests/test_extract_nbn.py +90 -0
  67. notability_extractor-0.1.0/tests/test_extract_platform_check.py +40 -0
  68. notability_extractor-0.1.0/tests/test_model.py +151 -0
  69. notability_extractor-0.1.0/upload-to-pypi.sh +70 -0
  70. 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
+ ```