Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:30:10 +08:00
commit f0bd18fb4e
824 changed files with 331919 additions and 0 deletions

View File

@@ -0,0 +1,334 @@
#!/usr/bin/env python3
"""
LabArchives Entry Operations
Utilities for creating entries, uploading attachments, and managing notebook content.
"""
import argparse
import sys
import yaml
import os
from pathlib import Path
from datetime import datetime
def load_config(config_path='config.yaml'):
"""Load configuration from YAML file"""
try:
with open(config_path, 'r') as f:
return yaml.safe_load(f)
except FileNotFoundError:
print(f"❌ Configuration file not found: {config_path}")
print(" Run setup_config.py first to create configuration")
sys.exit(1)
except Exception as e:
print(f"❌ Error loading configuration: {e}")
sys.exit(1)
def init_client(config):
"""Initialize LabArchives API client"""
try:
from labarchivespy.client import Client
return Client(
config['api_url'],
config['access_key_id'],
config['access_password']
)
except ImportError:
print("❌ labarchives-py package not installed")
print(" Install with: pip install git+https://github.com/mcmero/labarchives-py")
sys.exit(1)
def get_user_id(client, config):
"""Get user ID via authentication"""
import xml.etree.ElementTree as ET
login_params = {
'login_or_email': config['user_email'],
'password': config['user_external_password']
}
try:
response = client.make_call('users', 'user_access_info', params=login_params)
if response.status_code == 200:
uid = ET.fromstring(response.content)[0].text
return uid
else:
print(f"❌ Authentication failed: HTTP {response.status_code}")
print(f" Response: {response.content.decode('utf-8')[:200]}")
sys.exit(1)
except Exception as e:
print(f"❌ Error during authentication: {e}")
sys.exit(1)
def create_entry(client, uid, nbid, title, content=None, date=None):
"""Create a new entry in a notebook"""
print(f"\n📝 Creating entry: {title}")
# Prepare parameters
params = {
'uid': uid,
'nbid': nbid,
'title': title
}
if content:
# Ensure content is HTML formatted
if not content.startswith('<'):
content = f'<p>{content}</p>'
params['content'] = content
if date:
params['date'] = date
try:
response = client.make_call('entries', 'create_entry', params=params)
if response.status_code == 200:
print("✅ Entry created successfully")
# Try to extract entry ID from response
try:
import xml.etree.ElementTree as ET
root = ET.fromstring(response.content)
entry_id = root.find('.//entry_id')
if entry_id is not None:
print(f" Entry ID: {entry_id.text}")
return entry_id.text
except:
pass
return True
else:
print(f"❌ Entry creation failed: HTTP {response.status_code}")
print(f" Response: {response.content.decode('utf-8')[:200]}")
return None
except Exception as e:
print(f"❌ Error creating entry: {e}")
return None
def create_comment(client, uid, nbid, entry_id, comment):
"""Add a comment to an existing entry"""
print(f"\n💬 Adding comment to entry {entry_id}")
params = {
'uid': uid,
'nbid': nbid,
'entry_id': entry_id,
'comment': comment
}
try:
response = client.make_call('entries', 'create_comment', params=params)
if response.status_code == 200:
print("✅ Comment added successfully")
return True
else:
print(f"❌ Comment creation failed: HTTP {response.status_code}")
return False
except Exception as e:
print(f"❌ Error creating comment: {e}")
return False
def upload_attachment(client, config, uid, nbid, entry_id, file_path):
"""Upload a file attachment to an entry"""
import requests
file_path = Path(file_path)
if not file_path.exists():
print(f"❌ File not found: {file_path}")
return False
print(f"\n📎 Uploading attachment: {file_path.name}")
print(f" Size: {file_path.stat().st_size / 1024:.2f} KB")
url = f"{config['api_url']}/entries/upload_attachment"
try:
with open(file_path, 'rb') as f:
files = {'file': f}
data = {
'uid': uid,
'nbid': nbid,
'entry_id': entry_id,
'filename': file_path.name,
'access_key_id': config['access_key_id'],
'access_password': config['access_password']
}
response = requests.post(url, files=files, data=data)
if response.status_code == 200:
print("✅ Attachment uploaded successfully")
return True
else:
print(f"❌ Upload failed: HTTP {response.status_code}")
print(f" Response: {response.content.decode('utf-8')[:200]}")
return False
except Exception as e:
print(f"❌ Error uploading attachment: {e}")
return False
def batch_upload(client, config, uid, nbid, entry_id, directory):
"""Upload all files from a directory as attachments"""
directory = Path(directory)
if not directory.is_dir():
print(f"❌ Directory not found: {directory}")
return
files = list(directory.glob('*'))
files = [f for f in files if f.is_file()]
if not files:
print(f"❌ No files found in {directory}")
return
print(f"\n📦 Batch uploading {len(files)} files from {directory}")
successful = 0
failed = 0
for file_path in files:
if upload_attachment(client, config, uid, nbid, entry_id, file_path):
successful += 1
else:
failed += 1
print("\n" + "="*60)
print(f"Batch upload complete: {successful} successful, {failed} failed")
print("="*60)
def create_entry_with_attachments(client, config, uid, nbid, title, content,
attachments):
"""Create entry and upload multiple attachments"""
# Create entry
entry_id = create_entry(client, uid, nbid, title, content)
if not entry_id:
print("❌ Cannot upload attachments without entry ID")
return False
# Upload attachments
for attachment_path in attachments:
upload_attachment(client, config, uid, nbid, entry_id, attachment_path)
return True
def main():
"""Main command-line interface"""
parser = argparse.ArgumentParser(
description='LabArchives Entry Operations',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Create simple entry
python3 entry_operations.py create --nbid 12345 --title "Experiment Results"
# Create entry with content
python3 entry_operations.py create --nbid 12345 --title "Results" \\
--content "PCR amplification successful"
# Create entry with HTML content
python3 entry_operations.py create --nbid 12345 --title "Results" \\
--content "<p>Results:</p><ul><li>Sample A: Positive</li></ul>"
# Upload attachment to existing entry
python3 entry_operations.py upload --nbid 12345 --entry-id 67890 \\
--file data.csv
# Batch upload multiple files
python3 entry_operations.py batch-upload --nbid 12345 --entry-id 67890 \\
--directory ./experiment_data/
# Add comment to entry
python3 entry_operations.py comment --nbid 12345 --entry-id 67890 \\
--text "Follow-up analysis needed"
"""
)
parser.add_argument('--config', default='config.yaml',
help='Path to configuration file (default: config.yaml)')
parser.add_argument('--nbid', required=True,
help='Notebook ID')
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
# Create entry command
create_parser = subparsers.add_parser('create', help='Create new entry')
create_parser.add_argument('--title', required=True, help='Entry title')
create_parser.add_argument('--content', help='Entry content (HTML supported)')
create_parser.add_argument('--date', help='Entry date (YYYY-MM-DD)')
create_parser.add_argument('--attachments', nargs='+',
help='Files to attach to the new entry')
# Upload attachment command
upload_parser = subparsers.add_parser('upload', help='Upload attachment to entry')
upload_parser.add_argument('--entry-id', required=True, help='Entry ID')
upload_parser.add_argument('--file', required=True, help='File to upload')
# Batch upload command
batch_parser = subparsers.add_parser('batch-upload',
help='Upload all files from directory')
batch_parser.add_argument('--entry-id', required=True, help='Entry ID')
batch_parser.add_argument('--directory', required=True,
help='Directory containing files to upload')
# Comment command
comment_parser = subparsers.add_parser('comment', help='Add comment to entry')
comment_parser.add_argument('--entry-id', required=True, help='Entry ID')
comment_parser.add_argument('--text', required=True, help='Comment text')
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
# Load configuration and initialize
config = load_config(args.config)
client = init_client(config)
uid = get_user_id(client, config)
# Execute command
if args.command == 'create':
if args.attachments:
create_entry_with_attachments(
client, config, uid, args.nbid, args.title,
args.content, args.attachments
)
else:
create_entry(client, uid, args.nbid, args.title,
args.content, args.date)
elif args.command == 'upload':
upload_attachment(client, config, uid, args.nbid,
args.entry_id, args.file)
elif args.command == 'batch-upload':
batch_upload(client, config, uid, args.nbid,
args.entry_id, args.directory)
elif args.command == 'comment':
create_comment(client, uid, args.nbid, args.entry_id, args.text)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,269 @@
#!/usr/bin/env python3
"""
LabArchives Notebook Operations
Utilities for listing, backing up, and managing LabArchives notebooks.
"""
import argparse
import sys
import yaml
from datetime import datetime
from pathlib import Path
def load_config(config_path='config.yaml'):
"""Load configuration from YAML file"""
try:
with open(config_path, 'r') as f:
return yaml.safe_load(f)
except FileNotFoundError:
print(f"❌ Configuration file not found: {config_path}")
print(" Run setup_config.py first to create configuration")
sys.exit(1)
except Exception as e:
print(f"❌ Error loading configuration: {e}")
sys.exit(1)
def init_client(config):
"""Initialize LabArchives API client"""
try:
from labarchivespy.client import Client
return Client(
config['api_url'],
config['access_key_id'],
config['access_password']
)
except ImportError:
print("❌ labarchives-py package not installed")
print(" Install with: pip install git+https://github.com/mcmero/labarchives-py")
sys.exit(1)
def get_user_id(client, config):
"""Get user ID via authentication"""
import xml.etree.ElementTree as ET
login_params = {
'login_or_email': config['user_email'],
'password': config['user_external_password']
}
try:
response = client.make_call('users', 'user_access_info', params=login_params)
if response.status_code == 200:
uid = ET.fromstring(response.content)[0].text
return uid
else:
print(f"❌ Authentication failed: HTTP {response.status_code}")
print(f" Response: {response.content.decode('utf-8')[:200]}")
sys.exit(1)
except Exception as e:
print(f"❌ Error during authentication: {e}")
sys.exit(1)
def list_notebooks(client, uid):
"""List all accessible notebooks for a user"""
import xml.etree.ElementTree as ET
print(f"\n📚 Listing notebooks for user ID: {uid}\n")
# Get user access info which includes notebook list
login_params = {'uid': uid}
try:
response = client.make_call('users', 'user_access_info', params=login_params)
if response.status_code == 200:
root = ET.fromstring(response.content)
notebooks = root.findall('.//notebook')
if not notebooks:
print("No notebooks found")
return []
notebook_list = []
print(f"{'Notebook ID':<15} {'Name':<40} {'Role':<10}")
print("-" * 70)
for nb in notebooks:
nbid = nb.find('nbid').text if nb.find('nbid') is not None else 'N/A'
name = nb.find('name').text if nb.find('name') is not None else 'Unnamed'
role = nb.find('role').text if nb.find('role') is not None else 'N/A'
notebook_list.append({'nbid': nbid, 'name': name, 'role': role})
print(f"{nbid:<15} {name:<40} {role:<10}")
print(f"\nTotal notebooks: {len(notebooks)}")
return notebook_list
else:
print(f"❌ Failed to list notebooks: HTTP {response.status_code}")
return []
except Exception as e:
print(f"❌ Error listing notebooks: {e}")
return []
def backup_notebook(client, uid, nbid, output_dir='backups', json_format=False,
no_attachments=False):
"""Backup a notebook"""
print(f"\n💾 Backing up notebook {nbid}...")
# Create output directory
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
# Prepare parameters
params = {
'uid': uid,
'nbid': nbid,
'json': 'true' if json_format else 'false',
'no_attachments': 'true' if no_attachments else 'false'
}
try:
response = client.make_call('notebooks', 'notebook_backup', params=params)
if response.status_code == 200:
# Determine file extension
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
if no_attachments:
ext = 'json' if json_format else 'xml'
filename = f"notebook_{nbid}_{timestamp}.{ext}"
else:
filename = f"notebook_{nbid}_{timestamp}.7z"
output_file = output_path / filename
# Write to file
with open(output_file, 'wb') as f:
f.write(response.content)
file_size = output_file.stat().st_size / (1024 * 1024) # MB
print(f"✅ Backup saved: {output_file}")
print(f" File size: {file_size:.2f} MB")
return str(output_file)
else:
print(f"❌ Backup failed: HTTP {response.status_code}")
print(f" Response: {response.content.decode('utf-8')[:200]}")
return None
except Exception as e:
print(f"❌ Error during backup: {e}")
return None
def backup_all_notebooks(client, uid, output_dir='backups', json_format=False,
no_attachments=False):
"""Backup all accessible notebooks"""
print("\n📦 Backing up all notebooks...\n")
notebooks = list_notebooks(client, uid)
if not notebooks:
print("No notebooks to backup")
return
successful = 0
failed = 0
for nb in notebooks:
nbid = nb['nbid']
name = nb['name']
print(f"\n--- Backing up: {name} (ID: {nbid}) ---")
result = backup_notebook(client, uid, nbid, output_dir, json_format, no_attachments)
if result:
successful += 1
else:
failed += 1
print("\n" + "="*60)
print(f"Backup complete: {successful} successful, {failed} failed")
print("="*60)
def main():
"""Main command-line interface"""
parser = argparse.ArgumentParser(
description='LabArchives Notebook Operations',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# List all notebooks
python3 notebook_operations.py list
# Backup specific notebook
python3 notebook_operations.py backup --nbid 12345
# Backup all notebooks (JSON format, no attachments)
python3 notebook_operations.py backup-all --json --no-attachments
# Backup to custom directory
python3 notebook_operations.py backup --nbid 12345 --output my_backups/
"""
)
parser.add_argument('--config', default='config.yaml',
help='Path to configuration file (default: config.yaml)')
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
# List command
subparsers.add_parser('list', help='List all accessible notebooks')
# Backup command
backup_parser = subparsers.add_parser('backup', help='Backup a specific notebook')
backup_parser.add_argument('--nbid', required=True, help='Notebook ID to backup')
backup_parser.add_argument('--output', default='backups',
help='Output directory (default: backups)')
backup_parser.add_argument('--json', action='store_true',
help='Return data in JSON format instead of XML')
backup_parser.add_argument('--no-attachments', action='store_true',
help='Exclude attachments from backup')
# Backup all command
backup_all_parser = subparsers.add_parser('backup-all',
help='Backup all accessible notebooks')
backup_all_parser.add_argument('--output', default='backups',
help='Output directory (default: backups)')
backup_all_parser.add_argument('--json', action='store_true',
help='Return data in JSON format instead of XML')
backup_all_parser.add_argument('--no-attachments', action='store_true',
help='Exclude attachments from backup')
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
# Load configuration and initialize
config = load_config(args.config)
client = init_client(config)
uid = get_user_id(client, config)
# Execute command
if args.command == 'list':
list_notebooks(client, uid)
elif args.command == 'backup':
backup_notebook(client, uid, args.nbid, args.output, args.json, args.no_attachments)
elif args.command == 'backup-all':
backup_all_notebooks(client, uid, args.output, args.json, args.no_attachments)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""
LabArchives Configuration Setup Script
This script helps create a config.yaml file with necessary credentials
for LabArchives API access.
"""
import yaml
import os
from pathlib import Path
def get_regional_endpoint():
"""Prompt user to select regional API endpoint"""
print("\nSelect your regional API endpoint:")
print("1. US/International (mynotebook.labarchives.com)")
print("2. Australia (aunotebook.labarchives.com)")
print("3. UK (uknotebook.labarchives.com)")
print("4. Custom endpoint")
choice = input("\nEnter choice (1-4): ").strip()
endpoints = {
'1': 'https://api.labarchives.com/api',
'2': 'https://auapi.labarchives.com/api',
'3': 'https://ukapi.labarchives.com/api'
}
if choice in endpoints:
return endpoints[choice]
elif choice == '4':
return input("Enter custom API endpoint URL: ").strip()
else:
print("Invalid choice, defaulting to US/International")
return endpoints['1']
def get_credentials():
"""Prompt user for API credentials"""
print("\n" + "="*60)
print("LabArchives API Credentials")
print("="*60)
print("\nYou need two sets of credentials:")
print("1. Institutional API credentials (from LabArchives administrator)")
print("2. User authentication credentials (from your account settings)")
print()
# Institutional credentials
print("Institutional Credentials:")
access_key_id = input(" Access Key ID: ").strip()
access_password = input(" Access Password: ").strip()
# User credentials
print("\nUser Credentials:")
user_email = input(" Your LabArchives email: ").strip()
print("\nExternal Applications Password:")
print("(Set this in your LabArchives Account Settings → Security & Privacy)")
user_password = input(" External Applications Password: ").strip()
return {
'access_key_id': access_key_id,
'access_password': access_password,
'user_email': user_email,
'user_external_password': user_password
}
def create_config_file(config_data, output_path='config.yaml'):
"""Create YAML configuration file"""
with open(output_path, 'w') as f:
yaml.dump(config_data, f, default_flow_style=False, sort_keys=False)
# Set file permissions to user read/write only for security
os.chmod(output_path, 0o600)
print(f"\n✅ Configuration saved to: {os.path.abspath(output_path)}")
print(" File permissions set to 600 (user read/write only)")
def verify_config(config_path='config.yaml'):
"""Verify configuration file can be loaded"""
try:
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
required_keys = ['api_url', 'access_key_id', 'access_password',
'user_email', 'user_external_password']
missing = [key for key in required_keys if key not in config or not config[key]]
if missing:
print(f"\n⚠️ Warning: Missing required fields: {', '.join(missing)}")
return False
print("\n✅ Configuration file verified successfully")
return True
except Exception as e:
print(f"\n❌ Error verifying configuration: {e}")
return False
def test_authentication(config_path='config.yaml'):
"""Test authentication with LabArchives API"""
print("\nWould you like to test the connection? (requires labarchives-py package)")
test = input("Test connection? (y/n): ").strip().lower()
if test != 'y':
return
try:
# Try to import labarchives-py
from labarchivespy.client import Client
import xml.etree.ElementTree as ET
# Load config
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
# Initialize client
print("\nInitializing client...")
client = Client(
config['api_url'],
config['access_key_id'],
config['access_password']
)
# Test authentication
print("Testing authentication...")
login_params = {
'login_or_email': config['user_email'],
'password': config['user_external_password']
}
response = client.make_call('users', 'user_access_info', params=login_params)
if response.status_code == 200:
# Extract UID
uid = ET.fromstring(response.content)[0].text
print(f"\n✅ Authentication successful!")
print(f" User ID: {uid}")
# Get notebook count
root = ET.fromstring(response.content)
notebooks = root.findall('.//notebook')
print(f" Accessible notebooks: {len(notebooks)}")
else:
print(f"\n❌ Authentication failed: HTTP {response.status_code}")
print(f" Response: {response.content.decode('utf-8')[:200]}")
except ImportError:
print("\n⚠️ labarchives-py package not installed")
print(" Install with: pip install git+https://github.com/mcmero/labarchives-py")
except Exception as e:
print(f"\n❌ Connection test failed: {e}")
def main():
"""Main setup workflow"""
print("="*60)
print("LabArchives API Configuration Setup")
print("="*60)
# Check if config already exists
if os.path.exists('config.yaml'):
print("\n⚠️ config.yaml already exists")
overwrite = input("Overwrite existing configuration? (y/n): ").strip().lower()
if overwrite != 'y':
print("Setup cancelled")
return
# Get configuration
api_url = get_regional_endpoint()
credentials = get_credentials()
# Combine configuration
config_data = {
'api_url': api_url,
**credentials
}
# Create config file
create_config_file(config_data)
# Verify
verify_config()
# Test connection
test_authentication()
print("\n" + "="*60)
print("Setup complete!")
print("="*60)
print("\nNext steps:")
print("1. Add config.yaml to .gitignore if using version control")
print("2. Use notebook_operations.py to list and backup notebooks")
print("3. Use entry_operations.py to create entries and upload files")
print("\nFor more information, see references/authentication_guide.md")
if __name__ == '__main__':
main()