repotoire 0.1.2__cp314-cp314-win_amd64.whl → 0.1.3__cp314-cp314-win_amd64.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.
- repotoire/api/v1/__init__.py +2 -0
- repotoire/api/v1/routes/graph.py +476 -0
- repotoire/cli/__init__.py +8 -90
- repotoire/cli/auth.py +33 -0
- repotoire/detectors/async_antipattern.py +1 -1
- repotoire/detectors/circular_dependency.py +1 -1
- repotoire/detectors/data_clumps.py +1 -1
- repotoire/detectors/dead_code.py +1 -1
- repotoire/detectors/engine.py +1 -1
- repotoire/detectors/generator_misuse.py +1 -1
- repotoire/detectors/graph_algorithms.py +1 -1
- repotoire/detectors/inappropriate_intimacy.py +1 -1
- repotoire/detectors/long_parameter_list.py +1 -1
- repotoire/detectors/message_chain.py +1 -1
- repotoire/detectors/test_smell.py +1 -1
- repotoire/detectors/type_hint_coverage.py +1 -1
- repotoire/graph/cloud_client.py +215 -0
- repotoire/graph/factory.py +8 -18
- repotoire/graph/schema.py +2 -2
- {repotoire-0.1.2.dist-info → repotoire-0.1.3.dist-info}/METADATA +8 -9
- {repotoire-0.1.2.dist-info → repotoire-0.1.3.dist-info}/RECORD +25 -23
- repotoire_fast/repotoire_fast.cp314-win_amd64.pyd +0 -0
- {repotoire-0.1.2.dist-info → repotoire-0.1.3.dist-info}/WHEEL +0 -0
- {repotoire-0.1.2.dist-info → repotoire-0.1.3.dist-info}/entry_points.txt +0 -0
- {repotoire-0.1.2.dist-info → repotoire-0.1.3.dist-info}/licenses/LICENSE +0 -0
repotoire/api/v1/__init__.py
CHANGED
|
@@ -19,6 +19,7 @@ from repotoire.api.v1.routes import (
|
|
|
19
19
|
findings,
|
|
20
20
|
fixes,
|
|
21
21
|
github,
|
|
22
|
+
graph,
|
|
22
23
|
historical,
|
|
23
24
|
marketplace,
|
|
24
25
|
notifications,
|
|
@@ -223,6 +224,7 @@ v1_app.include_router(customer_webhooks.router)
|
|
|
223
224
|
v1_app.include_router(findings.router)
|
|
224
225
|
v1_app.include_router(fixes.router)
|
|
225
226
|
v1_app.include_router(github.router)
|
|
227
|
+
v1_app.include_router(graph.router)
|
|
226
228
|
v1_app.include_router(historical.router)
|
|
227
229
|
v1_app.include_router(marketplace.router)
|
|
228
230
|
v1_app.include_router(notifications.router)
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""Graph proxy API routes for CLI operations.
|
|
2
|
+
|
|
3
|
+
This module provides API endpoints that proxy graph database operations
|
|
4
|
+
to the internal FalkorDB instance. This allows the CLI to perform graph
|
|
5
|
+
operations without direct database access.
|
|
6
|
+
|
|
7
|
+
All operations are authenticated via API key and scoped to the user's
|
|
8
|
+
organization graph.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, Depends, Header, HTTPException, status
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
from sqlalchemy import select
|
|
19
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
20
|
+
|
|
21
|
+
from repotoire.api.shared.auth.clerk import get_clerk_client
|
|
22
|
+
from repotoire.db.models import Organization, OrganizationMembership, User
|
|
23
|
+
from repotoire.db.session import get_db
|
|
24
|
+
from repotoire.graph.tenant_factory import get_factory
|
|
25
|
+
from repotoire.logging_config import get_logger
|
|
26
|
+
from repotoire.models import (
|
|
27
|
+
Entity,
|
|
28
|
+
FileEntity,
|
|
29
|
+
ClassEntity,
|
|
30
|
+
FunctionEntity,
|
|
31
|
+
ModuleEntity,
|
|
32
|
+
NodeType,
|
|
33
|
+
Relationship,
|
|
34
|
+
RelationshipType,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
router = APIRouter(prefix="/graph", tags=["graph"])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# API Key Authentication
|
|
44
|
+
# =============================================================================
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class APIKeyUser:
|
|
49
|
+
"""Authenticated user from API key."""
|
|
50
|
+
|
|
51
|
+
org_id: str # Our internal org UUID
|
|
52
|
+
org_slug: str
|
|
53
|
+
user_id: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def get_current_api_key_user(
|
|
57
|
+
db: AsyncSession = Depends(get_db),
|
|
58
|
+
authorization: Optional[str] = Header(None, alias="Authorization"),
|
|
59
|
+
) -> APIKeyUser:
|
|
60
|
+
"""Validate API key and return authenticated user with org info."""
|
|
61
|
+
if not authorization:
|
|
62
|
+
raise HTTPException(
|
|
63
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
64
|
+
detail="Missing Authorization header",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
parts = authorization.split(" ", 1)
|
|
68
|
+
if len(parts) != 2 or parts[0].lower() != "bearer":
|
|
69
|
+
raise HTTPException(
|
|
70
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
71
|
+
detail="Invalid Authorization format. Use: Bearer <api_key>",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
api_key = parts[1]
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
clerk = get_clerk_client()
|
|
78
|
+
api_key_data = await asyncio.to_thread(
|
|
79
|
+
clerk.api_keys.verify_api_key, secret=api_key
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
subject = api_key_data.subject
|
|
83
|
+
clerk_org_id = None
|
|
84
|
+
org = None
|
|
85
|
+
|
|
86
|
+
if subject.startswith("org_"):
|
|
87
|
+
clerk_org_id = subject
|
|
88
|
+
elif hasattr(api_key_data, "org_id") and api_key_data.org_id:
|
|
89
|
+
clerk_org_id = api_key_data.org_id
|
|
90
|
+
elif subject.startswith("user_"):
|
|
91
|
+
# User-scoped key - look up user's organization
|
|
92
|
+
result = await db.execute(
|
|
93
|
+
select(User).where(User.clerk_user_id == subject)
|
|
94
|
+
)
|
|
95
|
+
db_user = result.scalar_one_or_none()
|
|
96
|
+
if not db_user:
|
|
97
|
+
raise HTTPException(
|
|
98
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
99
|
+
detail="User not found",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
result = await db.execute(
|
|
103
|
+
select(OrganizationMembership).where(
|
|
104
|
+
OrganizationMembership.user_id == db_user.id
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
membership = result.scalar_one_or_none()
|
|
108
|
+
if membership:
|
|
109
|
+
result = await db.execute(
|
|
110
|
+
select(Organization).where(
|
|
111
|
+
Organization.id == membership.organization_id
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
org = result.scalar_one_or_none()
|
|
115
|
+
if org:
|
|
116
|
+
clerk_org_id = org.clerk_org_id
|
|
117
|
+
|
|
118
|
+
# Look up org by Clerk ID
|
|
119
|
+
if not org and clerk_org_id:
|
|
120
|
+
result = await db.execute(
|
|
121
|
+
select(Organization).where(Organization.clerk_org_id == clerk_org_id)
|
|
122
|
+
)
|
|
123
|
+
org = result.scalar_one_or_none()
|
|
124
|
+
|
|
125
|
+
if not org:
|
|
126
|
+
raise HTTPException(
|
|
127
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
128
|
+
detail="Organization not found",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return APIKeyUser(
|
|
132
|
+
org_id=str(org.id),
|
|
133
|
+
org_slug=org.slug,
|
|
134
|
+
user_id=subject if subject.startswith("user_") else None,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
except HTTPException:
|
|
138
|
+
raise
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"API key validation failed: {e}")
|
|
141
|
+
raise HTTPException(
|
|
142
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
143
|
+
detail="Invalid or expired API key",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# =============================================================================
|
|
148
|
+
# Request/Response Models
|
|
149
|
+
# =============================================================================
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class QueryRequest(BaseModel):
|
|
153
|
+
"""Request to execute a Cypher query."""
|
|
154
|
+
|
|
155
|
+
query: str = Field(..., description="Cypher query to execute")
|
|
156
|
+
parameters: Optional[Dict[str, Any]] = Field(
|
|
157
|
+
default=None, description="Query parameters"
|
|
158
|
+
)
|
|
159
|
+
timeout: Optional[float] = Field(
|
|
160
|
+
default=None, description="Query timeout in seconds"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class QueryResponse(BaseModel):
|
|
165
|
+
"""Response from a Cypher query."""
|
|
166
|
+
|
|
167
|
+
results: List[Dict[str, Any]] = Field(..., description="Query results")
|
|
168
|
+
count: int = Field(..., description="Number of results")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class StatsResponse(BaseModel):
|
|
172
|
+
"""Graph statistics response."""
|
|
173
|
+
|
|
174
|
+
stats: Dict[str, int] = Field(..., description="Node/relationship counts")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class BatchNodesRequest(BaseModel):
|
|
178
|
+
"""Request to batch create nodes."""
|
|
179
|
+
|
|
180
|
+
entities: List[Dict[str, Any]] = Field(..., description="Entities to create")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class BatchNodesResponse(BaseModel):
|
|
184
|
+
"""Response from batch node creation."""
|
|
185
|
+
|
|
186
|
+
created: Dict[str, str] = Field(
|
|
187
|
+
..., description="Map of qualified_name to node ID"
|
|
188
|
+
)
|
|
189
|
+
count: int = Field(..., description="Number of nodes created")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class BatchRelationshipsRequest(BaseModel):
|
|
193
|
+
"""Request to batch create relationships."""
|
|
194
|
+
|
|
195
|
+
relationships: List[Dict[str, Any]] = Field(
|
|
196
|
+
..., description="Relationships to create"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class BatchRelationshipsResponse(BaseModel):
|
|
201
|
+
"""Response from batch relationship creation."""
|
|
202
|
+
|
|
203
|
+
count: int = Field(..., description="Number of relationships created")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class FileMetadataResponse(BaseModel):
|
|
207
|
+
"""File metadata for incremental ingestion."""
|
|
208
|
+
|
|
209
|
+
metadata: Optional[Dict[str, Any]] = Field(
|
|
210
|
+
None, description="File metadata or None if not found"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class FilePathsResponse(BaseModel):
|
|
215
|
+
"""List of file paths in the graph."""
|
|
216
|
+
|
|
217
|
+
paths: List[str] = Field(..., description="File paths")
|
|
218
|
+
count: int = Field(..., description="Number of files")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class DeleteResponse(BaseModel):
|
|
222
|
+
"""Response from delete operation."""
|
|
223
|
+
|
|
224
|
+
deleted: int = Field(..., description="Number of items deleted")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# =============================================================================
|
|
228
|
+
# Helper Functions
|
|
229
|
+
# =============================================================================
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _get_client_for_user(user: APIKeyUser):
|
|
233
|
+
"""Get graph client scoped to user's organization."""
|
|
234
|
+
factory = get_factory()
|
|
235
|
+
return factory.get_client(org_id=UUID(user.org_id), org_slug=user.org_slug)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# =============================================================================
|
|
239
|
+
# Endpoints
|
|
240
|
+
# =============================================================================
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@router.post("/query", response_model=QueryResponse)
|
|
244
|
+
async def execute_query(
|
|
245
|
+
request: QueryRequest,
|
|
246
|
+
user: APIKeyUser = Depends(get_current_api_key_user),
|
|
247
|
+
) -> QueryResponse:
|
|
248
|
+
"""Execute a Cypher query on the organization's graph."""
|
|
249
|
+
client = _get_client_for_user(user)
|
|
250
|
+
try:
|
|
251
|
+
results = client.execute_query(
|
|
252
|
+
request.query,
|
|
253
|
+
parameters=request.parameters,
|
|
254
|
+
timeout=request.timeout,
|
|
255
|
+
)
|
|
256
|
+
return QueryResponse(results=results, count=len(results))
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.error(f"Query failed: {e}", org_id=user.org_id)
|
|
259
|
+
raise HTTPException(
|
|
260
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
261
|
+
detail=f"Query failed: {str(e)}",
|
|
262
|
+
)
|
|
263
|
+
finally:
|
|
264
|
+
client.close()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@router.get("/stats", response_model=StatsResponse)
|
|
268
|
+
async def get_stats(
|
|
269
|
+
user: APIKeyUser = Depends(get_current_api_key_user),
|
|
270
|
+
) -> StatsResponse:
|
|
271
|
+
"""Get graph statistics."""
|
|
272
|
+
client = _get_client_for_user(user)
|
|
273
|
+
try:
|
|
274
|
+
stats = client.get_stats()
|
|
275
|
+
return StatsResponse(stats=stats)
|
|
276
|
+
finally:
|
|
277
|
+
client.close()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@router.delete("/clear", response_model=DeleteResponse)
|
|
281
|
+
async def clear_graph(
|
|
282
|
+
user: APIKeyUser = Depends(get_current_api_key_user),
|
|
283
|
+
) -> DeleteResponse:
|
|
284
|
+
"""Clear all nodes and relationships from the graph."""
|
|
285
|
+
client = _get_client_for_user(user)
|
|
286
|
+
try:
|
|
287
|
+
# Get count before clearing for response
|
|
288
|
+
stats = client.get_stats()
|
|
289
|
+
total = sum(stats.values())
|
|
290
|
+
client.clear_graph()
|
|
291
|
+
logger.info(f"Graph cleared", org_id=user.org_id, deleted=total)
|
|
292
|
+
return DeleteResponse(deleted=total)
|
|
293
|
+
finally:
|
|
294
|
+
client.close()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@router.post("/batch/nodes", response_model=BatchNodesResponse)
|
|
298
|
+
async def batch_create_nodes(
|
|
299
|
+
request: BatchNodesRequest,
|
|
300
|
+
user: APIKeyUser = Depends(get_current_api_key_user),
|
|
301
|
+
) -> BatchNodesResponse:
|
|
302
|
+
"""Batch create nodes in the graph."""
|
|
303
|
+
client = _get_client_for_user(user)
|
|
304
|
+
try:
|
|
305
|
+
# Convert dicts to appropriate Entity subclass
|
|
306
|
+
entities = []
|
|
307
|
+
for e in request.entities:
|
|
308
|
+
entity_type = e.get("entity_type", "Unknown")
|
|
309
|
+
node_type = NodeType(entity_type) if entity_type in [t.value for t in NodeType] else None
|
|
310
|
+
|
|
311
|
+
# Common fields for all entities
|
|
312
|
+
base_fields = {
|
|
313
|
+
"name": e["name"],
|
|
314
|
+
"qualified_name": e["qualified_name"],
|
|
315
|
+
"file_path": e.get("file_path", ""),
|
|
316
|
+
"line_start": e.get("line_start", 0),
|
|
317
|
+
"line_end": e.get("line_end", 0),
|
|
318
|
+
"docstring": e.get("docstring"),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Create appropriate entity type
|
|
322
|
+
if entity_type == "File":
|
|
323
|
+
entity = FileEntity(
|
|
324
|
+
**base_fields,
|
|
325
|
+
node_type=NodeType.FILE,
|
|
326
|
+
language=e.get("language", "python"),
|
|
327
|
+
loc=e.get("loc", 0),
|
|
328
|
+
hash=e.get("hash"),
|
|
329
|
+
exports=e.get("exports", []),
|
|
330
|
+
)
|
|
331
|
+
elif entity_type == "Class":
|
|
332
|
+
entity = ClassEntity(
|
|
333
|
+
**base_fields,
|
|
334
|
+
node_type=NodeType.CLASS,
|
|
335
|
+
is_abstract=e.get("is_abstract", False),
|
|
336
|
+
complexity=e.get("complexity", 0),
|
|
337
|
+
decorators=e.get("decorators", []),
|
|
338
|
+
)
|
|
339
|
+
elif entity_type == "Function":
|
|
340
|
+
entity = FunctionEntity(
|
|
341
|
+
**base_fields,
|
|
342
|
+
node_type=NodeType.FUNCTION,
|
|
343
|
+
parameters=e.get("parameters", []),
|
|
344
|
+
return_type=e.get("return_type"),
|
|
345
|
+
is_async=e.get("is_async", False),
|
|
346
|
+
decorators=e.get("decorators", []),
|
|
347
|
+
complexity=e.get("complexity", 0),
|
|
348
|
+
is_method=e.get("is_method", False),
|
|
349
|
+
is_static=e.get("is_static", False),
|
|
350
|
+
is_classmethod=e.get("is_classmethod", False),
|
|
351
|
+
is_property=e.get("is_property", False),
|
|
352
|
+
)
|
|
353
|
+
elif entity_type == "Module":
|
|
354
|
+
entity = ModuleEntity(
|
|
355
|
+
**base_fields,
|
|
356
|
+
node_type=NodeType.MODULE,
|
|
357
|
+
is_external=e.get("is_external", False),
|
|
358
|
+
package=e.get("package"),
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
# Fall back to base Entity for unknown types
|
|
362
|
+
entity = Entity(
|
|
363
|
+
**base_fields,
|
|
364
|
+
node_type=node_type,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Set repo_id and repo_slug for multi-tenant isolation
|
|
368
|
+
if e.get("repo_id"):
|
|
369
|
+
entity.repo_id = e["repo_id"]
|
|
370
|
+
if e.get("repo_slug"):
|
|
371
|
+
entity.repo_slug = e["repo_slug"]
|
|
372
|
+
|
|
373
|
+
entities.append(entity)
|
|
374
|
+
|
|
375
|
+
created = client.batch_create_nodes(entities)
|
|
376
|
+
return BatchNodesResponse(created=created, count=len(created))
|
|
377
|
+
except Exception as e:
|
|
378
|
+
logger.error(f"Batch create nodes failed: {e}", org_id=user.org_id)
|
|
379
|
+
raise HTTPException(
|
|
380
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
381
|
+
detail=f"Batch create failed: {str(e)}",
|
|
382
|
+
)
|
|
383
|
+
finally:
|
|
384
|
+
client.close()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@router.post("/batch/relationships", response_model=BatchRelationshipsResponse)
|
|
388
|
+
async def batch_create_relationships(
|
|
389
|
+
request: BatchRelationshipsRequest,
|
|
390
|
+
user: APIKeyUser = Depends(get_current_api_key_user),
|
|
391
|
+
) -> BatchRelationshipsResponse:
|
|
392
|
+
"""Batch create relationships in the graph."""
|
|
393
|
+
client = _get_client_for_user(user)
|
|
394
|
+
try:
|
|
395
|
+
# Convert dicts to Relationship objects
|
|
396
|
+
relationships = []
|
|
397
|
+
for r in request.relationships:
|
|
398
|
+
# Parse rel_type from string to enum
|
|
399
|
+
rel_type_str = r.get("rel_type", "CALLS")
|
|
400
|
+
try:
|
|
401
|
+
rel_type = RelationshipType(rel_type_str)
|
|
402
|
+
except ValueError:
|
|
403
|
+
rel_type = RelationshipType.CALLS # Default fallback
|
|
404
|
+
|
|
405
|
+
rel = Relationship(
|
|
406
|
+
source_id=r["source_id"],
|
|
407
|
+
target_id=r["target_id"],
|
|
408
|
+
rel_type=rel_type,
|
|
409
|
+
properties=r.get("properties", {}),
|
|
410
|
+
)
|
|
411
|
+
relationships.append(rel)
|
|
412
|
+
|
|
413
|
+
count = client.batch_create_relationships(relationships)
|
|
414
|
+
return BatchRelationshipsResponse(count=count)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.error(f"Batch create relationships failed: {e}", org_id=user.org_id)
|
|
417
|
+
raise HTTPException(
|
|
418
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
419
|
+
detail=f"Batch create failed: {str(e)}",
|
|
420
|
+
)
|
|
421
|
+
finally:
|
|
422
|
+
client.close()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@router.get("/files", response_model=FilePathsResponse)
|
|
426
|
+
async def get_file_paths(
|
|
427
|
+
user: APIKeyUser = Depends(get_current_api_key_user),
|
|
428
|
+
) -> FilePathsResponse:
|
|
429
|
+
"""Get all file paths in the graph."""
|
|
430
|
+
client = _get_client_for_user(user)
|
|
431
|
+
try:
|
|
432
|
+
paths = client.get_all_file_paths()
|
|
433
|
+
return FilePathsResponse(paths=paths, count=len(paths))
|
|
434
|
+
finally:
|
|
435
|
+
client.close()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@router.get("/files/{file_path:path}/metadata", response_model=FileMetadataResponse)
|
|
439
|
+
async def get_file_metadata(
|
|
440
|
+
file_path: str,
|
|
441
|
+
user: APIKeyUser = Depends(get_current_api_key_user),
|
|
442
|
+
) -> FileMetadataResponse:
|
|
443
|
+
"""Get metadata for a specific file (for incremental ingestion)."""
|
|
444
|
+
client = _get_client_for_user(user)
|
|
445
|
+
try:
|
|
446
|
+
metadata = client.get_file_metadata(file_path)
|
|
447
|
+
return FileMetadataResponse(metadata=metadata)
|
|
448
|
+
finally:
|
|
449
|
+
client.close()
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@router.delete("/files/{file_path:path}", response_model=DeleteResponse)
|
|
453
|
+
async def delete_file(
|
|
454
|
+
file_path: str,
|
|
455
|
+
user: APIKeyUser = Depends(get_current_api_key_user),
|
|
456
|
+
) -> DeleteResponse:
|
|
457
|
+
"""Delete a file and its related entities from the graph."""
|
|
458
|
+
client = _get_client_for_user(user)
|
|
459
|
+
try:
|
|
460
|
+
deleted = client.delete_file_entities(file_path)
|
|
461
|
+
return DeleteResponse(deleted=deleted)
|
|
462
|
+
finally:
|
|
463
|
+
client.close()
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@router.post("/indexes")
|
|
467
|
+
async def create_indexes(
|
|
468
|
+
user: APIKeyUser = Depends(get_current_api_key_user),
|
|
469
|
+
) -> Dict[str, str]:
|
|
470
|
+
"""Create indexes for better query performance."""
|
|
471
|
+
client = _get_client_for_user(user)
|
|
472
|
+
try:
|
|
473
|
+
client.create_indexes()
|
|
474
|
+
return {"status": "ok", "message": "Indexes created"}
|
|
475
|
+
finally:
|
|
476
|
+
client.close()
|
repotoire/cli/__init__.py
CHANGED
|
@@ -445,23 +445,6 @@ def whoami() -> None:
|
|
|
445
445
|
|
|
446
446
|
@cli.command()
|
|
447
447
|
@click.argument("repo_path", type=click.Path(exists=True))
|
|
448
|
-
@click.option(
|
|
449
|
-
"--db-type",
|
|
450
|
-
type=click.Choice(["neo4j", "falkordb"], case_sensitive=False),
|
|
451
|
-
default=None,
|
|
452
|
-
envvar="REPOTOIRE_DB_TYPE",
|
|
453
|
-
help="Database type: neo4j or falkordb (default: neo4j, or REPOTOIRE_DB_TYPE env)",
|
|
454
|
-
)
|
|
455
|
-
@click.option(
|
|
456
|
-
"--neo4j-uri", default=None, help="Neo4j connection URI (overrides config)"
|
|
457
|
-
)
|
|
458
|
-
@click.option("--neo4j-user", default=None, help="Neo4j username (overrides config)")
|
|
459
|
-
@click.option(
|
|
460
|
-
"--neo4j-password",
|
|
461
|
-
default=None,
|
|
462
|
-
envvar="REPOTOIRE_NEO4J_PASSWORD",
|
|
463
|
-
help="Neo4j password (overrides config, prompts if not provided for neo4j)",
|
|
464
|
-
)
|
|
465
448
|
@click.option(
|
|
466
449
|
"--pattern",
|
|
467
450
|
"-p",
|
|
@@ -556,10 +539,6 @@ def whoami() -> None:
|
|
|
556
539
|
def ingest(
|
|
557
540
|
ctx: click.Context,
|
|
558
541
|
repo_path: str,
|
|
559
|
-
db_type: str | None,
|
|
560
|
-
neo4j_uri: str | None,
|
|
561
|
-
neo4j_user: str | None,
|
|
562
|
-
neo4j_password: str | None,
|
|
563
542
|
pattern: tuple | None,
|
|
564
543
|
follow_symlinks: bool | None,
|
|
565
544
|
max_file_size: float | None,
|
|
@@ -579,11 +558,15 @@ def ingest(
|
|
|
579
558
|
"""Ingest a codebase into the knowledge graph.
|
|
580
559
|
|
|
581
560
|
\b
|
|
582
|
-
Parses source code and builds a
|
|
561
|
+
Parses source code and builds a knowledge graph containing:
|
|
583
562
|
- Files, modules, classes, functions, and variables
|
|
584
563
|
- Relationships: IMPORTS, CALLS, CONTAINS, INHERITS, USES
|
|
585
564
|
- Optional: AI-powered semantic clues and vector embeddings
|
|
586
565
|
|
|
566
|
+
\b
|
|
567
|
+
PREREQUISITES:
|
|
568
|
+
Login first: repotoire login
|
|
569
|
+
|
|
587
570
|
\b
|
|
588
571
|
EXAMPLES:
|
|
589
572
|
# Basic ingestion
|
|
@@ -595,9 +578,6 @@ def ingest(
|
|
|
595
578
|
# Force full re-ingestion (ignore cache)
|
|
596
579
|
$ repotoire ingest ./my-project --force-full
|
|
597
580
|
|
|
598
|
-
# Use FalkorDB instead of Neo4j
|
|
599
|
-
$ repotoire ingest ./my-project --db-type falkordb
|
|
600
|
-
|
|
601
581
|
\b
|
|
602
582
|
INCREMENTAL MODE (default):
|
|
603
583
|
Only processes files changed since last ingestion. Uses MD5 hashes
|
|
@@ -611,11 +591,6 @@ def ingest(
|
|
|
611
591
|
- File size limits (10MB default)
|
|
612
592
|
- Secrets detection with configurable policy
|
|
613
593
|
|
|
614
|
-
\b
|
|
615
|
-
DATABASE BACKENDS:
|
|
616
|
-
neo4j Full-featured graph database (recommended)
|
|
617
|
-
falkordb Lightweight Redis-based alternative (faster startup)
|
|
618
|
-
|
|
619
594
|
\b
|
|
620
595
|
EMBEDDING BACKENDS:
|
|
621
596
|
auto Auto-select best available (default)
|
|
@@ -626,9 +601,7 @@ def ingest(
|
|
|
626
601
|
|
|
627
602
|
\b
|
|
628
603
|
ENVIRONMENT VARIABLES:
|
|
629
|
-
|
|
630
|
-
REPOTOIRE_NEO4J_PASSWORD Neo4j password
|
|
631
|
-
REPOTOIRE_DB_TYPE Database type (neo4j/falkordb)
|
|
604
|
+
REPOTOIRE_API_KEY API key (or use 'repotoire login')
|
|
632
605
|
OPENAI_API_KEY For OpenAI embeddings
|
|
633
606
|
VOYAGE_API_KEY For Voyage embeddings
|
|
634
607
|
DEEPINFRA_API_KEY For DeepInfra embeddings
|
|
@@ -636,24 +609,12 @@ def ingest(
|
|
|
636
609
|
# Get config from context
|
|
637
610
|
config: FalkorConfig = ctx.obj['config']
|
|
638
611
|
|
|
639
|
-
# Determine database type
|
|
640
|
-
if db_type:
|
|
641
|
-
final_db_type = db_type
|
|
642
|
-
elif hasattr(config.neo4j, 'db_type') and config.neo4j.db_type:
|
|
643
|
-
final_db_type = config.neo4j.db_type
|
|
644
|
-
else:
|
|
645
|
-
final_db_type = "neo4j"
|
|
646
|
-
use_falkordb = final_db_type.lower() == "falkordb"
|
|
647
|
-
|
|
648
612
|
# Validate inputs before execution
|
|
649
613
|
try:
|
|
650
614
|
# Validate repository path
|
|
651
615
|
validated_repo_path = validate_repository_path(repo_path)
|
|
652
616
|
|
|
653
617
|
# Apply config defaults (CLI options override config)
|
|
654
|
-
final_neo4j_uri = neo4j_uri or config.neo4j.uri
|
|
655
|
-
final_neo4j_user = neo4j_user or config.neo4j.user
|
|
656
|
-
final_neo4j_password = neo4j_password or config.neo4j.password
|
|
657
618
|
final_patterns = list(pattern) if pattern else config.ingestion.patterns
|
|
658
619
|
final_follow_symlinks = follow_symlinks if follow_symlinks is not None else config.ingestion.follow_symlinks
|
|
659
620
|
final_max_file_size = max_file_size if max_file_size is not None else config.ingestion.max_file_size_mb
|
|
@@ -667,43 +628,12 @@ def ingest(
|
|
|
667
628
|
# Convert secrets policy string to enum
|
|
668
629
|
final_secrets_policy = SecretsPolicy(final_secrets_policy_str)
|
|
669
630
|
|
|
670
|
-
if use_falkordb:
|
|
671
|
-
# FalkorDB validation - simpler, no auth required
|
|
672
|
-
console.print("[dim]Checking FalkorDB connectivity...[/dim]")
|
|
673
|
-
# FalkorDB uses Redis protocol, not bolt
|
|
674
|
-
# Password is optional for FalkorDB
|
|
675
|
-
else:
|
|
676
|
-
# Neo4j validation
|
|
677
|
-
# Validate Neo4j URI
|
|
678
|
-
final_neo4j_uri = validate_neo4j_uri(final_neo4j_uri)
|
|
679
|
-
|
|
680
|
-
# Prompt for password if not provided
|
|
681
|
-
if not final_neo4j_password:
|
|
682
|
-
final_neo4j_password = click.prompt("Neo4j password", hide_input=True)
|
|
683
|
-
|
|
684
|
-
# Validate credentials
|
|
685
|
-
final_neo4j_user, final_neo4j_password = validate_neo4j_credentials(
|
|
686
|
-
final_neo4j_user, final_neo4j_password
|
|
687
|
-
)
|
|
688
|
-
|
|
689
|
-
# Test Neo4j connection is reachable
|
|
690
|
-
console.print("[dim]Checking Neo4j connectivity...[/dim]")
|
|
691
|
-
validate_neo4j_connection(final_neo4j_uri, final_neo4j_user, final_neo4j_password)
|
|
692
|
-
console.print("[green]✓[/green] Neo4j connection validated\n")
|
|
693
|
-
|
|
694
631
|
# Validate file size limit
|
|
695
632
|
final_max_file_size = validate_file_size_limit(final_max_file_size)
|
|
696
633
|
|
|
697
634
|
# Validate batch size
|
|
698
635
|
validated_batch_size = validate_batch_size(final_batch_size)
|
|
699
636
|
|
|
700
|
-
# Validate retry configuration
|
|
701
|
-
validated_retries = validate_retry_config(
|
|
702
|
-
config.neo4j.max_retries,
|
|
703
|
-
config.neo4j.retry_backoff_factor,
|
|
704
|
-
config.neo4j.retry_base_delay
|
|
705
|
-
)
|
|
706
|
-
|
|
707
637
|
except ValidationError as e:
|
|
708
638
|
console.print(f"\n[red]❌ Validation Error:[/red] {e.message}")
|
|
709
639
|
if e.suggestion:
|
|
@@ -712,7 +642,6 @@ def ingest(
|
|
|
712
642
|
|
|
713
643
|
console.print(f"\n[bold cyan]🎼 Repotoire Ingestion[/bold cyan]\n")
|
|
714
644
|
console.print(f"Repository: {repo_path}")
|
|
715
|
-
console.print(f"Database: {final_db_type}")
|
|
716
645
|
console.print(f"Patterns: {', '.join(final_patterns)}")
|
|
717
646
|
console.print(f"Follow symlinks: {final_follow_symlinks}")
|
|
718
647
|
console.print(f"Max file size: {final_max_file_size}MB")
|
|
@@ -741,19 +670,8 @@ def ingest(
|
|
|
741
670
|
with LogContext(operation="ingest", repo_path=repo_path):
|
|
742
671
|
logger.info("Starting ingestion")
|
|
743
672
|
|
|
744
|
-
# Create database client
|
|
745
|
-
|
|
746
|
-
db = create_client(db_type="falkordb")
|
|
747
|
-
else:
|
|
748
|
-
db = create_client(
|
|
749
|
-
uri=final_neo4j_uri,
|
|
750
|
-
db_type="neo4j",
|
|
751
|
-
username=final_neo4j_user,
|
|
752
|
-
password=final_neo4j_password,
|
|
753
|
-
max_retries=validated_retries[0],
|
|
754
|
-
retry_backoff_factor=validated_retries[1],
|
|
755
|
-
retry_base_delay=validated_retries[2],
|
|
756
|
-
)
|
|
673
|
+
# Create database client (connects to Repotoire Cloud)
|
|
674
|
+
db = _get_db_client(quiet=quiet)
|
|
757
675
|
|
|
758
676
|
try:
|
|
759
677
|
# Clear database if force-full is requested
|
repotoire/cli/auth.py
CHANGED
|
@@ -39,6 +39,22 @@ class AuthenticationError(Exception):
|
|
|
39
39
|
pass
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
@dataclass
|
|
43
|
+
class CLICredentials:
|
|
44
|
+
"""Credentials for CLI authentication.
|
|
45
|
+
|
|
46
|
+
Contains the access token (API key) and optional metadata
|
|
47
|
+
for making authenticated API requests.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
access_token: str
|
|
51
|
+
org_id: Optional[str] = None
|
|
52
|
+
org_slug: Optional[str] = None
|
|
53
|
+
plan: Optional[str] = None
|
|
54
|
+
user_email: Optional[str] = None
|
|
55
|
+
user_id: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
|
|
42
58
|
@dataclass
|
|
43
59
|
class OAuthCallbackResult:
|
|
44
60
|
"""Result from OAuth callback."""
|
|
@@ -310,6 +326,23 @@ class CLIAuth:
|
|
|
310
326
|
if source:
|
|
311
327
|
console.print(f"[bold]Stored in:[/] {source}")
|
|
312
328
|
|
|
329
|
+
def get_current_user(self) -> Optional[CLICredentials]:
|
|
330
|
+
"""Get current user credentials if authenticated.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
CLICredentials if logged in, None otherwise
|
|
334
|
+
"""
|
|
335
|
+
api_key = self.get_api_key()
|
|
336
|
+
if not api_key:
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
return CLICredentials(access_token=api_key)
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def api_url(self) -> str:
|
|
343
|
+
"""Get API URL for tier limit checks."""
|
|
344
|
+
return os.environ.get("REPOTOIRE_API_URL", "https://repotoire-api.fly.dev")
|
|
345
|
+
|
|
313
346
|
|
|
314
347
|
def is_offline_mode() -> bool:
|
|
315
348
|
"""Check if running in offline mode.
|
|
@@ -90,7 +90,7 @@ class AsyncAntipatternDetector(CodeSmellDetector):
|
|
|
90
90
|
"""
|
|
91
91
|
super().__init__(neo4j_client)
|
|
92
92
|
self.enricher = enricher
|
|
93
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
93
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
94
94
|
|
|
95
95
|
# Allow config to override thresholds
|
|
96
96
|
config = detector_config or {}
|
|
@@ -42,7 +42,7 @@ class CircularDependencyDetector(CodeSmellDetector):
|
|
|
42
42
|
super().__init__(neo4j_client)
|
|
43
43
|
self.enricher = enricher
|
|
44
44
|
# FalkorDB uses id() while Neo4j uses elementId()
|
|
45
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
45
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
46
46
|
self.id_func = "id" if self.is_falkordb else "elementId"
|
|
47
47
|
|
|
48
48
|
def detect(self) -> List[Finding]:
|
|
@@ -75,7 +75,7 @@ class DataClumpsDetector(CodeSmellDetector):
|
|
|
75
75
|
super().__init__(neo4j_client)
|
|
76
76
|
self.enricher = enricher
|
|
77
77
|
# FalkorDB uses id() while Neo4j uses elementId()
|
|
78
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
78
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
79
79
|
|
|
80
80
|
# Allow config to override thresholds
|
|
81
81
|
config = detector_config or {}
|
repotoire/detectors/dead_code.py
CHANGED
|
@@ -36,7 +36,7 @@ class DeadCodeDetector(CodeSmellDetector):
|
|
|
36
36
|
self.validated_confidence = 0.95 # When Vulture confirms
|
|
37
37
|
|
|
38
38
|
# FalkorDB doesn't support EXISTS {} subqueries
|
|
39
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
39
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
40
40
|
|
|
41
41
|
@property
|
|
42
42
|
def needs_previous_findings(self) -> bool:
|
repotoire/detectors/engine.py
CHANGED
|
@@ -132,7 +132,7 @@ class AnalysisEngine:
|
|
|
132
132
|
self.parallel = parallel
|
|
133
133
|
self.max_workers = max_workers
|
|
134
134
|
# Check if using FalkorDB (no GDS support)
|
|
135
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
135
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
136
136
|
config = detector_config or {}
|
|
137
137
|
|
|
138
138
|
# Initialize GraphEnricher for cross-detector collaboration (REPO-151 Phase 2)
|
|
@@ -65,7 +65,7 @@ class GeneratorMisuseDetector(CodeSmellDetector):
|
|
|
65
65
|
"""
|
|
66
66
|
super().__init__(neo4j_client)
|
|
67
67
|
self.enricher = enricher
|
|
68
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
68
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
69
69
|
|
|
70
70
|
# Allow config to override thresholds
|
|
71
71
|
config = detector_config or {}
|
|
@@ -150,7 +150,7 @@ class GraphAlgorithms:
|
|
|
150
150
|
True if GDS is available, False otherwise
|
|
151
151
|
"""
|
|
152
152
|
# FalkorDB doesn't have GDS - skip immediately without retry
|
|
153
|
-
if type(self.client).__name__ == "FalkorDBClient":
|
|
153
|
+
if getattr(self.client, "is_falkordb", False) or type(self.client).__name__ == "FalkorDBClient":
|
|
154
154
|
logger.info("FalkorDB detected - GDS not available (using Rust algorithms)")
|
|
155
155
|
return False
|
|
156
156
|
|
|
@@ -31,7 +31,7 @@ class InappropriateIntimacyDetector(CodeSmellDetector):
|
|
|
31
31
|
self.min_mutual_access = config.get("min_mutual_access", 5)
|
|
32
32
|
self.logger = get_logger(__name__)
|
|
33
33
|
# FalkorDB uses id() while Neo4j uses elementId()
|
|
34
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
34
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
35
35
|
self.id_func = "id" if self.is_falkordb else "elementId"
|
|
36
36
|
|
|
37
37
|
def detect(self) -> List[Finding]:
|
|
@@ -58,7 +58,7 @@ class LongParameterListDetector(CodeSmellDetector):
|
|
|
58
58
|
"""
|
|
59
59
|
super().__init__(neo4j_client)
|
|
60
60
|
self.enricher = enricher
|
|
61
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
61
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
62
62
|
|
|
63
63
|
# Allow config to override thresholds
|
|
64
64
|
config = detector_config or {}
|
|
@@ -68,7 +68,7 @@ class MessageChainDetector(CodeSmellDetector):
|
|
|
68
68
|
"""
|
|
69
69
|
super().__init__(neo4j_client)
|
|
70
70
|
self.enricher = enricher
|
|
71
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
71
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
72
72
|
|
|
73
73
|
# Allow config to override thresholds
|
|
74
74
|
config = detector_config or {}
|
|
@@ -125,7 +125,7 @@ class TestSmellDetector(CodeSmellDetector):
|
|
|
125
125
|
"""
|
|
126
126
|
super().__init__(neo4j_client)
|
|
127
127
|
self.enricher = enricher
|
|
128
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
128
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
129
129
|
|
|
130
130
|
# Allow config to override thresholds
|
|
131
131
|
config = detector_config or {}
|
|
@@ -70,7 +70,7 @@ class TypeHintCoverageDetector(CodeSmellDetector):
|
|
|
70
70
|
"""
|
|
71
71
|
super().__init__(neo4j_client)
|
|
72
72
|
self.enricher = enricher
|
|
73
|
-
self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
|
|
73
|
+
self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
|
|
74
74
|
|
|
75
75
|
# Allow config to override thresholds
|
|
76
76
|
config = detector_config or {}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Cloud proxy client for graph database operations.
|
|
2
|
+
|
|
3
|
+
This client proxies all graph operations through the Repotoire API,
|
|
4
|
+
allowing the CLI to work without direct database access.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from repotoire.graph.base import DatabaseClient
|
|
13
|
+
from repotoire.logging_config import get_logger
|
|
14
|
+
from repotoire.models import Entity, Relationship
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
DEFAULT_API_URL = "https://repotoire-api.fly.dev"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CloudProxyClient(DatabaseClient):
|
|
22
|
+
"""Graph database client that proxies through the Repotoire API.
|
|
23
|
+
|
|
24
|
+
All operations are sent to the API which executes them on the
|
|
25
|
+
internal FalkorDB instance. This allows the CLI to work without
|
|
26
|
+
direct database connectivity.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
api_key: str,
|
|
32
|
+
api_url: Optional[str] = None,
|
|
33
|
+
timeout: float = 60.0,
|
|
34
|
+
):
|
|
35
|
+
"""Initialize the cloud proxy client.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
api_key: Repotoire API key for authentication
|
|
39
|
+
api_url: API base URL (defaults to production)
|
|
40
|
+
timeout: Request timeout in seconds
|
|
41
|
+
"""
|
|
42
|
+
self.api_key = api_key
|
|
43
|
+
self.api_url = api_url or os.environ.get("REPOTOIRE_API_URL", DEFAULT_API_URL)
|
|
44
|
+
self.timeout = timeout
|
|
45
|
+
self._client = httpx.Client(
|
|
46
|
+
base_url=f"{self.api_url}/api/v1/graph",
|
|
47
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_falkordb(self) -> bool:
|
|
53
|
+
"""Cloud backend uses FalkorDB."""
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
"""Close the HTTP client."""
|
|
58
|
+
self._client.close()
|
|
59
|
+
|
|
60
|
+
def _request(
|
|
61
|
+
self,
|
|
62
|
+
method: str,
|
|
63
|
+
endpoint: str,
|
|
64
|
+
json: Optional[Dict] = None,
|
|
65
|
+
params: Optional[Dict] = None,
|
|
66
|
+
) -> Dict:
|
|
67
|
+
"""Make an API request.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
method: HTTP method
|
|
71
|
+
endpoint: API endpoint (relative to /api/v1/graph)
|
|
72
|
+
json: JSON body
|
|
73
|
+
params: Query parameters
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Response JSON
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
Exception: On API error
|
|
80
|
+
"""
|
|
81
|
+
response = self._client.request(
|
|
82
|
+
method=method,
|
|
83
|
+
url=endpoint,
|
|
84
|
+
json=json,
|
|
85
|
+
params=params,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if response.status_code >= 400:
|
|
89
|
+
try:
|
|
90
|
+
error = response.json()
|
|
91
|
+
detail = error.get("detail", str(error))
|
|
92
|
+
except Exception:
|
|
93
|
+
detail = response.text
|
|
94
|
+
raise Exception(f"API error ({response.status_code}): {detail}")
|
|
95
|
+
|
|
96
|
+
return response.json()
|
|
97
|
+
|
|
98
|
+
def execute_query(
|
|
99
|
+
self,
|
|
100
|
+
query: str,
|
|
101
|
+
parameters: Optional[Dict] = None,
|
|
102
|
+
timeout: Optional[float] = None,
|
|
103
|
+
) -> List[Dict]:
|
|
104
|
+
"""Execute a Cypher query via the API."""
|
|
105
|
+
response = self._request(
|
|
106
|
+
"POST",
|
|
107
|
+
"/query",
|
|
108
|
+
json={
|
|
109
|
+
"query": query,
|
|
110
|
+
"parameters": parameters,
|
|
111
|
+
"timeout": timeout,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
return response.get("results", [])
|
|
115
|
+
|
|
116
|
+
def create_node(self, entity: Entity) -> str:
|
|
117
|
+
"""Create a single node."""
|
|
118
|
+
result = self.batch_create_nodes([entity])
|
|
119
|
+
return result.get(entity.qualified_name, "")
|
|
120
|
+
|
|
121
|
+
def create_relationship(self, rel: Relationship) -> None:
|
|
122
|
+
"""Create a single relationship."""
|
|
123
|
+
self.batch_create_relationships([rel])
|
|
124
|
+
|
|
125
|
+
def batch_create_nodes(self, entities: List[Entity]) -> Dict[str, str]:
|
|
126
|
+
"""Create multiple nodes via the API."""
|
|
127
|
+
entity_dicts = []
|
|
128
|
+
for e in entities:
|
|
129
|
+
entity_dict = {
|
|
130
|
+
"entity_type": e.node_type.value if e.node_type else "Unknown",
|
|
131
|
+
"name": e.name,
|
|
132
|
+
"qualified_name": e.qualified_name,
|
|
133
|
+
"file_path": e.file_path,
|
|
134
|
+
"line_start": e.line_start,
|
|
135
|
+
"line_end": e.line_end,
|
|
136
|
+
"docstring": e.docstring,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Add repo_id and repo_slug for multi-tenant isolation
|
|
140
|
+
if e.repo_id:
|
|
141
|
+
entity_dict["repo_id"] = e.repo_id
|
|
142
|
+
if e.repo_slug:
|
|
143
|
+
entity_dict["repo_slug"] = e.repo_slug
|
|
144
|
+
|
|
145
|
+
# Add type-specific fields (matching FalkorDB client)
|
|
146
|
+
for attr in ["is_external", "package", "loc", "hash", "language",
|
|
147
|
+
"exports", "is_abstract", "complexity", "parameters",
|
|
148
|
+
"return_type", "is_async", "decorators", "is_method",
|
|
149
|
+
"is_static", "is_classmethod", "is_property"]:
|
|
150
|
+
if hasattr(e, attr):
|
|
151
|
+
val = getattr(e, attr)
|
|
152
|
+
if val is not None:
|
|
153
|
+
entity_dict[attr] = val
|
|
154
|
+
|
|
155
|
+
entity_dicts.append(entity_dict)
|
|
156
|
+
|
|
157
|
+
response = self._request(
|
|
158
|
+
"POST",
|
|
159
|
+
"/batch/nodes",
|
|
160
|
+
json={"entities": entity_dicts},
|
|
161
|
+
)
|
|
162
|
+
return response.get("created", {})
|
|
163
|
+
|
|
164
|
+
def batch_create_relationships(self, relationships: List[Relationship]) -> int:
|
|
165
|
+
"""Create multiple relationships via the API."""
|
|
166
|
+
rel_dicts = []
|
|
167
|
+
for r in relationships:
|
|
168
|
+
rel_dict = {
|
|
169
|
+
"source_id": r.source_id,
|
|
170
|
+
"target_id": r.target_id,
|
|
171
|
+
"rel_type": r.rel_type.value if hasattr(r.rel_type, 'value') else str(r.rel_type),
|
|
172
|
+
"properties": r.properties or {},
|
|
173
|
+
}
|
|
174
|
+
rel_dicts.append(rel_dict)
|
|
175
|
+
|
|
176
|
+
response = self._request(
|
|
177
|
+
"POST",
|
|
178
|
+
"/batch/relationships",
|
|
179
|
+
json={"relationships": rel_dicts},
|
|
180
|
+
)
|
|
181
|
+
return response.get("count", 0)
|
|
182
|
+
|
|
183
|
+
def clear_graph(self) -> None:
|
|
184
|
+
"""Clear all nodes and relationships."""
|
|
185
|
+
self._request("DELETE", "/clear")
|
|
186
|
+
|
|
187
|
+
def create_indexes(self) -> None:
|
|
188
|
+
"""Create indexes for better performance."""
|
|
189
|
+
self._request("POST", "/indexes")
|
|
190
|
+
|
|
191
|
+
def get_stats(self) -> Dict[str, int]:
|
|
192
|
+
"""Get graph statistics."""
|
|
193
|
+
response = self._request("GET", "/stats")
|
|
194
|
+
return response.get("stats", {})
|
|
195
|
+
|
|
196
|
+
def get_all_file_paths(self) -> List[str]:
|
|
197
|
+
"""Get all file paths in the graph."""
|
|
198
|
+
response = self._request("GET", "/files")
|
|
199
|
+
return response.get("paths", [])
|
|
200
|
+
|
|
201
|
+
def get_file_metadata(self, file_path: str) -> Optional[Dict[str, Any]]:
|
|
202
|
+
"""Get metadata for a specific file."""
|
|
203
|
+
try:
|
|
204
|
+
response = self._request(
|
|
205
|
+
"GET",
|
|
206
|
+
f"/files/{file_path}/metadata",
|
|
207
|
+
)
|
|
208
|
+
return response.get("metadata")
|
|
209
|
+
except Exception:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def delete_file_entities(self, file_path: str) -> int:
|
|
213
|
+
"""Delete a file and its related entities."""
|
|
214
|
+
response = self._request("DELETE", f"/files/{file_path}")
|
|
215
|
+
return response.get("deleted", 0)
|
repotoire/graph/factory.py
CHANGED
|
@@ -354,10 +354,11 @@ def create_cloud_client(
|
|
|
354
354
|
show_indicator: bool = True,
|
|
355
355
|
command: Optional[str] = None,
|
|
356
356
|
) -> DatabaseClient:
|
|
357
|
-
"""Create a cloud
|
|
357
|
+
"""Create a cloud proxy client.
|
|
358
358
|
|
|
359
|
-
|
|
360
|
-
|
|
359
|
+
Returns a client that proxies all graph operations through the
|
|
360
|
+
Repotoire API. This allows the CLI to work without direct database
|
|
361
|
+
connectivity.
|
|
361
362
|
|
|
362
363
|
Args:
|
|
363
364
|
api_key: Repotoire API key (starts with 'ak_' or 'rp_')
|
|
@@ -365,13 +366,13 @@ def create_cloud_client(
|
|
|
365
366
|
command: CLI command being executed (for audit logging)
|
|
366
367
|
|
|
367
368
|
Returns:
|
|
368
|
-
|
|
369
|
+
CloudProxyClient that proxies operations through the API
|
|
369
370
|
|
|
370
371
|
Raises:
|
|
371
372
|
CloudAuthenticationError: If API key is invalid or expired
|
|
372
373
|
CloudConnectionError: If cannot connect to Repotoire Cloud
|
|
373
374
|
"""
|
|
374
|
-
from repotoire.graph.
|
|
375
|
+
from repotoire.graph.cloud_client import CloudProxyClient
|
|
375
376
|
|
|
376
377
|
# Check cache first
|
|
377
378
|
auth_info = _get_cached_auth(api_key)
|
|
@@ -391,19 +392,8 @@ def create_cloud_client(
|
|
|
391
392
|
# Log connection to Neon (fire-and-forget, non-blocking)
|
|
392
393
|
_log_cloud_connection(api_key, auth_info, cached=used_cache, command=command)
|
|
393
394
|
|
|
394
|
-
# Create
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return FalkorDBClient(
|
|
398
|
-
host=db_config["host"],
|
|
399
|
-
port=db_config.get("port", 6379),
|
|
400
|
-
graph_name=db_config["graph"],
|
|
401
|
-
# REPO-395: Use derived password from API response
|
|
402
|
-
# The password is derived from the API key using HMAC-SHA256,
|
|
403
|
-
# so users never see the master FalkorDB password
|
|
404
|
-
password=db_config.get("password"),
|
|
405
|
-
ssl=db_config.get("ssl", False),
|
|
406
|
-
)
|
|
395
|
+
# Create cloud proxy client
|
|
396
|
+
return CloudProxyClient(api_key=api_key)
|
|
407
397
|
|
|
408
398
|
|
|
409
399
|
def _validate_api_key(api_key: str) -> CloudAuthInfo:
|
repotoire/graph/schema.py
CHANGED
|
@@ -201,8 +201,8 @@ class GraphSchema:
|
|
|
201
201
|
client: Neo4j or FalkorDB client instance
|
|
202
202
|
"""
|
|
203
203
|
self.client = client
|
|
204
|
-
# Detect if we're using FalkorDB
|
|
205
|
-
self.is_falkordb = type(client).__name__ == "FalkorDBClient"
|
|
204
|
+
# Detect if we're using FalkorDB (check property first, then class name)
|
|
205
|
+
self.is_falkordb = getattr(client, "is_falkordb", False) or type(client).__name__ == "FalkorDBClient"
|
|
206
206
|
|
|
207
207
|
def create_constraints(self) -> None:
|
|
208
208
|
"""Create all uniqueness constraints."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repotoire
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.13
|
|
11
11
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
12
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: falkordb>=1.0.0
|
|
13
13
|
Requires-Dist: openai>=1.0.0
|
|
14
14
|
Requires-Dist: spacy>=3.7.0
|
|
15
15
|
Requires-Dist: click>=8.1.0
|
|
@@ -49,6 +49,12 @@ Requires-Dist: uv-secure>=0.15.0
|
|
|
49
49
|
Requires-Dist: datasets>=2.14.0
|
|
50
50
|
Requires-Dist: sentence-transformers>=2.2.0
|
|
51
51
|
Requires-Dist: accelerate>=0.26.0
|
|
52
|
+
Requires-Dist: mypy>=1.7.0
|
|
53
|
+
Requires-Dist: pylint>=3.0.0
|
|
54
|
+
Requires-Dist: bandit>=1.7.0
|
|
55
|
+
Requires-Dist: radon>=6.0.0
|
|
56
|
+
Requires-Dist: vulture>=2.0.0
|
|
57
|
+
Requires-Dist: semgrep>=1.0.0
|
|
52
58
|
Requires-Dist: pytest>=7.4.0 ; extra == 'dev'
|
|
53
59
|
Requires-Dist: pytest-cov>=4.1.0 ; extra == 'dev'
|
|
54
60
|
Requires-Dist: pytest-xdist>=3.5.0 ; extra == 'dev'
|
|
@@ -81,12 +87,6 @@ Requires-Dist: opentelemetry-api>=1.20.0 ; extra == 'observability'
|
|
|
81
87
|
Requires-Dist: opentelemetry-sdk>=1.20.0 ; extra == 'observability'
|
|
82
88
|
Requires-Dist: opentelemetry-exporter-otlp>=1.20.0 ; extra == 'observability'
|
|
83
89
|
Requires-Dist: sentry-sdk[fastapi,celery,sqlalchemy]>=2.0.0 ; extra == 'observability'
|
|
84
|
-
Requires-Dist: mypy>=1.7.0 ; extra == 'detectors'
|
|
85
|
-
Requires-Dist: pylint>=3.0.0 ; extra == 'detectors'
|
|
86
|
-
Requires-Dist: bandit>=1.7.0 ; extra == 'detectors'
|
|
87
|
-
Requires-Dist: radon>=6.0.0 ; extra == 'detectors'
|
|
88
|
-
Requires-Dist: vulture>=2.0.0 ; extra == 'detectors'
|
|
89
|
-
Requires-Dist: semgrep>=1.0.0 ; extra == 'detectors'
|
|
90
90
|
Requires-Dist: questionary>=2.0.0 ; extra == 'ml'
|
|
91
91
|
Requires-Dist: scikit-learn>=1.3.0 ; extra == 'ml'
|
|
92
92
|
Requires-Dist: joblib>=1.3.0 ; extra == 'ml'
|
|
@@ -105,7 +105,6 @@ Provides-Extra: graphiti
|
|
|
105
105
|
Provides-Extra: security
|
|
106
106
|
Provides-Extra: local-embeddings
|
|
107
107
|
Provides-Extra: observability
|
|
108
|
-
Provides-Extra: detectors
|
|
109
108
|
Provides-Extra: ml
|
|
110
109
|
Provides-Extra: sandbox
|
|
111
110
|
Provides-Extra: voyage
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
LICENSE,sha256=kZQMuoI-6QQZH9KFUiVK5u_tDmT9SO-Z4QFp0hE-cfg,1105
|
|
2
|
-
repotoire-0.1.
|
|
3
|
-
repotoire-0.1.
|
|
4
|
-
repotoire-0.1.
|
|
5
|
-
repotoire-0.1.
|
|
2
|
+
repotoire-0.1.3.dist-info/METADATA,sha256=gMjG0KQAcg2E9VfA6hM86xrg8eaFfM4e52-_bNl4r4E,23796
|
|
3
|
+
repotoire-0.1.3.dist-info/WHEEL,sha256=ZrAiv7lKoKZh206bhSq4g-n3q3ySUOJQmQ4WXAA7TE8,97
|
|
4
|
+
repotoire-0.1.3.dist-info/entry_points.txt,sha256=qDZ_wx7dF6GDyrQJ70OXdRPBMJF6hEZIOxEZqX8jfdw,299
|
|
5
|
+
repotoire-0.1.3.dist-info/licenses/LICENSE,sha256=kZQMuoI-6QQZH9KFUiVK5u_tDmT9SO-Z4QFp0hE-cfg,1105
|
|
6
6
|
repotoire/__init__.py,sha256=hgmEsSDh0eG73leeSicWDGAb_BDdff9ItMpKw51veHg,515
|
|
7
7
|
repotoire/ai/__init__.py,sha256=LYHXLUjXF9hcXangbWNhrUg8fbGyDrdxj7DvpwBtJIA,1955
|
|
8
8
|
repotoire/ai/contextual.py,sha256=CBepfMFUwESqrQf1GgfDuxdfOKojPVj_JDKLot_9tP4,15667
|
|
@@ -80,7 +80,7 @@ repotoire/api/shared/services/encryption.py,sha256=o3DQmQv1ZfcSnN61NVP03Mec4DctW
|
|
|
80
80
|
repotoire/api/shared/services/gdpr.py,sha256=mEGokakr5WjXyqgFiiSDu-UfBooDm5Bw99wspwur4KY,18282
|
|
81
81
|
repotoire/api/shared/services/github.py,sha256=BKGaPMsgmHE0hnabTaPsK_wuGWSF0Q6Ph5Vfv-rFHEw,11094
|
|
82
82
|
repotoire/api/shared/services/stripe_service.py,sha256=WnZmUee3K6YpBk3GfWrpXnX1UrZJ7ZoM73coFb9E_wE,24363
|
|
83
|
-
repotoire/api/v1/__init__.py,sha256=
|
|
83
|
+
repotoire/api/v1/__init__.py,sha256=xO7jwYU3Nhdo4qtVb6m1g4GL6XkTAT1ov6nFbuE2ztM,8422
|
|
84
84
|
repotoire/api/v1/routes/__init__.py,sha256=EJAQ63XgFtFap2RS3tIZxP14huFo1y7jndA8mKIcJb4,793
|
|
85
85
|
repotoire/api/v1/routes/account.py,sha256=L2C3dkmym9IdrgSgsSKU8lwg3oGeukrLiAIQqvfzEck,19505
|
|
86
86
|
repotoire/api/v1/routes/admin/__init__.py,sha256=YUEAmdX1vuhVVpTuXuTahxXHM7iUa0Kl_e3GuGyYhK8,179
|
|
@@ -100,6 +100,7 @@ repotoire/api/v1/routes/customer_webhooks.py,sha256=lgYLZQ1F6H1UI7WR1HMkdUTcLxXC
|
|
|
100
100
|
repotoire/api/v1/routes/findings.py,sha256=mmWrWY0HMNOs8AWaCrKjNHm6NciC7JBveeiUBfGKDJk,24686
|
|
101
101
|
repotoire/api/v1/routes/fixes.py,sha256=egdOH0_jPPCg3kL4tTZR5TdHOzsjnDdQPui7PkLkgK0,47019
|
|
102
102
|
repotoire/api/v1/routes/github.py,sha256=0XPFcaXPDANUYGFfKxx3Pn0L4SP43t6609Xe7CI9k58,53089
|
|
103
|
+
repotoire/api/v1/routes/graph.py,sha256=bTnNVUhbL3kxTvsOxTmzyl9TaxKhGaub1d22o1Oyy5Q,16312
|
|
103
104
|
repotoire/api/v1/routes/historical.py,sha256=NqAzG_KnBDVd5LYGM3uFRe3WXyK9skZFevM18yP_MW0,11470
|
|
104
105
|
repotoire/api/v1/routes/marketplace.py,sha256=tPTWE463KFowU3JZZib-Qm3rObjltZvEfBgfuYnPm20,89371
|
|
105
106
|
repotoire/api/v1/routes/notifications.py,sha256=e_geSoFAagN2Beo5o7e9VRPH462W1EPgMKdydY-Prtc,6473
|
|
@@ -145,9 +146,9 @@ repotoire/cache/base.py,sha256=8Yr1Y4onn2ns3F-GlBoRX6u6O4jzeDl-whJ9-4frlaQ,12040
|
|
|
145
146
|
repotoire/cache/preview.py,sha256=hzGk0xlE6CwYWeABkF0SvP26W4xeAURLKUCQ5CwfIRo,5261
|
|
146
147
|
repotoire/cache/scan.py,sha256=vwmMtC17ea8tO_GyqcWGf9FwoXWZIP53jvPVxwxhSHY,7581
|
|
147
148
|
repotoire/cache/skill.py,sha256=TZ7RRRd2NAxsZjivcLu9iO7S_OC6L4WMm8ilqnS7y0s,10672
|
|
148
|
-
repotoire/cli/__init__.py,sha256=
|
|
149
|
+
repotoire/cli/__init__.py,sha256=EmH_jDFYS2AmUEKeL57kAEejPZMHkB_VL8Mmb1tnCnQ,226612
|
|
149
150
|
repotoire/cli/api_keys.py,sha256=H5wJZXJFDWacKEp9lvkFDLeOXCKRYDYShBPsXMCDOZQ,6777
|
|
150
|
-
repotoire/cli/auth.py,sha256=
|
|
151
|
+
repotoire/cli/auth.py,sha256=zU0EB9tfuGmiDkchuFa6qKnLYR7tEKO92LwLeQLv9AQ,11515
|
|
151
152
|
repotoire/cli/auth_commands.py,sha256=tfKpovH_xqbdFkcmupN2ga_WvEEYEu4qPxd4O443dw4,2297
|
|
152
153
|
repotoire/cli/credentials.py,sha256=ZTt-yp326xn5Mm-HQKXtcom4jPHkQM9T0E02IGqSXQ4,8991
|
|
153
154
|
repotoire/cli/graph.py,sha256=FYtKZGzKwbOGcsU19Z7JfM57Z9SkRR7gs6Dh4ZbyyNw,13895
|
|
@@ -188,28 +189,28 @@ repotoire/db/repositories/quota_override.py,sha256=TYdYc07GXxfcF_acmUDiprPDHI4pL
|
|
|
188
189
|
repotoire/db/session.py,sha256=SISDlr5JJQAAmh5kXXqHG9faUc7NsntpmHONvl1qY0o,6793
|
|
189
190
|
repotoire/detectors/__init__.py,sha256=SfUgFV8BlIqkjye5VgYBr9Yr1hvQLRGaCn3atTFFj3Q,3099
|
|
190
191
|
repotoire/detectors/architectural_bottleneck.py,sha256=7WFmV4QLsou6-_wuVdG-znkPmjKF8t7RZTlvTjqox9g,14555
|
|
191
|
-
repotoire/detectors/async_antipattern.py,sha256=
|
|
192
|
+
repotoire/detectors/async_antipattern.py,sha256=zZOVixe_4I6lJ_7eIcJD7PYOreo4SSbkaWsVTFWb7Kg,19356
|
|
192
193
|
repotoire/detectors/bandit_detector.py,sha256=DVXw7v10RO4lO9baQTEhPjyE3pp4B6uYbj89UJh4xz0,17057
|
|
193
194
|
repotoire/detectors/base.py,sha256=-9KWUvn0f3l9VALUfeuK8MLiG_oVhixE60b4ah4N2J4,1633
|
|
194
|
-
repotoire/detectors/circular_dependency.py,sha256=
|
|
195
|
+
repotoire/detectors/circular_dependency.py,sha256=54LfQpjLzivjfxpSLqMPvGJaFt-zkyWrsGIbp_Ub5PE,13279
|
|
195
196
|
repotoire/detectors/core_utility.py,sha256=ovp5QTt0Kv5qKYg_hLImmRK4W8zLZF4VZuBFLs4aW6o,12136
|
|
196
|
-
repotoire/detectors/data_clumps.py,sha256=
|
|
197
|
-
repotoire/detectors/dead_code.py,sha256=
|
|
197
|
+
repotoire/detectors/data_clumps.py,sha256=5csvR7JTY45CqwZos4LyLQXK5BPIGJQcS715-P45x00,16678
|
|
198
|
+
repotoire/detectors/dead_code.py,sha256=So8EHoaKJgBBfeNx4miwYaWSgQ9zmg5Bh0-nhZ5VbsU,30294
|
|
198
199
|
repotoire/detectors/deduplicator.py,sha256=_lqtJ8-Us0s-sHxmbg9999_edq5s5xV6IVcrcpJnbA0,14678
|
|
199
200
|
repotoire/detectors/degree_centrality.py,sha256=Q7qQHLvdm-uIx6GWZWyYaWRrGvuun4_VSAAXdt5ysUg,16580
|
|
200
201
|
repotoire/detectors/duplicate_rust.py,sha256=A7R60RKh9AsLL0nQNd6M_aPRqySbl7MpUxE9m6_jaaY,18280
|
|
201
|
-
repotoire/detectors/engine.py,sha256=
|
|
202
|
+
repotoire/detectors/engine.py,sha256=1LNObA0dvDWWftn5mbaiCgG1vhZEUQEowpFcua5ioZo,40301
|
|
202
203
|
repotoire/detectors/feature_envy.py,sha256=Ci1PP7zL4SH7tLrX4xyWhq1Y7pjMtQeZp5visWS8t_U,11672
|
|
203
|
-
repotoire/detectors/generator_misuse.py,sha256=
|
|
204
|
+
repotoire/detectors/generator_misuse.py,sha256=Z4eoYza4m6ClN2nr1v7qa74esKHVQ6X9NJau17GflSI,28973
|
|
204
205
|
repotoire/detectors/god_class.py,sha256=qsyPyhhnPMjvjpz_grwq2oK8iwl7zj2nV9oF23AVFOY,38078
|
|
205
|
-
repotoire/detectors/graph_algorithms.py,sha256=
|
|
206
|
+
repotoire/detectors/graph_algorithms.py,sha256=UAySR9IvaRAWrXDn0IiB9B-N5Q6yQ1Dp54IXF1h6fQs,58609
|
|
206
207
|
repotoire/detectors/graphsage_detector.py,sha256=FN-2Y1AUDQYxA93BckZ_sOUfPp6rJoZxDCaF_kV3F_c,10303
|
|
207
|
-
repotoire/detectors/inappropriate_intimacy.py,sha256=
|
|
208
|
+
repotoire/detectors/inappropriate_intimacy.py,sha256=OsPLNo1bEzQMVxpox0foYphjR84l98QuchDAzL1KI7I,8566
|
|
208
209
|
repotoire/detectors/influential_code.py,sha256=3bunilaxP6K5ShwSeLln1-iToQoiYGGQn-oM7ZuVTEo,16064
|
|
209
210
|
repotoire/detectors/jscpd_detector.py,sha256=bolku-5QKr2FftpIyGrOTdbSgFyqTF2y42NFWyEIl8U,16637
|
|
210
211
|
repotoire/detectors/lazy_class.py,sha256=ciO4E2s9I4OCkhT8PD_wNMqc3AXMW19cwzIl68c3US8,8951
|
|
211
|
-
repotoire/detectors/long_parameter_list.py,sha256=
|
|
212
|
-
repotoire/detectors/message_chain.py,sha256=
|
|
212
|
+
repotoire/detectors/long_parameter_list.py,sha256=MOsJL8emMb4eHMslpdgS1IKij6SXBOgIynHACF9hlK8,15592
|
|
213
|
+
repotoire/detectors/message_chain.py,sha256=84o0gXg2t2K800kIge9WHAK8CbngbeYTi6Mg6umtWcQ,18879
|
|
213
214
|
repotoire/detectors/middle_man.py,sha256=HEpZILxxk7YV3Mu4gh9oP0LxdTnM8ZwyvMXuT8xWTNQ,8181
|
|
214
215
|
repotoire/detectors/ml_bug_detector.py,sha256=PbHSNwqRA-8PDaLsnVmjr617PpHJDwe9fJfB2bg4Hlw,9954
|
|
215
216
|
repotoire/detectors/module_cohesion.py,sha256=lnOrwC20YilFKVnPLhInIw998QXzlX1jjc52j3GYEC4,15803
|
|
@@ -227,9 +228,9 @@ repotoire/detectors/semgrep_detector.py,sha256=taYuIjkda8N6WiyvbyLgPHVAIUVX-cV2P
|
|
|
227
228
|
repotoire/detectors/shotgun_surgery.py,sha256=uKVYCWO-TnECx6wG2W-SgrlDtM6pbsp4o0SAXH2kA7Y,7552
|
|
228
229
|
repotoire/detectors/taint_detector.py,sha256=Ol7jU1gyKY9AFbyHqssiJecLlwf_oy3M1f0FFD1q8a4,23273
|
|
229
230
|
repotoire/detectors/temporal_metrics.py,sha256=YTU7-ud9YyFVXew1tToCYe7AX4Asjq0St30s74Bm3dg,16551
|
|
230
|
-
repotoire/detectors/test_smell.py,sha256=
|
|
231
|
+
repotoire/detectors/test_smell.py,sha256=KR6ITF4CaPCL2UeX-hAfqZikfCq-8nT_vxLvb9hUW6o,27118
|
|
231
232
|
repotoire/detectors/truly_unused_imports.py,sha256=M2JFFEBPOPIQs1aqwW44XBOpVaGji3Ftcbdm57kx9LY,8622
|
|
232
|
-
repotoire/detectors/type_hint_coverage.py,sha256=
|
|
233
|
+
repotoire/detectors/type_hint_coverage.py,sha256=sF_Nj6oZ3kT8Ssd9A2HkvMcmwqB7_fLUE831xku7Wck,20409
|
|
233
234
|
repotoire/detectors/voting_engine.py,sha256=wjo3rpMh5gg8KcWDBGmXj4hrChDQUbDxApmyUBAHHP4,22204
|
|
234
235
|
repotoire/detectors/vulture_detector.py,sha256=M8QEEIaSFIyrKAkpcsUoMNpAgN1aGKoCZZdWHRbQd4I,23507
|
|
235
236
|
repotoire/github/__init__.py,sha256=MQ1cwg-UPYRXR8-nahSkFRfKSVGwdxr4_4_u2jqMrDQ,112
|
|
@@ -238,9 +239,10 @@ repotoire/github/pr_commenter.py,sha256=-MyUb3tlDzC_iW9Sm1sB2Z9aPD-o8KzpLPmMbLTI
|
|
|
238
239
|
repotoire/graph/__init__.py,sha256=jgChDzW_7iE_ATIIH5qMQZELCK1T9jYfrM9MG1nOWVg,1362
|
|
239
240
|
repotoire/graph/base.py,sha256=9jEcqhVnZLlFCTs1PM0RNu0CB3OvKIgxgAFH3UnjmkM,5714
|
|
240
241
|
repotoire/graph/client.py,sha256=eryWMuuGgDoUGnui-dTJWtYD_PWUvpmz4HtmuPJbKhU,39988
|
|
242
|
+
repotoire/graph/cloud_client.py,sha256=MfHUa0MkTIIq1xnb_i__fTZGa2unfthkCmnS541Ifys,7147
|
|
241
243
|
repotoire/graph/enricher.py,sha256=5IOITHRSVsZhKNJdl_YMFJjYH2Nog5hUHDcJWJYEAFU,17441
|
|
242
244
|
repotoire/graph/external_labels.py,sha256=eMdDaG11wTP1_mmgr-cUCFZl8Trs3XUh8vath7E3IB4,5446
|
|
243
|
-
repotoire/graph/factory.py,sha256=
|
|
245
|
+
repotoire/graph/factory.py,sha256=cFBE6TQHntmTk0vCEMWthcect9OYlQVqpAgW9NiMgDs,18349
|
|
244
246
|
repotoire/graph/falkordb_client.py,sha256=4Yv6BPkzeZE8RB123MUvk6GRlOrkRABMRfO9Qs1VPhw,21190
|
|
245
247
|
repotoire/graph/incremental_scc.py,sha256=MbYsGzPah5_0VzJhU5OFYcyO9DK3z9iCTeJfjUM3yeY,14387
|
|
246
248
|
repotoire/graph/migration.py,sha256=Ya8O8yRT7CNJzqm9N6GkVUjycNiMKV6Kl4qjdVhyl_Q,14813
|
|
@@ -249,7 +251,7 @@ repotoire/graph/queries/__init__.py,sha256=iErJN8jB13njO73rnuJpnSzZarbcGjKs4pNp2
|
|
|
249
251
|
repotoire/graph/queries/builders.py,sha256=ng1BLfE4s4MEkcKB47ZQcT2l_T4yzLWRkninQCmSmyI,11333
|
|
250
252
|
repotoire/graph/queries/patterns.py,sha256=Wdg1DYHHOCalhiLcBsk1ODHAxVF04eIPiGaadymy83M,15662
|
|
251
253
|
repotoire/graph/queries/traversal.py,sha256=MkeE-GSS6xkjgGf74ziaWNfdqwegkplwiu8Av-wMff0,12405
|
|
252
|
-
repotoire/graph/schema.py,sha256=
|
|
254
|
+
repotoire/graph/schema.py,sha256=SeTJRT7oDvyFco0ObuNWjXah6Ymc5tCGnDUwC6gP4ac,17335
|
|
253
255
|
repotoire/graph/tenant_factory.py,sha256=kREcK-AFdLo_GH-RoEkIK6iIEioomTobV0YGNA7uCYs,19457
|
|
254
256
|
repotoire/historical/__init__.py,sha256=wpaf5LQfeIXcV6V-hdUwWDpPPjt7hB4N46c2uWQjnRU,709
|
|
255
257
|
repotoire/historical/git_graphiti.py,sha256=9RuMMhZ9_o14sfTlkXJfrHNGYRtdT0qMje_ai-6RGoE,12066
|
|
@@ -372,5 +374,5 @@ repotoire/workers/tasks.py,sha256=uqyIWg6JSEQGRBQnpDVLauja0gG7Hw9hqJ5HuVxjGLY,32
|
|
|
372
374
|
repotoire/workers/webhook_delivery.py,sha256=RrOGyGIQakGvYKiQgAD9-eOfG1QXGQpx2ouPCZnGMJQ,17932
|
|
373
375
|
repotoire/workers/webhooks.py,sha256=gpG-2N2Ik_oD1YFiPiXCysOcZY7jem3wg-o_jSgQyeo,17717
|
|
374
376
|
repotoire_fast/__init__.py,sha256=kauI9ONyuAO3Opp6qD63Xkqxin9RXvZRkYRPyoLnlQU,11709
|
|
375
|
-
repotoire_fast/repotoire_fast.cp314-win_amd64.pyd,sha256=
|
|
376
|
-
repotoire-0.1.
|
|
377
|
+
repotoire_fast/repotoire_fast.cp314-win_amd64.pyd,sha256=jTI7Zj3_RZe8Yt7gkwItnCvM8RFdJBkLVlQyHyba6xU,9132032
|
|
378
|
+
repotoire-0.1.3.dist-info/RECORD,,
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|