fastled 1.2.33__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.
- fastled/__init__.py +51 -192
- fastled/__main__.py +14 -0
- fastled/__version__.py +6 -0
- fastled/app.py +124 -27
- fastled/args.py +124 -0
- fastled/assets/localhost-key.pem +28 -0
- fastled/assets/localhost.pem +27 -0
- fastled/cli.py +10 -2
- fastled/cli_test.py +21 -0
- fastled/cli_test_interactive.py +21 -0
- fastled/client_server.py +334 -55
- fastled/compile_server.py +12 -1
- fastled/compile_server_impl.py +115 -42
- fastled/docker_manager.py +392 -69
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +100 -8
- fastled/find_good_connection.py +105 -0
- fastled/header_dump.py +63 -0
- fastled/install/__init__.py +1 -0
- fastled/install/examples_manager.py +62 -0
- fastled/install/extension_manager.py +113 -0
- fastled/install/main.py +156 -0
- fastled/install/project_detection.py +167 -0
- fastled/install/test_install.py +373 -0
- fastled/install/vscode_config.py +344 -0
- fastled/interruptible_http.py +148 -0
- fastled/keyboard.py +1 -0
- fastled/keyz.py +84 -0
- fastled/live_client.py +26 -1
- fastled/open_browser.py +133 -89
- fastled/parse_args.py +219 -15
- fastled/playwright/chrome_extension_downloader.py +207 -0
- fastled/playwright/playwright_browser.py +773 -0
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -0
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -17
- fastled/server_flask.py +487 -0
- fastled/server_start.py +21 -0
- fastled/settings.py +53 -4
- fastled/site/build.py +2 -10
- fastled/site/examples.py +10 -0
- fastled/sketch.py +129 -7
- fastled/string_diff.py +218 -9
- fastled/test/examples.py +7 -5
- fastled/types.py +22 -2
- fastled/util.py +78 -0
- fastled/version.py +41 -0
- fastled/web_compile.py +401 -218
- fastled/zip_files.py +76 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
- fastled-1.4.50.dist-info/RECORD +60 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
- fastled/open_browser2.py +0 -111
- fastled-1.2.33.dist-info/RECORD +0 -33
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/sketch.py
CHANGED
|
@@ -4,7 +4,9 @@ from pathlib import Path
|
|
|
4
4
|
_MAX_FILES_SEARCH_LIMIT = 10000
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def find_sketch_directories(directory: Path) -> list[Path]:
|
|
7
|
+
def find_sketch_directories(directory: Path | None = None) -> list[Path]:
|
|
8
|
+
if directory is None:
|
|
9
|
+
directory = Path(".")
|
|
8
10
|
file_count = 0
|
|
9
11
|
sketch_directories: list[Path] = []
|
|
10
12
|
# search all the paths one level deep
|
|
@@ -23,10 +25,27 @@ def find_sketch_directories(directory: Path) -> list[Path]:
|
|
|
23
25
|
if looks_like_sketch_directory(path, quick=True):
|
|
24
26
|
sketch_directories.append(path)
|
|
25
27
|
if dir_name.lower() == "examples":
|
|
26
|
-
for
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# Recursively search examples directory for sketch directories
|
|
29
|
+
def search_examples_recursive(
|
|
30
|
+
examples_path: Path, depth: int = 0, max_depth: int = 3
|
|
31
|
+
):
|
|
32
|
+
nonlocal file_count
|
|
33
|
+
if depth >= max_depth:
|
|
34
|
+
return
|
|
35
|
+
for example in examples_path.iterdir():
|
|
36
|
+
if example.is_dir():
|
|
37
|
+
if str(example.name).startswith("."):
|
|
38
|
+
continue
|
|
39
|
+
file_count += 1
|
|
40
|
+
if file_count > _MAX_FILES_SEARCH_LIMIT:
|
|
41
|
+
return
|
|
42
|
+
if looks_like_sketch_directory(example, quick=True):
|
|
43
|
+
sketch_directories.append(example)
|
|
44
|
+
else:
|
|
45
|
+
# Keep searching deeper if this isn't a sketch directory
|
|
46
|
+
search_examples_recursive(example, depth + 1, max_depth)
|
|
47
|
+
|
|
48
|
+
search_examples_recursive(path)
|
|
30
49
|
# make relative to cwd
|
|
31
50
|
sketch_directories = [p.relative_to(directory) for p in sketch_directories]
|
|
32
51
|
return sketch_directories
|
|
@@ -60,7 +79,7 @@ def get_sketch_files(directory: Path) -> list[Path]:
|
|
|
60
79
|
return files
|
|
61
80
|
|
|
62
81
|
|
|
63
|
-
def looks_like_fastled_repo(directory: Path) -> bool:
|
|
82
|
+
def looks_like_fastled_repo(directory: Path = Path(".")) -> bool:
|
|
64
83
|
libprops = directory / "library.properties"
|
|
65
84
|
if not libprops.exists():
|
|
66
85
|
return False
|
|
@@ -72,7 +91,10 @@ def _lots_and_lots_of_files(directory: Path) -> bool:
|
|
|
72
91
|
return len(get_sketch_files(directory)) > 100
|
|
73
92
|
|
|
74
93
|
|
|
75
|
-
def looks_like_sketch_directory(directory: Path, quick=False) -> bool:
|
|
94
|
+
def looks_like_sketch_directory(directory: Path | str | None, quick=False) -> bool:
|
|
95
|
+
if directory is None:
|
|
96
|
+
return False
|
|
97
|
+
directory = Path(directory)
|
|
76
98
|
if looks_like_fastled_repo(directory):
|
|
77
99
|
print("Directory looks like the FastLED repo")
|
|
78
100
|
return False
|
|
@@ -95,3 +117,103 @@ def looks_like_sketch_directory(directory: Path, quick=False) -> bool:
|
|
|
95
117
|
if platformini_file:
|
|
96
118
|
return True
|
|
97
119
|
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def find_sketch_by_partial_name(
|
|
123
|
+
partial_name: str, search_dir: Path | None = None
|
|
124
|
+
) -> Path | None:
|
|
125
|
+
"""
|
|
126
|
+
Find a sketch directory by partial name match.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
partial_name: Partial name to match against sketch directories
|
|
130
|
+
search_dir: Directory to search in (defaults to current directory)
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Path to the matching sketch directory, or None if no unique match found
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ValueError: If multiple matches are found with no clear best match, or no matches found
|
|
137
|
+
"""
|
|
138
|
+
if search_dir is None:
|
|
139
|
+
search_dir = Path(".")
|
|
140
|
+
|
|
141
|
+
# First, find all sketch directories
|
|
142
|
+
sketch_directories = find_sketch_directories(search_dir)
|
|
143
|
+
|
|
144
|
+
# Normalize the partial name to use forward slashes for cross-platform matching
|
|
145
|
+
partial_name_normalized = partial_name.replace("\\", "/").lower()
|
|
146
|
+
|
|
147
|
+
# Get the set of characters in the partial name for similarity check
|
|
148
|
+
partial_chars = set(partial_name_normalized)
|
|
149
|
+
|
|
150
|
+
# Find matches where the partial name appears in the path
|
|
151
|
+
matches = []
|
|
152
|
+
for sketch_dir in sketch_directories:
|
|
153
|
+
# Normalize the sketch directory path to use forward slashes
|
|
154
|
+
sketch_str_normalized = str(sketch_dir).replace("\\", "/").lower()
|
|
155
|
+
|
|
156
|
+
# Character similarity check: at least 50% of partial name chars must be in target
|
|
157
|
+
target_chars = set(sketch_str_normalized)
|
|
158
|
+
matching_chars = partial_chars & target_chars
|
|
159
|
+
similarity = (
|
|
160
|
+
len(matching_chars) / len(partial_chars) if len(partial_chars) > 0 else 0
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Check if partial_name matches the directory name or any part of the path
|
|
164
|
+
# AND has sufficient character similarity
|
|
165
|
+
if partial_name_normalized in sketch_str_normalized and similarity >= 0.5:
|
|
166
|
+
matches.append(sketch_dir)
|
|
167
|
+
|
|
168
|
+
if len(matches) == 0:
|
|
169
|
+
# Check if this is a total mismatch (low character similarity with all sketches)
|
|
170
|
+
all_low_similarity = True
|
|
171
|
+
for sketch_dir in sketch_directories:
|
|
172
|
+
sketch_str_normalized = str(sketch_dir).replace("\\", "/").lower()
|
|
173
|
+
target_chars = set(sketch_str_normalized)
|
|
174
|
+
matching_chars = partial_chars & target_chars
|
|
175
|
+
similarity = (
|
|
176
|
+
len(matching_chars) / len(partial_chars)
|
|
177
|
+
if len(partial_chars) > 0
|
|
178
|
+
else 0
|
|
179
|
+
)
|
|
180
|
+
if similarity > 0.5:
|
|
181
|
+
all_low_similarity = False
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
if all_low_similarity and len(sketch_directories) > 0:
|
|
185
|
+
# List all available sketches
|
|
186
|
+
sketches_str = "\n ".join(str(s) for s in sketch_directories)
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"'{partial_name}' does not look like any of the available sketches.\n\n"
|
|
189
|
+
f"Available sketches:\n {sketches_str}"
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
raise ValueError(f"No sketch directory found matching '{partial_name}'")
|
|
193
|
+
elif len(matches) == 1:
|
|
194
|
+
return matches[0]
|
|
195
|
+
else:
|
|
196
|
+
# Multiple matches - try to find the best match
|
|
197
|
+
# Best match criteria: exact match of the final directory name
|
|
198
|
+
exact_matches = []
|
|
199
|
+
for match in matches:
|
|
200
|
+
# Get the final directory name
|
|
201
|
+
final_dir_name = match.name.lower()
|
|
202
|
+
if final_dir_name == partial_name_normalized:
|
|
203
|
+
exact_matches.append(match)
|
|
204
|
+
|
|
205
|
+
if len(exact_matches) == 1:
|
|
206
|
+
# Found exactly one exact match - this is the best match
|
|
207
|
+
return exact_matches[0]
|
|
208
|
+
elif len(exact_matches) > 1:
|
|
209
|
+
# Multiple exact matches - still ambiguous
|
|
210
|
+
matches_str = "\n ".join(str(m) for m in exact_matches)
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"Multiple sketch directories found matching '{partial_name}':\n {matches_str}"
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
# No exact match - ambiguous partial matches
|
|
216
|
+
matches_str = "\n ".join(str(m) for m in matches)
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"Multiple sketch directories found matching '{partial_name}':\n {matches_str}"
|
|
219
|
+
)
|
fastled/string_diff.py
CHANGED
|
@@ -3,6 +3,49 @@ from pathlib import Path
|
|
|
3
3
|
from rapidfuzz import fuzz
|
|
4
4
|
|
|
5
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
|
+
|
|
6
49
|
# Returns the min distance strings. If there is a tie, it returns
|
|
7
50
|
# all the strings that have the same min distance.
|
|
8
51
|
# Returns a tuple of index and string.
|
|
@@ -13,19 +56,179 @@ def string_diff(
|
|
|
13
56
|
def normalize(s: str) -> str:
|
|
14
57
|
return s.lower() if ignore_case else s
|
|
15
58
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 exactly one exact match, and there are substring matches,
|
|
84
|
+
# check if we should prioritize the exact match or return all variants
|
|
85
|
+
if len(exact_matches) == 1 and len(substring_matches) > 1:
|
|
86
|
+
exact_match = exact_matches[0]
|
|
87
|
+
other_substring_matches = [s for s in substring_matches if s != exact_match]
|
|
88
|
+
|
|
89
|
+
# Prioritize exact match only if it appears at the start of other matches
|
|
90
|
+
# AND those matches have a camelCase boundary (indicating compound words)
|
|
91
|
+
# We need to use the original (non-lowercased) strings for camelCase detection
|
|
92
|
+
should_prioritize_exact = True
|
|
93
|
+
original_exact_match = map_string[exact_match] # Get the original casing
|
|
94
|
+
|
|
95
|
+
for other_match in other_substring_matches:
|
|
96
|
+
original_other_match = map_string[other_match] # Get the original casing
|
|
97
|
+
|
|
98
|
+
if not original_other_match.lower().startswith(
|
|
99
|
+
original_exact_match.lower()
|
|
100
|
+
):
|
|
101
|
+
# If the exact match isn't at the start, don't prioritize
|
|
102
|
+
should_prioritize_exact = False
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
# Check for camelCase boundary after the exact match in the ORIGINAL string
|
|
106
|
+
remainder = original_other_match[len(original_exact_match) :]
|
|
107
|
+
if remainder and remainder[0].isupper():
|
|
108
|
+
# Only prioritize exact match if the exact match is very short (4 chars or less)
|
|
109
|
+
# AND the remainder suggests a different concept
|
|
110
|
+
if len(original_exact_match) <= 4 and len(remainder) >= 6:
|
|
111
|
+
# This looks like a camelCase compound word (e.g., "wasm" -> "WasmScreenCoords")
|
|
112
|
+
continue
|
|
113
|
+
else:
|
|
114
|
+
# This looks like a variant (e.g., "Noise" -> "NoisePlayground", "Fire2012" -> "Fire2012WithPalette")
|
|
115
|
+
should_prioritize_exact = False
|
|
116
|
+
break
|
|
117
|
+
else:
|
|
118
|
+
# This looks like a variant/extension (e.g., "Blur" -> "Blur2d")
|
|
119
|
+
should_prioritize_exact = False
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
if should_prioritize_exact:
|
|
123
|
+
out: list[tuple[float, str]] = []
|
|
124
|
+
for i, s in enumerate(exact_matches):
|
|
125
|
+
s_mapped = map_string.get(s, s)
|
|
126
|
+
out.append((i, s_mapped))
|
|
127
|
+
return out
|
|
128
|
+
else:
|
|
129
|
+
# Apply character count filtering only for very specific compound terms
|
|
130
|
+
# Main criteria: contains numbers AND ends with numbers/letters (like Wave2d, Fire2012)
|
|
131
|
+
original_exact_match = map_string[exact_match]
|
|
132
|
+
should_apply_char_filter = (
|
|
133
|
+
len(original_exact_match) >= 5 # Longer terms
|
|
134
|
+
and any(c.isdigit() for c in original_exact_match) # Contains numbers
|
|
135
|
+
and (
|
|
136
|
+
original_exact_match[-1].isdigit()
|
|
137
|
+
or original_exact_match[-1].islower()
|
|
138
|
+
) # Ends specifically (compound pattern)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if should_apply_char_filter:
|
|
142
|
+
# Filter substring matches based on extra character count
|
|
143
|
+
# Use a more lenient threshold for shorter base terms
|
|
144
|
+
if len(original_exact_match) <= 6:
|
|
145
|
+
# For short terms, allow more extra chars (e.g., "Wave2d" + "FxWave2d")
|
|
146
|
+
MAX_EXTRA_CHARS = min(10, len(original_exact_match) * 2)
|
|
147
|
+
else:
|
|
148
|
+
# For longer terms, allow significant extensions (e.g., "Fire2012" + "Fire2012WithPalette")
|
|
149
|
+
MAX_EXTRA_CHARS = 12
|
|
150
|
+
|
|
151
|
+
filtered_matches = []
|
|
152
|
+
|
|
153
|
+
for s in substring_matches:
|
|
154
|
+
original_s = map_string[s]
|
|
155
|
+
if s == exact_match:
|
|
156
|
+
# Always include the exact match
|
|
157
|
+
filtered_matches.append(s)
|
|
158
|
+
else:
|
|
159
|
+
# Calculate extra characters
|
|
160
|
+
extra_chars = len(original_s) - len(original_exact_match)
|
|
161
|
+
if extra_chars <= MAX_EXTRA_CHARS:
|
|
162
|
+
filtered_matches.append(s)
|
|
163
|
+
|
|
164
|
+
# Return filtered matches
|
|
165
|
+
out: list[tuple[float, str]] = []
|
|
166
|
+
for i, s in enumerate(filtered_matches):
|
|
167
|
+
s_mapped = map_string.get(s, s) or s
|
|
168
|
+
out.append((i, s_mapped))
|
|
169
|
+
return out
|
|
170
|
+
else:
|
|
171
|
+
# Return all substring matches (original behavior for base terms)
|
|
172
|
+
out: list[tuple[float, str]] = []
|
|
173
|
+
for i, s in enumerate(substring_matches):
|
|
174
|
+
s_mapped = map_string.get(s, s) or s
|
|
175
|
+
out.append((i, s_mapped))
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
# If there's only an exact match and no other substring matches, return just the exact match
|
|
179
|
+
if exact_matches and len(substring_matches) == 1:
|
|
180
|
+
out: list[tuple[float, str]] = []
|
|
181
|
+
for i, s in enumerate(exact_matches):
|
|
182
|
+
s_mapped = map_string.get(s, s)
|
|
183
|
+
out.append((i, s_mapped))
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
# Apply set membership filtering for queries with 3+ characters
|
|
187
|
+
if len(input_string.strip()) >= 3:
|
|
188
|
+
filtered = _filter_out_obvious_bad_choices(input_string, string_list)
|
|
189
|
+
if filtered: # Only apply filter if it doesn't eliminate everything
|
|
190
|
+
string_list = filtered
|
|
191
|
+
|
|
192
|
+
# Second filter: exact substring filtering if applicable
|
|
193
|
+
if substring_matches:
|
|
194
|
+
string_list = substring_matches
|
|
195
|
+
# Return all substring matches
|
|
196
|
+
out: list[tuple[float, str]] = []
|
|
197
|
+
for i, s in enumerate(string_list):
|
|
198
|
+
s_mapped = map_string.get(s, s)
|
|
199
|
+
out.append((i, s_mapped))
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
# Third filter: in order exact match filtering if applicable.
|
|
203
|
+
in_order_matches = [s for s in string_list if is_in_order_match(input_string, s)]
|
|
204
|
+
if in_order_matches:
|
|
205
|
+
string_list = in_order_matches
|
|
206
|
+
|
|
207
|
+
# Calculate distances
|
|
20
208
|
distances: list[float] = []
|
|
21
209
|
for s in string_list:
|
|
22
210
|
dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
|
|
23
211
|
distances.append(1.0 / (dist + 1.0))
|
|
212
|
+
|
|
213
|
+
# Handle case where no strings remain after filtering
|
|
214
|
+
if not distances:
|
|
215
|
+
# Fall back to original list and calculate distances
|
|
216
|
+
string_list = original_string_list.copy()
|
|
217
|
+
if ignore_case:
|
|
218
|
+
string_list = [s.lower() for s in string_list]
|
|
219
|
+
|
|
220
|
+
distances = []
|
|
221
|
+
for s in string_list:
|
|
222
|
+
dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
|
|
223
|
+
distances.append(1.0 / (dist + 1.0))
|
|
224
|
+
|
|
24
225
|
min_distance = min(distances)
|
|
25
226
|
out: list[tuple[float, str]] = []
|
|
26
227
|
for i, d in enumerate(distances):
|
|
27
228
|
if d == min_distance:
|
|
28
|
-
|
|
229
|
+
s = string_list[i]
|
|
230
|
+
s_mapped = map_string.get(s, s)
|
|
231
|
+
out.append((i, s_mapped))
|
|
29
232
|
|
|
30
233
|
return out
|
|
31
234
|
|
|
@@ -33,10 +236,16 @@ def string_diff(
|
|
|
33
236
|
def string_diff_paths(
|
|
34
237
|
input_string: str | Path, path_list: list[Path], ignore_case=True
|
|
35
238
|
) -> list[tuple[float, Path]]:
|
|
36
|
-
|
|
37
|
-
|
|
239
|
+
# Normalize path separators to forward slashes for consistent comparison
|
|
240
|
+
string_list = [str(p).replace("\\", "/") for p in path_list]
|
|
241
|
+
input_str = str(input_string).replace("\\", "/")
|
|
242
|
+
|
|
243
|
+
tmp = string_diff(input_str, string_list, ignore_case)
|
|
38
244
|
out: list[tuple[float, Path]] = []
|
|
39
245
|
for i, j in tmp:
|
|
40
|
-
|
|
41
|
-
|
|
246
|
+
# Find the original path that matches the normalized result
|
|
247
|
+
for idx, orig_path in enumerate(path_list):
|
|
248
|
+
if str(orig_path).replace("\\", "/") == j:
|
|
249
|
+
out.append((i, orig_path))
|
|
250
|
+
break
|
|
42
251
|
return out
|
fastled/test/examples.py
CHANGED
|
@@ -2,6 +2,8 @@ from tempfile import TemporaryDirectory
|
|
|
2
2
|
from time import time
|
|
3
3
|
from warnings import warn
|
|
4
4
|
|
|
5
|
+
from fastled.emoji_util import safe_print
|
|
6
|
+
|
|
5
7
|
_FILTER = True
|
|
6
8
|
|
|
7
9
|
|
|
@@ -18,21 +20,21 @@ def test_examples(
|
|
|
18
20
|
examples.remove("LuminescentGrand")
|
|
19
21
|
with TemporaryDirectory() as tmpdir:
|
|
20
22
|
for example in examples:
|
|
21
|
-
|
|
23
|
+
safe_print(f"Initializing example: {example}")
|
|
22
24
|
try:
|
|
23
25
|
sketch_dir = Api.project_init(example, outputdir=tmpdir, host=host)
|
|
24
26
|
except Exception as e:
|
|
25
27
|
warn(f"Failed to initialize example: {example}, error: {e}")
|
|
26
28
|
out[example] = e
|
|
27
29
|
continue
|
|
28
|
-
|
|
30
|
+
safe_print(f"Project initialized at: {sketch_dir}")
|
|
29
31
|
start = time()
|
|
30
|
-
|
|
32
|
+
safe_print(f"Compiling example: {example}")
|
|
31
33
|
diff = time() - start
|
|
32
|
-
|
|
34
|
+
safe_print(f"Compilation took: {diff:.2f} seconds")
|
|
33
35
|
result = Api.web_compile(sketch_dir, host=host)
|
|
34
36
|
if not result.success:
|
|
35
|
-
|
|
37
|
+
safe_print(f"Compilation failed for {example}: {result.stdout}")
|
|
36
38
|
out[example] = Exception(result.stdout)
|
|
37
39
|
return out
|
|
38
40
|
|
fastled/types.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import argparse
|
|
2
1
|
from dataclasses import dataclass
|
|
3
2
|
from enum import Enum
|
|
4
3
|
from typing import Any
|
|
5
4
|
|
|
5
|
+
from fastled.args import Args
|
|
6
|
+
from fastled.print_filter import PrintFilterDefault
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
@dataclass
|
|
8
10
|
class CompileResult:
|
|
@@ -10,6 +12,10 @@ class CompileResult:
|
|
|
10
12
|
stdout: str
|
|
11
13
|
hash_value: str | None
|
|
12
14
|
zip_bytes: bytes
|
|
15
|
+
zip_time: float
|
|
16
|
+
libfastled_time: float
|
|
17
|
+
sketch_time: float
|
|
18
|
+
response_processing_time: float
|
|
13
19
|
|
|
14
20
|
def __bool__(self) -> bool:
|
|
15
21
|
return self.success
|
|
@@ -17,6 +23,11 @@ class CompileResult:
|
|
|
17
23
|
def to_dict(self) -> dict[str, Any]:
|
|
18
24
|
return self.__dict__.copy()
|
|
19
25
|
|
|
26
|
+
def __post_init__(self):
|
|
27
|
+
# Filter the stdout.
|
|
28
|
+
pf = PrintFilterDefault(echo=False)
|
|
29
|
+
self.stdout = pf.print(self.stdout)
|
|
30
|
+
|
|
20
31
|
|
|
21
32
|
class CompileServerError(Exception):
|
|
22
33
|
"""Error class for failing to instantiate CompileServer."""
|
|
@@ -38,7 +49,7 @@ class BuildMode(Enum):
|
|
|
38
49
|
raise ValueError(f"BUILD_MODE must be one of {valid_modes}, got {mode_str}")
|
|
39
50
|
|
|
40
51
|
@staticmethod
|
|
41
|
-
def from_args(args:
|
|
52
|
+
def from_args(args: Args) -> "BuildMode":
|
|
42
53
|
if args.debug:
|
|
43
54
|
return BuildMode.DEBUG
|
|
44
55
|
elif args.release:
|
|
@@ -59,3 +70,12 @@ class Platform(Enum):
|
|
|
59
70
|
raise ValueError(
|
|
60
71
|
f"Platform must be one of {valid_modes}, got {platform_str}"
|
|
61
72
|
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class FileResponse:
|
|
77
|
+
"""File response from the server."""
|
|
78
|
+
|
|
79
|
+
filename: str
|
|
80
|
+
content: str
|
|
81
|
+
mimetype: str
|
fastled/util.py
CHANGED
|
@@ -8,3 +8,81 @@ def hash_file(file_path: Path) -> str:
|
|
|
8
8
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
9
9
|
hasher.update(chunk)
|
|
10
10
|
return hasher.hexdigest()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def banner_string(msg: str) -> str:
|
|
14
|
+
lines = msg.splitlines()
|
|
15
|
+
max_length = max(len(line) for line in lines)
|
|
16
|
+
border = "#" * (max_length + 4)
|
|
17
|
+
out: list[str] = []
|
|
18
|
+
out.append(border)
|
|
19
|
+
for line in lines:
|
|
20
|
+
out.append(f"# {line} " + " " * (max_length - len(line)) + "#")
|
|
21
|
+
out.append(border)
|
|
22
|
+
outstr = "\n".join(out)
|
|
23
|
+
return f"\n{outstr}\n"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def print_banner(msg: str) -> None:
|
|
27
|
+
"""Print a message in a banner format."""
|
|
28
|
+
print(banner_string(msg))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def port_is_free(port: int) -> bool:
|
|
32
|
+
"""Check if a port is free."""
|
|
33
|
+
import socket
|
|
34
|
+
|
|
35
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
36
|
+
try:
|
|
37
|
+
_ = sock.bind(("localhost", port)) and sock.bind(("0.0.0.0", port))
|
|
38
|
+
return True
|
|
39
|
+
except OSError:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def find_free_port(start_port: int, end_port: int) -> int | None:
|
|
44
|
+
"""Find a free port on the system."""
|
|
45
|
+
|
|
46
|
+
for port in range(start_port, end_port):
|
|
47
|
+
if port_is_free(port):
|
|
48
|
+
return port
|
|
49
|
+
import warnings
|
|
50
|
+
|
|
51
|
+
warnings.warn(
|
|
52
|
+
f"No free port found in the range {start_port}-{end_port}. Using {start_port}."
|
|
53
|
+
)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def download_emsdk_headers(base_url: str, filepath: Path) -> None:
|
|
58
|
+
"""Download EMSDK headers from the specified URL and save to filepath.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
base_url: Base URL of the server (e.g., 'http://localhost:8080')
|
|
62
|
+
filepath: Path where to save the headers ZIP file (must end with .zip)
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If filepath doesn't end with .zip
|
|
66
|
+
RuntimeError: If download fails or server returns error
|
|
67
|
+
"""
|
|
68
|
+
if not str(filepath).endswith(".zip"):
|
|
69
|
+
raise ValueError("Filepath must end with .zip")
|
|
70
|
+
|
|
71
|
+
import httpx
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
timeout = httpx.Timeout(30.0, read=30.0)
|
|
75
|
+
with httpx.stream(
|
|
76
|
+
"GET", f"{base_url}/headers/emsdk", timeout=timeout
|
|
77
|
+
) as response:
|
|
78
|
+
if response.status_code == 200:
|
|
79
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
with open(filepath, "wb") as f:
|
|
81
|
+
for chunk in response.iter_bytes(chunk_size=512000):
|
|
82
|
+
f.write(chunk)
|
|
83
|
+
else:
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
f"Failed to get EMSDK headers: HTTP {response.status_code}"
|
|
86
|
+
)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
raise RuntimeError(f"Error downloading EMSDK headers: {e}") from e
|
fastled/version.py
ADDED
|
@@ -0,0 +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()
|