Add fetch_evening_tasks.py script and shared notion_helpers module

Refactors weekly-template's Notion client setup into a shared helper
(with .env/dotenv support and full pagination), and adds a dedicated
evening-prep script so both commands can fall back to script-based
fetching instead of the MCP search/fetch pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
aj.siegel
2026-09-21 23:05:57 -04:00
parent 339f9b1034
commit 20d8c1aedc
8 changed files with 229 additions and 59 deletions

View File

@@ -1,4 +1,4 @@
# Weekly Planning Scripts
# Daily/Weekly Planning Scripts
## Setup
@@ -24,30 +24,38 @@ For each database (Projects, Tasks Tracker, Reading List):
2. Click "..." menu → "Connections"
3. Add your "Weekly Planning Script" integration
### 4. Set environment variable
### 4. Set your Notion token
Add to your `~/.zshrc` or `~/.bash_profile`:
Create a `.env` file in the project root:
```bash
export NOTION_TOKEN="secret_your_token_here"
NOTION_TOKEN="secret_your_token_here"
```
Then reload: `source ~/.zshrc`
(`.env` is gitignored, and the script loads it automatically via `python-dotenv`.)
## Usage
Run the script to fetch weekly planning data:
### `fetch_weekly_data.py`
```bash
python3 scripts/fetch_weekly_data.py
```
Or add it to your PATH and run directly:
```bash
./scripts/fetch_weekly_data.py
```
This outputs JSON with:
- Projects marked "Focus this week"
- Active, non-personal projects (candidates for the weekly focus selection)
- Tasks due within the next 7 days
- Reading list articles (To Read or In Progress)
The weekly-template skill automatically calls this script to generate your weekly note, reducing AI inference costs.
The `/weekly-template` command calls this script to generate your weekly note, reducing AI inference costs. If the script fails, the command falls back to the Notion MCP search/fetch pattern.
### `fetch_evening_tasks.py`
```bash
python3 scripts/fetch_evening_tasks.py
```
This outputs JSON with:
- All open tasks (not Done, not Blocked) — paginated, so it stays complete as the tracker grows
- Every task currently marked `Top Task = true` (any status), for unmarking stale ones
The `/evening-prep` command calls this script to build the day's Suggested Tasks and manage the `Top Task` checkbox, with the same MCP fallback if the script fails.
Both scripts share Notion client setup, database IDs, and pagination logic in `notion_helpers.py`.

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""
Fetch candidate tasks for evening planning.
Returns JSON with all open (non-Done, non-Blocked) tasks, for local ranking
by blocking status, "Suggested for today", and due date.
"""
import json
import sys
from datetime import datetime
from notion_helpers import notion, get_data_source_id, query_all, TASKS_DB
def get_evening_tasks():
"""Get all tasks that are not Done and not Blocked."""
pages = query_all(
get_data_source_id(TASKS_DB),
filter={
"and": [
{
"property": "Status",
"status": {
"does_not_equal": "Done"
}
},
{
"property": "Status",
"status": {
"does_not_equal": "Blocked"
}
}
]
},
sorts=[
{
"property": "Due date",
"direction": "ascending"
}
]
)
tasks = []
for page in pages:
props = page["properties"]
due_date = None
if props["Due date"]["date"]:
due_date = props["Due date"]["date"]["start"]
suggested_for_today = None
if props["Suggested for today"]["date"]:
suggested_for_today = props["Suggested for today"]["date"]["start"]
project_names = []
if props["Project"]["relation"]:
for rel in props["Project"]["relation"]:
try:
project = notion.pages.retrieve(page_id=rel["id"])
project_name = project["properties"]["Name"]["title"][0]["plain_text"]
project_names.append(project_name)
except:
pass
has_blocking = len(props["Blocking"]["relation"]) > 0 if props["Blocking"]["relation"] else False
tasks.append({
"id": page["id"],
"url": page["url"],
"name": props["Task name"]["title"][0]["plain_text"] if props["Task name"]["title"] else "",
"due_date": due_date,
"suggested_for_today": suggested_for_today,
"status": props["Status"]["status"]["name"] if props["Status"]["status"] else "",
"priority": props["Priority"]["select"]["name"] if props["Priority"]["select"] else "",
"effort_level": props["Effort level"]["select"]["name"] if props["Effort level"]["select"] else "",
"energy_type": props["Energy type"]["select"]["name"] if props["Energy type"]["select"] else "",
"task_type": [t["name"] for t in props["Task type"]["multi_select"]],
"project": project_names[0] if project_names else "",
"has_blocking": has_blocking,
"top_task": props["Top Task"]["checkbox"]
})
return tasks
def get_marked_top_tasks():
"""Get all tasks currently marked Top Task = true, regardless of status.
Queried separately (without the Done/Blocked exclusion) so a stale Top Task
left checked on a now-Done task is still found and can be unmarked.
"""
pages = query_all(
get_data_source_id(TASKS_DB),
filter={
"property": "Top Task",
"checkbox": {
"equals": True
}
}
)
return [
{
"id": page["id"],
"name": page["properties"]["Task name"]["title"][0]["plain_text"] if page["properties"]["Task name"]["title"] else "",
"status": page["properties"]["Status"]["status"]["name"] if page["properties"]["Status"]["status"] else ""
}
for page in pages
]
def main():
try:
data = {
"tasks": get_evening_tasks(),
"marked_top_tasks": get_marked_top_tasks(),
"generated_at": datetime.now().isoformat()
}
print(json.dumps(data, indent=2))
return 0
except Exception as e:
print(json.dumps({"error": str(e)}), file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -5,54 +5,42 @@ Returns JSON with projects, tasks, and reading suggestions.
"""
import json
import os
import sys
from datetime import datetime, timedelta
from notion_client import Client
# Initialize Notion client - will use NOTION_TOKEN env var
notion_token = os.getenv('NOTION_TOKEN')
if not notion_token:
print(json.dumps({"error": "NOTION_TOKEN environment variable not set. See scripts/README.md for setup instructions."}), file=sys.stderr)
sys.exit(1)
from notion_helpers import notion, get_data_source_id, query_all, PROJECTS_DB, TASKS_DB, READING_LIST_DB
notion = Client(auth=notion_token)
# Database IDs
PROJECTS_DB = "2c0abd6c450a8090aca3e0b2b0373c17"
TASKS_DB = "2c0abd6c450a805098d3cc0e7d3dfccf"
READING_LIST_DB = "2c4abd6c450a80cbae55c440dd9e2427"
def get_focus_projects():
"""Get projects with 'Focus this week' = true and Status = Active."""
results = notion.databases.query(
database_id=PROJECTS_DB,
def get_active_projects():
"""Get non-personal projects with Status = Active (candidates for weekly focus selection)."""
pages = query_all(
get_data_source_id(PROJECTS_DB),
filter={
"and": [
{
"property": "Focus this week",
"checkbox": {
"equals": True
}
},
{
"property": "Status",
"select": {
"equals": "Active"
}
},
{
"property": "Type",
"select": {
"does_not_equal": "Personal"
}
}
]
}
)
projects = []
for page in results["results"]:
for page in pages:
props = page["properties"]
projects.append({
"id": page["id"],
"url": page["url"],
"name": props["Name"]["title"][0]["plain_text"] if props["Name"]["title"] else "",
"weekly_goal": props["Weekly goal"]["rich_text"][0]["plain_text"] if props["Weekly goal"]["rich_text"] else "",
"focus_this_week": props["Focus this week"]["checkbox"],
"icon": page.get("icon", {}).get("emoji", "")
})
@@ -64,8 +52,8 @@ def get_weekly_tasks():
today = datetime.now().date()
week_end = today + timedelta(days=7)
results = notion.databases.query(
database_id=TASKS_DB,
pages = query_all(
get_data_source_id(TASKS_DB),
filter={
"and": [
{
@@ -91,7 +79,7 @@ def get_weekly_tasks():
)
tasks = []
for page in results["results"]:
for page in pages:
props = page["properties"]
# Get due date
@@ -131,8 +119,8 @@ def get_weekly_tasks():
def get_reading_suggestions():
"""Get articles with Status = 'To Read' or 'In Progress'."""
results = notion.databases.query(
database_id=READING_LIST_DB,
pages = query_all(
get_data_source_id(READING_LIST_DB),
filter={
"or": [
{
@@ -152,7 +140,7 @@ def get_reading_suggestions():
)
articles = []
for page in results["results"]:
for page in pages:
props = page["properties"]
articles.append({
"id": page["id"],
@@ -168,7 +156,7 @@ def get_reading_suggestions():
def main():
try:
data = {
"projects": get_focus_projects(),
"projects": get_active_projects(),
"tasks": get_weekly_tasks(),
"reading": get_reading_suggestions(),
"generated_at": datetime.now().isoformat()

46
scripts/notion_helpers.py Normal file
View File

@@ -0,0 +1,46 @@
"""Shared Notion client setup and query helpers for the FullFocus scripts."""
import json
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
from notion_client import Client
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
_notion_token = os.getenv('NOTION_TOKEN')
if not _notion_token:
print(json.dumps({"error": "NOTION_TOKEN environment variable not set. See scripts/README.md for setup instructions."}), file=sys.stderr)
sys.exit(1)
notion = Client(auth=_notion_token)
# Database IDs
PROJECTS_DB = "2c0abd6c450a8090aca3e0b2b0373c17"
TASKS_DB = "2c0abd6c450a805098d3cc0e7d3dfccf"
READING_LIST_DB = "2c4abd6c450a80cbae55c440dd9e2427"
_data_source_id_cache = {}
def get_data_source_id(database_id):
"""Resolve a database ID to its first data source ID (notion-client 3.x API)."""
if database_id not in _data_source_id_cache:
db = notion.databases.retrieve(database_id=database_id)
_data_source_id_cache[database_id] = db["data_sources"][0]["id"]
return _data_source_id_cache[database_id]
def query_all(data_source_id, **kwargs):
"""Query a data source, following pagination until all matching pages are collected."""
pages = []
start_cursor = None
while True:
if start_cursor:
kwargs["start_cursor"] = start_cursor
results = notion.data_sources.query(data_source_id=data_source_id, **kwargs)
pages.extend(results["results"])
if not results.get("has_more"):
break
start_cursor = results["next_cursor"]
return pages

View File

@@ -1 +1,2 @@
notion-client>=2.0.0
python-dotenv>=1.0.0