mcp-code-indexer 3.1.4__py3-none-any.whl → 3.1.6__py3-none-any.whl
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.
- mcp_code_indexer/__init__.py +8 -6
- mcp_code_indexer/ask_handler.py +105 -75
- mcp_code_indexer/claude_api_handler.py +125 -82
- mcp_code_indexer/cleanup_manager.py +107 -81
- mcp_code_indexer/database/connection_health.py +230 -161
- mcp_code_indexer/database/database.py +529 -415
- mcp_code_indexer/database/exceptions.py +167 -118
- mcp_code_indexer/database/models.py +54 -19
- mcp_code_indexer/database/retry_executor.py +139 -103
- mcp_code_indexer/deepask_handler.py +178 -140
- mcp_code_indexer/error_handler.py +88 -76
- mcp_code_indexer/file_scanner.py +163 -141
- mcp_code_indexer/git_hook_handler.py +352 -261
- mcp_code_indexer/logging_config.py +76 -94
- mcp_code_indexer/main.py +406 -320
- mcp_code_indexer/middleware/error_middleware.py +106 -71
- mcp_code_indexer/query_preprocessor.py +40 -40
- mcp_code_indexer/server/mcp_server.py +785 -470
- mcp_code_indexer/token_counter.py +54 -47
- {mcp_code_indexer-3.1.4.dist-info → mcp_code_indexer-3.1.6.dist-info}/METADATA +3 -3
- mcp_code_indexer-3.1.6.dist-info/RECORD +37 -0
- mcp_code_indexer-3.1.4.dist-info/RECORD +0 -37
- {mcp_code_indexer-3.1.4.dist-info → mcp_code_indexer-3.1.6.dist-info}/WHEEL +0 -0
- {mcp_code_indexer-3.1.4.dist-info → mcp_code_indexer-3.1.6.dist-info}/entry_points.txt +0 -0
- {mcp_code_indexer-3.1.4.dist-info → mcp_code_indexer-3.1.6.dist-info}/licenses/LICENSE +0 -0
- {mcp_code_indexer-3.1.4.dist-info → mcp_code_indexer-3.1.6.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,6 @@ and manual cleanup methods.
|
|
9
9
|
import logging
|
10
10
|
import time
|
11
11
|
from typing import List, Optional
|
12
|
-
from pathlib import Path
|
13
12
|
|
14
13
|
logger = logging.getLogger(__name__)
|
15
14
|
|
@@ -17,200 +16,213 @@ logger = logging.getLogger(__name__)
|
|
17
16
|
class CleanupManager:
|
18
17
|
"""
|
19
18
|
Manages cleanup operations for file descriptions with retention policies.
|
20
|
-
|
19
|
+
|
21
20
|
Handles soft deletion by updating to_be_cleaned timestamps and provides
|
22
21
|
periodic cleanup to permanently remove old records after the retention period.
|
23
22
|
"""
|
24
|
-
|
23
|
+
|
25
24
|
def __init__(self, db_manager, retention_months: int = 6):
|
26
25
|
"""
|
27
26
|
Initialize cleanup manager.
|
28
|
-
|
27
|
+
|
29
28
|
Args:
|
30
29
|
db_manager: DatabaseManager instance
|
31
|
-
retention_months: Number of months to retain records before
|
30
|
+
retention_months: Number of months to retain records before
|
31
|
+
permanent deletion
|
32
32
|
"""
|
33
33
|
self.db_manager = db_manager
|
34
34
|
self.retention_months = retention_months
|
35
|
-
|
35
|
+
|
36
36
|
async def mark_file_for_cleanup(self, project_id: str, file_path: str) -> bool:
|
37
37
|
"""
|
38
38
|
Mark a specific file for cleanup by setting to_be_cleaned timestamp.
|
39
|
-
|
39
|
+
|
40
40
|
Args:
|
41
41
|
project_id: Project identifier
|
42
42
|
file_path: Path to file to mark for cleanup
|
43
|
-
|
43
|
+
|
44
44
|
Returns:
|
45
45
|
True if file was marked, False if file not found
|
46
46
|
"""
|
47
47
|
cleanup_timestamp = int(time.time())
|
48
|
-
|
49
|
-
async with self.db_manager.get_write_connection_with_retry(
|
48
|
+
|
49
|
+
async with self.db_manager.get_write_connection_with_retry(
|
50
|
+
"mark_file_for_cleanup"
|
51
|
+
) as db:
|
50
52
|
cursor = await db.execute(
|
51
53
|
"""
|
52
|
-
UPDATE file_descriptions
|
53
|
-
SET to_be_cleaned = ?
|
54
|
+
UPDATE file_descriptions
|
55
|
+
SET to_be_cleaned = ?
|
54
56
|
WHERE project_id = ? AND file_path = ? AND to_be_cleaned IS NULL
|
55
57
|
""",
|
56
|
-
(cleanup_timestamp, project_id, file_path)
|
58
|
+
(cleanup_timestamp, project_id, file_path),
|
57
59
|
)
|
58
60
|
await db.commit()
|
59
|
-
|
61
|
+
|
60
62
|
# Check if any rows were affected
|
61
63
|
return cursor.rowcount > 0
|
62
|
-
|
63
|
-
async def mark_files_for_cleanup(
|
64
|
+
|
65
|
+
async def mark_files_for_cleanup(
|
66
|
+
self, project_id: str, file_paths: List[str]
|
67
|
+
) -> int:
|
64
68
|
"""
|
65
69
|
Mark multiple files for cleanup in a batch operation.
|
66
|
-
|
70
|
+
|
67
71
|
Args:
|
68
72
|
project_id: Project identifier
|
69
73
|
file_paths: List of file paths to mark for cleanup
|
70
|
-
|
74
|
+
|
71
75
|
Returns:
|
72
76
|
Number of files marked for cleanup
|
73
77
|
"""
|
74
78
|
if not file_paths:
|
75
79
|
return 0
|
76
|
-
|
80
|
+
|
77
81
|
cleanup_timestamp = int(time.time())
|
78
|
-
|
82
|
+
|
79
83
|
async def batch_operation(conn):
|
80
84
|
data = [(cleanup_timestamp, project_id, path) for path in file_paths]
|
81
85
|
cursor = await conn.executemany(
|
82
86
|
"""
|
83
|
-
UPDATE file_descriptions
|
84
|
-
SET to_be_cleaned = ?
|
87
|
+
UPDATE file_descriptions
|
88
|
+
SET to_be_cleaned = ?
|
85
89
|
WHERE project_id = ? AND file_path = ? AND to_be_cleaned IS NULL
|
86
90
|
""",
|
87
|
-
data
|
91
|
+
data,
|
88
92
|
)
|
89
93
|
return cursor.rowcount
|
90
|
-
|
94
|
+
|
91
95
|
marked_count = await self.db_manager.execute_transaction_with_retry(
|
92
96
|
batch_operation,
|
93
97
|
f"mark_files_for_cleanup_{len(file_paths)}_files",
|
94
|
-
timeout_seconds=30.0
|
98
|
+
timeout_seconds=30.0,
|
95
99
|
)
|
96
|
-
|
100
|
+
|
97
101
|
logger.info(f"Marked {marked_count} files for cleanup in project {project_id}")
|
98
102
|
return marked_count
|
99
|
-
|
103
|
+
|
100
104
|
async def restore_file_from_cleanup(self, project_id: str, file_path: str) -> bool:
|
101
105
|
"""
|
102
106
|
Restore a file from cleanup by clearing its to_be_cleaned timestamp.
|
103
|
-
|
107
|
+
|
104
108
|
Args:
|
105
109
|
project_id: Project identifier
|
106
110
|
file_path: Path to file to restore
|
107
|
-
|
111
|
+
|
108
112
|
Returns:
|
109
113
|
True if file was restored, False if file not found
|
110
114
|
"""
|
111
|
-
async with self.db_manager.get_write_connection_with_retry(
|
115
|
+
async with self.db_manager.get_write_connection_with_retry(
|
116
|
+
"restore_file_from_cleanup"
|
117
|
+
) as db:
|
112
118
|
cursor = await db.execute(
|
113
119
|
"""
|
114
|
-
UPDATE file_descriptions
|
115
|
-
SET to_be_cleaned = NULL
|
120
|
+
UPDATE file_descriptions
|
121
|
+
SET to_be_cleaned = NULL
|
116
122
|
WHERE project_id = ? AND file_path = ? AND to_be_cleaned IS NOT NULL
|
117
123
|
""",
|
118
|
-
(project_id, file_path)
|
124
|
+
(project_id, file_path),
|
119
125
|
)
|
120
126
|
await db.commit()
|
121
|
-
|
127
|
+
|
122
128
|
return cursor.rowcount > 0
|
123
|
-
|
129
|
+
|
124
130
|
async def get_files_to_be_cleaned(self, project_id: str) -> List[dict]:
|
125
131
|
"""
|
126
132
|
Get list of files marked for cleanup in a project.
|
127
|
-
|
133
|
+
|
128
134
|
Args:
|
129
135
|
project_id: Project identifier
|
130
|
-
|
136
|
+
|
131
137
|
Returns:
|
132
138
|
List of dictionaries with file_path and to_be_cleaned timestamp
|
133
139
|
"""
|
134
140
|
async with self.db_manager.get_connection() as db:
|
135
141
|
cursor = await db.execute(
|
136
142
|
"""
|
137
|
-
SELECT file_path, to_be_cleaned
|
138
|
-
FROM file_descriptions
|
143
|
+
SELECT file_path, to_be_cleaned
|
144
|
+
FROM file_descriptions
|
139
145
|
WHERE project_id = ? AND to_be_cleaned IS NOT NULL
|
140
146
|
ORDER BY to_be_cleaned DESC, file_path
|
141
147
|
""",
|
142
|
-
(project_id,)
|
148
|
+
(project_id,),
|
143
149
|
)
|
144
150
|
rows = await cursor.fetchall()
|
145
|
-
|
151
|
+
|
146
152
|
return [
|
147
153
|
{
|
148
|
-
|
149
|
-
|
150
|
-
|
154
|
+
"file_path": row["file_path"],
|
155
|
+
"marked_for_cleanup": row["to_be_cleaned"],
|
156
|
+
"marked_date": time.strftime(
|
157
|
+
"%Y-%m-%d %H:%M:%S", time.localtime(row["to_be_cleaned"])
|
158
|
+
),
|
151
159
|
}
|
152
160
|
for row in rows
|
153
161
|
]
|
154
|
-
|
162
|
+
|
155
163
|
async def perform_cleanup(self, project_id: Optional[str] = None) -> int:
|
156
164
|
"""
|
157
165
|
Permanently delete records that exceed the retention period.
|
158
|
-
|
166
|
+
|
159
167
|
Args:
|
160
|
-
project_id: If specified, only clean up this project. Otherwise
|
161
|
-
|
168
|
+
project_id: If specified, only clean up this project. Otherwise
|
169
|
+
clean all projects.
|
170
|
+
|
162
171
|
Returns:
|
163
172
|
Number of records permanently deleted
|
164
173
|
"""
|
165
174
|
# Calculate cutoff timestamp (retention_months ago)
|
166
|
-
cutoff_seconds =
|
175
|
+
cutoff_seconds = (
|
176
|
+
self.retention_months * 30 * 24 * 60 * 60
|
177
|
+
) # Approximate months to seconds
|
167
178
|
cutoff_timestamp = int(time.time()) - cutoff_seconds
|
168
|
-
|
179
|
+
|
169
180
|
async def cleanup_operation(conn):
|
170
181
|
if project_id:
|
171
182
|
cursor = await conn.execute(
|
172
183
|
"""
|
173
|
-
DELETE FROM file_descriptions
|
174
|
-
WHERE project_id = ? AND to_be_cleaned IS NOT NULL
|
184
|
+
DELETE FROM file_descriptions
|
185
|
+
WHERE project_id = ? AND to_be_cleaned IS NOT NULL
|
186
|
+
AND to_be_cleaned < ?
|
175
187
|
""",
|
176
|
-
(project_id, cutoff_timestamp)
|
188
|
+
(project_id, cutoff_timestamp),
|
177
189
|
)
|
178
190
|
else:
|
179
191
|
cursor = await conn.execute(
|
180
192
|
"""
|
181
|
-
DELETE FROM file_descriptions
|
193
|
+
DELETE FROM file_descriptions
|
182
194
|
WHERE to_be_cleaned IS NOT NULL AND to_be_cleaned < ?
|
183
195
|
""",
|
184
|
-
(cutoff_timestamp,)
|
196
|
+
(cutoff_timestamp,),
|
185
197
|
)
|
186
|
-
|
198
|
+
|
187
199
|
return cursor.rowcount
|
188
|
-
|
200
|
+
|
189
201
|
deleted_count = await self.db_manager.execute_transaction_with_retry(
|
190
202
|
cleanup_operation,
|
191
203
|
f"perform_cleanup_{project_id or 'all_projects'}",
|
192
|
-
timeout_seconds=60.0
|
204
|
+
timeout_seconds=60.0,
|
193
205
|
)
|
194
|
-
|
206
|
+
|
195
207
|
if deleted_count > 0:
|
196
208
|
scope = f"project {project_id}" if project_id else "all projects"
|
197
209
|
logger.info(f"Permanently deleted {deleted_count} old records from {scope}")
|
198
|
-
|
210
|
+
|
199
211
|
return deleted_count
|
200
|
-
|
212
|
+
|
201
213
|
async def get_cleanup_stats(self, project_id: Optional[str] = None) -> dict:
|
202
214
|
"""
|
203
215
|
Get statistics about cleanup state.
|
204
|
-
|
216
|
+
|
205
217
|
Args:
|
206
218
|
project_id: If specified, get stats for this project only
|
207
|
-
|
219
|
+
|
208
220
|
Returns:
|
209
221
|
Dictionary with cleanup statistics
|
210
222
|
"""
|
211
223
|
cutoff_seconds = self.retention_months * 30 * 24 * 60 * 60
|
212
224
|
cutoff_timestamp = int(time.time()) - cutoff_seconds
|
213
|
-
|
225
|
+
|
214
226
|
async with self.db_manager.get_connection() as db:
|
215
227
|
if project_id:
|
216
228
|
base_where = "WHERE project_id = ?"
|
@@ -218,38 +230,52 @@ class CleanupManager:
|
|
218
230
|
else:
|
219
231
|
base_where = ""
|
220
232
|
params = ()
|
221
|
-
|
233
|
+
|
222
234
|
# Active files
|
223
235
|
cursor = await db.execute(
|
224
|
-
|
225
|
-
|
236
|
+
(
|
237
|
+
f"SELECT COUNT(*) FROM file_descriptions {base_where} "
|
238
|
+
"AND to_be_cleaned IS NULL"
|
239
|
+
),
|
240
|
+
params,
|
226
241
|
)
|
227
242
|
active_count = (await cursor.fetchone())[0]
|
228
|
-
|
243
|
+
|
229
244
|
# Files marked for cleanup
|
230
245
|
cursor = await db.execute(
|
231
|
-
|
232
|
-
|
246
|
+
(
|
247
|
+
f"SELECT COUNT(*) FROM file_descriptions {base_where} "
|
248
|
+
"AND to_be_cleaned IS NOT NULL"
|
249
|
+
),
|
250
|
+
params,
|
233
251
|
)
|
234
252
|
marked_count = (await cursor.fetchone())[0]
|
235
|
-
|
253
|
+
|
236
254
|
# Files eligible for permanent deletion
|
237
255
|
if project_id:
|
238
256
|
cursor = await db.execute(
|
239
|
-
|
240
|
-
|
257
|
+
(
|
258
|
+
"SELECT COUNT(*) FROM file_descriptions WHERE project_id = ? "
|
259
|
+
"AND to_be_cleaned IS NOT NULL AND to_be_cleaned < ?"
|
260
|
+
),
|
261
|
+
(project_id, cutoff_timestamp),
|
241
262
|
)
|
242
263
|
else:
|
243
264
|
cursor = await db.execute(
|
244
|
-
|
245
|
-
|
265
|
+
(
|
266
|
+
"SELECT COUNT(*) FROM file_descriptions WHERE "
|
267
|
+
"to_be_cleaned IS NOT NULL AND to_be_cleaned < ?"
|
268
|
+
),
|
269
|
+
(cutoff_timestamp,),
|
246
270
|
)
|
247
271
|
eligible_for_deletion = (await cursor.fetchone())[0]
|
248
|
-
|
272
|
+
|
249
273
|
return {
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
274
|
+
"active_files": active_count,
|
275
|
+
"marked_for_cleanup": marked_count,
|
276
|
+
"eligible_for_deletion": eligible_for_deletion,
|
277
|
+
"retention_months": self.retention_months,
|
278
|
+
"cutoff_date": time.strftime(
|
279
|
+
"%Y-%m-%d %H:%M:%S", time.localtime(cutoff_timestamp)
|
280
|
+
),
|
255
281
|
}
|