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.
Files changed (26) hide show
  1. quantumchecker-0.2.8/PKG-INFO +138 -0
  2. quantumchecker-0.2.8/QuantumCheck/main.py +188 -0
  3. {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumCheck/powerbi_evaluator.py +91 -173
  4. quantumchecker-0.2.8/QuantumCheck/prompts.py +185 -0
  5. quantumchecker-0.2.8/QuantumCheck/python_evaluator.py +194 -0
  6. quantumchecker-0.2.8/QuantumCheck/sql_evaluator.py +196 -0
  7. quantumchecker-0.2.8/QuantumCheck/ssis_evaluator.py +292 -0
  8. quantumchecker-0.2.8/QuantumChecker.egg-info/PKG-INFO +138 -0
  9. {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumChecker.egg-info/requires.txt +1 -0
  10. quantumchecker-0.2.8/README.md +112 -0
  11. {quantumchecker-0.2.6 → quantumchecker-0.2.8}/setup.py +3 -2
  12. quantumchecker-0.2.8/tests/test.py +388 -0
  13. quantumchecker-0.2.6/PKG-INFO +0 -33
  14. quantumchecker-0.2.6/QuantumCheck/main.py +0 -125
  15. quantumchecker-0.2.6/QuantumCheck/prompts.py +0 -230
  16. quantumchecker-0.2.6/QuantumCheck/python_evaluator.py +0 -95
  17. quantumchecker-0.2.6/QuantumCheck/sql_evaluator.py +0 -97
  18. quantumchecker-0.2.6/QuantumCheck/ssis_evaluator.py +0 -136
  19. quantumchecker-0.2.6/QuantumChecker.egg-info/PKG-INFO +0 -33
  20. quantumchecker-0.2.6/README.md +0 -8
  21. quantumchecker-0.2.6/tests/test.py +0 -29
  22. {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumCheck/__init__.py +0 -0
  23. {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumChecker.egg-info/SOURCES.txt +0 -0
  24. {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumChecker.egg-info/dependency_links.txt +0 -0
  25. {quantumchecker-0.2.6 → quantumchecker-0.2.8}/QuantumChecker.egg-info/top_level.txt +0 -0
  26. {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
+ }