microweb 0.1.1__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.
microweb/microweb.py ADDED
@@ -0,0 +1,332 @@
1
+ import usocket as socket
2
+ import ujson
3
+ import ure
4
+ import gc
5
+ import wifi
6
+
7
+ class Request:
8
+ def __init__(self, method, path, query_params, post_data):
9
+ self.method = method
10
+ self.path = path
11
+ self.query_params = query_params
12
+ self.form = post_data
13
+
14
+ class Response:
15
+ def __init__(self, content, status=200, content_type='text/html', headers=None):
16
+ self.content = content
17
+ self.status = status
18
+ self.content_type = content_type
19
+ self.headers = headers or {}
20
+ # Add CORS header by default
21
+ self.headers['Access-Control-Allow-Origin'] = '*'
22
+
23
+ def to_http_response(self):
24
+ status_text = {
25
+ 200: 'OK',
26
+ 404: 'Not Found',
27
+ 405: 'Method Not Allowed',
28
+ 500: 'Internal Server Error',
29
+ 302: 'Found'
30
+ }
31
+
32
+ response = f'HTTP/1.1 {self.status} {status_text.get(self.status, "OK")}\r\n'
33
+ response += f'Content-Type: {self.content_type}\r\n'
34
+
35
+ for key, value in self.headers.items():
36
+ response += f'{key}: {value}\r\n'
37
+
38
+ response += '\r\n'
39
+ response += str(self.content)
40
+
41
+ return response
42
+
43
+ class MicroWeb:
44
+ def __init__(self, ssid=None, password=None, port=80, debug=False, ap=None):
45
+ self.routes = {}
46
+ self.static_files = {}
47
+ self.config = {'port': port, 'debug': debug}
48
+ self.session = {}
49
+
50
+ if ap and isinstance(ap, dict) and 'ssid' in ap:
51
+ ap_ssid = ap.get('ssid', 'ESP32-MicroWeb')
52
+ ap_password = ap.get('password', '12345678')
53
+ elif ssid and password:
54
+ ap_ssid = ssid
55
+ ap_password = password
56
+ else:
57
+ ap_ssid = 'ESP32-MicroWeb'
58
+ ap_password = '12345678'
59
+
60
+ wifi.setup_ap(ap_ssid, ap_password)
61
+
62
+ def route(self, path, methods=['GET']):
63
+ def decorator(func):
64
+ self.routes[path] = {'func': func, 'methods': methods}
65
+ return func
66
+ return decorator
67
+
68
+ def add_static(self, path, file_path):
69
+ self.static_files[path] = file_path
70
+
71
+ def render_template(self, template_file, **kwargs):
72
+ try:
73
+ with open(template_file, 'r') as f:
74
+ content = f.read()
75
+
76
+ if self.config['debug']:
77
+ print(f'Template content before replacement: {content[:100]}...')
78
+ print(f'Template variables: {kwargs}')
79
+
80
+ for key, value in kwargs.items():
81
+ patterns = [
82
+ '{%' + key + '%}',
83
+ '{% ' + key + ' %}',
84
+ '{% ' + key + ' %}',
85
+ ]
86
+ for pattern in patterns:
87
+ content = content.replace(pattern, str(value))
88
+
89
+ if self.config['debug']:
90
+ print(f'Template content after replacement: {content[:100]}...')
91
+
92
+ return content
93
+ except Exception as e:
94
+ if self.config['debug']:
95
+ print(f'Template error: {e}')
96
+ return f'<h1>Template Error</h1><p>Template not found: {template_file}</p><p>Error: {str(e)}</p>'
97
+
98
+ def json_response(self, data, status=200):
99
+ return Response(ujson.dumps(data), status=status, content_type='application/json')
100
+
101
+ def html_response(self, content, status=200):
102
+ return Response(content, status=status, content_type='text/html')
103
+
104
+ def redirect(self, location, status=302):
105
+ return Response('', status=status, headers={'Location': location})
106
+
107
+
108
+ def parse_request(self, request):
109
+ try:
110
+ lines = request.split('\r\n')
111
+ if not lines or not lines[0]:
112
+ if self.config['debug']:
113
+ print('Invalid request: empty or no request line')
114
+ return None
115
+
116
+ request_parts = lines[0].split(' ')
117
+ if len(request_parts) < 2:
118
+ if self.config['debug']:
119
+ print('Invalid request: malformed request line')
120
+ return None
121
+
122
+ method = request_parts[0]
123
+ full_path = request_parts[1]
124
+
125
+ if self.config['debug']:
126
+ print(f'Raw request: {request[:100]}...')
127
+ print(f'Parsed method: {method}, path: {full_path}')
128
+
129
+ path = full_path.split('?')[0]
130
+ query_params = {}
131
+
132
+ if '?' in full_path:
133
+ query_string = full_path.split('?')[1]
134
+ for param in query_string.split('&'):
135
+ if '=' in param:
136
+ key, value = param.split('=', 1)
137
+ value = value.replace('%20', ' ').replace('%21', '!')
138
+ query_params[key] = value
139
+
140
+ post_data = {}
141
+ if method == 'POST':
142
+ body_start = -1
143
+ for i, line in enumerate(lines):
144
+ if line == '':
145
+ body_start = i + 1
146
+ break
147
+
148
+ if body_start > 0 and body_start < len(lines):
149
+ body = '\r\n'.join(lines[body_start:])
150
+ if self.config['debug']:
151
+ print(f'Request body: {body}')
152
+
153
+ # Check for Content-Type header case-insensitively
154
+ content_type_header = None
155
+ for line in lines:
156
+ if line.lower().startswith('content-type:'):
157
+ content_type_header = line.split(':', 1)[1].strip()
158
+ break
159
+
160
+ if content_type_header and 'application/x-www-form-urlencoded' in content_type_header.lower():
161
+ for param in body.split('&'):
162
+ if '=' in param:
163
+ key, value = param.split('=', 1)
164
+ post_data[key] = value.replace('%20', ' ')
165
+ elif content_type_header and 'application/json' in content_type_header.lower():
166
+ try:
167
+ post_data = ujson.loads(body)
168
+ except Exception as e:
169
+ if self.config['debug']:
170
+ print(f'JSON parse error: {e}')
171
+ post_data = {}
172
+
173
+ if self.config['debug']:
174
+ print(f'Parsed query_params: {query_params}')
175
+ print(f'Parsed post_data: {post_data}')
176
+
177
+ return Request(method, path, query_params, post_data)
178
+ except Exception as e:
179
+ if self.config['debug']:
180
+ print(f'Parse request error: {e}')
181
+ return None
182
+
183
+
184
+ def get_content_type(self, file_path):
185
+ if file_path.endswith('.html') or file_path.endswith('.htm'):
186
+ return 'text/html'
187
+ elif file_path.endswith('.css'):
188
+ return 'text/css'
189
+ elif file_path.endswith('.js'):
190
+ return 'application/javascript'
191
+ elif file_path.endswith('.json'):
192
+ return 'application/json'
193
+ elif file_path.endswith('.png'):
194
+ return 'image/png'
195
+ elif file_path.endswith('.jpg') or file_path.endswith('.jpeg'):
196
+ return 'image/jpeg'
197
+ elif file_path.endswith('.gif'):
198
+ return 'image/gif'
199
+ elif file_path.endswith('.ico'):
200
+ return 'image/x-icon'
201
+ elif file_path.endswith('.txt'):
202
+ return 'text/plain'
203
+ else:
204
+ return 'text/plain'
205
+
206
+ def match_route(self, path, route_pattern):
207
+ if route_pattern == path:
208
+ return True, None
209
+
210
+ if '<' in route_pattern and '>' in route_pattern:
211
+ regex_pattern = route_pattern
212
+ param_pattern = ure.compile(r'<([^>]+)>')
213
+ regex_pattern = param_pattern.sub(r'([^/]+)', regex_pattern)
214
+
215
+ if not regex_pattern.startswith('^'):
216
+ regex_pattern = '^' + regex_pattern
217
+ if not regex_pattern.endswith('$'):
218
+ regex_pattern = regex_pattern + '$'
219
+
220
+ try:
221
+ match = ure.match(regex_pattern, path)
222
+ if match:
223
+ return True, match
224
+ except Exception as e:
225
+ if self.config['debug']:
226
+ print(f'Regex match error: {e}')
227
+ return False, None
228
+
229
+ try:
230
+ pattern = route_pattern
231
+ if not pattern.startswith('^'):
232
+ pattern = '^' + pattern
233
+ if not pattern.endswith('$'):
234
+ pattern = pattern + '$'
235
+
236
+ match = ure.match(pattern, path)
237
+ if match:
238
+ return True, match
239
+ except Exception as e:
240
+ if self.config['debug']:
241
+ print(f'Direct regex error: {e}')
242
+
243
+ return False, None
244
+
245
+ def handle_request(self, request):
246
+ req = self.parse_request(request)
247
+
248
+ if not req:
249
+ return Response('<h1>400 Bad Request</h1>', status=400).to_http_response()
250
+
251
+ if self.config['debug']:
252
+ print(f'Request: {req.method} {req.path}')
253
+
254
+ if req.path in self.static_files:
255
+ try:
256
+ file_path = self.static_files[req.path]
257
+ with open(file_path, 'r') as f:
258
+ content = f.read()
259
+ content_type = self.get_content_type(file_path)
260
+ if self.config['debug']:
261
+ print(f'Serving static file: {file_path} as {content_type}')
262
+ return Response(content, content_type=content_type).to_http_response()
263
+ except Exception as e:
264
+ if self.config['debug']:
265
+ print(f'Static file error: {e}')
266
+ return Response('<h1>404 Not Found</h1><p>File not found</p>',
267
+ status=404).to_http_response()
268
+
269
+ for route_pattern, route_config in self.routes.items():
270
+ is_match, match_obj = self.match_route(req.path, route_pattern)
271
+
272
+ if is_match:
273
+ if req.method not in route_config['methods']:
274
+ return Response('<h1>405 Method Not Allowed</h1>',
275
+ status=405).to_http_response()
276
+
277
+ try:
278
+ if match_obj and hasattr(match_obj, 'group'):
279
+ result = route_config['func'](req, match_obj)
280
+ else:
281
+ result = route_config['func'](req)
282
+
283
+ if isinstance(result, Response):
284
+ return result.to_http_response()
285
+ elif isinstance(result, str):
286
+ return Response(result).to_http_response()
287
+ elif isinstance(result, dict):
288
+ return self.json_response(result).to_http_response()
289
+ else:
290
+ return Response(str(result)).to_http_response()
291
+
292
+ except Exception as e:
293
+ if self.config['debug']:
294
+ print(f'Route handler error: {e}')
295
+ return Response(f'<h1>500 Internal Server Error</h1><p>{str(e)}</p>',
296
+ status=500).to_http_response()
297
+
298
+ return Response('<h1>404 Not Found</h1><p>Page not found</p>',
299
+ status=404).to_http_response()
300
+
301
+ def run(self):
302
+ s = socket.socket()
303
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
304
+ s.bind(('0.0.0.0', self.config['port']))
305
+ s.listen(5)
306
+
307
+ if self.config['debug']:
308
+ print(f"MicroWeb running on http://0.0.0.0:{self.config['port']}")
309
+
310
+ while True:
311
+ conn = None
312
+ try:
313
+ conn, addr = s.accept()
314
+ if self.config['debug']:
315
+ print(f'Connection from {addr}')
316
+
317
+ request = conn.recv(1024).decode('utf-8')
318
+ if request:
319
+ response = self.handle_request(request)
320
+ conn.send(response.encode('utf-8'))
321
+
322
+ except Exception as e:
323
+ if self.config['debug']:
324
+ print(f'Request handling error: {e}')
325
+ finally:
326
+ if conn:
327
+ try:
328
+ conn.close()
329
+ except:
330
+ pass
331
+ gc.collect()
332
+
microweb/uploader.py ADDED
@@ -0,0 +1,79 @@
1
+ import subprocess
2
+ import os
3
+ import serial.tools.list_ports
4
+
5
+ def upload_file(file_path, port=None,destination=None):
6
+ """Upload a file to the ESP32 filesystem using mpremote."""
7
+ if not port:
8
+ ports = [p.device for p in serial.tools.list_ports.comports()]
9
+ port = ports[0] if ports else None
10
+ if not port:
11
+ raise Exception("No ESP32 found. Specify --port.")
12
+
13
+ file_name = file_path.split('/')[-1].split('\\')[-1]
14
+
15
+ try:
16
+ if not os.path.exists(file_path):
17
+ raise Exception(f"File {file_path} does not exist.")
18
+
19
+ # Use mpremote to copy the file
20
+ cmd = ['mpremote', 'connect', port, 'cp', file_path, f':{file_name}']
21
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
22
+
23
+ if result.returncode != 0:
24
+ raise Exception(f"mpremote failed: {result.stderr}")
25
+
26
+ # Verify file was uploaded
27
+ verify_cmd = ['mpremote', 'connect', port, 'ls']
28
+ verify_result = subprocess.run(verify_cmd, capture_output=True, text=True, timeout=10)
29
+
30
+ if verify_result.returncode == 0 and file_name in verify_result.stdout:
31
+ print(f"Successfully uploaded {file_name}")
32
+ else:
33
+ raise Exception(f"File {file_name} not found after upload")
34
+
35
+ except subprocess.TimeoutExpired:
36
+ raise Exception(f"Upload timeout for {file_name}")
37
+ except Exception as e:
38
+ raise Exception(f"Upload error for {file_name}: {e}")
39
+
40
+ def create_directory(dir_name, port=None):
41
+ """Create a directory on the ESP32 filesystem."""
42
+ if not port:
43
+ ports = [p.device for p in serial.tools.list_ports.comports()]
44
+ port = ports[0] if ports else None
45
+ if not port:
46
+ raise Exception("No ESP32 found. Specify --port.")
47
+
48
+ try:
49
+ cmd = ['mpremote', 'connect', port, 'mkdir', f':{dir_name}']
50
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
51
+
52
+ # mkdir might fail if directory exists, that's ok
53
+ if result.returncode == 0 or 'exists' in result.stderr.lower():
54
+ print(f"Directory {dir_name} ready")
55
+ else:
56
+ raise Exception(f"Failed to create directory {dir_name}: {result.stderr}")
57
+
58
+ except Exception as e:
59
+ raise Exception(f"Directory creation error for {dir_name}: {e}")
60
+
61
+ def verify_files(port, expected_files):
62
+ """Verify that expected files are present on the ESP32."""
63
+ try:
64
+ cmd = ['mpremote', 'connect', port, 'ls']
65
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
66
+
67
+ if result.returncode != 0:
68
+ raise Exception(f"Failed to list files: {result.stderr}")
69
+
70
+ output = result.stdout
71
+ missing = [f for f in expected_files if f not in output]
72
+
73
+ if missing:
74
+ print(f"Warning: Missing files on ESP32: {missing}")
75
+ else:
76
+ print("All expected files verified on ESP32")
77
+
78
+ except Exception as e:
79
+ print(f"Error verifying files: {e}")
microweb/wifi.py ADDED
@@ -0,0 +1,38 @@
1
+ import network
2
+ import time
3
+
4
+ def setup_ap(ssid="ESP32-MicroWeb", password="12345678"):
5
+ """Setup ESP32 as Access Point only."""
6
+ sta = network.WLAN(network.STA_IF)
7
+ sta.active(False)
8
+
9
+ ap = network.WLAN(network.AP_IF)
10
+ ap.active(True)
11
+
12
+ ap.config(
13
+ essid=ssid,
14
+ password=password,
15
+ authmode=network.AUTH_WPA_WPA2_PSK,
16
+ channel=11
17
+ )
18
+
19
+ while not ap.active():
20
+ time.sleep(0.1)
21
+
22
+ print("=" * 40)
23
+ print("ESP32 Access Point Ready!")
24
+ print(f"SSID: {ssid}")
25
+ print(f"Password: {password}")
26
+ print("IP Address:", ap.ifconfig()[0])
27
+ print("Connect to this WiFi and visit:")
28
+ print(f"http://{ap.ifconfig()[0]}")
29
+ print("=" * 40)
30
+
31
+ return ap.ifconfig()[0]
32
+
33
+ def get_ip():
34
+ """Get the AP IP address."""
35
+ ap = network.WLAN(network.AP_IF)
36
+ if ap.active():
37
+ return ap.ifconfig()[0]
38
+ return None