chronos-mcp 0.1.1__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.
Files changed (44) hide show
  1. chronos_mcp-0.1.1/PKG-INFO +140 -0
  2. chronos_mcp-0.1.1/README.md +110 -0
  3. chronos_mcp-0.1.1/pyproject.toml +71 -0
  4. chronos_mcp-0.1.1/setup.cfg +4 -0
  5. chronos_mcp-0.1.1/src/chronos/__init__.py +4 -0
  6. chronos_mcp-0.1.1/src/chronos/__main__.py +12 -0
  7. chronos_mcp-0.1.1/src/chronos/api/__init__.py +1 -0
  8. chronos_mcp-0.1.1/src/chronos/api/app.py +49 -0
  9. chronos_mcp-0.1.1/src/chronos/api/deps.py +23 -0
  10. chronos_mcp-0.1.1/src/chronos/api/errors.py +49 -0
  11. chronos_mcp-0.1.1/src/chronos/api/routes/__init__.py +1 -0
  12. chronos_mcp-0.1.1/src/chronos/api/routes/accounts.py +36 -0
  13. chronos_mcp-0.1.1/src/chronos/api/routes/calendars.py +59 -0
  14. chronos_mcp-0.1.1/src/chronos/api/routes/events.py +403 -0
  15. chronos_mcp-0.1.1/src/chronos/api/routes/messages.py +161 -0
  16. chronos_mcp-0.1.1/src/chronos/api/routes/query.py +78 -0
  17. chronos_mcp-0.1.1/src/chronos/api/routes/sync.py +150 -0
  18. chronos_mcp-0.1.1/src/chronos/cli/__init__.py +1 -0
  19. chronos_mcp-0.1.1/src/chronos/cli/auth.py +403 -0
  20. chronos_mcp-0.1.1/src/chronos/cli/main.py +731 -0
  21. chronos_mcp-0.1.1/src/chronos/config.py +98 -0
  22. chronos_mcp-0.1.1/src/chronos/db/__init__.py +5 -0
  23. chronos_mcp-0.1.1/src/chronos/db/connection.py +36 -0
  24. chronos_mcp-0.1.1/src/chronos/db/migrations.py +23 -0
  25. chronos_mcp-0.1.1/src/chronos/db/schema.py +234 -0
  26. chronos_mcp-0.1.1/src/chronos/mcp/__init__.py +1 -0
  27. chronos_mcp-0.1.1/src/chronos/mcp/server.py +348 -0
  28. chronos_mcp-0.1.1/src/chronos/models/__init__.py +125 -0
  29. chronos_mcp-0.1.1/src/chronos/sync/__init__.py +1 -0
  30. chronos_mcp-0.1.1/src/chronos/sync/engine.py +161 -0
  31. chronos_mcp-0.1.1/src/chronos/sync/gcal.py +728 -0
  32. chronos_mcp-0.1.1/src/chronos/sync/gmail.py +627 -0
  33. chronos_mcp-0.1.1/src/chronos_mcp.egg-info/PKG-INFO +140 -0
  34. chronos_mcp-0.1.1/src/chronos_mcp.egg-info/SOURCES.txt +42 -0
  35. chronos_mcp-0.1.1/src/chronos_mcp.egg-info/dependency_links.txt +1 -0
  36. chronos_mcp-0.1.1/src/chronos_mcp.egg-info/entry_points.txt +2 -0
  37. chronos_mcp-0.1.1/src/chronos_mcp.egg-info/requires.txt +17 -0
  38. chronos_mcp-0.1.1/src/chronos_mcp.egg-info/top_level.txt +1 -0
  39. chronos_mcp-0.1.1/tests/test_api.py +371 -0
  40. chronos_mcp-0.1.1/tests/test_cli_progress.py +155 -0
  41. chronos_mcp-0.1.1/tests/test_cli_wipe.py +226 -0
  42. chronos_mcp-0.1.1/tests/test_credential_flow.py +232 -0
  43. chronos_mcp-0.1.1/tests/test_mcp.py +65 -0
  44. chronos_mcp-0.1.1/tests/test_schema.py +165 -0
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: chronos-mcp
3
+ Version: 0.1.1
4
+ Summary: Local-first email and calendar inbox for AI agents
5
+ Author: Chronos Contributors
6
+ License: MIT
7
+ Keywords: email,calendar,gmail,mcp,agent,inbox
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: fastapi>=0.111
15
+ Requires-Dist: uvicorn[standard]>=0.29
16
+ Requires-Dist: mcp[cli]>=1.0
17
+ Requires-Dist: google-api-python-client>=2.127
18
+ Requires-Dist: google-auth-oauthlib>=1.2
19
+ Requires-Dist: google-auth-httplib2>=0.2
20
+ Requires-Dist: python-ulid>=2.0
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: click>=8.1
23
+ Requires-Dist: rich>=13.0
24
+ Requires-Dist: aiofiles>=23.0
25
+ Requires-Dist: pyyaml>=6.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
30
+
31
+ # Chronos — agent-inbox
32
+
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
35
+
36
+ Local-first email and calendar inbox for AI agents. Syncs Gmail and Google Calendar
37
+ into a single queryable SQLite database and exposes all data through an MCP server.
38
+
39
+ ## Features
40
+
41
+ - Sync multiple Gmail and Google Calendar accounts into one SQLite database
42
+ - Full-text search via FTS5 (messages and events)
43
+ - MCP server at `localhost:7071/sse` for agent integration
44
+ - HTTP REST API at `localhost:7070` for direct queries
45
+ - Read-only SQL interface via `POST /v1/query`
46
+ - Optimistic event writes with provider-wins conflict resolution
47
+ - Incremental sync with historyId (Gmail) and syncToken (Calendar)
48
+ - ULID primary keys, WAL mode, no cloud dependencies at query time
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pipx install chronos-mcp
54
+ ```
55
+
56
+ Or from source:
57
+
58
+ ```bash
59
+ git clone https://github.com/CourtimusPrime/chronos.git
60
+ cd chronos
61
+ pip install -e .
62
+ ```
63
+
64
+ ## Setup
65
+
66
+ ### Prerequisites
67
+
68
+ 1. Go to [console.cloud.google.com](https://console.cloud.google.com)
69
+ 2. Create a project and enable **Gmail API** and **Google Calendar API**
70
+ 3. Create an OAuth client ID (Desktop app type)
71
+ 4. Download the credentials JSON file
72
+
73
+ ### Register an account
74
+
75
+ > Run `chronos --help` for full Google Cloud Console setup instructions.
76
+
77
+ ```bash
78
+ # Step 1: Stage your credentials (Desktop app OAuth JSON from Google Cloud Console)
79
+ chronos --use /path/to/credentials.json
80
+
81
+ # Step 2: Register an account (opens browser for OAuth2 consent)
82
+ chronos --add personal
83
+ ```
84
+
85
+ Staged credentials persist until the next `--use` call, so you can register
86
+ multiple accounts with one credentials file:
87
+
88
+ ```bash
89
+ chronos --use /path/to/credentials.json
90
+ chronos --add personal
91
+ chronos --add work
92
+ ```
93
+
94
+ The `--add` step:
95
+
96
+ - Writes `~/.chronos/personal_token.json` (self-contained token file)
97
+ - Creates two rows in the accounts table (gmail + google_calendar)
98
+ - Prints a confirmation summary
99
+
100
+ ### Start syncing
101
+
102
+ ```bash
103
+ chronos --start
104
+ ```
105
+
106
+ The daemon starts on `127.0.0.1:7070` (HTTP) and `127.0.0.1:7071` (MCP/SSE).
107
+
108
+ ## CLI Reference
109
+
110
+ ```
111
+ chronos --use CREDENTIALS_PATH # Stage a credentials JSON for --add
112
+ chronos --add ALIAS # Register a new account (requires prior --use)
113
+ chronos --remove ALIAS # Remove an account and its data
114
+ chronos --list # List all registered accounts
115
+ chronos --test ALIAS # Test account tokens
116
+ chronos --start [--http-port N] [--mcp-port N] # Start daemon
117
+ chronos --stop # Stop the running daemon (preserves synced data)
118
+ chronos --status # Show sync status
119
+ chronos --sync [ALIAS] [--type full|incremental] # Trigger sync
120
+ ```
121
+
122
+ > **Ctrl+C vs --stop:** Pressing Ctrl+C while `chronos --start` is running wipes
123
+ > synced data (emails, threads, events, calendars) but preserves accounts.
124
+ > Use `chronos --stop` from another terminal for a clean shutdown that preserves data.
125
+ >
126
+ > Run `chronos --help` for Google Cloud Console setup steps.
127
+
128
+ ## Environment Variables
129
+
130
+ | Variable | Default | Description |
131
+ | ------------------- | -------------------------- | ---------------------------------- |
132
+ | `CHRONOS_HOME` | `~/.chronos` | Credentials and database directory |
133
+ | `CHRONOS_DB_PATH` | `$CHRONOS_HOME/chronos.db` | SQLite database file |
134
+ | `CHRONOS_HTTP_PORT` | `7070` | HTTP API port |
135
+ | `CHRONOS_MCP_PORT` | `7071` | MCP server port |
136
+ | `CHRONOS_LOG_LEVEL` | `INFO` | Log level |
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,110 @@
1
+ # Chronos — agent-inbox
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
5
+
6
+ Local-first email and calendar inbox for AI agents. Syncs Gmail and Google Calendar
7
+ into a single queryable SQLite database and exposes all data through an MCP server.
8
+
9
+ ## Features
10
+
11
+ - Sync multiple Gmail and Google Calendar accounts into one SQLite database
12
+ - Full-text search via FTS5 (messages and events)
13
+ - MCP server at `localhost:7071/sse` for agent integration
14
+ - HTTP REST API at `localhost:7070` for direct queries
15
+ - Read-only SQL interface via `POST /v1/query`
16
+ - Optimistic event writes with provider-wins conflict resolution
17
+ - Incremental sync with historyId (Gmail) and syncToken (Calendar)
18
+ - ULID primary keys, WAL mode, no cloud dependencies at query time
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pipx install chronos-mcp
24
+ ```
25
+
26
+ Or from source:
27
+
28
+ ```bash
29
+ git clone https://github.com/CourtimusPrime/chronos.git
30
+ cd chronos
31
+ pip install -e .
32
+ ```
33
+
34
+ ## Setup
35
+
36
+ ### Prerequisites
37
+
38
+ 1. Go to [console.cloud.google.com](https://console.cloud.google.com)
39
+ 2. Create a project and enable **Gmail API** and **Google Calendar API**
40
+ 3. Create an OAuth client ID (Desktop app type)
41
+ 4. Download the credentials JSON file
42
+
43
+ ### Register an account
44
+
45
+ > Run `chronos --help` for full Google Cloud Console setup instructions.
46
+
47
+ ```bash
48
+ # Step 1: Stage your credentials (Desktop app OAuth JSON from Google Cloud Console)
49
+ chronos --use /path/to/credentials.json
50
+
51
+ # Step 2: Register an account (opens browser for OAuth2 consent)
52
+ chronos --add personal
53
+ ```
54
+
55
+ Staged credentials persist until the next `--use` call, so you can register
56
+ multiple accounts with one credentials file:
57
+
58
+ ```bash
59
+ chronos --use /path/to/credentials.json
60
+ chronos --add personal
61
+ chronos --add work
62
+ ```
63
+
64
+ The `--add` step:
65
+
66
+ - Writes `~/.chronos/personal_token.json` (self-contained token file)
67
+ - Creates two rows in the accounts table (gmail + google_calendar)
68
+ - Prints a confirmation summary
69
+
70
+ ### Start syncing
71
+
72
+ ```bash
73
+ chronos --start
74
+ ```
75
+
76
+ The daemon starts on `127.0.0.1:7070` (HTTP) and `127.0.0.1:7071` (MCP/SSE).
77
+
78
+ ## CLI Reference
79
+
80
+ ```
81
+ chronos --use CREDENTIALS_PATH # Stage a credentials JSON for --add
82
+ chronos --add ALIAS # Register a new account (requires prior --use)
83
+ chronos --remove ALIAS # Remove an account and its data
84
+ chronos --list # List all registered accounts
85
+ chronos --test ALIAS # Test account tokens
86
+ chronos --start [--http-port N] [--mcp-port N] # Start daemon
87
+ chronos --stop # Stop the running daemon (preserves synced data)
88
+ chronos --status # Show sync status
89
+ chronos --sync [ALIAS] [--type full|incremental] # Trigger sync
90
+ ```
91
+
92
+ > **Ctrl+C vs --stop:** Pressing Ctrl+C while `chronos --start` is running wipes
93
+ > synced data (emails, threads, events, calendars) but preserves accounts.
94
+ > Use `chronos --stop` from another terminal for a clean shutdown that preserves data.
95
+ >
96
+ > Run `chronos --help` for Google Cloud Console setup steps.
97
+
98
+ ## Environment Variables
99
+
100
+ | Variable | Default | Description |
101
+ | ------------------- | -------------------------- | ---------------------------------- |
102
+ | `CHRONOS_HOME` | `~/.chronos` | Credentials and database directory |
103
+ | `CHRONOS_DB_PATH` | `$CHRONOS_HOME/chronos.db` | SQLite database file |
104
+ | `CHRONOS_HTTP_PORT` | `7070` | HTTP API port |
105
+ | `CHRONOS_MCP_PORT` | `7071` | MCP server port |
106
+ | `CHRONOS_LOG_LEVEL` | `INFO` | Log level |
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,71 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chronos-mcp"
7
+ version = "0.1.1"
8
+ description = "Local-first email and calendar inbox for AI agents"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Chronos Contributors" }]
13
+ keywords = ["email", "calendar", "gmail", "mcp", "agent", "inbox"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ ]
20
+ dependencies = [
21
+ "fastapi>=0.111",
22
+ "uvicorn[standard]>=0.29",
23
+ "mcp[cli]>=1.0",
24
+ "google-api-python-client>=2.127",
25
+ "google-auth-oauthlib>=1.2",
26
+ "google-auth-httplib2>=0.2",
27
+ "python-ulid>=2.0",
28
+ "httpx>=0.27",
29
+ "click>=8.1",
30
+ "rich>=13.0",
31
+ "aiofiles>=23.0",
32
+ "pyyaml>=6.0",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=8.0",
38
+ "pytest-cov>=5.0",
39
+ "pytest-asyncio>=0.23",
40
+ ]
41
+
42
+ [project.scripts]
43
+ chronos = "chronos.__main__:main"
44
+
45
+ [tool.pytest.ini_options]
46
+ addopts = "--cov=chronos --cov-report=term-missing --cov-report=html --cov-report=xml"
47
+ testpaths = ["tests"]
48
+
49
+ [tool.coverage.run]
50
+ branch = true
51
+ source = ["src/chronos"]
52
+ omit = [
53
+ "*/__main__.py",
54
+ "tests/*",
55
+ ]
56
+
57
+ [tool.coverage.report]
58
+ exclude_lines = [
59
+ "pragma: no cover",
60
+ "raise NotImplementedError",
61
+ "if __name__ == .__main__.:",
62
+ "if TYPE_CHECKING:",
63
+ ]
64
+ show_missing = true
65
+ skip_covered = false
66
+
67
+ [tool.setuptools.packages.find]
68
+ where = ["src"]
69
+
70
+ [tool.setuptools.package-dir]
71
+ "" = "src"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """Chronos — agent-inbox: local-first email and calendar sync daemon."""
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["__version__"]
@@ -0,0 +1,12 @@
1
+ """Entry point: chronos CLI."""
2
+ import sys
3
+
4
+
5
+ def main():
6
+ """Main entry point for the chronos CLI."""
7
+ from chronos.cli.main import cli
8
+ cli()
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1 @@
1
+ """HTTP API package."""
@@ -0,0 +1,49 @@
1
+ """FastAPI app factory with envelope middleware."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+
6
+ from fastapi import FastAPI, Request
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import JSONResponse
9
+
10
+ from chronos.api.routes import accounts, calendars, events, messages, query, sync
11
+
12
+
13
+ def create_app(db_path: Optional[str] = None, sync_engine=None) -> FastAPI:
14
+ """Create and configure the FastAPI application."""
15
+ app = FastAPI(
16
+ title="Chronos HTTP API",
17
+ description="Local-first email and calendar inbox for AI agents",
18
+ version="0.1.0",
19
+ )
20
+
21
+ # Store state
22
+ app.state.db_path = db_path
23
+ app.state.sync_engine = sync_engine
24
+
25
+ # CORS (localhost only, no credentials)
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
+ # Register routers with v1 prefix
34
+ app.include_router(accounts.router, prefix="/v1")
35
+ app.include_router(messages.router, prefix="/v1")
36
+ app.include_router(calendars.router, prefix="/v1")
37
+ app.include_router(events.router, prefix="/v1")
38
+ app.include_router(sync.router, prefix="/v1")
39
+ app.include_router(query.router, prefix="/v1")
40
+
41
+ @app.get("/")
42
+ def root():
43
+ return {"service": "chronos", "version": "0.1.0"}
44
+
45
+ @app.get("/health")
46
+ def health():
47
+ return {"ok": True, "data": {"status": "healthy"}, "error": None}
48
+
49
+ return app
@@ -0,0 +1,23 @@
1
+ """DB dependency injection for FastAPI."""
2
+ from __future__ import annotations
3
+
4
+ import sqlite3
5
+ from typing import Generator
6
+
7
+ from fastapi import Request
8
+
9
+
10
+ def get_db(request: Request) -> Generator[sqlite3.Connection, None, None]:
11
+ """FastAPI dependency that provides a DB connection per request."""
12
+ db_path = request.app.state.db_path
13
+ from chronos.db.connection import get_connection
14
+ conn = get_connection(db_path)
15
+ try:
16
+ yield conn
17
+ finally:
18
+ conn.close()
19
+
20
+
21
+ def get_sync_engine(request: Request):
22
+ """FastAPI dependency that provides the SyncEngine."""
23
+ return getattr(request.app.state, "sync_engine", None)
@@ -0,0 +1,49 @@
1
+ """All error codes from PRD §12."""
2
+ from __future__ import annotations
3
+
4
+ from fastapi import HTTPException
5
+ from fastapi.responses import JSONResponse
6
+
7
+
8
+ def error_response(code: str, message: str, status_code: int) -> JSONResponse:
9
+ """Return a standard error envelope response."""
10
+ return JSONResponse(
11
+ status_code=status_code,
12
+ content={
13
+ "ok": False,
14
+ "data": None,
15
+ "error": {"code": code, "message": message},
16
+ },
17
+ )
18
+
19
+
20
+ def ok_response(data) -> dict:
21
+ """Return a standard success envelope."""
22
+ return {"ok": True, "data": data, "error": None}
23
+
24
+
25
+ def list_response(items: list, total: int, limit: int, offset: int) -> dict:
26
+ """Return a paginated list envelope."""
27
+ return ok_response({
28
+ "items": items,
29
+ "total": total,
30
+ "limit": limit,
31
+ "offset": offset,
32
+ "has_more": (offset + len(items)) < total,
33
+ })
34
+
35
+
36
+ # Error code constants
37
+ ACCOUNT_NOT_FOUND = "ACCOUNT_NOT_FOUND"
38
+ MESSAGE_NOT_FOUND = "MESSAGE_NOT_FOUND"
39
+ EVENT_NOT_FOUND = "EVENT_NOT_FOUND"
40
+ THREAD_NOT_FOUND = "THREAD_NOT_FOUND"
41
+ CALENDAR_NOT_FOUND = "CALENDAR_NOT_FOUND"
42
+ WRITE_NOT_ALLOWED = "WRITE_NOT_ALLOWED"
43
+ INVALID_SQL = "INVALID_SQL"
44
+ QUERY_RESULT_EMPTY = "QUERY_RESULT_EMPTY"
45
+ EVENT_READ_ONLY = "EVENT_READ_ONLY"
46
+ PENDING_CHANGE_EXISTS = "PENDING_CHANGE_EXISTS"
47
+ SYNC_NOT_RUNNING = "SYNC_NOT_RUNNING"
48
+ PROVIDER_ERROR = "PROVIDER_ERROR"
49
+ INVALID_BODY = "INVALID_BODY"
@@ -0,0 +1 @@
1
+ """API routes package."""
@@ -0,0 +1,36 @@
1
+ """GET /v1/accounts endpoint."""
2
+ from __future__ import annotations
3
+
4
+ import sqlite3
5
+ from typing import Annotated
6
+
7
+ from fastapi import APIRouter, Depends
8
+
9
+ from chronos.api.deps import get_db
10
+ from chronos.api.errors import list_response, ok_response
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ def _account_to_dict(row: sqlite3.Row) -> dict:
16
+ """Serialize account row (never include auth_data)."""
17
+ return {
18
+ "id": row["id"],
19
+ "provider": row["provider"],
20
+ "email": row["email"],
21
+ "display_name": row["display_name"],
22
+ "sync_enabled": bool(row["sync_enabled"]),
23
+ "last_synced_at": row["last_synced_at"],
24
+ "created_at": row["created_at"],
25
+ }
26
+
27
+
28
+ @router.get("/accounts")
29
+ def list_accounts(conn: Annotated[sqlite3.Connection, Depends(get_db)]):
30
+ """GET /v1/accounts — Returns all accounts."""
31
+ rows = conn.execute(
32
+ "SELECT * FROM accounts ORDER BY created_at"
33
+ ).fetchall()
34
+
35
+ items = [_account_to_dict(r) for r in rows]
36
+ return list_response(items, len(items), 50, 0)
@@ -0,0 +1,59 @@
1
+ """Calendars endpoint."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sqlite3
6
+ from typing import Annotated, Optional
7
+
8
+ from fastapi import APIRouter, Depends, Query
9
+
10
+ from chronos.api.deps import get_db
11
+ from chronos.api.errors import list_response
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ def _calendar_to_dict(row: sqlite3.Row) -> dict:
17
+ return {
18
+ "id": row["id"],
19
+ "account_id": row["account_id"],
20
+ "provider_calendar_id": row["provider_calendar_id"],
21
+ "name": row["name"],
22
+ "description": row["description"],
23
+ "color": row["color"],
24
+ "is_primary": bool(row["is_primary"]),
25
+ "is_read_only": bool(row["is_read_only"]),
26
+ "timezone": row["timezone"],
27
+ "created_at": row["created_at"],
28
+ }
29
+
30
+
31
+ @router.get("/calendars")
32
+ def list_calendars(
33
+ conn: Annotated[sqlite3.Connection, Depends(get_db)],
34
+ account_id: Optional[str] = None,
35
+ limit: int = Query(default=50, ge=1, le=500),
36
+ offset: int = Query(default=0, ge=0),
37
+ ):
38
+ """GET /v1/calendars — List calendars."""
39
+ conditions = []
40
+ params = []
41
+
42
+ if account_id:
43
+ conditions.append("account_id = ?")
44
+ params.append(account_id)
45
+
46
+ where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
47
+
48
+ count_row = conn.execute(
49
+ f"SELECT COUNT(*) FROM calendars {where_clause}", params
50
+ ).fetchone()
51
+ total = count_row[0]
52
+
53
+ rows = conn.execute(
54
+ f"SELECT * FROM calendars {where_clause} ORDER BY is_primary DESC, name LIMIT ? OFFSET ?",
55
+ params + [limit, offset],
56
+ ).fetchall()
57
+
58
+ items = [_calendar_to_dict(r) for r in rows]
59
+ return list_response(items, total, limit, offset)