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,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
@@ -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