kodit 0.3.9__py3-none-any.whl → 0.3.11__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 +38 -18
- kodit/application/services/code_indexing_application_service.py +9 -0
- kodit/config.py +22 -1
- kodit/domain/entities.py +5 -0
- kodit/domain/protocols.py +4 -0
- kodit/domain/services/index_service.py +24 -9
- kodit/infrastructure/api/__init__.py +1 -0
- kodit/infrastructure/api/middleware/__init__.py +1 -0
- kodit/infrastructure/api/middleware/auth.py +34 -0
- kodit/infrastructure/api/v1/__init__.py +5 -0
- kodit/infrastructure/api/v1/dependencies.py +70 -0
- kodit/infrastructure/api/v1/routers/__init__.py +6 -0
- kodit/infrastructure/api/v1/routers/indexes.py +114 -0
- kodit/infrastructure/api/v1/routers/search.py +74 -0
- kodit/infrastructure/api/v1/schemas/__init__.py +25 -0
- kodit/infrastructure/api/v1/schemas/context.py +11 -0
- kodit/infrastructure/api/v1/schemas/index.py +101 -0
- kodit/infrastructure/api/v1/schemas/search.py +219 -0
- kodit/infrastructure/slicing/slicer.py +43 -18
- kodit/infrastructure/sqlalchemy/index_repository.py +29 -0
- kodit/utils/dump_openapi.py +33 -0
- {kodit-0.3.9.dist-info → kodit-0.3.11.dist-info}/METADATA +1 -1
- {kodit-0.3.9.dist-info → kodit-0.3.11.dist-info}/RECORD +27 -14
- {kodit-0.3.9.dist-info → kodit-0.3.11.dist-info}/WHEEL +0 -0
- {kodit-0.3.9.dist-info → kodit-0.3.11.dist-info}/entry_points.txt +0 -0
- {kodit-0.3.9.dist-info → kodit-0.3.11.dist-info}/licenses/LICENSE +0 -0
kodit/_version.py
CHANGED
kodit/app.py
CHANGED
|
@@ -4,10 +4,14 @@ from collections.abc import AsyncIterator
|
|
|
4
4
|
from contextlib import asynccontextmanager
|
|
5
5
|
|
|
6
6
|
from asgi_correlation_id import CorrelationIdMiddleware
|
|
7
|
-
from fastapi import FastAPI
|
|
7
|
+
from fastapi import FastAPI, Response
|
|
8
|
+
from fastapi.responses import RedirectResponse
|
|
8
9
|
|
|
10
|
+
from kodit._version import version
|
|
9
11
|
from kodit.application.services.sync_scheduler import SyncSchedulerService
|
|
10
12
|
from kodit.config import AppContext
|
|
13
|
+
from kodit.infrastructure.api.v1.routers import indexes_router, search_router
|
|
14
|
+
from kodit.infrastructure.api.v1.schemas.context import AppLifespanState
|
|
11
15
|
from kodit.infrastructure.indexing.auto_indexing_service import AutoIndexingService
|
|
12
16
|
from kodit.mcp import mcp
|
|
13
17
|
from kodit.middleware import ASGICancelledErrorMiddleware, logging_middleware
|
|
@@ -18,7 +22,7 @@ _sync_scheduler_service: SyncSchedulerService | None = None
|
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
@asynccontextmanager
|
|
21
|
-
async def app_lifespan(_: FastAPI) -> AsyncIterator[
|
|
25
|
+
async def app_lifespan(_: FastAPI) -> AsyncIterator[AppLifespanState]:
|
|
22
26
|
"""Manage application lifespan for auto-indexing and sync."""
|
|
23
27
|
global _auto_indexing_service, _sync_scheduler_service # noqa: PLW0603
|
|
24
28
|
|
|
@@ -42,7 +46,7 @@ async def app_lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
|
42
46
|
interval_seconds=app_context.periodic_sync.interval_seconds
|
|
43
47
|
)
|
|
44
48
|
|
|
45
|
-
yield
|
|
49
|
+
yield AppLifespanState(app_context=app_context)
|
|
46
50
|
|
|
47
51
|
# Stop services
|
|
48
52
|
if _sync_scheduler_service:
|
|
@@ -57,33 +61,49 @@ mcp_http_app = mcp.http_app(transport="http", path="/")
|
|
|
57
61
|
|
|
58
62
|
|
|
59
63
|
@asynccontextmanager
|
|
60
|
-
async def combined_lifespan(app: FastAPI) -> AsyncIterator[
|
|
61
|
-
"""Combine app and MCP lifespans."""
|
|
64
|
+
async def combined_lifespan(app: FastAPI) -> AsyncIterator[AppLifespanState]:
|
|
65
|
+
"""Combine app and MCP lifespans, yielding state from app_lifespan."""
|
|
62
66
|
async with (
|
|
63
|
-
app_lifespan(app),
|
|
67
|
+
app_lifespan(app) as app_state,
|
|
64
68
|
mcp_sse_app.router.lifespan_context(app),
|
|
65
69
|
mcp_http_app.router.lifespan_context(app),
|
|
66
70
|
):
|
|
67
|
-
yield
|
|
71
|
+
yield app_state
|
|
68
72
|
|
|
69
73
|
|
|
70
|
-
app = FastAPI(
|
|
74
|
+
app = FastAPI(
|
|
75
|
+
title="kodit API",
|
|
76
|
+
lifespan=combined_lifespan,
|
|
77
|
+
responses={
|
|
78
|
+
500: {"description": "Internal server error"},
|
|
79
|
+
},
|
|
80
|
+
description="""
|
|
81
|
+
This is the REST API for the Kodit server. Please refer to the
|
|
82
|
+
[Kodit documentation](https://docs.helix.ml/kodit/) for more information.
|
|
83
|
+
""",
|
|
84
|
+
version=version,
|
|
85
|
+
)
|
|
71
86
|
|
|
72
|
-
# Add middleware
|
|
73
|
-
app.middleware("http")(logging_middleware)
|
|
74
|
-
app.add_middleware(CorrelationIdMiddleware)
|
|
87
|
+
# Add middleware. Remember, last runs first. Order is important.
|
|
88
|
+
app.middleware("http")(logging_middleware) # Then always log
|
|
89
|
+
app.add_middleware(CorrelationIdMiddleware) # Add correlation id first.
|
|
75
90
|
|
|
76
91
|
|
|
77
|
-
@app.get("/")
|
|
78
|
-
async def root() ->
|
|
79
|
-
"""
|
|
80
|
-
return
|
|
92
|
+
@app.get("/", include_in_schema=False)
|
|
93
|
+
async def root() -> RedirectResponse:
|
|
94
|
+
"""Redirect to the API documentation."""
|
|
95
|
+
return RedirectResponse(url="/docs")
|
|
81
96
|
|
|
82
97
|
|
|
83
98
|
@app.get("/healthz")
|
|
84
|
-
async def healthz() ->
|
|
99
|
+
async def healthz() -> Response:
|
|
85
100
|
"""Return a health check for the kodit API."""
|
|
86
|
-
return
|
|
101
|
+
return Response(status_code=200)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Include API routers
|
|
105
|
+
app.include_router(indexes_router)
|
|
106
|
+
app.include_router(search_router)
|
|
87
107
|
|
|
88
108
|
|
|
89
109
|
# Add mcp routes last, otherwise previous routes aren't added
|
|
@@ -93,4 +113,4 @@ app.mount("/mcp", mcp_http_app)
|
|
|
93
113
|
|
|
94
114
|
# Wrap the entire app with ASGI middleware after all routes are added to suppress
|
|
95
115
|
# CancelledError at the ASGI level
|
|
96
|
-
|
|
116
|
+
ASGICancelledErrorMiddleware(app)
|
|
@@ -382,3 +382,12 @@ class CodeIndexingApplicationService:
|
|
|
382
382
|
)
|
|
383
383
|
|
|
384
384
|
await reporter.done("text_embeddings")
|
|
385
|
+
|
|
386
|
+
async def delete_index(self, index: Index) -> None:
|
|
387
|
+
"""Delete an index."""
|
|
388
|
+
# Delete the index from the domain
|
|
389
|
+
await self.index_domain_service.delete_index(index)
|
|
390
|
+
|
|
391
|
+
# Delete index from the database
|
|
392
|
+
await self.index_repository.delete(index)
|
|
393
|
+
await self.session.commit()
|
kodit/config.py
CHANGED
|
@@ -5,13 +5,15 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
from functools import wraps
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
8
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
|
+
import structlog
|
|
11
12
|
from pydantic import BaseModel, Field, field_validator
|
|
12
13
|
from pydantic_settings import (
|
|
13
14
|
BaseSettings,
|
|
14
15
|
EnvSettingsSource,
|
|
16
|
+
NoDecode,
|
|
15
17
|
PydanticBaseSettingsSource,
|
|
16
18
|
SettingsConfigDict,
|
|
17
19
|
)
|
|
@@ -189,7 +191,26 @@ class AppContext(BaseSettings):
|
|
|
189
191
|
periodic_sync: PeriodicSyncConfig = Field(
|
|
190
192
|
default=PeriodicSyncConfig(), description="Periodic sync configuration"
|
|
191
193
|
)
|
|
194
|
+
api_keys: Annotated[list[str], NoDecode] = Field(
|
|
195
|
+
default_factory=list,
|
|
196
|
+
description="Comma-separated list of valid API keys (e.g. 'key1,key2')",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@field_validator("api_keys", mode="before")
|
|
200
|
+
@classmethod
|
|
201
|
+
def parse_api_keys(cls, v: Any) -> list[str]:
|
|
202
|
+
"""Parse API keys from CSV format."""
|
|
203
|
+
if v is None:
|
|
204
|
+
return []
|
|
205
|
+
if isinstance(v, list):
|
|
206
|
+
return v
|
|
207
|
+
if isinstance(v, str):
|
|
208
|
+
# Split by comma and strip whitespace
|
|
209
|
+
return [key.strip() for key in v.strip().split(",") if key.strip()]
|
|
210
|
+
return v
|
|
211
|
+
|
|
192
212
|
_db: Database | None = None
|
|
213
|
+
_log = structlog.get_logger(__name__)
|
|
193
214
|
|
|
194
215
|
def model_post_init(self, _: Any) -> None:
|
|
195
216
|
"""Post-initialization hook."""
|
kodit/domain/entities.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Pure domain entities using Pydantic."""
|
|
2
2
|
|
|
3
|
+
import shutil
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from pathlib import Path
|
|
@@ -193,6 +194,10 @@ class WorkingCopy(BaseModel):
|
|
|
193
194
|
for file in self.files:
|
|
194
195
|
file.file_processing_status = FileProcessingStatus.CLEAN
|
|
195
196
|
|
|
197
|
+
def delete(self) -> None:
|
|
198
|
+
"""Delete the working copy."""
|
|
199
|
+
shutil.rmtree(self.cloned_path)
|
|
200
|
+
|
|
196
201
|
|
|
197
202
|
class Source(BaseModel):
|
|
198
203
|
"""Source domain entity."""
|
kodit/domain/protocols.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Pure domain service for Index aggregate operations."""
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
+
from collections import defaultdict
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import structlog
|
|
@@ -104,30 +105,39 @@ class IndexDomainService:
|
|
|
104
105
|
|
|
105
106
|
# Create a set of languages to extract snippets for
|
|
106
107
|
extensions = {file.extension() for file in files}
|
|
107
|
-
|
|
108
|
+
lang_files_map: dict[str, list[domain_entities.File]] = defaultdict(list)
|
|
108
109
|
for ext in extensions:
|
|
109
110
|
try:
|
|
110
|
-
|
|
111
|
+
lang = LanguageMapping.get_language_for_extension(ext)
|
|
112
|
+
lang_files_map[lang].extend(
|
|
113
|
+
file for file in files if file.extension() == ext
|
|
114
|
+
)
|
|
111
115
|
except ValueError as e:
|
|
112
|
-
self.log.
|
|
116
|
+
self.log.debug("Skipping", error=str(e))
|
|
113
117
|
continue
|
|
114
118
|
|
|
119
|
+
self.log.info(
|
|
120
|
+
"Languages to process",
|
|
121
|
+
languages=lang_files_map.keys(),
|
|
122
|
+
)
|
|
123
|
+
|
|
115
124
|
reporter = Reporter(self.log, progress_callback)
|
|
116
125
|
await reporter.start(
|
|
117
126
|
"extract_snippets",
|
|
118
|
-
len(
|
|
127
|
+
len(lang_files_map.keys()),
|
|
119
128
|
"Extracting code snippets...",
|
|
120
129
|
)
|
|
130
|
+
|
|
121
131
|
# Calculate snippets for each language
|
|
122
132
|
slicer = Slicer()
|
|
123
|
-
for i,
|
|
133
|
+
for i, (lang, lang_files) in enumerate(lang_files_map.items()):
|
|
124
134
|
await reporter.step(
|
|
125
135
|
"extract_snippets",
|
|
126
|
-
|
|
127
|
-
len(
|
|
128
|
-
"Extracting code snippets...",
|
|
136
|
+
i,
|
|
137
|
+
len(lang_files_map.keys()),
|
|
138
|
+
f"Extracting code snippets for {lang}...",
|
|
129
139
|
)
|
|
130
|
-
s = slicer.extract_snippets(
|
|
140
|
+
s = slicer.extract_snippets(lang_files, language=lang)
|
|
131
141
|
index.snippets.extend(s)
|
|
132
142
|
|
|
133
143
|
await reporter.done("extract_snippets")
|
|
@@ -280,3 +290,8 @@ class IndexDomainService:
|
|
|
280
290
|
continue
|
|
281
291
|
|
|
282
292
|
return working_copy
|
|
293
|
+
|
|
294
|
+
async def delete_index(self, index: domain_entities.Index) -> None:
|
|
295
|
+
"""Delete an index."""
|
|
296
|
+
# Delete the working copy
|
|
297
|
+
index.source.working_copy.delete()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API infrastructure modules."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API middleware modules."""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""API key-based authentication middleware for the REST API."""
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, HTTPException, Request, Security
|
|
4
|
+
from fastapi.security import APIKeyHeader
|
|
5
|
+
|
|
6
|
+
api_key_header_value = APIKeyHeader(
|
|
7
|
+
name="x-api-key",
|
|
8
|
+
auto_error=False,
|
|
9
|
+
description="API key for authentication (only if set in environmental variables)",
|
|
10
|
+
scheme_name="Header (X-API-KEY)",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def valid_keys(request: Request) -> list[str]:
|
|
15
|
+
"""Get the valid keys from the app context."""
|
|
16
|
+
if not hasattr(request.state, "app_context"):
|
|
17
|
+
raise HTTPException(status_code=500, detail="App context not found")
|
|
18
|
+
app_context = request.state.app_context
|
|
19
|
+
return app_context.api_keys
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def api_key_auth(
|
|
23
|
+
api_key: str = Security(api_key_header_value),
|
|
24
|
+
valid_keys: list[str] = Depends(valid_keys),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Validate the API key."""
|
|
27
|
+
if len(valid_keys) == 0:
|
|
28
|
+
return
|
|
29
|
+
# Check if the API key is valid
|
|
30
|
+
if api_key not in valid_keys:
|
|
31
|
+
raise HTTPException(
|
|
32
|
+
status_code=401,
|
|
33
|
+
detail="Invalid API key",
|
|
34
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""FastAPI dependencies for the REST API."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from typing import Annotated, cast
|
|
5
|
+
|
|
6
|
+
from fastapi import Depends, Request
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
|
|
9
|
+
from kodit.application.factories.code_indexing_factory import (
|
|
10
|
+
create_code_indexing_application_service,
|
|
11
|
+
)
|
|
12
|
+
from kodit.application.services.code_indexing_application_service import (
|
|
13
|
+
CodeIndexingApplicationService,
|
|
14
|
+
)
|
|
15
|
+
from kodit.config import AppContext
|
|
16
|
+
from kodit.domain.services.index_query_service import IndexQueryService
|
|
17
|
+
from kodit.infrastructure.indexing.fusion_service import ReciprocalRankFusionService
|
|
18
|
+
from kodit.infrastructure.sqlalchemy.index_repository import SqlAlchemyIndexRepository
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_app_context(request: Request) -> AppContext:
|
|
22
|
+
"""Get the app context dependency."""
|
|
23
|
+
app_context = cast("AppContext", request.state.app_context)
|
|
24
|
+
if app_context is None:
|
|
25
|
+
raise RuntimeError("App context not initialized")
|
|
26
|
+
return app_context
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
AppContextDep = Annotated[AppContext, Depends(get_app_context)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def get_db_session(
|
|
33
|
+
app_context: AppContextDep,
|
|
34
|
+
) -> AsyncGenerator[AsyncSession, None]:
|
|
35
|
+
"""Get database session dependency."""
|
|
36
|
+
db = await app_context.get_db()
|
|
37
|
+
async with db.session_factory() as session:
|
|
38
|
+
yield session
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
DBSessionDep = Annotated[AsyncSession, Depends(get_db_session)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def get_index_query_service(
|
|
45
|
+
session: DBSessionDep,
|
|
46
|
+
) -> IndexQueryService:
|
|
47
|
+
"""Get index query service dependency."""
|
|
48
|
+
return IndexQueryService(
|
|
49
|
+
index_repository=SqlAlchemyIndexRepository(session=session),
|
|
50
|
+
fusion_service=ReciprocalRankFusionService(),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
IndexQueryServiceDep = Annotated[IndexQueryService, Depends(get_index_query_service)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def get_indexing_app_service(
|
|
58
|
+
app_context: AppContextDep,
|
|
59
|
+
session: DBSessionDep,
|
|
60
|
+
) -> CodeIndexingApplicationService:
|
|
61
|
+
"""Get indexing application service dependency."""
|
|
62
|
+
return create_code_indexing_application_service(
|
|
63
|
+
app_context=app_context,
|
|
64
|
+
session=session,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
IndexingAppServiceDep = Annotated[
|
|
69
|
+
CodeIndexingApplicationService, Depends(get_indexing_app_service)
|
|
70
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Index management router for the REST API."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
|
4
|
+
|
|
5
|
+
from kodit.infrastructure.api.middleware.auth import api_key_auth
|
|
6
|
+
from kodit.infrastructure.api.v1.dependencies import (
|
|
7
|
+
IndexingAppServiceDep,
|
|
8
|
+
IndexQueryServiceDep,
|
|
9
|
+
)
|
|
10
|
+
from kodit.infrastructure.api.v1.schemas.index import (
|
|
11
|
+
IndexAttributes,
|
|
12
|
+
IndexCreateRequest,
|
|
13
|
+
IndexData,
|
|
14
|
+
IndexDetailResponse,
|
|
15
|
+
IndexListResponse,
|
|
16
|
+
IndexResponse,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
router = APIRouter(
|
|
20
|
+
prefix="/api/v1/indexes",
|
|
21
|
+
tags=["indexes"],
|
|
22
|
+
dependencies=[Depends(api_key_auth)],
|
|
23
|
+
responses={
|
|
24
|
+
401: {"description": "Unauthorized"},
|
|
25
|
+
422: {"description": "Invalid request"},
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get("")
|
|
31
|
+
async def list_indexes(
|
|
32
|
+
query_service: IndexQueryServiceDep,
|
|
33
|
+
) -> IndexListResponse:
|
|
34
|
+
"""List all indexes."""
|
|
35
|
+
indexes = await query_service.list_indexes()
|
|
36
|
+
return IndexListResponse(
|
|
37
|
+
data=[
|
|
38
|
+
IndexData(
|
|
39
|
+
type="index",
|
|
40
|
+
id=str(idx.id),
|
|
41
|
+
attributes=IndexAttributes(
|
|
42
|
+
created_at=idx.created_at,
|
|
43
|
+
updated_at=idx.updated_at,
|
|
44
|
+
uri=str(idx.source.working_copy.remote_uri),
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
for idx in indexes
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.post("", status_code=202)
|
|
53
|
+
async def create_index(
|
|
54
|
+
request: IndexCreateRequest,
|
|
55
|
+
background_tasks: BackgroundTasks,
|
|
56
|
+
app_service: IndexingAppServiceDep,
|
|
57
|
+
) -> IndexResponse:
|
|
58
|
+
"""Create a new index and start async indexing."""
|
|
59
|
+
# Create index using the application service
|
|
60
|
+
index = await app_service.create_index_from_uri(request.data.attributes.uri)
|
|
61
|
+
|
|
62
|
+
# Start async indexing in background
|
|
63
|
+
background_tasks.add_task(app_service.run_index, index)
|
|
64
|
+
|
|
65
|
+
return IndexResponse(
|
|
66
|
+
data=IndexData(
|
|
67
|
+
type="index",
|
|
68
|
+
id=str(index.id),
|
|
69
|
+
attributes=IndexAttributes(
|
|
70
|
+
created_at=index.created_at,
|
|
71
|
+
updated_at=index.updated_at,
|
|
72
|
+
uri=str(index.source.working_copy.remote_uri),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@router.get("/{index_id}", responses={404: {"description": "Index not found"}})
|
|
79
|
+
async def get_index(
|
|
80
|
+
index_id: int,
|
|
81
|
+
query_service: IndexQueryServiceDep,
|
|
82
|
+
) -> IndexDetailResponse:
|
|
83
|
+
"""Get index details."""
|
|
84
|
+
index = await query_service.get_index_by_id(index_id)
|
|
85
|
+
if not index:
|
|
86
|
+
raise HTTPException(status_code=404, detail="Index not found")
|
|
87
|
+
|
|
88
|
+
return IndexDetailResponse(
|
|
89
|
+
data=IndexData(
|
|
90
|
+
type="index",
|
|
91
|
+
id=str(index.id),
|
|
92
|
+
attributes=IndexAttributes(
|
|
93
|
+
created_at=index.created_at,
|
|
94
|
+
updated_at=index.updated_at,
|
|
95
|
+
uri=str(index.source.working_copy.remote_uri),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.delete(
|
|
102
|
+
"/{index_id}", status_code=204, responses={404: {"description": "Index not found"}}
|
|
103
|
+
)
|
|
104
|
+
async def delete_index(
|
|
105
|
+
index_id: int,
|
|
106
|
+
query_service: IndexQueryServiceDep,
|
|
107
|
+
app_service: IndexingAppServiceDep,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Delete an index."""
|
|
110
|
+
index = await query_service.get_index_by_id(index_id)
|
|
111
|
+
if not index:
|
|
112
|
+
raise HTTPException(status_code=404, detail="Index not found")
|
|
113
|
+
|
|
114
|
+
await app_service.delete_index(index)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Search router for the REST API."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from kodit.domain.value_objects import MultiSearchRequest, SnippetSearchFilters
|
|
6
|
+
from kodit.infrastructure.api.v1.dependencies import (
|
|
7
|
+
IndexingAppServiceDep,
|
|
8
|
+
)
|
|
9
|
+
from kodit.infrastructure.api.v1.schemas.search import (
|
|
10
|
+
SearchRequest,
|
|
11
|
+
SearchResponse,
|
|
12
|
+
SnippetAttributes,
|
|
13
|
+
SnippetData,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
router = APIRouter(tags=["search"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.post("/api/v1/search")
|
|
20
|
+
async def search_snippets(
|
|
21
|
+
request: SearchRequest,
|
|
22
|
+
app_service: IndexingAppServiceDep,
|
|
23
|
+
) -> SearchResponse:
|
|
24
|
+
"""Search code snippets with filters matching MCP tool."""
|
|
25
|
+
# Convert API request to domain request
|
|
26
|
+
domain_request = MultiSearchRequest(
|
|
27
|
+
keywords=request.data.attributes.keywords,
|
|
28
|
+
code_query=request.data.attributes.code,
|
|
29
|
+
text_query=request.data.attributes.text,
|
|
30
|
+
top_k=request.limit or 10,
|
|
31
|
+
filters=SnippetSearchFilters(
|
|
32
|
+
language=request.languages[0] if request.languages else None,
|
|
33
|
+
author=request.authors[0] if request.authors else None,
|
|
34
|
+
created_after=request.start_date,
|
|
35
|
+
created_before=request.end_date,
|
|
36
|
+
source_repo=request.sources[0] if request.sources else None,
|
|
37
|
+
file_path=request.file_patterns[0] if request.file_patterns else None,
|
|
38
|
+
)
|
|
39
|
+
if any(
|
|
40
|
+
[
|
|
41
|
+
request.languages,
|
|
42
|
+
request.authors,
|
|
43
|
+
request.start_date,
|
|
44
|
+
request.end_date,
|
|
45
|
+
request.sources,
|
|
46
|
+
request.file_patterns,
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
else None,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Execute search using application service
|
|
53
|
+
results = await app_service.search(domain_request)
|
|
54
|
+
|
|
55
|
+
return SearchResponse(
|
|
56
|
+
data=[
|
|
57
|
+
SnippetData(
|
|
58
|
+
type="snippet",
|
|
59
|
+
id=result.id,
|
|
60
|
+
attributes=SnippetAttributes(
|
|
61
|
+
content=result.content,
|
|
62
|
+
created_at=result.created_at,
|
|
63
|
+
updated_at=result.created_at, # Use created_at as fallback
|
|
64
|
+
original_scores=result.original_scores,
|
|
65
|
+
source_uri=result.source_uri,
|
|
66
|
+
relative_path=result.relative_path,
|
|
67
|
+
language=result.language,
|
|
68
|
+
authors=result.authors,
|
|
69
|
+
summary=result.summary,
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
for result in results
|
|
73
|
+
]
|
|
74
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""JSON:API schemas for the REST API."""
|
|
2
|
+
|
|
3
|
+
from .index import (
|
|
4
|
+
IndexCreateRequest,
|
|
5
|
+
IndexDetailResponse,
|
|
6
|
+
IndexListResponse,
|
|
7
|
+
IndexResponse,
|
|
8
|
+
)
|
|
9
|
+
from .search import (
|
|
10
|
+
SearchRequest,
|
|
11
|
+
SearchResponse,
|
|
12
|
+
SearchResponseWithIncluded,
|
|
13
|
+
SnippetDetailResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"IndexCreateRequest",
|
|
18
|
+
"IndexDetailResponse",
|
|
19
|
+
"IndexListResponse",
|
|
20
|
+
"IndexResponse",
|
|
21
|
+
"SearchRequest",
|
|
22
|
+
"SearchResponse",
|
|
23
|
+
"SearchResponseWithIncluded",
|
|
24
|
+
"SnippetDetailResponse",
|
|
25
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""JSON:API schemas for index operations."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IndexAttributes(BaseModel):
|
|
9
|
+
"""Index attributes for JSON:API responses."""
|
|
10
|
+
|
|
11
|
+
created_at: datetime
|
|
12
|
+
updated_at: datetime
|
|
13
|
+
uri: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SnippetData(BaseModel):
|
|
17
|
+
"""Snippet data for JSON:API relationships."""
|
|
18
|
+
|
|
19
|
+
type: str = "snippet"
|
|
20
|
+
id: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class IndexData(BaseModel):
|
|
24
|
+
"""Index data for JSON:API responses."""
|
|
25
|
+
|
|
26
|
+
type: str = "index"
|
|
27
|
+
id: str
|
|
28
|
+
attributes: IndexAttributes
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class IndexResponse(BaseModel):
|
|
32
|
+
"""JSON:API response for single index."""
|
|
33
|
+
|
|
34
|
+
data: IndexData
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class IndexListResponse(BaseModel):
|
|
38
|
+
"""JSON:API response for index list."""
|
|
39
|
+
|
|
40
|
+
data: list[IndexData]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class IndexCreateAttributes(BaseModel):
|
|
44
|
+
"""Attributes for creating an index."""
|
|
45
|
+
|
|
46
|
+
uri: str = Field(..., description="URI of the source to index")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class IndexCreateData(BaseModel):
|
|
50
|
+
"""Data for creating an index."""
|
|
51
|
+
|
|
52
|
+
type: str = "index"
|
|
53
|
+
attributes: IndexCreateAttributes
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class IndexCreateRequest(BaseModel):
|
|
57
|
+
"""JSON:API request for creating an index."""
|
|
58
|
+
|
|
59
|
+
data: IndexCreateData
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AuthorData(BaseModel):
|
|
63
|
+
"""Author data for JSON:API relationships."""
|
|
64
|
+
|
|
65
|
+
type: str = "author"
|
|
66
|
+
id: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AuthorsRelationship(BaseModel):
|
|
70
|
+
"""Authors relationship for JSON:API."""
|
|
71
|
+
|
|
72
|
+
data: list[AuthorData]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class FileRelationships(BaseModel):
|
|
76
|
+
"""File relationships for JSON:API."""
|
|
77
|
+
|
|
78
|
+
authors: AuthorsRelationship
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class FileAttributes(BaseModel):
|
|
82
|
+
"""File attributes for JSON:API included resources."""
|
|
83
|
+
|
|
84
|
+
uri: str
|
|
85
|
+
sha256: str
|
|
86
|
+
mime_type: str
|
|
87
|
+
created_at: datetime
|
|
88
|
+
updated_at: datetime
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AuthorAttributes(BaseModel):
|
|
92
|
+
"""Author attributes for JSON:API included resources."""
|
|
93
|
+
|
|
94
|
+
name: str
|
|
95
|
+
email: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class IndexDetailResponse(BaseModel):
|
|
99
|
+
"""JSON:API response for index details with included resources."""
|
|
100
|
+
|
|
101
|
+
data: IndexData
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""JSON:API schemas for search operations."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SearchFilters(BaseModel):
|
|
9
|
+
"""Search filters for JSON:API requests."""
|
|
10
|
+
|
|
11
|
+
languages: list[str] | None = Field(
|
|
12
|
+
None, description="Programming languages to filter by"
|
|
13
|
+
)
|
|
14
|
+
authors: list[str] | None = Field(None, description="Authors to filter by")
|
|
15
|
+
start_date: datetime | None = Field(
|
|
16
|
+
None, description="Filter snippets created after this date"
|
|
17
|
+
)
|
|
18
|
+
end_date: datetime | None = Field(
|
|
19
|
+
None, description="Filter snippets created before this date"
|
|
20
|
+
)
|
|
21
|
+
sources: list[str] | None = Field(
|
|
22
|
+
None, description="Source repositories to filter by"
|
|
23
|
+
)
|
|
24
|
+
file_patterns: list[str] | None = Field(
|
|
25
|
+
None, description="File path patterns to filter by"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SearchAttributes(BaseModel):
|
|
30
|
+
"""Search attributes for JSON:API requests."""
|
|
31
|
+
|
|
32
|
+
keywords: list[str] | None = Field(None, description="Search keywords")
|
|
33
|
+
code: str | None = Field(None, description="Code search query")
|
|
34
|
+
text: str | None = Field(None, description="Text search query")
|
|
35
|
+
limit: int | None = Field(10, description="Maximum number of results to return")
|
|
36
|
+
filters: SearchFilters | None = Field(None, description="Search filters")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SearchData(BaseModel):
|
|
40
|
+
"""Search data for JSON:API requests."""
|
|
41
|
+
|
|
42
|
+
type: str = "search"
|
|
43
|
+
attributes: SearchAttributes
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SearchRequest(BaseModel):
|
|
47
|
+
"""JSON:API request for searching snippets."""
|
|
48
|
+
|
|
49
|
+
data: SearchData
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def limit(self) -> int | None:
|
|
53
|
+
"""Get the limit from the search request."""
|
|
54
|
+
return self.data.attributes.limit
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def languages(self) -> list[str] | None:
|
|
58
|
+
"""Get the languages from the search request."""
|
|
59
|
+
return (
|
|
60
|
+
self.data.attributes.filters.languages
|
|
61
|
+
if self.data.attributes.filters
|
|
62
|
+
else None
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def authors(self) -> list[str] | None:
|
|
67
|
+
"""Get the authors from the search request."""
|
|
68
|
+
return (
|
|
69
|
+
self.data.attributes.filters.authors
|
|
70
|
+
if self.data.attributes.filters
|
|
71
|
+
else None
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def start_date(self) -> datetime | None:
|
|
76
|
+
"""Get the start date from the search request."""
|
|
77
|
+
return (
|
|
78
|
+
self.data.attributes.filters.start_date
|
|
79
|
+
if self.data.attributes.filters
|
|
80
|
+
else None
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def end_date(self) -> datetime | None:
|
|
85
|
+
"""Get the end date from the search request."""
|
|
86
|
+
return (
|
|
87
|
+
self.data.attributes.filters.end_date
|
|
88
|
+
if self.data.attributes.filters
|
|
89
|
+
else None
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def sources(self) -> list[str] | None:
|
|
94
|
+
"""Get the sources from the search request."""
|
|
95
|
+
return (
|
|
96
|
+
self.data.attributes.filters.sources
|
|
97
|
+
if self.data.attributes.filters
|
|
98
|
+
else None
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def file_patterns(self) -> list[str] | None:
|
|
103
|
+
"""Get the file patterns from the search request."""
|
|
104
|
+
return (
|
|
105
|
+
self.data.attributes.filters.file_patterns
|
|
106
|
+
if self.data.attributes.filters
|
|
107
|
+
else None
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class SnippetAttributes(BaseModel):
|
|
112
|
+
"""Snippet attributes for JSON:API responses."""
|
|
113
|
+
|
|
114
|
+
content: str
|
|
115
|
+
created_at: datetime
|
|
116
|
+
updated_at: datetime
|
|
117
|
+
original_scores: list[float]
|
|
118
|
+
source_uri: str
|
|
119
|
+
relative_path: str
|
|
120
|
+
language: str
|
|
121
|
+
authors: list[str]
|
|
122
|
+
summary: str
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SnippetData(BaseModel):
|
|
126
|
+
"""Snippet data for JSON:API responses."""
|
|
127
|
+
|
|
128
|
+
type: str = "snippet"
|
|
129
|
+
id: int
|
|
130
|
+
attributes: SnippetAttributes
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class SearchResponse(BaseModel):
|
|
134
|
+
"""JSON:API response for search results."""
|
|
135
|
+
|
|
136
|
+
data: list[SnippetData]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class FileAttributes(BaseModel):
|
|
140
|
+
"""File attributes for JSON:API included resources."""
|
|
141
|
+
|
|
142
|
+
uri: str
|
|
143
|
+
sha256: str
|
|
144
|
+
mime_type: str
|
|
145
|
+
created_at: datetime
|
|
146
|
+
updated_at: datetime
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class AuthorData(BaseModel):
|
|
150
|
+
"""Author data for JSON:API relationships."""
|
|
151
|
+
|
|
152
|
+
type: str = "author"
|
|
153
|
+
id: int
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class AuthorsRelationship(BaseModel):
|
|
157
|
+
"""Authors relationship for JSON:API."""
|
|
158
|
+
|
|
159
|
+
data: list[AuthorData]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class FileRelationships(BaseModel):
|
|
163
|
+
"""File relationships for JSON:API."""
|
|
164
|
+
|
|
165
|
+
authors: AuthorsRelationship
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class FileDataWithRelationships(BaseModel):
|
|
169
|
+
"""File data with relationships for JSON:API included resources."""
|
|
170
|
+
|
|
171
|
+
type: str = "file"
|
|
172
|
+
id: int
|
|
173
|
+
attributes: FileAttributes
|
|
174
|
+
relationships: FileRelationships
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class AuthorAttributes(BaseModel):
|
|
178
|
+
"""Author attributes for JSON:API included resources."""
|
|
179
|
+
|
|
180
|
+
name: str
|
|
181
|
+
email: str
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class AuthorDataWithAttributes(BaseModel):
|
|
185
|
+
"""Author data with attributes for JSON:API included resources."""
|
|
186
|
+
|
|
187
|
+
type: str = "author"
|
|
188
|
+
id: int
|
|
189
|
+
attributes: AuthorAttributes
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SearchResponseWithIncluded(BaseModel):
|
|
193
|
+
"""JSON:API response for search results with included resources."""
|
|
194
|
+
|
|
195
|
+
data: list[SnippetData]
|
|
196
|
+
included: list[FileDataWithRelationships | AuthorDataWithAttributes] | None = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class SnippetDetailAttributes(BaseModel):
|
|
200
|
+
"""Snippet detail attributes for JSON:API responses."""
|
|
201
|
+
|
|
202
|
+
created_at: datetime
|
|
203
|
+
updated_at: datetime
|
|
204
|
+
original_content: dict
|
|
205
|
+
summary_content: dict
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class SnippetDetailData(BaseModel):
|
|
209
|
+
"""Snippet detail data for JSON:API responses."""
|
|
210
|
+
|
|
211
|
+
type: str = "snippet"
|
|
212
|
+
id: str
|
|
213
|
+
attributes: SnippetDetailAttributes
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class SnippetDetailResponse(BaseModel):
|
|
217
|
+
"""JSON:API response for snippet details."""
|
|
218
|
+
|
|
219
|
+
data: SnippetDetailData
|
|
@@ -10,10 +10,12 @@ from dataclasses import dataclass, field
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, ClassVar
|
|
12
12
|
|
|
13
|
+
import structlog
|
|
13
14
|
from tree_sitter import Node, Parser, Tree
|
|
14
15
|
from tree_sitter_language_pack import get_language
|
|
15
16
|
|
|
16
17
|
from kodit.domain.entities import File, Snippet
|
|
18
|
+
from kodit.domain.value_objects import LanguageMapping
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
@dataclass
|
|
@@ -145,8 +147,9 @@ class Slicer:
|
|
|
145
147
|
|
|
146
148
|
def __init__(self) -> None:
|
|
147
149
|
"""Initialize an empty slicer."""
|
|
150
|
+
self.log = structlog.get_logger(__name__)
|
|
148
151
|
|
|
149
|
-
def extract_snippets(
|
|
152
|
+
def extract_snippets( # noqa: C901
|
|
150
153
|
self, files: list[File], language: str = "python"
|
|
151
154
|
) -> list[Snippet]:
|
|
152
155
|
"""Extract code snippets from a list of files.
|
|
@@ -170,6 +173,7 @@ class Slicer:
|
|
|
170
173
|
|
|
171
174
|
# Get language configuration
|
|
172
175
|
if language not in LanguageConfig.CONFIGS:
|
|
176
|
+
self.log.debug("Skipping", language=language)
|
|
173
177
|
return []
|
|
174
178
|
|
|
175
179
|
config = LanguageConfig.CONFIGS[language]
|
|
@@ -185,16 +189,20 @@ class Slicer:
|
|
|
185
189
|
# Create mapping from Paths to File objects and extract paths
|
|
186
190
|
path_to_file_map: dict[Path, File] = {}
|
|
187
191
|
file_paths: list[Path] = []
|
|
188
|
-
|
|
189
192
|
for file in files:
|
|
190
193
|
file_path = file.as_path()
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
|
|
195
|
+
# Validate file matches language
|
|
196
|
+
if not self._file_matches_language(file_path.suffix, language):
|
|
197
|
+
raise ValueError(f"File {file_path} does not match language {language}")
|
|
193
198
|
|
|
194
199
|
# Validate file exists
|
|
195
200
|
if not file_path.exists():
|
|
196
201
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
197
202
|
|
|
203
|
+
path_to_file_map[file_path] = file
|
|
204
|
+
file_paths.append(file_path)
|
|
205
|
+
|
|
198
206
|
# Initialize state
|
|
199
207
|
state = AnalyzerState(parser=parser)
|
|
200
208
|
state.files = file_paths
|
|
@@ -209,7 +217,7 @@ class Slicer:
|
|
|
209
217
|
state.asts[file_path] = tree
|
|
210
218
|
except OSError:
|
|
211
219
|
# Skip files that can't be parsed
|
|
212
|
-
|
|
220
|
+
continue
|
|
213
221
|
|
|
214
222
|
# Build indexes
|
|
215
223
|
self._build_definition_and_import_indexes(state, config, language)
|
|
@@ -233,6 +241,19 @@ class Slicer:
|
|
|
233
241
|
|
|
234
242
|
return snippets
|
|
235
243
|
|
|
244
|
+
def _file_matches_language(self, file_extension: str, language: str) -> bool:
|
|
245
|
+
"""Check if a file extension matches the current language."""
|
|
246
|
+
if language not in LanguageConfig.CONFIGS:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
return (
|
|
251
|
+
language == LanguageMapping.get_language_for_extension(file_extension)
|
|
252
|
+
)
|
|
253
|
+
except ValueError:
|
|
254
|
+
# Extension not supported, so it doesn't match any language
|
|
255
|
+
return False
|
|
256
|
+
|
|
236
257
|
def _get_tree_sitter_language_name(self, language: str) -> str:
|
|
237
258
|
"""Map user language names to tree-sitter language names."""
|
|
238
259
|
mapping = {
|
|
@@ -299,19 +320,23 @@ class Slicer:
|
|
|
299
320
|
|
|
300
321
|
def _walk_tree(self, node: Node) -> Generator[Node, None, None]:
|
|
301
322
|
"""Walk the AST tree, yielding all nodes."""
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
323
|
+
# Use a simple queue-based approach to avoid recursion issues
|
|
324
|
+
queue = [node]
|
|
325
|
+
visited: set[int] = set() # Track by node id (memory address)
|
|
326
|
+
|
|
327
|
+
while queue:
|
|
328
|
+
current = queue.pop(0)
|
|
329
|
+
|
|
330
|
+
# Use node id (memory address) as unique identifier to avoid infinite loops
|
|
331
|
+
node_id = id(current)
|
|
332
|
+
if node_id in visited:
|
|
333
|
+
continue
|
|
334
|
+
visited.add(node_id)
|
|
335
|
+
|
|
336
|
+
yield current
|
|
337
|
+
|
|
338
|
+
# Add children to queue
|
|
339
|
+
queue.extend(current.children)
|
|
315
340
|
|
|
316
341
|
def _is_function_definition(self, node: Node, config: dict[str, Any]) -> bool:
|
|
317
342
|
"""Check if node is a function definition."""
|
|
@@ -577,3 +577,32 @@ class SqlAlchemyIndexRepository(IndexRepository):
|
|
|
577
577
|
domain_snippet, index.id
|
|
578
578
|
)
|
|
579
579
|
self._session.add(db_snippet)
|
|
580
|
+
|
|
581
|
+
async def delete(self, index: domain_entities.Index) -> None:
|
|
582
|
+
"""Delete everything related to an index."""
|
|
583
|
+
# Delete all snippets and embeddings
|
|
584
|
+
await self.delete_snippets(index.id)
|
|
585
|
+
|
|
586
|
+
# Delete all author file mappings
|
|
587
|
+
stmt = delete(db_entities.AuthorFileMapping).where(
|
|
588
|
+
db_entities.AuthorFileMapping.file_id.in_(
|
|
589
|
+
[file.id for file in index.source.working_copy.files]
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
await self._session.execute(stmt)
|
|
593
|
+
|
|
594
|
+
# Delete all files
|
|
595
|
+
stmt = delete(db_entities.File).where(
|
|
596
|
+
db_entities.File.source_id == index.source.id
|
|
597
|
+
)
|
|
598
|
+
await self._session.execute(stmt)
|
|
599
|
+
|
|
600
|
+
# Delete the source
|
|
601
|
+
stmt = delete(db_entities.Source).where(
|
|
602
|
+
db_entities.Source.id == index.source.id
|
|
603
|
+
)
|
|
604
|
+
await self._session.execute(stmt)
|
|
605
|
+
|
|
606
|
+
# Delete the index
|
|
607
|
+
stmt = delete(db_entities.Index).where(db_entities.Index.id == index.id)
|
|
608
|
+
await self._session.execute(stmt)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Dump the OpenAPI json schema to a file."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from openapi_markdown.generator import to_markdown # type: ignore[import-untyped]
|
|
9
|
+
from uvicorn.importer import import_from_string
|
|
10
|
+
|
|
11
|
+
parser = argparse.ArgumentParser(prog="dump-openapi.py")
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"app", help='App import string. Eg. "kodit.app:app"', default="kodit.app:app"
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument("--out-dir", help="Output directory", default="docs/reference/api")
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
app = import_from_string(args.app)
|
|
21
|
+
openapi = app.openapi()
|
|
22
|
+
version = openapi.get("openapi", "unknown version")
|
|
23
|
+
|
|
24
|
+
output_json_file = Path(args.out_dir) / "openapi.json"
|
|
25
|
+
|
|
26
|
+
with output_json_file.open("w") as f:
|
|
27
|
+
json.dump(openapi, f, indent=2)
|
|
28
|
+
|
|
29
|
+
output_md_file = Path(args.out_dir) / "index.md"
|
|
30
|
+
templates_dir = Path(args.out_dir) / "templates"
|
|
31
|
+
options: dict[str, Any] = {}
|
|
32
|
+
|
|
33
|
+
to_markdown(str(output_json_file), str(output_md_file), str(templates_dir), options)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
kodit/.gitignore,sha256=ztkjgRwL9Uud1OEi36hGQeDGk3OLK1NfDEO8YqGYy8o,11
|
|
2
2
|
kodit/__init__.py,sha256=aEKHYninUq1yh6jaNfvJBYg-6fenpN132nJt1UU6Jxs,59
|
|
3
|
-
kodit/_version.py,sha256=
|
|
4
|
-
kodit/app.py,sha256
|
|
3
|
+
kodit/_version.py,sha256=NHYomSf4Gkc1-16-B8_7rqOM-xY3AHv8-2MTb6vZqjE,513
|
|
4
|
+
kodit/app.py,sha256=-GY3jdRNKdTLXNoGsEtyJOGdRtxJ_t0HR_Kv24xeOdE,3862
|
|
5
5
|
kodit/cli.py,sha256=ZOS_VzCHGjJRZzZpaVR00QXSPIwRXPYu-pTrbEtlyR0,19328
|
|
6
|
-
kodit/config.py,sha256=
|
|
6
|
+
kodit/config.py,sha256=qk242V-ys2cLRq3vy6SrTi8_HFaDNL-f5i63HIpyh2s,8756
|
|
7
7
|
kodit/database.py,sha256=kI9yBm4uunsgV4-QeVoCBL0wLzU4kYmYv5qZilGnbPE,1740
|
|
8
8
|
kodit/log.py,sha256=r0o7IpNvV-dNW-cTNWu1ouJF71vD9wHYzvqDPzeDYfw,8768
|
|
9
9
|
kodit/mcp.py,sha256=aEcPc8dQiZaR0AswCZZNxcm_rhhUZNsEBimYti0ibSI,7221
|
|
@@ -13,21 +13,33 @@ kodit/application/__init__.py,sha256=mH50wTpgP9dhbKztFsL8Dda9Hi18TSnMVxXtpp4aGOA
|
|
|
13
13
|
kodit/application/factories/__init__.py,sha256=bU5CvEnaBePZ7JbkCOp1MGTNP752bnU2uEqmfy5FdRk,37
|
|
14
14
|
kodit/application/factories/code_indexing_factory.py,sha256=R9f0wsj4-3NJFS5SEt_-OIGR_s_01gJXaL3PkZd8MlU,5911
|
|
15
15
|
kodit/application/services/__init__.py,sha256=p5UQNw-H5sxQvs5Etfte93B3cJ1kKW6DNxK34uFvU1E,38
|
|
16
|
-
kodit/application/services/code_indexing_application_service.py,sha256=
|
|
16
|
+
kodit/application/services/code_indexing_application_service.py,sha256=1OA2nzsKCgD952bi9n7IygyzEymL3j7nX0yGkZaMOW0,14975
|
|
17
17
|
kodit/application/services/sync_scheduler.py,sha256=7ZWM0ACiOrTcsW300m52fTqfWMLFmgRRZ6YUPrgUaUk,4621
|
|
18
18
|
kodit/domain/__init__.py,sha256=TCpg4Xx-oF4mKV91lo4iXqMEfBT1OoRSYnbG-zVWolA,66
|
|
19
|
-
kodit/domain/entities.py,sha256=
|
|
19
|
+
kodit/domain/entities.py,sha256=EY43R0LOTmsaVsZGS3TWz0Bx5kF3Gm-Knqe6kLZaf9Y,8822
|
|
20
20
|
kodit/domain/errors.py,sha256=yIsgCjM_yOFIg8l7l-t7jM8pgeAX4cfPq0owf7iz3DA,106
|
|
21
21
|
kodit/domain/interfaces.py,sha256=Jkd0Ob4qSvhZHI9jRPFQ1n5Cv0SvU-y3Z-HCw2ikc4I,742
|
|
22
|
-
kodit/domain/protocols.py,sha256=
|
|
22
|
+
kodit/domain/protocols.py,sha256=6oeXIHKWMZzpPaZx9mDQ2eOtzj_IeUFu0Dqh2XP9Jmk,1953
|
|
23
23
|
kodit/domain/value_objects.py,sha256=MBZ0WdqQghDmL0Coz_QjPMoVMCiL8pjtpJ5FgaIynoc,17342
|
|
24
24
|
kodit/domain/services/__init__.py,sha256=Q1GhCK_PqKHYwYE4tkwDz5BIyXkJngLBBOHhzvX8nzo,42
|
|
25
25
|
kodit/domain/services/bm25_service.py,sha256=nsfTan3XtDwXuuAu1LUv-6Jukm6qFKVqqCVymjyepZQ,3625
|
|
26
26
|
kodit/domain/services/embedding_service.py,sha256=7drYRC2kjg0WJmo06a2E9N0vDnwInUlBB96twjz2BT8,4526
|
|
27
27
|
kodit/domain/services/enrichment_service.py,sha256=XsXg3nV-KN4rqtC7Zro_ZiZ6RSq-1eA1MG6IDzFGyBA,1316
|
|
28
28
|
kodit/domain/services/index_query_service.py,sha256=02UWfyB_HoHUskunGuHeq5XwQLSWxGSK4OhvxcqIfY0,2022
|
|
29
|
-
kodit/domain/services/index_service.py,sha256=
|
|
29
|
+
kodit/domain/services/index_service.py,sha256=zWKnCW5L8dMJ7mG2I8knXi-vEWweF9pmY-BhbAV6i4o,11340
|
|
30
30
|
kodit/infrastructure/__init__.py,sha256=HzEYIjoXnkz_i_MHO2e0sIVYweUcRnl2RpyBiTbMObU,28
|
|
31
|
+
kodit/infrastructure/api/__init__.py,sha256=U0TSMPpHrlj1zbAtleuZjU3nXGwudyMe-veNBgvODwM,34
|
|
32
|
+
kodit/infrastructure/api/middleware/__init__.py,sha256=6m7eE5k5buboJbuzyX5E9-Tf99yNwFaeJF0f_6HwLyM,30
|
|
33
|
+
kodit/infrastructure/api/middleware/auth.py,sha256=QSnMcMLWvfumqN1iG4ePj2vEZb2Dlsgr-WHptkEkkhE,1064
|
|
34
|
+
kodit/infrastructure/api/v1/__init__.py,sha256=XYv4_9Z6fo69oMvC2mEbtD6DaMqHth29KHUOelmQFwM,121
|
|
35
|
+
kodit/infrastructure/api/v1/dependencies.py,sha256=cmcItOFz2cR1b2elELQGyixULck4UXx450oI0zqEQCY,2164
|
|
36
|
+
kodit/infrastructure/api/v1/routers/__init__.py,sha256=L8hT_SkDzmCXIiWrFQWCkZXQ3UDy_ZMxPr8AIhjSWK0,160
|
|
37
|
+
kodit/infrastructure/api/v1/routers/indexes.py,sha256=EEY6Zru0e6SpA6GscqmeXuGwyIEu4Zw3r38dMEBgIgs,3287
|
|
38
|
+
kodit/infrastructure/api/v1/routers/search.py,sha256=da9YTR6VTzU85_6X3aaZemdTHGCEvcPNeKuMFBgmT_A,2452
|
|
39
|
+
kodit/infrastructure/api/v1/schemas/__init__.py,sha256=_5BVqv4EUi_vvWlAQOE_VfRulUDAF21ZQ7z27y7YOdw,498
|
|
40
|
+
kodit/infrastructure/api/v1/schemas/context.py,sha256=NlsIn9j1R3se7JkGZivS_CUN4gGP5NYaAtkRe3QH6dk,214
|
|
41
|
+
kodit/infrastructure/api/v1/schemas/index.py,sha256=NtL09YtO50h-ddpAFxNf-dyxu_Xi5v3yOpKW0W4xsAM,1950
|
|
42
|
+
kodit/infrastructure/api/v1/schemas/search.py,sha256=CWzg5SIMUJ_4yM-ZfgSLWCanMxov6AyGgQQcOMkRlGw,5618
|
|
31
43
|
kodit/infrastructure/bm25/__init__.py,sha256=DmGbrEO34FOJy4e685BbyxLA7gPW1eqs2gAxsp6JOuM,34
|
|
32
44
|
kodit/infrastructure/bm25/bm25_factory.py,sha256=I4eo7qRslnyXIRkBf-StZ5ga2Evrr5J5YFocTChFD3g,884
|
|
33
45
|
kodit/infrastructure/bm25/local_bm25_repository.py,sha256=rDx8orGhg38n0zSpybEt5QLOWHY2Yt5IxIf9UtlhXXU,4629
|
|
@@ -61,11 +73,11 @@ kodit/infrastructure/mappers/__init__.py,sha256=QPHOjNreXmBPPovZ6elnYFS0vD-IsmrG
|
|
|
61
73
|
kodit/infrastructure/mappers/index_mapper.py,sha256=ZSfu8kjTaa8_UY0nTqr4b02NS3VrjqZYkduCN71AL2g,12743
|
|
62
74
|
kodit/infrastructure/slicing/__init__.py,sha256=x7cjvHA9Ay2weUYE_dpdAaPaStp20M-4U2b5MLgT5KM,37
|
|
63
75
|
kodit/infrastructure/slicing/language_detection_service.py,sha256=JGJXrq9bLyfnisWJXeP7y1jbZMmKAISdPBlRBCosUcE,684
|
|
64
|
-
kodit/infrastructure/slicing/slicer.py,sha256=
|
|
76
|
+
kodit/infrastructure/slicing/slicer.py,sha256=GOqJykd00waOTO1WJHyE5KUgJ2RLx2rOQ7M7T_u5LLg,35600
|
|
65
77
|
kodit/infrastructure/sqlalchemy/__init__.py,sha256=UXPMSF_hgWaqr86cawRVqM8XdVNumQyyK5B8B97GnlA,33
|
|
66
78
|
kodit/infrastructure/sqlalchemy/embedding_repository.py,sha256=dC2Wzj_zQiWExwfScE1LAGiiyxPyg0YepwyLOgDwcs4,7905
|
|
67
79
|
kodit/infrastructure/sqlalchemy/entities.py,sha256=Dmh0z-dMI0wfMAPpf62kxU4md6NUH9P5Nx1QSTITOfg,5961
|
|
68
|
-
kodit/infrastructure/sqlalchemy/index_repository.py,sha256=
|
|
80
|
+
kodit/infrastructure/sqlalchemy/index_repository.py,sha256=UlDH6Qluuat1T0GaATko29fwQPAaUh2WLWiGurBW42w,23598
|
|
69
81
|
kodit/infrastructure/ui/__init__.py,sha256=CzbLOBwIZ6B6iAHEd1L8cIBydCj-n_kobxJAhz2I9_Y,32
|
|
70
82
|
kodit/infrastructure/ui/progress.py,sha256=LmEAQKWWSspqb0fOwruyxBfzBG7gmHd6z1iBco1d7_4,4823
|
|
71
83
|
kodit/infrastructure/ui/spinner.py,sha256=GcP115qtR0VEnGfMEtsGoAUpRzVGUSfiUXfoJJERngA,2357
|
|
@@ -81,9 +93,10 @@ kodit/migrations/versions/9e53ea8bb3b0_add_authors.py,sha256=a32Zm8KUQyiiLkjKNPY
|
|
|
81
93
|
kodit/migrations/versions/__init__.py,sha256=9-lHzptItTzq_fomdIRBegQNm4Znx6pVjwD4MiqRIdo,36
|
|
82
94
|
kodit/migrations/versions/c3f5137d30f5_index_all_the_things.py,sha256=r7ukmJ_axXLAWewYx-F1fEmZ4JbtFd37i7cSb0tq3y0,1722
|
|
83
95
|
kodit/utils/__init__.py,sha256=DPEB1i8evnLF4Ns3huuAYg-0pKBFKUFuiDzOKG9r-sw,33
|
|
96
|
+
kodit/utils/dump_openapi.py,sha256=_1wIrJLA-RlvgZ_8tMzDF0Kdee6UaPp_9DQjfHoM-xc,1062
|
|
84
97
|
kodit/utils/path_utils.py,sha256=thK6YGGNvQThdBaCYCCeCvS1L8x-lwl3AoGht2jnjGw,1645
|
|
85
|
-
kodit-0.3.
|
|
86
|
-
kodit-0.3.
|
|
87
|
-
kodit-0.3.
|
|
88
|
-
kodit-0.3.
|
|
89
|
-
kodit-0.3.
|
|
98
|
+
kodit-0.3.11.dist-info/METADATA,sha256=B06xZGWPvpr5EXz_4GjzeYKeOk6J7lFQg6o2zkgLGgc,6974
|
|
99
|
+
kodit-0.3.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
100
|
+
kodit-0.3.11.dist-info/entry_points.txt,sha256=hoTn-1aKyTItjnY91fnO-rV5uaWQLQ-Vi7V5et2IbHY,40
|
|
101
|
+
kodit-0.3.11.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
102
|
+
kodit-0.3.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|