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.
Files changed (86) hide show
  1. pylab/__init__.py +43 -0
  2. pylab/cli.py +282 -0
  3. pylab/data/Data.csv +11 -0
  4. pylab/data/alphabet_stock_data.csv +143 -0
  5. pylab/data/breast-cancer.csv +351 -0
  6. pylab/data/dirtydata.csv +33 -0
  7. pylab/data/groceries.csv +359 -0
  8. pylab/data/iris.csv +151 -0
  9. pylab/data/rainfall.csv +151 -0
  10. pylab/data/students.csv +7 -0
  11. pylab/data/winequality-red.csv +1601 -0
  12. pylab/data/winequality-white.csv +2401 -0
  13. pylab/datasets.py +277 -0
  14. pylab/environment.py +136 -0
  15. pylab/errors.py +53 -0
  16. pylab/generator.py +216 -0
  17. pylab/registry.py +562 -0
  18. pylab/templates/q1/variable_validator.py +43 -0
  19. pylab/templates/q10/password_strength.py +42 -0
  20. pylab/templates/q11/recursion_math.py +28 -0
  21. pylab/templates/q12/main_analyzer.py +46 -0
  22. pylab/templates/q12/text_analyzer.py +16 -0
  23. pylab/templates/q12/text_preprocessor.py +15 -0
  24. pylab/templates/q13/student.py +26 -0
  25. pylab/templates/q14/circle.py +26 -0
  26. pylab/templates/q15/employee_salary.py +46 -0
  27. pylab/templates/q16/smart_calculator.py +51 -0
  28. pylab/templates/q17/complex_number.py +43 -0
  29. pylab/templates/q18/vehicle_inheritance.py +43 -0
  30. pylab/templates/q19/file_word_counter.py +48 -0
  31. pylab/templates/q19/sample.txt +6 -0
  32. pylab/templates/q2/single_number.py +26 -0
  33. pylab/templates/q20/age_validator.py +27 -0
  34. pylab/templates/q21/transaction_manager.py +45 -0
  35. pylab/templates/q22/date_formatter.py +30 -0
  36. pylab/templates/q23/rainfall_analyzer.py +73 -0
  37. pylab/templates/q24/student_marks.py +58 -0
  38. pylab/templates/q25/library_manager.py +182 -0
  39. pylab/templates/q26/mysql_employee_manager.py +239 -0
  40. pylab/templates/q27/crud_client.py +98 -0
  41. pylab/templates/q27/mock_server.py +85 -0
  42. pylab/templates/q28/index.html +33 -0
  43. pylab/templates/q28/login_handler.py +31 -0
  44. pylab/templates/q28/server.py +44 -0
  45. pylab/templates/q29/index.html +50 -0
  46. pylab/templates/q29/register_handler.py +42 -0
  47. pylab/templates/q29/server.py +42 -0
  48. pylab/templates/q3/string_operations.py +32 -0
  49. pylab/templates/q30/crud_handler.py +60 -0
  50. pylab/templates/q30/database.py +66 -0
  51. pylab/templates/q30/index.html +59 -0
  52. pylab/templates/q30/server.py +51 -0
  53. pylab/templates/q31/numpy_matrix.py +40 -0
  54. pylab/templates/q32/numpy_slicing.py +43 -0
  55. pylab/templates/q33/pandas_students.py +41 -0
  56. pylab/templates/q34/pandas_salary.py +37 -0
  57. pylab/templates/q35/sales_barchart.py +58 -0
  58. pylab/templates/q36/weather_plot.py +50 -0
  59. pylab/templates/q37/age_categorization.py +38 -0
  60. pylab/templates/q38/concat_rows.py +38 -0
  61. pylab/templates/q39/concat_cols.py +38 -0
  62. pylab/templates/q4/menu_string_processor.py +68 -0
  63. pylab/templates/q40/concat_merge.py +44 -0
  64. pylab/templates/q41/pandas_joins.py +46 -0
  65. pylab/templates/q42/groupby_school.py +32 -0
  66. pylab/templates/q43/groupby_multi.py +29 -0
  67. pylab/templates/q44/alphabet_stock_viz.py +95 -0
  68. pylab/templates/q45/multi_plot.py +65 -0
  69. pylab/templates/q46/dataframe_barchart.py +60 -0
  70. pylab/templates/q47/preprocess_data.py +54 -0
  71. pylab/templates/q48/clean_dirtydata.py +62 -0
  72. pylab/templates/q49/wine_regression.py +169 -0
  73. pylab/templates/q5/phonebook.py +105 -0
  74. pylab/templates/q50/breast_cancer_classifier.py +147 -0
  75. pylab/templates/q51/iris_clustering.py +95 -0
  76. pylab/templates/q52/apriori_groceries.py +88 -0
  77. pylab/templates/q6/calculator.py +67 -0
  78. pylab/templates/q7/session_attendees.py +37 -0
  79. pylab/templates/q8/number_properties.py +68 -0
  80. pylab/templates/q9/greet_people.py +26 -0
  81. python_lab_toolkit-1.0.0.dist-info/LICENSE +21 -0
  82. python_lab_toolkit-1.0.0.dist-info/METADATA +427 -0
  83. python_lab_toolkit-1.0.0.dist-info/RECORD +86 -0
  84. python_lab_toolkit-1.0.0.dist-info/WHEEL +5 -0
  85. python_lab_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
  86. 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