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.
- fastled/__init__.py +30 -2
- fastled/__main__.py +14 -0
- fastled/__version__.py +1 -1
- fastled/app.py +51 -2
- fastled/args.py +33 -0
- fastled/client_server.py +188 -40
- fastled/compile_server.py +10 -0
- fastled/compile_server_impl.py +34 -1
- fastled/docker_manager.py +56 -14
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +6 -3
- 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/live_client.py +21 -1
- fastled/open_browser.py +84 -16
- fastled/parse_args.py +110 -9
- 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 -52
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -19
- fastled/server_flask.py +37 -1
- fastled/settings.py +47 -3
- fastled/sketch.py +121 -4
- fastled/string_diff.py +162 -26
- fastled/test/examples.py +7 -5
- fastled/types.py +4 -0
- fastled/util.py +34 -0
- fastled/version.py +41 -41
- fastled/web_compile.py +379 -236
- fastled/zip_files.py +76 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -508
- fastled-1.4.50.dist-info/RECORD +60 -0
- fastled-1.3.30.dist-info/RECORD +0 -44
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/WHEEL +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/sketch.py
CHANGED
|
@@ -25,10 +25,27 @@ def find_sketch_directories(directory: Path | None = None) -> list[Path]:
|
|
|
25
25
|
if looks_like_sketch_directory(path, quick=True):
|
|
26
26
|
sketch_directories.append(path)
|
|
27
27
|
if dir_name.lower() == "examples":
|
|
28
|
-
for
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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)
|
|
32
49
|
# make relative to cwd
|
|
33
50
|
sketch_directories = [p.relative_to(directory) for p in sketch_directories]
|
|
34
51
|
return sketch_directories
|
|
@@ -100,3 +117,103 @@ def looks_like_sketch_directory(directory: Path | str | None, quick=False) -> bo
|
|
|
100
117
|
if platformini_file:
|
|
101
118
|
return True
|
|
102
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
|
@@ -10,11 +10,14 @@ def _filter_out_obvious_bad_choices(
|
|
|
10
10
|
Filter out strings that are too different from the input string.
|
|
11
11
|
This is a heuristic and may not be perfect.
|
|
12
12
|
"""
|
|
13
|
-
|
|
13
|
+
if not input_str.strip(): # Handle empty input
|
|
14
|
+
return string_list
|
|
15
|
+
|
|
16
|
+
input_chars = set(input_str.lower())
|
|
14
17
|
filtered_list = []
|
|
15
18
|
for s in string_list:
|
|
16
19
|
# Check if at least half of the input characters are in the string
|
|
17
|
-
s_chars = set(s)
|
|
20
|
+
s_chars = set(s.lower())
|
|
18
21
|
common_chars = input_chars.intersection(s_chars)
|
|
19
22
|
if len(common_chars) >= len(input_chars) / 2:
|
|
20
23
|
filtered_list.append(s)
|
|
@@ -29,8 +32,8 @@ def is_in_order_match(input_str: str, other: str) -> bool:
|
|
|
29
32
|
"""
|
|
30
33
|
|
|
31
34
|
# Remove spaces from input string for matching
|
|
32
|
-
input_chars = [c for c in input_str if c != " "]
|
|
33
|
-
other_chars =
|
|
35
|
+
input_chars = [c.lower() for c in input_str if c != " "]
|
|
36
|
+
other_chars = [c.lower() for c in other]
|
|
34
37
|
input_index = 0
|
|
35
38
|
other_index = 0
|
|
36
39
|
while input_index < len(input_chars) and other_index < len(other_chars):
|
|
@@ -53,6 +56,14 @@ def string_diff(
|
|
|
53
56
|
def normalize(s: str) -> str:
|
|
54
57
|
return s.lower() if ignore_case else s
|
|
55
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
|
+
|
|
56
67
|
map_string: dict[str, str] = {}
|
|
57
68
|
|
|
58
69
|
if ignore_case:
|
|
@@ -60,38 +71,157 @@ def string_diff(
|
|
|
60
71
|
else:
|
|
61
72
|
map_string = {s: s for s in string_list}
|
|
62
73
|
|
|
74
|
+
original_string_list = string_list.copy()
|
|
63
75
|
if ignore_case:
|
|
64
76
|
string_list = [s.lower() for s in string_list]
|
|
65
77
|
input_string = input_string.lower()
|
|
66
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
|
+
|
|
67
186
|
# Apply set membership filtering for queries with 3+ characters
|
|
68
|
-
if len(input_string) >= 3:
|
|
69
|
-
|
|
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
|
|
70
191
|
|
|
71
192
|
# Second filter: exact substring filtering if applicable
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
80
201
|
|
|
81
202
|
# Third filter: in order exact match filtering if applicable.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
is_in_order = True
|
|
86
|
-
break
|
|
87
|
-
|
|
88
|
-
if is_in_order:
|
|
89
|
-
string_list = [s for s in string_list if is_in_order_match(input_string, s)]
|
|
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
|
|
90
206
|
|
|
207
|
+
# Calculate distances
|
|
91
208
|
distances: list[float] = []
|
|
92
209
|
for s in string_list:
|
|
93
210
|
dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
|
|
94
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
|
+
|
|
95
225
|
min_distance = min(distances)
|
|
96
226
|
out: list[tuple[float, str]] = []
|
|
97
227
|
for i, d in enumerate(distances):
|
|
@@ -106,10 +236,16 @@ def string_diff(
|
|
|
106
236
|
def string_diff_paths(
|
|
107
237
|
input_string: str | Path, path_list: list[Path], ignore_case=True
|
|
108
238
|
) -> list[tuple[float, Path]]:
|
|
109
|
-
|
|
110
|
-
|
|
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)
|
|
111
244
|
out: list[tuple[float, Path]] = []
|
|
112
245
|
for i, j in tmp:
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
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
fastled/util.py
CHANGED
|
@@ -52,3 +52,37 @@ def find_free_port(start_port: int, end_port: int) -> int | None:
|
|
|
52
52
|
f"No free port found in the range {start_port}-{end_port}. Using {start_port}."
|
|
53
53
|
)
|
|
54
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
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()
|