iotlabkit 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.
iotlabkit/doctor.py ADDED
@@ -0,0 +1,160 @@
1
+ """System diagnostic tool for verifying Python environment, dependencies, and configuration.
2
+
3
+ Ensures beginner students can pinpoint missing packages, tools, or configuration errors.
4
+ Follows strict security practices: NEVER prints sensitive credential values.
5
+ """
6
+
7
+ import os
8
+ import platform
9
+ import shutil
10
+ import sys
11
+ from typing import Dict, List, Tuple
12
+
13
+
14
+ def check_python_version() -> Tuple[bool, str]:
15
+ """Verifies that the Python version is at least 3.9."""
16
+ major, minor, micro = sys.version_info[:3]
17
+ version_str = f"{major}.{minor}.{micro}"
18
+ if sys.version_info >= (3, 9):
19
+ return True, f"Python {version_str} ({sys.executable})"
20
+ return (
21
+ False,
22
+ f"Python {version_str} is below required minimum 3.9. Please upgrade to Python 3.10+.",
23
+ )
24
+
25
+
26
+ def check_tool(binary_name: str) -> Tuple[bool, str]:
27
+ """Checks whether a system binary/executable is in PATH."""
28
+ path = shutil.which(binary_name)
29
+ if path:
30
+ return True, f"Found at {path}"
31
+ return False, "Not found in PATH (optional)"
32
+
33
+
34
+ def check_python_package(package_name: str, import_name: str = None) -> Tuple[bool, str]:
35
+ """Checks whether a Python package is importable and retrieves its version."""
36
+ import_target = import_name or package_name
37
+ try:
38
+ mod = __import__(import_target)
39
+ version = getattr(mod, "__version__", "installed")
40
+ return True, f"v{version}"
41
+ except ImportError:
42
+ return False, "Not installed"
43
+
44
+
45
+ def check_env_var_safe(var_name: str) -> Tuple[bool, str]:
46
+ """Safely checks if an environment variable is set without leaking secrets."""
47
+ val = os.environ.get(var_name)
48
+ if not val:
49
+ return False, "[Not Set]"
50
+ masked = f"[Configured: {len(val)} chars, ends with '...{val[-3:]}']" if len(val) >= 4 else "[Configured]"
51
+ return True, masked
52
+
53
+
54
+ def run_doctor() -> int:
55
+ """Executes the full diagnostic suite and prints beginner-friendly reports."""
56
+ print("=" * 70)
57
+ print(" iotlabkit Doctor - Environment Diagnostics")
58
+ print("=" * 70)
59
+
60
+ all_passed = True
61
+
62
+ # 1. Python Environment
63
+ print("\n[1] Python Runtime:")
64
+ py_ok, py_msg = check_python_version()
65
+ status_icon = "✓" if py_ok else "✗"
66
+ print(f" {status_icon} Python Version : {py_msg}")
67
+ print(f" Platform : {platform.system()} {platform.release()} ({platform.machine()})")
68
+ if not py_ok:
69
+ all_passed = False
70
+
71
+ # 2. Command Line Tools
72
+ print("\n[2] System Binaries:")
73
+ tools = [
74
+ ("git", "Version control & repository cloning"),
75
+ ("curl", "HTTP request testing tool"),
76
+ ("mosquitto", "Eclipse Mosquitto local MQTT broker (Prog 9)"),
77
+ ("mosquitto_sub", "Mosquitto CLI subscriber tool"),
78
+ ]
79
+ for binary, desc in tools:
80
+ found, msg = check_tool(binary)
81
+ icon = "✓" if found else "!"
82
+ print(f" {icon} {binary:<16}: {msg} - {desc}")
83
+
84
+ # 3. Layer Dependencies
85
+ print("\n[3] Python Packages by Layer:")
86
+
87
+ package_groups = {
88
+ "Core & Utilities": [
89
+ ("requests", "requests"),
90
+ ("python-dotenv", "dotenv"),
91
+ ("flask", "flask"),
92
+ ],
93
+ "IoT & Protocols": [
94
+ ("paho-mqtt", "paho.mqtt"),
95
+ ],
96
+ "Data Analytics": [
97
+ ("pandas", "pandas"),
98
+ ("matplotlib", "matplotlib"),
99
+ ],
100
+ "Machine Learning": [
101
+ ("scikit-learn", "sklearn"),
102
+ ("joblib", "joblib"),
103
+ ],
104
+ "Dashboards": [
105
+ ("streamlit", "streamlit"),
106
+ ],
107
+ }
108
+
109
+ missing_packages = []
110
+
111
+ for group_name, pkgs in package_groups.items():
112
+ print(f" -- {group_name} --")
113
+ for pkg_name, imp_name in pkgs:
114
+ installed, ver_info = check_python_package(pkg_name, imp_name)
115
+ icon = "✓" if installed else "✗"
116
+ print(f" {icon} {pkg_name:<16}: {ver_info}")
117
+ if not installed:
118
+ missing_packages.append(pkg_name)
119
+
120
+ # 4. Safe Cloud Configuration Check
121
+ print("\n[4] Cloud Credentials & Config (Safe Check - No Secrets Revealed):")
122
+ env_vars = [
123
+ ("SUPABASE_URL", "Supabase project URL (Prog 5, 6, 10)"),
124
+ ("SUPABASE_KEY", "Supabase anon/public API key (Prog 5, 6, 10)"),
125
+ ("FIREBASE_DATABASE_URL", "Firebase Realtime DB URL (Prog 5, 6, 10)"),
126
+ ("THINGSBOARD_ACCESS_TOKEN", "ThingsBoard Device Token (Prog 7)"),
127
+ ("THINGSBOARD_BROKER", "ThingsBoard Broker host (Prog 7)"),
128
+ ("MQTT_BROKER_HOST", "Custom MQTT broker host (Prog 6, 9)"),
129
+ ]
130
+ for var_name, desc in env_vars:
131
+ is_set, safe_info = check_env_var_safe(var_name)
132
+ icon = "✓" if is_set else "-"
133
+ print(f" {icon} {var_name:<26}: {safe_info} ({desc})")
134
+
135
+ # 5. Diagnostic Summary
136
+ print("\n" + "=" * 70)
137
+ print(" Summary & Recommendations")
138
+ print("=" * 70)
139
+ if missing_packages:
140
+ print(f"\n! Note: {len(missing_packages)} optional packages are currently uninstalled.")
141
+ print(" To install packages for specific practicals, you can run:")
142
+ print(" pip install iotlabkit[analytics] # For Data Analytics practicals (11-14)")
143
+ print(" pip install iotlabkit[ml] # For Machine Learning practicals (15-17)")
144
+ print(" pip install iotlabkit[iot] # For Communication practicals (5-7, 9-10)")
145
+ print(" pip install iotlabkit[dashboard] # For Streamlit dashboards")
146
+ print(" pip install iotlabkit[all] # To install all dependencies at once")
147
+ else:
148
+ print("\n✓ All Python packages for all 13 practicals are installed and ready!")
149
+
150
+ print(
151
+ "\nReminder: Programs 8 (CoAP) runs with zero external packages (pure standard library).\n"
152
+ "Cloud credentials only need to be configured in generated `.env` files for Programs 5, 6, 7, and 10."
153
+ )
154
+ print("=" * 70)
155
+
156
+ return 0 if py_ok else 1
157
+
158
+
159
+ if __name__ == "__main__":
160
+ sys.exit(run_doctor())
iotlabkit/generator.py ADDED
@@ -0,0 +1,110 @@
1
+ """Project generator for IoT Laboratory practicals.
2
+
3
+ Generates self-contained, runnable practical directories with requirements.txt,
4
+ README.md, .env.example, source files, and dataset loaders.
5
+ Supports two execution modes:
6
+ - 'learning': Rich comments, step-by-step guides, architecture diagrams, viva Q&A.
7
+ - 'exam': Clean, concise, minimal comments, essential commands, viva Q&A.
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ from typing import Dict, List, Optional
13
+ from iotlabkit.registry import get_program, ProgramInfo
14
+
15
+
16
+ def prompt_overwrite_confirmation(directory: str) -> bool:
17
+ """Prompts the user before overwriting existing files in a directory."""
18
+ if not os.path.exists(directory):
19
+ return True
20
+ existing_files = os.listdir(directory)
21
+ if not existing_files:
22
+ return True
23
+
24
+ print(f"\n⚠️ Warning: Target directory '{directory}' already exists and contains {len(existing_files)} file(s):")
25
+ for f in existing_files[:5]:
26
+ print(f" - {f}")
27
+ if len(existing_files) > 5:
28
+ print(f" ... and {len(existing_files) - 5} more.")
29
+
30
+ try:
31
+ reply = input("\nDo you want to overwrite existing files? [y/N]: ").strip().lower()
32
+ return reply in ("y", "yes")
33
+ except (EOFError, KeyboardInterrupt):
34
+ return False
35
+
36
+
37
+ def generate_program(
38
+ program_id: int,
39
+ output_dir: Optional[str] = None,
40
+ mode: str = "learning",
41
+ force: bool = False,
42
+ interactive: bool = True,
43
+ ) -> str:
44
+ """Generates a complete project directory for the requested practical.
45
+
46
+ Args:
47
+ program_id: Practical program number (5-17).
48
+ output_dir: Target directory path (defaults to 'labXX').
49
+ mode: Presentation mode ('learning' or 'exam').
50
+ force: If True, overwrites existing directory without prompt.
51
+ interactive: If True, prompts user before overwriting.
52
+
53
+ Returns:
54
+ The absolute path to the generated project directory.
55
+ """
56
+ prog_info: ProgramInfo = get_program(program_id)
57
+
58
+ valid_modes = ("learning", "exam")
59
+ clean_mode = mode.lower().strip()
60
+ if clean_mode not in valid_modes:
61
+ raise ValueError(
62
+ f"Invalid mode '{mode}'. Supported modes are: {', '.join(valid_modes)}"
63
+ )
64
+
65
+ target_dir = output_dir or f"lab{prog_info.code}"
66
+ target_abs = os.path.abspath(target_dir)
67
+
68
+ # Check for existing files
69
+ if not force and interactive:
70
+ if not prompt_overwrite_confirmation(target_dir):
71
+ print(f"Generation cancelled. Directory '{target_dir}' was preserved.")
72
+ return target_abs
73
+
74
+ os.makedirs(target_abs, exist_ok=True)
75
+
76
+ # Retrieve generator function from program template module
77
+ generator_fn = prog_info.get_generator()
78
+ files_to_write: Dict[str, str] = generator_fn(target_abs, clean_mode)
79
+
80
+ print(f"\n🚀 Generating Program {prog_info.id}: {prog_info.title}")
81
+ print(f" Mode: {clean_mode.upper()}")
82
+ print(f" Target Directory: {target_abs}\n")
83
+
84
+ for rel_path, content in files_to_write.items():
85
+ full_path = os.path.join(target_abs, rel_path)
86
+ parent_dir = os.path.dirname(full_path)
87
+ if parent_dir:
88
+ os.makedirs(parent_dir, exist_ok=True)
89
+
90
+ with open(full_path, "w", encoding="utf-8") as f:
91
+ f.write(content)
92
+ print(f" ✓ Created: {rel_path}")
93
+
94
+ print("\n" + "=" * 70)
95
+ print(f"🎉 Project successfully generated in '{target_dir}'!")
96
+ print("=" * 70)
97
+ print("\nNext steps to run this practical:")
98
+ print(f" 1. Navigate to the project directory:")
99
+ print(f" cd {target_dir}")
100
+ print(f" 2. Install dependencies (recommended in a virtual environment):")
101
+ print(f" pip install -r requirements.txt")
102
+ if prog_info.requires_credentials:
103
+ print(f" 3. Configure environment variables:")
104
+ print(f" cp .env.example .env")
105
+ print(f" (Open .env in any editor and enter your credentials)")
106
+ print(f" 4. Follow instructions in '{target_dir}/README.md':")
107
+ print(f" {prog_info.setup_steps[0] if prog_info.setup_steps else 'python main.py'}")
108
+ print("=" * 70 + "\n")
109
+
110
+ return target_abs