godspeed app sync
This commit is contained in:
298
src/cli/sync.py
298
src/cli/sync.py
@@ -1,8 +1,11 @@
|
||||
import click
|
||||
import asyncio
|
||||
import os
|
||||
from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn
|
||||
|
||||
from src.utils.mail_utils.helpers import ensure_directory_exists
|
||||
from src.utils.calendar_utils import save_events_to_vdir, save_events_to_file
|
||||
@@ -21,6 +24,180 @@ from src.services.microsoft_graph.mail import (
|
||||
process_outbox_async,
|
||||
)
|
||||
from src.services.microsoft_graph.auth import get_access_token
|
||||
from src.services.godspeed.client import GodspeedClient
|
||||
from src.services.godspeed.sync import GodspeedSync
|
||||
|
||||
|
||||
# Timing state management
|
||||
def get_sync_state_file():
|
||||
"""Get the path to the sync state file."""
|
||||
return os.path.expanduser("~/.local/share/gtd-terminal-tools/sync_state.json")
|
||||
|
||||
|
||||
def load_sync_state():
|
||||
"""Load the sync state from file."""
|
||||
state_file = get_sync_state_file()
|
||||
if os.path.exists(state_file):
|
||||
try:
|
||||
with open(state_file, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"last_godspeed_sync": 0,
|
||||
"last_sweep_date": None,
|
||||
"sweep_completed_today": False,
|
||||
}
|
||||
|
||||
|
||||
def save_sync_state(state):
|
||||
"""Save the sync state to file."""
|
||||
state_file = get_sync_state_file()
|
||||
os.makedirs(os.path.dirname(state_file), exist_ok=True)
|
||||
with open(state_file, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
|
||||
def should_run_godspeed_sync():
|
||||
"""Check if Godspeed sync should run (every 15 minutes)."""
|
||||
state = load_sync_state()
|
||||
current_time = time.time()
|
||||
last_sync = state.get("last_godspeed_sync", 0)
|
||||
return current_time - last_sync >= 900 # 15 minutes in seconds
|
||||
|
||||
|
||||
def should_run_sweep():
|
||||
"""Check if sweep should run (once after 6pm each day)."""
|
||||
state = load_sync_state()
|
||||
current_time = datetime.now()
|
||||
|
||||
# Check if it's after 6 PM
|
||||
if current_time.hour < 18:
|
||||
return False
|
||||
|
||||
# Check if we've already swept today
|
||||
today_str = current_time.strftime("%Y-%m-%d")
|
||||
last_sweep_date = state.get("last_sweep_date")
|
||||
|
||||
return last_sweep_date != today_str
|
||||
|
||||
|
||||
def get_godspeed_sync_directory():
|
||||
"""Get Godspeed sync directory from environment or default."""
|
||||
sync_dir = os.getenv("GODSPEED_SYNC_DIR")
|
||||
if sync_dir:
|
||||
return Path(sync_dir)
|
||||
|
||||
# Default to ~/Documents/Godspeed or ~/.local/share/gtd-terminal-tools/godspeed
|
||||
home = Path.home()
|
||||
|
||||
# Try Documents first
|
||||
docs_dir = home / "Documents" / "Godspeed"
|
||||
if docs_dir.parent.exists():
|
||||
return docs_dir
|
||||
|
||||
# Fall back to data directory
|
||||
data_dir = home / ".local" / "share" / "gtd-terminal-tools" / "godspeed"
|
||||
return data_dir
|
||||
|
||||
|
||||
def get_godspeed_credentials():
|
||||
"""Get Godspeed credentials from environment."""
|
||||
email = os.getenv("GODSPEED_EMAIL")
|
||||
password = os.getenv("GODSPEED_PASSWORD")
|
||||
token = os.getenv("GODSPEED_TOKEN")
|
||||
return email, password, token
|
||||
|
||||
|
||||
async def run_godspeed_sync(progress=None):
|
||||
"""Run Godspeed bidirectional sync."""
|
||||
try:
|
||||
email, password, token = get_godspeed_credentials()
|
||||
if not (token or (email and password)):
|
||||
if progress:
|
||||
progress.console.print(
|
||||
"[yellow]⚠️ Skipping Godspeed sync: No credentials configured[/yellow]"
|
||||
)
|
||||
return False
|
||||
|
||||
sync_dir = get_godspeed_sync_directory()
|
||||
|
||||
if progress:
|
||||
progress.console.print(
|
||||
f"[cyan]🔄 Running Godspeed sync to {sync_dir}...[/cyan]"
|
||||
)
|
||||
|
||||
client = GodspeedClient(email=email, password=password, token=token)
|
||||
sync_engine = GodspeedSync(client, sync_dir)
|
||||
sync_engine.sync_bidirectional()
|
||||
|
||||
# Update sync state
|
||||
state = load_sync_state()
|
||||
state["last_godspeed_sync"] = time.time()
|
||||
save_sync_state(state)
|
||||
|
||||
if progress:
|
||||
progress.console.print("[green]✅ Godspeed sync completed[/green]")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
if progress:
|
||||
progress.console.print(f"[red]❌ Godspeed sync failed: {e}[/red]")
|
||||
return False
|
||||
|
||||
|
||||
async def run_task_sweep(progress=None):
|
||||
"""Run task sweep from notes directory to Godspeed inbox."""
|
||||
try:
|
||||
from src.cli.godspeed import TaskSweeper
|
||||
|
||||
notes_dir_env = os.getenv("NOTES_DIR")
|
||||
if not notes_dir_env:
|
||||
if progress:
|
||||
progress.console.print(
|
||||
"[yellow]⚠️ Skipping task sweep: $NOTES_DIR not configured[/yellow]"
|
||||
)
|
||||
return False
|
||||
|
||||
notes_dir = Path(notes_dir_env)
|
||||
if not notes_dir.exists():
|
||||
if progress:
|
||||
progress.console.print(
|
||||
f"[yellow]⚠️ Skipping task sweep: Notes directory does not exist: {notes_dir}[/yellow]"
|
||||
)
|
||||
return False
|
||||
|
||||
godspeed_dir = get_godspeed_sync_directory()
|
||||
|
||||
if progress:
|
||||
progress.console.print(
|
||||
f"[cyan]🧹 Running task sweep from {notes_dir} to {godspeed_dir}...[/cyan]"
|
||||
)
|
||||
|
||||
sweeper = TaskSweeper(notes_dir, godspeed_dir, dry_run=False)
|
||||
result = sweeper.sweep_tasks()
|
||||
|
||||
# Update sweep state
|
||||
state = load_sync_state()
|
||||
state["last_sweep_date"] = datetime.now().strftime("%Y-%m-%d")
|
||||
save_sync_state(state)
|
||||
|
||||
if result["swept_tasks"] > 0:
|
||||
if progress:
|
||||
progress.console.print(
|
||||
f"[green]✅ Task sweep completed: {result['swept_tasks']} tasks swept[/green]"
|
||||
)
|
||||
else:
|
||||
if progress:
|
||||
progress.console.print(
|
||||
"[green]✅ Task sweep completed: No tasks to sweep[/green]"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
if progress:
|
||||
progress.console.print(f"[red]❌ Task sweep failed: {e}[/red]")
|
||||
return False
|
||||
|
||||
|
||||
# Function to create Maildir structure
|
||||
@@ -362,6 +539,36 @@ async def _sync_outlook_data(
|
||||
notify_new_emails(new_message_count, org)
|
||||
|
||||
progress.console.print("[bold green]Step 2: New data fetched.[/bold green]")
|
||||
|
||||
# Stage 3: Run Godspeed operations based on timing
|
||||
progress.console.print(
|
||||
"\n[bold cyan]Step 3: Running Godspeed operations...[/bold cyan]"
|
||||
)
|
||||
|
||||
# Check if Godspeed sync should run (every 15 minutes)
|
||||
if should_run_godspeed_sync():
|
||||
await run_godspeed_sync(progress)
|
||||
else:
|
||||
progress.console.print("[dim]⏭️ Skipping Godspeed sync (not due yet)[/dim]")
|
||||
|
||||
# Check if task sweep should run (once after 6pm daily)
|
||||
if should_run_sweep():
|
||||
await run_task_sweep(progress)
|
||||
else:
|
||||
current_hour = datetime.now().hour
|
||||
if current_hour < 18:
|
||||
progress.console.print(
|
||||
"[dim]⏭️ Skipping task sweep (before 6 PM)[/dim]"
|
||||
)
|
||||
else:
|
||||
progress.console.print(
|
||||
"[dim]⏭️ Skipping task sweep (already completed today)[/dim]"
|
||||
)
|
||||
|
||||
progress.console.print(
|
||||
"[bold green]Step 3: Godspeed operations completed.[/bold green]"
|
||||
)
|
||||
|
||||
click.echo("Sync complete.")
|
||||
|
||||
|
||||
@@ -656,59 +863,43 @@ async def daemon_mode(
|
||||
pending_email_count = len(pending_emails)
|
||||
outbox_changes = pending_email_count > 0
|
||||
|
||||
# Check Godspeed operations
|
||||
godspeed_sync_due = should_run_godspeed_sync()
|
||||
sweep_due = should_run_sweep()
|
||||
|
||||
# Determine what changed and show appropriate status
|
||||
if mail_changes and calendar_changes and outbox_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Calendar: {calendar_change_desc} | Outbox: {pending_email_count} pending. Starting sync...",
|
||||
"yellow",
|
||||
changes_detected = (
|
||||
mail_changes
|
||||
or calendar_changes
|
||||
or outbox_changes
|
||||
or godspeed_sync_due
|
||||
or sweep_due
|
||||
)
|
||||
|
||||
if changes_detected:
|
||||
change_parts = []
|
||||
if mail_changes:
|
||||
change_parts.append(
|
||||
f"Mail: Remote {remote_message_count}, Local {local_message_count}"
|
||||
)
|
||||
)
|
||||
elif mail_changes and calendar_changes:
|
||||
if calendar_changes:
|
||||
change_parts.append(f"Calendar: {calendar_change_desc}")
|
||||
if outbox_changes:
|
||||
change_parts.append(f"Outbox: {pending_email_count} pending")
|
||||
if godspeed_sync_due:
|
||||
change_parts.append("Godspeed sync due")
|
||||
if sweep_due:
|
||||
change_parts.append("Task sweep due")
|
||||
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Calendar: {calendar_change_desc}. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif mail_changes and outbox_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Outbox: {pending_email_count} pending. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif calendar_changes and outbox_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Changes detected! Calendar: {calendar_change_desc} | Outbox: {pending_email_count} pending. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif mail_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"New messages detected! Remote: {remote_message_count}, Local: {local_message_count}. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif calendar_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Calendar changes detected! {calendar_change_desc}. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif outbox_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Outbound emails detected! {pending_email_count} emails pending. Starting sync...",
|
||||
f"Changes detected! {' | '.join(change_parts)}. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
|
||||
# Sync if any changes detected
|
||||
if mail_changes or calendar_changes or outbox_changes:
|
||||
if changes_detected:
|
||||
await _sync_outlook_data(
|
||||
dry_run,
|
||||
vdir,
|
||||
@@ -732,6 +923,23 @@ async def daemon_mode(
|
||||
|
||||
status_parts.append(f"Outbox: {pending_email_count} pending")
|
||||
|
||||
# Add Godspeed status
|
||||
state = load_sync_state()
|
||||
last_godspeed = state.get("last_godspeed_sync", 0)
|
||||
minutes_since_godspeed = int((time.time() - last_godspeed) / 60)
|
||||
status_parts.append(f"Godspeed: {minutes_since_godspeed}m ago")
|
||||
|
||||
last_sweep = state.get("last_sweep_date")
|
||||
if last_sweep == datetime.now().strftime("%Y-%m-%d"):
|
||||
status_parts.append("Sweep: done today")
|
||||
else:
|
||||
current_hour = datetime.now().hour
|
||||
if current_hour >= 18:
|
||||
status_parts.append("Sweep: due")
|
||||
else:
|
||||
hours_until_sweep = 18 - current_hour
|
||||
status_parts.append(f"Sweep: in {hours_until_sweep}h")
|
||||
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"No changes detected ({', '.join(status_parts)})",
|
||||
|
||||
Reference in New Issue
Block a user