iotlabkit 1.0.0__tar.gz → 1.0.2__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 (52) hide show
  1. {iotlabkit-1.0.0/src/iotlabkit.egg-info → iotlabkit-1.0.2}/PKG-INFO +1 -1
  2. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/pyproject.toml +1 -1
  3. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/__init__.py +1 -1
  4. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/cli.py +12 -12
  5. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/doctor.py +6 -6
  6. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/generator.py +4 -4
  7. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/registry.py +3 -0
  8. iotlabkit-1.0.2/src/iotlabkit/templates/program_05.py +282 -0
  9. iotlabkit-1.0.2/src/iotlabkit/templates/program_06.py +277 -0
  10. iotlabkit-1.0.2/src/iotlabkit/templates/program_07.py +117 -0
  11. iotlabkit-1.0.2/src/iotlabkit/templates/program_08.py +129 -0
  12. iotlabkit-1.0.2/src/iotlabkit/templates/program_09.py +161 -0
  13. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/templates/program_10.py +125 -90
  14. iotlabkit-1.0.2/src/iotlabkit/templates/program_11.py +188 -0
  15. iotlabkit-1.0.2/src/iotlabkit/templates/program_12.py +164 -0
  16. iotlabkit-1.0.2/src/iotlabkit/templates/program_13.py +145 -0
  17. iotlabkit-1.0.2/src/iotlabkit/templates/program_14.py +141 -0
  18. iotlabkit-1.0.2/src/iotlabkit/templates/program_15.py +192 -0
  19. iotlabkit-1.0.2/src/iotlabkit/templates/program_16.py +193 -0
  20. iotlabkit-1.0.2/src/iotlabkit/templates/program_17.py +203 -0
  21. {iotlabkit-1.0.0 → iotlabkit-1.0.2/src/iotlabkit.egg-info}/PKG-INFO +1 -1
  22. iotlabkit-1.0.0/src/iotlabkit/templates/program_05.py +0 -436
  23. iotlabkit-1.0.0/src/iotlabkit/templates/program_06.py +0 -404
  24. iotlabkit-1.0.0/src/iotlabkit/templates/program_07.py +0 -249
  25. iotlabkit-1.0.0/src/iotlabkit/templates/program_08.py +0 -278
  26. iotlabkit-1.0.0/src/iotlabkit/templates/program_09.py +0 -329
  27. iotlabkit-1.0.0/src/iotlabkit/templates/program_11.py +0 -347
  28. iotlabkit-1.0.0/src/iotlabkit/templates/program_12.py +0 -317
  29. iotlabkit-1.0.0/src/iotlabkit/templates/program_13.py +0 -272
  30. iotlabkit-1.0.0/src/iotlabkit/templates/program_14.py +0 -287
  31. iotlabkit-1.0.0/src/iotlabkit/templates/program_15.py +0 -411
  32. iotlabkit-1.0.0/src/iotlabkit/templates/program_16.py +0 -382
  33. iotlabkit-1.0.0/src/iotlabkit/templates/program_17.py +0 -422
  34. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/LICENSE +0 -0
  35. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/README.md +0 -0
  36. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/setup.cfg +0 -0
  37. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/coap_util.py +0 -0
  38. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/data/beijing_air/beijing_sample.csv +0 -0
  39. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/data/india_air/city_day_sample.csv +0 -0
  40. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/data/occupancy/datatraining.txt +0 -0
  41. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/SOURCES.txt +0 -0
  42. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/dependency_links.txt +0 -0
  43. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/entry_points.txt +0 -0
  44. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/requires.txt +0 -0
  45. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/top_level.txt +0 -0
  46. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_api_parsing.py +0 -0
  47. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_cli.py +0 -0
  48. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_coap_util.py +0 -0
  49. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_doctor.py +0 -0
  50. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_generator.py +0 -0
  51. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_ml_pipeline.py +0 -0
  52. {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iotlabkit
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: A Beginner-Friendly IoT Lab Code Generator for MSc/MCA Practicals (Programs 5-17)
5
5
  Author: IoT Lab Contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "iotlabkit"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  description = "A Beginner-Friendly IoT Lab Code Generator for MSc/MCA Practicals (Programs 5-17)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -4,7 +4,7 @@ Designed for MSc/MCA Semester 3 IoT Laboratory practicals (Programs 5-17).
4
4
  Generates clean, working, self-contained project templates in Learning and Exam modes.
5
5
  """
6
6
 
7
- __version__ = "1.0.0"
7
+ __version__ = "1.0.2"
8
8
  __author__ = "IoT Lab Contributors"
9
9
  __license__ = "MIT"
10
10
 
@@ -70,14 +70,14 @@ def cmd_info(args: argparse.Namespace) -> int:
70
70
  program_id = args.program_id
71
71
 
72
72
  if program_id in EXCLUDED_PROGRAM_IDS:
73
- print(f"\n❌ Error: Program {program_id} is outside the supported syllabus scope.")
73
+ print(f"\nError: Program {program_id} is outside the supported syllabus scope.")
74
74
  print(" Only Programs 5 through 17 are included in iotlabkit.\n")
75
75
  return 1
76
76
 
77
77
  try:
78
78
  p = get_program(program_id)
79
79
  except ValueError as err:
80
- print(f"\n❌ Error: {err}\n")
80
+ print(f"\nError: {err}\n")
81
81
  return 1
82
82
 
83
83
  print("\n" + "=" * 75)
@@ -89,11 +89,11 @@ def cmd_info(args: argparse.Namespace) -> int:
89
89
  print(f"Data Source / API: {p.source_info}")
90
90
 
91
91
  print("\nOperational Flags:")
92
- print(f" • Internet Required : {'Yes' if p.requires_internet else 'No'}")
93
- print(f" • Cloud Credentials : {'Yes' if p.requires_credentials else 'No'}")
94
- print(f" • External Broker : {'Yes' if p.requires_broker else 'No'}")
95
- print(f" • Local Dataset : {'Yes' if p.requires_dataset else 'No'}")
96
- print(f" • Local Server : {'Yes' if p.requires_server else 'No'}")
92
+ print(f" - Internet Required : {'Yes' if p.requires_internet else 'No'}")
93
+ print(f" - Cloud Credentials : {'Yes' if p.requires_credentials else 'No'}")
94
+ print(f" - External Broker : {'Yes' if p.requires_broker else 'No'}")
95
+ print(f" - Local Dataset : {'Yes' if p.requires_dataset else 'No'}")
96
+ print(f" - Local Server : {'Yes' if p.requires_server else 'No'}")
97
97
 
98
98
  print("\nRequired Python Dependencies:")
99
99
  if p.dependencies:
@@ -111,7 +111,7 @@ def cmd_info(args: argparse.Namespace) -> int:
111
111
 
112
112
  print("\nGenerated Files:")
113
113
  for gf in p.generated_files:
114
- print(f" • {gf}")
114
+ print(f" - {gf}")
115
115
 
116
116
  if p.viva_preview:
117
117
  print("\nSample Viva Questions & Answers:")
@@ -121,8 +121,8 @@ def cmd_info(args: argparse.Namespace) -> int:
121
121
 
122
122
  print("=" * 75)
123
123
  print(f"To generate this practical:")
124
- print(f" iotlabkit create {p.id} --mode learning # Detailed guide & comments")
125
- print(f" iotlabkit create {p.id} --mode exam # Concise code for lab examination")
124
+ print(f" iotlabkit create {p.id} --mode learning (detailed guide)")
125
+ print(f" iotlabkit create {p.id} --mode exam (concise exam code)")
126
126
  print("=" * 75 + "\n")
127
127
  return 0
128
128
 
@@ -132,7 +132,7 @@ def cmd_create(args: argparse.Namespace) -> int:
132
132
  program_id = args.program_id
133
133
 
134
134
  if program_id in EXCLUDED_PROGRAM_IDS:
135
- print(f"\n❌ Error: Program {program_id} is outside the supported syllabus scope.")
135
+ print(f"\nError: Program {program_id} is outside the supported syllabus scope.")
136
136
  print(" Only Programs 5 through 17 are included in iotlabkit.\n")
137
137
  return 1
138
138
 
@@ -146,7 +146,7 @@ def cmd_create(args: argparse.Namespace) -> int:
146
146
  )
147
147
  return 0
148
148
  except Exception as err:
149
- print(f"\n❌ Error generating practical {program_id}: {err}\n")
149
+ print(f"\nError generating practical {program_id}: {err}\n")
150
150
  return 1
151
151
 
152
152
 
@@ -62,7 +62,7 @@ def run_doctor() -> int:
62
62
  # 1. Python Environment
63
63
  print("\n[1] Python Runtime:")
64
64
  py_ok, py_msg = check_python_version()
65
- status_icon = "✓" if py_ok else "✗"
65
+ status_icon = "[OK]" if py_ok else "[X]"
66
66
  print(f" {status_icon} Python Version : {py_msg}")
67
67
  print(f" Platform : {platform.system()} {platform.release()} ({platform.machine()})")
68
68
  if not py_ok:
@@ -78,7 +78,7 @@ def run_doctor() -> int:
78
78
  ]
79
79
  for binary, desc in tools:
80
80
  found, msg = check_tool(binary)
81
- icon = "✓" if found else "!"
81
+ icon = "[OK]" if found else "[ ]"
82
82
  print(f" {icon} {binary:<16}: {msg} - {desc}")
83
83
 
84
84
  # 3. Layer Dependencies
@@ -112,7 +112,7 @@ def run_doctor() -> int:
112
112
  print(f" -- {group_name} --")
113
113
  for pkg_name, imp_name in pkgs:
114
114
  installed, ver_info = check_python_package(pkg_name, imp_name)
115
- icon = "✓" if installed else "✗"
115
+ icon = "[OK]" if installed else "[ ]"
116
116
  print(f" {icon} {pkg_name:<16}: {ver_info}")
117
117
  if not installed:
118
118
  missing_packages.append(pkg_name)
@@ -129,7 +129,7 @@ def run_doctor() -> int:
129
129
  ]
130
130
  for var_name, desc in env_vars:
131
131
  is_set, safe_info = check_env_var_safe(var_name)
132
- icon = "✓" if is_set else "-"
132
+ icon = "[OK]" if is_set else "[-]"
133
133
  print(f" {icon} {var_name:<26}: {safe_info} ({desc})")
134
134
 
135
135
  # 5. Diagnostic Summary
@@ -137,7 +137,7 @@ def run_doctor() -> int:
137
137
  print(" Summary & Recommendations")
138
138
  print("=" * 70)
139
139
  if missing_packages:
140
- print(f"\n! Note: {len(missing_packages)} optional packages are currently uninstalled.")
140
+ print(f"\nNote: {len(missing_packages)} optional packages are currently uninstalled.")
141
141
  print(" To install packages for specific practicals, you can run:")
142
142
  print(" pip install iotlabkit[analytics] # For Data Analytics practicals (11-14)")
143
143
  print(" pip install iotlabkit[ml] # For Machine Learning practicals (15-17)")
@@ -145,7 +145,7 @@ def run_doctor() -> int:
145
145
  print(" pip install iotlabkit[dashboard] # For Streamlit dashboards")
146
146
  print(" pip install iotlabkit[all] # To install all dependencies at once")
147
147
  else:
148
- print("\n✓ All Python packages for all 13 practicals are installed and ready!")
148
+ print("\nAll Python packages for all 13 practicals are installed and ready!")
149
149
 
150
150
  print(
151
151
  "\nReminder: Programs 8 (CoAP) runs with zero external packages (pure standard library).\n"
@@ -21,7 +21,7 @@ def prompt_overwrite_confirmation(directory: str) -> bool:
21
21
  if not existing_files:
22
22
  return True
23
23
 
24
- print(f"\n⚠️ Warning: Target directory '{directory}' already exists and contains {len(existing_files)} file(s):")
24
+ print(f"\nWarning: Target directory '{directory}' already exists and contains {len(existing_files)} file(s):")
25
25
  for f in existing_files[:5]:
26
26
  print(f" - {f}")
27
27
  if len(existing_files) > 5:
@@ -77,7 +77,7 @@ def generate_program(
77
77
  generator_fn = prog_info.get_generator()
78
78
  files_to_write: Dict[str, str] = generator_fn(target_abs, clean_mode)
79
79
 
80
- print(f"\n🚀 Generating Program {prog_info.id}: {prog_info.title}")
80
+ print(f"\nGenerating Program {prog_info.id}: {prog_info.title}")
81
81
  print(f" Mode: {clean_mode.upper()}")
82
82
  print(f" Target Directory: {target_abs}\n")
83
83
 
@@ -89,10 +89,10 @@ def generate_program(
89
89
 
90
90
  with open(full_path, "w", encoding="utf-8") as f:
91
91
  f.write(content)
92
- print(f" ✓ Created: {rel_path}")
92
+ print(f" Created: {rel_path}")
93
93
 
94
94
  print("\n" + "=" * 70)
95
- print(f"🎉 Project successfully generated in '{target_dir}'!")
95
+ print(f"Project generated in '{target_dir}'.")
96
96
  print("=" * 70)
97
97
  print("\nNext steps to run this practical:")
98
98
  print(f" 1. Navigate to the project directory:")
@@ -81,6 +81,7 @@ PROGRAMS: Dict[int, ProgramInfo] = {
81
81
  "dashboard.py",
82
82
  "supabase_schema.sql",
83
83
  "firebase_setup.md",
84
+ "init_db.py",
84
85
  ],
85
86
  prerequisites=[
86
87
  "Python 3.9+",
@@ -130,6 +131,7 @@ PROGRAMS: Dict[int, ProgramInfo] = {
130
131
  "publisher.py",
131
132
  "dashboard.py",
132
133
  "supabase_schema.sql",
134
+ "init_db.py",
133
135
  ],
134
136
  prerequisites=[
135
137
  "Python 3.9+",
@@ -323,6 +325,7 @@ PROGRAMS: Dict[int, ProgramInfo] = {
323
325
  "dashboard.py",
324
326
  "supabase_schema.sql",
325
327
  "coap_core.py",
328
+ "init_db.py",
326
329
  ],
327
330
  prerequisites=[
328
331
  "Python 3.9+",
@@ -0,0 +1,282 @@
1
+ """Template generator for Program 5: HTTP REST API + Cloud Database + Dashboard."""
2
+
3
+ from typing import Dict
4
+
5
+
6
+ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
7
+ files = {}
8
+
9
+ files["requirements.txt"] = (
10
+ "requests>=2.28.0\n"
11
+ "python-dotenv>=1.0.0\n"
12
+ "flask>=2.2.0\n"
13
+ "streamlit>=1.25.0\n"
14
+ "psycopg2-binary>=2.9.0\n"
15
+ )
16
+
17
+ files[".env.example"] = (
18
+ "PORT=5000\n"
19
+ "HOST=127.0.0.1\n"
20
+ "CLOUD_PROVIDER=supabase\n"
21
+ "SUPABASE_URL=https://your-project.supabase.co\n"
22
+ "SUPABASE_KEY=your-supabase-key\n"
23
+ "SUPABASE_DB_PASSWORD=your-database-password\n"
24
+ "FIREBASE_DATABASE_URL=https://your-project.firebaseio.com\n"
25
+ )
26
+
27
+ files[".gitignore"] = (
28
+ ".env\n"
29
+ "__pycache__/\n"
30
+ "*.py[cod]\n"
31
+ "data/local_telemetry.json\n"
32
+ )
33
+
34
+ files["supabase_schema.sql"] = (
35
+ "CREATE TABLE IF NOT EXISTS sensor_telemetry (\n"
36
+ " id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
37
+ " created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
38
+ " sensor_id TEXT NOT NULL DEFAULT 'DHT22_01',\n"
39
+ " temperature DOUBLE PRECISION NOT NULL,\n"
40
+ " humidity DOUBLE PRECISION NOT NULL\n"
41
+ ");\n"
42
+ )
43
+
44
+ files["firebase_setup.md"] = (
45
+ "# Firebase Setup Guide\n\n"
46
+ "1. Open https://console.firebase.google.com/ and create a project.\n"
47
+ "2. Go to Build -> Realtime Database -> Create Database.\n"
48
+ "3. Select test mode rules.\n"
49
+ "4. Copy database URL to .env under FIREBASE_DATABASE_URL.\n"
50
+ "5. Set CLOUD_PROVIDER=firebase in .env.\n"
51
+ )
52
+
53
+ # server.py
54
+ files["server.py"] = (
55
+ "import json\n"
56
+ "import os\n"
57
+ "from datetime import datetime, timezone\n"
58
+ "from flask import Flask, request, jsonify\n"
59
+ "import requests\n"
60
+ "from dotenv import load_dotenv\n\n"
61
+ "load_dotenv()\n\n"
62
+ "app = Flask(__name__)\n\n"
63
+ "HOST = os.getenv('HOST', '127.0.0.1')\n"
64
+ "PORT = int(os.getenv('PORT', 5000))\n"
65
+ "CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'supabase').lower().strip()\n"
66
+ "SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
67
+ "SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
68
+ "FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
69
+ "LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_telemetry.json')\n\n"
70
+ "def save_to_supabase(record):\n"
71
+ " if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
72
+ " return False\n"
73
+ " headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}', 'Content-Type': 'application/json'}\n"
74
+ " resp = requests.post(f'{SUPABASE_URL}/rest/v1/sensor_telemetry', json=record, headers=headers, timeout=5)\n"
75
+ " return resp.status_code in (200, 201, 204)\n\n"
76
+ "def save_to_firebase(record):\n"
77
+ " if not FIREBASE_URL or 'your-firebase' in FIREBASE_URL:\n"
78
+ " return False\n"
79
+ " resp = requests.post(f'{FIREBASE_URL}/sensor_telemetry.json', json=record, timeout=5)\n"
80
+ " return resp.status_code == 200\n\n"
81
+ "def save_local(record):\n"
82
+ " os.makedirs(os.path.dirname(LOCAL_BACKUP), exist_ok=True)\n"
83
+ " data = []\n"
84
+ " if os.path.exists(LOCAL_BACKUP):\n"
85
+ " try:\n"
86
+ " with open(LOCAL_BACKUP, 'r', encoding='utf-8') as f:\n"
87
+ " data = json.load(f)\n"
88
+ " except Exception:\n"
89
+ " data = []\n"
90
+ " data.append(record)\n"
91
+ " with open(LOCAL_BACKUP, 'w', encoding='utf-8') as f:\n"
92
+ " json.dump(data, f, indent=2)\n\n"
93
+ "@app.route('/api/telemetry', methods=['POST'])\n"
94
+ "def receive_telemetry():\n"
95
+ " payload = request.get_json(silent=True) or {}\n"
96
+ " temp = float(payload.get('temperature', 25.0))\n"
97
+ " hum = float(payload.get('humidity', 60.0))\n"
98
+ " record = {\n"
99
+ " 'sensor_id': str(payload.get('sensor_id', 'DHT22_01')),\n"
100
+ " 'temperature': temp,\n"
101
+ " 'humidity': hum,\n"
102
+ " 'created_at': datetime.now(timezone.utc).isoformat()\n"
103
+ " }\n"
104
+ " saved_cloud = False\n"
105
+ " if CLOUD_PROVIDER == 'supabase':\n"
106
+ " saved_cloud = save_to_supabase(record)\n"
107
+ " elif CLOUD_PROVIDER == 'firebase':\n"
108
+ " saved_cloud = save_to_firebase(record)\n"
109
+ " save_local(record)\n"
110
+ " status = 'Cloud Stored' if saved_cloud else 'Saved Locally'\n"
111
+ " print(f\"[{datetime.now().strftime('%H:%M:%S')}] Telemetry: Temp={temp}C, Humidity={hum}% -> {status}\")\n"
112
+ " return jsonify({'message': 'Accepted', 'cloud_synced': saved_cloud, 'record': record}), 201\n\n"
113
+ "@app.route('/api/telemetry', methods=['GET'])\n"
114
+ "def get_telemetry():\n"
115
+ " if os.path.exists(LOCAL_BACKUP):\n"
116
+ " with open(LOCAL_BACKUP, 'r', encoding='utf-8') as f:\n"
117
+ " data = json.load(f)\n"
118
+ " return jsonify(list(reversed(data[-50:]))), 200\n"
119
+ " return jsonify([]), 200\n\n"
120
+ "if __name__ == '__main__':\n"
121
+ " try:\n"
122
+ " import init_db\n"
123
+ " init_db.init_table()\n"
124
+ " except Exception:\n"
125
+ " pass\n"
126
+ " print(f'Starting REST API server on http://{HOST}:{PORT}')\n"
127
+ " app.run(host=HOST, port=PORT, debug=False)\n"
128
+ )
129
+
130
+ files["init_db.py"] = (
131
+ "import os\n"
132
+ "import re\n"
133
+ "import requests\n"
134
+ "from dotenv import load_dotenv\n\n"
135
+ "load_dotenv()\n\n"
136
+ "SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
137
+ "SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
138
+ "DATABASE_URL = os.getenv('DATABASE_URL', '')\n"
139
+ "DB_PASSWORD = os.getenv('SUPABASE_DB_PASSWORD', '')\n"
140
+ "ACCESS_TOKEN = os.getenv('SUPABASE_ACCESS_TOKEN', '')\n\n"
141
+ "SQL_QUERY = '''\n"
142
+ "CREATE TABLE IF NOT EXISTS sensor_telemetry (\n"
143
+ " id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
144
+ " created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
145
+ " sensor_id TEXT NOT NULL DEFAULT 'DHT22_01',\n"
146
+ " temperature DOUBLE PRECISION NOT NULL,\n"
147
+ " humidity DOUBLE PRECISION NOT NULL\n"
148
+ ");\n"
149
+ "ALTER TABLE sensor_telemetry ENABLE ROW LEVEL SECURITY;\n"
150
+ "CREATE POLICY \"Allow anon read\" ON sensor_telemetry FOR SELECT USING (true);\n"
151
+ "CREATE POLICY \"Allow anon insert\" ON sensor_telemetry FOR INSERT WITH CHECK (true);\n"
152
+ "'''\n\n"
153
+ "def init_table():\n"
154
+ " if not SUPABASE_URL or 'your-project' in SUPABASE_URL:\n"
155
+ " return False\n"
156
+ " headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}'}\n"
157
+ " try:\n"
158
+ " r = requests.get(f'{SUPABASE_URL}/rest/v1/sensor_telemetry?limit=1', headers=headers, timeout=4)\n"
159
+ " if r.status_code == 200:\n"
160
+ " print('Supabase: Table sensor_telemetry is ready.')\n"
161
+ " return True\n"
162
+ " except Exception:\n"
163
+ " pass\n"
164
+ " m = re.search(r'https://([a-z0-9_-]+)\\.supabase\\.co', SUPABASE_URL)\n"
165
+ " ref = m.group(1) if m else None\n"
166
+ " if ACCESS_TOKEN and ref:\n"
167
+ " try:\n"
168
+ " resp = requests.post(f'https://api.supabase.com/v1/projects/{ref}/database/query', json={'query': SQL_QUERY}, headers={'Authorization': f'Bearer {ACCESS_TOKEN}', 'Content-Type': 'application/json'}, timeout=8)\n"
169
+ " if resp.status_code in (200, 201):\n"
170
+ " print('Supabase: Table sensor_telemetry created via API.')\n"
171
+ " return True\n"
172
+ " except Exception:\n"
173
+ " pass\n"
174
+ " conn_str = DATABASE_URL\n"
175
+ " if not conn_str and DB_PASSWORD and ref:\n"
176
+ " conn_str = f'postgresql://postgres:{DB_PASSWORD}@db.{ref}.supabase.co:5432/postgres'\n"
177
+ " if conn_str:\n"
178
+ " try:\n"
179
+ " import psycopg2\n"
180
+ " conn = psycopg2.connect(conn_str, connect_timeout=6)\n"
181
+ " cur = conn.cursor()\n"
182
+ " cur.execute(SQL_QUERY)\n"
183
+ " conn.commit()\n"
184
+ " cur.close()\n"
185
+ " conn.close()\n"
186
+ " print('Supabase: Table sensor_telemetry created via PostgreSQL.')\n"
187
+ " return True\n"
188
+ " except Exception as e:\n"
189
+ " print(f'Supabase PostgreSQL note: {e}')\n"
190
+ " print('Supabase: Table not found. To create via code, add SUPABASE_DB_PASSWORD in .env, or paste supabase_schema.sql in Supabase SQL editor.')\n"
191
+ " return False\n\n"
192
+ "if __name__ == '__main__':\n"
193
+ " init_table()\n"
194
+ )
195
+
196
+ # client_sender.py
197
+ files["client_sender.py"] = (
198
+ "import argparse\n"
199
+ "import time\n"
200
+ "import requests\n\n"
201
+ "DEFAULT_URL = 'http://127.0.0.1:5000/api/telemetry'\n"
202
+ "READINGS = [\n"
203
+ " (24.5, 62.0), (24.8, 61.5), (25.1, 60.8), (25.4, 59.9), (25.7, 58.5)\n"
204
+ "]\n\n"
205
+ "def send_reading(url, temp, hum):\n"
206
+ " payload = {'sensor_id': 'DHT22_01', 'temperature': temp, 'humidity': hum}\n"
207
+ " try:\n"
208
+ " r = requests.post(url, json=payload, timeout=3)\n"
209
+ " print(f\"Sent: Temp={temp}C, Hum={hum}% | Response: {r.status_code}\")\n"
210
+ " except Exception as e:\n"
211
+ " print('Send error:', e)\n\n"
212
+ "def main():\n"
213
+ " parser = argparse.ArgumentParser()\n"
214
+ " parser.add_argument('--url', default=DEFAULT_URL)\n"
215
+ " parser.add_argument('--temp', type=float)\n"
216
+ " parser.add_argument('--humidity', type=float)\n"
217
+ " args = parser.parse_args()\n\n"
218
+ " if args.temp is not None and args.humidity is not None:\n"
219
+ " send_reading(args.url, args.temp, args.humidity)\n"
220
+ " return\n\n"
221
+ " print('Streaming sample sensor readings...')\n"
222
+ " for t, h in READINGS:\n"
223
+ " send_reading(args.url, t, h)\n"
224
+ " time.sleep(1.5)\n\n"
225
+ "if __name__ == '__main__':\n"
226
+ " main()\n"
227
+ )
228
+
229
+ # dashboard.py
230
+ files["dashboard.py"] = (
231
+ "import os\n"
232
+ "import json\n"
233
+ "import requests\n"
234
+ "import streamlit as st\n\n"
235
+ "st.title('Sensor Telemetry Dashboard')\n\n"
236
+ "REST_API_URL = os.getenv('REST_API_URL', 'http://127.0.0.1:5000/api/telemetry')\n"
237
+ "LOCAL_FILE = os.path.join(os.path.dirname(__file__), 'data', 'local_telemetry.json')\n\n"
238
+ "def fetch_data():\n"
239
+ " try:\n"
240
+ " resp = requests.get(REST_API_URL, timeout=3)\n"
241
+ " if resp.status_code == 200:\n"
242
+ " return resp.json()\n"
243
+ " except Exception:\n"
244
+ " pass\n"
245
+ " if os.path.exists(LOCAL_FILE):\n"
246
+ " try:\n"
247
+ " with open(LOCAL_FILE, 'r', encoding='utf-8') as f:\n"
248
+ " return list(reversed(json.load(f)))\n"
249
+ " except Exception:\n"
250
+ " pass\n"
251
+ " return []\n\n"
252
+ "records = fetch_data()\n"
253
+ "if not records:\n"
254
+ " st.write('No telemetry records found. Start python server.py and send data.')\n"
255
+ "else:\n"
256
+ " latest = records[0]\n"
257
+ " st.write(f\"Latest Reading: Temperature = {latest.get('temperature', 0)} C | Humidity = {latest.get('humidity', 0)} %\")\n"
258
+ " temps = [r.get('temperature', 0) for r in reversed(records[-30:])]\n"
259
+ " hums = [r.get('humidity', 0) for r in reversed(records[-30:])]\n"
260
+ " st.line_chart({'Temperature': temps, 'Humidity': hums})\n"
261
+ " st.write('Recent Records:')\n"
262
+ " st.dataframe(records[:20])\n"
263
+ )
264
+
265
+ # README.md
266
+ files["README.md"] = (
267
+ "# Program 5 — HTTP REST API + Cloud DB + Dashboard\n\n"
268
+ "## Run\n"
269
+ "```bash\n"
270
+ "# Terminal 1: Start REST Server\n"
271
+ "python server.py\n\n"
272
+ "# Terminal 2: Send Sensor Data\n"
273
+ "python client_sender.py\n\n"
274
+ "# Terminal 3: View Dashboard\n"
275
+ "streamlit run dashboard.py\n"
276
+ "```\n\n"
277
+ "## Viva Q&A\n"
278
+ "- Q: What is REST?\n"
279
+ " A: Representational State Transfer, a stateless HTTP-based architecture using methods like GET and POST.\n"
280
+ )
281
+
282
+ return files