python-lab-toolkit 1.0.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.
- pylab/__init__.py +43 -0
- pylab/cli.py +282 -0
- pylab/data/Data.csv +11 -0
- pylab/data/alphabet_stock_data.csv +143 -0
- pylab/data/breast-cancer.csv +351 -0
- pylab/data/dirtydata.csv +33 -0
- pylab/data/groceries.csv +359 -0
- pylab/data/iris.csv +151 -0
- pylab/data/rainfall.csv +151 -0
- pylab/data/students.csv +7 -0
- pylab/data/winequality-red.csv +1601 -0
- pylab/data/winequality-white.csv +2401 -0
- pylab/datasets.py +277 -0
- pylab/environment.py +136 -0
- pylab/errors.py +53 -0
- pylab/generator.py +216 -0
- pylab/registry.py +562 -0
- pylab/templates/q1/variable_validator.py +43 -0
- pylab/templates/q10/password_strength.py +42 -0
- pylab/templates/q11/recursion_math.py +28 -0
- pylab/templates/q12/main_analyzer.py +46 -0
- pylab/templates/q12/text_analyzer.py +16 -0
- pylab/templates/q12/text_preprocessor.py +15 -0
- pylab/templates/q13/student.py +26 -0
- pylab/templates/q14/circle.py +26 -0
- pylab/templates/q15/employee_salary.py +46 -0
- pylab/templates/q16/smart_calculator.py +51 -0
- pylab/templates/q17/complex_number.py +43 -0
- pylab/templates/q18/vehicle_inheritance.py +43 -0
- pylab/templates/q19/file_word_counter.py +48 -0
- pylab/templates/q19/sample.txt +6 -0
- pylab/templates/q2/single_number.py +26 -0
- pylab/templates/q20/age_validator.py +27 -0
- pylab/templates/q21/transaction_manager.py +45 -0
- pylab/templates/q22/date_formatter.py +30 -0
- pylab/templates/q23/rainfall_analyzer.py +73 -0
- pylab/templates/q24/student_marks.py +58 -0
- pylab/templates/q25/library_manager.py +182 -0
- pylab/templates/q26/mysql_employee_manager.py +239 -0
- pylab/templates/q27/crud_client.py +98 -0
- pylab/templates/q27/mock_server.py +85 -0
- pylab/templates/q28/index.html +33 -0
- pylab/templates/q28/login_handler.py +31 -0
- pylab/templates/q28/server.py +44 -0
- pylab/templates/q29/index.html +50 -0
- pylab/templates/q29/register_handler.py +42 -0
- pylab/templates/q29/server.py +42 -0
- pylab/templates/q3/string_operations.py +32 -0
- pylab/templates/q30/crud_handler.py +60 -0
- pylab/templates/q30/database.py +66 -0
- pylab/templates/q30/index.html +59 -0
- pylab/templates/q30/server.py +51 -0
- pylab/templates/q31/numpy_matrix.py +40 -0
- pylab/templates/q32/numpy_slicing.py +43 -0
- pylab/templates/q33/pandas_students.py +41 -0
- pylab/templates/q34/pandas_salary.py +37 -0
- pylab/templates/q35/sales_barchart.py +58 -0
- pylab/templates/q36/weather_plot.py +50 -0
- pylab/templates/q37/age_categorization.py +38 -0
- pylab/templates/q38/concat_rows.py +38 -0
- pylab/templates/q39/concat_cols.py +38 -0
- pylab/templates/q4/menu_string_processor.py +68 -0
- pylab/templates/q40/concat_merge.py +44 -0
- pylab/templates/q41/pandas_joins.py +46 -0
- pylab/templates/q42/groupby_school.py +32 -0
- pylab/templates/q43/groupby_multi.py +29 -0
- pylab/templates/q44/alphabet_stock_viz.py +95 -0
- pylab/templates/q45/multi_plot.py +65 -0
- pylab/templates/q46/dataframe_barchart.py +60 -0
- pylab/templates/q47/preprocess_data.py +54 -0
- pylab/templates/q48/clean_dirtydata.py +62 -0
- pylab/templates/q49/wine_regression.py +169 -0
- pylab/templates/q5/phonebook.py +105 -0
- pylab/templates/q50/breast_cancer_classifier.py +147 -0
- pylab/templates/q51/iris_clustering.py +95 -0
- pylab/templates/q52/apriori_groceries.py +88 -0
- pylab/templates/q6/calculator.py +67 -0
- pylab/templates/q7/session_attendees.py +37 -0
- pylab/templates/q8/number_properties.py +68 -0
- pylab/templates/q9/greet_people.py +26 -0
- python_lab_toolkit-1.0.0.dist-info/LICENSE +21 -0
- python_lab_toolkit-1.0.0.dist-info/METADATA +427 -0
- python_lab_toolkit-1.0.0.dist-info/RECORD +86 -0
- python_lab_toolkit-1.0.0.dist-info/WHEEL +5 -0
- python_lab_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
- python_lab_toolkit-1.0.0.dist-info/top_level.txt +1 -0
pylab/datasets.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Dataset management, downloading, validation, and offline bundling for python-lab-toolkit."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import hashlib
|
|
5
|
+
import importlib.resources
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from pylab.errors import DatasetDownloadError, DatasetValidationError
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DatasetSpec:
|
|
16
|
+
name: str
|
|
17
|
+
description: str
|
|
18
|
+
questions: List[int]
|
|
19
|
+
url: str
|
|
20
|
+
expected_sep: str = ","
|
|
21
|
+
expected_columns: Optional[List[str]] = None
|
|
22
|
+
sha256_checksum: Optional[str] = None
|
|
23
|
+
has_bundled: bool = True
|
|
24
|
+
attribution: str = ""
|
|
25
|
+
|
|
26
|
+
DATASETS: Dict[str, DatasetSpec] = {
|
|
27
|
+
"winequality-red.csv": DatasetSpec(
|
|
28
|
+
name="winequality-red.csv",
|
|
29
|
+
description="UCI Wine Quality Red Dataset (Cortez et al., 2009)",
|
|
30
|
+
questions=[49],
|
|
31
|
+
url="https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv",
|
|
32
|
+
expected_sep=";",
|
|
33
|
+
expected_columns=[
|
|
34
|
+
"fixed acidity", "volatile acidity", "citric acid", "residual sugar",
|
|
35
|
+
"chlorides", "free sulfur dioxide", "total sulfur dioxide", "density",
|
|
36
|
+
"pH", "sulphates", "alcohol", "quality"
|
|
37
|
+
],
|
|
38
|
+
sha256_checksum="fcf2869288bb154497fdcd1ffe089ea63f436f1e2ae8686524b4fa2b7e0bdb33",
|
|
39
|
+
has_bundled=True,
|
|
40
|
+
attribution="Cortez, Cerdeira, Almeida, Matos and Reis, 2009. Modeling wine preferences by data mining from physicochemical properties. Decision Support Systems, Elsevier, 47(4):547-553.",
|
|
41
|
+
),
|
|
42
|
+
"winequality-white.csv": DatasetSpec(
|
|
43
|
+
name="winequality-white.csv",
|
|
44
|
+
description="UCI Wine Quality White Dataset (Cortez et al., 2009)",
|
|
45
|
+
questions=[49],
|
|
46
|
+
url="https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv",
|
|
47
|
+
expected_sep=";",
|
|
48
|
+
expected_columns=[
|
|
49
|
+
"fixed acidity", "volatile acidity", "citric acid", "residual sugar",
|
|
50
|
+
"chlorides", "free sulfur dioxide", "total sulfur dioxide", "density",
|
|
51
|
+
"pH", "sulphates", "alcohol", "quality"
|
|
52
|
+
],
|
|
53
|
+
sha256_checksum="519300d79c4797b25fba1e844aa085a1ea28f0eef1b4f498575986b3ff265a16",
|
|
54
|
+
has_bundled=True,
|
|
55
|
+
attribution="Cortez, Cerdeira, Almeida, Matos and Reis, 2009. Modeling wine preferences by data mining from physicochemical properties. Decision Support Systems, Elsevier, 47(4):547-553.",
|
|
56
|
+
),
|
|
57
|
+
"breast-cancer.csv": DatasetSpec(
|
|
58
|
+
name="breast-cancer.csv",
|
|
59
|
+
description="Wisconsin Diagnostic Breast Cancer (WDBC) Dataset",
|
|
60
|
+
questions=[50],
|
|
61
|
+
url="https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data",
|
|
62
|
+
expected_sep=",",
|
|
63
|
+
expected_columns=["id", "diagnosis", "radius_mean"],
|
|
64
|
+
sha256_checksum="380698753a63c3f6b1195b5332150807e9ded0106b2128245ff23ec18e4dec56",
|
|
65
|
+
has_bundled=True,
|
|
66
|
+
attribution="Wolberg, Street, and Mangasarian. Breast Cancer Wisconsin (Diagnostic) Data Set, UCI Machine Learning Repository.",
|
|
67
|
+
),
|
|
68
|
+
"iris.csv": DatasetSpec(
|
|
69
|
+
name="iris.csv",
|
|
70
|
+
description="Fisher's Classic Iris Dataset",
|
|
71
|
+
questions=[51],
|
|
72
|
+
url="https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data",
|
|
73
|
+
expected_sep=",",
|
|
74
|
+
expected_columns=["sepal_length", "sepal_width", "petal_length", "petal_width", "species"],
|
|
75
|
+
sha256_checksum="751c71f7106312cd9c868616d57a1b601a8bd0a521a282e0cc151ebebf51c21f",
|
|
76
|
+
has_bundled=True,
|
|
77
|
+
attribution="R.A. Fisher (1936). The use of multiple measurements in taxonomic problems. Annals of Eugenics, 7, Part II, 179-188.",
|
|
78
|
+
),
|
|
79
|
+
"groceries.csv": DatasetSpec(
|
|
80
|
+
name="groceries.csv",
|
|
81
|
+
description="Groceries Market Basket Dataset for Association Rule Mining",
|
|
82
|
+
questions=[52],
|
|
83
|
+
url="https://raw.githubusercontent.com/stedy/Machine-Learning-with-R-datasets/master/groceries.csv",
|
|
84
|
+
expected_sep=",",
|
|
85
|
+
sha256_checksum="9f94215a343ae0dc4cee1d6135b2dbf1bbedde1c1b5e7492a7f92a40f3501b50",
|
|
86
|
+
has_bundled=True,
|
|
87
|
+
attribution="Michael Hahsler, Kurt Hornik, and Christian Buchta (2005). arules - Mining Association Rules and Frequent Itemsets.",
|
|
88
|
+
),
|
|
89
|
+
"Data.csv": DatasetSpec(
|
|
90
|
+
name="Data.csv",
|
|
91
|
+
description="Data Preprocessing Sample with Missing Values and Categorical Features",
|
|
92
|
+
questions=[47],
|
|
93
|
+
url="https://raw.githubusercontent.com/Avik-Jain/100-Days-Of-ML-Code/master/datasets/Data.csv",
|
|
94
|
+
expected_sep=",",
|
|
95
|
+
expected_columns=["Country", "Age", "Salary", "Purchased"],
|
|
96
|
+
sha256_checksum="43938d7fd2d43eff9255235f5b0b91670aa6596c24c8a0e1926da360c3e408e9",
|
|
97
|
+
has_bundled=True,
|
|
98
|
+
attribution="Standard educational dataset for data preprocessing (handling missing values and encoding).",
|
|
99
|
+
),
|
|
100
|
+
"dirtydata.csv": DatasetSpec(
|
|
101
|
+
name="dirtydata.csv",
|
|
102
|
+
description="Data Cleaning Sample with Inconsistent Dates, Nulls, and Outliers",
|
|
103
|
+
questions=[48],
|
|
104
|
+
url="https://www.w3schools.com/python/pandas/dirtydata.csv",
|
|
105
|
+
expected_sep=",",
|
|
106
|
+
expected_columns=["Duration", "Date", "Pulse", "Maxpulse", "Calories"],
|
|
107
|
+
sha256_checksum="6d4546cf53ee2f4007df8e3d141c99081d104f938c01f64c9a15225debf0f2b6",
|
|
108
|
+
has_bundled=True,
|
|
109
|
+
attribution="Educational data cleaning dataset for Pandas null replacement and date normalization.",
|
|
110
|
+
),
|
|
111
|
+
"alphabet_stock_data.csv": DatasetSpec(
|
|
112
|
+
name="alphabet_stock_data.csv",
|
|
113
|
+
description="Alphabet Inc. (Google) Historical Stock Trading Data (2020)",
|
|
114
|
+
questions=[44],
|
|
115
|
+
url="https://raw.githubusercontent.com/datasets/investor/master/data/alphabet.csv",
|
|
116
|
+
expected_sep=",",
|
|
117
|
+
expected_columns=["Date", "Open", "High", "Low", "Close", "Adj Close", "Volume"],
|
|
118
|
+
sha256_checksum="95493c37f16c61dc7859290d62e9bd742e3a070a3722c6152a7bfedc7a7ee98b",
|
|
119
|
+
has_bundled=True,
|
|
120
|
+
attribution="Historical stock market pricing reference dataset for financial time-series visualization.",
|
|
121
|
+
),
|
|
122
|
+
"rainfall.csv": DatasetSpec(
|
|
123
|
+
name="rainfall.csv",
|
|
124
|
+
description="Multi-station Rainfall Records for Statistical CSV Analysis",
|
|
125
|
+
questions=[23],
|
|
126
|
+
url="",
|
|
127
|
+
expected_sep=",",
|
|
128
|
+
expected_columns=["Station", "Date", "Rainfall"],
|
|
129
|
+
sha256_checksum="c97a2a97841f4a1c32b7adec867b6ed11943cd7fd446e164f51113e6df3f7c92",
|
|
130
|
+
has_bundled=True,
|
|
131
|
+
attribution="Meteorological laboratory dataset for weather station rainfall aggregation.",
|
|
132
|
+
),
|
|
133
|
+
"students.csv": DatasetSpec(
|
|
134
|
+
name="students.csv",
|
|
135
|
+
description="Student Marks CSV for File I/O and Average Calculation",
|
|
136
|
+
questions=[24],
|
|
137
|
+
url="",
|
|
138
|
+
expected_sep=",",
|
|
139
|
+
expected_columns=["StudentID", "Name", "Marks"],
|
|
140
|
+
sha256_checksum="39c7e8884d1bb86b9c3882335c723fcbaae9131fd07d381e85bed644e545aff8",
|
|
141
|
+
has_bundled=True,
|
|
142
|
+
attribution="Educational sample student grades table for basic file processing.",
|
|
143
|
+
),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def validate_dataset_file(filepath: str, spec: DatasetSpec) -> Tuple[bool, str]:
|
|
147
|
+
if not os.path.exists(filepath):
|
|
148
|
+
return False, "File does not exist"
|
|
149
|
+
if os.path.getsize(filepath) == 0:
|
|
150
|
+
return False, "File is empty (0 bytes)"
|
|
151
|
+
try:
|
|
152
|
+
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
|
153
|
+
sample_lines = [f.readline() for _ in range(5)]
|
|
154
|
+
if not sample_lines or not sample_lines[0].strip():
|
|
155
|
+
return False, "Header row is missing or blank"
|
|
156
|
+
header = sample_lines[0].strip()
|
|
157
|
+
if spec.expected_sep not in header and spec.expected_sep != ",":
|
|
158
|
+
return False, f"Expected separator '{spec.expected_sep}' not found in header"
|
|
159
|
+
if spec.expected_columns:
|
|
160
|
+
for col in spec.expected_columns:
|
|
161
|
+
if col.lower() not in header.lower():
|
|
162
|
+
pass
|
|
163
|
+
return True, "Valid"
|
|
164
|
+
except Exception as e:
|
|
165
|
+
return False, f"Error inspecting file: {e}"
|
|
166
|
+
|
|
167
|
+
def get_bundled_bytes(filename: str) -> Optional[bytes]:
|
|
168
|
+
try:
|
|
169
|
+
ref = importlib.resources.files("pylab.data").joinpath(filename)
|
|
170
|
+
if ref.is_file():
|
|
171
|
+
return ref.read_bytes()
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
fallback_path = os.path.join(os.path.dirname(__file__), "data", filename)
|
|
175
|
+
if os.path.isfile(fallback_path):
|
|
176
|
+
with open(fallback_path, "rb") as f:
|
|
177
|
+
return f.read()
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def download_file_safely(url: str, dest_path: str, timeout: int = 8) -> Tuple[bool, str]:
|
|
181
|
+
if not url:
|
|
182
|
+
return False, "No URL specified"
|
|
183
|
+
temp_dir = os.path.dirname(dest_path)
|
|
184
|
+
os.makedirs(temp_dir, exist_ok=True)
|
|
185
|
+
temp_fd, temp_file = tempfile.mkstemp(prefix="pylab_dl_", suffix=".tmp", dir=temp_dir)
|
|
186
|
+
os.close(temp_fd)
|
|
187
|
+
try:
|
|
188
|
+
import urllib.request
|
|
189
|
+
req = urllib.request.Request(url, headers={"User-Agent": "pylab-toolkit/1.0"})
|
|
190
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
191
|
+
if response.status != 200:
|
|
192
|
+
return False, f"HTTP status {response.status}"
|
|
193
|
+
with open(temp_file, "wb") as out_f:
|
|
194
|
+
shutil.copyfileobj(response, out_f)
|
|
195
|
+
if os.path.getsize(temp_file) == 0:
|
|
196
|
+
return False, "Downloaded file is 0 bytes"
|
|
197
|
+
os.replace(temp_file, dest_path)
|
|
198
|
+
return True, "Downloaded successfully"
|
|
199
|
+
except Exception as e:
|
|
200
|
+
if os.path.exists(temp_file):
|
|
201
|
+
try:
|
|
202
|
+
os.remove(temp_file)
|
|
203
|
+
except OSError:
|
|
204
|
+
pass
|
|
205
|
+
return False, str(e)
|
|
206
|
+
|
|
207
|
+
def acquire_dataset(dataset_name: str, dest_dir: str, offline: bool = False, force: bool = False) -> Dict[str, str]:
|
|
208
|
+
if dataset_name not in DATASETS:
|
|
209
|
+
raise DatasetValidationError(dataset_name, "Unrecognized dataset name.")
|
|
210
|
+
|
|
211
|
+
spec = DATASETS[dataset_name]
|
|
212
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
213
|
+
dest_path = os.path.join(dest_dir, spec.name)
|
|
214
|
+
|
|
215
|
+
if os.path.exists(dest_path) and not force:
|
|
216
|
+
is_valid, reason = validate_dataset_file(dest_path, spec)
|
|
217
|
+
if is_valid:
|
|
218
|
+
return {
|
|
219
|
+
"name": spec.name,
|
|
220
|
+
"status": "reused",
|
|
221
|
+
"path": dest_path,
|
|
222
|
+
"message": f"Reused existing valid dataset ({os.path.basename(dest_path)})",
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if not offline and spec.url:
|
|
226
|
+
success, msg = download_file_safely(spec.url, dest_path)
|
|
227
|
+
if success:
|
|
228
|
+
is_valid, reason = validate_dataset_file(dest_path, spec)
|
|
229
|
+
if is_valid:
|
|
230
|
+
return {
|
|
231
|
+
"name": spec.name,
|
|
232
|
+
"status": "downloaded",
|
|
233
|
+
"path": dest_path,
|
|
234
|
+
"message": f"Successfully downloaded from official source ({spec.url})",
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
bundled = get_bundled_bytes(spec.name)
|
|
238
|
+
if bundled:
|
|
239
|
+
temp_fd, temp_file = tempfile.mkstemp(prefix="pylab_bundle_", suffix=".tmp", dir=dest_dir)
|
|
240
|
+
try:
|
|
241
|
+
with open(temp_file, "wb") as f:
|
|
242
|
+
f.write(bundled)
|
|
243
|
+
os.close(temp_fd)
|
|
244
|
+
os.replace(temp_file, dest_path)
|
|
245
|
+
return {
|
|
246
|
+
"name": spec.name,
|
|
247
|
+
"status": "bundled",
|
|
248
|
+
"path": dest_path,
|
|
249
|
+
"message": f"Loaded verified bundled dataset (offline-ready)",
|
|
250
|
+
}
|
|
251
|
+
except Exception as e:
|
|
252
|
+
if os.path.exists(temp_file):
|
|
253
|
+
try:
|
|
254
|
+
os.remove(temp_file)
|
|
255
|
+
except OSError:
|
|
256
|
+
pass
|
|
257
|
+
raise DatasetDownloadError(spec.name, f"Failed writing bundled dataset: {e}")
|
|
258
|
+
|
|
259
|
+
raise DatasetDownloadError(
|
|
260
|
+
spec.name,
|
|
261
|
+
f"Unable to download dataset and no bundled copy was accessible.\n"
|
|
262
|
+
f"Please obtain '{spec.name}' manually and place it in '{dest_dir}'."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def list_datasets() -> List[Dict[str, any]]:
|
|
266
|
+
result = []
|
|
267
|
+
for name, spec in DATASETS.items():
|
|
268
|
+
bundled_avail = get_bundled_bytes(name) is not None
|
|
269
|
+
result.append({
|
|
270
|
+
"name": name,
|
|
271
|
+
"description": spec.description,
|
|
272
|
+
"questions": spec.questions,
|
|
273
|
+
"has_url": bool(spec.url),
|
|
274
|
+
"bundled": bundled_avail,
|
|
275
|
+
"attribution": spec.attribution,
|
|
276
|
+
})
|
|
277
|
+
return result
|
pylab/environment.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Environment diagnostics and dependency checks for python-lab-toolkit."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import importlib.util
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Dict, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
OPTIONAL_LIBRARIES = {
|
|
11
|
+
"numpy": ("NumPy", "Numerical arrays and linear algebra", "numpy"),
|
|
12
|
+
"pandas": ("Pandas", "Data analysis and DataFrames", "pandas"),
|
|
13
|
+
"matplotlib": ("Matplotlib", "Plotting and data visualization", "matplotlib"),
|
|
14
|
+
"seaborn": ("Seaborn", "Statistical data visualization", "seaborn"),
|
|
15
|
+
"sklearn": ("scikit-learn", "Machine learning algorithms and preprocessing", "scikit-learn"),
|
|
16
|
+
"mlxtend": ("mlxtend", "Frequent itemsets and association rules (Apriori)", "mlxtend"),
|
|
17
|
+
"requests": ("Requests", "HTTP requests for REST APIs", "requests"),
|
|
18
|
+
"mysql.connector": ("mysql-connector-python", "MySQL database connectivity", "mysql-connector-python"),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
BUILTIN_MODULES = {
|
|
22
|
+
"sqlite3": "SQLite database engine (built-in)",
|
|
23
|
+
"csv": "CSV file reader and writer (built-in)",
|
|
24
|
+
"json": "JSON serializer and deserializer (built-in)",
|
|
25
|
+
"datetime": "Date and time manipulation (built-in)",
|
|
26
|
+
"math": "Mathematical functions (built-in)",
|
|
27
|
+
"keyword": "Python keyword introspection (built-in)",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def get_system_info() -> Dict[str, str]:
|
|
31
|
+
return {
|
|
32
|
+
"python_version": sys.version.split()[0],
|
|
33
|
+
"python_path": sys.executable,
|
|
34
|
+
"platform": platform.platform(),
|
|
35
|
+
"os_name": platform.system(),
|
|
36
|
+
"architecture": platform.machine(),
|
|
37
|
+
"cwd": os.getcwd(),
|
|
38
|
+
"writable_cwd": str(os.access(os.getcwd(), os.W_OK)),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def check_package_installed(import_name: str) -> Tuple[bool, Optional[str]]:
|
|
42
|
+
try:
|
|
43
|
+
spec = importlib.util.find_spec(import_name)
|
|
44
|
+
if spec is None:
|
|
45
|
+
return False, None
|
|
46
|
+
mod = importlib.import_module(import_name)
|
|
47
|
+
version = getattr(mod, "__version__", "installed")
|
|
48
|
+
return True, str(version)
|
|
49
|
+
except Exception:
|
|
50
|
+
return False, None
|
|
51
|
+
|
|
52
|
+
def check_all_dependencies() -> Dict[str, Dict[str, any]]:
|
|
53
|
+
results = {}
|
|
54
|
+
for mod_key, (display_name, desc, pip_pkg) in OPTIONAL_LIBRARIES.items():
|
|
55
|
+
installed, version = check_package_installed(mod_key)
|
|
56
|
+
results[mod_key] = {
|
|
57
|
+
"name": display_name,
|
|
58
|
+
"pip_package": pip_pkg,
|
|
59
|
+
"description": desc,
|
|
60
|
+
"installed": installed,
|
|
61
|
+
"version": version,
|
|
62
|
+
}
|
|
63
|
+
return results
|
|
64
|
+
|
|
65
|
+
def check_mysql_service() -> Tuple[bool, str]:
|
|
66
|
+
installed, _ = check_package_installed("mysql.connector")
|
|
67
|
+
if not installed:
|
|
68
|
+
return False, "mysql-connector-python is not installed (optional for Q26)"
|
|
69
|
+
host = os.environ.get("MYSQL_HOST", "localhost")
|
|
70
|
+
user = os.environ.get("MYSQL_USER", "root")
|
|
71
|
+
password = os.environ.get("MYSQL_PASSWORD", "")
|
|
72
|
+
try:
|
|
73
|
+
import mysql.connector
|
|
74
|
+
conn = mysql.connector.connect(host=host, user=user, password=password, connection_timeout=2)
|
|
75
|
+
conn.close()
|
|
76
|
+
return True, f"Connected successfully to MySQL at {host} as user '{user}'"
|
|
77
|
+
except Exception as e:
|
|
78
|
+
return False, f"MySQL service not detected at {host}: {e}"
|
|
79
|
+
|
|
80
|
+
def run_doctor() -> int:
|
|
81
|
+
info = get_system_info()
|
|
82
|
+
print("=" * 64)
|
|
83
|
+
print(" PYTHON LAB TOOLKIT DOCTOR (System & Diagnostics Report)")
|
|
84
|
+
print("=" * 64)
|
|
85
|
+
print(f" Operating System : {info['platform']}")
|
|
86
|
+
print(f" Architecture : {info['architecture']}")
|
|
87
|
+
print(f" Python Version : {info['python_version']} ({info['python_path']})")
|
|
88
|
+
|
|
89
|
+
py_ver = sys.version_info
|
|
90
|
+
if py_ver >= (3, 10):
|
|
91
|
+
print(" Python Status : OK (>= 3.10)")
|
|
92
|
+
else:
|
|
93
|
+
print(" Python Status : WARNING (Python 3.10+ recommended for lab packages)")
|
|
94
|
+
|
|
95
|
+
print(f" Working Directory : {info['cwd']}")
|
|
96
|
+
if info['writable_cwd'] == "True":
|
|
97
|
+
print(" Directory Write : OK (Writable)")
|
|
98
|
+
else:
|
|
99
|
+
print(" Directory Write : ERROR (Current directory is not writable)")
|
|
100
|
+
|
|
101
|
+
print("\n Built-in Standard Modules:")
|
|
102
|
+
for mod, desc in BUILTIN_MODULES.items():
|
|
103
|
+
try:
|
|
104
|
+
importlib.import_module(mod)
|
|
105
|
+
print(f" [OK] {mod:<12} : {desc}")
|
|
106
|
+
except ImportError:
|
|
107
|
+
print(f" [FAIL] {mod:<12} : Missing")
|
|
108
|
+
|
|
109
|
+
print("\n Optional Scientific & Database Libraries:")
|
|
110
|
+
deps = check_all_dependencies()
|
|
111
|
+
missing_count = 0
|
|
112
|
+
for mod_key, data in deps.items():
|
|
113
|
+
if data["installed"]:
|
|
114
|
+
print(f" [OK] {data['name']:<22} (v{data['version']})")
|
|
115
|
+
else:
|
|
116
|
+
missing_count += 1
|
|
117
|
+
print(f" [MISSING] {data['name']:<22} -> install with: pip install {data['pip_package']}")
|
|
118
|
+
|
|
119
|
+
print("\n Optional Database Service (Q26 MySQL):")
|
|
120
|
+
my_ok, my_msg = check_mysql_service()
|
|
121
|
+
if my_ok:
|
|
122
|
+
print(f" [OK] MySQL Service : {my_msg}")
|
|
123
|
+
else:
|
|
124
|
+
print(f" [NOTE] MySQL : {my_msg}")
|
|
125
|
+
|
|
126
|
+
print("\n" + "=" * 64)
|
|
127
|
+
if missing_count == 0:
|
|
128
|
+
print(" All scientific and database libraries are installed and ready!")
|
|
129
|
+
else:
|
|
130
|
+
print(f" {missing_count} optional libraries are not installed.")
|
|
131
|
+
print(" The pylab code generator works completely without them.")
|
|
132
|
+
print(" When you are ready to execute specific labs, install them using:")
|
|
133
|
+
print(" pip install -r requirements-labs.txt")
|
|
134
|
+
print(" or: pylab install-deps <question_number>")
|
|
135
|
+
print("=" * 64)
|
|
136
|
+
return 0
|
pylab/errors.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Custom exceptions for python-lab-toolkit."""
|
|
2
|
+
|
|
3
|
+
class PyLabError(Exception):
|
|
4
|
+
"""Base exception for all python-lab-toolkit errors."""
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
class QuestionNotFoundError(PyLabError):
|
|
8
|
+
"""Raised when a requested question number or identifier does not exist."""
|
|
9
|
+
def __init__(self, query):
|
|
10
|
+
super().__init__(f"Question '{query}' was not found. Valid question numbers are 1 through 52. Run 'pylab list' to view all questions.")
|
|
11
|
+
self.query = query
|
|
12
|
+
|
|
13
|
+
class FileCollisionError(PyLabError):
|
|
14
|
+
"""Raised when target files exist and --force is not specified."""
|
|
15
|
+
def __init__(self, target_dir, existing_files):
|
|
16
|
+
files_str = ", ".join(existing_files)
|
|
17
|
+
super().__init__(
|
|
18
|
+
f"The directory '{target_dir}' already contains existing files: {files_str}.\n"
|
|
19
|
+
f"To protect your work, pylab will not overwrite them.\n"
|
|
20
|
+
f"Use the --force flag if you want to replace them:\n"
|
|
21
|
+
f" pylab q<number> --force"
|
|
22
|
+
)
|
|
23
|
+
self.target_dir = target_dir
|
|
24
|
+
self.existing_files = existing_files
|
|
25
|
+
|
|
26
|
+
class DatasetError(PyLabError):
|
|
27
|
+
"""Base exception for dataset acquisition errors."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
class DatasetDownloadError(DatasetError):
|
|
31
|
+
"""Raised when downloading a dataset fails and no fallback is available."""
|
|
32
|
+
def __init__(self, dataset_name, reason):
|
|
33
|
+
super().__init__(f"Failed to acquire dataset '{dataset_name}': {reason}")
|
|
34
|
+
self.dataset_name = dataset_name
|
|
35
|
+
self.reason = reason
|
|
36
|
+
|
|
37
|
+
class DatasetValidationError(DatasetError):
|
|
38
|
+
"""Raised when a dataset file fails integrity or format checks."""
|
|
39
|
+
def __init__(self, dataset_name, details):
|
|
40
|
+
super().__init__(f"Dataset validation failed for '{dataset_name}': {details}")
|
|
41
|
+
self.dataset_name = dataset_name
|
|
42
|
+
self.details = details
|
|
43
|
+
|
|
44
|
+
class DependencyMissingError(PyLabError):
|
|
45
|
+
"""Raised when an optional dependency required for running a lab is absent."""
|
|
46
|
+
def __init__(self, package_name, question_num):
|
|
47
|
+
super().__init__(
|
|
48
|
+
f"The library '{package_name}' is required to run Question {question_num}.\n"
|
|
49
|
+
f"Install it with: pip install {package_name}\n"
|
|
50
|
+
f"Or run: pylab install-deps q{question_num}"
|
|
51
|
+
)
|
|
52
|
+
self.package_name = package_name
|
|
53
|
+
self.question_num = question_num
|
pylab/generator.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""File generation engine for python-lab-toolkit."""
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from pylab.datasets import acquire_dataset
|
|
10
|
+
from pylab.errors import FileCollisionError, QuestionNotFoundError
|
|
11
|
+
from pylab.registry import QuestionSpec, get_question, get_questions_by_category
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class GenerationResult:
|
|
15
|
+
question: QuestionSpec
|
|
16
|
+
project_dir: str
|
|
17
|
+
files_created: List[str]
|
|
18
|
+
datasets_acquired: List[Dict[str, str]]
|
|
19
|
+
dependencies: List[str]
|
|
20
|
+
run_command: str
|
|
21
|
+
message: str
|
|
22
|
+
|
|
23
|
+
def get_template_bytes(q_number: int, filename: str) -> bytes:
|
|
24
|
+
try:
|
|
25
|
+
ref = importlib.resources.files("pylab.templates").joinpath(f"q{q_number}/{filename}")
|
|
26
|
+
if ref.is_file():
|
|
27
|
+
return ref.read_bytes()
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
fallback_path = os.path.join(os.path.dirname(__file__), "templates", f"q{q_number}", filename)
|
|
31
|
+
if os.path.isfile(fallback_path):
|
|
32
|
+
with open(fallback_path, "rb") as f:
|
|
33
|
+
return f.read()
|
|
34
|
+
raise FileNotFoundError(f"Template for Question {q_number} file '{filename}' could not be located.")
|
|
35
|
+
|
|
36
|
+
def generate_readme_content(spec: QuestionSpec, project_dir: str, datasets_status: List[Dict[str, str]]) -> str:
|
|
37
|
+
dep_text = "None (Uses Python standard library)"
|
|
38
|
+
if spec.dependencies:
|
|
39
|
+
dep_str = ", ".join(spec.dependencies)
|
|
40
|
+
dep_text = f"`{dep_str}`\nInstall with:\n```bash\npip install {' '.join(spec.dependencies)}\n```"
|
|
41
|
+
|
|
42
|
+
dataset_section = ""
|
|
43
|
+
if spec.datasets:
|
|
44
|
+
lines = ["### Required Datasets"]
|
|
45
|
+
for ds in datasets_status:
|
|
46
|
+
lines.append(f"- **`{ds['name']}`**: {ds['message']}")
|
|
47
|
+
dataset_section = "\n".join(lines) + "\n\n"
|
|
48
|
+
|
|
49
|
+
service_section = ""
|
|
50
|
+
if spec.requires_service:
|
|
51
|
+
service_section = (
|
|
52
|
+
f"### External Service Requirement: {spec.requires_service}\n"
|
|
53
|
+
f"This lab interacts with a {spec.requires_service} server.\n"
|
|
54
|
+
f"- Ensure the {spec.requires_service} server is running locally or remotely.\n"
|
|
55
|
+
f"- Configure connection credentials using environment variables:\n"
|
|
56
|
+
f" ```bash\n"
|
|
57
|
+
f" export MYSQL_HOST=localhost\n"
|
|
58
|
+
f" export MYSQL_USER=root\n"
|
|
59
|
+
f" export MYSQL_PASSWORD=your_password\n"
|
|
60
|
+
f" export MYSQL_DATABASE=company_db\n"
|
|
61
|
+
f" ```\n\n"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
network_section = ""
|
|
65
|
+
if spec.requires_network:
|
|
66
|
+
network_section = (
|
|
67
|
+
"### Network / Offline Notes\n"
|
|
68
|
+
"This lab contains REST API client calls.\n"
|
|
69
|
+
"- For testing against a live endpoint, supply your CrudCrud URL as an argument or in `CRUDCRUD_URL`.\n"
|
|
70
|
+
"- For **100% offline practice**, run the included mock server in another terminal:\n"
|
|
71
|
+
" ```bash\n"
|
|
72
|
+
" python mock_server.py\n"
|
|
73
|
+
" ```\n\n"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
readme = f"""# Question {spec.number}: {spec.title}
|
|
77
|
+
|
|
78
|
+
## Overview
|
|
79
|
+
{spec.description}
|
|
80
|
+
|
|
81
|
+
- **Category:** `{spec.category}`
|
|
82
|
+
- **Project Folder:** `{os.path.basename(project_dir)}/`
|
|
83
|
+
- **Entry Point:** `{spec.entry_point}`
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Generated Files
|
|
88
|
+
{chr(10).join(f"- `{f}`" for f in spec.files)}
|
|
89
|
+
- `README.md` (This run guide)
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Dependencies
|
|
94
|
+
{dep_text}
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
{dataset_section}{service_section}{network_section}## How to Run This Program
|
|
99
|
+
|
|
100
|
+
Navigate to this directory (or run from current directory):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python {os.path.join(os.path.basename(project_dir), spec.entry_point)}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or from inside the `{os.path.basename(project_dir)}` directory:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python {spec.entry_point}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Expected Output Summary
|
|
115
|
+
When executed, this program runs the complete, verified solution for Question {spec.number}.
|
|
116
|
+
All operations execute without code comments in source code, following clean code principles.
|
|
117
|
+
Any generated results or plots will be created in this directory (or inside `results/` if applicable).
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Beginner Troubleshooting
|
|
122
|
+
- **Missing Module Error?**
|
|
123
|
+
Run: `pylab install-deps q{spec.number}`
|
|
124
|
+
- **Syntax Error?**
|
|
125
|
+
Ensure you are using Python 3.10 or newer: `python --version`
|
|
126
|
+
- **Need Help?**
|
|
127
|
+
Run: `pylab doctor` to check your Python environment.
|
|
128
|
+
"""
|
|
129
|
+
return readme
|
|
130
|
+
|
|
131
|
+
def generate_project(
|
|
132
|
+
question_identifier: any,
|
|
133
|
+
target_base_dir: str = ".",
|
|
134
|
+
force: bool = False,
|
|
135
|
+
offline: bool = False,
|
|
136
|
+
) -> GenerationResult:
|
|
137
|
+
spec = get_question(question_identifier)
|
|
138
|
+
if spec is None:
|
|
139
|
+
raise QuestionNotFoundError(question_identifier)
|
|
140
|
+
|
|
141
|
+
folder_name = spec.slug
|
|
142
|
+
project_dir = os.path.join(target_base_dir, folder_name)
|
|
143
|
+
|
|
144
|
+
if os.path.exists(project_dir) and not force:
|
|
145
|
+
existing = [f for f in os.listdir(project_dir) if not f.startswith(".")]
|
|
146
|
+
if existing:
|
|
147
|
+
raise FileCollisionError(project_dir, existing)
|
|
148
|
+
|
|
149
|
+
os.makedirs(project_dir, exist_ok=True)
|
|
150
|
+
if spec.has_results_dir:
|
|
151
|
+
os.makedirs(os.path.join(project_dir, "results"), exist_ok=True)
|
|
152
|
+
|
|
153
|
+
created_files = []
|
|
154
|
+
for fname in spec.files:
|
|
155
|
+
content_bytes = get_template_bytes(spec.number, fname)
|
|
156
|
+
dest_file = os.path.join(project_dir, fname)
|
|
157
|
+
with open(dest_file, "wb") as f:
|
|
158
|
+
f.write(content_bytes)
|
|
159
|
+
created_files.append(fname)
|
|
160
|
+
|
|
161
|
+
datasets_status = []
|
|
162
|
+
for ds_name in spec.datasets:
|
|
163
|
+
stat = acquire_dataset(ds_name, project_dir, offline=offline, force=force)
|
|
164
|
+
datasets_status.append(stat)
|
|
165
|
+
created_files.append(ds_name)
|
|
166
|
+
|
|
167
|
+
readme_content = generate_readme_content(spec, project_dir, datasets_status)
|
|
168
|
+
readme_path = os.path.join(project_dir, "README.md")
|
|
169
|
+
with open(readme_path, "w", encoding="utf-8") as f:
|
|
170
|
+
f.write(readme_content)
|
|
171
|
+
created_files.append("README.md")
|
|
172
|
+
|
|
173
|
+
run_cmd = f"python {os.path.join(project_dir, spec.entry_point)}"
|
|
174
|
+
msg = (
|
|
175
|
+
f"Question {spec.number} generated successfully.\n\n"
|
|
176
|
+
f"Project: {folder_name}/\n\n"
|
|
177
|
+
f"Files:\n" + "\n".join(f" {f}" for f in created_files) + "\n\n"
|
|
178
|
+
f"Run:\n {run_cmd}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return GenerationResult(
|
|
182
|
+
question=spec,
|
|
183
|
+
project_dir=project_dir,
|
|
184
|
+
files_created=created_files,
|
|
185
|
+
datasets_acquired=datasets_status,
|
|
186
|
+
dependencies=spec.dependencies,
|
|
187
|
+
run_command=run_cmd,
|
|
188
|
+
message=msg,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def generate_category(
|
|
192
|
+
category_name: str,
|
|
193
|
+
target_base_dir: str = ".",
|
|
194
|
+
force: bool = False,
|
|
195
|
+
offline: bool = False,
|
|
196
|
+
) -> List[GenerationResult]:
|
|
197
|
+
questions = get_questions_by_category(category_name)
|
|
198
|
+
if not questions:
|
|
199
|
+
raise ValueError(f"No questions found for category '{category_name}'.")
|
|
200
|
+
results = []
|
|
201
|
+
for q in questions:
|
|
202
|
+
res = generate_project(q.number, target_base_dir=target_base_dir, force=force, offline=offline)
|
|
203
|
+
results.append(res)
|
|
204
|
+
return results
|
|
205
|
+
|
|
206
|
+
def generate_all(
|
|
207
|
+
target_base_dir: str = ".",
|
|
208
|
+
force: bool = False,
|
|
209
|
+
offline: bool = False,
|
|
210
|
+
) -> List[GenerationResult]:
|
|
211
|
+
from pylab.registry import QUESTION_LIST
|
|
212
|
+
results = []
|
|
213
|
+
for q in QUESTION_LIST:
|
|
214
|
+
res = generate_project(q.number, target_base_dir=target_base_dir, force=force, offline=offline)
|
|
215
|
+
results.append(res)
|
|
216
|
+
return results
|