remind-plugin-google-tasks 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ # Environment
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # Databases
7
+ *.db
8
+ *.db-wal
9
+ *.db-shm
10
+ *.sqlite
11
+ *.sqlite3
12
+ *.pgsql
13
+
14
+ # Python
15
+ __pycache__/
16
+ *.py[cod]
17
+ *$py.class
18
+ *.so
19
+ .Python
20
+ build/
21
+ develop-eggs/
22
+ dist/
23
+ downloads/
24
+ eggs/
25
+ .eggs/
26
+ lib/
27
+ lib64/
28
+ parts/
29
+ sdist/
30
+ var/
31
+ wheels/
32
+ *.egg-info/
33
+ .installed.cfg
34
+ *.egg
35
+
36
+ # Virtual environments
37
+ venv/
38
+ env/
39
+ ENV/
40
+ .venv
41
+
42
+ # IDE
43
+ .vscode/
44
+ .idea/
45
+ *.swp
46
+ *.swo
47
+ *~
48
+
49
+ # Testing
50
+ .pytest_cache/
51
+ .coverage
52
+ htmlcov/
53
+ .tox/
54
+
55
+ # OS
56
+ .DS_Store
57
+ Thumbs.db
58
+
59
+ # Build artifacts
60
+ dist/
61
+ build/
62
+ *.egg-info/
63
+
64
+ # Logs
65
+ *.log
66
+ logs/
67
+
68
+ # Node (future web app)
69
+ node_modules/
70
+ .next/
71
+ out/
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: remind-plugin-google-tasks
3
+ Version: 0.1.0
4
+ Summary: Google Tasks integration plugin for Remind CLI
5
+ Author-email: Hamza Plojovic <hello@hamzaplojovic.com>
6
+ License: MIT
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: google-api-python-client>=2.100.0
9
+ Requires-Dist: google-auth-httplib2>=0.2.0
10
+ Requires-Dist: google-auth-oauthlib>=1.2.0
11
+ Requires-Dist: remind-plugin-base>=0.1.0
12
+ Requires-Dist: typer[all]>=0.9.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
15
+ Requires-Dist: pytest-mock>=3.11.0; extra == 'dev'
16
+ Requires-Dist: pytest>=7.0; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # remind-plugin-google-tasks
20
+
21
+ Sync reminders from Remind CLI with Google Tasks.
22
+
23
+ When you create a reminder in Remind, it automatically appears in your Google Task list. Keep your tasks organized in one place.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ # Install the plugin
29
+ pip install remind-plugin-google-tasks
30
+
31
+ # Or with uv
32
+ uv pip install remind-plugin-google-tasks
33
+ ```
34
+
35
+ ## Setup
36
+
37
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com) and create a new project
38
+ 2. Enable **Google Tasks API** for your project
39
+ 3. Create an **OAuth 2.0 credential** (Desktop app type)
40
+ 4. Download the `client_secrets.json` file
41
+ 5. Set the path:
42
+
43
+ ```bash
44
+ export REMIND_GOOGLE_CLIENT_SECRETS_PATH=~/Downloads/client_secrets.json
45
+ ```
46
+
47
+ 6. Configure the plugin:
48
+
49
+ ```bash
50
+ remind plugins add google-tasks
51
+ ```
52
+
53
+ This will open your browser for OAuth authorization and let you select which Google Task list to sync with.
54
+
55
+ ## Usage
56
+
57
+ Simply use Remind as normal:
58
+
59
+ ```bash
60
+ remind add 'Buy groceries' --due 'tomorrow 5pm'
61
+ ```
62
+
63
+ The reminder is instantly synced to your Google Task list.
64
+
65
+ ## Status
66
+
67
+ Check plugin status anytime:
68
+
69
+ ```bash
70
+ remind plugins
71
+ remind doctor
72
+ ```
73
+
74
+ ## Uninstall
75
+
76
+ ```bash
77
+ remind plugins remove google-tasks
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,64 @@
1
+ # remind-plugin-google-tasks
2
+
3
+ Sync reminders from Remind CLI with Google Tasks.
4
+
5
+ When you create a reminder in Remind, it automatically appears in your Google Task list. Keep your tasks organized in one place.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # Install the plugin
11
+ pip install remind-plugin-google-tasks
12
+
13
+ # Or with uv
14
+ uv pip install remind-plugin-google-tasks
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com) and create a new project
20
+ 2. Enable **Google Tasks API** for your project
21
+ 3. Create an **OAuth 2.0 credential** (Desktop app type)
22
+ 4. Download the `client_secrets.json` file
23
+ 5. Set the path:
24
+
25
+ ```bash
26
+ export REMIND_GOOGLE_CLIENT_SECRETS_PATH=~/Downloads/client_secrets.json
27
+ ```
28
+
29
+ 6. Configure the plugin:
30
+
31
+ ```bash
32
+ remind plugins add google-tasks
33
+ ```
34
+
35
+ This will open your browser for OAuth authorization and let you select which Google Task list to sync with.
36
+
37
+ ## Usage
38
+
39
+ Simply use Remind as normal:
40
+
41
+ ```bash
42
+ remind add 'Buy groceries' --due 'tomorrow 5pm'
43
+ ```
44
+
45
+ The reminder is instantly synced to your Google Task list.
46
+
47
+ ## Status
48
+
49
+ Check plugin status anytime:
50
+
51
+ ```bash
52
+ remind plugins
53
+ remind doctor
54
+ ```
55
+
56
+ ## Uninstall
57
+
58
+ ```bash
59
+ remind plugins remove google-tasks
60
+ ```
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "remind-plugin-google-tasks"
7
+ version = "0.1.0"
8
+ description = "Google Tasks integration plugin for Remind CLI"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Hamza Plojovic", email = "hello@hamzaplojovic.com" }]
12
+ requires-python = ">=3.12"
13
+ dependencies = [
14
+ "remind-plugin-base>=0.1.0",
15
+ "typer[all]>=0.9.0",
16
+ "google-auth-oauthlib>=1.2.0",
17
+ "google-auth-httplib2>=0.2.0",
18
+ "google-api-python-client>=2.100.0",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "pytest>=7.0",
24
+ "pytest-asyncio>=0.21",
25
+ "pytest-mock>=3.11.0",
26
+ ]
27
+
28
+ [project.entry-points."remind.plugins"]
29
+ google-tasks = "remind_plugin_google_tasks:GoogleTasksPlugin"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/remind_plugin_google_tasks"]
33
+
34
+ [tool.uv.sources]
35
+ remind-plugin-base = { workspace = true }
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ addopts = "-v --tb=short"
40
+ asyncio_mode = "auto"
@@ -0,0 +1,7 @@
1
+ """Google Tasks plugin for Remind."""
2
+
3
+ from remind_plugin_google_tasks.plugin import GoogleTasksPlugin
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["GoogleTasksPlugin", "__version__"]
@@ -0,0 +1,17 @@
1
+ """Configuration models for Google Tasks plugin."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class GoogleTasksConfig(BaseModel):
7
+ """Google Tasks plugin configuration."""
8
+
9
+ task_list_id: str
10
+ """The ID of the Google Task list to sync with."""
11
+
12
+ task_list_name: str
13
+ """The human-readable name of the task list (e.g., 'Remind')."""
14
+
15
+ class Config:
16
+ """Pydantic config."""
17
+ json_file = "config.json"
@@ -0,0 +1,165 @@
1
+ """Google OAuth 2.0 flow for Google Tasks."""
2
+
3
+ import webbrowser
4
+ from http.server import BaseHTTPRequestHandler, HTTPServer
5
+ from pathlib import Path
6
+ from threading import Thread
7
+ from typing import Optional
8
+ from urllib.parse import parse_qs, urlparse
9
+
10
+ from google.auth.transport.requests import Request
11
+ from google.oauth2.credentials import Credentials
12
+ from google_auth_oauthlib.flow import InstalledAppFlow
13
+
14
+
15
+ class LocalhostServer:
16
+ """Simple HTTP server to capture OAuth callback on localhost:8080."""
17
+
18
+ def __init__(self, port: int = 8080) -> None:
19
+ """Initialize server.
20
+
21
+ Args:
22
+ port: Port to listen on (default 8080).
23
+ """
24
+ self.port = port
25
+ self.auth_code: Optional[str] = None
26
+ self.server: Optional[HTTPServer] = None
27
+
28
+ def start(self) -> None:
29
+ """Start the server in a background thread."""
30
+ self.server = HTTPServer(("localhost", self.port), self._make_handler())
31
+
32
+ thread = Thread(target=self.server.handle_request, daemon=True)
33
+ thread.start()
34
+
35
+ def _make_handler(self):
36
+ """Create a request handler that captures the auth code."""
37
+ server = self
38
+
39
+ class Handler(BaseHTTPRequestHandler):
40
+ def do_GET(self):
41
+ """Handle GET request (OAuth callback)."""
42
+ parsed = urlparse(self.path)
43
+ params = parse_qs(parsed.query)
44
+
45
+ if "code" in params:
46
+ server.auth_code = params["code"][0]
47
+ self.send_response(200)
48
+ self.send_header("Content-type", "text/html")
49
+ self.end_headers()
50
+ html = """
51
+ <html>
52
+ <head><title>Authorized</title></head>
53
+ <body>
54
+ <h1>Authorization Successful</h1>
55
+ <p>You can close this window and return to your terminal.</p>
56
+ </body>
57
+ </html>
58
+ """
59
+ self.wfile.write(html.encode())
60
+ else:
61
+ self.send_response(400)
62
+ self.end_headers()
63
+
64
+ def log_message(self, _format, *_args):
65
+ """Suppress server logs."""
66
+ pass
67
+
68
+ return Handler
69
+
70
+ def stop(self) -> None:
71
+ """Stop the server."""
72
+ if self.server:
73
+ self.server.server_close()
74
+
75
+
76
+ def get_google_credentials(
77
+ config_dir: Path, client_secrets_path: Optional[Path | str] = None
78
+ ) -> Credentials:
79
+ """Authenticate with Google and return credentials.
80
+
81
+ First tries to load existing credentials from config_dir.
82
+ If not found or expired, runs OAuth flow.
83
+
84
+ Args:
85
+ config_dir: Directory to store credentials (e.g., ~/.remind/plugins/google-tasks/).
86
+ client_secrets_path: Path to client_secrets.json from Google Cloud Console.
87
+ If None, looks for REMIND_GOOGLE_CLIENT_SECRETS_PATH env var.
88
+
89
+ Returns:
90
+ Valid Google OAuth credentials.
91
+
92
+ Raises:
93
+ Exception: If OAuth flow fails or credentials not found.
94
+ """
95
+ import os
96
+
97
+ credentials_file = config_dir / "credentials.json"
98
+
99
+ # Try to load existing credentials
100
+ if credentials_file.exists():
101
+ creds = Credentials.from_authorized_user_file(str(credentials_file))
102
+ if creds.valid:
103
+ return creds
104
+ if creds.expired and creds.refresh_token:
105
+ creds.refresh(Request())
106
+ _save_credentials(creds, credentials_file)
107
+ return creds
108
+
109
+ # No valid credentials, run OAuth flow
110
+ if client_secrets_path is None:
111
+ env_path = os.environ.get("REMIND_GOOGLE_CLIENT_SECRETS_PATH")
112
+ if not env_path:
113
+ raise Exception(
114
+ "Google OAuth client secrets not found. "
115
+ "Set REMIND_GOOGLE_CLIENT_SECRETS_PATH or provide path to client_secrets.json from Google Cloud Console."
116
+ )
117
+ client_secrets_path = Path(env_path).expanduser()
118
+ else:
119
+ client_secrets_path = Path(client_secrets_path).expanduser()
120
+
121
+ if not client_secrets_path.exists():
122
+ raise Exception(f"Client secrets file not found: {client_secrets_path}")
123
+
124
+ # Run OAuth flow
125
+ scopes = ["https://www.googleapis.com/auth/tasks"]
126
+ flow = InstalledAppFlow.from_client_secrets_file(str(client_secrets_path), scopes)
127
+
128
+ # Run local server to capture callback
129
+ server = LocalhostServer()
130
+ server.start()
131
+
132
+ # Open browser for user to authorize
133
+ auth_url, _ = flow.authorization_url()
134
+ webbrowser.open(auth_url)
135
+
136
+ # Wait for callback (max 10 minutes)
137
+ import time
138
+
139
+ for _ in range(600): # 10 minutes
140
+ if server.auth_code:
141
+ break
142
+ time.sleep(1)
143
+ else:
144
+ raise Exception("OAuth callback not received (timed out)")
145
+
146
+ server.stop()
147
+
148
+ # Exchange auth code for credentials
149
+ flow.fetch_token(code=server.auth_code)
150
+ creds = flow.credentials
151
+
152
+ # Save credentials
153
+ _save_credentials(creds, credentials_file)
154
+
155
+ return creds
156
+
157
+
158
+ def _save_credentials(creds: Credentials, path: Path) -> None:
159
+ """Save credentials to a JSON file.
160
+
161
+ Args:
162
+ creds: Google OAuth credentials.
163
+ path: Path to save credentials to.
164
+ """
165
+ path.write_text(creds.to_json())
@@ -0,0 +1,191 @@
1
+ """Google Tasks plugin for Remind."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from remind_plugin_base import BasePlugin, Reminder
8
+
9
+ from remind_plugin_google_tasks.config import GoogleTasksConfig
10
+ from remind_plugin_google_tasks.tasks_client import GoogleTasksClient
11
+
12
+
13
+ class GoogleTasksPlugin(BasePlugin):
14
+ """Sync Remind reminders with Google Tasks."""
15
+
16
+ name = "google-tasks"
17
+ display_name = "Google Tasks"
18
+ version = "0.1.0"
19
+
20
+ def setup(self, config_dir: Path) -> None:
21
+ """Interactive setup wizard for Google Tasks.
22
+
23
+ Prompts user for Google OAuth credentials and task list selection.
24
+
25
+ Args:
26
+ config_dir: Path to ~/.remind/plugins/google-tasks/
27
+
28
+ Raises:
29
+ Exception: If setup fails.
30
+ """
31
+ typer.echo("\n[bold cyan]Google Tasks Setup[/bold cyan]\n")
32
+
33
+ try:
34
+ # Get OAuth credentials
35
+ typer.echo("1. Authenticating with Google...")
36
+ client = GoogleTasksClient(config_dir)
37
+
38
+ # List task lists and let user pick one
39
+ typer.echo("2. Fetching your Google Task lists...")
40
+ task_lists = client.list_task_lists()
41
+
42
+ if not task_lists:
43
+ typer.echo(
44
+ "No task lists found. Creating a new one called 'Remind'...\n"
45
+ )
46
+ task_list_id = client.create_task_list("Remind")
47
+ task_list_name = "Remind"
48
+ else:
49
+ typer.echo("\nAvailable task lists:")
50
+ for i, tl in enumerate(task_lists, 1):
51
+ typer.echo(f" {i}. {tl['title']}")
52
+
53
+ choice = typer.prompt(
54
+ "\nSelect a task list (number) or enter a name to create a new one",
55
+ type=str,
56
+ )
57
+
58
+ if choice.isdigit() and 1 <= int(choice) <= len(task_lists):
59
+ selected = task_lists[int(choice) - 1]
60
+ task_list_id = selected["id"]
61
+ task_list_name = selected["title"]
62
+ else:
63
+ # Create new task list
64
+ task_list_name = choice
65
+ typer.echo(f"\nCreating new task list '{task_list_name}'...")
66
+ task_list_id = client.create_task_list(task_list_name)
67
+
68
+ # Save config
69
+ config = GoogleTasksConfig(
70
+ task_list_id=task_list_id, task_list_name=task_list_name
71
+ )
72
+ config_file = config_dir / "config.json"
73
+ config_file.write_text(config.model_dump_json(indent=2))
74
+
75
+ # Create test task
76
+ typer.echo(f"\nCreating test task in '{task_list_name}'...")
77
+ client.insert_task(
78
+ task_list_id,
79
+ "✓ Remind CLI is connected to Google Tasks!",
80
+ "This task was created by the Remind CLI integration.",
81
+ )
82
+
83
+ typer.echo("\n[bold green]✓ Setup complete![/bold green]\n")
84
+
85
+ except Exception as e:
86
+ typer.echo(f"\n[bold red]✗ Setup failed: {e}[/bold red]\n")
87
+ raise
88
+
89
+ def teardown(self, config_dir: Path) -> None:
90
+ """Remove plugin credentials and configuration.
91
+
92
+ Args:
93
+ config_dir: Path to ~/.remind/plugins/google-tasks/
94
+
95
+ Raises:
96
+ Exception: If teardown fails.
97
+ """
98
+ try:
99
+ # Delete credentials file
100
+ credentials_file = config_dir / "credentials.json"
101
+ if credentials_file.exists():
102
+ credentials_file.unlink()
103
+
104
+ # Delete config file
105
+ config_file = config_dir / "config.json"
106
+ if config_file.exists():
107
+ config_file.unlink()
108
+
109
+ # Remove the plugin directory
110
+ if config_dir.exists():
111
+ config_dir.rmdir()
112
+
113
+ except Exception as e:
114
+ raise Exception(f"Failed to remove plugin configuration: {e}") from e
115
+
116
+ def is_configured(self, config_dir: Path) -> bool:
117
+ """Check if plugin is properly configured.
118
+
119
+ Args:
120
+ config_dir: Path to ~/.remind/plugins/google-tasks/
121
+
122
+ Returns:
123
+ True if both credentials and config exist.
124
+ """
125
+ credentials_file = config_dir / "credentials.json"
126
+ config_file = config_dir / "config.json"
127
+ return credentials_file.exists() and config_file.exists()
128
+
129
+ def status(self, config_dir: Path) -> str:
130
+ """Get status for doctor command.
131
+
132
+ Args:
133
+ config_dir: Path to ~/.remind/plugins/google-tasks/
134
+
135
+ Returns:
136
+ Status string.
137
+ """
138
+ if not self.is_configured(config_dir):
139
+ return "Not configured"
140
+
141
+ try:
142
+ config_file = config_dir / "config.json"
143
+ config_data = json.loads(config_file.read_text())
144
+ task_list_name = config_data.get("task_list_name", "Unknown")
145
+ return f"Authenticated, syncing to '{task_list_name}'"
146
+ except Exception:
147
+ return "Configured but error reading status"
148
+
149
+ def on_reminder_created(self, reminder: Reminder, config_dir: Path) -> None:
150
+ """Create a task in Google Tasks when a reminder is created.
151
+
152
+ Args:
153
+ reminder: The newly created reminder.
154
+ config_dir: Path to ~/.remind/plugins/google-tasks/
155
+ """
156
+ try:
157
+ config_file = config_dir / "config.json"
158
+ config_data = json.loads(config_file.read_text())
159
+ task_list_id = config_data["task_list_id"]
160
+
161
+ # Format task note with due date
162
+ notes = None
163
+ if reminder.due_date:
164
+ notes = f"Due: {reminder.due_date}"
165
+
166
+ client = GoogleTasksClient(config_dir)
167
+ client.insert_task(task_list_id, reminder.text, notes)
168
+
169
+ except Exception as e:
170
+ print(f"Error syncing reminder to Google Tasks: {e}")
171
+
172
+ def on_reminder_due(self, _reminder: Reminder, _config_dir: Path) -> None:
173
+ """Optionally send a notification when a reminder is due.
174
+
175
+ Args:
176
+ _reminder: The reminder that is due.
177
+ _config_dir: Path to ~/.remind/plugins/google-tasks/
178
+ """
179
+ # For now, just a no-op. Could integrate with Google Calendar or send emails.
180
+ pass
181
+
182
+ def on_reminder_done(self, _reminder: Reminder, _config_dir: Path) -> None:
183
+ """Mark task as done in Google Tasks when marked done in Remind.
184
+
185
+ Args:
186
+ _reminder: The reminder that was completed.
187
+ _config_dir: Path to ~/.remind/plugins/google-tasks/
188
+ """
189
+ # This would require tracking which Remind reminder ID maps to which Google Task ID.
190
+ # For now, this is a placeholder. Full implementation would store a mapping file.
191
+ pass
@@ -0,0 +1,97 @@
1
+ """Google Tasks API client."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from googleapiclient.discovery import build
7
+
8
+ from remind_plugin_google_tasks.oauth import get_google_credentials
9
+
10
+
11
+ class GoogleTasksClient:
12
+ """Client for interacting with Google Tasks API."""
13
+
14
+ def __init__(self, config_dir: Path, client_secrets_path: Optional[Path] = None) -> None:
15
+ """Initialize client.
16
+
17
+ Args:
18
+ config_dir: Directory with credentials (e.g., ~/.remind/plugins/google-tasks/).
19
+ client_secrets_path: Path to client_secrets.json from Google Cloud Console.
20
+ """
21
+ self.config_dir = config_dir
22
+ self.client_secrets_path = client_secrets_path
23
+ self._service = None
24
+
25
+ def _get_service(self):
26
+ """Lazily initialize the Google Tasks service."""
27
+ if self._service is None:
28
+ creds = get_google_credentials(self.config_dir, self.client_secrets_path)
29
+ self._service = build("tasks", "v1", credentials=creds)
30
+ return self._service
31
+
32
+ def list_task_lists(self) -> list[dict]:
33
+ """Get all task lists.
34
+
35
+ Returns:
36
+ List of task list dicts with 'id', 'title' keys.
37
+ """
38
+ service = self._get_service()
39
+ results = service.tasklists().list(maxResults=10).execute()
40
+ return results.get("items", [])
41
+
42
+ def create_task_list(self, title: str) -> str:
43
+ """Create a new task list.
44
+
45
+ Args:
46
+ title: Name of the task list.
47
+
48
+ Returns:
49
+ ID of the created task list.
50
+ """
51
+ service = self._get_service()
52
+ task_list = {"title": title}
53
+ result = service.tasklists().insert(body=task_list).execute()
54
+ return result["id"]
55
+
56
+ def insert_task(self, task_list_id: str, title: str, notes: Optional[str] = None) -> str:
57
+ """Insert a new task.
58
+
59
+ Args:
60
+ task_list_id: ID of the task list.
61
+ title: Task title.
62
+ notes: Optional task notes/description.
63
+
64
+ Returns:
65
+ ID of the created task.
66
+ """
67
+ service = self._get_service()
68
+ task = {"title": title}
69
+ if notes:
70
+ task["notes"] = notes
71
+
72
+ result = service.tasks().insert(tasklist=task_list_id, body=task).execute()
73
+ return result["id"]
74
+
75
+ def update_task(
76
+ self, task_list_id: str, task_id: str, completed: bool = False
77
+ ) -> None:
78
+ """Update a task's status.
79
+
80
+ Args:
81
+ task_list_id: ID of the task list.
82
+ task_id: ID of the task to update.
83
+ completed: Whether the task is completed.
84
+ """
85
+ service = self._get_service()
86
+ task = {"id": task_id, "status": "completed" if completed else "needsAction"}
87
+ service.tasks().update(tasklist=task_list_id, task=task_id, body=task).execute()
88
+
89
+ def delete_task(self, task_list_id: str, task_id: str) -> None:
90
+ """Delete a task.
91
+
92
+ Args:
93
+ task_list_id: ID of the task list.
94
+ task_id: ID of the task to delete.
95
+ """
96
+ service = self._get_service()
97
+ service.tasks().delete(tasklist=task_list_id, task=task_id).execute()
@@ -0,0 +1,104 @@
1
+ """Tests for Google Tasks plugin."""
2
+
3
+ import json
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+ from remind_plugin_google_tasks import GoogleTasksPlugin
9
+ from remind_plugin_base import Reminder
10
+
11
+
12
+ @pytest.fixture
13
+ def plugin():
14
+ """Provide a plugin instance."""
15
+ return GoogleTasksPlugin()
16
+
17
+
18
+ @pytest.fixture
19
+ def config_dir(tmp_path):
20
+ """Provide a temporary config directory."""
21
+ return tmp_path
22
+
23
+
24
+ def test_plugin_metadata():
25
+ """Test plugin metadata."""
26
+ plugin = GoogleTasksPlugin()
27
+ assert plugin.name == "google-tasks"
28
+ assert plugin.display_name == "Google Tasks"
29
+ assert plugin.version == "0.1.0"
30
+
31
+
32
+ def test_is_configured_false_when_no_files(plugin, config_dir):
33
+ """Test is_configured returns False when credentials don't exist."""
34
+ assert not plugin.is_configured(config_dir)
35
+
36
+
37
+ def test_is_configured_true_when_files_exist(plugin, config_dir):
38
+ """Test is_configured returns True when credentials and config exist."""
39
+ (config_dir / "credentials.json").write_text("{}")
40
+ (config_dir / "config.json").write_text("{}")
41
+ assert plugin.is_configured(config_dir)
42
+
43
+
44
+ def test_status_not_configured(plugin, config_dir):
45
+ """Test status when not configured."""
46
+ status = plugin.status(config_dir)
47
+ assert status == "Not configured"
48
+
49
+
50
+ def test_status_configured(plugin, config_dir):
51
+ """Test status when configured."""
52
+ (config_dir / "credentials.json").write_text("{}")
53
+ config = {"task_list_id": "test-list", "task_list_name": "My Tasks"}
54
+ (config_dir / "config.json").write_text(json.dumps(config))
55
+
56
+ status = plugin.status(config_dir)
57
+ assert "My Tasks" in status
58
+ assert "Authenticated" in status
59
+
60
+
61
+ def test_teardown_removes_files(plugin, config_dir):
62
+ """Test teardown removes config files."""
63
+ (config_dir / "credentials.json").write_text("{}")
64
+ (config_dir / "config.json").write_text("{}")
65
+
66
+ plugin.teardown(config_dir)
67
+
68
+ assert not (config_dir / "credentials.json").exists()
69
+ assert not (config_dir / "config.json").exists()
70
+
71
+
72
+ def test_on_reminder_created_creates_task(plugin, config_dir):
73
+ """Test on_reminder_created syncs to Google Tasks."""
74
+ config = {"task_list_id": "test-list", "task_list_name": "My Tasks"}
75
+ (config_dir / "config.json").write_text(json.dumps(config))
76
+ (config_dir / "credentials.json").write_text("{}")
77
+
78
+ reminder = Reminder(
79
+ id=1,
80
+ text="Buy groceries",
81
+ due_date="2025-02-18T17:00:00",
82
+ is_done=False,
83
+ )
84
+
85
+ with patch("remind_plugin_google_tasks.plugin.GoogleTasksClient") as mock_client:
86
+ mock_instance = MagicMock()
87
+ mock_client.return_value = mock_instance
88
+
89
+ plugin.on_reminder_created(reminder, config_dir)
90
+
91
+ mock_instance.insert_task.assert_called_once()
92
+ call_args = mock_instance.insert_task.call_args
93
+ assert call_args[0][0] == "test-list"
94
+ assert call_args[0][1] == "Buy groceries"
95
+ assert "2025-02-18T17:00:00" in call_args[0][2]
96
+
97
+
98
+ def test_on_reminder_hooks_dont_error(plugin, config_dir):
99
+ """Test reminder hooks don't raise errors."""
100
+ reminder = Reminder(id=1, text="Test", is_done=False)
101
+
102
+ # Should not raise
103
+ plugin.on_reminder_due(reminder, config_dir)
104
+ plugin.on_reminder_done(reminder, config_dir)