notesctl 1.0.3__tar.gz → 1.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.
- notesctl-1.1.0/.github/dependabot.yml +20 -0
- notesctl-1.1.0/.github/workflows/ci.yml +28 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/.github/workflows/publish.yml +13 -8
- notesctl-1.1.0/AGENTS.md +8 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/CHANGELOG.md +9 -1
- {notesctl-1.0.3 → notesctl-1.1.0}/CLAUDE.md +8 -9
- notesctl-1.1.0/Makefile +15 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/PKG-INFO +12 -16
- {notesctl-1.0.3 → notesctl-1.1.0}/README.md +10 -12
- {notesctl-1.0.3 → notesctl-1.1.0}/pyproject.toml +3 -31
- notesctl-1.1.0/scripts/live-e2e.py +54 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/__init__.py +1 -1
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/cli.py +43 -13
- {notesctl-1.0.3 → notesctl-1.1.0}/uv.lock +230 -435
- notesctl-1.0.3/.github/workflows/ci.yml +0 -68
- notesctl-1.0.3/tests/__init__.py +0 -1
- notesctl-1.0.3/tests/test_cli.py +0 -49
- notesctl-1.0.3/tests/test_connection.py +0 -94
- notesctl-1.0.3/tests/test_converter.py +0 -216
- notesctl-1.0.3/tests/test_models.py +0 -101
- notesctl-1.0.3/tests/test_parser.py +0 -68
- {notesctl-1.0.3 → notesctl-1.1.0}/.gitignore +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/LICENSE +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/proto/notestore.proto +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/converter/__init__.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/converter/markdown.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/database/__init__.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/database/connection.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/database/models.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/database/queries.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/exporter.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/parser/__init__.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/parser/note_parser.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/parser/protobuf/__init__.py +0 -0
- {notesctl-1.0.3 → notesctl-1.1.0}/src/notesctl/parser/protobuf/notestore_pb2.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: uv
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
cooldown:
|
|
8
|
+
default-days: 7
|
|
9
|
+
groups:
|
|
10
|
+
python:
|
|
11
|
+
patterns: ["*"]
|
|
12
|
+
- package-ecosystem: github-actions
|
|
13
|
+
directory: /
|
|
14
|
+
schedule:
|
|
15
|
+
interval: weekly
|
|
16
|
+
cooldown:
|
|
17
|
+
default-days: 7
|
|
18
|
+
groups:
|
|
19
|
+
actions:
|
|
20
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
check:
|
|
13
|
+
runs-on: macos-latest
|
|
14
|
+
timeout-minutes: 15
|
|
15
|
+
strategy:
|
|
16
|
+
matrix:
|
|
17
|
+
python: ["3.11", "3.12", "3.13"]
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
|
+
with:
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python }}
|
|
25
|
+
- name: Static checks and package build
|
|
26
|
+
run: make check
|
|
27
|
+
- name: Check installed CLI
|
|
28
|
+
run: uv run --locked --no-config notesctl --version
|
|
@@ -10,24 +10,29 @@ on:
|
|
|
10
10
|
required: false
|
|
11
11
|
default: 'false'
|
|
12
12
|
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
13
16
|
jobs:
|
|
14
17
|
build:
|
|
15
18
|
runs-on: ubuntu-latest
|
|
16
19
|
|
|
17
20
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
22
|
+
with:
|
|
23
|
+
persist-credentials: false
|
|
19
24
|
|
|
20
25
|
- name: Install uv
|
|
21
|
-
uses: astral-sh/setup-uv@
|
|
26
|
+
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
22
27
|
|
|
23
28
|
- name: Set up Python
|
|
24
29
|
run: uv python install 3.12
|
|
25
30
|
|
|
26
31
|
- name: Build package
|
|
27
|
-
run:
|
|
32
|
+
run: make check
|
|
28
33
|
|
|
29
34
|
- name: Upload artifacts
|
|
30
|
-
uses: actions/upload-artifact@
|
|
35
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
31
36
|
with:
|
|
32
37
|
name: dist
|
|
33
38
|
path: dist/
|
|
@@ -46,13 +51,13 @@ jobs:
|
|
|
46
51
|
|
|
47
52
|
steps:
|
|
48
53
|
- name: Download artifacts
|
|
49
|
-
uses: actions/download-artifact@
|
|
54
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
50
55
|
with:
|
|
51
56
|
name: dist
|
|
52
57
|
path: dist/
|
|
53
58
|
|
|
54
59
|
- name: Publish to Test PyPI
|
|
55
|
-
uses: pypa/gh-action-pypi-publish@
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
56
61
|
with:
|
|
57
62
|
repository-url: https://test.pypi.org/legacy/
|
|
58
63
|
|
|
@@ -70,10 +75,10 @@ jobs:
|
|
|
70
75
|
|
|
71
76
|
steps:
|
|
72
77
|
- name: Download artifacts
|
|
73
|
-
uses: actions/download-artifact@
|
|
78
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
74
79
|
with:
|
|
75
80
|
name: dist
|
|
76
81
|
path: dist/
|
|
77
82
|
|
|
78
83
|
- name: Publish to PyPI
|
|
79
|
-
uses: pypa/gh-action-pypi-publish@
|
|
84
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
notesctl-1.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Agent instructions
|
|
2
|
+
|
|
3
|
+
Run `make check` for static checks and package validation.
|
|
4
|
+
Run `make test` for real Apple Notes reads when database access is available.
|
|
5
|
+
Report missing database access as unverified.
|
|
6
|
+
Keep only live end-to-end tests and static checks. Do not add unit or mock tests.
|
|
7
|
+
Preserve read-only access to Apple Notes. Never write to the source database.
|
|
8
|
+
Use feature branches and pull requests.
|
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-10-10
|
|
11
|
+
|
|
12
|
+
- Add version aliases and machine-readable doctor output.
|
|
13
|
+
- Send errors and progress to stderr; preserve read-only source access.
|
|
14
|
+
- Replace synthetic suites with real Notes checks and static package validation.
|
|
15
|
+
- Keep package and CLI version metadata consistent.
|
|
16
|
+
|
|
10
17
|
## [1.0.3] - 2026-06-08
|
|
11
18
|
|
|
12
19
|
### Fixed
|
|
@@ -57,7 +64,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
57
64
|
- Original Notes database is never modified
|
|
58
65
|
- All queries validated to be SELECT statements only
|
|
59
66
|
|
|
60
|
-
[Unreleased]: https://github.com/jwmoss/notesctl/compare/v1.0
|
|
67
|
+
[Unreleased]: https://github.com/jwmoss/notesctl/compare/v1.1.0...HEAD
|
|
68
|
+
[1.1.0]: https://github.com/jwmoss/notesctl/compare/v1.0.3...v1.1.0
|
|
61
69
|
[1.0.3]: https://github.com/jwmoss/notesctl/compare/v1.0.2...v1.0.3
|
|
62
70
|
[1.0.2]: https://github.com/jwmoss/notesctl/compare/v1.0.1...v1.0.2
|
|
63
71
|
[1.0.1]: https://github.com/jwmoss/notesctl/compare/v1.0.0...v1.0.1
|
|
@@ -14,15 +14,11 @@ uv run notesctl list-notes
|
|
|
14
14
|
uv run notesctl list-folders
|
|
15
15
|
uv run notesctl stats
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# Linting and type checking
|
|
23
|
-
uv run ruff check src tests # lint
|
|
24
|
-
uv run ruff format src tests # format
|
|
25
|
-
uv run ty check # type check
|
|
17
|
+
# Static checks and package build
|
|
18
|
+
make check
|
|
19
|
+
|
|
20
|
+
# Actual local Apple Notes reads (requires macOS database access)
|
|
21
|
+
make test
|
|
26
22
|
```
|
|
27
23
|
|
|
28
24
|
## Architecture
|
|
@@ -61,3 +57,6 @@ The tool is designed to never modify the original Notes database:
|
|
|
61
57
|
### Protobuf Files
|
|
62
58
|
|
|
63
59
|
The `parser/protobuf/notestore_pb2.py` is auto-generated and excluded from ty/ruff checks. The protobuf schema defines how Apple Notes stores note content (text, formatting, attachments).
|
|
60
|
+
|
|
61
|
+
Keep only real end-to-end CLI tests and static checks. Do not add unit or mock tests.
|
|
62
|
+
Live checks must not write to the source Notes database.
|
notesctl-1.1.0/Makefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.PHONY: check test build
|
|
2
|
+
|
|
3
|
+
check:
|
|
4
|
+
uv lock --check --no-config
|
|
5
|
+
uv sync --locked --all-extras --no-config
|
|
6
|
+
uv run --locked --no-config ruff check src/ scripts/
|
|
7
|
+
uv run --locked --no-config ruff format --check src/ scripts/
|
|
8
|
+
uv run --locked --no-config ty check
|
|
9
|
+
uv build --no-config
|
|
10
|
+
|
|
11
|
+
test:
|
|
12
|
+
uv run --locked --no-config python scripts/live-e2e.py
|
|
13
|
+
|
|
14
|
+
build:
|
|
15
|
+
uv build --no-config
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: notesctl
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Export Apple iCloud Notes to Markdown files safely
|
|
5
5
|
Project-URL: Homepage, https://github.com/jwmoss/notesctl
|
|
6
6
|
Project-URL: Repository, https://github.com/jwmoss/notesctl
|
|
@@ -28,8 +28,6 @@ Requires-Dist: rich>=13.0.0
|
|
|
28
28
|
Requires-Dist: typer>=0.9.0
|
|
29
29
|
Provides-Extra: dev
|
|
30
30
|
Requires-Dist: grpcio-tools>=1.60.0; extra == 'dev'
|
|
31
|
-
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
32
|
-
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
33
31
|
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
34
32
|
Requires-Dist: ty>=0.0.1; extra == 'dev'
|
|
35
33
|
Description-Content-Type: text/markdown
|
|
@@ -88,21 +86,19 @@ This tool prioritizes safety:
|
|
|
88
86
|
## Development
|
|
89
87
|
|
|
90
88
|
```bash
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# Run tests
|
|
95
|
-
uv run pytest
|
|
89
|
+
make check # lint, format, types, lock, package build
|
|
90
|
+
make test # real local Apple Notes reads
|
|
91
|
+
```
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
Live checks require a populated Apple Notes database and macOS file access. They
|
|
94
|
+
read database copies and preview a Markdown export through the installed CLI.
|
|
95
|
+
They never write to Apple Notes or save exported notes.
|
|
96
|
+
CI runs static and package checks. The project has no unit or mock test suite.
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
Use `notesctl --version` or `notesctl version` for the package version.
|
|
99
|
+
Use `notesctl doctor --json` to verify database access and read note counts.
|
|
100
|
+
Command errors use stderr; data and tables use stdout.
|
|
102
101
|
|
|
103
|
-
# Format code
|
|
104
|
-
uv run ruff format src tests
|
|
105
|
-
```
|
|
106
102
|
|
|
107
103
|
## Requirements
|
|
108
104
|
|
|
@@ -52,21 +52,19 @@ This tool prioritizes safety:
|
|
|
52
52
|
## Development
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Run tests
|
|
59
|
-
uv run pytest
|
|
55
|
+
make check # lint, format, types, lock, package build
|
|
56
|
+
make test # real local Apple Notes reads
|
|
57
|
+
```
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
Live checks require a populated Apple Notes database and macOS file access. They
|
|
60
|
+
read database copies and preview a Markdown export through the installed CLI.
|
|
61
|
+
They never write to Apple Notes or save exported notes.
|
|
62
|
+
CI runs static and package checks. The project has no unit or mock test suite.
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
Use `notesctl --version` or `notesctl version` for the package version.
|
|
65
|
+
Use `notesctl doctor --json` to verify database access and read note counts.
|
|
66
|
+
Command errors use stderr; data and tables use stdout.
|
|
66
67
|
|
|
67
|
-
# Format code
|
|
68
|
-
uv run ruff format src tests
|
|
69
|
-
```
|
|
70
68
|
|
|
71
69
|
## Requirements
|
|
72
70
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "notesctl"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Export Apple iCloud Notes to Markdown files safely"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -39,8 +39,6 @@ Changelog = "https://github.com/jwmoss/notesctl/blob/main/CHANGELOG.md"
|
|
|
39
39
|
|
|
40
40
|
[project.optional-dependencies]
|
|
41
41
|
dev = [
|
|
42
|
-
"pytest>=7.0.0",
|
|
43
|
-
"pytest-cov>=4.0.0",
|
|
44
42
|
"ruff>=0.8.0",
|
|
45
43
|
"ty>=0.0.1",
|
|
46
44
|
"grpcio-tools>=1.60.0",
|
|
@@ -55,7 +53,7 @@ packages = ["src/notesctl"]
|
|
|
55
53
|
[tool.ruff]
|
|
56
54
|
line-length = 100
|
|
57
55
|
target-version = "py311"
|
|
58
|
-
src = ["src"
|
|
56
|
+
src = ["src"]
|
|
59
57
|
|
|
60
58
|
[tool.ruff.lint]
|
|
61
59
|
select = [
|
|
@@ -103,7 +101,6 @@ ignore = [
|
|
|
103
101
|
]
|
|
104
102
|
|
|
105
103
|
[tool.ruff.lint.per-file-ignores]
|
|
106
|
-
"tests/*" = ["S101", "ARG", "PLR2004", "DTZ", "SIM105", "PLC0415", "SLF001", "SIM117"]
|
|
107
104
|
"*_pb2.py" = ["ALL"]
|
|
108
105
|
|
|
109
106
|
[tool.ruff.lint.isort]
|
|
@@ -113,7 +110,7 @@ known-first-party = ["notesctl"]
|
|
|
113
110
|
python-version = "3.11"
|
|
114
111
|
|
|
115
112
|
[tool.ty.src]
|
|
116
|
-
include = ["src"
|
|
113
|
+
include = ["src"]
|
|
117
114
|
exclude = ["*_pb2.py"]
|
|
118
115
|
|
|
119
116
|
[[tool.ty.overrides]]
|
|
@@ -128,28 +125,3 @@ missing-argument = "ignore"
|
|
|
128
125
|
too-many-positional-arguments = "ignore"
|
|
129
126
|
invalid-return-type = "ignore"
|
|
130
127
|
unresolved-attribute = "ignore"
|
|
131
|
-
|
|
132
|
-
[tool.pytest.ini_options]
|
|
133
|
-
testpaths = ["tests"]
|
|
134
|
-
pythonpath = ["src"]
|
|
135
|
-
addopts = [
|
|
136
|
-
"-v",
|
|
137
|
-
"--tb=short",
|
|
138
|
-
"--strict-markers",
|
|
139
|
-
]
|
|
140
|
-
markers = [
|
|
141
|
-
"slow: marks tests as slow",
|
|
142
|
-
]
|
|
143
|
-
|
|
144
|
-
[tool.coverage.run]
|
|
145
|
-
source = ["src/notesctl"]
|
|
146
|
-
branch = true
|
|
147
|
-
omit = ["src/notesctl/parser/protobuf/*"]
|
|
148
|
-
|
|
149
|
-
[tool.coverage.report]
|
|
150
|
-
exclude_lines = [
|
|
151
|
-
"pragma: no cover",
|
|
152
|
-
"def __repr__",
|
|
153
|
-
"raise NotImplementedError",
|
|
154
|
-
"if TYPE_CHECKING:",
|
|
155
|
-
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Read the real Apple Notes database through the installed CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from tempfile import TemporaryDirectory
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
"""Check database access and an actual note listing without source writes."""
|
|
13
|
+
result = subprocess.run(
|
|
14
|
+
["notesctl", "doctor", "--json"],
|
|
15
|
+
capture_output=True,
|
|
16
|
+
text=True,
|
|
17
|
+
timeout=60,
|
|
18
|
+
check=True,
|
|
19
|
+
)
|
|
20
|
+
counts = json.loads(result.stdout)
|
|
21
|
+
if not isinstance(counts.get("total_notes"), int) or counts["total_notes"] < 1:
|
|
22
|
+
raise RuntimeError("No real Notes records found; live proof requires a populated database")
|
|
23
|
+
result = subprocess.run(
|
|
24
|
+
["notesctl", "list-notes", "--limit", "1"],
|
|
25
|
+
capture_output=True,
|
|
26
|
+
text=True,
|
|
27
|
+
timeout=60,
|
|
28
|
+
check=True,
|
|
29
|
+
)
|
|
30
|
+
if "Notes (" not in result.stdout:
|
|
31
|
+
raise RuntimeError("The CLI did not list a real Notes record")
|
|
32
|
+
with TemporaryDirectory(prefix="notesctl-live-") as work_dir:
|
|
33
|
+
output = Path(work_dir) / "export"
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["notesctl", "export", "--dry-run", "--output", str(output)],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
timeout=120,
|
|
39
|
+
check=True,
|
|
40
|
+
)
|
|
41
|
+
exported = re.search(r"Would export (\d+) notes", result.stdout)
|
|
42
|
+
if not exported or int(exported[1]) < 1 or "Failed to export" in result.stderr:
|
|
43
|
+
raise RuntimeError("The live export preview did not convert real Notes records")
|
|
44
|
+
if output.exists():
|
|
45
|
+
raise RuntimeError("The export preview created its output directory")
|
|
46
|
+
print("PASS real Apple Notes CLI reads and export preview")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
try:
|
|
51
|
+
main()
|
|
52
|
+
except (RuntimeError, ValueError, OSError, subprocess.SubprocessError) as error:
|
|
53
|
+
print(f"FAIL {error}; run notesctl doctor to inspect database access", file=sys.stderr)
|
|
54
|
+
sys.exit(1)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Command-line interface for notesctl."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
from importlib.metadata import version as package_version
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Annotated
|
|
5
7
|
|
|
@@ -14,8 +16,28 @@ app = typer.Typer(
|
|
|
14
16
|
name="notesctl",
|
|
15
17
|
help="Export Apple iCloud Notes to Markdown files.",
|
|
16
18
|
no_args_is_help=True,
|
|
19
|
+
invoke_without_command=True,
|
|
17
20
|
)
|
|
18
21
|
console = Console()
|
|
22
|
+
error_console = Console(stderr=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("version")
|
|
26
|
+
def version() -> None:
|
|
27
|
+
"""Print version information."""
|
|
28
|
+
typer.echo(f"notesctl, version {package_version('notesctl')}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.callback()
|
|
32
|
+
def root(
|
|
33
|
+
show_version: Annotated[
|
|
34
|
+
bool, typer.Option("--version", is_eager=True, help="Print version information")
|
|
35
|
+
] = False,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Export Apple Notes through read-only database access."""
|
|
38
|
+
if show_version:
|
|
39
|
+
version()
|
|
40
|
+
raise typer.Exit
|
|
19
41
|
|
|
20
42
|
|
|
21
43
|
@app.command()
|
|
@@ -93,14 +115,14 @@ def export(
|
|
|
93
115
|
with Progress(
|
|
94
116
|
SpinnerColumn(),
|
|
95
117
|
TextColumn("[progress.description]{task.description}"),
|
|
96
|
-
console=
|
|
118
|
+
console=error_console,
|
|
97
119
|
) as progress:
|
|
98
120
|
result = exporter.export(options, progress)
|
|
99
121
|
except FileNotFoundError as e:
|
|
100
|
-
|
|
122
|
+
error_console.print(f"[red]Error:[/red] {e}")
|
|
101
123
|
raise typer.Exit(1) from None
|
|
102
124
|
except Exception as e:
|
|
103
|
-
|
|
125
|
+
error_console.print(f"[red]Error:[/red] {e}")
|
|
104
126
|
raise typer.Exit(1) from None
|
|
105
127
|
|
|
106
128
|
# Print results
|
|
@@ -119,15 +141,15 @@ def export(
|
|
|
119
141
|
console.print(f" Attachments copied: {result.attachments_copied}")
|
|
120
142
|
|
|
121
143
|
if result.skipped_encrypted:
|
|
122
|
-
|
|
144
|
+
error_console.print(f"[yellow]Skipped {result.skipped_encrypted} encrypted notes[/yellow]")
|
|
123
145
|
|
|
124
146
|
if result.skipped_errors:
|
|
125
|
-
|
|
147
|
+
error_console.print(f"[red]Failed to export {result.skipped_errors} notes[/red]")
|
|
126
148
|
|
|
127
149
|
if result.errors:
|
|
128
|
-
|
|
150
|
+
error_console.print("\n[red]Errors:[/red]")
|
|
129
151
|
for error in result.errors[:10]:
|
|
130
|
-
|
|
152
|
+
error_console.print(f" {error}")
|
|
131
153
|
|
|
132
154
|
if not dry_run and (result.skipped_errors or result.errors):
|
|
133
155
|
raise typer.Exit(1)
|
|
@@ -174,10 +196,10 @@ def list_notes(
|
|
|
174
196
|
try:
|
|
175
197
|
notes = exporter.list_notes(folder, include_trashed)
|
|
176
198
|
except FileNotFoundError as e:
|
|
177
|
-
|
|
199
|
+
error_console.print(f"[red]Error:[/red] {e}")
|
|
178
200
|
raise typer.Exit(1) from None
|
|
179
201
|
except Exception as e:
|
|
180
|
-
|
|
202
|
+
error_console.print(f"[red]Error:[/red] {e}")
|
|
181
203
|
raise typer.Exit(1) from None
|
|
182
204
|
|
|
183
205
|
if not notes:
|
|
@@ -231,10 +253,10 @@ def list_folders(
|
|
|
231
253
|
try:
|
|
232
254
|
folders = exporter.list_folders()
|
|
233
255
|
except FileNotFoundError as e:
|
|
234
|
-
|
|
256
|
+
error_console.print(f"[red]Error:[/red] {e}")
|
|
235
257
|
raise typer.Exit(1) from None
|
|
236
258
|
except Exception as e:
|
|
237
|
-
|
|
259
|
+
error_console.print(f"[red]Error:[/red] {e}")
|
|
238
260
|
raise typer.Exit(1) from None
|
|
239
261
|
|
|
240
262
|
if not folders:
|
|
@@ -251,8 +273,12 @@ def list_folders(
|
|
|
251
273
|
console.print(table)
|
|
252
274
|
|
|
253
275
|
|
|
276
|
+
@app.command("doctor")
|
|
254
277
|
@app.command("stats")
|
|
255
278
|
def stats(
|
|
279
|
+
json_output: Annotated[
|
|
280
|
+
bool, typer.Option("--json", help="Print database counts as JSON")
|
|
281
|
+
] = False,
|
|
256
282
|
db_path: Annotated[
|
|
257
283
|
Path | None,
|
|
258
284
|
typer.Option(
|
|
@@ -268,12 +294,16 @@ def stats(
|
|
|
268
294
|
try:
|
|
269
295
|
db_stats = exporter.get_stats()
|
|
270
296
|
except FileNotFoundError as e:
|
|
271
|
-
|
|
297
|
+
error_console.print(f"[red]Error:[/red] {e}")
|
|
272
298
|
raise typer.Exit(1) from None
|
|
273
299
|
except Exception as e:
|
|
274
|
-
|
|
300
|
+
error_console.print(f"[red]Error:[/red] {e}")
|
|
275
301
|
raise typer.Exit(1) from None
|
|
276
302
|
|
|
303
|
+
if json_output:
|
|
304
|
+
typer.echo(json.dumps(db_stats))
|
|
305
|
+
return
|
|
306
|
+
|
|
277
307
|
console.print("[bold]Notes Database Statistics[/bold]\n")
|
|
278
308
|
console.print(f" Total notes: {db_stats['total_notes']}")
|
|
279
309
|
console.print(f" Encrypted notes: {db_stats['encrypted_notes']}")
|