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.
- code_analyser-0.1.0.dist-info/METADATA +283 -0
- code_analyser-0.1.0.dist-info/RECORD +34 -0
- code_analyser-0.1.0.dist-info/WHEEL +4 -0
- code_analyser-0.1.0.dist-info/licenses/LICENSE +21 -0
- codelens/__init__.py +7 -0
- codelens/__main__.py +19 -0
- codelens/analyzers/__init__.py +30 -0
- codelens/analyzers/base.py +139 -0
- codelens/analyzers/manager.py +207 -0
- codelens/analyzers/python_analyzer.py +344 -0
- codelens/analyzers/similarity_analyzer.py +512 -0
- codelens/api/__init__.py +1 -0
- codelens/api/routes/__init__.py +1 -0
- codelens/api/routes/analysis.py +441 -0
- codelens/api/routes/reports.py +438 -0
- codelens/api/routes/rubrics.py +349 -0
- codelens/api/schemas.py +305 -0
- codelens/cli.py +297 -0
- codelens/core/__init__.py +1 -0
- codelens/core/config.py +91 -0
- codelens/db/__init__.py +1 -0
- codelens/db/database.py +57 -0
- codelens/main.py +111 -0
- codelens/models/__init__.py +14 -0
- codelens/models/assignments.py +105 -0
- codelens/models/reports.py +172 -0
- codelens/models/rubrics.py +76 -0
- codelens/services/__init__.py +37 -0
- codelens/services/batch_processor.py +508 -0
- codelens/services/code_executor.py +310 -0
- codelens/services/sandbox.py +375 -0
- codelens/services/similarity_service.py +449 -0
- codelens/utils/__init__.py +29 -0
- codelens/utils/helpers.py +217 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API endpoints for rubric management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
from codelens.api.schemas import (
|
|
12
|
+
AssignmentCreate,
|
|
13
|
+
AssignmentResponse,
|
|
14
|
+
RubricCreate,
|
|
15
|
+
RubricResponse,
|
|
16
|
+
)
|
|
17
|
+
from codelens.db.database import get_db
|
|
18
|
+
from codelens.models import Assignment, Rubric
|
|
19
|
+
|
|
20
|
+
logger = structlog.get_logger()
|
|
21
|
+
router = APIRouter()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.post("/", response_model=RubricResponse, status_code=status.HTTP_201_CREATED)
|
|
25
|
+
async def create_rubric(
|
|
26
|
+
rubric: RubricCreate,
|
|
27
|
+
db: AsyncSession = Depends(get_db)
|
|
28
|
+
) -> RubricResponse:
|
|
29
|
+
"""Create a new grading rubric"""
|
|
30
|
+
try:
|
|
31
|
+
# Create rubric instance
|
|
32
|
+
db_rubric = Rubric(
|
|
33
|
+
name=rubric.name,
|
|
34
|
+
description=rubric.description,
|
|
35
|
+
language=rubric.language,
|
|
36
|
+
criteria=rubric.criteria,
|
|
37
|
+
weights=rubric.weights,
|
|
38
|
+
total_points=rubric.total_points,
|
|
39
|
+
analysis_config=rubric.analysis_config
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
db.add(db_rubric)
|
|
43
|
+
await db.commit()
|
|
44
|
+
await db.refresh(db_rubric)
|
|
45
|
+
|
|
46
|
+
logger.info("Created rubric", rubric_id=db_rubric.id, name=rubric.name)
|
|
47
|
+
|
|
48
|
+
return RubricResponse.model_validate(db_rubric)
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error("Failed to create rubric", error=str(e))
|
|
52
|
+
await db.rollback()
|
|
53
|
+
raise HTTPException(
|
|
54
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
55
|
+
detail=f"Failed to create rubric: {str(e)}"
|
|
56
|
+
) from None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get("/", response_model=list[RubricResponse])
|
|
60
|
+
async def list_rubrics(
|
|
61
|
+
language: str | None = None,
|
|
62
|
+
limit: int = 50,
|
|
63
|
+
offset: int = 0,
|
|
64
|
+
db: AsyncSession = Depends(get_db)
|
|
65
|
+
) -> list[RubricResponse]:
|
|
66
|
+
"""List all rubrics with optional filtering"""
|
|
67
|
+
try:
|
|
68
|
+
query = select(Rubric)
|
|
69
|
+
|
|
70
|
+
if language:
|
|
71
|
+
query = query.where(Rubric.language == language)
|
|
72
|
+
|
|
73
|
+
query = query.offset(offset).limit(limit)
|
|
74
|
+
|
|
75
|
+
result = await db.execute(query)
|
|
76
|
+
rubrics = result.scalars().all()
|
|
77
|
+
|
|
78
|
+
logger.info("Listed rubrics", count=len(rubrics), language_filter=language)
|
|
79
|
+
|
|
80
|
+
return [RubricResponse.model_validate(rubric) for rubric in rubrics]
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error("Failed to list rubrics", error=str(e))
|
|
84
|
+
raise HTTPException(
|
|
85
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
86
|
+
detail=f"Failed to list rubrics: {str(e)}"
|
|
87
|
+
) from None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@router.get("/{rubric_id}", response_model=RubricResponse)
|
|
91
|
+
async def get_rubric(
|
|
92
|
+
rubric_id: int,
|
|
93
|
+
db: AsyncSession = Depends(get_db)
|
|
94
|
+
) -> RubricResponse:
|
|
95
|
+
"""Get a specific rubric by ID"""
|
|
96
|
+
try:
|
|
97
|
+
result = await db.execute(select(Rubric).where(Rubric.id == rubric_id))
|
|
98
|
+
rubric = result.scalar_one_or_none()
|
|
99
|
+
|
|
100
|
+
if not rubric:
|
|
101
|
+
raise HTTPException(
|
|
102
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
103
|
+
detail=f"Rubric {rubric_id} not found"
|
|
104
|
+
) from None
|
|
105
|
+
|
|
106
|
+
logger.info("Retrieved rubric", rubric_id=rubric_id)
|
|
107
|
+
|
|
108
|
+
return RubricResponse.model_validate(rubric)
|
|
109
|
+
|
|
110
|
+
except HTTPException:
|
|
111
|
+
raise
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error("Failed to get rubric", rubric_id=rubric_id, error=str(e))
|
|
114
|
+
raise HTTPException(
|
|
115
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
116
|
+
detail=f"Failed to get rubric: {str(e)}"
|
|
117
|
+
) from None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@router.put("/{rubric_id}", response_model=RubricResponse)
|
|
121
|
+
async def update_rubric(
|
|
122
|
+
rubric_id: int,
|
|
123
|
+
rubric_update: RubricCreate,
|
|
124
|
+
db: AsyncSession = Depends(get_db)
|
|
125
|
+
) -> RubricResponse:
|
|
126
|
+
"""Update an existing rubric"""
|
|
127
|
+
try:
|
|
128
|
+
result = await db.execute(select(Rubric).where(Rubric.id == rubric_id))
|
|
129
|
+
rubric = result.scalar_one_or_none()
|
|
130
|
+
|
|
131
|
+
if not rubric:
|
|
132
|
+
raise HTTPException(
|
|
133
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
134
|
+
detail=f"Rubric {rubric_id} not found"
|
|
135
|
+
) from None
|
|
136
|
+
|
|
137
|
+
# Update rubric fields
|
|
138
|
+
rubric.name = rubric_update.name
|
|
139
|
+
rubric.description = rubric_update.description
|
|
140
|
+
rubric.language = rubric_update.language
|
|
141
|
+
rubric.criteria = rubric_update.criteria
|
|
142
|
+
rubric.weights = rubric_update.weights
|
|
143
|
+
rubric.total_points = rubric_update.total_points
|
|
144
|
+
rubric.analysis_config = rubric_update.analysis_config or {}
|
|
145
|
+
|
|
146
|
+
await db.commit()
|
|
147
|
+
await db.refresh(rubric)
|
|
148
|
+
|
|
149
|
+
logger.info("Updated rubric", rubric_id=rubric_id)
|
|
150
|
+
|
|
151
|
+
return RubricResponse.model_validate(rubric)
|
|
152
|
+
|
|
153
|
+
except HTTPException:
|
|
154
|
+
raise
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.error("Failed to update rubric", rubric_id=rubric_id, error=str(e))
|
|
157
|
+
await db.rollback()
|
|
158
|
+
raise HTTPException(
|
|
159
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
160
|
+
detail=f"Failed to update rubric: {str(e)}"
|
|
161
|
+
) from None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@router.delete("/{rubric_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
165
|
+
async def delete_rubric(
|
|
166
|
+
rubric_id: int,
|
|
167
|
+
db: AsyncSession = Depends(get_db)
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Delete a rubric"""
|
|
170
|
+
try:
|
|
171
|
+
result = await db.execute(select(Rubric).where(Rubric.id == rubric_id))
|
|
172
|
+
rubric = result.scalar_one_or_none()
|
|
173
|
+
|
|
174
|
+
if not rubric:
|
|
175
|
+
raise HTTPException(
|
|
176
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
177
|
+
detail=f"Rubric {rubric_id} not found"
|
|
178
|
+
) from None
|
|
179
|
+
|
|
180
|
+
# Check if rubric is being used by assignments
|
|
181
|
+
assignments_result = await db.execute(
|
|
182
|
+
select(Assignment).where(Assignment.rubric_id == rubric_id)
|
|
183
|
+
)
|
|
184
|
+
assignments = assignments_result.scalars().all()
|
|
185
|
+
|
|
186
|
+
if assignments:
|
|
187
|
+
raise HTTPException(
|
|
188
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
189
|
+
detail=f"Cannot delete rubric: it is used by {len(assignments)} assignment(s)"
|
|
190
|
+
) from None
|
|
191
|
+
|
|
192
|
+
await db.delete(rubric)
|
|
193
|
+
await db.commit()
|
|
194
|
+
|
|
195
|
+
logger.info("Deleted rubric", rubric_id=rubric_id)
|
|
196
|
+
|
|
197
|
+
except HTTPException:
|
|
198
|
+
raise
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.error("Failed to delete rubric", rubric_id=rubric_id, error=str(e))
|
|
201
|
+
await db.rollback()
|
|
202
|
+
raise HTTPException(
|
|
203
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
204
|
+
detail=f"Failed to delete rubric: {str(e)}"
|
|
205
|
+
) from None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@router.get("/language/{language}", response_model=list[RubricResponse])
|
|
209
|
+
async def get_rubrics_by_language(
|
|
210
|
+
language: str,
|
|
211
|
+
db: AsyncSession = Depends(get_db)
|
|
212
|
+
) -> list[RubricResponse]:
|
|
213
|
+
"""Get all rubrics for a specific programming language"""
|
|
214
|
+
try:
|
|
215
|
+
result = await db.execute(
|
|
216
|
+
select(Rubric).where(Rubric.language == language.lower())
|
|
217
|
+
)
|
|
218
|
+
rubrics = result.scalars().all()
|
|
219
|
+
|
|
220
|
+
logger.info("Retrieved rubrics by language", language=language, count=len(rubrics))
|
|
221
|
+
|
|
222
|
+
return [RubricResponse.model_validate(rubric) for rubric in rubrics]
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error("Failed to get rubrics by language", language=language, error=str(e))
|
|
226
|
+
raise HTTPException(
|
|
227
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
228
|
+
detail=f"Failed to get rubrics for language {language}: {str(e)}"
|
|
229
|
+
) from None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# Assignment endpoints
|
|
233
|
+
@router.post("/assignments/", response_model=AssignmentResponse, status_code=status.HTTP_201_CREATED)
|
|
234
|
+
async def create_assignment(
|
|
235
|
+
assignment: AssignmentCreate,
|
|
236
|
+
db: AsyncSession = Depends(get_db)
|
|
237
|
+
) -> AssignmentResponse:
|
|
238
|
+
"""Create a new assignment"""
|
|
239
|
+
try:
|
|
240
|
+
# Verify rubric exists
|
|
241
|
+
result = await db.execute(select(Rubric).where(Rubric.id == assignment.rubric_id))
|
|
242
|
+
rubric = result.scalar_one_or_none()
|
|
243
|
+
|
|
244
|
+
if not rubric:
|
|
245
|
+
raise HTTPException(
|
|
246
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
247
|
+
detail=f"Rubric {assignment.rubric_id} not found"
|
|
248
|
+
) from None
|
|
249
|
+
|
|
250
|
+
# Create assignment instance
|
|
251
|
+
db_assignment = Assignment(
|
|
252
|
+
name=assignment.name,
|
|
253
|
+
description=assignment.description,
|
|
254
|
+
course_id=assignment.course_id,
|
|
255
|
+
course_name=assignment.course_name,
|
|
256
|
+
semester=assignment.semester,
|
|
257
|
+
language=assignment.language,
|
|
258
|
+
rubric_id=assignment.rubric_id,
|
|
259
|
+
requirements=assignment.requirements,
|
|
260
|
+
test_cases=assignment.test_cases,
|
|
261
|
+
starter_code=assignment.starter_code,
|
|
262
|
+
similarity_enabled=assignment.similarity_enabled,
|
|
263
|
+
similarity_threshold=assignment.similarity_threshold,
|
|
264
|
+
cross_cohort_check=assignment.cross_cohort_check,
|
|
265
|
+
due_date=assignment.due_date,
|
|
266
|
+
late_penalty=assignment.late_penalty
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
db.add(db_assignment)
|
|
270
|
+
await db.commit()
|
|
271
|
+
await db.refresh(db_assignment)
|
|
272
|
+
|
|
273
|
+
logger.info("Created assignment", assignment_id=db_assignment.id, name=assignment.name)
|
|
274
|
+
|
|
275
|
+
return AssignmentResponse.model_validate(db_assignment)
|
|
276
|
+
|
|
277
|
+
except HTTPException:
|
|
278
|
+
raise
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.error("Failed to create assignment", error=str(e))
|
|
281
|
+
await db.rollback()
|
|
282
|
+
raise HTTPException(
|
|
283
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
284
|
+
detail=f"Failed to create assignment: {str(e)}"
|
|
285
|
+
) from None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@router.get("/assignments/", response_model=list[AssignmentResponse])
|
|
289
|
+
async def list_assignments(
|
|
290
|
+
course_id: str | None = None,
|
|
291
|
+
language: str | None = None,
|
|
292
|
+
limit: int = 50,
|
|
293
|
+
offset: int = 0,
|
|
294
|
+
db: AsyncSession = Depends(get_db)
|
|
295
|
+
) -> list[AssignmentResponse]:
|
|
296
|
+
"""List assignments with optional filtering"""
|
|
297
|
+
try:
|
|
298
|
+
query = select(Assignment)
|
|
299
|
+
|
|
300
|
+
if course_id:
|
|
301
|
+
query = query.where(Assignment.course_id == course_id)
|
|
302
|
+
if language:
|
|
303
|
+
query = query.where(Assignment.language == language)
|
|
304
|
+
|
|
305
|
+
query = query.offset(offset).limit(limit)
|
|
306
|
+
|
|
307
|
+
result = await db.execute(query)
|
|
308
|
+
assignments = result.scalars().all()
|
|
309
|
+
|
|
310
|
+
logger.info("Listed assignments", count=len(assignments))
|
|
311
|
+
|
|
312
|
+
return [AssignmentResponse.model_validate(assignment) for assignment in assignments]
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error("Failed to list assignments", error=str(e))
|
|
316
|
+
raise HTTPException(
|
|
317
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
318
|
+
detail=f"Failed to list assignments: {str(e)}"
|
|
319
|
+
) from None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@router.get("/assignments/{assignment_id}", response_model=AssignmentResponse)
|
|
323
|
+
async def get_assignment(
|
|
324
|
+
assignment_id: int,
|
|
325
|
+
db: AsyncSession = Depends(get_db)
|
|
326
|
+
) -> AssignmentResponse:
|
|
327
|
+
"""Get a specific assignment by ID"""
|
|
328
|
+
try:
|
|
329
|
+
result = await db.execute(select(Assignment).where(Assignment.id == assignment_id))
|
|
330
|
+
assignment = result.scalar_one_or_none()
|
|
331
|
+
|
|
332
|
+
if not assignment:
|
|
333
|
+
raise HTTPException(
|
|
334
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
335
|
+
detail=f"Assignment {assignment_id} not found"
|
|
336
|
+
) from None
|
|
337
|
+
|
|
338
|
+
logger.info("Retrieved assignment", assignment_id=assignment_id)
|
|
339
|
+
|
|
340
|
+
return AssignmentResponse.model_validate(assignment)
|
|
341
|
+
|
|
342
|
+
except HTTPException:
|
|
343
|
+
raise
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.error("Failed to get assignment", assignment_id=assignment_id, error=str(e))
|
|
346
|
+
raise HTTPException(
|
|
347
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
348
|
+
detail=f"Failed to get assignment: {str(e)}"
|
|
349
|
+
) from None
|
codelens/api/schemas.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic schemas for API request/response models
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field, validator
|
|
10
|
+
|
|
11
|
+
from codelens.analyzers.base import Severity
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AnalysisLanguage(str, Enum):
|
|
15
|
+
"""Supported programming languages"""
|
|
16
|
+
PYTHON = "python"
|
|
17
|
+
JAVASCRIPT = "javascript"
|
|
18
|
+
HTML = "html"
|
|
19
|
+
CSS = "css"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AnalysisIssueSchema(BaseModel):
|
|
23
|
+
"""Schema for analysis issues"""
|
|
24
|
+
line: int = Field(..., ge=1, description="Line number where issue occurs")
|
|
25
|
+
column: int = Field(0, ge=0, description="Column position")
|
|
26
|
+
severity: Severity = Field(..., description="Issue severity level")
|
|
27
|
+
code: str = Field("", description="Error/warning code")
|
|
28
|
+
message: str = Field(..., description="Issue description")
|
|
29
|
+
category: str = Field("general", description="Issue category")
|
|
30
|
+
suggestion: str | None = Field(None, description="How to fix the issue")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CodeMetricsSchema(BaseModel):
|
|
34
|
+
"""Schema for code quality metrics"""
|
|
35
|
+
lines_of_code: int = Field(0, ge=0)
|
|
36
|
+
lines_of_comments: int = Field(0, ge=0)
|
|
37
|
+
blank_lines: int = Field(0, ge=0)
|
|
38
|
+
cyclomatic_complexity: int = Field(0, ge=0)
|
|
39
|
+
cognitive_complexity: int = Field(0, ge=0)
|
|
40
|
+
function_count: int = Field(0, ge=0)
|
|
41
|
+
class_count: int = Field(0, ge=0)
|
|
42
|
+
max_nesting_depth: int = Field(0, ge=0)
|
|
43
|
+
maintainability_index: float = Field(0.0, ge=0.0, le=100.0)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestCaseSchema(BaseModel):
|
|
47
|
+
"""Schema for test case definition"""
|
|
48
|
+
name: str = Field(..., description="Test case name")
|
|
49
|
+
function: str = Field("main", description="Function to test")
|
|
50
|
+
inputs: list[Any] = Field(default_factory=list, description="Function inputs")
|
|
51
|
+
expected: Any = Field(None, description="Expected output")
|
|
52
|
+
description: str | None = Field(None, description="Test description")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AnalysisRequest(BaseModel):
|
|
56
|
+
"""Request schema for code analysis"""
|
|
57
|
+
code: str = Field(..., min_length=1, description="Source code to analyze")
|
|
58
|
+
language: AnalysisLanguage = Field(AnalysisLanguage.PYTHON, description="Programming language")
|
|
59
|
+
rubric_id: int | None = Field(None, description="Rubric ID for grading")
|
|
60
|
+
assignment_id: int | None = Field(None, description="Assignment ID")
|
|
61
|
+
|
|
62
|
+
# Student information (optional)
|
|
63
|
+
student_id: str | None = Field(None, description="Student identifier")
|
|
64
|
+
student_name: str | None = Field(None, description="Student name")
|
|
65
|
+
|
|
66
|
+
# Analysis options
|
|
67
|
+
check_similarity: bool = Field(True, description="Enable similarity checking")
|
|
68
|
+
run_tests: bool = Field(False, description="Run test cases")
|
|
69
|
+
test_cases: list[TestCaseSchema] | None = Field(None, description="Custom test cases")
|
|
70
|
+
|
|
71
|
+
# Execution options
|
|
72
|
+
execute_code: bool = Field(False, description="Execute code in sandbox")
|
|
73
|
+
input_data: str | None = Field(None, description="Input data for execution")
|
|
74
|
+
|
|
75
|
+
# Analysis configuration overrides
|
|
76
|
+
analyzer_config: dict[str, Any] | None = Field(None, description="Custom analyzer config")
|
|
77
|
+
|
|
78
|
+
@validator('code')
|
|
79
|
+
def validate_code_length(cls, v: str) -> str:
|
|
80
|
+
if len(v.encode('utf-8')) > 1024 * 1024: # 1MB limit
|
|
81
|
+
raise ValueError('Code size exceeds 1MB limit')
|
|
82
|
+
return v
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class BatchAnalysisRequest(BaseModel):
|
|
86
|
+
"""Request schema for batch analysis"""
|
|
87
|
+
files: list[dict[str, str]] = Field(..., min_length=1, description="List of files with code and path")
|
|
88
|
+
language: AnalysisLanguage = Field(AnalysisLanguage.PYTHON, description="Programming language")
|
|
89
|
+
assignment_id: int | None = Field(None, description="Assignment ID")
|
|
90
|
+
rubric_id: int | None = Field(None, description="Rubric ID for grading")
|
|
91
|
+
|
|
92
|
+
# Batch options
|
|
93
|
+
check_similarity: bool = Field(True, description="Enable cross-submission similarity checking")
|
|
94
|
+
parallel_processing: bool = Field(True, description="Process files in parallel")
|
|
95
|
+
|
|
96
|
+
@validator('files')
|
|
97
|
+
def validate_files(cls, v: list[dict[str, str]]) -> list[dict[str, str]]:
|
|
98
|
+
if len(v) > 100: # Max 100 files per batch
|
|
99
|
+
raise ValueError('Maximum 100 files per batch')
|
|
100
|
+
for file_info in v:
|
|
101
|
+
if 'code' not in file_info or 'path' not in file_info:
|
|
102
|
+
raise ValueError('Each file must have code and path fields')
|
|
103
|
+
return v
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ExecutionResultSchema(BaseModel):
|
|
107
|
+
"""Schema for code execution results"""
|
|
108
|
+
success: bool
|
|
109
|
+
stdout: str = ""
|
|
110
|
+
stderr: str = ""
|
|
111
|
+
exit_code: int = 0
|
|
112
|
+
execution_time: float = 0.0
|
|
113
|
+
memory_used: str | None = None
|
|
114
|
+
timed_out: bool = False
|
|
115
|
+
error_message: str | None = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestResultSchema(BaseModel):
|
|
119
|
+
"""Schema for test execution results"""
|
|
120
|
+
total_tests: int = 0
|
|
121
|
+
passed_tests: int = 0
|
|
122
|
+
failed_tests: list[dict[str, Any]] = Field(default_factory=list)
|
|
123
|
+
test_output: str = ""
|
|
124
|
+
execution_result: ExecutionResultSchema | None = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class SimilarityResultSchema(BaseModel):
|
|
128
|
+
"""Schema for similarity analysis results"""
|
|
129
|
+
highest_similarity: float = Field(0.0, ge=0.0, le=1.0)
|
|
130
|
+
flagged_submissions: list[dict[str, Any]] = Field(default_factory=list)
|
|
131
|
+
ai_baseline_similarity: float | None = Field(None, ge=0.0, le=1.0)
|
|
132
|
+
methods_used: list[str] = Field(default_factory=list)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class GradeBreakdownSchema(BaseModel):
|
|
136
|
+
"""Schema for grade breakdown by category"""
|
|
137
|
+
functionality: float = Field(0.0, ge=0.0, le=100.0)
|
|
138
|
+
style: float = Field(0.0, ge=0.0, le=100.0)
|
|
139
|
+
documentation: float = Field(0.0, ge=0.0, le=100.0)
|
|
140
|
+
testing: float = Field(0.0, ge=0.0, le=100.0)
|
|
141
|
+
total: float = Field(0.0, ge=0.0, le=100.0)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class FeedbackSchema(BaseModel):
|
|
145
|
+
"""Schema for feedback and recommendations"""
|
|
146
|
+
strengths: list[str] = Field(default_factory=list)
|
|
147
|
+
improvements: list[str] = Field(default_factory=list)
|
|
148
|
+
resources: list[str] = Field(default_factory=list)
|
|
149
|
+
detailed_comments: dict[str, str] = Field(default_factory=dict)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class AnalysisResponse(BaseModel):
|
|
153
|
+
"""Response schema for code analysis"""
|
|
154
|
+
success: bool
|
|
155
|
+
submission_id: str = Field(..., description="Unique submission identifier")
|
|
156
|
+
|
|
157
|
+
# Analysis results
|
|
158
|
+
syntax_valid: bool = True
|
|
159
|
+
syntax_errors: list[AnalysisIssueSchema] = Field(default_factory=list)
|
|
160
|
+
issues: list[AnalysisIssueSchema] = Field(default_factory=list)
|
|
161
|
+
metrics: CodeMetricsSchema = Field(default_factory=lambda: CodeMetricsSchema(
|
|
162
|
+
lines_of_code=0,
|
|
163
|
+
lines_of_comments=0,
|
|
164
|
+
blank_lines=0,
|
|
165
|
+
cyclomatic_complexity=0,
|
|
166
|
+
cognitive_complexity=0,
|
|
167
|
+
function_count=0,
|
|
168
|
+
class_count=0,
|
|
169
|
+
max_nesting_depth=0,
|
|
170
|
+
maintainability_index=0.0
|
|
171
|
+
))
|
|
172
|
+
|
|
173
|
+
# Execution results (if requested)
|
|
174
|
+
execution_result: ExecutionResultSchema | None = None
|
|
175
|
+
test_result: TestResultSchema | None = None
|
|
176
|
+
|
|
177
|
+
# Similarity results (if enabled)
|
|
178
|
+
similarity_result: SimilarityResultSchema | None = None
|
|
179
|
+
|
|
180
|
+
# Grading results (if rubric provided)
|
|
181
|
+
grade_breakdown: GradeBreakdownSchema | None = None
|
|
182
|
+
total_score: float | None = Field(None, ge=0.0, le=100.0)
|
|
183
|
+
max_score: float = Field(100.0, ge=0.0)
|
|
184
|
+
|
|
185
|
+
# Feedback
|
|
186
|
+
feedback: FeedbackSchema = Field(default_factory=FeedbackSchema)
|
|
187
|
+
|
|
188
|
+
# Metadata
|
|
189
|
+
analysis_version: str = ""
|
|
190
|
+
processing_time: float = 0.0
|
|
191
|
+
tools_used: dict[str, Any] = Field(default_factory=dict)
|
|
192
|
+
analyzed_at: datetime = Field(default_factory=datetime.utcnow)
|
|
193
|
+
|
|
194
|
+
# Error information
|
|
195
|
+
error_message: str | None = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class BatchAnalysisResponse(BaseModel):
|
|
199
|
+
"""Response schema for batch analysis"""
|
|
200
|
+
success: bool
|
|
201
|
+
batch_id: str = Field(..., description="Unique batch identifier")
|
|
202
|
+
total_files: int = Field(..., ge=0)
|
|
203
|
+
processed_files: int = Field(..., ge=0)
|
|
204
|
+
failed_files: int = Field(..., ge=0)
|
|
205
|
+
|
|
206
|
+
# Individual results
|
|
207
|
+
results: list[AnalysisResponse] = Field(default_factory=list)
|
|
208
|
+
|
|
209
|
+
# Batch-level similarity analysis
|
|
210
|
+
cross_similarity_results: list[dict[str, Any]] | None = None
|
|
211
|
+
|
|
212
|
+
# Timing information
|
|
213
|
+
total_processing_time: float = 0.0
|
|
214
|
+
average_processing_time: float = 0.0
|
|
215
|
+
|
|
216
|
+
# Status
|
|
217
|
+
completed_at: datetime = Field(default_factory=datetime.utcnow)
|
|
218
|
+
error_message: str | None = None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Rubric schemas
|
|
222
|
+
class RubricCriterionCreate(BaseModel):
|
|
223
|
+
"""Schema for creating a rubric criterion"""
|
|
224
|
+
name: str = Field(..., max_length=100)
|
|
225
|
+
description: str
|
|
226
|
+
category: str = Field(..., max_length=50)
|
|
227
|
+
max_points: int = Field(..., gt=0)
|
|
228
|
+
weight: float = Field(1.0, gt=0.0)
|
|
229
|
+
auto_gradable: bool = True
|
|
230
|
+
evaluation_method: str | None = Field(None, max_length=50)
|
|
231
|
+
evaluation_config: dict[str, Any] | None = None
|
|
232
|
+
performance_levels: dict[str, Any] = Field(..., description="Performance level definitions")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class RubricCreate(BaseModel):
|
|
236
|
+
"""Schema for creating a rubric"""
|
|
237
|
+
name: str = Field(..., max_length=200)
|
|
238
|
+
description: str | None = None
|
|
239
|
+
language: str = Field(..., max_length=50)
|
|
240
|
+
criteria: dict[str, Any] = Field(..., description="Grading criteria")
|
|
241
|
+
weights: dict[str, float] = Field(..., description="Category weights")
|
|
242
|
+
total_points: int = Field(100, gt=0)
|
|
243
|
+
analysis_config: dict[str, Any] | None = None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class RubricResponse(BaseModel):
|
|
247
|
+
"""Schema for rubric response"""
|
|
248
|
+
id: int
|
|
249
|
+
name: str
|
|
250
|
+
description: str | None = None
|
|
251
|
+
language: str
|
|
252
|
+
criteria: dict[str, Any]
|
|
253
|
+
weights: dict[str, float]
|
|
254
|
+
total_points: int
|
|
255
|
+
analysis_config: dict[str, Any] | None = None
|
|
256
|
+
created_at: datetime
|
|
257
|
+
updated_at: datetime
|
|
258
|
+
|
|
259
|
+
class Config:
|
|
260
|
+
from_attributes = True
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Assignment schemas
|
|
264
|
+
class AssignmentCreate(BaseModel):
|
|
265
|
+
"""Schema for creating an assignment"""
|
|
266
|
+
name: str = Field(..., max_length=200)
|
|
267
|
+
description: str
|
|
268
|
+
course_id: str | None = Field(None, max_length=50)
|
|
269
|
+
course_name: str | None = Field(None, max_length=200)
|
|
270
|
+
semester: str | None = Field(None, max_length=20)
|
|
271
|
+
language: str = Field(..., max_length=50)
|
|
272
|
+
rubric_id: int
|
|
273
|
+
requirements: dict[str, Any] = Field(..., description="Technical requirements")
|
|
274
|
+
test_cases: dict[str, Any] | None = None
|
|
275
|
+
starter_code: str | None = None
|
|
276
|
+
similarity_enabled: bool = True
|
|
277
|
+
similarity_threshold: float = Field(0.8, ge=0.0, le=1.0)
|
|
278
|
+
cross_cohort_check: bool = False
|
|
279
|
+
due_date: datetime | None = None
|
|
280
|
+
late_penalty: float | None = Field(None, ge=0.0)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class AssignmentResponse(BaseModel):
|
|
284
|
+
"""Schema for assignment response"""
|
|
285
|
+
id: int
|
|
286
|
+
name: str
|
|
287
|
+
description: str
|
|
288
|
+
course_id: str | None = None
|
|
289
|
+
course_name: str | None = None
|
|
290
|
+
semester: str | None = None
|
|
291
|
+
language: str
|
|
292
|
+
rubric_id: int
|
|
293
|
+
requirements: dict[str, Any]
|
|
294
|
+
test_cases: dict[str, Any] | None = None
|
|
295
|
+
starter_code: str | None = None
|
|
296
|
+
similarity_enabled: bool
|
|
297
|
+
similarity_threshold: float
|
|
298
|
+
cross_cohort_check: bool
|
|
299
|
+
due_date: datetime | None = None
|
|
300
|
+
late_penalty: float | None = None
|
|
301
|
+
created_at: datetime
|
|
302
|
+
updated_at: datetime
|
|
303
|
+
|
|
304
|
+
class Config:
|
|
305
|
+
from_attributes = True
|