code-analyser 0.1.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.
@@ -0,0 +1,441 @@
1
+ """
2
+ API endpoints for code analysis
3
+ """
4
+
5
+ import uuid
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ import structlog
10
+ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+
13
+ from codelens.analyzers import AnalysisResult, analyzer_manager
14
+ from codelens.api.schemas import (
15
+ AnalysisIssueSchema,
16
+ AnalysisRequest,
17
+ AnalysisResponse,
18
+ BatchAnalysisRequest,
19
+ BatchAnalysisResponse,
20
+ CodeMetricsSchema,
21
+ ExecutionResultSchema,
22
+ FeedbackSchema,
23
+ GradeBreakdownSchema,
24
+ SimilarityResultSchema,
25
+ TestResultSchema,
26
+ )
27
+ from codelens.core.config import settings
28
+ from codelens.db.database import get_db
29
+ from codelens.models import AnalysisReport
30
+ from codelens.services import CodeExecutionRequest, code_executor
31
+
32
+ logger = structlog.get_logger()
33
+ router = APIRouter()
34
+
35
+
36
+ def convert_analysis_issues(issues: list[Any]) -> list[AnalysisIssueSchema]:
37
+ """Convert analyzer issues to schema format"""
38
+ return [
39
+ AnalysisIssueSchema(
40
+ line=issue.line,
41
+ column=issue.column,
42
+ severity=issue.severity,
43
+ code=issue.code,
44
+ message=issue.message,
45
+ category=issue.category,
46
+ suggestion=issue.suggestion
47
+ )
48
+ for issue in issues
49
+ ]
50
+
51
+
52
+ def convert_metrics(metrics: Any) -> CodeMetricsSchema:
53
+ """Convert analyzer metrics to schema format"""
54
+ return CodeMetricsSchema(
55
+ lines_of_code=metrics.lines_of_code,
56
+ lines_of_comments=metrics.lines_of_comments,
57
+ blank_lines=metrics.blank_lines,
58
+ cyclomatic_complexity=metrics.cyclomatic_complexity,
59
+ cognitive_complexity=metrics.cognitive_complexity,
60
+ function_count=metrics.function_count,
61
+ class_count=metrics.class_count,
62
+ max_nesting_depth=metrics.max_nesting_depth,
63
+ maintainability_index=metrics.maintainability_index
64
+ )
65
+
66
+
67
+ async def store_analysis_report(
68
+ analysis_result: AnalysisResult,
69
+ request: AnalysisRequest,
70
+ submission_id: str,
71
+ db: AsyncSession,
72
+ grade_breakdown: dict[str, float] | None = None,
73
+ total_score: float | None = None,
74
+ test_result: Any = None,
75
+ similarity_result: Any = None
76
+ ) -> None:
77
+ """Store analysis report in database"""
78
+ try:
79
+ # Create analysis report
80
+ report = AnalysisReport(
81
+ assignment_id=request.assignment_id,
82
+ student_id=request.student_id,
83
+ student_name=request.student_name,
84
+ submission_id=submission_id,
85
+ file_name="submission.py", # Default filename
86
+ file_size=len(request.code.encode('utf-8')),
87
+ file_hash=analysis_result.analyzer_version, # Use analyzer version as temp hash
88
+ language=request.language.value,
89
+ analysis_version=analysis_result.analyzer_version,
90
+ syntax_valid=analysis_result.success and len([i for i in analysis_result.issues if i.category == "syntax"]) == 0,
91
+ syntax_errors={
92
+ "errors": [
93
+ {"line": i.line, "message": i.message}
94
+ for i in analysis_result.issues if i.category == "syntax"
95
+ ]
96
+ },
97
+ quality_metrics={
98
+ "complexity": {
99
+ "cyclomatic": analysis_result.metrics.cyclomatic_complexity,
100
+ "cognitive": analysis_result.metrics.cognitive_complexity
101
+ },
102
+ "lines_of_code": analysis_result.metrics.lines_of_code,
103
+ "style_issues": [
104
+ {"line": i.line, "issue": i.message, "severity": i.severity.value}
105
+ for i in analysis_result.issues if i.category == "style"
106
+ ],
107
+ "type_issues": [
108
+ {"line": i.line, "issue": i.message}
109
+ for i in analysis_result.issues if i.category == "types"
110
+ ]
111
+ },
112
+ test_results={
113
+ "total_tests": test_result.total_tests if test_result else 0,
114
+ "passed_tests": test_result.passed_tests if test_result else 0,
115
+ "failed_tests": test_result.failed_tests if test_result else [],
116
+ "execution_time": test_result.execution_result.execution_time if test_result and test_result.execution_result else 0
117
+ } if test_result else None,
118
+ grade_breakdown=grade_breakdown or {},
119
+ total_score=total_score or 0.0,
120
+ similarity_results=similarity_result.__dict__ if similarity_result else None,
121
+ feedback={
122
+ "strengths": ["Code structure looks good"] if analysis_result.success else [],
123
+ "improvements": [i.message for i in analysis_result.issues[:3]], # Top 3 issues
124
+ "resources": []
125
+ },
126
+ processing_time=analysis_result.execution_time,
127
+ tools_used={
128
+ "analyzer": analysis_result.analyzer_version,
129
+ "execution": test_result is not None
130
+ },
131
+ status="completed"
132
+ )
133
+
134
+ db.add(report)
135
+ await db.commit()
136
+ await db.refresh(report)
137
+
138
+ logger.info("Analysis report stored", report_id=report.id, submission_id=submission_id)
139
+
140
+ except Exception as e:
141
+ logger.error("Failed to store analysis report", error=str(e), submission_id=submission_id)
142
+ await db.rollback()
143
+
144
+
145
+ @router.post("/python", response_model=AnalysisResponse)
146
+ async def analyze_python_code(
147
+ request: AnalysisRequest,
148
+ background_tasks: BackgroundTasks,
149
+ db: AsyncSession = Depends(get_db)
150
+ ) -> AnalysisResponse:
151
+ """Analyze Python code submission"""
152
+ start_time = datetime.utcnow()
153
+ submission_id = str(uuid.uuid4())
154
+
155
+ try:
156
+ logger.info("Starting Python code analysis", submission_id=submission_id)
157
+
158
+ # Run static analysis
159
+ analysis_result = await analyzer_manager.analyze_code(
160
+ code=request.code,
161
+ language=request.language.value,
162
+ file_path="submission.py",
163
+ analyzer_config=request.analyzer_config
164
+ )
165
+
166
+ if not analysis_result.success:
167
+ logger.warning("Analysis failed", submission_id=submission_id)
168
+ return AnalysisResponse(
169
+ success=False,
170
+ submission_id=submission_id,
171
+ error_message="Static analysis failed",
172
+ issues=convert_analysis_issues(analysis_result.issues),
173
+ processing_time=(datetime.utcnow() - start_time).total_seconds(),
174
+ total_score=0.0,
175
+ max_score=100.0
176
+ )
177
+
178
+ # Code execution (if requested)
179
+ execution_result = None
180
+ test_result = None
181
+ if request.execute_code or request.run_tests:
182
+ if code_executor.is_available():
183
+ exec_request = CodeExecutionRequest(
184
+ code=request.code,
185
+ language=request.language.value,
186
+ input_data=request.input_data,
187
+ run_tests=request.run_tests,
188
+ test_cases=[tc.dict() for tc in request.test_cases] if request.test_cases else None
189
+ )
190
+
191
+ exec_response = await code_executor.execute_code(exec_request)
192
+ if exec_response.execution_result:
193
+ execution_result = ExecutionResultSchema(**exec_response.execution_result.__dict__)
194
+ if exec_response.test_result:
195
+ test_result = TestResultSchema(**exec_response.test_result.__dict__)
196
+ else:
197
+ logger.warning("Code execution requested but sandbox not available")
198
+
199
+ # Similarity checking (placeholder - will implement later)
200
+ similarity_result = None
201
+ if request.check_similarity:
202
+ similarity_result = SimilarityResultSchema(
203
+ highest_similarity=0.0,
204
+ flagged_submissions=[],
205
+ ai_baseline_similarity=0.0,
206
+ methods_used=["ast_similarity"]
207
+ )
208
+
209
+ # Grading (if rubric provided)
210
+ grade_breakdown = None
211
+ total_score = None
212
+ if request.rubric_id:
213
+ # Calculate grade based on rubric (simplified implementation)
214
+ grade_breakdown = GradeBreakdownSchema(
215
+ functionality=85.0 if not test_result or test_result.passed_tests == test_result.total_tests else 70.0,
216
+ style=90.0 if len([i for i in analysis_result.issues if i.category == "style"]) < 5 else 75.0,
217
+ documentation=80.0, # Placeholder
218
+ testing=95.0 if test_result and test_result.passed_tests > 0 else 60.0,
219
+ total=82.5 # Average
220
+ )
221
+ total_score = grade_breakdown.total
222
+
223
+ # Generate feedback
224
+ feedback = FeedbackSchema(
225
+ strengths=["Code compiles successfully"] if analysis_result.success else [],
226
+ improvements=[
227
+ f"Fix {i.category} issue: {i.message}"
228
+ for i in analysis_result.issues[:3]
229
+ ],
230
+ resources=[]
231
+ )
232
+ # Add style resource if needed
233
+ if any(i.category == "style" for i in analysis_result.issues):
234
+ feedback.resources.append("https://pep8.org/")
235
+
236
+ # Create response
237
+ response = AnalysisResponse(
238
+ success=True,
239
+ submission_id=submission_id,
240
+ syntax_valid=analysis_result.success,
241
+ syntax_errors=[i for i in convert_analysis_issues(analysis_result.issues) if i.category == "syntax"],
242
+ issues=convert_analysis_issues(analysis_result.issues),
243
+ metrics=convert_metrics(analysis_result.metrics),
244
+ execution_result=execution_result,
245
+ test_result=test_result,
246
+ similarity_result=similarity_result,
247
+ grade_breakdown=grade_breakdown,
248
+ total_score=total_score or 0.0,
249
+ max_score=100.0,
250
+ feedback=feedback,
251
+ analysis_version=analysis_result.analyzer_version,
252
+ processing_time=analysis_result.execution_time,
253
+ tools_used={"analyzer": analysis_result.analyzer_version},
254
+ analyzed_at=start_time
255
+ )
256
+
257
+ # Store report in background
258
+ if request.assignment_id:
259
+ background_tasks.add_task(
260
+ store_analysis_report,
261
+ analysis_result,
262
+ request,
263
+ submission_id,
264
+ db,
265
+ grade_breakdown.dict() if grade_breakdown else None,
266
+ total_score or 0.0,
267
+ test_result,
268
+ similarity_result
269
+ )
270
+
271
+ logger.info("Python code analysis completed",
272
+ submission_id=submission_id,
273
+ processing_time=response.processing_time,
274
+ issue_count=len(response.issues))
275
+
276
+ return response
277
+
278
+ except Exception as e:
279
+ logger.error("Python code analysis failed", error=str(e), submission_id=submission_id)
280
+ raise HTTPException(
281
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
282
+ detail=f"Analysis failed: {str(e)}"
283
+ ) from None
284
+
285
+
286
+ @router.post("/batch", response_model=BatchAnalysisResponse)
287
+ async def analyze_batch(
288
+ request: BatchAnalysisRequest,
289
+ background_tasks: BackgroundTasks,
290
+ db: AsyncSession = Depends(get_db)
291
+ ) -> BatchAnalysisResponse:
292
+ """Analyze multiple code files in batch"""
293
+ datetime.utcnow()
294
+ batch_id = str(uuid.uuid4())
295
+
296
+ try:
297
+ logger.info("Starting batch analysis",
298
+ batch_id=batch_id,
299
+ file_count=len(request.files))
300
+
301
+ results = []
302
+ total_processing_time = 0.0
303
+ failed_files = 0
304
+
305
+ # Process files (could be parallelized further)
306
+ for i, file_info in enumerate(request.files):
307
+ try:
308
+ # Create individual analysis request
309
+ analysis_request = AnalysisRequest(
310
+ code=file_info["code"],
311
+ language=request.language,
312
+ rubric_id=request.rubric_id,
313
+ assignment_id=request.assignment_id,
314
+ student_id=file_info.get("student_id"),
315
+ student_name=file_info.get("student_name"),
316
+ check_similarity=request.check_similarity,
317
+ run_tests=False,
318
+ test_cases=[],
319
+ execute_code=False,
320
+ input_data=None,
321
+ analyzer_config=None
322
+ )
323
+
324
+ # Analyze individual file
325
+ file_result = await analyze_python_code(analysis_request, background_tasks, db)
326
+ results.append(file_result)
327
+ total_processing_time += file_result.processing_time
328
+
329
+ logger.info("Processed file in batch",
330
+ batch_id=batch_id,
331
+ file_index=i,
332
+ success=file_result.success)
333
+
334
+ except Exception as e:
335
+ logger.error("Failed to process file in batch",
336
+ batch_id=batch_id,
337
+ file_index=i,
338
+ error=str(e))
339
+ failed_files += 1
340
+
341
+ # Add error result
342
+ error_result = AnalysisResponse(
343
+ success=False,
344
+ submission_id=str(uuid.uuid4()),
345
+ error_message=f"Processing failed: {str(e)}",
346
+ processing_time=0.0,
347
+ total_score=0.0,
348
+ max_score=100.0
349
+ )
350
+ results.append(error_result)
351
+
352
+ # Cross-file similarity analysis (placeholder)
353
+ cross_similarity_results = []
354
+ if request.check_similarity and len(results) > 1:
355
+ # This would implement cross-submission similarity checking
356
+ cross_similarity_results = [
357
+ {
358
+ "submission_1": results[0].submission_id,
359
+ "submission_2": results[1].submission_id,
360
+ "similarity_score": 0.1,
361
+ "method": "ast_similarity"
362
+ }
363
+ ] if len(results) >= 2 else []
364
+
365
+ # Calculate batch statistics
366
+ processed_files = len(results) - failed_files
367
+ average_processing_time = total_processing_time / len(results) if results else 0.0
368
+
369
+ response = BatchAnalysisResponse(
370
+ success=failed_files == 0,
371
+ batch_id=batch_id,
372
+ total_files=len(request.files),
373
+ processed_files=processed_files,
374
+ failed_files=failed_files,
375
+ results=results,
376
+ cross_similarity_results=cross_similarity_results,
377
+ total_processing_time=total_processing_time,
378
+ average_processing_time=average_processing_time,
379
+ completed_at=datetime.utcnow()
380
+ )
381
+
382
+ logger.info("Batch analysis completed",
383
+ batch_id=batch_id,
384
+ processed=processed_files,
385
+ failed=failed_files,
386
+ total_time=total_processing_time)
387
+
388
+ return response
389
+
390
+ except Exception as e:
391
+ logger.error("Batch analysis failed", error=str(e), batch_id=batch_id)
392
+ raise HTTPException(
393
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
394
+ detail=f"Batch analysis failed: {str(e)}"
395
+ ) from None
396
+
397
+
398
+ @router.get("/status")
399
+ async def get_analysis_status() -> dict[str, Any]:
400
+ """Get analyzer status and configuration"""
401
+ return {
402
+ "status": "healthy",
403
+ "supported_languages": analyzer_manager.get_supported_languages(),
404
+ "analyzers": analyzer_manager.get_all_analyzer_info(),
405
+ "sandbox_available": code_executor.is_available(),
406
+ "configuration": {
407
+ "max_file_size": settings.max_file_size,
408
+ "max_files_per_batch": settings.max_files_per_batch,
409
+ "execution_timeout": settings.analyzer.execution_timeout,
410
+ "memory_limit": settings.analyzer.memory_limit
411
+ }
412
+ }
413
+
414
+
415
+ @router.get("/tools")
416
+ async def get_available_tools() -> dict[str, Any]:
417
+ """Get information about available analysis tools"""
418
+ return {
419
+ "python": {
420
+ "ruff": {
421
+ "enabled": settings.analyzer.ruff_enabled,
422
+ "description": "Fast Python linter and formatter"
423
+ },
424
+ "mypy": {
425
+ "enabled": settings.analyzer.mypy_enabled,
426
+ "description": "Static type checker for Python"
427
+ }
428
+ },
429
+ "execution": {
430
+ "docker": {
431
+ "enabled": settings.docker_enabled,
432
+ "description": "Docker-based code execution sandbox"
433
+ }
434
+ },
435
+ "similarity": {
436
+ "ast_similarity": {
437
+ "enabled": settings.similarity.enabled,
438
+ "description": "AST-based structural similarity detection"
439
+ }
440
+ }
441
+ }