A Python library for reading and parsing Apple Notes SQLite databases with comprehensive CLI tools
Project description
Apple Notes Parser
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.
Features
- 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.
Installation
Using uv (recommended)
uv pip install apple-notes-parser
Using pip
pip install apple-notes-parser
Quick Start
from 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)}")
Database Location
The Apple Notes database is typically located at:
~/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite
Command Line Interface (CLI)
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.
Installation and Access
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 --help
CLI Commands Overview
The 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
Global Options
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
CLI Usage Examples
Quick Database Overview:
apple-notes-parser stats --verbose
Find Work-Related Notes:
apple-notes-parser list --folder "Work" --tag "urgent"
Find Notes About Specific Topic:
apple-notes-parser search "budget meeting" --content
Save all images from notes
apple-notes-parser attachments --type image --save ./images
Custom Database Path:
# Use specific database file
apple-notes-parser --database /path/to/custom/NoteStore.sqlite stats
API Reference
Main Parser Class
AppleNotesParser(database_path: str)
Main 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])
Tag and Content Filtering
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 and Filter
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
Mentions and Links
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
Attachments
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
export_notes_to_dict(include_content: bool = True)- Export to dictionary/JSON
Data Models
Note
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
Folder
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
Attachment
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
Attachment Properties
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
Account
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
Examples
Find Notes by Tags
# 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")
Search and Filter
# 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")
Working with Attachments
# 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 Data
# 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)
Technical Details
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.
Limitations
- Encrypted Notes: Password-protected notes cannot be decrypted without the password
- Rich Formatting: Complex formatting information is not fully preserved in plain text output
Development and Building
Prerequisites
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)
Development Setup
-
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
Running Tests
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_parser
Code Quality and Linting
The project uses modern Python tooling for code quality assurance:
Running Ruff (Linting and Formatting)
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 --verbose
Running MyPy (Type Checking)
MyPy 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.py
Pre-commit Workflow
Before submitting code, run the complete quality check using our automated scripts:
./check
Protobuf Code Generation
Important: 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
How to Regenerate Protobuf Files
-
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.pyOr 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
Adding Support for New Database Versions
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
Building and Distribution
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.gz
Dependency Management
The 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
Troubleshooting
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
Contributing
Contributions are welcome! Please feel free to submit pull requests or open issues.
Contribution Guidelines
- 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
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file apple_notes_parser-0.2.0.tar.gz.
File metadata
- Download URL: apple_notes_parser-0.2.0.tar.gz
- Upload date:
- Size: 3.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6031d81ec3347c90bf7e53cad30bb09dd0daf5fec9e89cc699c303ad5ed4e9a
|
|
| MD5 |
3f2e178bba88bac4d15933370b1dd46a
|
|
| BLAKE2b-256 |
fcc1939bac79729bcb32aaef71983ad4e1ebdefc77636b6506197baa068bd4a9
|
File details
Details for the file apple_notes_parser-0.2.0-py3-none-any.whl.
File metadata
- Download URL: apple_notes_parser-0.2.0-py3-none-any.whl
- Upload date:
- Size: 40.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0817af295f49d3561e50bd4a92c7fa8fa53663e4c40c352dd179133b1e27b937
|
|
| MD5 |
13848528f277954cf016b140be5e581c
|
|
| BLAKE2b-256 |
90232f1cc4e1bdf408edf5ebe95f9313bb22d8fce348188613e0ba012d74a414
|