graph-dependency-analyzer 0.2.0__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.
Files changed (86) hide show
  1. graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
  2. graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
  3. graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
  4. graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
  5. graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
  6. src/__init__.py +0 -0
  7. src/application/__init__.py +0 -0
  8. src/application/dtos/__init__.py +0 -0
  9. src/application/dtos/affected_component.py +59 -0
  10. src/application/dtos/batch_processing_result.py +17 -0
  11. src/application/dtos/impact_analysis_response.py +56 -0
  12. src/application/dtos/impact_query_request.py +38 -0
  13. src/application/dtos/index_batch_request.py +53 -0
  14. src/application/dtos/index_batch_response.py +72 -0
  15. src/application/dtos/index_result.py +20 -0
  16. src/application/dtos/repository_index_result.py +79 -0
  17. src/application/exceptions.py +41 -0
  18. src/application/services/__init__.py +0 -0
  19. src/application/services/file_scanner_service.py +258 -0
  20. src/application/services/progress_tracker.py +132 -0
  21. src/application/use_cases/__init__.py +0 -0
  22. src/application/use_cases/clear_all_data_use_case.py +180 -0
  23. src/application/use_cases/index_batch_use_case.py +153 -0
  24. src/application/use_cases/index_repository_use_case.py +683 -0
  25. src/application/use_cases/indexing_orchestrator.py +181 -0
  26. src/application/use_cases/list_repositories_use_case.py +84 -0
  27. src/cli.py +512 -0
  28. src/config/__init__.py +5 -0
  29. src/config/container.py +211 -0
  30. src/config/logging_config.py +123 -0
  31. src/config/settings.py +165 -0
  32. src/domain/__init__.py +0 -0
  33. src/domain/entities/__init__.py +20 -0
  34. src/domain/entities/ast_node.py +32 -0
  35. src/domain/entities/code_chunk.py +45 -0
  36. src/domain/entities/code_file.py +44 -0
  37. src/domain/entities/dependency_graph.py +110 -0
  38. src/domain/entities/repository.py +46 -0
  39. src/domain/exceptions.py +36 -0
  40. src/domain/repositories/__init__.py +22 -0
  41. src/domain/repositories/i_code_parser.py +50 -0
  42. src/domain/repositories/i_dependency_extractor.py +35 -0
  43. src/domain/repositories/i_embedding_provider.py +31 -0
  44. src/domain/repositories/i_graph_repository.py +78 -0
  45. src/domain/repositories/i_vector_repository.py +55 -0
  46. src/domain/services/__init__.py +8 -0
  47. src/domain/services/dependency_extractor.py +962 -0
  48. src/domain/services/semantic_chunk_builder.py +719 -0
  49. src/domain/value_objects/__init__.py +12 -0
  50. src/domain/value_objects/chunk_type.py +24 -0
  51. src/domain/value_objects/dependency_type.py +22 -0
  52. src/domain/value_objects/node_type.py +22 -0
  53. src/domain/value_objects/qualified_name.py +78 -0
  54. src/infrastructure/__init__.py +0 -0
  55. src/infrastructure/exceptions.py +41 -0
  56. src/infrastructure/external_apis/__init__.py +0 -0
  57. src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
  58. src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
  59. src/infrastructure/parsing/__init__.py +20 -0
  60. src/infrastructure/parsing/config_file_parser.py +347 -0
  61. src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
  62. src/infrastructure/parsing/groovy_parser.py +198 -0
  63. src/infrastructure/parsing/maven_dependency_parser.py +147 -0
  64. src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
  65. src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
  66. src/infrastructure/parsing/python_parser.py +211 -0
  67. src/infrastructure/parsing/sql_schema_parser.py +658 -0
  68. src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
  69. src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
  70. src/infrastructure/persistence/__init__.py +0 -0
  71. src/infrastructure/persistence/graph_export_repository.py +443 -0
  72. src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
  73. src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
  74. src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
  75. src/presentation/__init__.py +0 -0
  76. src/presentation/api/__init__.py +0 -0
  77. src/presentation/api/rest/__init__.py +0 -0
  78. src/presentation/api/rest/exception_handlers.py +174 -0
  79. src/presentation/api/rest/main.py +80 -0
  80. src/presentation/api/rest/routers/__init__.py +5 -0
  81. src/presentation/api/rest/routers/admin.py +103 -0
  82. src/presentation/api/rest/routers/analysis.py +980 -0
  83. src/presentation/api/rest/routers/export.py +442 -0
  84. src/presentation/api/rest/routers/health.py +117 -0
  85. src/presentation/api/rest/routers/indexing.py +148 -0
  86. src/presentation/api/rest/routers/relationships.py +1089 -0
@@ -0,0 +1,357 @@
1
+ """
2
+ Qdrant Vector Repository Implementation (Task 5.2)
3
+
4
+ Implements persistence layer for code chunk embeddings in Qdrant.
5
+ Supports batch upsert with 1000 points per batch for optimal performance.
6
+ """
7
+ from dataclasses import dataclass
8
+ from uuid import uuid4
9
+ from typing import Any
10
+ import structlog
11
+
12
+ from qdrant_client import QdrantClient
13
+ from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue, MatchAny
14
+ from qdrant_client.http.exceptions import UnexpectedResponse
15
+
16
+ logger = structlog.get_logger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class VectorStorageResult:
21
+ """Result of storing embeddings in Qdrant"""
22
+ points_upserted: int
23
+
24
+
25
+ class QdrantVectorRepository:
26
+ """
27
+ Qdrant-based repository for storing code chunk embeddings.
28
+
29
+ Implements:
30
+ - Collection management (create if not exists)
31
+ - Batch upsert with 1000 points per batch
32
+ - 1536-dimensional embeddings (text-embedding-3-small)
33
+ - Complete metadata payload
34
+ - Unique repository listing
35
+
36
+ Requirements: 9.1-9.6
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ client: QdrantClient,
42
+ collection_name: str = "code_chunks",
43
+ batch_size: int = 1000,
44
+ vector_size: int = 1536,
45
+ ):
46
+ """
47
+ Initialize repository with Qdrant client.
48
+
49
+ Args:
50
+ client: QdrantClient instance
51
+ collection_name: Name of Qdrant collection
52
+ batch_size: Batch size for upsert operations (default 1000)
53
+ vector_size: Embedding dimension — must match the provider model
54
+ (1536 for OpenAI/Flow, 768 for nomic-embed-text, etc.)
55
+ """
56
+ self.client = client
57
+ self.collection_name = collection_name
58
+ self.batch_size = batch_size
59
+ self.vector_size = vector_size
60
+
61
+ async def upsert_batch(
62
+ self,
63
+ chunks: list,
64
+ embeddings: list[list[float]],
65
+ batch_size: int | None = None
66
+ ) -> VectorStorageResult:
67
+ """
68
+ Upsert code chunks with embeddings to Qdrant.
69
+
70
+ Uses batch processing for performance. Each point contains:
71
+ - UUID v4 id (from chunk.id)
72
+ - 1536-dimensional embedding vector
73
+ - Complete metadata payload
74
+
75
+ Args:
76
+ chunks: List of CodeChunk entities
77
+ embeddings: List of embedding vectors (1536-dim each)
78
+ batch_size: Override default batch size (optional)
79
+
80
+ Returns:
81
+ VectorStorageResult with count of points upserted
82
+
83
+ Raises:
84
+ ValueError: If validation fails (dimensions, length mismatch)
85
+ """
86
+ # Validation
87
+ self._validate_inputs(chunks, embeddings)
88
+
89
+ # Use provided batch_size or default
90
+ effective_batch_size = batch_size or self.batch_size
91
+
92
+ # Ensure collection exists
93
+ await self.ensure_collection_exists()
94
+
95
+ # Process in batches
96
+ total_upserted = 0
97
+ for i in range(0, len(chunks), effective_batch_size):
98
+ batch_chunks = chunks[i:i + effective_batch_size]
99
+ batch_embeddings = embeddings[i:i + effective_batch_size]
100
+
101
+ # Create points for this batch
102
+ points = self._create_points(batch_chunks, batch_embeddings)
103
+
104
+ # Upsert to Qdrant
105
+ self.client.upsert(
106
+ collection_name=self.collection_name,
107
+ points=points
108
+ )
109
+
110
+ total_upserted += len(points)
111
+
112
+ logger.info(
113
+ "embeddings_upserted",
114
+ collection=self.collection_name,
115
+ points_upserted=total_upserted,
116
+ batch_count=(len(chunks) + effective_batch_size - 1) // effective_batch_size
117
+ )
118
+
119
+ return VectorStorageResult(points_upserted=total_upserted)
120
+
121
+ def _validate_inputs(
122
+ self,
123
+ chunks: list,
124
+ embeddings: list[list[float]]
125
+ ) -> None:
126
+ """
127
+ Validate chunks and embeddings inputs.
128
+
129
+ Args:
130
+ chunks: List of CodeChunk
131
+ embeddings: List of embedding vectors
132
+
133
+ Raises:
134
+ ValueError: If validation fails
135
+ """
136
+ # Validate lengths match
137
+ if len(chunks) != len(embeddings):
138
+ raise ValueError(
139
+ f"Chunks and embeddings must have same length. "
140
+ f"Got {len(chunks)} chunks and {len(embeddings)} embeddings."
141
+ )
142
+
143
+ # Validate embedding dimensions — uses self.vector_size set at init
144
+ for i, embedding in enumerate(embeddings):
145
+ if len(embedding) != self.vector_size:
146
+ raise ValueError(
147
+ f"Embedding at index {i} must have {self.vector_size} dimensions, "
148
+ f"got {len(embedding)}. "
149
+ f"Check QDRANT_VECTOR_SIZE in .env matches your embedding model."
150
+ )
151
+
152
+ def _create_points(
153
+ self,
154
+ chunks: list,
155
+ embeddings: list[list[float]]
156
+ ) -> list[PointStruct]:
157
+ """
158
+ Create Qdrant PointStruct from chunks and embeddings.
159
+
160
+ Args:
161
+ chunks: List of CodeChunk
162
+ embeddings: List of embedding vectors
163
+
164
+ Returns:
165
+ List of PointStruct ready for upsert
166
+ """
167
+ points = []
168
+
169
+ for chunk, embedding in zip(chunks, embeddings):
170
+ # Build payload from chunk metadata
171
+ payload = {
172
+ "repository": chunk.metadata.get("repository", ""),
173
+ "file_path": chunk.metadata.get("file_path", ""),
174
+ "language": chunk.metadata.get("language", "java"),
175
+ "type": chunk.chunk_type,
176
+ "name": chunk.metadata.get("name", ""),
177
+ "qualified_name": chunk.metadata.get("qualified_name", ""),
178
+ "content": chunk.content,
179
+ "line_start": chunk.metadata.get("line_start", 0),
180
+ "line_end": chunk.metadata.get("line_end", 0),
181
+ }
182
+
183
+ # Add optional metadata fields
184
+ if "parameters" in chunk.metadata:
185
+ payload["parameters"] = chunk.metadata["parameters"]
186
+ if "return_type" in chunk.metadata:
187
+ payload["return_type"] = chunk.metadata["return_type"]
188
+ if "parent_class" in chunk.metadata:
189
+ payload["parent_class"] = chunk.metadata["parent_class"]
190
+
191
+ # Create point with UUID from chunk
192
+ point = PointStruct(
193
+ id=str(chunk.id),
194
+ vector=embedding,
195
+ payload=payload
196
+ )
197
+
198
+ points.append(point)
199
+
200
+ return points
201
+
202
+ async def search_similar(
203
+ self,
204
+ query_vector: list[float],
205
+ limit: int = 10,
206
+ repositories: list[str] | None = None,
207
+ score_threshold: float = 0.35,
208
+ ) -> list[dict]:
209
+ """
210
+ Search for code chunks semantically similar to the query vector.
211
+
212
+ Args:
213
+ query_vector: 1536-dimensional embedding of the search query
214
+ limit: Maximum number of results to return
215
+ repositories: Optional list of repo names to filter by
216
+ score_threshold: Minimum cosine similarity score (0-1)
217
+
218
+ Returns:
219
+ List of dicts with keys: score, repository, file_path,
220
+ qualified_name, name, type, content, line_start, line_end
221
+ """
222
+ # Build optional filter for repository
223
+ query_filter = None
224
+ if repositories:
225
+ if len(repositories) == 1:
226
+ query_filter = Filter(
227
+ must=[FieldCondition(
228
+ key="repository",
229
+ match=MatchValue(value=repositories[0])
230
+ )]
231
+ )
232
+ else:
233
+ # MatchAny matches any of the given values — equivalent to OR
234
+ query_filter = Filter(
235
+ must=[FieldCondition(
236
+ key="repository",
237
+ match=MatchAny(any=repositories)
238
+ )]
239
+ )
240
+
241
+ try:
242
+ # qdrant-client >= 1.7 uses query_points; older versions use search.
243
+ # Try query_points first, fall back to search.
244
+ if hasattr(self.client, "query_points"):
245
+ response = self.client.query_points(
246
+ collection_name=self.collection_name,
247
+ query=query_vector,
248
+ limit=limit,
249
+ query_filter=query_filter,
250
+ score_threshold=score_threshold,
251
+ with_payload=True,
252
+ )
253
+ results = response.points
254
+ else:
255
+ results = self.client.search(
256
+ collection_name=self.collection_name,
257
+ query_vector=query_vector,
258
+ limit=limit,
259
+ query_filter=query_filter,
260
+ score_threshold=score_threshold,
261
+ with_payload=True,
262
+ with_vectors=False,
263
+ )
264
+ except Exception:
265
+ return []
266
+
267
+ hits = []
268
+ for point in results:
269
+ payload = point.payload or {}
270
+ hits.append({
271
+ "score": round(point.score, 4),
272
+ "repository": payload.get("repository", ""),
273
+ "file_path": payload.get("file_path", ""),
274
+ "qualified_name": payload.get("qualified_name", ""),
275
+ "name": payload.get("name", ""),
276
+ "type": payload.get("type", ""),
277
+ "language": payload.get("language", ""),
278
+ "line_start": payload.get("line_start", 0),
279
+ "line_end": payload.get("line_end", 0),
280
+ "content": (payload.get("content", "")[:500]
281
+ if payload.get("content") else ""),
282
+ })
283
+
284
+ logger.info(
285
+ "semantic_search_completed",
286
+ results=len(hits),
287
+ repositories=repositories,
288
+ score_threshold=score_threshold,
289
+ )
290
+ return hits
291
+
292
+ async def get_unique_repositories(self) -> list[str]:
293
+ """
294
+ Get list of unique repository names indexed in Qdrant.
295
+
296
+ Returns:
297
+ List of unique repository names
298
+
299
+ Raises:
300
+ Exception: If Qdrant query fails
301
+ """
302
+ # Scroll through all points to collect unique repositories
303
+ # Using scroll instead of search since we want all data
304
+ records, next_page = self.client.scroll(
305
+ collection_name=self.collection_name,
306
+ limit=10000, # Large limit to get all in one call
307
+ with_payload=["repository"],
308
+ with_vectors=False # Don't need vectors for this
309
+ )
310
+
311
+ # Extract unique repository names
312
+ repositories = set()
313
+ for record in records:
314
+ if "repository" in record.payload:
315
+ repositories.add(record.payload["repository"])
316
+
317
+ logger.debug(
318
+ "unique_repositories_retrieved",
319
+ collection=self.collection_name,
320
+ count=len(repositories)
321
+ )
322
+
323
+ return sorted(list(repositories))
324
+
325
+ async def ensure_collection_exists(self) -> None:
326
+ """
327
+ Ensure Qdrant collection exists, create if not.
328
+
329
+ Creates collection with:
330
+ - vector_size: 1536 (text-embedding-3-small)
331
+ - distance: Cosine
332
+
333
+ Raises:
334
+ Exception: If collection creation fails
335
+ """
336
+ try:
337
+ # Try to get collection info (QdrantClient is synchronous)
338
+ self.client.get_collection(self.collection_name)
339
+ logger.debug("collection_exists", collection=self.collection_name)
340
+
341
+ except UnexpectedResponse as e:
342
+ if e.status_code == 404:
343
+ # Collection doesn't exist, create it
344
+ logger.info("creating_collection", collection=self.collection_name)
345
+
346
+ self.client.create_collection(
347
+ collection_name=self.collection_name,
348
+ vectors_config=VectorParams(
349
+ size=self.vector_size,
350
+ distance=Distance.COSINE
351
+ )
352
+ )
353
+
354
+ logger.info("collection_created", collection=self.collection_name)
355
+ else:
356
+ # Other error, re-raise
357
+ raise