kodit 0.2.5__py3-none-any.whl → 0.2.7__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.2.5'
21
- __version_tuple__ = version_tuple = (0, 2, 5)
20
+ __version__ = version = '0.2.7'
21
+ __version_tuple__ = version_tuple = (0, 2, 7)
@@ -1,6 +1,7 @@
1
1
  """Application service for indexing operations."""
2
2
 
3
3
  import structlog
4
+ from sqlalchemy.ext.asyncio import AsyncSession
4
5
 
5
6
  from kodit.application.commands.snippet_commands import CreateIndexSnippetsCommand
6
7
  from kodit.application.services.snippet_application_service import (
@@ -8,6 +9,7 @@ from kodit.application.services.snippet_application_service import (
8
9
  )
9
10
  from kodit.domain.entities import Snippet
10
11
  from kodit.domain.enums import SnippetExtractionStrategy
12
+ from kodit.domain.errors import EmptySourceError
11
13
  from kodit.domain.interfaces import ProgressCallback
12
14
  from kodit.domain.services.bm25_service import BM25DomainService
13
15
  from kodit.domain.services.embedding_service import EmbeddingDomainService
@@ -51,6 +53,7 @@ class IndexingApplicationService:
51
53
  text_search_service: EmbeddingDomainService,
52
54
  enrichment_service: EnrichmentDomainService,
53
55
  snippet_application_service: SnippetApplicationService,
56
+ session: AsyncSession,
54
57
  ) -> None:
55
58
  """Initialize the indexing application service.
56
59
 
@@ -62,11 +65,13 @@ class IndexingApplicationService:
62
65
  text_search_service: The text search domain service.
63
66
  enrichment_service: The enrichment domain service.
64
67
  snippet_application_service: The snippet application service.
68
+ session: The database session for transaction management.
65
69
 
66
70
  """
67
71
  self.indexing_domain_service = indexing_domain_service
68
72
  self.source_service = source_service
69
73
  self.snippet_application_service = snippet_application_service
74
+ self.session = session
70
75
  self.log = structlog.get_logger(__name__)
71
76
  self.bm25_service = bm25_service
72
77
  self.code_search_service = code_search_service
@@ -93,7 +98,12 @@ class IndexingApplicationService:
93
98
 
94
99
  # Create the index
95
100
  request = IndexCreateRequest(source_id=source.id)
96
- return await self.indexing_domain_service.create_index(request)
101
+ index_view = await self.indexing_domain_service.create_index(request)
102
+
103
+ # Commit the index creation
104
+ await self.session.commit()
105
+
106
+ return index_view
97
107
 
98
108
  async def list_indexes(self) -> list[IndexView]:
99
109
  """List all available indexes with their details.
@@ -125,7 +135,7 @@ class IndexingApplicationService:
125
135
  progress_callback: Optional progress callback for reporting progress.
126
136
 
127
137
  Raises:
128
- ValueError: If the index doesn't exist.
138
+ ValueError: If the index doesn't exist or no indexable snippets are found.
129
139
 
130
140
  """
131
141
  log_event("kodit.index.run")
@@ -138,8 +148,11 @@ class IndexingApplicationService:
138
148
 
139
149
  # Delete old snippets so we don't duplicate
140
150
  await self.indexing_domain_service.delete_all_snippets(index.id)
151
+ # Commit the deletion
152
+ await self.session.commit()
141
153
 
142
154
  # Create snippets for supported file types using the snippet application service
155
+ # (snippet_application_service handles its own commits)
143
156
  self.log.info("Creating snippets for files", index_id=index.id)
144
157
  command = CreateIndexSnippetsCommand(
145
158
  index_id=index.id, strategy=SnippetExtractionStrategy.METHOD_BASED
@@ -150,6 +163,11 @@ class IndexingApplicationService:
150
163
 
151
164
  snippets = await self.indexing_domain_service.get_snippets_for_index(index.id)
152
165
 
166
+ # Check if any snippets were extracted
167
+ if not snippets:
168
+ msg = f"No indexable snippets found for index {index.id}"
169
+ raise EmptySourceError(msg)
170
+
153
171
  # Create BM25 index
154
172
  self.log.info("Creating keyword index")
155
173
  reporter = Reporter(self.log, progress_callback)
@@ -184,6 +202,8 @@ class IndexingApplicationService:
184
202
 
185
203
  # Update index timestamp
186
204
  await self.indexing_domain_service.update_index_timestamp(index.id)
205
+ # Commit the timestamp update
206
+ await self.session.commit()
187
207
 
188
208
  async def _create_bm25_index(
189
209
  self, snippets: list[Snippet], progress_callback: ProgressCallback | None = None
@@ -264,6 +284,10 @@ class IndexingApplicationService:
264
284
  "enrichment", processed, len(snippets), "Enriching snippets..."
265
285
  )
266
286
 
287
+ # Commit all snippet content updates as a single transaction
288
+ if enriched_contents:
289
+ await self.session.commit()
290
+
267
291
  await reporter.done("enrichment")
268
292
 
269
293
  async def _create_text_embeddings(
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
  from typing import Any
5
5
 
6
6
  import structlog
7
+ from sqlalchemy.ext.asyncio import AsyncSession
7
8
 
8
9
  from kodit.application.commands.snippet_commands import (
9
10
  CreateIndexSnippetsCommand,
@@ -28,6 +29,7 @@ class SnippetApplicationService:
28
29
  snippet_extraction_service: SnippetExtractionDomainService,
29
30
  snippet_repository: SnippetRepository,
30
31
  file_repository: FileRepository,
32
+ session: AsyncSession,
31
33
  ) -> None:
32
34
  """Initialize the snippet application service.
33
35
 
@@ -35,11 +37,13 @@ class SnippetApplicationService:
35
37
  snippet_extraction_service: Domain service for snippet extraction
36
38
  snippet_repository: Repository for snippet persistence
37
39
  file_repository: Repository for file operations
40
+ session: The database session for transaction management
38
41
 
39
42
  """
40
43
  self.snippet_extraction_service = snippet_extraction_service
41
44
  self.snippet_repository = snippet_repository
42
45
  self.file_repository = file_repository
46
+ self.session = session
43
47
  self.log = structlog.get_logger(__name__)
44
48
 
45
49
  async def extract_snippets_from_file(
@@ -140,4 +144,6 @@ class SnippetApplicationService:
140
144
  message=f"Processing {file.cloned_path}...",
141
145
  )
142
146
 
147
+ # Commit all snippet creations in a single transaction
148
+ await self.session.commit()
143
149
  await reporter.done("create_snippets")
kodit/cli.py CHANGED
@@ -19,6 +19,7 @@ from kodit.config import (
19
19
  with_app_context,
20
20
  with_session,
21
21
  )
22
+ from kodit.domain.errors import EmptySourceError
22
23
  from kodit.domain.services.source_service import SourceService
23
24
  from kodit.domain.value_objects import MultiSearchRequest
24
25
  from kodit.infrastructure.indexing.indexing_factory import (
@@ -58,6 +59,7 @@ def create_snippet_application_service(
58
59
  snippet_extraction_service=snippet_extraction_service,
59
60
  snippet_repository=snippet_repository,
60
61
  file_repository=file_repository,
62
+ session=session,
61
63
  )
62
64
 
63
65
 
@@ -100,6 +102,7 @@ async def index(
100
102
  sources: list[str],
101
103
  ) -> None:
102
104
  """List indexes, or index data sources."""
105
+ log = structlog.get_logger(__name__)
103
106
  source_service = SourceService(
104
107
  clone_dir=app_context.get_clone_dir(),
105
108
  session_factory=lambda: session,
@@ -152,7 +155,17 @@ async def index(
152
155
 
153
156
  # Create a new progress callback for the indexing operations
154
157
  indexing_progress_callback = create_multi_stage_progress_callback()
155
- await service.run_index(index.id, indexing_progress_callback)
158
+ try:
159
+ await service.run_index(index.id, indexing_progress_callback)
160
+ except EmptySourceError as e:
161
+ log.exception("Empty source error", error=e)
162
+ msg = f"""{e}. This could mean:
163
+ • The repository contains no supported file types
164
+ • All files are excluded by ignore patterns
165
+ • The files contain no extractable code snippets
166
+ Please check the repository contents and try again.
167
+ """
168
+ click.echo(msg)
156
169
 
157
170
 
158
171
  @cli.group()
kodit/domain/errors.py ADDED
@@ -0,0 +1,5 @@
1
+ """Domain errors."""
2
+
3
+
4
+ class EmptySourceError(ValueError):
5
+ """Error raised when a source is empty."""
@@ -49,7 +49,7 @@ class SourceService:
49
49
  """Create a source."""
50
50
  async with self._session_factory() as session:
51
51
  repo = SqlAlchemySourceRepository(session)
52
- git_factory, folder_factory = self._build_factories(repo)
52
+ git_factory, folder_factory = self._build_factories(repo, session)
53
53
 
54
54
  if is_valid_clone_target(uri_or_path_like):
55
55
  source = await git_factory.create(uri_or_path_like, progress_callback)
@@ -60,17 +60,18 @@ class SourceService:
60
60
  else:
61
61
  raise ValueError(f"Unsupported source: {uri_or_path_like}")
62
62
 
63
- await session.commit() # one commit for all work
63
+ # Factories handle their own commits now
64
64
  return source
65
65
 
66
66
  def _build_factories(
67
- self, repository: SourceRepository
67
+ self, repository: SourceRepository, session: AsyncSession
68
68
  ) -> tuple[GitSourceFactory, FolderSourceFactory]:
69
69
  # Git-specific collaborators
70
70
  git_wc = GitWorkingCopyProvider(self.clone_dir)
71
71
  git_factory = GitSourceFactory(
72
72
  repository=repository,
73
73
  working_copy=git_wc,
74
+ session=session,
74
75
  )
75
76
 
76
77
  # Folder-specific collaborators
@@ -78,6 +79,7 @@ class SourceService:
78
79
  folder_factory = FolderSourceFactory(
79
80
  repository=repository,
80
81
  working_copy=fold_wc,
82
+ session=session,
81
83
  )
82
84
 
83
85
  return git_factory, folder_factory
@@ -3,6 +3,7 @@
3
3
  from pathlib import Path
4
4
 
5
5
  import structlog
6
+ from sqlalchemy.ext.asyncio import AsyncSession
6
7
 
7
8
  from kodit.domain.entities import AuthorFileMapping, Source, SourceType
8
9
  from kodit.domain.interfaces import NullProgressCallback, ProgressCallback
@@ -22,6 +23,7 @@ class FolderSourceFactory:
22
23
  self,
23
24
  repository: SourceRepository,
24
25
  working_copy: FolderWorkingCopyProvider,
26
+ session: AsyncSession,
25
27
  ) -> None:
26
28
  """Initialize the source factory."""
27
29
  self.log = structlog.get_logger(__name__)
@@ -29,6 +31,7 @@ class FolderSourceFactory:
29
31
  self.working_copy = working_copy
30
32
  self.metadata_extractor = FolderFileMetadataExtractor()
31
33
  self.author_extractor = NoOpAuthorExtractor()
34
+ self.session = session
32
35
 
33
36
  async def create(
34
37
  self, uri: str, progress_callback: ProgressCallback | None = None
@@ -55,7 +58,7 @@ class FolderSourceFactory:
55
58
  clone_path = await self.working_copy.prepare(directory.as_uri())
56
59
 
57
60
  # Create source record
58
- source = await self.repository.create_source(
61
+ source = await self.repository.save(
59
62
  Source(
60
63
  uri=directory.as_uri(),
61
64
  cloned_path=str(clone_path),
@@ -63,12 +66,18 @@ class FolderSourceFactory:
63
66
  )
64
67
  )
65
68
 
69
+ # Commit source creation so we get an ID for foreign key relationships
70
+ await self.session.commit()
71
+
66
72
  # Get all files to process
67
73
  files = [f for f in clone_path.rglob("*") if f.is_file()]
68
74
 
69
75
  # Process files
70
76
  await self._process_files(source, files, progress_callback)
71
77
 
78
+ # Commit file processing
79
+ await self.session.commit()
80
+
72
81
  return source
73
82
 
74
83
  async def _process_files(
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
 
6
6
  import git
7
7
  import structlog
8
+ from sqlalchemy.ext.asyncio import AsyncSession
8
9
 
9
10
  from kodit.domain.entities import AuthorFileMapping, Source, SourceType
10
11
  from kodit.domain.interfaces import NullProgressCallback, ProgressCallback
@@ -16,6 +17,7 @@ from kodit.infrastructure.cloning.metadata import (
16
17
  GitAuthorExtractor,
17
18
  GitFileMetadataExtractor,
18
19
  )
20
+ from kodit.infrastructure.git.git_utils import sanitize_git_url
19
21
  from kodit.infrastructure.ignore.ignore_pattern_provider import GitIgnorePatternProvider
20
22
 
21
23
 
@@ -26,6 +28,7 @@ class GitSourceFactory:
26
28
  self,
27
29
  repository: SourceRepository,
28
30
  working_copy: GitWorkingCopyProvider,
31
+ session: AsyncSession,
29
32
  ) -> None:
30
33
  """Initialize the source factory."""
31
34
  self.log = structlog.get_logger(__name__)
@@ -33,6 +36,7 @@ class GitSourceFactory:
33
36
  self.working_copy = working_copy
34
37
  self.metadata_extractor = GitFileMetadataExtractor()
35
38
  self.author_extractor = GitAuthorExtractor(repository)
39
+ self.session = session
36
40
 
37
41
  async def create(
38
42
  self, uri: str, progress_callback: ProgressCallback | None = None
@@ -43,33 +47,41 @@ class GitSourceFactory:
43
47
  progress_callback = NullProgressCallback()
44
48
 
45
49
  # Normalize the URI
46
- self.log.debug("Normalising git uri", uri=uri)
50
+ # Never log the raw URI in production
51
+ self.log.debug("Normalising git uri", uri="[REDACTED]" + uri[-4:])
47
52
  with tempfile.TemporaryDirectory() as temp_dir:
48
53
  git.Repo.clone_from(uri, temp_dir)
49
54
  remote = git.Repo(temp_dir).remote()
50
55
  uri = remote.url
51
56
 
57
+ # Sanitize the URI to remove any credentials
58
+ sanitized_uri = sanitize_git_url(uri)
59
+ self.log.debug("Sanitized git uri", sanitized_uri=sanitized_uri)
60
+
52
61
  # Check if source already exists
53
- self.log.debug("Checking if source already exists", uri=uri)
54
- source = await self.repository.get_by_uri(uri)
62
+ self.log.debug("Checking if source already exists", uri=sanitized_uri)
63
+ source = await self.repository.get_by_uri(sanitized_uri)
55
64
 
56
65
  if source:
57
66
  self.log.info("Source already exists, reusing...", source_id=source.id)
58
67
  return source
59
68
 
60
- # Prepare working copy
69
+ # Prepare working copy (use original URI for cloning, sanitized for storage)
61
70
  clone_path = await self.working_copy.prepare(uri)
62
71
 
63
72
  # Create source record
64
- self.log.debug("Creating source", uri=uri, clone_path=str(clone_path))
65
- source = await self.repository.create_source(
73
+ self.log.debug("Creating source", uri=sanitized_uri, clone_path=str(clone_path))
74
+ source = await self.repository.save(
66
75
  Source(
67
- uri=uri,
76
+ uri=sanitized_uri,
68
77
  cloned_path=str(clone_path),
69
78
  source_type=SourceType.GIT,
70
79
  )
71
80
  )
72
81
 
82
+ # Commit source creation so we get an ID for foreign key relationships
83
+ await self.session.commit()
84
+
73
85
  # Get files to process using ignore patterns
74
86
  ignore_provider = GitIgnorePatternProvider(clone_path)
75
87
  ignore_service = IgnoreService(ignore_provider)
@@ -83,6 +95,9 @@ class GitSourceFactory:
83
95
  self.log.info("Inspecting files", source_id=source.id, num_files=len(files))
84
96
  await self._process_files(source, files, progress_callback)
85
97
 
98
+ # Commit file processing
99
+ await self.session.commit()
100
+
86
101
  return source
87
102
 
88
103
  async def _process_files(
@@ -111,6 +126,11 @@ class GitSourceFactory:
111
126
 
112
127
  # Extract authors
113
128
  authors = await self.author_extractor.extract(path, source)
129
+
130
+ # Commit authors so they get IDs before creating mappings
131
+ if authors:
132
+ await self.session.commit()
133
+
114
134
  for author in authors:
115
135
  await self.repository.upsert_author_file_mapping(
116
136
  AuthorFileMapping(
@@ -5,6 +5,8 @@ from pathlib import Path
5
5
  import git
6
6
  import structlog
7
7
 
8
+ from kodit.infrastructure.git.git_utils import sanitize_git_url
9
+
8
10
 
9
11
  class GitWorkingCopyProvider:
10
12
  """Working copy provider for git-based sources."""
@@ -16,17 +18,23 @@ class GitWorkingCopyProvider:
16
18
 
17
19
  async def prepare(self, uri: str) -> Path:
18
20
  """Prepare a Git working copy."""
19
- # Create a unique directory name for the clone
20
- clone_path = self.clone_dir / uri.replace("/", "_").replace(":", "_")
21
+ # Sanitize the URI for directory name to prevent credential leaks
22
+ sanitized_uri = sanitize_git_url(uri)
23
+
24
+ # Create a unique directory name for the clone using the sanitized URI
25
+ clone_path = self.clone_dir / sanitized_uri.replace("/", "_").replace(":", "_")
21
26
  clone_path.mkdir(parents=True, exist_ok=True)
22
27
 
23
28
  try:
24
- self.log.info("Cloning repository", uri=uri, clone_path=str(clone_path))
29
+ self.log.info(
30
+ "Cloning repository", uri=sanitized_uri, clone_path=str(clone_path)
31
+ )
32
+ # Use the original URI for cloning (with credentials if present)
25
33
  git.Repo.clone_from(uri, clone_path)
26
34
  except git.GitCommandError as e:
27
35
  if "already exists and is not an empty directory" not in str(e):
28
36
  msg = f"Failed to clone repository: {e}"
29
37
  raise ValueError(msg) from e
30
- self.log.info("Repository already exists, reusing...", uri=uri)
38
+ self.log.info("Repository already exists, reusing...", uri=sanitized_uri)
31
39
 
32
40
  return clone_path
@@ -1,6 +1,7 @@
1
1
  """Git utilities for infrastructure operations."""
2
2
 
3
3
  import tempfile
4
+ from urllib.parse import urlparse, urlunparse
4
5
 
5
6
  import git
6
7
 
@@ -22,3 +23,65 @@ def is_valid_clone_target(target: str) -> bool:
22
23
  return False
23
24
  else:
24
25
  return True
26
+
27
+
28
+ def sanitize_git_url(url: str) -> str:
29
+ """Remove credentials from a git URL while preserving the rest of the URL structure.
30
+
31
+ This function handles various git URL formats:
32
+ - HTTPS URLs with username:password@host
33
+ - HTTPS URLs with username@host (no password)
34
+ - SSH URLs (left unchanged)
35
+ - File URLs (left unchanged)
36
+
37
+ Args:
38
+ url: The git URL that may contain credentials.
39
+
40
+ Returns:
41
+ The sanitized URL with credentials removed.
42
+
43
+ Examples:
44
+ >>> sanitize_git_url("https://phil:token@dev.azure.com/org/project/_git/repo")
45
+ "https://dev.azure.com/org/project/_git/repo"
46
+ >>> sanitize_git_url("https://username@github.com/user/repo.git")
47
+ "https://github.com/user/repo.git"
48
+ >>> sanitize_git_url("git@github.com:user/repo.git")
49
+ "git@github.com:user/repo.git"
50
+
51
+ """
52
+ # Handle SSH URLs (they don't have credentials in the URL format)
53
+ if url.startswith(("git@", "ssh://")):
54
+ return url
55
+
56
+ # Handle file URLs
57
+ if url.startswith("file://"):
58
+ return url
59
+
60
+ try:
61
+ # Parse the URL
62
+ parsed = urlparse(url)
63
+
64
+ # If there are no credentials, return the URL as-is
65
+ if not parsed.username:
66
+ return url
67
+
68
+ # Reconstruct the URL without credentials
69
+ # Keep scheme, netloc (without username/password), path, params, query, fragment
70
+ sanitized_netloc = parsed.hostname
71
+ if parsed.port:
72
+ sanitized_netloc = f"{parsed.hostname}:{parsed.port}"
73
+
74
+ return urlunparse(
75
+ (
76
+ parsed.scheme,
77
+ sanitized_netloc,
78
+ parsed.path,
79
+ parsed.params,
80
+ parsed.query,
81
+ parsed.fragment,
82
+ )
83
+ )
84
+
85
+ except Exception: # noqa: BLE001
86
+ # If URL parsing fails, return the original URL
87
+ return url
@@ -42,7 +42,6 @@ class SQLAlchemyIndexRepository(IndexRepository):
42
42
 
43
43
  index = Index(source_id=source_id)
44
44
  self.session.add(index)
45
- await self.session.commit()
46
45
 
47
46
  # Get source for the view
48
47
  source_query = select(Source).where(Source.id == source_id)
@@ -158,7 +157,6 @@ class SQLAlchemyIndexRepository(IndexRepository):
158
157
 
159
158
  if index:
160
159
  index.updated_at = datetime.now(UTC)
161
- await self.session.commit()
162
160
 
163
161
  async def delete_all_snippets(self, index_id: int) -> None:
164
162
  """Delete all snippets for an index.
@@ -178,7 +176,6 @@ class SQLAlchemyIndexRepository(IndexRepository):
178
176
  # Now delete the snippets
179
177
  query = delete(Snippet).where(Snippet.index_id == index_id)
180
178
  await self.session.execute(query)
181
- await self.session.commit()
182
179
 
183
180
  async def get_snippets_for_index(self, index_id: int) -> list[Snippet]:
184
181
  """Get all snippets for an index.
@@ -207,7 +204,6 @@ class SQLAlchemyIndexRepository(IndexRepository):
207
204
  content=snippet["content"],
208
205
  )
209
206
  self.session.add(db_snippet)
210
- await self.session.commit()
211
207
 
212
208
  async def update_snippet_content(self, snippet_id: int, content: str) -> None:
213
209
  """Update the content of an existing snippet.
@@ -224,7 +220,6 @@ class SQLAlchemyIndexRepository(IndexRepository):
224
220
  if snippet:
225
221
  snippet.content = content
226
222
  # SQLAlchemy will automatically track this change
227
- await self.session.commit()
228
223
 
229
224
  async def list_snippets_by_ids(self, ids: list[int]) -> list[tuple[dict, dict]]:
230
225
  """List snippets by IDs.
@@ -53,6 +53,7 @@ def create_snippet_application_service(
53
53
  snippet_extraction_service=snippet_extraction_service,
54
54
  snippet_repository=snippet_repository,
55
55
  file_repository=file_repository,
56
+ session=session,
56
57
  )
57
58
 
58
59
 
@@ -108,4 +109,5 @@ def create_indexing_application_service(
108
109
  text_search_service=text_search_service,
109
110
  enrichment_service=enrichment_service,
110
111
  snippet_application_service=snippet_application_service,
112
+ session=session,
111
113
  )
@@ -30,7 +30,6 @@ class SqlAlchemyEmbeddingRepository:
30
30
 
31
31
  """
32
32
  self.session.add(embedding)
33
- await self.session.commit()
34
33
  return embedding
35
34
 
36
35
  async def get_embedding_by_snippet_id_and_type(
@@ -81,7 +80,6 @@ class SqlAlchemyEmbeddingRepository:
81
80
  embeddings = result.scalars().all()
82
81
  for embedding in embeddings:
83
82
  await self.session.delete(embedding)
84
- await self.session.commit()
85
83
 
86
84
  async def list_semantic_results(
87
85
  self, embedding_type: EmbeddingType, embedding: list[float], top_k: int = 10
@@ -21,6 +21,25 @@ class SqlAlchemyFileRepository(FileRepository):
21
21
  """
22
22
  self.session = session
23
23
 
24
+ async def get(self, id: int) -> File | None: # noqa: A002
25
+ """Get a file by ID."""
26
+ return await self.session.get(File, id)
27
+
28
+ async def save(self, entity: File) -> File:
29
+ """Save entity."""
30
+ self.session.add(entity)
31
+ return entity
32
+
33
+ async def delete(self, id: int) -> None: # noqa: A002
34
+ """Delete entity by ID."""
35
+ file = await self.get(id)
36
+ if file:
37
+ await self.session.delete(file)
38
+
39
+ async def list(self) -> Sequence[File]:
40
+ """List all entities."""
41
+ return (await self.session.scalars(select(File))).all()
42
+
24
43
  async def get_files_for_index(self, index_id: int) -> Sequence[File]:
25
44
  """Get all files for an index.
26
45
 
@@ -57,17 +76,3 @@ class SqlAlchemyFileRepository(FileRepository):
57
76
  query = select(File).where(File.id == file_id)
58
77
  result = await self.session.execute(query)
59
78
  return result.scalar_one_or_none()
60
-
61
- async def save(self, file: File) -> File:
62
- """Save file using SQLAlchemy.
63
-
64
- Args:
65
- file: The file to save
66
-
67
- Returns:
68
- The saved file
69
-
70
- """
71
- self.session.add(file)
72
- await self.session.commit()
73
- return file
@@ -1,6 +1,7 @@
1
1
  """SQLAlchemy repository."""
2
2
 
3
3
  from collections.abc import Sequence
4
+ from typing import cast
4
5
 
5
6
  from sqlalchemy import select
6
7
  from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,41 +17,43 @@ class SqlAlchemySourceRepository(SourceRepository):
16
17
  """Initialize the repository."""
17
18
  self._session = session
18
19
 
19
- async def get(self, source_id: int) -> Source | None:
20
+ async def get(self, id: int) -> Source | None: # noqa: A002
20
21
  """Get a source by ID."""
21
- return await self._session.get(Source, source_id)
22
+ return await self._session.get(Source, id)
23
+
24
+ async def save(self, entity: Source) -> Source:
25
+ """Save entity."""
26
+ self._session.add(entity)
27
+ return entity
28
+
29
+ async def delete(self, id: int) -> None: # noqa: A002
30
+ """Delete entity by ID."""
31
+ source = await self.get(id)
32
+ if source:
33
+ await self._session.delete(source)
34
+
35
+ async def list(self) -> Sequence[Source]:
36
+ """List all entities."""
37
+ stmt = select(Source)
38
+ return (await self._session.scalars(stmt)).all()
22
39
 
23
40
  async def get_by_uri(self, uri: str) -> Source | None:
24
41
  """Get a source by URI."""
25
42
  stmt = select(Source).where(Source.uri == uri)
26
- return await self._session.scalar(stmt) # None if no row
43
+ return cast("Source | None", await self._session.scalar(stmt))
27
44
 
28
- async def list(self, *, source_type: SourceType | None = None) -> Sequence[Source]:
29
- """List sources."""
45
+ async def list_by_type(
46
+ self, source_type: SourceType | None = None
47
+ ) -> Sequence[Source]:
48
+ """List sources by type."""
30
49
  stmt = select(Source)
31
50
  if source_type is not None:
32
51
  stmt = stmt.where(Source.type == source_type)
33
52
  return (await self._session.scalars(stmt)).all()
34
53
 
35
- async def add(self, source: Source) -> None:
36
- """Add a source."""
37
- self._session.add(source) # INSERT on flush
38
- await self._session.flush() # Flush to get the ID
39
-
40
- async def create_source(self, source: Source) -> Source:
41
- """Create a source and commit it."""
42
- self._session.add(source)
43
- await self._session.commit()
44
- return source
45
-
46
- async def remove(self, source: Source) -> None:
47
- """Remove a source."""
48
- await self._session.delete(source) # DELETE on flush
49
-
50
54
  async def create_file(self, file: File) -> File:
51
55
  """Create a new file record."""
52
56
  self._session.add(file)
53
- await self._session.commit()
54
57
  return file
55
58
 
56
59
  async def upsert_author(self, author: Author) -> Author:
@@ -59,14 +62,13 @@ class SqlAlchemySourceRepository(SourceRepository):
59
62
  stmt = select(Author).where(
60
63
  Author.name == author.name, Author.email == author.email
61
64
  )
62
- existing_author = await self._session.scalar(stmt)
65
+ existing_author = cast("Author | None", await self._session.scalar(stmt))
63
66
 
64
67
  if existing_author:
65
68
  return existing_author
66
69
 
67
70
  # Author doesn't exist, create new one
68
71
  self._session.add(author)
69
- await self._session.commit()
70
72
  return author
71
73
 
72
74
  async def upsert_author_file_mapping(
@@ -78,14 +80,15 @@ class SqlAlchemySourceRepository(SourceRepository):
78
80
  AuthorFileMapping.author_id == mapping.author_id,
79
81
  AuthorFileMapping.file_id == mapping.file_id,
80
82
  )
81
- existing_mapping = await self._session.scalar(stmt)
83
+ existing_mapping = cast(
84
+ "AuthorFileMapping | None", await self._session.scalar(stmt)
85
+ )
82
86
 
83
87
  if existing_mapping:
84
88
  return existing_mapping
85
89
 
86
90
  # Mapping doesn't exist, create new one
87
91
  self._session.add(mapping)
88
- await self._session.commit()
89
92
  return mapping
90
93
 
91
94
 
@@ -96,26 +99,35 @@ class SqlAlchemyAuthorRepository(AuthorRepository):
96
99
  """Initialize the repository."""
97
100
  self._session = session
98
101
 
99
- async def get(self, author_id: int) -> Author | None:
102
+ async def get(self, id: int) -> Author | None: # noqa: A002
100
103
  """Get an author by ID."""
101
- return await self._session.get(Author, author_id)
104
+ return await self._session.get(Author, id)
102
105
 
103
- async def get_by_name(self, name: str) -> Author | None:
104
- """Get an author by name."""
105
- return await self._session.scalar(select(Author).where(Author.name == name))
106
+ async def save(self, entity: Author) -> Author:
107
+ """Save entity."""
108
+ self._session.add(entity)
109
+ return entity
106
110
 
107
- async def get_by_email(self, email: str) -> Author | None:
108
- """Get an author by email."""
109
- return await self._session.scalar(select(Author).where(Author.email == email))
111
+ async def delete(self, id: int) -> None: # noqa: A002
112
+ """Delete entity by ID."""
113
+ author = await self.get(id)
114
+ if author:
115
+ await self._session.delete(author)
110
116
 
111
117
  async def list(self) -> Sequence[Author]:
112
118
  """List authors."""
113
119
  return (await self._session.scalars(select(Author))).all()
114
120
 
115
- async def add(self, author: Author) -> None:
116
- """Add an author."""
117
- self._session.add(author)
121
+ async def get_by_name(self, name: str) -> Author | None:
122
+ """Get an author by name."""
123
+ return cast(
124
+ "Author | None",
125
+ await self._session.scalar(select(Author).where(Author.name == name)),
126
+ )
118
127
 
119
- async def remove(self, author: Author) -> None:
120
- """Remove an author."""
121
- await self._session.delete(author)
128
+ async def get_by_email(self, email: str) -> Author | None:
129
+ """Get an author by email."""
130
+ return cast(
131
+ "Author | None",
132
+ await self._session.scalar(select(Author).where(Author.email == email)),
133
+ )
@@ -21,19 +21,24 @@ class SqlAlchemySnippetRepository(SnippetRepository):
21
21
  """
22
22
  self.session = session
23
23
 
24
- async def save(self, snippet: Snippet) -> Snippet:
25
- """Save snippet using SQLAlchemy.
24
+ async def get(self, id: int) -> Snippet | None: # noqa: A002
25
+ """Get a snippet by ID."""
26
+ return await self.session.get(Snippet, id)
26
27
 
27
- Args:
28
- snippet: The snippet to save
28
+ async def save(self, entity: Snippet) -> Snippet:
29
+ """Save entity."""
30
+ self.session.add(entity)
31
+ return entity
29
32
 
30
- Returns:
31
- The saved snippet
33
+ async def delete(self, id: int) -> None: # noqa: A002
34
+ """Delete entity by ID."""
35
+ snippet = await self.get(id)
36
+ if snippet:
37
+ await self.session.delete(snippet)
32
38
 
33
- """
34
- self.session.add(snippet)
35
- await self.session.commit()
36
- return snippet
39
+ async def list(self) -> Sequence[Snippet]:
40
+ """List all entities."""
41
+ return (await self.session.scalars(select(Snippet))).all()
37
42
 
38
43
  async def get_by_id(self, snippet_id: int) -> Snippet | None:
39
44
  """Get a snippet by ID.
@@ -72,4 +77,3 @@ class SqlAlchemySnippetRepository(SnippetRepository):
72
77
  """
73
78
  query = delete(Snippet).where(Snippet.index_id == index_id)
74
79
  await self.session.execute(query)
75
- await self.session.commit()
kodit/mcp.py CHANGED
@@ -97,6 +97,7 @@ def create_snippet_application_service(
97
97
  snippet_extraction_service=snippet_extraction_service,
98
98
  snippet_repository=snippet_repository,
99
99
  file_repository=file_repository,
100
+ session=session,
100
101
  )
101
102
 
102
103
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kodit
3
- Version: 0.2.5
3
+ Version: 0.2.7
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,23 +1,24 @@
1
1
  kodit/.gitignore,sha256=ztkjgRwL9Uud1OEi36hGQeDGk3OLK1NfDEO8YqGYy8o,11
2
2
  kodit/__init__.py,sha256=aEKHYninUq1yh6jaNfvJBYg-6fenpN132nJt1UU6Jxs,59
3
- kodit/_version.py,sha256=N3oBwJUFmS-AwCjqOcSlRW4GvSq-uJJMaBvoGfv1-hM,511
3
+ kodit/_version.py,sha256=Xk20v7uvkFqkpy9aLJzVngs1eKQn0FYUP2oyA1MEQUU,511
4
4
  kodit/app.py,sha256=qKBWJ0VNSY_M6G3VFfAQ0133q5bnS99cUFD0p396taw,1032
5
- kodit/cli.py,sha256=SUoo9R-Jut0OlZLajD-nSPdhvHPOZ2SpN35SDyON8tc,11500
5
+ kodit/cli.py,sha256=JnhTlG1s04O0m8AzsBdrwP8T_BqSZMPXnRLvI7T_Gxc,12004
6
6
  kodit/config.py,sha256=3yh7hfLSILjZK_qJMhcExwRcrWJ0b5Eb1JjjOvMPJZo,4146
7
7
  kodit/database.py,sha256=kI9yBm4uunsgV4-QeVoCBL0wLzU4kYmYv5qZilGnbPE,1740
8
8
  kodit/log.py,sha256=sHPHYetlMcKTor2VaFLMyao1_fZ_xhuzqXCAt5F5UMU,8575
9
- kodit/mcp.py,sha256=kiWyZ2Ptluh0jLDQNxKjxNmD18bEG1-zAFVOoZAPsWI,6192
9
+ kodit/mcp.py,sha256=bUvG4by2CDN3QSkKNUer0yxpzAGbq-hg8HTU7S5fMv4,6217
10
10
  kodit/middleware.py,sha256=I6FOkqG9-8RH5kR1-0ZoQWfE4qLCB8lZYv8H_OCH29o,2714
11
11
  kodit/reporting.py,sha256=icce1ZyiADsA_Qz-mSjgn2H4SSqKuGfLKnw-yrl9nsg,2722
12
12
  kodit/application/__init__.py,sha256=mH50wTpgP9dhbKztFsL8Dda9Hi18TSnMVxXtpp4aGOA,35
13
13
  kodit/application/commands/__init__.py,sha256=AOVs25fwboBnMCWdgDB7fPbAYTljurAPVMkATIGRKuk,38
14
14
  kodit/application/commands/snippet_commands.py,sha256=WzRrnJOnLpIK8-wvN7c-ecGs_4LosQ_jR30dQkFqFBY,600
15
15
  kodit/application/services/__init__.py,sha256=p5UQNw-H5sxQvs5Etfte93B3cJ1kKW6DNxK34uFvU1E,38
16
- kodit/application/services/indexing_application_service.py,sha256=n7ViSfAPshUqe_gebgduRKwIPkB3Cl72CDw2U_qUJXY,13558
17
- kodit/application/services/snippet_application_service.py,sha256=NJxR3X78lTK0zXpYPdiCCw8NAJdLwyMyynfGnBfvONM,4759
16
+ kodit/application/services/indexing_application_service.py,sha256=tfdEiFTVvqkZQ6I_ZkW5IECQJHZi35OJX4bj96lkZrc,14455
17
+ kodit/application/services/snippet_application_service.py,sha256=2qW7ZIUQ63bdtAngf6xgQ09N-vygNpd12sXwyKwOcSs,5037
18
18
  kodit/domain/__init__.py,sha256=TCpg4Xx-oF4mKV91lo4iXqMEfBT1OoRSYnbG-zVWolA,66
19
19
  kodit/domain/entities.py,sha256=6XVuwDIQjkBw5Bm51io5ZUxB6_O4A774CDke2bfKWTY,5584
20
20
  kodit/domain/enums.py,sha256=Ik_h3D3eZ0FsSlPsU0ikm-Yv3Rmvzicffi9yBn19UIE,191
21
+ kodit/domain/errors.py,sha256=yIsgCjM_yOFIg8l7l-t7jM8pgeAX4cfPq0owf7iz3DA,106
21
22
  kodit/domain/interfaces.py,sha256=Jkd0Ob4qSvhZHI9jRPFQ1n5Cv0SvU-y3Z-HCw2ikc4I,742
22
23
  kodit/domain/repositories.py,sha256=bdKxSKGI6XzrpzeKcv-NDV2JBirbEMRK-Y4UCZmDtoY,2706
23
24
  kodit/domain/value_objects.py,sha256=4Vs7Uk1wQgTjnCkZlOlw7E1Q8NiyAmBpFv38Lhs3WZ0,3869
@@ -28,7 +29,7 @@ kodit/domain/services/enrichment_service.py,sha256=XsXg3nV-KN4rqtC7Zro_ZiZ6RSq-1
28
29
  kodit/domain/services/ignore_service.py,sha256=boEN-IRLmUtwO9ZnuACaVFZbIKrtUG8YwnsXKEDIG28,1136
29
30
  kodit/domain/services/indexing_service.py,sha256=FEizu2GkvZA32xHOYXXch0LuHoWg6Z-BbJMPjZslzjc,5853
30
31
  kodit/domain/services/snippet_extraction_service.py,sha256=QW_99bXWpr8g6ZI-hp4Aj57VCSrUf71dLwQca5T6pyg,3065
31
- kodit/domain/services/source_service.py,sha256=CM9TKTvn-9xfsk3AoKvIoPQ5_MridXng2jY8skn6_5Q,2987
32
+ kodit/domain/services/source_service.py,sha256=9XGS3imJn65v855cztsJSaaFod6LhkF2xfUVMaytx-A,3068
32
33
  kodit/infrastructure/__init__.py,sha256=HzEYIjoXnkz_i_MHO2e0sIVYweUcRnl2RpyBiTbMObU,28
33
34
  kodit/infrastructure/bm25/__init__.py,sha256=DmGbrEO34FOJy4e685BbyxLA7gPW1eqs2gAxsp6JOuM,34
34
35
  kodit/infrastructure/bm25/bm25_factory.py,sha256=I4eo7qRslnyXIRkBf-StZ5ga2Evrr5J5YFocTChFD3g,884
@@ -37,11 +38,11 @@ kodit/infrastructure/bm25/vectorchord_bm25_repository.py,sha256=0Db9XWFjiS4TFrsN
37
38
  kodit/infrastructure/cloning/__init__.py,sha256=IzIvX-yeRRFZ-lfvPVSEe_qXszO6DGQdjKwwDigexyQ,30
38
39
  kodit/infrastructure/cloning/metadata.py,sha256=C5LLmsUzi29RhSbzVDNqiShbekg7qdp1ihGUyFXy5yM,4277
39
40
  kodit/infrastructure/cloning/folder/__init__.py,sha256=w6ykrVtbYJlUDEXAjqgf6w2rMsUMCrrpIbl3QMjubgY,37
40
- kodit/infrastructure/cloning/folder/factory.py,sha256=qpFXxZuwYbdO17FKurWtm84ahwerDpcaLubClEZrbtk,3955
41
+ kodit/infrastructure/cloning/folder/factory.py,sha256=vl1hwnYA7lczjotn2fahJQAt7IK96CSArx8cSaRFKeY,4242
41
42
  kodit/infrastructure/cloning/folder/working_copy.py,sha256=FPhwzuPj40yGoYvwcm9VG8mv8MbJxwfby_N5JS-_daA,1154
42
43
  kodit/infrastructure/cloning/git/__init__.py,sha256=20ePcp0qE6BuLsjsv4KYB1DzKhMIMsPXwEqIEZtjTJs,34
43
- kodit/infrastructure/cloning/git/factory.py,sha256=pjIvl3NIhXNNbRcI86wedz8CP-jV_Z3OVpGl4BUGCmo,4585
44
- kodit/infrastructure/cloning/git/working_copy.py,sha256=DMW_p7WWGoSeyDI9g55ItwsRomZSotXWRrlopqwszaQ,1115
44
+ kodit/infrastructure/cloning/git/factory.py,sha256=cY0cxapp0NCvjMRpzesW_qRzbWbh-tMKIeAj0Eodyhw,5409
45
+ kodit/infrastructure/cloning/git/working_copy.py,sha256=r_Uu6NYhRQLAQce6k4KThXLtGPqkzr6CgWx7AJ89gN4,1462
45
46
  kodit/infrastructure/embedding/__init__.py,sha256=F-8nLlWAerYJ0MOIA4tbXHLan8bW5rRR84vzxx6tRKI,39
46
47
  kodit/infrastructure/embedding/embedding_factory.py,sha256=1AypjhWJGxvLnZt1SEH_FHPk9P0Vkt9fXdSGzFPp2ow,3432
47
48
  kodit/infrastructure/embedding/local_vector_search_repository.py,sha256=UO8A3Eb_djFVrWKKSukAo4u7k8djDD1SlOPHk2pP9ps,3921
@@ -58,13 +59,13 @@ kodit/infrastructure/enrichment/local_enrichment_provider.py,sha256=8CATNtgMHgBR
58
59
  kodit/infrastructure/enrichment/null_enrichment_provider.py,sha256=5Ksyxl3qDLxUjmOeIdHZ0UAIULy7RcbLXJoT7_CNXoQ,775
59
60
  kodit/infrastructure/enrichment/openai_enrichment_provider.py,sha256=fenq4HiJ2UkrzsE2D0A0qpmro38z9mKaIzKKU5v7hnY,3189
60
61
  kodit/infrastructure/git/__init__.py,sha256=0iMosFzudj4_xNIMe2SRbV6l5bWqkjnUsZoFsoZFuM8,33
61
- kodit/infrastructure/git/git_utils.py,sha256=lOujEx41UuWfYSnFWbY4HC2tK5utytyzNkW1e5IPCr0,543
62
+ kodit/infrastructure/git/git_utils.py,sha256=2DH6cyTjDRwFfL5Bzt1y2w0DwHZNypbC6R0Gm_A3hhg,2476
62
63
  kodit/infrastructure/ignore/__init__.py,sha256=VzFv8XOzHmsu0MEGnWVSF6KsgqLBmvHlRqAkT1Xb1MY,36
63
64
  kodit/infrastructure/ignore/ignore_pattern_provider.py,sha256=9m2XCsgW87UBTfzHr6Z0Ns6WpzwkLir3zyBY3PwsgXk,2225
64
65
  kodit/infrastructure/indexing/__init__.py,sha256=7UPRa2jwCAsa0Orsp6PqXSF8iIXJVzXHMFmrKkI9yH8,38
65
66
  kodit/infrastructure/indexing/fusion_service.py,sha256=mXUUcx3-8e75mWkxXMfl30HIoFXrTNHzB1w90MmEbak,1806
66
- kodit/infrastructure/indexing/index_repository.py,sha256=iHK9wcC8893Q6vY8OhW9UrN0gj1_GVzjTVlbewyZdxI,9094
67
- kodit/infrastructure/indexing/indexing_factory.py,sha256=BbavU9DJZapAe0V9H6Zsk7J6oPY48SJhGP3nrCPvzoQ,3914
67
+ kodit/infrastructure/indexing/index_repository.py,sha256=4aSCBE_Gn9ihOx_kXOpUTTIv6_Q71-VRFHEBgpWaAEw,8906
68
+ kodit/infrastructure/indexing/indexing_factory.py,sha256=KHA8c0XR9QrgqSR6gRUQk9wp6md97_oA1lwZFzoJAtk,3964
68
69
  kodit/infrastructure/snippet_extraction/__init__.py,sha256=v6KqrRDjSj0nt87m7UwRGx2GN_fz_14VWq9Q0uABR_s,54
69
70
  kodit/infrastructure/snippet_extraction/language_detection_service.py,sha256=Lo9xPLVia-70yP9gzyH4cQcBQzsp7WXjGOa5NBggScg,1158
70
71
  kodit/infrastructure/snippet_extraction/snippet_extraction_factory.py,sha256=LGbm614KCPNsM9K8r1z-E763NyAMIZA9ETJ_C61EknA,2759
@@ -76,10 +77,10 @@ kodit/infrastructure/snippet_extraction/languages/javascript.scm,sha256=Ini5TsVN
76
77
  kodit/infrastructure/snippet_extraction/languages/python.scm,sha256=ee85R9PBzwye3IMTE7-iVoKWd_ViU3EJISTyrFGrVeo,429
77
78
  kodit/infrastructure/snippet_extraction/languages/typescript.scm,sha256=U-ujbbv4tylbUBj9wuhL-e5cW6hmgPCNs4xrIX3r_hE,448
78
79
  kodit/infrastructure/sqlalchemy/__init__.py,sha256=UXPMSF_hgWaqr86cawRVqM8XdVNumQyyK5B8B97GnlA,33
79
- kodit/infrastructure/sqlalchemy/embedding_repository.py,sha256=vdjn3E5dFriFkceK8E8QMZzZk83etSf3NQOyS_LCeGY,7548
80
- kodit/infrastructure/sqlalchemy/file_repository.py,sha256=9O6ysCG_ldZFzDtpcxaTJQ8xya1A2q3CU2NafQo7GS0,2027
81
- kodit/infrastructure/sqlalchemy/repository.py,sha256=xWVAO9Bx9NKh3fSM-D1KGehT-6MNIYW2YQQcUcTE2xk,4369
82
- kodit/infrastructure/sqlalchemy/snippet_repository.py,sha256=zMqc5KxD0rP6r2BG2qLVAf3rh3IONkhZkI_pXg-r3a8,2137
80
+ kodit/infrastructure/sqlalchemy/embedding_repository.py,sha256=dCCRV5rD8T5xBksPKvdr-z4F72WKo4-dH9mXazDByXQ,7476
81
+ kodit/infrastructure/sqlalchemy/file_repository.py,sha256=9_kXHJ1YiWA1ingpvBNq8cuxkMu59PHwl_m9_Ttnq2o,2353
82
+ kodit/infrastructure/sqlalchemy/repository.py,sha256=EpZnOjR3wfPEqIauWw_KczpkSqBQPTq5sIyCpJCuW2w,4565
83
+ kodit/infrastructure/sqlalchemy/snippet_repository.py,sha256=RYhBnyvvBo-6obrUkds6BhEjchs4HYQL8k9x0Cy7BtM,2430
83
84
  kodit/infrastructure/ui/__init__.py,sha256=CzbLOBwIZ6B6iAHEd1L8cIBydCj-n_kobxJAhz2I9_Y,32
84
85
  kodit/infrastructure/ui/progress.py,sha256=BaAeMEgXlSSb0c_t_NPxnThIktkzzCS9kegb5ExULJs,4791
85
86
  kodit/infrastructure/ui/spinner.py,sha256=GcP115qtR0VEnGfMEtsGoAUpRzVGUSfiUXfoJJERngA,2357
@@ -92,8 +93,8 @@ kodit/migrations/versions/85155663351e_initial.py,sha256=Cg7zlF871o9ShV5rQMQ1v7h
92
93
  kodit/migrations/versions/9e53ea8bb3b0_add_authors.py,sha256=a32Zm8KUQyiiLkjKNPYdaJDgjW6VsV-GhaLnPnK_fpI,3884
93
94
  kodit/migrations/versions/__init__.py,sha256=9-lHzptItTzq_fomdIRBegQNm4Znx6pVjwD4MiqRIdo,36
94
95
  kodit/migrations/versions/c3f5137d30f5_index_all_the_things.py,sha256=rI8LmjF-I2OMxZ2nOIF_NRmqOLXe45hL_iz_nx97DTQ,1680
95
- kodit-0.2.5.dist-info/METADATA,sha256=UuPIpWyvjccCITCITmMcJhUwIxRd211UvDmVXi8MbY4,5867
96
- kodit-0.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
97
- kodit-0.2.5.dist-info/entry_points.txt,sha256=hoTn-1aKyTItjnY91fnO-rV5uaWQLQ-Vi7V5et2IbHY,40
98
- kodit-0.2.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
99
- kodit-0.2.5.dist-info/RECORD,,
96
+ kodit-0.2.7.dist-info/METADATA,sha256=G5rdRgHtm6V-p1tt0h7g7pZJKnQ5mAnCgDXVFRJG7Fg,5867
97
+ kodit-0.2.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
98
+ kodit-0.2.7.dist-info/entry_points.txt,sha256=hoTn-1aKyTItjnY91fnO-rV5uaWQLQ-Vi7V5et2IbHY,40
99
+ kodit-0.2.7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
100
+ kodit-0.2.7.dist-info/RECORD,,
File without changes