Since I hijacked a few other threads, like the predecessor, it's probably more appropriate to create a dedicated thread.
joppy supports basically two use cases. For more details, see the Github readme. In both cases, it can be installed by pip install joppy.
Joplin Client API
Requires a running Joplin client including web clipper. Functions from Joplin's data API are supported.
Example:
from joppy.client_api import ClientApi
# Create a new Api instance.
api = ClientApi(token=YOUR_TOKEN)
# Get all notes. Note that this method calls get_notes() multiple times to assemble the unpaginated result.
notes = api.get_all_notes()
This is an attempt to build a wrapper that's working and sufficient for my needs. It is still experimental: Make a backup of your data before using. I can't provide any help at sync issues or lost data.
Supported: Some reverse engineered functions with similar interface like the client API.
Not supported:
Encryption
Some functions that were either to complex or I simply didn't need.
Example (requires a running Joplin server):
from joppy.server_api import ServerApi
# Create a new Api instance.
api = ServerApi(user="admin@localhost", password="admin", url="http://localhost:22300")
# Acquire a sync lock.
with api.sync_lock():
# Add a notebook.
notebook_id = api.add_notebook(title="My first notebook")
# Add a note in the previously created notebook.
note_id = api.add_note(title="My first note", body="With some content", parent_id=notebook_id)
This is really cool!
And maybe this helps somebody who also tries to make great things with joplin and joppy against the Joplin Server. I created this to see which users are registered.
#!/usr/bin/env python3
import argparse
import logging
from joppy.server_api import ServerApi
from pathlib import Path
import sys
from typing import Dict, Optional, Tuple
import re
from datetime import datetime
import json
from json import JSONEncoder
class DateTimeEncoder(JSONEncoder):
"""Custom JSON encoder for datetime objects"""
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
def setup_logging(debug: bool = False) -> None:
"""Configure logging with appropriate level and format"""
level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(
level=level,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
def read_config() -> Dict[str, str]:
"""Reads configuration from ~/.joplin.creds"""
config_path = Path.home() / '.joplin.creds'
if not config_path.exists():
raise FileNotFoundError(f"Configuration file not found: {config_path}")
config = {}
try:
with open(config_path, 'r') as f:
for line in f:
line = line.strip()
if line and '=' in line:
key, value = line.split('=', 1)
value = value.strip().strip('"\'')
config[key.strip()] = value
except Exception as e:
logging.error(f"Error reading config file: {e}")
raise
return {
'url': config.get('JOPLIN_SERVER_URL', '').rstrip('/'),
'admin_user': config.get('JOPLIN_ADMIN', ''),
'admin_password': config.get('JOPLIN_ADMIN_PASS', '')
}
def create_api_client(config: Dict[str, str]) -> Optional[ServerApi]:
"""Creates an admin API client with error handling"""
if not (config['admin_user'] and config['admin_password']):
logging.warning("Admin credentials not provided in config")
return None
try:
admin_api = ServerApi(
user=config['admin_user'],
password=config['admin_password'],
url=config['url']
)
logging.info(f"š Admin user logged in as: {config['admin_user']}")
return admin_api
except Exception as e:
logging.error(f"Failed to create admin API client: {e}")
return None
def perform_admin_operations(admin_api: ServerApi) -> None:
"""Performs administrative operations with enhanced error handling"""
if not admin_api:
logging.error("No admin API client available")
return
try:
with admin_api.sync_lock():
users = admin_api.get_all_users()
logging.info(f"\n[{admin_api.user}] Registered users:")
for user in users:
logging.info(f" - {user.email}")
if logging.getLogger().isEnabledFor(logging.DEBUG):
logging.debug(f" User details: {json.dumps(user.__dict__, indent=2, cls=DateTimeEncoder)}")
except Exception as e:
logging.error(f"Error during admin operations: {e}")
if logging.getLogger().isEnabledFor(logging.DEBUG):
logging.debug("Stack trace:", exc_info=True)
def parse_arguments():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(description='Joplin Admin Tool')
parser.add_argument('--debug', action='store_true', help='Enable debug logging')
parser.add_argument('--config', type=str, help='Custom config file path')
return parser.parse_args()
def main() -> None:
"""Main function with enhanced API examples and error handling"""
args = parse_arguments()
setup_logging(args.debug)
try:
config = read_config()
admin_api = create_api_client(config)
if admin_api:
logging.info("\nš¤ Admin Functions:")
logging.info("=" * 50)
perform_admin_operations(admin_api)
else:
logging.error("Failed to initialize admin functionality")
except Exception as e:
logging.error(f"Critical error: {str(e)}")
if logging.getLogger().isEnabledFor(logging.DEBUG):
logging.debug("Stack trace:", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
main()
I agree, this looks super cool; with automated tests and everything. I'm surprised I haven't seen this thread pick up more attention. One of those "I don't have a use case for but super appreciate existing incase I suddenly do" things for me.