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.
- chronos_mcp-0.1.1/PKG-INFO +140 -0
- chronos_mcp-0.1.1/README.md +110 -0
- chronos_mcp-0.1.1/pyproject.toml +71 -0
- chronos_mcp-0.1.1/setup.cfg +4 -0
- chronos_mcp-0.1.1/src/chronos/__init__.py +4 -0
- chronos_mcp-0.1.1/src/chronos/__main__.py +12 -0
- chronos_mcp-0.1.1/src/chronos/api/__init__.py +1 -0
- chronos_mcp-0.1.1/src/chronos/api/app.py +49 -0
- chronos_mcp-0.1.1/src/chronos/api/deps.py +23 -0
- chronos_mcp-0.1.1/src/chronos/api/errors.py +49 -0
- chronos_mcp-0.1.1/src/chronos/api/routes/__init__.py +1 -0
- chronos_mcp-0.1.1/src/chronos/api/routes/accounts.py +36 -0
- chronos_mcp-0.1.1/src/chronos/api/routes/calendars.py +59 -0
- chronos_mcp-0.1.1/src/chronos/api/routes/events.py +403 -0
- chronos_mcp-0.1.1/src/chronos/api/routes/messages.py +161 -0
- chronos_mcp-0.1.1/src/chronos/api/routes/query.py +78 -0
- chronos_mcp-0.1.1/src/chronos/api/routes/sync.py +150 -0
- chronos_mcp-0.1.1/src/chronos/cli/__init__.py +1 -0
- chronos_mcp-0.1.1/src/chronos/cli/auth.py +403 -0
- chronos_mcp-0.1.1/src/chronos/cli/main.py +731 -0
- chronos_mcp-0.1.1/src/chronos/config.py +98 -0
- chronos_mcp-0.1.1/src/chronos/db/__init__.py +5 -0
- chronos_mcp-0.1.1/src/chronos/db/connection.py +36 -0
- chronos_mcp-0.1.1/src/chronos/db/migrations.py +23 -0
- chronos_mcp-0.1.1/src/chronos/db/schema.py +234 -0
- chronos_mcp-0.1.1/src/chronos/mcp/__init__.py +1 -0
- chronos_mcp-0.1.1/src/chronos/mcp/server.py +348 -0
- chronos_mcp-0.1.1/src/chronos/models/__init__.py +125 -0
- chronos_mcp-0.1.1/src/chronos/sync/__init__.py +1 -0
- chronos_mcp-0.1.1/src/chronos/sync/engine.py +161 -0
- chronos_mcp-0.1.1/src/chronos/sync/gcal.py +728 -0
- chronos_mcp-0.1.1/src/chronos/sync/gmail.py +627 -0
- chronos_mcp-0.1.1/src/chronos_mcp.egg-info/PKG-INFO +140 -0
- chronos_mcp-0.1.1/src/chronos_mcp.egg-info/SOURCES.txt +42 -0
- chronos_mcp-0.1.1/src/chronos_mcp.egg-info/dependency_links.txt +1 -0
- chronos_mcp-0.1.1/src/chronos_mcp.egg-info/entry_points.txt +2 -0
- chronos_mcp-0.1.1/src/chronos_mcp.egg-info/requires.txt +17 -0
- chronos_mcp-0.1.1/src/chronos_mcp.egg-info/top_level.txt +1 -0
- chronos_mcp-0.1.1/tests/test_api.py +371 -0
- chronos_mcp-0.1.1/tests/test_cli_progress.py +155 -0
- chronos_mcp-0.1.1/tests/test_cli_wipe.py +226 -0
- chronos_mcp-0.1.1/tests/test_credential_flow.py +232 -0
- chronos_mcp-0.1.1/tests/test_mcp.py +65 -0
- 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
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
[](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
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](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 @@
|
|
|
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)
|