fastled 1.3.30__py3-none-any.whl → 1.4.50__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 (47) hide show
  1. fastled/__init__.py +30 -2
  2. fastled/__main__.py +14 -0
  3. fastled/__version__.py +1 -1
  4. fastled/app.py +51 -2
  5. fastled/args.py +33 -0
  6. fastled/client_server.py +188 -40
  7. fastled/compile_server.py +10 -0
  8. fastled/compile_server_impl.py +34 -1
  9. fastled/docker_manager.py +56 -14
  10. fastled/emoji_util.py +27 -0
  11. fastled/filewatcher.py +6 -3
  12. fastled/find_good_connection.py +105 -0
  13. fastled/header_dump.py +63 -0
  14. fastled/install/__init__.py +1 -0
  15. fastled/install/examples_manager.py +62 -0
  16. fastled/install/extension_manager.py +113 -0
  17. fastled/install/main.py +156 -0
  18. fastled/install/project_detection.py +167 -0
  19. fastled/install/test_install.py +373 -0
  20. fastled/install/vscode_config.py +344 -0
  21. fastled/interruptible_http.py +148 -0
  22. fastled/live_client.py +21 -1
  23. fastled/open_browser.py +84 -16
  24. fastled/parse_args.py +110 -9
  25. fastled/playwright/chrome_extension_downloader.py +207 -0
  26. fastled/playwright/playwright_browser.py +773 -0
  27. fastled/playwright/resize_tracking.py +127 -0
  28. fastled/print_filter.py +52 -52
  29. fastled/project_init.py +20 -13
  30. fastled/select_sketch_directory.py +142 -19
  31. fastled/server_flask.py +37 -1
  32. fastled/settings.py +47 -3
  33. fastled/sketch.py +121 -4
  34. fastled/string_diff.py +162 -26
  35. fastled/test/examples.py +7 -5
  36. fastled/types.py +4 -0
  37. fastled/util.py +34 -0
  38. fastled/version.py +41 -41
  39. fastled/web_compile.py +379 -236
  40. fastled/zip_files.py +76 -0
  41. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -508
  42. fastled-1.4.50.dist-info/RECORD +60 -0
  43. fastled-1.3.30.dist-info/RECORD +0 -44
  44. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/WHEEL +0 -0
  45. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
  46. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/licenses/LICENSE +0 -0
  47. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/web_compile.py CHANGED
@@ -1,35 +1,23 @@
1
- import _thread
2
1
  import io
3
- import json
4
2
  import os
5
3
  import shutil
6
4
  import tempfile
7
5
  import time
8
6
  import zipfile
9
- from concurrent.futures import Future, ThreadPoolExecutor, as_completed
10
- from dataclasses import dataclass
11
7
  from pathlib import Path
12
8
 
13
9
  import httpx
14
10
 
15
- from fastled.settings import SERVER_PORT
16
- from fastled.sketch import get_sketch_files
11
+ from fastled.emoji_util import EMO, safe_print
12
+ from fastled.find_good_connection import find_good_connection
13
+ from fastled.interruptible_http import make_interruptible_post_request
14
+ from fastled.settings import AUTH_TOKEN, SERVER_PORT
17
15
  from fastled.types import BuildMode, CompileResult
18
- from fastled.util import hash_file
16
+ from fastled.zip_files import ZipResult, zip_files
19
17
 
20
18
  DEFAULT_HOST = "https://fastled.onrender.com"
21
19
  ENDPOINT_COMPILED_WASM = "compile/wasm"
22
- _TIMEOUT = 60 * 4 # 2 mins timeout
23
- _AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
24
- ENABLE_EMBEDDED_DATA = True
25
- _EXECUTOR = ThreadPoolExecutor(max_workers=8)
26
-
27
-
28
- @dataclass
29
- class ConnectionResult:
30
- host: str
31
- success: bool
32
- ipv4: bool
20
+ _TIMEOUT = 60 * 4 # 4 mins timeout
33
21
 
34
22
 
35
23
  def _sanitize_host(host: str) -> str:
@@ -42,120 +30,36 @@ def _sanitize_host(host: str) -> str:
42
30
  return host if host.startswith("http://") else f"http://{host}"
43
31
 
44
32
 
45
- def _test_connection(host: str, use_ipv4: bool) -> ConnectionResult:
46
- # Function static cache
47
- host = _sanitize_host(host)
48
- transport = httpx.HTTPTransport(local_address="0.0.0.0") if use_ipv4 else None
49
- result: ConnectionResult | None = None
50
- try:
51
- with httpx.Client(
52
- timeout=_TIMEOUT,
53
- transport=transport,
54
- ) as test_client:
55
- test_response = test_client.get(
56
- f"{host}/healthz", timeout=3, follow_redirects=True
57
- )
58
- result = ConnectionResult(host, test_response.status_code == 200, use_ipv4)
59
- except KeyboardInterrupt:
60
- _thread.interrupt_main()
61
- result = ConnectionResult(host, False, use_ipv4)
62
- except TimeoutError:
63
- result = ConnectionResult(host, False, use_ipv4)
64
- except Exception:
65
- result = ConnectionResult(host, False, use_ipv4)
66
- return result
67
-
68
-
69
- def _file_info(file_path: Path) -> str:
70
- hash_txt = hash_file(file_path)
71
- file_size = file_path.stat().st_size
72
- json_str = json.dumps({"hash": hash_txt, "size": file_size})
73
- return json_str
74
-
75
-
76
- @dataclass
77
- class ZipResult:
78
- zip_bytes: bytes
79
- zip_embedded_bytes: bytes | None
80
- success: bool
81
- error: str | None
82
-
83
-
84
- def zip_files(directory: Path, build_mode: BuildMode) -> ZipResult | Exception:
85
- print("Zipping files...")
86
- try:
87
- files = get_sketch_files(directory)
88
- if not files:
89
- raise FileNotFoundError(f"No files found in {directory}")
90
- for f in files:
91
- print(f"Adding file: {f}")
92
- # Create in-memory zip file
93
- has_embedded_zip = False
94
- zip_embedded_buffer = io.BytesIO()
95
- zip_buffer = io.BytesIO()
96
- with zipfile.ZipFile(
97
- zip_embedded_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
98
- ) as emebedded_zip_file:
99
- with zipfile.ZipFile(
100
- zip_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
101
- ) as zip_file:
102
- for file_path in files:
103
- if "fastled_js" in str(file_path):
104
- # These can be huge, don't send the output files back to the server!
105
- continue
106
- relative_path = file_path.relative_to(directory)
107
- achive_path = str(Path("wasm") / relative_path)
108
- if str(relative_path).startswith("data") and ENABLE_EMBEDDED_DATA:
109
- _file_info_str = _file_info(file_path)
110
- zip_file.writestr(
111
- achive_path + ".embedded.json", _file_info_str
112
- )
113
- emebedded_zip_file.write(file_path, relative_path)
114
- has_embedded_zip = True
115
- else:
116
- zip_file.write(file_path, achive_path)
117
- # write build mode into the file as build.txt so that sketches are fingerprinted
118
- # based on the build mode. Otherwise the same sketch with different build modes
119
- # will have the same fingerprint.
120
- zip_file.writestr(
121
- str(Path("wasm") / "build_mode.txt"), build_mode.value
122
- )
123
- result = ZipResult(
124
- zip_bytes=zip_buffer.getvalue(),
125
- zip_embedded_bytes=(
126
- zip_embedded_buffer.getvalue() if has_embedded_zip else None
127
- ),
128
- success=True,
129
- error=None,
130
- )
131
- return result
132
- except Exception as e:
133
- return e
134
-
135
-
136
- def find_good_connection(
137
- urls: list[str], filter_out_bad=True, use_ipv6: bool = True
138
- ) -> ConnectionResult | None:
139
- futures: list[Future] = []
140
- for url in urls:
141
-
142
- f = _EXECUTOR.submit(_test_connection, url, use_ipv4=True)
143
- futures.append(f)
144
- if use_ipv6 and "localhost" not in url:
145
- f_v6 = _EXECUTOR.submit(_test_connection, url, use_ipv4=False)
146
- futures.append(f_v6)
33
+ def _check_embedded_http_status(response_content: bytes) -> tuple[bool, int | None]:
34
+ """
35
+ Check if the response content has an embedded HTTP status at the end.
147
36
 
37
+ Returns:
38
+ tuple: (has_embedded_status, status_code)
39
+ has_embedded_status is True if an embedded status was found
40
+ status_code is the embedded status code or None if not found
41
+ """
148
42
  try:
149
- # Return first successful result
150
- for future in as_completed(futures):
151
- result: ConnectionResult = future.result()
152
- if result.success or not filter_out_bad:
153
- return result
154
- finally:
155
- # Cancel any remaining futures
156
- for future in futures:
157
- future.cancel()
158
- return None
43
+ # Convert bytes to string for parsing
44
+ content_str = response_content.decode("utf-8", errors="ignore")
45
+
46
+ # Look for HTTP_STATUS: at the end of the content
47
+ lines = content_str.strip().split("\n")
48
+ if lines:
49
+ last_line = lines[-1].strip()
50
+ if "HTTP_STATUS:" in last_line:
51
+ try:
52
+ right = last_line.split("HTTP_STATUS:")[-1].strip()
53
+ status_code = int(right)
54
+ return True, status_code
55
+ except (ValueError, IndexError):
56
+ # Malformed status line
57
+ return True, None
58
+
59
+ return False, None
60
+ except Exception:
61
+ # If we can't parse the content, assume no embedded status
62
+ return False, None
159
63
 
160
64
 
161
65
  def _banner(msg: str) -> str:
@@ -186,7 +90,212 @@ def _banner(msg: str) -> str:
186
90
 
187
91
 
188
92
  def _print_banner(msg: str) -> None:
189
- print(_banner(msg))
93
+ safe_print(_banner(msg))
94
+
95
+
96
+ def _compile_libfastled(
97
+ host: str,
98
+ auth_token: str,
99
+ build_mode: BuildMode,
100
+ ) -> httpx.Response:
101
+ """Compile the FastLED library separately."""
102
+ host = _sanitize_host(host)
103
+ urls = [host]
104
+ domain = host.split("://")[-1]
105
+ if ":" not in domain:
106
+ urls.append(f"{host}:{SERVER_PORT}")
107
+
108
+ connection_result = find_good_connection(urls)
109
+ if connection_result is None:
110
+ raise ConnectionError(
111
+ "Connection failed to all endpoints for libfastled compilation"
112
+ )
113
+
114
+ ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
115
+ transport = (
116
+ httpx.HTTPTransport(local_address="0.0.0.0") if connection_result.ipv4 else None
117
+ )
118
+
119
+ headers = {
120
+ "accept": "application/json",
121
+ "authorization": auth_token,
122
+ "build": build_mode.value.lower(),
123
+ }
124
+
125
+ url = f"{connection_result.host}/compile/libfastled"
126
+ print(f"Compiling libfastled on {url} via {ipv4_stmt}")
127
+
128
+ # Use interruptible HTTP request
129
+ response = make_interruptible_post_request(
130
+ url=url,
131
+ files={}, # No files for libfastled compilation
132
+ headers=headers,
133
+ transport=transport,
134
+ timeout=_TIMEOUT * 2, # Give more time for library compilation
135
+ follow_redirects=False,
136
+ )
137
+
138
+ return response
139
+
140
+
141
+ def _send_compile_request(
142
+ host: str,
143
+ zip_bytes: bytes,
144
+ auth_token: str,
145
+ build_mode: BuildMode,
146
+ profile: bool,
147
+ no_platformio: bool,
148
+ ) -> httpx.Response:
149
+ """Send the compile request to the server and return the response."""
150
+ host = _sanitize_host(host)
151
+ urls = [host]
152
+ domain = host.split("://")[-1]
153
+ if ":" not in domain:
154
+ urls.append(f"{host}:{SERVER_PORT}")
155
+
156
+ connection_result = find_good_connection(urls)
157
+ if connection_result is None:
158
+ raise ConnectionError("Connection failed to all endpoints")
159
+
160
+ ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
161
+ transport = (
162
+ httpx.HTTPTransport(local_address="0.0.0.0") if connection_result.ipv4 else None
163
+ )
164
+
165
+ archive_size = len(zip_bytes)
166
+
167
+ headers = {
168
+ "accept": "application/json",
169
+ "authorization": auth_token,
170
+ "build": (
171
+ build_mode.value.lower() if build_mode else BuildMode.QUICK.value.lower()
172
+ ),
173
+ "profile": "true" if profile else "false",
174
+ "no-platformio": "true" if no_platformio else "false",
175
+ "allow-libcompile": "false", # Always false since we handle it manually
176
+ }
177
+
178
+ url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
179
+ print(f"Compiling sketch on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes")
180
+ files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
181
+
182
+ # Use interruptible HTTP request
183
+ response = make_interruptible_post_request(
184
+ url=url,
185
+ files=files,
186
+ headers=headers,
187
+ transport=transport,
188
+ timeout=_TIMEOUT,
189
+ follow_redirects=True,
190
+ )
191
+
192
+ return response
193
+
194
+
195
+ def _process_compile_response(
196
+ response: httpx.Response,
197
+ zip_result: ZipResult,
198
+ start_time: float,
199
+ zip_time: float,
200
+ libfastled_time: float,
201
+ sketch_time: float,
202
+ ) -> CompileResult:
203
+ """Process the compile response and return the final result."""
204
+ if response.status_code != 200:
205
+ json_response = response.json()
206
+ detail = json_response.get("detail", "Could not compile")
207
+ return CompileResult(
208
+ success=False,
209
+ stdout=detail,
210
+ hash_value=None,
211
+ zip_bytes=b"",
212
+ zip_time=zip_time,
213
+ libfastled_time=libfastled_time,
214
+ sketch_time=sketch_time,
215
+ response_processing_time=0.0, # No response processing in error case
216
+ )
217
+
218
+ print(f"Response status code: {response}")
219
+
220
+ # Time the response processing
221
+ response_processing_start = time.time()
222
+
223
+ # Create a temporary directory to extract the zip
224
+ with tempfile.TemporaryDirectory() as extract_dir:
225
+ extract_path = Path(extract_dir)
226
+
227
+ # Write the response content to a temporary zip file
228
+ temp_zip = extract_path / "response.zip"
229
+ temp_zip.write_bytes(response.content)
230
+
231
+ # Extract the zip
232
+ shutil.unpack_archive(temp_zip, extract_path, "zip")
233
+
234
+ if zip_result.zip_embedded_bytes:
235
+ # extract the embedded bytes, which were not sent to the server
236
+ temp_zip.write_bytes(zip_result.zip_embedded_bytes)
237
+ shutil.unpack_archive(temp_zip, extract_path, "zip")
238
+
239
+ # we don't need the temp zip anymore
240
+ temp_zip.unlink()
241
+
242
+ # Read stdout from out.txt if it exists
243
+ stdout_file = extract_path / "out.txt"
244
+ hash_file = extract_path / "hash.txt"
245
+ stdout = (
246
+ stdout_file.read_text(encoding="utf-8", errors="replace")
247
+ if stdout_file.exists()
248
+ else ""
249
+ )
250
+ hash_value = (
251
+ hash_file.read_text(encoding="utf-8", errors="replace")
252
+ if hash_file.exists()
253
+ else None
254
+ )
255
+
256
+ # now rezip the extracted files since we added the embedded json files
257
+ out_buffer = io.BytesIO()
258
+ with zipfile.ZipFile(
259
+ out_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
260
+ ) as out_zip:
261
+ for root, _, _files in os.walk(extract_path):
262
+ for file in _files:
263
+ file_path = Path(root) / file
264
+ relative_path = file_path.relative_to(extract_path)
265
+ out_zip.write(file_path, relative_path)
266
+
267
+ response_processing_time = time.time() - response_processing_start
268
+ diff_time = time.time() - start_time
269
+
270
+ # Create detailed timing breakdown
271
+ unaccounted_time = diff_time - (
272
+ zip_time + libfastled_time + sketch_time + response_processing_time
273
+ )
274
+ msg = f"Compilation success, took {diff_time:.2f} seconds\n"
275
+ msg += f" zip creation: {zip_time:.2f}\n"
276
+ if libfastled_time > 0:
277
+ msg += f" libfastled: {libfastled_time:.2f}\n"
278
+ msg += f" sketch compile + link: {sketch_time:.2f}\n"
279
+ msg += f" response processing: {response_processing_time:.2f}\n"
280
+ if unaccounted_time > 0.01: # Only show if significant
281
+ msg += f" other overhead: {unaccounted_time:.2f}"
282
+
283
+ _print_banner(msg)
284
+ return CompileResult(
285
+ success=True,
286
+ stdout=stdout,
287
+ hash_value=hash_value,
288
+ zip_bytes=out_buffer.getvalue(),
289
+ zip_time=zip_time,
290
+ libfastled_time=libfastled_time,
291
+ sketch_time=sketch_time,
292
+ response_processing_time=response_processing_time,
293
+ )
294
+
295
+
296
+ def _libcompiled_is_allowed(host: str) -> bool:
297
+ """Check if libcompiled is allowed for the given host."""
298
+ return "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
190
299
 
191
300
 
192
301
  def web_compile(
@@ -195,6 +304,8 @@ def web_compile(
195
304
  auth_token: str | None = None,
196
305
  build_mode: BuildMode | None = None,
197
306
  profile: bool = False,
307
+ no_platformio: bool = False,
308
+ allow_libcompile: bool = True,
198
309
  ) -> CompileResult:
199
310
  start_time = time.time()
200
311
  if isinstance(directory, str):
@@ -202,124 +313,156 @@ def web_compile(
202
313
  host = _sanitize_host(host or DEFAULT_HOST)
203
314
  build_mode = build_mode or BuildMode.QUICK
204
315
  _print_banner(f"Compiling on {host}")
205
- auth_token = auth_token or _AUTH_TOKEN
316
+ auth_token = auth_token or AUTH_TOKEN
206
317
  if not directory.exists():
207
318
  raise FileNotFoundError(f"Directory not found: {directory}")
208
- zip_result = zip_files(directory, build_mode=build_mode)
319
+
320
+ if allow_libcompile and not _libcompiled_is_allowed(host):
321
+ print(
322
+ f"{EMO('🚫', '[ERROR]')} libcompile is not allowed for host {host}, disabling."
323
+ )
324
+ allow_libcompile = False
325
+
326
+ # Time the zip creation
327
+ zip_start_time = time.time()
328
+ zip_result: ZipResult | Exception = zip_files(directory, build_mode=build_mode)
329
+ zip_time = time.time() - zip_start_time
330
+
209
331
  if isinstance(zip_result, Exception):
210
332
  return CompileResult(
211
- success=False, stdout=str(zip_result), hash_value=None, zip_bytes=b""
333
+ success=False,
334
+ stdout=str(zip_result),
335
+ hash_value=None,
336
+ zip_bytes=b"",
337
+ zip_time=zip_time,
338
+ libfastled_time=0.0, # No libfastled compilation in zip error case
339
+ sketch_time=0.0, # No sketch compilation in zip error case
340
+ response_processing_time=0.0, # No response processing in zip error case
212
341
  )
213
342
  zip_bytes = zip_result.zip_bytes
214
- archive_size = len(zip_bytes)
215
343
  print(f"Web compiling on {host}...")
344
+
345
+ # Track timing for each step
346
+ libfastled_time = 0.0
347
+ sketch_time = 0.0
348
+
216
349
  try:
217
- host = _sanitize_host(host)
218
- urls = [host]
219
- domain = host.split("://")[-1]
220
- if ":" not in domain:
221
- urls.append(f"{host}:{SERVER_PORT}")
222
-
223
- connection_result = find_good_connection(urls)
224
- if connection_result is None:
225
- _print_banner("Connection failed to all endpoints")
226
- return CompileResult(
227
- success=False,
228
- stdout="Connection failed",
229
- hash_value=None,
230
- zip_bytes=b"",
231
- )
232
-
233
- ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
234
- transport = (
235
- httpx.HTTPTransport(local_address="0.0.0.0")
236
- if connection_result.ipv4
237
- else None
350
+ # Step 1: Compile libfastled if requested
351
+ if allow_libcompile:
352
+ print("Step 1: Compiling libfastled...")
353
+ libfastled_start_time = time.time()
354
+ try:
355
+ libfastled_response = _compile_libfastled(host, auth_token, build_mode)
356
+
357
+ # Check HTTP response status first
358
+ if libfastled_response.status_code != 200:
359
+ msg = f"Error: libfastled compilation failed with HTTP status {libfastled_response.status_code}"
360
+
361
+ # Error out here, this is a critical error
362
+ stdout = libfastled_response.content
363
+ if stdout is not None:
364
+ stdout = stdout.decode("utf-8", errors="replace") + "\n" + msg
365
+ else:
366
+ stdout = msg
367
+ return CompileResult(
368
+ success=False,
369
+ stdout=stdout,
370
+ hash_value=None,
371
+ zip_bytes=b"",
372
+ zip_time=zip_time,
373
+ libfastled_time=libfastled_time,
374
+ sketch_time=0.0, # No sketch compilation when libfastled fails
375
+ response_processing_time=0.0, # No response processing when libfastled fails
376
+ )
377
+ else:
378
+ # Check for embedded HTTP status in response content
379
+ has_embedded_status, embedded_status = _check_embedded_http_status(
380
+ libfastled_response.content
381
+ )
382
+ if has_embedded_status:
383
+ if embedded_status is not None and embedded_status != 200:
384
+ msg = f"Warning: libfastled compilation failed with embedded status {embedded_status}"
385
+ # this is a critical error
386
+ stdout = libfastled_response.content
387
+ if stdout is not None:
388
+ stdout = (
389
+ stdout.decode("utf-8", errors="replace")
390
+ + "\n"
391
+ + msg
392
+ )
393
+ else:
394
+ stdout = msg
395
+ return CompileResult(
396
+ success=False,
397
+ stdout=stdout,
398
+ hash_value=None,
399
+ zip_bytes=b"",
400
+ zip_time=zip_time,
401
+ libfastled_time=libfastled_time,
402
+ sketch_time=0.0, # No sketch compilation when libfastled fails
403
+ response_processing_time=0.0, # No response processing when libfastled fails
404
+ )
405
+ # Continue with sketch compilation even if libfastled fails
406
+ elif embedded_status is None:
407
+ print(
408
+ "Warning: libfastled compilation returned malformed embedded status"
409
+ )
410
+ # Continue with sketch compilation even if libfastled fails
411
+ else:
412
+ print("✅ libfastled compilation successful")
413
+ else:
414
+ print("✅ libfastled compilation successful")
415
+ libfastled_time = time.time() - libfastled_start_time
416
+ except Exception as e:
417
+ libfastled_time = time.time() - libfastled_start_time
418
+ print(f"Warning: libfastled compilation failed: {e}")
419
+ # Continue with sketch compilation even if libfastled fails
420
+ else:
421
+ print("Step 1 (skipped): Compiling libfastled")
422
+
423
+ # Step 2: Compile the sketch
424
+ print("Step 2: Compiling sketch...")
425
+ sketch_start_time = time.time()
426
+ response = _send_compile_request(
427
+ host,
428
+ zip_bytes,
429
+ auth_token,
430
+ build_mode,
431
+ profile,
432
+ no_platformio,
433
+ )
434
+ sketch_time = time.time() - sketch_start_time
435
+
436
+ return _process_compile_response(
437
+ response, zip_result, start_time, zip_time, libfastled_time, sketch_time
238
438
  )
239
- with httpx.Client(
240
- transport=transport,
241
- timeout=_TIMEOUT,
242
- ) as client:
243
- headers = {
244
- "accept": "application/json",
245
- "authorization": auth_token,
246
- "build": (
247
- build_mode.value.lower()
248
- if build_mode
249
- else BuildMode.QUICK.value.lower()
250
- ),
251
- "profile": "true" if profile else "false",
252
- }
253
-
254
- url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
255
- print(f"Compiling on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes")
256
- files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
257
- response = client.post(
258
- url,
259
- follow_redirects=True,
260
- files=files,
261
- headers=headers,
262
- timeout=_TIMEOUT,
263
- )
264
-
265
- if response.status_code != 200:
266
- json_response = response.json()
267
- detail = json_response.get("detail", "Could not compile")
268
- return CompileResult(
269
- success=False, stdout=detail, hash_value=None, zip_bytes=b""
270
- )
271
-
272
- print(f"Response status code: {response}")
273
- # Create a temporary directory to extract the zip
274
- with tempfile.TemporaryDirectory() as extract_dir:
275
- extract_path = Path(extract_dir)
276
-
277
- # Write the response content to a temporary zip file
278
- temp_zip = extract_path / "response.zip"
279
- temp_zip.write_bytes(response.content)
280
-
281
- # Extract the zip
282
- shutil.unpack_archive(temp_zip, extract_path, "zip")
283
-
284
- if zip_result.zip_embedded_bytes:
285
- # extract the embedded bytes, which were not sent to the server
286
- temp_zip.write_bytes(zip_result.zip_embedded_bytes)
287
- shutil.unpack_archive(temp_zip, extract_path, "zip")
288
-
289
- # we don't need the temp zip anymore
290
- temp_zip.unlink()
291
-
292
- # Read stdout from out.txt if it exists
293
- stdout_file = extract_path / "out.txt"
294
- hash_file = extract_path / "hash.txt"
295
- stdout = stdout_file.read_text() if stdout_file.exists() else ""
296
- hash_value = hash_file.read_text() if hash_file.exists() else None
297
-
298
- # now rezip the extracted files since we added the embedded json files
299
- out_buffer = io.BytesIO()
300
- with zipfile.ZipFile(
301
- out_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
302
- ) as out_zip:
303
- for root, _, _files in os.walk(extract_path):
304
- for file in _files:
305
- file_path = Path(root) / file
306
- relative_path = file_path.relative_to(extract_path)
307
- out_zip.write(file_path, relative_path)
308
-
309
- diff_time = time.time() - start_time
310
- msg = f"Compilation success, took {diff_time:.2f} seconds"
311
- _print_banner(msg)
312
- return CompileResult(
313
- success=True,
314
- stdout=stdout,
315
- hash_value=hash_value,
316
- zip_bytes=out_buffer.getvalue(),
317
- )
439
+
318
440
  except KeyboardInterrupt:
319
441
  print("Keyboard interrupt")
320
442
  raise
443
+ except (ConnectionError, httpx.RemoteProtocolError, httpx.RequestError) as e:
444
+ # Handle connection and server disconnection issues
445
+ error_msg = f"Server connection error: {e}"
446
+ _print_banner(error_msg)
447
+ return CompileResult(
448
+ success=False,
449
+ stdout=error_msg,
450
+ hash_value=None,
451
+ zip_bytes=b"",
452
+ zip_time=zip_time,
453
+ libfastled_time=libfastled_time,
454
+ sketch_time=sketch_time,
455
+ response_processing_time=0.0,
456
+ )
321
457
  except httpx.HTTPError as e:
322
458
  print(f"Error: {e}")
323
459
  return CompileResult(
324
- success=False, stdout=str(e), hash_value=None, zip_bytes=b""
460
+ success=False,
461
+ stdout=str(e),
462
+ hash_value=None,
463
+ zip_bytes=b"",
464
+ zip_time=zip_time,
465
+ libfastled_time=libfastled_time,
466
+ sketch_time=sketch_time,
467
+ response_processing_time=0.0, # No response processing in HTTP error case
325
468
  )