kodit 0.4.2__py3-none-any.whl → 0.4.3__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.
Potentially problematic release.
This version of kodit might be problematic. Click here for more details.
- kodit/_version.py +2 -2
- kodit/app.py +6 -1
- kodit/application/factories/code_indexing_factory.py +14 -12
- kodit/application/factories/reporting_factory.py +10 -5
- kodit/application/services/auto_indexing_service.py +28 -32
- kodit/application/services/code_indexing_application_service.py +43 -26
- kodit/application/services/indexing_worker_service.py +10 -12
- kodit/application/services/reporting.py +72 -54
- kodit/cli.py +68 -78
- kodit/config.py +2 -2
- kodit/domain/entities.py +99 -1
- kodit/domain/protocols.py +28 -3
- kodit/domain/services/index_service.py +11 -9
- kodit/domain/services/task_status_query_service.py +19 -0
- kodit/domain/value_objects.py +26 -29
- kodit/infrastructure/api/v1/dependencies.py +19 -4
- kodit/infrastructure/api/v1/routers/indexes.py +45 -0
- kodit/infrastructure/api/v1/schemas/task_status.py +39 -0
- kodit/infrastructure/cloning/git/working_copy.py +9 -2
- kodit/infrastructure/enrichment/local_enrichment_provider.py +41 -30
- kodit/infrastructure/mappers/task_status_mapper.py +85 -0
- kodit/infrastructure/reporting/db_progress.py +23 -0
- kodit/infrastructure/reporting/log_progress.py +5 -33
- kodit/infrastructure/reporting/tdqm_progress.py +10 -45
- kodit/infrastructure/sqlalchemy/entities.py +61 -0
- kodit/infrastructure/sqlalchemy/task_status_repository.py +79 -0
- kodit/mcp.py +6 -2
- kodit/migrations/env.py +0 -1
- kodit/migrations/versions/b9cd1c3fd762_add_task_status.py +77 -0
- {kodit-0.4.2.dist-info → kodit-0.4.3.dist-info}/METADATA +1 -1
- {kodit-0.4.2.dist-info → kodit-0.4.3.dist-info}/RECORD +34 -28
- {kodit-0.4.2.dist-info → kodit-0.4.3.dist-info}/WHEEL +0 -0
- {kodit-0.4.2.dist-info → kodit-0.4.3.dist-info}/entry_points.txt +0 -0
- {kodit-0.4.2.dist-info → kodit-0.4.3.dist-info}/licenses/LICENSE +0 -0
kodit/cli.py
CHANGED
|
@@ -183,56 +183,52 @@ async def _index_local(
|
|
|
183
183
|
|
|
184
184
|
# Get database session
|
|
185
185
|
db = await app_context.get_db()
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
session_factory=db.session_factory
|
|
195
|
-
),
|
|
196
|
-
fusion_service=ReciprocalRankFusionService(),
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
if auto_index:
|
|
200
|
-
sources = await _handle_auto_index(app_context, sources)
|
|
201
|
-
if not sources:
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
if sync:
|
|
205
|
-
await _handle_sync(service, index_query_service, sources)
|
|
206
|
-
return
|
|
186
|
+
service = create_cli_code_indexing_application_service(
|
|
187
|
+
app_context=app_context,
|
|
188
|
+
session_factory=db.session_factory,
|
|
189
|
+
)
|
|
190
|
+
index_query_service = IndexQueryService(
|
|
191
|
+
index_repository=create_index_repository(session_factory=db.session_factory),
|
|
192
|
+
fusion_service=ReciprocalRankFusionService(),
|
|
193
|
+
)
|
|
207
194
|
|
|
195
|
+
if auto_index:
|
|
196
|
+
sources = await _handle_auto_index(app_context, sources)
|
|
208
197
|
if not sources:
|
|
209
|
-
await _handle_list_indexes(index_query_service)
|
|
210
198
|
return
|
|
211
199
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
200
|
+
if sync:
|
|
201
|
+
await _handle_sync(service, index_query_service, sources)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
if not sources:
|
|
205
|
+
await _handle_list_indexes(index_query_service)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Handle source indexing
|
|
209
|
+
for source in sources:
|
|
210
|
+
if Path(source).is_file():
|
|
211
|
+
msg = "File indexing is not implemented yet"
|
|
212
|
+
raise click.UsageError(msg)
|
|
213
|
+
|
|
214
|
+
# Index source with progress
|
|
215
|
+
log_event("kodit.cli.index.create")
|
|
216
|
+
|
|
217
|
+
# Create a lazy progress callback that only shows progress when needed
|
|
218
|
+
index = await service.create_index_from_uri(source)
|
|
219
|
+
|
|
220
|
+
# Create a new progress callback for the indexing operations
|
|
221
|
+
try:
|
|
222
|
+
await service.run_index(index)
|
|
223
|
+
except EmptySourceError as e:
|
|
224
|
+
log.exception("Empty source error", error=e)
|
|
225
|
+
msg = f"""{e}. This could mean:
|
|
230
226
|
• The repository contains no supported file types
|
|
231
227
|
• All files are excluded by ignore patterns
|
|
232
228
|
• The files contain no extractable code snippets
|
|
233
229
|
Please check the repository contents and try again.
|
|
234
230
|
"""
|
|
235
|
-
|
|
231
|
+
click.echo(msg)
|
|
236
232
|
|
|
237
233
|
|
|
238
234
|
async def _index_remote(
|
|
@@ -319,35 +315,33 @@ async def _search_local( # noqa: PLR0913
|
|
|
319
315
|
|
|
320
316
|
# Get database session
|
|
321
317
|
db = await app_context.get_db()
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
session_factory=db.session_factory,
|
|
327
|
-
)
|
|
318
|
+
service = create_cli_code_indexing_application_service(
|
|
319
|
+
app_context=app_context,
|
|
320
|
+
session_factory=db.session_factory,
|
|
321
|
+
)
|
|
328
322
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
323
|
+
filters = _parse_filters(
|
|
324
|
+
language, author, created_after, created_before, source_repo
|
|
325
|
+
)
|
|
332
326
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
)
|
|
327
|
+
snippets = await service.search(
|
|
328
|
+
MultiSearchRequest(
|
|
329
|
+
keywords=keywords,
|
|
330
|
+
code_query=code_query,
|
|
331
|
+
text_query=text_query,
|
|
332
|
+
top_k=top_k,
|
|
333
|
+
filters=filters,
|
|
341
334
|
)
|
|
335
|
+
)
|
|
342
336
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
337
|
+
if len(snippets) == 0:
|
|
338
|
+
click.echo("No snippets found")
|
|
339
|
+
return
|
|
346
340
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
341
|
+
if output_format == "text":
|
|
342
|
+
click.echo(MultiSearchResult.to_string(snippets))
|
|
343
|
+
elif output_format == "json":
|
|
344
|
+
click.echo(MultiSearchResult.to_jsonlines(snippets))
|
|
351
345
|
|
|
352
346
|
|
|
353
347
|
async def _search_remote( # noqa: PLR0913
|
|
@@ -785,19 +779,15 @@ async def snippets(
|
|
|
785
779
|
# Local mode
|
|
786
780
|
log_event("kodit.cli.show.snippets")
|
|
787
781
|
db = await app_context.get_db()
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
)
|
|
797
|
-
if output_format == "text":
|
|
798
|
-
click.echo(MultiSearchResult.to_string(snippets))
|
|
799
|
-
elif output_format == "json":
|
|
800
|
-
click.echo(MultiSearchResult.to_jsonlines(snippets))
|
|
782
|
+
service = create_cli_code_indexing_application_service(
|
|
783
|
+
app_context=app_context,
|
|
784
|
+
session_factory=db.session_factory,
|
|
785
|
+
)
|
|
786
|
+
snippets = await service.list_snippets(file_path=by_path, source_uri=by_source)
|
|
787
|
+
if output_format == "text":
|
|
788
|
+
click.echo(MultiSearchResult.to_string(snippets))
|
|
789
|
+
elif output_format == "json":
|
|
790
|
+
click.echo(MultiSearchResult.to_jsonlines(snippets))
|
|
801
791
|
else:
|
|
802
792
|
# Remote mode - not supported
|
|
803
793
|
click.echo("⚠️ Warning: 'show snippets' is not implemented in remote mode")
|
kodit/config.py
CHANGED
|
@@ -305,7 +305,7 @@ class AppContext(BaseSettings):
|
|
|
305
305
|
with_app_context = click.make_pass_decorator(AppContext)
|
|
306
306
|
|
|
307
307
|
|
|
308
|
-
def wrap_async(f: Callable[..., Coroutine[Any, Any, T]]) -> Callable[..., T]:
|
|
308
|
+
def wrap_async[T](f: Callable[..., Coroutine[Any, Any, T]]) -> Callable[..., T]:
|
|
309
309
|
"""Decorate async Click commands.
|
|
310
310
|
|
|
311
311
|
This decorator wraps an async function to run it with asyncio.run().
|
|
@@ -326,7 +326,7 @@ def wrap_async(f: Callable[..., Coroutine[Any, Any, T]]) -> Callable[..., T]:
|
|
|
326
326
|
return wrapper
|
|
327
327
|
|
|
328
328
|
|
|
329
|
-
def with_session(f: Callable[..., Coroutine[Any, Any, T]]) -> Callable[..., T]:
|
|
329
|
+
def with_session[T](f: Callable[..., Coroutine[Any, Any, T]]) -> Callable[..., T]:
|
|
330
330
|
"""Provide a database session to CLI commands."""
|
|
331
331
|
|
|
332
332
|
@wraps(f)
|
kodit/domain/entities.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import shutil
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from datetime import datetime
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Protocol
|
|
8
8
|
from urllib.parse import urlparse, urlunparse
|
|
@@ -12,10 +12,13 @@ from pydantic import AnyUrl, BaseModel
|
|
|
12
12
|
from kodit.domain.value_objects import (
|
|
13
13
|
FileProcessingStatus,
|
|
14
14
|
QueuePriority,
|
|
15
|
+
ReportingState,
|
|
15
16
|
SnippetContent,
|
|
16
17
|
SnippetContentType,
|
|
17
18
|
SourceType,
|
|
19
|
+
TaskOperation,
|
|
18
20
|
TaskType,
|
|
21
|
+
TrackableType,
|
|
19
22
|
)
|
|
20
23
|
from kodit.utils.path_utils import path_from_uri
|
|
21
24
|
|
|
@@ -321,3 +324,98 @@ class Task(BaseModel):
|
|
|
321
324
|
priority=priority.value,
|
|
322
325
|
payload={"index_id": index_id},
|
|
323
326
|
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TaskStatus(BaseModel):
|
|
330
|
+
"""Task status domain entity."""
|
|
331
|
+
|
|
332
|
+
id: str
|
|
333
|
+
state: ReportingState
|
|
334
|
+
operation: TaskOperation
|
|
335
|
+
message: str = ""
|
|
336
|
+
|
|
337
|
+
created_at: datetime = datetime.now(UTC)
|
|
338
|
+
updated_at: datetime = datetime.now(UTC)
|
|
339
|
+
total: int = 0
|
|
340
|
+
current: int = 0
|
|
341
|
+
|
|
342
|
+
error: str | None = None
|
|
343
|
+
parent: "TaskStatus | None" = None
|
|
344
|
+
trackable_id: int | None = None
|
|
345
|
+
trackable_type: TrackableType | None = None
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def create(
|
|
349
|
+
operation: TaskOperation,
|
|
350
|
+
parent: "TaskStatus | None" = None,
|
|
351
|
+
trackable_type: TrackableType | None = None,
|
|
352
|
+
trackable_id: int | None = None,
|
|
353
|
+
) -> "TaskStatus":
|
|
354
|
+
"""Create a task status."""
|
|
355
|
+
return TaskStatus(
|
|
356
|
+
id=TaskStatus._create_id(operation, trackable_type, trackable_id),
|
|
357
|
+
operation=operation,
|
|
358
|
+
parent=parent,
|
|
359
|
+
trackable_type=trackable_type,
|
|
360
|
+
trackable_id=trackable_id,
|
|
361
|
+
state=ReportingState.STARTED,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
@staticmethod
|
|
365
|
+
def _create_id(
|
|
366
|
+
step: TaskOperation,
|
|
367
|
+
trackable_type: TrackableType | None = None,
|
|
368
|
+
trackable_id: int | None = None,
|
|
369
|
+
) -> str:
|
|
370
|
+
"""Create a unique id for a task."""
|
|
371
|
+
result = []
|
|
372
|
+
# Nice to be prefixed by tracking information if it exists
|
|
373
|
+
if trackable_type:
|
|
374
|
+
result.append(str(trackable_type))
|
|
375
|
+
if trackable_id:
|
|
376
|
+
result.append(str(trackable_id))
|
|
377
|
+
result.append(str(step))
|
|
378
|
+
return "-".join(result)
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def completion_percent(self) -> float:
|
|
382
|
+
"""Calculate the percentage of completion."""
|
|
383
|
+
if self.total == 0:
|
|
384
|
+
return 0.0
|
|
385
|
+
return min(100.0, max(0.0, (self.current / self.total) * 100.0))
|
|
386
|
+
|
|
387
|
+
def skip(self, message: str) -> None:
|
|
388
|
+
"""Skip the task."""
|
|
389
|
+
self.state = ReportingState.SKIPPED
|
|
390
|
+
self.message = message
|
|
391
|
+
|
|
392
|
+
def fail(self, error: str) -> None:
|
|
393
|
+
"""Fail the task."""
|
|
394
|
+
self.state = ReportingState.FAILED
|
|
395
|
+
self.error = error
|
|
396
|
+
|
|
397
|
+
def set_total(self, total: int) -> None:
|
|
398
|
+
"""Set the total for the step."""
|
|
399
|
+
self.total = total
|
|
400
|
+
|
|
401
|
+
def set_current(self, current: int, message: str | None = None) -> None:
|
|
402
|
+
"""Progress the step."""
|
|
403
|
+
self.state = ReportingState.IN_PROGRESS
|
|
404
|
+
self.current = current
|
|
405
|
+
if message:
|
|
406
|
+
self.message = message
|
|
407
|
+
|
|
408
|
+
def set_tracking_info(
|
|
409
|
+
self, trackable_id: int, trackable_type: TrackableType
|
|
410
|
+
) -> None:
|
|
411
|
+
"""Set the tracking info."""
|
|
412
|
+
self.trackable_id = trackable_id
|
|
413
|
+
self.trackable_type = trackable_type
|
|
414
|
+
|
|
415
|
+
def complete(self) -> None:
|
|
416
|
+
"""Complete the task."""
|
|
417
|
+
if ReportingState.is_terminal(self.state):
|
|
418
|
+
return # Already in terminal state
|
|
419
|
+
|
|
420
|
+
self.state = ReportingState.COMPLETED
|
|
421
|
+
self.current = self.total # Ensure progress shows 100%
|
kodit/domain/protocols.py
CHANGED
|
@@ -5,8 +5,15 @@ from typing import Protocol
|
|
|
5
5
|
|
|
6
6
|
from pydantic import AnyUrl
|
|
7
7
|
|
|
8
|
-
from kodit.domain.entities import
|
|
9
|
-
|
|
8
|
+
from kodit.domain.entities import (
|
|
9
|
+
Index,
|
|
10
|
+
Snippet,
|
|
11
|
+
SnippetWithContext,
|
|
12
|
+
Task,
|
|
13
|
+
TaskStatus,
|
|
14
|
+
WorkingCopy,
|
|
15
|
+
)
|
|
16
|
+
from kodit.domain.value_objects import MultiSearchRequest, TaskType
|
|
10
17
|
|
|
11
18
|
|
|
12
19
|
class TaskRepository(Protocol):
|
|
@@ -95,6 +102,24 @@ class IndexRepository(Protocol):
|
|
|
95
102
|
class ReportingModule(Protocol):
|
|
96
103
|
"""Reporting module."""
|
|
97
104
|
|
|
98
|
-
def on_change(self,
|
|
105
|
+
async def on_change(self, progress: TaskStatus) -> None:
|
|
99
106
|
"""On step changed."""
|
|
100
107
|
...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TaskStatusRepository(Protocol):
|
|
111
|
+
"""Repository interface for persisting progress state only."""
|
|
112
|
+
|
|
113
|
+
async def save(self, status: TaskStatus) -> None:
|
|
114
|
+
"""Save a progress state."""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
async def load_with_hierarchy(
|
|
118
|
+
self, trackable_type: str, trackable_id: int
|
|
119
|
+
) -> list[TaskStatus]:
|
|
120
|
+
"""Load progress states with IDs and parent IDs from database."""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
async def delete(self, status: TaskStatus) -> None:
|
|
124
|
+
"""Delete a progress state."""
|
|
125
|
+
...
|
|
@@ -126,9 +126,9 @@ class IndexDomainService:
|
|
|
126
126
|
|
|
127
127
|
# Calculate snippets for each language
|
|
128
128
|
slicer = Slicer()
|
|
129
|
-
step.set_total(len(lang_files_map.keys()))
|
|
129
|
+
await step.set_total(len(lang_files_map.keys()))
|
|
130
130
|
for i, (lang, lang_files) in enumerate(lang_files_map.items()):
|
|
131
|
-
step.set_current(i)
|
|
131
|
+
await step.set_current(i, f"Extracting snippets for {lang}")
|
|
132
132
|
s = slicer.extract_snippets(lang_files, language=lang)
|
|
133
133
|
index.snippets.extend(s)
|
|
134
134
|
|
|
@@ -142,10 +142,10 @@ class IndexDomainService:
|
|
|
142
142
|
"""Enrich snippets with AI-generated summaries."""
|
|
143
143
|
reporting_step = reporting_step or create_noop_operation()
|
|
144
144
|
if not snippets or len(snippets) == 0:
|
|
145
|
-
reporting_step.skip("No snippets to enrich")
|
|
145
|
+
await reporting_step.skip("No snippets to enrich")
|
|
146
146
|
return snippets
|
|
147
147
|
|
|
148
|
-
reporting_step.set_total(len(snippets))
|
|
148
|
+
await reporting_step.set_total(len(snippets))
|
|
149
149
|
snippet_map = {snippet.id: snippet for snippet in snippets if snippet.id}
|
|
150
150
|
|
|
151
151
|
enrichment_request = EnrichmentIndexRequest(
|
|
@@ -162,7 +162,9 @@ class IndexDomainService:
|
|
|
162
162
|
snippet_map[result.snippet_id].add_summary(result.text)
|
|
163
163
|
|
|
164
164
|
processed += 1
|
|
165
|
-
reporting_step.set_current(
|
|
165
|
+
await reporting_step.set_current(
|
|
166
|
+
processed, f"Enriching snippets for {processed} snippets"
|
|
167
|
+
)
|
|
166
168
|
|
|
167
169
|
return list(snippet_map.values())
|
|
168
170
|
|
|
@@ -222,12 +224,12 @@ class IndexDomainService:
|
|
|
222
224
|
|
|
223
225
|
# Setup reporter
|
|
224
226
|
processed = 0
|
|
225
|
-
step.set_total(num_files_to_process)
|
|
227
|
+
await step.set_total(num_files_to_process)
|
|
226
228
|
|
|
227
229
|
# First check to see if any files have been deleted
|
|
228
230
|
for file_path in deleted_file_paths:
|
|
229
231
|
processed += 1
|
|
230
|
-
step.set_current(processed)
|
|
232
|
+
await step.set_current(processed, f"Deleting file {file_path}")
|
|
231
233
|
previous_files_map[
|
|
232
234
|
file_path
|
|
233
235
|
].file_processing_status = domain_entities.FileProcessingStatus.DELETED
|
|
@@ -235,7 +237,7 @@ class IndexDomainService:
|
|
|
235
237
|
# Then check to see if there are any new files
|
|
236
238
|
for file_path in new_file_paths:
|
|
237
239
|
processed += 1
|
|
238
|
-
step.set_current(processed)
|
|
240
|
+
await step.set_current(processed, f"Adding new file {file_path}")
|
|
239
241
|
try:
|
|
240
242
|
working_copy.files.append(
|
|
241
243
|
await metadata_extractor.extract(file_path=file_path)
|
|
@@ -247,7 +249,7 @@ class IndexDomainService:
|
|
|
247
249
|
# Finally check if there are any modified files
|
|
248
250
|
for file_path in modified_file_paths:
|
|
249
251
|
processed += 1
|
|
250
|
-
step.set_current(processed)
|
|
252
|
+
await step.set_current(processed, f"Modifying file {file_path}")
|
|
251
253
|
try:
|
|
252
254
|
previous_file = previous_files_map[file_path]
|
|
253
255
|
new_file = await metadata_extractor.extract(file_path=file_path)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Domain service for querying task status."""
|
|
2
|
+
|
|
3
|
+
from kodit.domain.entities import TaskStatus
|
|
4
|
+
from kodit.domain.protocols import TaskStatusRepository
|
|
5
|
+
from kodit.domain.value_objects import TrackableType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskStatusQueryService:
|
|
9
|
+
"""Query service for task status information."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, repository: TaskStatusRepository) -> None:
|
|
12
|
+
"""Initialize the task status query service."""
|
|
13
|
+
self._repository = repository
|
|
14
|
+
|
|
15
|
+
async def get_index_status(self, index_id: int) -> list[TaskStatus]:
|
|
16
|
+
"""Get the status of tasks for a specific index."""
|
|
17
|
+
return await self._repository.load_with_hierarchy(
|
|
18
|
+
trackable_type=TrackableType.INDEX.value, trackable_id=index_id
|
|
19
|
+
)
|
kodit/domain/value_objects.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Pure domain value objects and DTOs."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from enum import Enum, IntEnum, StrEnum
|
|
7
7
|
from pathlib import Path
|
|
@@ -676,37 +676,34 @@ class ReportingState(StrEnum):
|
|
|
676
676
|
FAILED = "failed"
|
|
677
677
|
SKIPPED = "skipped"
|
|
678
678
|
|
|
679
|
+
@staticmethod
|
|
680
|
+
def is_terminal(state: "ReportingState") -> bool:
|
|
681
|
+
"""Check if a state is completed."""
|
|
682
|
+
return state in [
|
|
683
|
+
ReportingState.COMPLETED,
|
|
684
|
+
ReportingState.FAILED,
|
|
685
|
+
ReportingState.SKIPPED,
|
|
686
|
+
]
|
|
679
687
|
|
|
680
|
-
@dataclass(frozen=True)
|
|
681
|
-
class Progress:
|
|
682
|
-
"""Immutable representation of a step's state."""
|
|
683
|
-
|
|
684
|
-
name: str
|
|
685
|
-
state: ReportingState
|
|
686
|
-
message: str = ""
|
|
687
|
-
error: BaseException | None = None
|
|
688
|
-
total: int = 0
|
|
689
|
-
current: int = 0
|
|
690
688
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
"""Calculate the percentage of completion."""
|
|
694
|
-
if self.total == 0:
|
|
695
|
-
return 0.0
|
|
696
|
-
return min(100.0, max(0.0, (self.current / self.total) * 100.0))
|
|
689
|
+
class TrackableType(StrEnum):
|
|
690
|
+
"""Trackable type."""
|
|
697
691
|
|
|
698
|
-
|
|
699
|
-
"""Return a new snapshot with updated error."""
|
|
700
|
-
return replace(self, error=error)
|
|
692
|
+
INDEX = "indexes"
|
|
701
693
|
|
|
702
|
-
def with_total(self, total: int) -> "Progress":
|
|
703
|
-
"""Return a new snapshot with updated total."""
|
|
704
|
-
return replace(self, total=total)
|
|
705
694
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
return replace(self, current=current)
|
|
695
|
+
class TaskOperation(StrEnum):
|
|
696
|
+
"""Task operation."""
|
|
709
697
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
698
|
+
ROOT = "kodit.root"
|
|
699
|
+
CREATE_INDEX = "kodit.index.create"
|
|
700
|
+
RUN_INDEX = "kodit.index.run"
|
|
701
|
+
REFRESH_WORKING_COPY = "kodit.index.run.refresh_working_copy"
|
|
702
|
+
DELETE_OLD_SNIPPETS = "kodit.index.run.delete_old_snippets"
|
|
703
|
+
EXTRACT_SNIPPETS = "kodit.index.run.extract_snippets"
|
|
704
|
+
CREATE_BM25_INDEX = "kodit.index.run.create_bm25_index"
|
|
705
|
+
CREATE_CODE_EMBEDDINGS = "kodit.index.run.create_code_embeddings"
|
|
706
|
+
ENRICH_SNIPPETS = "kodit.index.run.enrich_snippets"
|
|
707
|
+
CREATE_TEXT_EMBEDDINGS = "kodit.index.run.create_text_embeddings"
|
|
708
|
+
UPDATE_INDEX_TIMESTAMP = "kodit.index.run.update_index_timestamp"
|
|
709
|
+
CLEAR_FILE_PROCESSING_STATUSES = "kodit.index.run.clear_file_processing_statuses"
|
|
@@ -15,8 +15,12 @@ from kodit.application.services.code_indexing_application_service import (
|
|
|
15
15
|
from kodit.application.services.queue_service import QueueService
|
|
16
16
|
from kodit.config import AppContext
|
|
17
17
|
from kodit.domain.services.index_query_service import IndexQueryService
|
|
18
|
+
from kodit.domain.services.task_status_query_service import TaskStatusQueryService
|
|
18
19
|
from kodit.infrastructure.indexing.fusion_service import ReciprocalRankFusionService
|
|
19
20
|
from kodit.infrastructure.sqlalchemy.index_repository import create_index_repository
|
|
21
|
+
from kodit.infrastructure.sqlalchemy.task_status_repository import (
|
|
22
|
+
create_task_status_repository,
|
|
23
|
+
)
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
def get_app_context(request: Request) -> AppContext:
|
|
@@ -70,13 +74,10 @@ IndexQueryServiceDep = Annotated[IndexQueryService, Depends(get_index_query_serv
|
|
|
70
74
|
|
|
71
75
|
async def get_indexing_app_service(
|
|
72
76
|
app_context: AppContextDep,
|
|
73
|
-
session: DBSessionDep,
|
|
74
77
|
session_factory: DBSessionFactoryDep,
|
|
75
78
|
) -> CodeIndexingApplicationService:
|
|
76
79
|
"""Get indexing application service dependency."""
|
|
77
|
-
return create_server_code_indexing_application_service(
|
|
78
|
-
app_context, session, session_factory
|
|
79
|
-
)
|
|
80
|
+
return create_server_code_indexing_application_service(app_context, session_factory)
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
IndexingAppServiceDep = Annotated[
|
|
@@ -94,3 +95,17 @@ async def get_queue_service(
|
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
QueueServiceDep = Annotated[QueueService, Depends(get_queue_service)]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def get_task_status_query_service(
|
|
101
|
+
session_factory: DBSessionFactoryDep,
|
|
102
|
+
) -> TaskStatusQueryService:
|
|
103
|
+
"""Get task status query service dependency."""
|
|
104
|
+
return TaskStatusQueryService(
|
|
105
|
+
repository=create_task_status_repository(session_factory=session_factory)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
TaskStatusQueryServiceDep = Annotated[
|
|
110
|
+
TaskStatusQueryService, Depends(get_task_status_query_service)
|
|
111
|
+
]
|
|
@@ -9,6 +9,7 @@ from kodit.infrastructure.api.v1.dependencies import (
|
|
|
9
9
|
IndexingAppServiceDep,
|
|
10
10
|
IndexQueryServiceDep,
|
|
11
11
|
QueueServiceDep,
|
|
12
|
+
TaskStatusQueryServiceDep,
|
|
12
13
|
)
|
|
13
14
|
from kodit.infrastructure.api.v1.schemas.index import (
|
|
14
15
|
IndexAttributes,
|
|
@@ -18,6 +19,11 @@ from kodit.infrastructure.api.v1.schemas.index import (
|
|
|
18
19
|
IndexListResponse,
|
|
19
20
|
IndexResponse,
|
|
20
21
|
)
|
|
22
|
+
from kodit.infrastructure.api.v1.schemas.task_status import (
|
|
23
|
+
TaskStatusAttributes,
|
|
24
|
+
TaskStatusData,
|
|
25
|
+
TaskStatusListResponse,
|
|
26
|
+
)
|
|
21
27
|
|
|
22
28
|
router = APIRouter(
|
|
23
29
|
prefix="/api/v1/indexes",
|
|
@@ -103,6 +109,45 @@ async def get_index(
|
|
|
103
109
|
)
|
|
104
110
|
|
|
105
111
|
|
|
112
|
+
@router.get(
|
|
113
|
+
"/{index_id}/status",
|
|
114
|
+
responses={404: {"description": "Index not found"}},
|
|
115
|
+
)
|
|
116
|
+
async def get_index_status(
|
|
117
|
+
index_id: int,
|
|
118
|
+
query_service: IndexQueryServiceDep,
|
|
119
|
+
status_service: TaskStatusQueryServiceDep,
|
|
120
|
+
) -> TaskStatusListResponse:
|
|
121
|
+
"""Get the status of tasks for an index."""
|
|
122
|
+
# Verify the index exists
|
|
123
|
+
index = await query_service.get_index_by_id(index_id)
|
|
124
|
+
if not index:
|
|
125
|
+
raise HTTPException(status_code=404, detail="Index not found")
|
|
126
|
+
|
|
127
|
+
# Get all task statuses for this index
|
|
128
|
+
progress_trackers = await status_service.get_index_status(index_id)
|
|
129
|
+
|
|
130
|
+
# Convert progress trackers to API response format
|
|
131
|
+
task_statuses = []
|
|
132
|
+
for _i, status in enumerate(progress_trackers):
|
|
133
|
+
task_statuses.append(
|
|
134
|
+
TaskStatusData(
|
|
135
|
+
id=status.id,
|
|
136
|
+
attributes=TaskStatusAttributes(
|
|
137
|
+
step=status.operation,
|
|
138
|
+
state=status.state,
|
|
139
|
+
progress=status.completion_percent,
|
|
140
|
+
total=status.total,
|
|
141
|
+
current=status.current,
|
|
142
|
+
created_at=status.created_at,
|
|
143
|
+
updated_at=status.updated_at,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return TaskStatusListResponse(data=task_statuses)
|
|
149
|
+
|
|
150
|
+
|
|
106
151
|
@router.delete(
|
|
107
152
|
"/{index_id}", status_code=204, responses={404: {"description": "Index not found"}}
|
|
108
153
|
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""JSON:API schemas for task status operations."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskStatusAttributes(BaseModel):
|
|
9
|
+
"""Task status attributes for JSON:API responses."""
|
|
10
|
+
|
|
11
|
+
step: str = Field(..., description="Name of the task/operation")
|
|
12
|
+
state: str = Field(..., description="Current state of the task")
|
|
13
|
+
progress: float = Field(
|
|
14
|
+
default=0.0, ge=0.0, le=100.0, description="Progress percentage (0-100)"
|
|
15
|
+
)
|
|
16
|
+
total: int = Field(default=0, description="Total number of items to process")
|
|
17
|
+
current: int = Field(default=0, description="Current number of items processed")
|
|
18
|
+
created_at: datetime | None = Field(default=None, description="Task start time")
|
|
19
|
+
updated_at: datetime | None = Field(default=None, description="Last update time")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskStatusData(BaseModel):
|
|
23
|
+
"""Task status data for JSON:API responses."""
|
|
24
|
+
|
|
25
|
+
type: str = "task_status"
|
|
26
|
+
id: str
|
|
27
|
+
attributes: TaskStatusAttributes
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TaskStatusResponse(BaseModel):
|
|
31
|
+
"""JSON:API response for single task status."""
|
|
32
|
+
|
|
33
|
+
data: TaskStatusData
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TaskStatusListResponse(BaseModel):
|
|
37
|
+
"""JSON:API response for task status list."""
|
|
38
|
+
|
|
39
|
+
data: list[TaskStatusData]
|