A Python library for reading and parsing Apple Notes SQLite databases. This library extracts data from Apple Notes SQLite stores, including support for reading tags on notes and finding notes that have specific tags.
This is a python implementation of some of the functionality found in Apple Cloud Notes Parser. If you can use Ruby and need a more full featured solution, I recommend trying Apple Cloud Notes Parser.
See also my companion project macnotesapp which uses AppleScript to interact with the Notes app including creating and modifying notes. Apple Notes Parser only supports reading and parsing Apple Notes databases.
- Full Database Parsing: Read all accounts, folders, and notes from Apple Notes databases
- Protobuf Support: Parse compressed note data using Protocol Buffers
- Tag Extraction: Automatically extract hashtags from note content
- Tag Filtering: Find notes by specific tags or combinations of tags
- Mention Support: Extract and search for @mentions in notes
- Link Extraction: Find and filter notes containing URLs
- Attachment Support: Extract attachment metadata and filter notes by attachment type
- Multi-Version Support: Works with macOS 10.11+ Notes databases
- Search Functionality: Full-text search across note content
- Export Capabilities: Export data to JSON format
- Metadata Access: Access creation dates, modification dates, pinned status, etc.
uv pip install apple-notes-parserpip install apple-notes-parserfrom apple_notes_parser import AppleNotesParser
# Initialize parser with your Notes database
parser = AppleNotesParser()
# Load all data
parser.load_data()
# Get basic statistics
print(f"Found {len(parser.notes)} notes in {len(parser.folders)} folders")
# Find notes with specific tags
work_notes = parser.get_notes_by_tag("work")
print(f"Found {len(work_notes)} notes tagged with #work")
# Search for notes containing text
important_notes = parser.search_notes("important")
# Get all unique tags
all_tags = parser.get_all_tags()
print(f"All tags: {', '.join(all_tags)}")The Apple Notes database is typically located at:
~/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite
The package includes a simpl command-line interface for working with Apple Notes databases without writing any code. The CLI provides access to all major functionality including listing notes, searching, exporting data, and analyzing attachments.
After installing the package, the CLI is available as apple-notes-parser:
# Install from PyPI
pip install apple-notes-parser
# Use the CLI
apple-notes-parser --helpThe CLI provides several commands for different use cases:
list- List and filter notessearch- Search notes by text contentexport- Export notes to JSON formatstats- Show database statistics and summaryattachments- List and extract attachmentstags- Analyze and list hashtags
All commands support these global options:
apple-notes-parser [--database PATH] [--version] COMMAND [OPTIONS]--database,-d- Path to NoteStore.sqlite file (auto-detected if not specified)--version- Show version information--help- Show help message
Quick Database Overview:
apple-notes-parser stats --verboseFind Work-Related Notes:
apple-notes-parser list --folder "Work" --tag "urgent"Find Notes About Specific Topic:
apple-notes-parser search "budget meeting" --contentSave all images from notes
apple-notes-parser attachments --type image --save ./imagesCustom Database Path:
# Use specific database file
apple-notes-parser --database /path/to/custom/NoteStore.sqlite statsMain parser class for Apple Notes databases.
Methods:
load_data()- Load all data from the databasenotes- Get all notes (list[Note])folders- Get all folders (list[Folder])accounts- Get all accounts (list[Account])
get_notes_by_tag(tag: str)- Get notes with a specific tagget_notes_by_tags(tags: list[str], match_all: bool = False)- Get notes with multiple tagsget_all_tags()- Get all unique hashtagsget_tag_counts()- Get usage count for each tag
search_notes(query: str, case_sensitive: bool = False)- Full-text searchget_notes_by_folder(folder_name: str)- Get notes in specific folderget_notes_by_account(account_name: str)- Get notes in specific accountget_note_by_applescript_id(applescript_id: str)- Get note by AppleScript ID (e.g. "x-coredata://5A2C18B7-767B-41A9-BF71-E4E966775D32/ICNote/p4884")get_pinned_notes()- Get all pinned notesget_protected_notes()- Get password-protected notesfilter_notes(filter_func: Callable[[Note], bool])- Custom filtering
get_notes_with_mentions()- Get notes containing @mentionsget_notes_by_mention(mention: str)- Get notes mentioning specific userget_notes_with_links()- Get notes containing URLsget_notes_by_link_domain(domain: str)- Get notes with links to specific domain
get_notes_with_attachments()- Get notes that have attachmentsget_notes_by_attachment_type(attachment_type: str)- Get notes with specific attachment types (image, video, audio, document)get_all_attachments()- Get all attachments across all notes
export_notes_to_dict(include_content: bool = True)- Export to dictionary/JSON
id: int- Database primary keynote_id: int- Note identifiertitle: str- Note titlecontent: str- Note text contentcreation_date: datetime- When note was createdmodification_date: datetime- When note was last modifiedaccount: Account- Owning accountfolder: Folder- Containing folderis_pinned: bool- Whether note is pinnedis_password_protected: bool- Whether note is encrypteduuid: str- Unique identifierapplescript_id: str- AppleScript ID of the note (this is the unique identifier used by AppleScript to interact with the note)tags: list[str]- Hashtags found in notementions: list[str]- @mentions found in notelinks: list[str]- URLs found in noteattachments: list[Attachment]- File attachments in noteget_folder_path(): str- Returns the path of the folder the note is inget_attachments_by_extension(extension: str) -> list[Attachment]- Returns a list of attachments with the specified extensionget_attachments_by_type(attachment_type: str) -> list[Attachment]- Returns a list of attachments with the specified type
name: str- Folder nameaccount: Account- Owning accountuuid: str- Unique identifierparent_id: int- Parent folder ID (for nested folders)get_path(): str- Returns the path of the folderget_parent(): Folder- Returns the parent folderis_root(): bool- Returns whether the folder is the root folder
id: int- Database primary keyfilename: str- Attachment filename (e.g., "document.pdf")file_size: int- File size in bytestype_uti: str- Uniform Type Identifier (e.g., "com.adobe.pdf")note_id: int- Parent note IDcreation_date: datetime- When attachment was createdmodification_date: datetime- When attachment was last modifieduuid: str- Unique identifieris_remote: bool- Whether attachment is stored remotelyremote_url: str- Remote URL if applicable
file_extension: str- File extension (e.g., "pdf", "jpg")mime_type: str- MIME type (e.g., "application/pdf", "image/jpeg")is_image: bool- Whether attachment is an imageis_video: bool- Whether attachment is a videois_audio: bool- Whether attachment is audiois_document: bool- Whether attachment is a document
id: int- Database primary keyname: str- Account name (e.g., "iCloud", "On My Mac")identifier: str- Account identifieruser_record_name: str- CloudKit user record name
# Find notes with specific tag
work_notes = parser.get_notes_by_tag("work")
# Find notes with multiple tags (OR logic)
important_or_urgent = parser.get_notes_by_tags(["important", "urgent"], match_all=False)
# Find notes with multiple tags (AND logic)
work_and_important = parser.get_notes_by_tags(["work", "important"], match_all=True)
# Get tag statistics
tag_counts = parser.get_tag_counts()
for tag, count in tag_counts.items():
print(f"#{tag}: {count} notes")# Full-text search
meeting_notes = parser.search_notes("meeting")
# Custom filtering
recent_notes = parser.filter_notes(
lambda note: note.modification_date and
note.modification_date > datetime.now() - timedelta(days=7)
)
# Find notes with attachments
notes_with_attachments = parser.get_notes_with_attachments()
print(f"Found {len(notes_with_attachments)} notes with attachments")
# Find notes with specific attachment types
image_notes = parser.get_notes_by_attachment_type("image")
document_notes = parser.get_notes_by_attachment_type("document")
video_notes = parser.get_notes_by_attachment_type("video")
audio_notes = parser.get_notes_by_attachment_type("audio")# Get all notes that have attachments
notes_with_attachments = parser.get_notes_with_attachments()
print(f"Found {len(notes_with_attachments)} notes with attachments")
# Filter by attachment type
image_notes = parser.get_notes_by_attachment_type("image")
document_notes = parser.get_notes_by_attachment_type("document")
video_notes = parser.get_notes_by_attachment_type("video")
audio_notes = parser.get_notes_by_attachment_type("audio")
# Get all attachments across all notes
all_attachments = parser.get_all_attachments()
for attachment in all_attachments:
print(f"{attachment.filename} ({attachment.file_size} bytes) - {attachment.mime_type}")
# Work with individual note attachments
for note in notes_with_attachments:
print(f"Note: {note.title}")
for attachment in note.attachments:
print(f" - {attachment.filename}")
print(f" Size: {attachment.file_size} bytes")
print(f" Type: {attachment.type_uti}")
print(f" MIME: {attachment.mime_type}")
print(f" Is Image: {attachment.is_image}")
print(f" Is Document: {attachment.is_document}")
# Filter by file extension
if attachment.file_extension == "pdf":
print(f" Found PDF: {attachment.filename}")# Export all data to JSON
data = parser.export_notes_to_dict()
with open("notes_backup.json", "w") as f:
json.dump(data, f, indent=2)
# Export without content (for privacy)
metadata_only = parser.export_notes_to_dict(include_content=False)The library uses Protocol Buffers to parse compressed note data. See Revisiting Apple Notes (6): The Protobuf for more details about how Apple Notes stores data.
- Encrypted Notes: Password-protected notes cannot be decrypted without the password
- Rich Formatting: Complex formatting information is not fully preserved in plain text output
For using the library (end users), you only need:
- Python 3.11+
- Dependencies are automatically installed via
uv:
For development and building, you need:
- Python 3.11+
uvpackage manager (recommended)- Development dependencies including
grpcio-tools(for protobuf code generation, if needed)
-
Clone the repository:
git clone git@github.com:RhetTbull/apple-notes-parser.git cd apple-notes-parser -
Install in development mode with uv (recommended):
uv sync --all-extras
The project includes a comprehensive test suite with 54+ tests:
# Run all tests
uv run pytest
# Run specific test file
uv run pytest tests/test_real_database.py
# Run tests with coverage
uv run pytest --cov=apple_notes_parserThe project uses modern Python tooling for code quality assurance:
Ruff is used for fast linting and import sorting:
# Check for linting issues
uv run ruff check src/
# Automatically fix linting issues (safe fixes only)
uv run ruff check --fix src/
# Apply formatting
uv run ruff format src/
# Check imports are properly sorted
uv run ruff check --select I src/The script ./check in the project root directory is a convenient wrapper for running all linting and formatting checks and automatically fixing safe issues:
uv run ./check --verboseMyPy is used for static type checking:
# Run type checking on the entire codebase
uv run mypy src/apple_notes_parser/
# Run type checking with verbose output
uv run mypy --verbose src/apple_notes_parser/
# Check specific file
uv run mypy src/apple_notes_parser/parser.pyBefore submitting code, run the complete quality check using our automated scripts:
./checkImportant: The protobuf Python files (notestore_pb2.py) are pre-generated and included in the repository. You typically don't need to regenerate them unless:
- You're modifying the
notestore.protofile - You're updating to a newer protobuf version
- You encounter protobuf version compatibility warnings
-
Ensure you have the required development tools:
uv sync --all-extras # Install development dependencies including grpcio-tools -
Navigate to the protobuf source directory:
cd src/apple_notes_parser -
Regenerate the Python protobuf files using the automated script:
python scripts/regenerate_protobuf.py
Or manually:
cd src/apple_notes_parser python -m grpc_tools.protoc --proto_path=. --python_out=. notestore.proto cd ../.. # Back to project root uv run pytest # Verify everything works
The automated script will:
- Regenerate the protobuf files
- Verify the version was updated correctly
- Run the test suite to ensure compatibility
The library is designed to be extensible for future macOS versions:
-
Add new test database:
# Place new database in tests/data/ cp /path/to/NoteStore-macOS-16.sqlite tests/data/ -
Update test fixtures in
tests/conftest.py:@pytest.fixture(params=["macos_15", "macos_16"]) # Add new version def versioned_database(request): if request.param == "macos_16": database_path = Path(__file__).parent / "data" / "NoteStore-macOS-16.sqlite" # ... handle new version
-
Update version detection in
src/apple_notes_parser/database.py:def get_macos_version(self) -> int: # Add detection logic for new version if "NEW_COLUMN_NAME" in columns: self._macos_version = 26 # New version number
-
Run tests to ensure compatibility:
uv run pytest tests/test_version_agnostic.py
To build the package for distribution:
# Install build tools
uv add --dev build
# Build the package
uv run python -m build
# This creates:
# dist/apple_notes_parser-0.1.0-py3-none-any.whl
# dist/apple_notes_parser-0.1.0.tar.gzThe project uses these key dependencies:
-
Runtime dependencies (required for end users):
protobuf>=6.31.1- Protocol buffer runtime for parsing compressed note data
-
Development dependencies (for contributors):
pytest>=8.0.0- Testing frameworkpytest-cov>=4.0.0- Coverage reportingruff>=0.8.0- Fast Python linter and formattermypy>=1.13.0- Static type checkergrpcio-tools>=1.74.0- Protocol buffer compiler for code generationtypes-protobuf>=6.30.2.20250703- Type stubs for protobuf
Protobuf version warnings:
- Regenerate protobuf files using the steps above
- Ensure
protobufandgrpcio-toolsare at compatible versions (install dev dependencies withuv sync --dev)
Test failures:
- Ensure you have the real test database in
tests/data/ - Check that your Python version is 3.11+
- Try running
uv sync --all-extrasto refresh dependencies
Import errors:
- Verify installation with
uv run python -c "import apple_notes_parser; print('OK')" - Check that you're in the correct virtual environment
Contributions are welcome! Please feel free to submit pull requests or open issues.
- Fork the repository and create a feature branch
- Write tests for any new functionality
- Run quality checks before submitting:
uv run ./check- Follow existing code style and patterns
- Update documentation for user-facing changes
- Submit a pull request with a clear description
This project is licensed under the MIT License - see the LICENSE file for details.
This library builds upon the excellent work from:
- threeplanetssoftware/apple_cloud_notes_parser - Ruby implementation and protobuf definitions
- HamburgChimps/apple-notes-liberator - Java implementation
- Ciofeca Forensics - Technical research on Apple Notes storage format
This project was built with assistance of AI, specifically using Claude Code