kodit 0.5.4__py3-none-any.whl → 0.5.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kodit might be problematic. Click here for more details.

Files changed (64) hide show
  1. kodit/_version.py +2 -2
  2. kodit/app.py +2 -0
  3. kodit/application/factories/server_factory.py +58 -32
  4. kodit/application/services/code_search_application_service.py +89 -12
  5. kodit/application/services/commit_indexing_application_service.py +527 -195
  6. kodit/application/services/enrichment_query_service.py +311 -43
  7. kodit/application/services/indexing_worker_service.py +1 -1
  8. kodit/application/services/queue_service.py +15 -10
  9. kodit/application/services/sync_scheduler.py +2 -1
  10. kodit/domain/enrichments/architecture/architecture.py +1 -1
  11. kodit/domain/enrichments/architecture/database_schema/__init__.py +1 -0
  12. kodit/domain/enrichments/architecture/database_schema/database_schema.py +17 -0
  13. kodit/domain/enrichments/architecture/physical/physical.py +1 -1
  14. kodit/domain/enrichments/development/development.py +1 -1
  15. kodit/domain/enrichments/development/snippet/snippet.py +12 -5
  16. kodit/domain/enrichments/enrichment.py +31 -4
  17. kodit/domain/enrichments/history/__init__.py +1 -0
  18. kodit/domain/enrichments/history/commit_description/__init__.py +1 -0
  19. kodit/domain/enrichments/history/commit_description/commit_description.py +17 -0
  20. kodit/domain/enrichments/history/history.py +18 -0
  21. kodit/domain/enrichments/usage/api_docs.py +1 -1
  22. kodit/domain/enrichments/usage/usage.py +1 -1
  23. kodit/domain/entities/git.py +30 -25
  24. kodit/domain/factories/git_repo_factory.py +20 -5
  25. kodit/domain/protocols.py +60 -125
  26. kodit/domain/services/embedding_service.py +14 -16
  27. kodit/domain/services/git_repository_service.py +60 -38
  28. kodit/domain/services/git_service.py +18 -11
  29. kodit/domain/tracking/resolution_service.py +6 -16
  30. kodit/domain/value_objects.py +6 -9
  31. kodit/infrastructure/api/v1/dependencies.py +12 -3
  32. kodit/infrastructure/api/v1/query_params.py +27 -0
  33. kodit/infrastructure/api/v1/routers/commits.py +91 -85
  34. kodit/infrastructure/api/v1/routers/repositories.py +53 -37
  35. kodit/infrastructure/api/v1/routers/search.py +1 -1
  36. kodit/infrastructure/api/v1/schemas/enrichment.py +14 -0
  37. kodit/infrastructure/api/v1/schemas/repository.py +1 -1
  38. kodit/infrastructure/cloning/git/git_python_adaptor.py +41 -0
  39. kodit/infrastructure/database_schema/__init__.py +1 -0
  40. kodit/infrastructure/database_schema/database_schema_detector.py +268 -0
  41. kodit/infrastructure/slicing/api_doc_extractor.py +0 -2
  42. kodit/infrastructure/sqlalchemy/embedding_repository.py +44 -34
  43. kodit/infrastructure/sqlalchemy/enrichment_association_repository.py +73 -0
  44. kodit/infrastructure/sqlalchemy/enrichment_v2_repository.py +145 -97
  45. kodit/infrastructure/sqlalchemy/entities.py +12 -116
  46. kodit/infrastructure/sqlalchemy/git_branch_repository.py +52 -244
  47. kodit/infrastructure/sqlalchemy/git_commit_repository.py +35 -324
  48. kodit/infrastructure/sqlalchemy/git_file_repository.py +70 -0
  49. kodit/infrastructure/sqlalchemy/git_repository.py +60 -230
  50. kodit/infrastructure/sqlalchemy/git_tag_repository.py +53 -240
  51. kodit/infrastructure/sqlalchemy/query.py +331 -0
  52. kodit/infrastructure/sqlalchemy/repository.py +203 -0
  53. kodit/infrastructure/sqlalchemy/task_repository.py +79 -58
  54. kodit/infrastructure/sqlalchemy/task_status_repository.py +45 -52
  55. kodit/migrations/versions/4b1a3b2c8fa5_refactor_git_tracking.py +190 -0
  56. {kodit-0.5.4.dist-info → kodit-0.5.6.dist-info}/METADATA +1 -1
  57. {kodit-0.5.4.dist-info → kodit-0.5.6.dist-info}/RECORD +60 -50
  58. kodit/infrastructure/mappers/enrichment_mapper.py +0 -83
  59. kodit/infrastructure/mappers/git_mapper.py +0 -193
  60. kodit/infrastructure/mappers/snippet_mapper.py +0 -104
  61. kodit/infrastructure/sqlalchemy/snippet_v2_repository.py +0 -479
  62. {kodit-0.5.4.dist-info → kodit-0.5.6.dist-info}/WHEEL +0 -0
  63. {kodit-0.5.4.dist-info → kodit-0.5.6.dist-info}/entry_points.txt +0 -0
  64. {kodit-0.5.4.dist-info → kodit-0.5.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,118 +1,166 @@
1
1
  """EnrichmentV2 repository."""
2
2
 
3
- from collections.abc import Callable, Sequence
3
+ from collections.abc import Callable
4
4
 
5
- import structlog
6
- from sqlalchemy import delete, select
7
5
  from sqlalchemy.ext.asyncio import AsyncSession
8
6
 
7
+ from kodit.domain.enrichments.architecture.architecture import (
8
+ ENRICHMENT_TYPE_ARCHITECTURE,
9
+ )
10
+ from kodit.domain.enrichments.architecture.database_schema.database_schema import (
11
+ ENRICHMENT_SUBTYPE_DATABASE_SCHEMA,
12
+ DatabaseSchemaEnrichment,
13
+ )
14
+ from kodit.domain.enrichments.architecture.physical.physical import (
15
+ ENRICHMENT_SUBTYPE_PHYSICAL,
16
+ PhysicalArchitectureEnrichment,
17
+ )
18
+ from kodit.domain.enrichments.development.development import ENRICHMENT_TYPE_DEVELOPMENT
19
+ from kodit.domain.enrichments.development.snippet.snippet import (
20
+ ENRICHMENT_SUBTYPE_SNIPPET,
21
+ ENRICHMENT_SUBTYPE_SNIPPET_SUMMARY,
22
+ SnippetEnrichment,
23
+ SnippetEnrichmentSummary,
24
+ )
9
25
  from kodit.domain.enrichments.enrichment import EnrichmentV2
10
- from kodit.infrastructure.mappers.enrichment_mapper import EnrichmentMapper
26
+ from kodit.domain.enrichments.history.commit_description.commit_description import (
27
+ ENRICHMENT_SUBTYPE_COMMIT_DESCRIPTION,
28
+ CommitDescriptionEnrichment,
29
+ )
30
+ from kodit.domain.enrichments.history.history import ENRICHMENT_TYPE_HISTORY
31
+ from kodit.domain.enrichments.usage.api_docs import (
32
+ ENRICHMENT_SUBTYPE_API_DOCS,
33
+ APIDocEnrichment,
34
+ )
35
+ from kodit.domain.enrichments.usage.usage import ENRICHMENT_TYPE_USAGE
36
+ from kodit.domain.protocols import EnrichmentV2Repository
11
37
  from kodit.infrastructure.sqlalchemy import entities as db_entities
38
+ from kodit.infrastructure.sqlalchemy.repository import SqlAlchemyRepository
12
39
  from kodit.infrastructure.sqlalchemy.unit_of_work import SqlAlchemyUnitOfWork
13
40
 
14
41
 
15
- class EnrichmentV2Repository:
42
+ def create_enrichment_v2_repository(
43
+ session_factory: Callable[[], AsyncSession],
44
+ ) -> EnrichmentV2Repository:
45
+ """Create a enrichment v2 repository."""
46
+ return SQLAlchemyEnrichmentV2Repository(session_factory=session_factory)
47
+
48
+
49
+ class SQLAlchemyEnrichmentV2Repository(
50
+ SqlAlchemyRepository[EnrichmentV2, db_entities.EnrichmentV2], EnrichmentV2Repository
51
+ ):
16
52
  """Repository for managing enrichments and their associations."""
17
53
 
18
- def __init__(
19
- self,
20
- session_factory: Callable[[], AsyncSession],
21
- ) -> None:
22
- """Initialize the repository."""
23
- self.session_factory = session_factory
24
- self.mapper = EnrichmentMapper()
25
- self.log = structlog.get_logger(__name__)
54
+ def _get_id(self, entity: EnrichmentV2) -> int | None:
55
+ """Extract ID from domain entity."""
56
+ return entity.id
26
57
 
27
- async def enrichments_for_entity_type(
28
- self,
29
- entity_type: str,
30
- entity_ids: list[str],
31
- ) -> list[EnrichmentV2]:
32
- """Get all enrichments for multiple entities of the same type."""
33
- if not entity_ids:
34
- return []
58
+ @property
59
+ def db_entity_type(self) -> type[db_entities.EnrichmentV2]:
60
+ """The SQLAlchemy model type."""
61
+ return db_entities.EnrichmentV2
35
62
 
63
+ async def save(self, entity: EnrichmentV2) -> EnrichmentV2:
64
+ """Save entity (create new or update existing)."""
36
65
  async with SqlAlchemyUnitOfWork(self.session_factory) as session:
37
- stmt = (
38
- select(
39
- db_entities.EnrichmentV2,
40
- db_entities.EnrichmentAssociation.entity_id,
41
- )
42
- .join(db_entities.EnrichmentAssociation)
43
- .where(
44
- db_entities.EnrichmentAssociation.entity_type == entity_type,
45
- db_entities.EnrichmentAssociation.entity_id.in_(entity_ids),
46
- )
66
+ entity_id = self._get_id(entity)
67
+ # Skip session.get if entity_id is None (new entity not yet persisted)
68
+ existing_db_entity = (
69
+ await session.get(self.db_entity_type, entity_id)
70
+ if entity_id is not None
71
+ else None
47
72
  )
48
73
 
49
- result = await session.execute(stmt)
50
- rows = result.all()
51
-
52
- return [
53
- self.mapper.to_domain(db_enrichment, entity_type, entity_id)
54
- for db_enrichment, entity_id in rows
55
- ]
56
-
57
- async def bulk_save_enrichments(
58
- self,
59
- enrichments: Sequence[EnrichmentV2],
60
- ) -> None:
61
- """Bulk save enrichments with their associations."""
62
- if not enrichments:
63
- return
64
-
65
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
66
- enrichment_records = []
67
- for enrichment in enrichments:
68
- db_enrichment = db_entities.EnrichmentV2(
69
- type=enrichment.type,
70
- subtype=enrichment.subtype,
71
- content=enrichment.content,
72
- )
73
- session.add(db_enrichment)
74
- enrichment_records.append((enrichment, db_enrichment))
74
+ if existing_db_entity:
75
+ # Update existing entity
76
+ new_db_entity = self.to_db(entity)
77
+ self._update_db_entity(existing_db_entity, new_db_entity)
78
+ db_entity = existing_db_entity
79
+ else:
80
+ # Create new entity
81
+ db_entity = self.to_db(entity)
82
+ session.add(db_entity)
75
83
 
76
84
  await session.flush()
77
-
78
- for enrichment, db_enrichment in enrichment_records:
79
- db_association = db_entities.EnrichmentAssociation(
80
- enrichment_id=db_enrichment.id,
81
- entity_type=enrichment.entity_type_key(),
82
- entity_id=enrichment.entity_id,
83
- )
84
- session.add(db_association)
85
-
86
- async def bulk_delete_enrichments(
87
- self,
88
- entity_type: str,
89
- entity_ids: list[str],
90
- ) -> None:
91
- """Bulk delete enrichments for multiple entities of the same type."""
92
- if not entity_ids:
93
- return
94
-
95
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
96
- stmt = select(db_entities.EnrichmentAssociation.enrichment_id).where(
97
- db_entities.EnrichmentAssociation.entity_type == entity_type,
98
- db_entities.EnrichmentAssociation.entity_id.in_(entity_ids),
85
+ return self.to_domain(db_entity)
86
+
87
+ @staticmethod
88
+ def to_db(domain_entity: EnrichmentV2) -> db_entities.EnrichmentV2:
89
+ """Convert domain enrichment to database entity."""
90
+ enrichment = db_entities.EnrichmentV2(
91
+ type=domain_entity.type,
92
+ subtype=domain_entity.subtype,
93
+ content=domain_entity.content,
94
+ )
95
+ if domain_entity.id is not None:
96
+ enrichment.id = domain_entity.id
97
+ return enrichment
98
+
99
+ @staticmethod
100
+ def to_domain(db_entity: db_entities.EnrichmentV2) -> EnrichmentV2:
101
+ """Convert database enrichment to domain entity."""
102
+ # Use the stored type and subtype to determine the correct domain class
103
+ if (
104
+ db_entity.type == ENRICHMENT_TYPE_DEVELOPMENT
105
+ and db_entity.subtype == ENRICHMENT_SUBTYPE_SNIPPET_SUMMARY
106
+ ):
107
+ return SnippetEnrichmentSummary(
108
+ id=db_entity.id,
109
+ content=db_entity.content,
110
+ created_at=db_entity.created_at,
111
+ updated_at=db_entity.updated_at,
99
112
  )
100
- result = await session.execute(stmt)
101
- enrichment_ids = result.scalars().all()
102
-
103
- if enrichment_ids:
104
- await session.execute(
105
- delete(db_entities.EnrichmentV2).where(
106
- db_entities.EnrichmentV2.id.in_(enrichment_ids)
107
- )
108
- )
109
-
110
- async def delete_enrichment(self, enrichment_id: int) -> bool:
111
- """Delete a specific enrichment by ID."""
112
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
113
- result = await session.execute(
114
- delete(db_entities.EnrichmentV2).where(
115
- db_entities.EnrichmentV2.id == enrichment_id
116
- )
113
+ if (
114
+ db_entity.type == ENRICHMENT_TYPE_DEVELOPMENT
115
+ and db_entity.subtype == ENRICHMENT_SUBTYPE_SNIPPET
116
+ ):
117
+ return SnippetEnrichment(
118
+ id=db_entity.id,
119
+ content=db_entity.content,
120
+ created_at=db_entity.created_at,
121
+ updated_at=db_entity.updated_at,
122
+ )
123
+ if (
124
+ db_entity.type == ENRICHMENT_TYPE_USAGE
125
+ and db_entity.subtype == ENRICHMENT_SUBTYPE_API_DOCS
126
+ ):
127
+ return APIDocEnrichment(
128
+ id=db_entity.id,
129
+ content=db_entity.content,
130
+ created_at=db_entity.created_at,
131
+ updated_at=db_entity.updated_at,
117
132
  )
118
- return result.rowcount > 0
133
+ if (
134
+ db_entity.type == ENRICHMENT_TYPE_ARCHITECTURE
135
+ and db_entity.subtype == ENRICHMENT_SUBTYPE_PHYSICAL
136
+ ):
137
+ return PhysicalArchitectureEnrichment(
138
+ id=db_entity.id,
139
+ content=db_entity.content,
140
+ created_at=db_entity.created_at,
141
+ updated_at=db_entity.updated_at,
142
+ )
143
+ if (
144
+ db_entity.type == ENRICHMENT_TYPE_HISTORY
145
+ and db_entity.subtype == ENRICHMENT_SUBTYPE_COMMIT_DESCRIPTION
146
+ ):
147
+ return CommitDescriptionEnrichment(
148
+ id=db_entity.id,
149
+ content=db_entity.content,
150
+ created_at=db_entity.created_at,
151
+ updated_at=db_entity.updated_at,
152
+ )
153
+ if (
154
+ db_entity.type == ENRICHMENT_TYPE_ARCHITECTURE
155
+ and db_entity.subtype == ENRICHMENT_SUBTYPE_DATABASE_SCHEMA
156
+ ):
157
+ return DatabaseSchemaEnrichment(
158
+ id=db_entity.id,
159
+ content=db_entity.content,
160
+ created_at=db_entity.created_at,
161
+ updated_at=db_entity.updated_at,
162
+ )
163
+
164
+ raise ValueError(
165
+ f"Unknown enrichment type: {db_entity.type}/{db_entity.subtype}"
166
+ )
@@ -9,7 +9,6 @@ from sqlalchemy import (
9
9
  DateTime,
10
10
  Float,
11
11
  ForeignKey,
12
- ForeignKeyConstraint,
13
12
  Index,
14
13
  Integer,
15
14
  String,
@@ -210,11 +209,15 @@ class GitRepo(Base, CommonMixin):
210
209
  num_commits: Mapped[int] = mapped_column(Integer, default=0)
211
210
  num_branches: Mapped[int] = mapped_column(Integer, default=0)
212
211
  num_tags: Mapped[int] = mapped_column(Integer, default=0)
212
+ tracking_type: Mapped[str] = mapped_column(String(255), index=True)
213
+ tracking_name: Mapped[str] = mapped_column(String(255), index=True)
213
214
 
214
215
  def __init__( # noqa: PLR0913
215
216
  self,
216
217
  sanitized_remote_uri: str,
217
218
  remote_uri: str,
219
+ tracking_type: str,
220
+ tracking_name: str,
218
221
  cloned_path: Path | None,
219
222
  last_scanned_at: datetime | None = None,
220
223
  num_commits: int = 0,
@@ -225,6 +228,8 @@ class GitRepo(Base, CommonMixin):
225
228
  super().__init__()
226
229
  self.sanitized_remote_uri = sanitized_remote_uri
227
230
  self.remote_uri = remote_uri
231
+ self.tracking_type = tracking_type
232
+ self.tracking_name = tracking_name
228
233
  self.cloned_path = cloned_path
229
234
  self.last_scanned_at = last_scanned_at
230
235
  self.num_commits = num_commits
@@ -293,7 +298,12 @@ class GitBranch(Base):
293
298
 
294
299
  __table_args__ = (UniqueConstraint("repo_id", "name", name="uix_repo_branch"),)
295
300
 
296
- def __init__(self, repo_id: int, name: str, head_commit_sha: str) -> None:
301
+ def __init__(
302
+ self,
303
+ repo_id: int,
304
+ name: str,
305
+ head_commit_sha: str,
306
+ ) -> None:
297
307
  """Initialize Git branch."""
298
308
  super().__init__()
299
309
  self.repo_id = repo_id
@@ -301,31 +311,6 @@ class GitBranch(Base):
301
311
  self.head_commit_sha = head_commit_sha
302
312
 
303
313
 
304
- class GitTrackingBranch(Base):
305
- """Git tracking branch model."""
306
-
307
- __tablename__ = "git_tracking_branches"
308
- repo_id: Mapped[int] = mapped_column(
309
- ForeignKey("git_repos.id"), index=True, primary_key=True
310
- )
311
- name: Mapped[str] = mapped_column(String(255), index=True, primary_key=True)
312
- created_at: Mapped[datetime] = mapped_column(
313
- TZDateTime, nullable=False, default=lambda: datetime.now(UTC)
314
- )
315
- updated_at: Mapped[datetime] = mapped_column(
316
- TZDateTime,
317
- nullable=False,
318
- default=lambda: datetime.now(UTC),
319
- onupdate=lambda: datetime.now(UTC),
320
- )
321
-
322
- def __init__(self, repo_id: int, name: str) -> None:
323
- """Initialize Git tracking branch."""
324
- super().__init__()
325
- self.repo_id = repo_id
326
- self.name = name
327
-
328
-
329
314
  class GitTag(Base):
330
315
  """Git tag model."""
331
316
 
@@ -395,95 +380,6 @@ class GitCommitFile(Base):
395
380
  self.extension = extension
396
381
 
397
382
 
398
- class SnippetV2(Base):
399
- """SnippetV2 model for commit-based snippets."""
400
-
401
- __tablename__ = "snippets_v2"
402
-
403
- sha: Mapped[str] = mapped_column(String(64), primary_key=True)
404
- created_at: Mapped[datetime] = mapped_column(
405
- TZDateTime, nullable=False, default=lambda: datetime.now(UTC)
406
- )
407
- updated_at: Mapped[datetime] = mapped_column(
408
- TZDateTime,
409
- nullable=False,
410
- default=lambda: datetime.now(UTC),
411
- onupdate=lambda: datetime.now(UTC),
412
- )
413
- content: Mapped[str] = mapped_column(UnicodeText)
414
- extension: Mapped[str] = mapped_column(String(255), index=True)
415
-
416
- def __init__(
417
- self,
418
- sha: str,
419
- content: str,
420
- extension: str,
421
- ) -> None:
422
- """Initialize snippet."""
423
- super().__init__()
424
- self.sha = sha
425
- self.content = content
426
- self.extension = extension
427
-
428
-
429
- class SnippetV2File(Base):
430
- """Association between snippets and files."""
431
-
432
- __tablename__ = "snippet_v2_files"
433
-
434
- id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
435
- snippet_sha: Mapped[str] = mapped_column(ForeignKey("snippets_v2.sha"), index=True)
436
- blob_sha: Mapped[str] = mapped_column(String(64), index=True)
437
- commit_sha: Mapped[str] = mapped_column(String(64), index=True)
438
- file_path: Mapped[str] = mapped_column(String(1024), index=True)
439
-
440
- __table_args__ = (
441
- ForeignKeyConstraint(
442
- ["commit_sha", "file_path"],
443
- ["git_commit_files.commit_sha", "git_commit_files.path"],
444
- ),
445
- UniqueConstraint(
446
- "snippet_sha",
447
- "blob_sha",
448
- "commit_sha",
449
- "file_path",
450
- name="uix_snippet_file",
451
- ),
452
- )
453
-
454
- def __init__(
455
- self, snippet_sha: str, blob_sha: str, commit_sha: str, file_path: str
456
- ) -> None:
457
- """Initialize snippet file association."""
458
- super().__init__()
459
- self.snippet_sha = snippet_sha
460
- self.blob_sha = blob_sha
461
- self.commit_sha = commit_sha
462
- self.file_path = file_path
463
-
464
-
465
- class CommitSnippetV2(Base):
466
- """Association table for commits and snippets v2."""
467
-
468
- __tablename__ = "commit_snippets_v2"
469
-
470
- id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
471
- commit_sha: Mapped[str] = mapped_column(
472
- ForeignKey("git_commits.commit_sha"), index=True
473
- )
474
- snippet_sha: Mapped[str] = mapped_column(ForeignKey("snippets_v2.sha"), index=True)
475
-
476
- __table_args__ = (
477
- UniqueConstraint("commit_sha", "snippet_sha", name="uix_commit_snippet"),
478
- )
479
-
480
- def __init__(self, commit_sha: str, snippet_sha: str) -> None:
481
- """Initialize commit snippet association."""
482
- super().__init__()
483
- self.commit_sha = commit_sha
484
- self.snippet_sha = snippet_sha
485
-
486
-
487
383
  class CommitIndex(Base):
488
384
  """Commit index model."""
489
385