kodit 0.3.10__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 CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.3.10'
21
- __version_tuple__ = version_tuple = (0, 3, 10)
20
+ __version__ = version = '0.3.11'
21
+ __version_tuple__ = version_tuple = (0, 3, 11)
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[None]:
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[None]:
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(title="kodit API", lifespan=combined_lifespan)
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() -> dict[str, str]:
79
- """Return a welcome message for the kodit API."""
80
- return {"message": "Hello, World!"}
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() -> dict[str, str]:
99
+ async def healthz() -> Response:
85
100
  """Return a health check for the kodit API."""
86
- return {"status": "ok"}
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
- app = ASGICancelledErrorMiddleware(app) # type: ignore[assignment]
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
@@ -24,6 +24,10 @@ class IndexRepository(Protocol):
24
24
  """Get an index by ID."""
25
25
  ...
26
26
 
27
+ async def delete(self, index: Index) -> None:
28
+ """Delete an index."""
29
+ ...
30
+
27
31
  async def all(self) -> list[Index]:
28
32
  """List all indexes."""
29
33
  ...
@@ -290,3 +290,8 @@ class IndexDomainService:
290
290
  continue
291
291
 
292
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,5 @@
1
+ """API v1 modules."""
2
+
3
+ from .routers import indexes_router, search_router
4
+
5
+ __all__ = ["indexes_router", "search_router"]
@@ -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,6 @@
1
+ """API v1 routers."""
2
+
3
+ from .indexes import router as indexes_router
4
+ from .search import router as search_router
5
+
6
+ __all__ = ["indexes_router", "search_router"]
@@ -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,11 @@
1
+ """Schemas for the application context."""
2
+
3
+ from typing import TypedDict
4
+
5
+ from kodit.config import AppContext
6
+
7
+
8
+ class AppLifespanState(TypedDict):
9
+ """Application lifespan state."""
10
+
11
+ app_context: AppContext
@@ -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
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kodit
3
- Version: 0.3.10
3
+ Version: 0.3.11
4
4
  Summary: Code indexing for better AI code generation
5
5
  Project-URL: Homepage, https://docs.helixml.tech/kodit/
6
6
  Project-URL: Documentation, https://docs.helixml.tech/kodit/
@@ -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=8f9qESpn_-snEACtTM18TNc6AEJLWU6rpXlL21ijVSc,513
4
- kodit/app.py,sha256=3_smkoioIQEYtRLIGHDtgGkmkP6Movd5CygQEMOStP8,3043
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=Il_eeyg7s83QF5lmiFB6qX6pmpiqCWncHtPgPcdA4xA,8063
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=SuIuyBoSPOSjj5VaXIbxcYqaTEeMuUCu7w1tO8orrOY,14656
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=Mcku1Wmk3Xl3YJhY65_RoiLeffOLKOHI0uCAXWJrmvQ,8698
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=L94FwChhCoj39xicaVrK2UFhFbPzi5JEXW_KmgODsLA,1859
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=r6skJzN0Hp_lJNaUjQSpHSRETCHNnfmJWH4X6A2-rFE,11159
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
@@ -65,7 +77,7 @@ kodit/infrastructure/slicing/slicer.py,sha256=GOqJykd00waOTO1WJHyE5KUgJ2RLx2rOQ7
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=fMnR3OxZN37dtp1M2Menf0xy31GjK1iv_0zn7EvRKYs,22575
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.10.dist-info/METADATA,sha256=SUpOyQI6dJQnd9Mza8Dml3A4zOeoM_XV6Q6cac2k3rw,6974
86
- kodit-0.3.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
87
- kodit-0.3.10.dist-info/entry_points.txt,sha256=hoTn-1aKyTItjnY91fnO-rV5uaWQLQ-Vi7V5et2IbHY,40
88
- kodit-0.3.10.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
89
- kodit-0.3.10.dist-info/RECORD,,
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