QuantumChecker 0.2.6__tar.gz → 0.2.8__tar.gz
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.
- quantumchecker-0.2.8/PKG-INFO +138 -0
- quantumchecker-0.2.8/QuantumCheck/main.py +188 -0
- {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumCheck/powerbi_evaluator.py +91 -173
- quantumchecker-0.2.8/QuantumCheck/prompts.py +185 -0
- quantumchecker-0.2.8/QuantumCheck/python_evaluator.py +194 -0
- quantumchecker-0.2.8/QuantumCheck/sql_evaluator.py +196 -0
- quantumchecker-0.2.8/QuantumCheck/ssis_evaluator.py +292 -0
- quantumchecker-0.2.8/QuantumChecker.egg-info/PKG-INFO +138 -0
- {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumChecker.egg-info/requires.txt +1 -0
- quantumchecker-0.2.8/README.md +112 -0
- {quantumchecker-0.2.6 → quantumchecker-0.2.8}/setup.py +3 -2
- quantumchecker-0.2.8/tests/test.py +388 -0
- quantumchecker-0.2.6/PKG-INFO +0 -33
- quantumchecker-0.2.6/QuantumCheck/main.py +0 -125
- quantumchecker-0.2.6/QuantumCheck/prompts.py +0 -230
- quantumchecker-0.2.6/QuantumCheck/python_evaluator.py +0 -95
- quantumchecker-0.2.6/QuantumCheck/sql_evaluator.py +0 -97
- quantumchecker-0.2.6/QuantumCheck/ssis_evaluator.py +0 -136
- quantumchecker-0.2.6/QuantumChecker.egg-info/PKG-INFO +0 -33
- quantumchecker-0.2.6/README.md +0 -8
- quantumchecker-0.2.6/tests/test.py +0 -29
- {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumCheck/__init__.py +0 -0
- {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumChecker.egg-info/SOURCES.txt +0 -0
- {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumChecker.egg-info/dependency_links.txt +0 -0
- {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumChecker.egg-info/top_level.txt +0 -0
- {quantumchecker-0.2.6 → quantumchecker-0.2.8}/setup.cfg +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: QuantumChecker
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: A package to evaluate homework submissions in Python, SQL, PowerBI, and SSIS.
|
|
5
|
+
Author: Qobiljon
|
|
6
|
+
Author-email: qobiljonkhayrullayev@gmail.com
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: requests>=2.31.0
|
|
13
|
+
Requires-Dist: tenacity>=8.2.3
|
|
14
|
+
Requires-Dist: pdf2image>=1.16.3
|
|
15
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
16
|
+
Requires-Dist: Pillow>=10.0.0
|
|
17
|
+
Requires-Dist: PyPDF2>=3.0.1
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# 📘 HomeworkEvaluator
|
|
28
|
+
|
|
29
|
+
The **HomeworkEvaluator** is a Python-based evaluation engine designed to automatically assess student assignments across different technologies including Python, SQL, Power BI, and SSIS. It uses AI to parse and evaluate student-submitted answers against a set of markdown-formatted questions.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## ✨ Features
|
|
34
|
+
|
|
35
|
+
- Supports multiple file types:
|
|
36
|
+
- `.py` → Python
|
|
37
|
+
- `.sql` → SQL
|
|
38
|
+
- `.zip` → Power BI
|
|
39
|
+
- `.dtsx` / `.DTSX` → SSIS
|
|
40
|
+
- `.txt` / `.md` → Plain Text
|
|
41
|
+
- Smart evaluator routing based on file extension.
|
|
42
|
+
- AI-powered feedback generation and scoring.
|
|
43
|
+
- Logging for each evaluation by file type and timestamp.
|
|
44
|
+
- Automatic fallback to backup API keys when quota is exceeded.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 📦 Folder Structure
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
.
|
|
52
|
+
├── homework_evaluator/
|
|
53
|
+
│ ├── homework_evaluator.py # Main evaluator class
|
|
54
|
+
│ ├── python_evaluator.py # Python evaluator logic
|
|
55
|
+
│ ├── sql_evaluator.py # SQL evaluator logic
|
|
56
|
+
│ ├── powerbi_evaluator.py # Power BI evaluator logic
|
|
57
|
+
│ ├── ssis_evaluator.py # SSIS evaluator logic
|
|
58
|
+
│ └── logs/ # Log files categorized by type and timestamp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🔧 Installation
|
|
64
|
+
|
|
65
|
+
Clone this repository and install the necessary dependencies:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/yourusername/homework-evaluator.git
|
|
69
|
+
cd homework-evaluator
|
|
70
|
+
pip install -r requirements.txt
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🧠 Usage
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from homework_evaluator import HomeworkEvaluator
|
|
79
|
+
|
|
80
|
+
evaluator = HomeworkEvaluator()
|
|
81
|
+
|
|
82
|
+
result = evaluator.evaluate_from_content(
|
|
83
|
+
question_content=markdown_questions,
|
|
84
|
+
answer_path="/path/to/answer/file.py",
|
|
85
|
+
api_key="your-main-api-key",
|
|
86
|
+
backup_api_keys=["backup-key-1", "backup-key-2"]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(result["mark"]) # e.g., 85
|
|
90
|
+
print(result["feedback"]) # Structured feedback
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 🗂️ Question Format
|
|
96
|
+
|
|
97
|
+
The evaluator expects `question_content` as a Markdown-formatted string where each question is separated by a **double newline** (`\n\n`). Example:
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
### Q1
|
|
101
|
+
Write a Python function to reverse a string.
|
|
102
|
+
|
|
103
|
+
### Q2
|
|
104
|
+
Explain the purpose of list comprehensions in Python.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 🛠️ Logging
|
|
110
|
+
|
|
111
|
+
All evaluations are logged under the `logs/` directory, grouped by file type and timestamp.
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
logs/
|
|
115
|
+
├── python/
|
|
116
|
+
│ └── evaluation_2025-05-26_14-00-00.log
|
|
117
|
+
├── sql/
|
|
118
|
+
│ └── evaluation_2025-05-26_14-10-12.log
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🧪 Exception Handling
|
|
124
|
+
|
|
125
|
+
- If a file is not found or questions are malformed, an informative error is raised.
|
|
126
|
+
- If the API quota is exceeded (429 errors or rate limits), it retries using backup keys.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 📄 License
|
|
131
|
+
|
|
132
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 🤝 Contributing
|
|
137
|
+
|
|
138
|
+
Pull requests are welcome. For major changes, please open an issue first to discuss your ideas.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import zipfile
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import List, Dict, Optional
|
|
6
|
+
from .python_evaluator import PythonEvaluator
|
|
7
|
+
from .sql_evaluator import SQLEvaluator
|
|
8
|
+
from .powerbi_evaluator import PowerBIEvaluator
|
|
9
|
+
from .ssis_evaluator import SSISEvaluator
|
|
10
|
+
from pprint import pprint
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
class HomeworkEvaluator:
|
|
15
|
+
EXTENSION_TO_TYPE = {
|
|
16
|
+
".py": "python",
|
|
17
|
+
".sql": "sql",
|
|
18
|
+
".pbit": "powerbi",
|
|
19
|
+
".pdf": "powerbi",
|
|
20
|
+
".dtsx": "ssis",
|
|
21
|
+
".DTSX": "ssis",
|
|
22
|
+
".txt": "text",
|
|
23
|
+
".md": "text"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def _setup_logger(self, file_type: str) -> logging.Logger:
|
|
27
|
+
base_log_dir = os.path.join(os.path.dirname(__file__), "logs")
|
|
28
|
+
type_log_dir = os.path.join(base_log_dir, file_type)
|
|
29
|
+
os.makedirs(type_log_dir, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
32
|
+
log_file_path = os.path.join(type_log_dir, f"evaluation_{timestamp}.log")
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(f"{file_type}_{timestamp}")
|
|
35
|
+
logger.setLevel(logging.INFO)
|
|
36
|
+
|
|
37
|
+
if not logger.handlers:
|
|
38
|
+
file_handler = logging.FileHandler(log_file_path, encoding="utf-8")
|
|
39
|
+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
40
|
+
file_handler.setFormatter(formatter)
|
|
41
|
+
logger.addHandler(file_handler)
|
|
42
|
+
|
|
43
|
+
return logger
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def parse_questions(md_content: str) -> List[str]:
|
|
47
|
+
questions = [q.strip() for q in md_content.strip().split("\n\n") if q.strip()]
|
|
48
|
+
if not questions:
|
|
49
|
+
logger.error("No valid questions found in the question content")
|
|
50
|
+
raise ValueError("No valid questions found in the question content")
|
|
51
|
+
logger.info("Parsed %d questions from content", len(questions))
|
|
52
|
+
return questions
|
|
53
|
+
|
|
54
|
+
def _detect_zip_content_type(self, zip_path: str) -> str:
|
|
55
|
+
"""Determine the file type based on ZIP contents."""
|
|
56
|
+
try:
|
|
57
|
+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
58
|
+
files = [f for f in zip_ref.namelist() if not f.startswith('__MACOSX/')]
|
|
59
|
+
extensions = {os.path.splitext(f)[1].lower() for f in files if os.path.splitext(f)[1]}
|
|
60
|
+
|
|
61
|
+
if not extensions:
|
|
62
|
+
logger.warning("No valid files found in ZIP: %s", zip_path)
|
|
63
|
+
return "text"
|
|
64
|
+
|
|
65
|
+
# Check for specific file types in order of priority: sql, powerbi, ssis, python
|
|
66
|
+
for ext in [".sql", ".pbit", ".pdf", ".dtsx", ".DTSX", ".py"]:
|
|
67
|
+
if ext in extensions and ext in self.EXTENSION_TO_TYPE:
|
|
68
|
+
file_type = self.EXTENSION_TO_TYPE[ext]
|
|
69
|
+
logger.info("Detected file type: %s from extension: %s in ZIP: %s", file_type, ext, zip_path)
|
|
70
|
+
return file_type
|
|
71
|
+
|
|
72
|
+
# Fallback to text if only .txt or .md are present
|
|
73
|
+
if extensions.issubset({".txt", ".md"}):
|
|
74
|
+
logger.info("Defaulting to text type for ZIP contents with extensions: %s", extensions)
|
|
75
|
+
return "text"
|
|
76
|
+
|
|
77
|
+
logger.warning("No recognized specific file types in ZIP: %s, extensions: %s", zip_path, extensions)
|
|
78
|
+
return "text"
|
|
79
|
+
except zipfile.BadZipFile:
|
|
80
|
+
logger.error("Invalid ZIP file: %s", zip_path)
|
|
81
|
+
return "text"
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error("Error inspecting ZIP file %s: %s", zip_path, str(e))
|
|
84
|
+
return "text"
|
|
85
|
+
|
|
86
|
+
def evaluate_from_content(
|
|
87
|
+
self,
|
|
88
|
+
question_content: str,
|
|
89
|
+
answer_path: str,
|
|
90
|
+
api_key: str,
|
|
91
|
+
backup_api_keys: Optional[List[str]] = None,
|
|
92
|
+
) -> Dict[str, any]:
|
|
93
|
+
if backup_api_keys is None:
|
|
94
|
+
backup_api_keys = []
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
questions = self.parse_questions(question_content)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error("Failed to parse question content: %s", str(e))
|
|
100
|
+
return {
|
|
101
|
+
"score": 0,
|
|
102
|
+
"feedback": f"Error parsing question content: {str(e)}",
|
|
103
|
+
"issues": [str(e)],
|
|
104
|
+
"recommendations": []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
answer_path = answer_path.strip()
|
|
108
|
+
_, ext = os.path.splitext(answer_path)
|
|
109
|
+
ext = ext.lower()
|
|
110
|
+
|
|
111
|
+
# Determine file type
|
|
112
|
+
if ext == ".zip":
|
|
113
|
+
file_type = self._detect_zip_content_type(answer_path)
|
|
114
|
+
else:
|
|
115
|
+
file_type = self.EXTENSION_TO_TYPE.get(ext, "text")
|
|
116
|
+
|
|
117
|
+
eval_logger = self._setup_logger(file_type)
|
|
118
|
+
eval_logger.info("Processing answer_path: %s", answer_path)
|
|
119
|
+
eval_logger.info("Extracted extension: %s", ext)
|
|
120
|
+
eval_logger.info("Detected file type: %s for file: %s", file_type, answer_path)
|
|
121
|
+
pprint(f"Processing {len(questions)} questions for file type: {file_type}")
|
|
122
|
+
|
|
123
|
+
if not os.path.exists(answer_path):
|
|
124
|
+
eval_logger.error("Answer file not found: %s", answer_path)
|
|
125
|
+
return {
|
|
126
|
+
"score": 0,
|
|
127
|
+
"feedback": f"Answer file not found: {answer_path}",
|
|
128
|
+
"issues": [f"Answer file not found: {answer_path}"],
|
|
129
|
+
"recommendations": []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
def create_evaluator(ftype, key):
|
|
133
|
+
if ftype == "python":
|
|
134
|
+
eval_logger.info("Using PythonEvaluator for file type: %s", ftype)
|
|
135
|
+
return PythonEvaluator(key)
|
|
136
|
+
elif ftype == "sql":
|
|
137
|
+
eval_logger.info("Using SQLEvaluator for file type: %s", ftype)
|
|
138
|
+
return SQLEvaluator(key)
|
|
139
|
+
elif ftype == "powerbi":
|
|
140
|
+
eval_logger.info("Using PowerBIEvaluator for file type: %s", ftype)
|
|
141
|
+
return PowerBIEvaluator(key)
|
|
142
|
+
elif ftype == "ssis":
|
|
143
|
+
eval_logger.info("Using SSISEvaluator for file type: %s", ftype)
|
|
144
|
+
return SSISEvaluator(key)
|
|
145
|
+
else:
|
|
146
|
+
eval_logger.warning("Unknown file type %s, defaulting to PythonEvaluator", ftype)
|
|
147
|
+
return PythonEvaluator(key)
|
|
148
|
+
|
|
149
|
+
keys_to_try = [api_key] + backup_api_keys[:5]
|
|
150
|
+
|
|
151
|
+
last_exception = None
|
|
152
|
+
for i, key in enumerate(keys_to_try):
|
|
153
|
+
evaluator = create_evaluator(file_type, key)
|
|
154
|
+
try:
|
|
155
|
+
evaluation = evaluator.evaluate(questions, answer_path)
|
|
156
|
+
eval_logger.info(f"Evaluation complete with API key #{i + 1}: Score = {evaluation.get('score')}")
|
|
157
|
+
return {
|
|
158
|
+
"score": evaluation.get("score", 0),
|
|
159
|
+
"feedback": evaluation.get("feedback", "No feedback provided"),
|
|
160
|
+
"issues": evaluation.get("issues", []),
|
|
161
|
+
"recommendations": evaluation.get("recommendations", [])
|
|
162
|
+
}
|
|
163
|
+
except Exception as e:
|
|
164
|
+
error_msg = str(e).lower()
|
|
165
|
+
if (
|
|
166
|
+
"429" in error_msg
|
|
167
|
+
or "rate limit" in error_msg
|
|
168
|
+
or "quota exceeded" in error_msg
|
|
169
|
+
or "daily limit exceeded" in error_msg
|
|
170
|
+
or "quota" in error_msg
|
|
171
|
+
):
|
|
172
|
+
eval_logger.warning(f"API key #{i + 1} limited or quota exceeded. Trying next key if available.")
|
|
173
|
+
last_exception = e
|
|
174
|
+
continue
|
|
175
|
+
else:
|
|
176
|
+
eval_logger.error(f"Evaluation failed with API key #{i + 1}: %s", str(e))
|
|
177
|
+
return {
|
|
178
|
+
"score": 0,
|
|
179
|
+
"feedback": f"Evaluation failed: {str(e)}",
|
|
180
|
+
"issues": [str(e)],
|
|
181
|
+
"recommendations": []
|
|
182
|
+
}
|
|
183
|
+
else:
|
|
184
|
+
eval_logger.error("All API keys exhausted and evaluation failed.")
|
|
185
|
+
return {
|
|
186
|
+
"score": 0,
|
|
187
|
+
"feedback": f"All API keys exhausted: {str(last_exception) if last_exception else 'Unknown error'}",
|
|
188
|
+
}
|