feat: Add api

This commit is contained in:
Ching L 2025-04-30 16:36:21 +08:00
parent 455803c02a
commit e0dd319239
2 changed files with 177 additions and 0 deletions

1
.gitignore vendored
View File

@ -168,3 +168,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
tasks.db

176
app.py Normal file
View File

@ -0,0 +1,176 @@
"""
Flask application providing task management and Google Calendar scheduling.
-------------------------------------------------------
Prerequisites:
Python 3.9+
pip install flask peewee google-api-python-client google-auth google-auth-httplib2 google-auth-oauthlib
Configuration:
Set environment variable API_KEY with the shared secret that clients must send in the
HTTP header `X-API-Key`.
Place your Google ServiceAccount JSON credentials next to this file and set
GOOGLE_CREDENTIALS_FILE (default 'credentials.json').
Set CALENDAR_ID to your target calendar (e.g. 'primary' or a specific ID).
Run with:
python app.py
"""
import os
import datetime as dt
from functools import wraps
from zoneinfo import ZoneInfo # Python 3.9+ standard timezone database
from flask import Flask, jsonify, request, abort
from peewee import (
Model, SqliteDatabase, AutoField, CharField, IntegerField, DateTimeField,
)
from google.oauth2 import service_account
from googleapiclient.discovery import build
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DATABASE_FILE = os.getenv("TASK_DB", "tasks.db")
API_KEY = os.getenv("API_KEY", "change-me")
CREDENTIALS_FILE = os.getenv("GOOGLE_CREDENTIALS_FILE", "credentials.json")
CALENDAR_ID = os.getenv("CALENDAR_ID", "primary")
SCOPES = ["https://www.googleapis.com/auth/calendar"]
TZ = ZoneInfo("Asia/Shanghai") # 使用北京时间
# ---------------------------------------------------------------------------
# Database setup
# ---------------------------------------------------------------------------
db = SqliteDatabase(DATABASE_FILE, check_same_thread=False)
class BaseModel(Model):
class Meta:
database = db
class Task(BaseModel):
id = AutoField()
name = CharField()
min_interval_days = IntegerField() # "最少 x 天执行一次"
last_schedule_id = CharField(null=True) # 上次日程 ID
last_execution_time = DateTimeField(null=True)
priority = IntegerField(default=0) # 优先级 (越大越高)
# Create table if it doesn't exist
with db:
db.create_tables([Task])
# ---------------------------------------------------------------------------
# Google Calendar client
# ---------------------------------------------------------------------------
creds = service_account.Credentials.from_service_account_file(
CREDENTIALS_FILE, scopes=SCOPES
)
calendar_service = build("calendar", "v3", credentials=creds, cache_discovery=False)
# ---------------------------------------------------------------------------
# Flask app + helpers
# ---------------------------------------------------------------------------
app = Flask(__name__)
def require_api_key(func):
"""Simple headerbased authentication decorator."""
@wraps(func)
def wrapper(*args, **kwargs):
if request.headers.get("X-API-Key") != API_KEY:
abort(401, description="Invalid API key")
return func(*args, **kwargs)
return wrapper
# ---------------------------------------------------------------------------
# Utilities
# ---------------------------------------------------------------------------
def _to_datetime(value):
"""Ensure value is a timezoneaware datetime."""
if value is None:
return None
if isinstance(value, dt.datetime):
return value if value.tzinfo else value.replace(tzinfo=TZ)
# Peewee on SQLite may return str parse ISO 8601
parsed = dt.datetime.fromisoformat(value)
return parsed if parsed.tzinfo else parsed.replace(tzinfo=TZ)
def compute_color(task: Task, now: dt.datetime) -> str:
"""Compute trafficlight color for a task based on last_execution_time."""
last_dt = _to_datetime(task.last_execution_time)
if last_dt is None:
return "green"
days_passed = (now - last_dt).total_seconds() / 86400
threshold = task.min_interval_days or 0
if days_passed <= (2 * threshold) / 3:
return "green"
elif days_passed <= threshold:
return "yellow"
return "red"
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.route("/tasks", methods=["GET"])
@require_api_key
def list_tasks():
now = dt.datetime.now(TZ)
tasks = (
Task.select().order_by(Task.priority.desc())
)
response = [
{
"id": t.id,
"name": t.name,
"color": compute_color(t, now),
}
for t in tasks
]
return jsonify(response)
@app.route("/tasks/<int:task_id>/schedule", methods=["POST"])
@require_api_key
def schedule_task(task_id: int):
task = Task.get_or_none(Task.id == task_id)
if not task:
abort(404, description="Task not found")
# Event time is now (Asia/Shanghai)
start_dt = dt.datetime.now(TZ).replace(microsecond=0)
start_time = start_dt.isoformat()
event_body = {
"summary": task.name,
"description": task.name,
"start": {"dateTime": start_time, "timeZone": "Asia/Shanghai"},
"end": {"dateTime": start_time, "timeZone": "Asia/Shanghai"},
}
event = (
calendar_service.events()
.insert(calendarId=CALENDAR_ID, body=event_body)
.execute()
)
# Update task with new schedule info
task.last_schedule_id = event["id"]
task.last_execution_time = start_dt
task.save()
return jsonify({
"success": True,
"event_id": event["id"],
"task_id": task.id,
}), 201
# ---------------------------------------------------------------------------
# Application entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.getenv("PORT", 5000)), debug=True)