fastled 1.4.31__py3-none-any.whl → 1.4.33__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.
fastled/string_diff.py CHANGED
@@ -1,165 +1,165 @@
1
- from pathlib import Path
2
-
3
- from rapidfuzz import fuzz
4
-
5
-
6
- def _filter_out_obvious_bad_choices(
7
- input_str: str, string_list: list[str]
8
- ) -> list[str]:
9
- """
10
- Filter out strings that are too different from the input string.
11
- This is a heuristic and may not be perfect.
12
- """
13
- if not input_str.strip(): # Handle empty input
14
- return string_list
15
-
16
- input_chars = set(input_str.lower())
17
- filtered_list = []
18
- for s in string_list:
19
- # Check if at least half of the input characters are in the string
20
- s_chars = set(s.lower())
21
- common_chars = input_chars.intersection(s_chars)
22
- if len(common_chars) >= len(input_chars) / 2:
23
- filtered_list.append(s)
24
- return filtered_list
25
-
26
-
27
- def is_in_order_match(input_str: str, other: str) -> bool:
28
- """
29
- Check if the input string is an in-order match for any string in the list.
30
- An in-order match means that the characters of the input string appear
31
- in the same order in the string from the list, ignoring spaces in the input.
32
- """
33
-
34
- # Remove spaces from input string for matching
35
- input_chars = [c.lower() for c in input_str if c != " "]
36
- other_chars = [c.lower() for c in other]
37
- input_index = 0
38
- other_index = 0
39
- while input_index < len(input_chars) and other_index < len(other_chars):
40
- if input_chars[input_index] == other_chars[other_index]:
41
- input_index += 1
42
- other_index += 1
43
- # If we reached the end of the input string, it means all characters were found in order
44
- if input_index == len(input_chars):
45
- return True
46
- return False
47
-
48
-
49
- # Returns the min distance strings. If there is a tie, it returns
50
- # all the strings that have the same min distance.
51
- # Returns a tuple of index and string.
52
- def string_diff(
53
- input_string: str, string_list: list[str], ignore_case=True
54
- ) -> list[tuple[float, str]]:
55
-
56
- def normalize(s: str) -> str:
57
- return s.lower() if ignore_case else s
58
-
59
- # Handle empty input or empty list
60
- if not input_string.strip():
61
- # Return all strings with equal distance for empty input
62
- return [(i, s) for i, s in enumerate(string_list)]
63
-
64
- if not string_list:
65
- return []
66
-
67
- map_string: dict[str, str] = {}
68
-
69
- if ignore_case:
70
- map_string = {s.lower(): s for s in string_list}
71
- else:
72
- map_string = {s: s for s in string_list}
73
-
74
- original_string_list = string_list.copy()
75
- if ignore_case:
76
- string_list = [s.lower() for s in string_list]
77
- input_string = input_string.lower()
78
-
79
- # Check for exact matches, but also check if there are other substring matches
80
- exact_matches = [s for s in string_list if s == input_string]
81
- substring_matches = [s for s in string_list if input_string in s]
82
-
83
- # If there's an exact match AND other substring matches, return all substring matches
84
- # This provides better user experience for partial matching
85
- if exact_matches and len(substring_matches) > 1:
86
- out: list[tuple[float, str]] = []
87
- for i, s in enumerate(substring_matches):
88
- s_mapped = map_string.get(s, s)
89
- out.append((i, s_mapped))
90
- return out
91
-
92
- # If there's only an exact match and no other substring matches, return just the exact match
93
- if exact_matches and len(substring_matches) == 1:
94
- out: list[tuple[float, str]] = []
95
- for i, s in enumerate(exact_matches):
96
- s_mapped = map_string.get(s, s)
97
- out.append((i, s_mapped))
98
- return out
99
-
100
- # Apply set membership filtering for queries with 3+ characters
101
- if len(input_string.strip()) >= 3:
102
- filtered = _filter_out_obvious_bad_choices(input_string, string_list)
103
- if filtered: # Only apply filter if it doesn't eliminate everything
104
- string_list = filtered
105
-
106
- # Second filter: exact substring filtering if applicable
107
- if substring_matches:
108
- string_list = substring_matches
109
- # Return all substring matches
110
- out: list[tuple[float, str]] = []
111
- for i, s in enumerate(string_list):
112
- s_mapped = map_string.get(s, s)
113
- out.append((i, s_mapped))
114
- return out
115
-
116
- # Third filter: in order exact match filtering if applicable.
117
- in_order_matches = [s for s in string_list if is_in_order_match(input_string, s)]
118
- if in_order_matches:
119
- string_list = in_order_matches
120
-
121
- # Calculate distances
122
- distances: list[float] = []
123
- for s in string_list:
124
- dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
125
- distances.append(1.0 / (dist + 1.0))
126
-
127
- # Handle case where no strings remain after filtering
128
- if not distances:
129
- # Fall back to original list and calculate distances
130
- string_list = original_string_list.copy()
131
- if ignore_case:
132
- string_list = [s.lower() for s in string_list]
133
-
134
- distances = []
135
- for s in string_list:
136
- dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
137
- distances.append(1.0 / (dist + 1.0))
138
-
139
- min_distance = min(distances)
140
- out: list[tuple[float, str]] = []
141
- for i, d in enumerate(distances):
142
- if d == min_distance:
143
- s = string_list[i]
144
- s_mapped = map_string.get(s, s)
145
- out.append((i, s_mapped))
146
-
147
- return out
148
-
149
-
150
- def string_diff_paths(
151
- input_string: str | Path, path_list: list[Path], ignore_case=True
152
- ) -> list[tuple[float, Path]]:
153
- # Normalize path separators to forward slashes for consistent comparison
154
- string_list = [str(p).replace("\\", "/") for p in path_list]
155
- input_str = str(input_string).replace("\\", "/")
156
-
157
- tmp = string_diff(input_str, string_list, ignore_case)
158
- out: list[tuple[float, Path]] = []
159
- for i, j in tmp:
160
- # Find the original path that matches the normalized result
161
- for idx, orig_path in enumerate(path_list):
162
- if str(orig_path).replace("\\", "/") == j:
163
- out.append((i, orig_path))
164
- break
165
- return out
1
+ from pathlib import Path
2
+
3
+ from rapidfuzz import fuzz
4
+
5
+
6
+ def _filter_out_obvious_bad_choices(
7
+ input_str: str, string_list: list[str]
8
+ ) -> list[str]:
9
+ """
10
+ Filter out strings that are too different from the input string.
11
+ This is a heuristic and may not be perfect.
12
+ """
13
+ if not input_str.strip(): # Handle empty input
14
+ return string_list
15
+
16
+ input_chars = set(input_str.lower())
17
+ filtered_list = []
18
+ for s in string_list:
19
+ # Check if at least half of the input characters are in the string
20
+ s_chars = set(s.lower())
21
+ common_chars = input_chars.intersection(s_chars)
22
+ if len(common_chars) >= len(input_chars) / 2:
23
+ filtered_list.append(s)
24
+ return filtered_list
25
+
26
+
27
+ def is_in_order_match(input_str: str, other: str) -> bool:
28
+ """
29
+ Check if the input string is an in-order match for any string in the list.
30
+ An in-order match means that the characters of the input string appear
31
+ in the same order in the string from the list, ignoring spaces in the input.
32
+ """
33
+
34
+ # Remove spaces from input string for matching
35
+ input_chars = [c.lower() for c in input_str if c != " "]
36
+ other_chars = [c.lower() for c in other]
37
+ input_index = 0
38
+ other_index = 0
39
+ while input_index < len(input_chars) and other_index < len(other_chars):
40
+ if input_chars[input_index] == other_chars[other_index]:
41
+ input_index += 1
42
+ other_index += 1
43
+ # If we reached the end of the input string, it means all characters were found in order
44
+ if input_index == len(input_chars):
45
+ return True
46
+ return False
47
+
48
+
49
+ # Returns the min distance strings. If there is a tie, it returns
50
+ # all the strings that have the same min distance.
51
+ # Returns a tuple of index and string.
52
+ def string_diff(
53
+ input_string: str, string_list: list[str], ignore_case=True
54
+ ) -> list[tuple[float, str]]:
55
+
56
+ def normalize(s: str) -> str:
57
+ return s.lower() if ignore_case else s
58
+
59
+ # Handle empty input or empty list
60
+ if not input_string.strip():
61
+ # Return all strings with equal distance for empty input
62
+ return [(i, s) for i, s in enumerate(string_list)]
63
+
64
+ if not string_list:
65
+ return []
66
+
67
+ map_string: dict[str, str] = {}
68
+
69
+ if ignore_case:
70
+ map_string = {s.lower(): s for s in string_list}
71
+ else:
72
+ map_string = {s: s for s in string_list}
73
+
74
+ original_string_list = string_list.copy()
75
+ if ignore_case:
76
+ string_list = [s.lower() for s in string_list]
77
+ input_string = input_string.lower()
78
+
79
+ # Check for exact matches, but also check if there are other substring matches
80
+ exact_matches = [s for s in string_list if s == input_string]
81
+ substring_matches = [s for s in string_list if input_string in s]
82
+
83
+ # If there's an exact match AND other substring matches, return all substring matches
84
+ # This provides better user experience for partial matching
85
+ if exact_matches and len(substring_matches) > 1:
86
+ out: list[tuple[float, str]] = []
87
+ for i, s in enumerate(substring_matches):
88
+ s_mapped = map_string.get(s, s)
89
+ out.append((i, s_mapped))
90
+ return out
91
+
92
+ # If there's only an exact match and no other substring matches, return just the exact match
93
+ if exact_matches and len(substring_matches) == 1:
94
+ out: list[tuple[float, str]] = []
95
+ for i, s in enumerate(exact_matches):
96
+ s_mapped = map_string.get(s, s)
97
+ out.append((i, s_mapped))
98
+ return out
99
+
100
+ # Apply set membership filtering for queries with 3+ characters
101
+ if len(input_string.strip()) >= 3:
102
+ filtered = _filter_out_obvious_bad_choices(input_string, string_list)
103
+ if filtered: # Only apply filter if it doesn't eliminate everything
104
+ string_list = filtered
105
+
106
+ # Second filter: exact substring filtering if applicable
107
+ if substring_matches:
108
+ string_list = substring_matches
109
+ # Return all substring matches
110
+ out: list[tuple[float, str]] = []
111
+ for i, s in enumerate(string_list):
112
+ s_mapped = map_string.get(s, s)
113
+ out.append((i, s_mapped))
114
+ return out
115
+
116
+ # Third filter: in order exact match filtering if applicable.
117
+ in_order_matches = [s for s in string_list if is_in_order_match(input_string, s)]
118
+ if in_order_matches:
119
+ string_list = in_order_matches
120
+
121
+ # Calculate distances
122
+ distances: list[float] = []
123
+ for s in string_list:
124
+ dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
125
+ distances.append(1.0 / (dist + 1.0))
126
+
127
+ # Handle case where no strings remain after filtering
128
+ if not distances:
129
+ # Fall back to original list and calculate distances
130
+ string_list = original_string_list.copy()
131
+ if ignore_case:
132
+ string_list = [s.lower() for s in string_list]
133
+
134
+ distances = []
135
+ for s in string_list:
136
+ dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
137
+ distances.append(1.0 / (dist + 1.0))
138
+
139
+ min_distance = min(distances)
140
+ out: list[tuple[float, str]] = []
141
+ for i, d in enumerate(distances):
142
+ if d == min_distance:
143
+ s = string_list[i]
144
+ s_mapped = map_string.get(s, s)
145
+ out.append((i, s_mapped))
146
+
147
+ return out
148
+
149
+
150
+ def string_diff_paths(
151
+ input_string: str | Path, path_list: list[Path], ignore_case=True
152
+ ) -> list[tuple[float, Path]]:
153
+ # Normalize path separators to forward slashes for consistent comparison
154
+ string_list = [str(p).replace("\\", "/") for p in path_list]
155
+ input_str = str(input_string).replace("\\", "/")
156
+
157
+ tmp = string_diff(input_str, string_list, ignore_case)
158
+ out: list[tuple[float, Path]] = []
159
+ for i, j in tmp:
160
+ # Find the original path that matches the normalized result
161
+ for idx, orig_path in enumerate(path_list):
162
+ if str(orig_path).replace("\\", "/") == j:
163
+ out.append((i, orig_path))
164
+ break
165
+ return out
fastled/types.py CHANGED
@@ -12,6 +12,10 @@ class CompileResult:
12
12
  stdout: str
13
13
  hash_value: str | None
14
14
  zip_bytes: bytes
15
+ zip_time: float
16
+ libfastled_time: float
17
+ sketch_time: float
18
+ response_processing_time: float
15
19
 
16
20
  def __bool__(self) -> bool:
17
21
  return self.success
fastled/version.py CHANGED
@@ -1,41 +1,41 @@
1
- from concurrent.futures import Future, ThreadPoolExecutor
2
-
3
- import httpx
4
-
5
- from fastled.__version__ import __version_url_latest__
6
-
7
-
8
- def _fetch_version() -> str | Exception:
9
- """
10
- Helper function to fetch the latest version from the GitHub repository.
11
- """
12
- try:
13
- response = httpx.get(__version_url_latest__)
14
- response.raise_for_status()
15
- # Extract the version string from the response text
16
- version_line = response.text.split("__version__ = ")[1].split('"')[1]
17
- return version_line
18
- except Exception as e:
19
- return e
20
-
21
-
22
- def get_latest_version() -> Future[str | Exception]:
23
- """
24
- Fetch the latest version from the GitHub repository.
25
- Returns a future that will resolve with the version string or an exception.
26
- """
27
- executor = ThreadPoolExecutor()
28
- return executor.submit(_fetch_version)
29
-
30
-
31
- def unit_test() -> None:
32
- future = get_latest_version()
33
- latest_version = future.result() # Wait for the future to complete
34
- if isinstance(latest_version, Exception):
35
- print(f"Error fetching latest version: {latest_version}")
36
- else:
37
- print(f"Latest version: {latest_version}")
38
-
39
-
40
- if __name__ == "__main__":
41
- unit_test()
1
+ from concurrent.futures import Future, ThreadPoolExecutor
2
+
3
+ import httpx
4
+
5
+ from fastled.__version__ import __version_url_latest__
6
+
7
+
8
+ def _fetch_version() -> str | Exception:
9
+ """
10
+ Helper function to fetch the latest version from the GitHub repository.
11
+ """
12
+ try:
13
+ response = httpx.get(__version_url_latest__)
14
+ response.raise_for_status()
15
+ # Extract the version string from the response text
16
+ version_line = response.text.split("__version__ = ")[1].split('"')[1]
17
+ return version_line
18
+ except Exception as e:
19
+ return e
20
+
21
+
22
+ def get_latest_version() -> Future[str | Exception]:
23
+ """
24
+ Fetch the latest version from the GitHub repository.
25
+ Returns a future that will resolve with the version string or an exception.
26
+ """
27
+ executor = ThreadPoolExecutor()
28
+ return executor.submit(_fetch_version)
29
+
30
+
31
+ def unit_test() -> None:
32
+ future = get_latest_version()
33
+ latest_version = future.result() # Wait for the future to complete
34
+ if isinstance(latest_version, Exception):
35
+ print(f"Error fetching latest version: {latest_version}")
36
+ else:
37
+ print(f"Latest version: {latest_version}")
38
+
39
+
40
+ if __name__ == "__main__":
41
+ unit_test()
fastled/web_compile.py CHANGED
@@ -196,16 +196,30 @@ def _process_compile_response(
196
196
  response: httpx.Response,
197
197
  zip_result: ZipResult,
198
198
  start_time: float,
199
+ zip_time: float,
200
+ libfastled_time: float,
201
+ sketch_time: float,
199
202
  ) -> CompileResult:
200
203
  """Process the compile response and return the final result."""
201
204
  if response.status_code != 200:
202
205
  json_response = response.json()
203
206
  detail = json_response.get("detail", "Could not compile")
204
207
  return CompileResult(
205
- success=False, stdout=detail, hash_value=None, zip_bytes=b""
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
206
216
  )
207
217
 
208
218
  print(f"Response status code: {response}")
219
+
220
+ # Time the response processing
221
+ response_processing_start = time.time()
222
+
209
223
  # Create a temporary directory to extract the zip
210
224
  with tempfile.TemporaryDirectory() as extract_dir:
211
225
  extract_path = Path(extract_dir)
@@ -250,14 +264,32 @@ def _process_compile_response(
250
264
  relative_path = file_path.relative_to(extract_path)
251
265
  out_zip.write(file_path, relative_path)
252
266
 
267
+ response_processing_time = time.time() - response_processing_start
253
268
  diff_time = time.time() - start_time
254
- msg = f"Compilation success, took {diff_time:.2f} seconds"
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
+
255
283
  _print_banner(msg)
256
284
  return CompileResult(
257
285
  success=True,
258
286
  stdout=stdout,
259
287
  hash_value=hash_value,
260
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,
261
293
  )
262
294
 
263
295
 
@@ -279,17 +311,35 @@ def web_compile(
279
311
  auth_token = auth_token or AUTH_TOKEN
280
312
  if not directory.exists():
281
313
  raise FileNotFoundError(f"Directory not found: {directory}")
314
+
315
+ # Time the zip creation
316
+ zip_start_time = time.time()
282
317
  zip_result: ZipResult | Exception = zip_files(directory, build_mode=build_mode)
318
+ zip_time = time.time() - zip_start_time
319
+
283
320
  if isinstance(zip_result, Exception):
284
321
  return CompileResult(
285
- success=False, stdout=str(zip_result), hash_value=None, zip_bytes=b""
322
+ success=False,
323
+ stdout=str(zip_result),
324
+ hash_value=None,
325
+ zip_bytes=b"",
326
+ zip_time=zip_time,
327
+ libfastled_time=0.0, # No libfastled compilation in zip error case
328
+ sketch_time=0.0, # No sketch compilation in zip error case
329
+ response_processing_time=0.0, # No response processing in zip error case
286
330
  )
287
331
  zip_bytes = zip_result.zip_bytes
288
332
  print(f"Web compiling on {host}...")
333
+
334
+ # Track timing for each step
335
+ libfastled_time = 0.0
336
+ sketch_time = 0.0
337
+
289
338
  try:
290
339
  # Step 1: Compile libfastled if requested
291
340
  if allow_libcompile:
292
341
  print("Step 1: Compiling libfastled...")
342
+ libfastled_start_time = time.time()
293
343
  try:
294
344
  libfastled_response = _compile_libfastled(host, auth_token, build_mode)
295
345
 
@@ -308,6 +358,10 @@ def web_compile(
308
358
  stdout=stdout,
309
359
  hash_value=None,
310
360
  zip_bytes=b"",
361
+ zip_time=zip_time,
362
+ libfastled_time=libfastled_time,
363
+ sketch_time=0.0, # No sketch compilation when libfastled fails
364
+ response_processing_time=0.0, # No response processing when libfastled fails
311
365
  )
312
366
  else:
313
367
  # Check for embedded HTTP status in response content
@@ -332,6 +386,10 @@ def web_compile(
332
386
  stdout=stdout,
333
387
  hash_value=None,
334
388
  zip_bytes=b"",
389
+ zip_time=zip_time,
390
+ libfastled_time=libfastled_time,
391
+ sketch_time=0.0, # No sketch compilation when libfastled fails
392
+ response_processing_time=0.0, # No response processing when libfastled fails
335
393
  )
336
394
  # Continue with sketch compilation even if libfastled fails
337
395
  elif embedded_status is None:
@@ -343,7 +401,9 @@ def web_compile(
343
401
  print("✅ libfastled compilation successful")
344
402
  else:
345
403
  print("✅ libfastled compilation successful")
404
+ libfastled_time = time.time() - libfastled_start_time
346
405
  except Exception as e:
406
+ libfastled_time = time.time() - libfastled_start_time
347
407
  print(f"Warning: libfastled compilation failed: {e}")
348
408
  # Continue with sketch compilation even if libfastled fails
349
409
  else:
@@ -351,6 +411,7 @@ def web_compile(
351
411
 
352
412
  # Step 2: Compile the sketch
353
413
  print("Step 2: Compiling sketch...")
414
+ sketch_start_time = time.time()
354
415
  response = _send_compile_request(
355
416
  host,
356
417
  zip_bytes,
@@ -360,8 +421,11 @@ def web_compile(
360
421
  no_platformio,
361
422
  False, # allow_libcompile is always False since we handle it manually
362
423
  )
424
+ sketch_time = time.time() - sketch_start_time
363
425
 
364
- return _process_compile_response(response, zip_result, start_time)
426
+ return _process_compile_response(
427
+ response, zip_result, start_time, zip_time, libfastled_time, sketch_time
428
+ )
365
429
 
366
430
  except ConnectionError as e:
367
431
  _print_banner(str(e))
@@ -370,6 +434,10 @@ def web_compile(
370
434
  stdout=str(e),
371
435
  hash_value=None,
372
436
  zip_bytes=b"",
437
+ zip_time=zip_time,
438
+ libfastled_time=libfastled_time,
439
+ sketch_time=sketch_time,
440
+ response_processing_time=0.0, # No response processing in connection error case
373
441
  )
374
442
  except KeyboardInterrupt:
375
443
  print("Keyboard interrupt")
@@ -377,5 +445,12 @@ def web_compile(
377
445
  except httpx.HTTPError as e:
378
446
  print(f"Error: {e}")
379
447
  return CompileResult(
380
- success=False, stdout=str(e), hash_value=None, zip_bytes=b""
448
+ success=False,
449
+ stdout=str(e),
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, # No response processing in HTTP error case
381
456
  )