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
codelens/cli.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for CodeLens batch processing
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
from codelens.core.config import settings
|
|
15
|
+
from codelens.services.batch_processor import BatchProcessingConfig, batch_processor
|
|
16
|
+
from codelens.utils import calculate_grade_letter, format_file_size
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def process_directory_command(args: Any) -> int:
|
|
22
|
+
"""Process a directory of code submissions"""
|
|
23
|
+
try:
|
|
24
|
+
print(f"CodeLens Batch Processor v{settings.version}")
|
|
25
|
+
print(f"Processing directory: {args.directory}")
|
|
26
|
+
print("-" * 50)
|
|
27
|
+
|
|
28
|
+
# Configure batch processor
|
|
29
|
+
config = BatchProcessingConfig(
|
|
30
|
+
parallel_processing=not args.sequential,
|
|
31
|
+
max_concurrent=args.max_concurrent,
|
|
32
|
+
skip_unsupported_files=not args.include_unsupported,
|
|
33
|
+
extract_student_info=args.extract_student_info,
|
|
34
|
+
default_language=args.language
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Override student ID patterns if provided
|
|
38
|
+
if args.student_id_patterns:
|
|
39
|
+
config.student_id_patterns = args.student_id_patterns.split(',')
|
|
40
|
+
|
|
41
|
+
# Create batch processor with config
|
|
42
|
+
processor = batch_processor.__class__(config)
|
|
43
|
+
|
|
44
|
+
# Process directory
|
|
45
|
+
result = await processor.process_directory(
|
|
46
|
+
directory_path=args.directory,
|
|
47
|
+
assignment_id=args.assignment_id,
|
|
48
|
+
rubric_id=args.rubric_id,
|
|
49
|
+
language=args.language
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Display results
|
|
53
|
+
print("\n" + "=" * 60)
|
|
54
|
+
print("BATCH PROCESSING RESULTS")
|
|
55
|
+
print("=" * 60)
|
|
56
|
+
|
|
57
|
+
print(f"Batch ID: {result.batch_id}")
|
|
58
|
+
print(f"Total Files: {result.total_files}")
|
|
59
|
+
print(f"Processed: {result.processed_files}")
|
|
60
|
+
print(f"Failed: {result.failed_files}")
|
|
61
|
+
print(f"Success Rate: {(result.processed_files / result.total_files * 100):.1f}%" if result.total_files > 0 else "0%")
|
|
62
|
+
print(f"Processing Time: {result.processing_time:.2f} seconds")
|
|
63
|
+
|
|
64
|
+
if result.average_score is not None:
|
|
65
|
+
print(f"Average Score: {result.average_score:.1f}%")
|
|
66
|
+
print(f"Average Grade: {calculate_grade_letter(result.average_score)}")
|
|
67
|
+
|
|
68
|
+
# Score distribution
|
|
69
|
+
if result.score_distribution:
|
|
70
|
+
print("\nScore Distribution:")
|
|
71
|
+
for grade_range, count in result.score_distribution.items():
|
|
72
|
+
percentage = (count / result.processed_files * 100) if result.processed_files > 0 else 0
|
|
73
|
+
print(f" {grade_range}: {count} students ({percentage:.1f}%)")
|
|
74
|
+
|
|
75
|
+
# Errors
|
|
76
|
+
if result.errors:
|
|
77
|
+
print("\nErrors:")
|
|
78
|
+
for i, error in enumerate(result.errors[:10], 1): # Show first 10 errors
|
|
79
|
+
print(f" {i}. {error}")
|
|
80
|
+
if len(result.errors) > 10:
|
|
81
|
+
print(f" ... and {len(result.errors) - 10} more errors")
|
|
82
|
+
|
|
83
|
+
# Detailed results
|
|
84
|
+
if args.detailed and result.results:
|
|
85
|
+
print("\n" + "-" * 60)
|
|
86
|
+
print("DETAILED RESULTS")
|
|
87
|
+
print("-" * 60)
|
|
88
|
+
|
|
89
|
+
for i, res in enumerate(result.results[:args.max_details], 1):
|
|
90
|
+
print(f"\n{i}. Submission: {res.submission_id}")
|
|
91
|
+
print(f" Success: {res.success}")
|
|
92
|
+
if res.total_score is not None:
|
|
93
|
+
print(f" Score: {res.total_score:.1f}% ({calculate_grade_letter(res.total_score)})")
|
|
94
|
+
print(f" Issues: {len(res.issues)}")
|
|
95
|
+
if res.metrics:
|
|
96
|
+
print(f" LOC: {res.metrics.lines_of_code}")
|
|
97
|
+
print(f" Complexity: {res.metrics.cyclomatic_complexity}")
|
|
98
|
+
if res.error_message:
|
|
99
|
+
print(f" Error: {res.error_message}")
|
|
100
|
+
|
|
101
|
+
# Save results to file if requested
|
|
102
|
+
if args.output:
|
|
103
|
+
output_data = {
|
|
104
|
+
"batch_id": result.batch_id,
|
|
105
|
+
"summary": {
|
|
106
|
+
"total_files": result.total_files,
|
|
107
|
+
"processed_files": result.processed_files,
|
|
108
|
+
"failed_files": result.failed_files,
|
|
109
|
+
"processing_time": result.processing_time,
|
|
110
|
+
"average_score": result.average_score,
|
|
111
|
+
"score_distribution": result.score_distribution
|
|
112
|
+
},
|
|
113
|
+
"results": [
|
|
114
|
+
{
|
|
115
|
+
"submission_id": r.submission_id,
|
|
116
|
+
"success": r.success,
|
|
117
|
+
"total_score": r.total_score,
|
|
118
|
+
"issues_count": len(r.issues),
|
|
119
|
+
"metrics": r.metrics.dict() if r.metrics else None,
|
|
120
|
+
"error_message": r.error_message
|
|
121
|
+
}
|
|
122
|
+
for r in result.results
|
|
123
|
+
],
|
|
124
|
+
"errors": result.errors
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
with open(args.output, 'w') as f:
|
|
128
|
+
json.dump(output_data, f, indent=2, default=str)
|
|
129
|
+
|
|
130
|
+
print(f"\nResults saved to: {args.output}")
|
|
131
|
+
|
|
132
|
+
return 0 if result.success else 1
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.error("Directory processing failed", error=str(e))
|
|
136
|
+
print(f"Error: {str(e)}", file=sys.stderr)
|
|
137
|
+
return 1
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def analyze_single_file(args: Any) -> int:
|
|
141
|
+
"""Analyze a single code file"""
|
|
142
|
+
try:
|
|
143
|
+
file_path = Path(args.file)
|
|
144
|
+
|
|
145
|
+
if not file_path.exists():
|
|
146
|
+
print(f"Error: File {args.file} does not exist", file=sys.stderr)
|
|
147
|
+
return 1
|
|
148
|
+
|
|
149
|
+
print(f"Analyzing file: {file_path}")
|
|
150
|
+
print(f"File size: {format_file_size(file_path.stat().st_size)}")
|
|
151
|
+
print("-" * 40)
|
|
152
|
+
|
|
153
|
+
# Read file content
|
|
154
|
+
with open(file_path, encoding='utf-8') as f:
|
|
155
|
+
code = f.read()
|
|
156
|
+
|
|
157
|
+
# Process as single file batch
|
|
158
|
+
files_data = [{
|
|
159
|
+
"code": code,
|
|
160
|
+
"path": str(file_path),
|
|
161
|
+
"student_id": args.student_id,
|
|
162
|
+
"student_name": args.student_name
|
|
163
|
+
}]
|
|
164
|
+
|
|
165
|
+
result = await batch_processor.process_files_list(
|
|
166
|
+
files_data=files_data,
|
|
167
|
+
assignment_id=args.assignment_id,
|
|
168
|
+
rubric_id=args.rubric_id,
|
|
169
|
+
language=args.language
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if result.results:
|
|
173
|
+
res = result.results[0]
|
|
174
|
+
print(f"Analysis completed in {result.processing_time:.2f} seconds")
|
|
175
|
+
print(f"Success: {res.success}")
|
|
176
|
+
|
|
177
|
+
if res.total_score is not None:
|
|
178
|
+
print(f"Score: {res.total_score:.1f}% ({calculate_grade_letter(res.total_score)})")
|
|
179
|
+
|
|
180
|
+
if res.metrics:
|
|
181
|
+
print("\nCode Metrics:")
|
|
182
|
+
print(f" Lines of Code: {res.metrics.lines_of_code}")
|
|
183
|
+
print(f" Cyclomatic Complexity: {res.metrics.cyclomatic_complexity}")
|
|
184
|
+
print(f" Functions: {res.metrics.function_count}")
|
|
185
|
+
print(f" Classes: {res.metrics.class_count}")
|
|
186
|
+
|
|
187
|
+
if res.issues:
|
|
188
|
+
print(f"\nIssues Found ({len(res.issues)}):")
|
|
189
|
+
for i, issue in enumerate(res.issues[:10], 1): # Show first 10 issues
|
|
190
|
+
print(f" {i}. Line {issue.line}: {issue.message} ({issue.severity.value})")
|
|
191
|
+
if len(res.issues) > 10:
|
|
192
|
+
print(f" ... and {len(res.issues) - 10} more issues")
|
|
193
|
+
|
|
194
|
+
if res.error_message:
|
|
195
|
+
print(f"\nError: {res.error_message}")
|
|
196
|
+
|
|
197
|
+
return 0 if result.success else 1
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.error("File analysis failed", error=str(e))
|
|
201
|
+
print(f"Error: {str(e)}", file=sys.stderr)
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
206
|
+
"""Create command-line argument parser"""
|
|
207
|
+
parser = argparse.ArgumentParser(
|
|
208
|
+
description="CodeLens - Automated Code Analysis for Educational Use",
|
|
209
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
210
|
+
epilog="""
|
|
211
|
+
Examples:
|
|
212
|
+
# Process a directory of Python submissions
|
|
213
|
+
python -m codelens.cli batch /path/to/submissions --language python
|
|
214
|
+
|
|
215
|
+
# Process with specific rubric and assignment
|
|
216
|
+
python -m codelens.cli batch /path/to/submissions --rubric-id 1 --assignment-id 5
|
|
217
|
+
|
|
218
|
+
# Analyze a single file
|
|
219
|
+
python -m codelens.cli analyze submission.py --student-id cs123456
|
|
220
|
+
|
|
221
|
+
# Generate detailed report with output file
|
|
222
|
+
python -m codelens.cli batch /submissions --detailed --output results.json
|
|
223
|
+
"""
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
|
227
|
+
|
|
228
|
+
# Batch processing command
|
|
229
|
+
batch_parser = subparsers.add_parser('batch', help='Process directory of code submissions')
|
|
230
|
+
batch_parser.add_argument('directory', help='Directory containing code submissions')
|
|
231
|
+
batch_parser.add_argument('--language', default='python',
|
|
232
|
+
help='Programming language (default: python)')
|
|
233
|
+
batch_parser.add_argument('--assignment-id', type=int,
|
|
234
|
+
help='Assignment ID for database storage')
|
|
235
|
+
batch_parser.add_argument('--rubric-id', type=int,
|
|
236
|
+
help='Rubric ID for grading')
|
|
237
|
+
batch_parser.add_argument('--sequential', action='store_true',
|
|
238
|
+
help='Process files sequentially instead of parallel')
|
|
239
|
+
batch_parser.add_argument('--max-concurrent', type=int, default=5,
|
|
240
|
+
help='Maximum concurrent processing (default: 5)')
|
|
241
|
+
batch_parser.add_argument('--include-unsupported', action='store_true',
|
|
242
|
+
help='Include unsupported file types')
|
|
243
|
+
batch_parser.add_argument('--no-extract-student-info', dest='extract_student_info',
|
|
244
|
+
action='store_false', default=True,
|
|
245
|
+
help='Disable automatic student info extraction')
|
|
246
|
+
batch_parser.add_argument('--student-id-patterns',
|
|
247
|
+
help='Comma-separated regex patterns for student ID extraction')
|
|
248
|
+
batch_parser.add_argument('--detailed', action='store_true',
|
|
249
|
+
help='Show detailed results for each submission')
|
|
250
|
+
batch_parser.add_argument('--max-details', type=int, default=20,
|
|
251
|
+
help='Maximum detailed results to show (default: 20)')
|
|
252
|
+
batch_parser.add_argument('--output', '-o',
|
|
253
|
+
help='Output file for results (JSON format)')
|
|
254
|
+
|
|
255
|
+
# Single file analysis command
|
|
256
|
+
analyze_parser = subparsers.add_parser('analyze', help='Analyze a single code file')
|
|
257
|
+
analyze_parser.add_argument('file', help='Code file to analyze')
|
|
258
|
+
analyze_parser.add_argument('--language', default='python',
|
|
259
|
+
help='Programming language (default: python)')
|
|
260
|
+
analyze_parser.add_argument('--student-id', help='Student ID')
|
|
261
|
+
analyze_parser.add_argument('--student-name', help='Student name')
|
|
262
|
+
analyze_parser.add_argument('--assignment-id', type=int,
|
|
263
|
+
help='Assignment ID for database storage')
|
|
264
|
+
analyze_parser.add_argument('--rubric-id', type=int,
|
|
265
|
+
help='Rubric ID for grading')
|
|
266
|
+
|
|
267
|
+
return parser
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def main() -> int:
|
|
271
|
+
"""Main CLI entry point"""
|
|
272
|
+
parser = create_parser()
|
|
273
|
+
args = parser.parse_args()
|
|
274
|
+
|
|
275
|
+
if not args.command:
|
|
276
|
+
parser.print_help()
|
|
277
|
+
return 1
|
|
278
|
+
|
|
279
|
+
if args.command == 'batch':
|
|
280
|
+
return await process_directory_command(args)
|
|
281
|
+
elif args.command == 'analyze':
|
|
282
|
+
return await analyze_single_file(args)
|
|
283
|
+
else:
|
|
284
|
+
parser.print_help()
|
|
285
|
+
return 1
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
try:
|
|
290
|
+
exit_code = asyncio.run(main())
|
|
291
|
+
sys.exit(exit_code)
|
|
292
|
+
except KeyboardInterrupt:
|
|
293
|
+
print("\nInterrupted by user", file=sys.stderr)
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
print(f"Unexpected error: {str(e)}", file=sys.stderr)
|
|
297
|
+
sys.exit(1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core application components"""
|
codelens/core/config.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application configuration management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnalyzerConfig(BaseModel):
|
|
11
|
+
"""Configuration for code analysis tools"""
|
|
12
|
+
|
|
13
|
+
# Python analyzers
|
|
14
|
+
ruff_enabled: bool = True
|
|
15
|
+
ruff_config: str | None = None
|
|
16
|
+
mypy_enabled: bool = True
|
|
17
|
+
mypy_config: str | None = None
|
|
18
|
+
|
|
19
|
+
# Analysis options
|
|
20
|
+
max_complexity: int = 10
|
|
21
|
+
max_line_length: int = 88
|
|
22
|
+
check_type_hints: bool = True
|
|
23
|
+
check_docstrings: bool = True
|
|
24
|
+
|
|
25
|
+
# Execution limits
|
|
26
|
+
execution_timeout: int = 30 # seconds
|
|
27
|
+
memory_limit: str = "128m" # Docker memory limit
|
|
28
|
+
cpu_limit: str = "0.5" # Docker CPU limit
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SimilarityConfig(BaseModel):
|
|
32
|
+
"""Configuration for similarity detection"""
|
|
33
|
+
|
|
34
|
+
enabled: bool = True
|
|
35
|
+
threshold: float = 0.8 # Similarity threshold for flagging
|
|
36
|
+
methods: list[str] = ["ast_structural", "token_based"]
|
|
37
|
+
|
|
38
|
+
# AI-generated baseline comparison
|
|
39
|
+
use_ai_baselines: bool = True
|
|
40
|
+
ai_baseline_count: int = 5
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DatabaseConfig(BaseModel):
|
|
44
|
+
"""Database configuration"""
|
|
45
|
+
|
|
46
|
+
url: str = "sqlite+aiosqlite:///./codelens.db"
|
|
47
|
+
echo: bool = False # SQL logging
|
|
48
|
+
pool_size: int = 5
|
|
49
|
+
max_overflow: int = 10
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Settings(BaseSettings):
|
|
53
|
+
"""Application settings"""
|
|
54
|
+
|
|
55
|
+
model_config = SettingsConfigDict(
|
|
56
|
+
env_file=".env",
|
|
57
|
+
env_file_encoding="utf-8",
|
|
58
|
+
case_sensitive=False
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Application
|
|
62
|
+
app_name: str = "CodeLens"
|
|
63
|
+
debug: bool = False
|
|
64
|
+
version: str = "0.1.0"
|
|
65
|
+
|
|
66
|
+
# API
|
|
67
|
+
api_prefix: str = "/api/v1"
|
|
68
|
+
host: str = "localhost"
|
|
69
|
+
port: int = 8000
|
|
70
|
+
docs_enabled: bool = True # Always enable Swagger docs for educational tool
|
|
71
|
+
|
|
72
|
+
# Security
|
|
73
|
+
secret_key: str = Field(default="your-secret-key-change-in-production")
|
|
74
|
+
access_token_expire_minutes: int = 30
|
|
75
|
+
|
|
76
|
+
# Analysis configuration
|
|
77
|
+
analyzer: AnalyzerConfig = Field(default_factory=AnalyzerConfig)
|
|
78
|
+
similarity: SimilarityConfig = Field(default_factory=SimilarityConfig)
|
|
79
|
+
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
|
|
80
|
+
|
|
81
|
+
# Docker settings
|
|
82
|
+
docker_enabled: bool = True
|
|
83
|
+
docker_image: str = "python:3.11-slim"
|
|
84
|
+
|
|
85
|
+
# File limits
|
|
86
|
+
max_file_size: int = 1024 * 1024 # 1MB
|
|
87
|
+
max_files_per_batch: int = 100
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Global settings instance
|
|
91
|
+
settings = Settings()
|
codelens/db/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Database configuration and connection"""
|
codelens/db/database.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database configuration and connection management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import AsyncGenerator
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
9
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
10
|
+
|
|
11
|
+
from codelens.core.config import settings
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger()
|
|
14
|
+
|
|
15
|
+
# Create async engine
|
|
16
|
+
engine = create_async_engine(
|
|
17
|
+
settings.database.url,
|
|
18
|
+
echo=settings.database.echo,
|
|
19
|
+
pool_size=settings.database.pool_size,
|
|
20
|
+
max_overflow=settings.database.max_overflow,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Create async session maker
|
|
24
|
+
AsyncSessionLocal = async_sessionmaker(
|
|
25
|
+
bind=engine,
|
|
26
|
+
class_=AsyncSession,
|
|
27
|
+
expire_on_commit=False,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Base(DeclarativeBase):
|
|
32
|
+
"""Base class for all database models"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
37
|
+
"""Dependency to get database session"""
|
|
38
|
+
async with AsyncSessionLocal() as session:
|
|
39
|
+
try:
|
|
40
|
+
yield session
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error("Database session error", error=str(e))
|
|
43
|
+
await session.rollback()
|
|
44
|
+
raise
|
|
45
|
+
finally:
|
|
46
|
+
await session.close()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def init_db() -> None:
|
|
50
|
+
"""Initialize database tables"""
|
|
51
|
+
async with engine.begin() as conn:
|
|
52
|
+
# Import all models to ensure they're registered
|
|
53
|
+
|
|
54
|
+
# Create all tables
|
|
55
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
56
|
+
|
|
57
|
+
logger.info("Database tables initialized")
|
codelens/main.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI application entry point
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
|
|
11
|
+
from codelens.api.routes import analysis, reports, rubrics
|
|
12
|
+
from codelens.core.config import settings
|
|
13
|
+
from codelens.db.database import init_db
|
|
14
|
+
|
|
15
|
+
# Configure structured logging
|
|
16
|
+
structlog.configure(
|
|
17
|
+
processors=[
|
|
18
|
+
structlog.stdlib.filter_by_level,
|
|
19
|
+
structlog.stdlib.add_logger_name,
|
|
20
|
+
structlog.stdlib.add_log_level,
|
|
21
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
22
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
23
|
+
structlog.processors.StackInfoRenderer(),
|
|
24
|
+
structlog.processors.format_exc_info,
|
|
25
|
+
structlog.processors.UnicodeDecoder(),
|
|
26
|
+
structlog.processors.JSONRenderer()
|
|
27
|
+
],
|
|
28
|
+
context_class=dict,
|
|
29
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
30
|
+
cache_logger_on_first_use=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
logger = structlog.get_logger()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_app() -> FastAPI:
|
|
37
|
+
"""Create FastAPI application"""
|
|
38
|
+
|
|
39
|
+
app = FastAPI(
|
|
40
|
+
title=settings.app_name,
|
|
41
|
+
description="Automated Code Analysis & Grading Assistant for Educators",
|
|
42
|
+
version=settings.version,
|
|
43
|
+
debug=settings.debug,
|
|
44
|
+
openapi_url="/openapi.json" if settings.docs_enabled else None,
|
|
45
|
+
docs_url="/docs" if settings.docs_enabled else None,
|
|
46
|
+
redoc_url="/redoc" if settings.docs_enabled else None,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# CORS middleware
|
|
50
|
+
app.add_middleware(
|
|
51
|
+
CORSMiddleware,
|
|
52
|
+
allow_origins=["*"] if settings.debug else [],
|
|
53
|
+
allow_credentials=True,
|
|
54
|
+
allow_methods=["*"],
|
|
55
|
+
allow_headers=["*"],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Include routers
|
|
59
|
+
app.include_router(
|
|
60
|
+
analysis.router,
|
|
61
|
+
prefix=f"{settings.api_prefix}/analyze",
|
|
62
|
+
tags=["analysis"]
|
|
63
|
+
)
|
|
64
|
+
app.include_router(
|
|
65
|
+
rubrics.router,
|
|
66
|
+
prefix=f"{settings.api_prefix}/rubrics",
|
|
67
|
+
tags=["rubrics"]
|
|
68
|
+
)
|
|
69
|
+
app.include_router(
|
|
70
|
+
reports.router,
|
|
71
|
+
prefix=f"{settings.api_prefix}/reports",
|
|
72
|
+
tags=["reports"]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@app.on_event("startup")
|
|
76
|
+
async def startup_event() -> None:
|
|
77
|
+
"""Initialize application on startup"""
|
|
78
|
+
logger.info("Starting CodeLens application")
|
|
79
|
+
await init_db()
|
|
80
|
+
logger.info("Database initialized")
|
|
81
|
+
|
|
82
|
+
@app.on_event("shutdown")
|
|
83
|
+
async def shutdown_event() -> None:
|
|
84
|
+
"""Cleanup on shutdown"""
|
|
85
|
+
logger.info("Shutting down CodeLens application")
|
|
86
|
+
|
|
87
|
+
@app.get("/health")
|
|
88
|
+
async def health_check() -> dict[str, Any]:
|
|
89
|
+
"""Health check endpoint"""
|
|
90
|
+
return {
|
|
91
|
+
"status": "healthy",
|
|
92
|
+
"version": settings.version,
|
|
93
|
+
"app": settings.app_name
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return app
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Create app instance
|
|
100
|
+
app = create_app()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
import uvicorn
|
|
105
|
+
uvicorn.run(
|
|
106
|
+
"codelens.main:app",
|
|
107
|
+
host=settings.host,
|
|
108
|
+
port=settings.port,
|
|
109
|
+
reload=settings.debug,
|
|
110
|
+
log_config=None, # Use structlog configuration
|
|
111
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Database models"""
|
|
2
|
+
|
|
3
|
+
from .assignments import Assignment, TestCase
|
|
4
|
+
from .reports import AnalysisReport, SimilarityMatch
|
|
5
|
+
from .rubrics import Rubric, RubricCriterion
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Rubric",
|
|
9
|
+
"RubricCriterion",
|
|
10
|
+
"Assignment",
|
|
11
|
+
"TestCase",
|
|
12
|
+
"AnalysisReport",
|
|
13
|
+
"SimilarityMatch",
|
|
14
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database models for assignments and specifications
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, Integer, String, Text
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
10
|
+
from sqlalchemy.sql import func
|
|
11
|
+
|
|
12
|
+
from codelens.db.database import Base
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .reports import AnalysisReport
|
|
16
|
+
from .rubrics import Rubric
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Assignment(Base):
|
|
20
|
+
"""Assignment specification and requirements"""
|
|
21
|
+
|
|
22
|
+
__tablename__ = "assignments"
|
|
23
|
+
|
|
24
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
|
25
|
+
name: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
|
|
26
|
+
description: Mapped[str] = mapped_column(Text, nullable=False)
|
|
27
|
+
|
|
28
|
+
# Course information
|
|
29
|
+
course_id: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True)
|
|
30
|
+
course_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
|
31
|
+
semester: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
|
32
|
+
|
|
33
|
+
# Assignment configuration
|
|
34
|
+
language: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
|
35
|
+
rubric_id: Mapped[int] = mapped_column(
|
|
36
|
+
Integer, ForeignKey("rubrics.id"), nullable=False, index=True
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Requirements and specifications
|
|
40
|
+
requirements: Mapped[dict] = mapped_column(JSON, nullable=False) # Technical requirements
|
|
41
|
+
test_cases: Mapped[dict | None] = mapped_column(JSON, nullable=True) # Expected outputs
|
|
42
|
+
starter_code: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
43
|
+
|
|
44
|
+
# Similarity checking configuration
|
|
45
|
+
similarity_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
46
|
+
similarity_threshold: Mapped[float] = mapped_column(nullable=False, default=0.8)
|
|
47
|
+
cross_cohort_check: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
48
|
+
|
|
49
|
+
# AI baseline configuration
|
|
50
|
+
ai_baselines: Mapped[dict | None] = mapped_column(JSON, nullable=True) # Generated code variants
|
|
51
|
+
|
|
52
|
+
# Deadlines
|
|
53
|
+
due_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
54
|
+
late_penalty: Mapped[float | None] = mapped_column(nullable=True, default=0.0) # Per day penalty
|
|
55
|
+
|
|
56
|
+
# Metadata
|
|
57
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
58
|
+
DateTime(timezone=True),
|
|
59
|
+
server_default=func.now(),
|
|
60
|
+
nullable=False
|
|
61
|
+
)
|
|
62
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
63
|
+
DateTime(timezone=True),
|
|
64
|
+
server_default=func.now(),
|
|
65
|
+
onupdate=func.now(),
|
|
66
|
+
nullable=False
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Relationships
|
|
70
|
+
rubric: Mapped["Rubric"] = relationship("Rubric", back_populates="assignments")
|
|
71
|
+
reports: Mapped[list["AnalysisReport"]] = relationship(
|
|
72
|
+
"AnalysisReport", back_populates="assignment", cascade="all, delete-orphan"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestCase(Base):
|
|
77
|
+
"""Test cases for assignment validation"""
|
|
78
|
+
|
|
79
|
+
__tablename__ = "test_cases"
|
|
80
|
+
|
|
81
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
|
82
|
+
assignment_id: Mapped[int] = mapped_column(
|
|
83
|
+
Integer, ForeignKey("assignments.id"), nullable=False, index=True
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Test case details
|
|
87
|
+
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
88
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
89
|
+
test_type: Mapped[str] = mapped_column(String(50), nullable=False) # unit, integration, etc
|
|
90
|
+
|
|
91
|
+
# Test configuration
|
|
92
|
+
input_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
93
|
+
expected_output: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
94
|
+
test_code: Mapped[str | None] = mapped_column(Text, nullable=True) # Custom test code
|
|
95
|
+
|
|
96
|
+
# Scoring
|
|
97
|
+
points: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
98
|
+
required: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
99
|
+
|
|
100
|
+
# Metadata
|
|
101
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
102
|
+
DateTime(timezone=True),
|
|
103
|
+
server_default=func.now(),
|
|
104
|
+
nullable=False
|
|
105
|
+
)
|