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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resize tracking for Playwright browser integration.
|
|
3
|
+
|
|
4
|
+
This module provides a class to track browser window resize events and adjust
|
|
5
|
+
the viewport accordingly without using internal polling loops.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ResizeTracker:
|
|
12
|
+
"""Tracks browser window resize events and adjusts viewport accordingly."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, page: Any):
|
|
15
|
+
"""Initialize the resize tracker.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
page: The Playwright page object to track
|
|
19
|
+
"""
|
|
20
|
+
self.page = page
|
|
21
|
+
self.last_outer_size: tuple[int, int] | None = None
|
|
22
|
+
|
|
23
|
+
async def _get_window_info(self) -> dict[str, int] | None:
|
|
24
|
+
"""Get browser window dimensions information.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dictionary containing window dimensions or None if unable to retrieve
|
|
28
|
+
"""
|
|
29
|
+
if self.page is None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
return await self.page.evaluate(
|
|
34
|
+
"""
|
|
35
|
+
() => {
|
|
36
|
+
return {
|
|
37
|
+
outerWidth: window.outerWidth,
|
|
38
|
+
outerHeight: window.outerHeight,
|
|
39
|
+
innerWidth: window.innerWidth,
|
|
40
|
+
innerHeight: window.innerHeight,
|
|
41
|
+
contentWidth: document.documentElement.clientWidth,
|
|
42
|
+
contentHeight: document.documentElement.clientHeight
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
)
|
|
47
|
+
except Exception:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
async def set_viewport_size(self, width: int, height: int) -> None:
|
|
51
|
+
"""Set the viewport size.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
width: The viewport width
|
|
55
|
+
height: The viewport height
|
|
56
|
+
"""
|
|
57
|
+
if self.page is None:
|
|
58
|
+
raise Exception("Page is None")
|
|
59
|
+
|
|
60
|
+
await self.page.set_viewport_size({"width": width, "height": height})
|
|
61
|
+
|
|
62
|
+
async def update(self) -> Exception | None:
|
|
63
|
+
"""Update the resize tracking and adjust viewport if needed.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
None if successful, Exception if an error occurred
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
# Check if page is still alive
|
|
70
|
+
if self.page is None or self.page.is_closed():
|
|
71
|
+
return Exception("Page closed")
|
|
72
|
+
|
|
73
|
+
window_info = await self._get_window_info()
|
|
74
|
+
|
|
75
|
+
if window_info:
|
|
76
|
+
current_outer = (
|
|
77
|
+
window_info["outerWidth"],
|
|
78
|
+
window_info["outerHeight"],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Check if window size changed
|
|
82
|
+
if (
|
|
83
|
+
self.last_outer_size is None
|
|
84
|
+
or current_outer != self.last_outer_size
|
|
85
|
+
):
|
|
86
|
+
|
|
87
|
+
if self.last_outer_size is not None:
|
|
88
|
+
print("[PYTHON] *** BROWSER WINDOW RESIZED ***")
|
|
89
|
+
print(
|
|
90
|
+
f"[PYTHON] Outer window changed from {self.last_outer_size[0]}x{self.last_outer_size[1]} to {current_outer[0]}x{current_outer[1]}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.last_outer_size = current_outer
|
|
94
|
+
|
|
95
|
+
# Set viewport to match the outer window size
|
|
96
|
+
outer_width = int(window_info["outerWidth"])
|
|
97
|
+
outer_height = int(window_info["outerHeight"])
|
|
98
|
+
|
|
99
|
+
print(
|
|
100
|
+
f"[PYTHON] Setting viewport to match outer window size: {outer_width}x{outer_height}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
await self.set_viewport_size(outer_width, outer_height)
|
|
104
|
+
print("[PYTHON] Viewport set successfully")
|
|
105
|
+
|
|
106
|
+
# Query the actual window dimensions after the viewport change
|
|
107
|
+
updated_window_info = await self._get_window_info()
|
|
108
|
+
|
|
109
|
+
if updated_window_info:
|
|
110
|
+
print(f"[PYTHON] Updated window info: {updated_window_info}")
|
|
111
|
+
|
|
112
|
+
# Update our tracking with the actual final outer size
|
|
113
|
+
self.last_outer_size = (
|
|
114
|
+
updated_window_info["outerWidth"],
|
|
115
|
+
updated_window_info["outerHeight"],
|
|
116
|
+
)
|
|
117
|
+
print(
|
|
118
|
+
f"[PYTHON] Updated last_outer_size to actual final size: {self.last_outer_size}"
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
print("[PYTHON] Could not get updated window info")
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return e
|
|
125
|
+
|
|
126
|
+
# Success case
|
|
127
|
+
return None
|
fastled/print_filter.py
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from abc import ABC, abstractmethod
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class PrintFilter(ABC):
|
|
7
|
-
"""Abstract base class for filtering text output."""
|
|
8
|
-
|
|
9
|
-
def __init__(self, echo: bool = True) -> None:
|
|
10
|
-
self.echo = echo
|
|
11
|
-
|
|
12
|
-
@abstractmethod
|
|
13
|
-
def filter(self, text: str) -> str:
|
|
14
|
-
"""Filter the text according to implementation-specific rules."""
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
def print(self, text: str | bytes) -> str:
|
|
18
|
-
"""Prints the text to the console after filtering."""
|
|
19
|
-
if isinstance(text, bytes):
|
|
20
|
-
text = text.decode("utf-8")
|
|
21
|
-
text = self.filter(text)
|
|
22
|
-
if self.echo:
|
|
23
|
-
print(text, end="")
|
|
24
|
-
return text
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _handle_ino_cpp(line: str) -> str:
|
|
28
|
-
if ".ino.cpp" in line[0:30]:
|
|
29
|
-
# Extract the filename without path and extension
|
|
30
|
-
match = re.search(r"src/([^/]+)\.ino\.cpp", line)
|
|
31
|
-
if match:
|
|
32
|
-
filename = match.group(1)
|
|
33
|
-
# Replace with examples/Filename/Filename.ino format
|
|
34
|
-
line = line.replace(
|
|
35
|
-
f"src/{filename}.ino.cpp", f"examples/{filename}/{filename}.ino"
|
|
36
|
-
)
|
|
37
|
-
else:
|
|
38
|
-
# Fall back to simple extension replacement if regex doesn't match
|
|
39
|
-
line = line.replace(".ino.cpp", ".ino")
|
|
40
|
-
return line
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class PrintFilterDefault(PrintFilter):
|
|
44
|
-
"""Provides default filtering for FastLED output."""
|
|
45
|
-
|
|
46
|
-
def filter(self, text: str) -> str:
|
|
47
|
-
return text
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class CompileOrLink(Enum):
|
|
51
|
-
COMPILE = "compile"
|
|
52
|
-
LINK = "link"
|
|
1
|
+
import re
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PrintFilter(ABC):
|
|
7
|
+
"""Abstract base class for filtering text output."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, echo: bool = True) -> None:
|
|
10
|
+
self.echo = echo
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def filter(self, text: str) -> str:
|
|
14
|
+
"""Filter the text according to implementation-specific rules."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def print(self, text: str | bytes) -> str:
|
|
18
|
+
"""Prints the text to the console after filtering."""
|
|
19
|
+
if isinstance(text, bytes):
|
|
20
|
+
text = text.decode("utf-8")
|
|
21
|
+
text = self.filter(text)
|
|
22
|
+
if self.echo:
|
|
23
|
+
print(text, end="")
|
|
24
|
+
return text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _handle_ino_cpp(line: str) -> str:
|
|
28
|
+
if ".ino.cpp" in line[0:30]:
|
|
29
|
+
# Extract the filename without path and extension
|
|
30
|
+
match = re.search(r"src/([^/]+)\.ino\.cpp", line)
|
|
31
|
+
if match:
|
|
32
|
+
filename = match.group(1)
|
|
33
|
+
# Replace with examples/Filename/Filename.ino format
|
|
34
|
+
line = line.replace(
|
|
35
|
+
f"src/{filename}.ino.cpp", f"examples/{filename}/{filename}.ino"
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
# Fall back to simple extension replacement if regex doesn't match
|
|
39
|
+
line = line.replace(".ino.cpp", ".ino")
|
|
40
|
+
return line
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PrintFilterDefault(PrintFilter):
|
|
44
|
+
"""Provides default filtering for FastLED output."""
|
|
45
|
+
|
|
46
|
+
def filter(self, text: str) -> str:
|
|
47
|
+
return text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CompileOrLink(Enum):
|
|
51
|
+
COMPILE = "compile"
|
|
52
|
+
LINK = "link"
|
fastled/project_init.py
CHANGED
|
@@ -22,20 +22,27 @@ def get_examples(host: str | None = None) -> list[str]:
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _prompt_for_example() -> str:
|
|
25
|
+
from fastled.select_sketch_directory import _disambiguate_user_choice
|
|
26
|
+
|
|
25
27
|
examples = get_examples()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
|
|
29
|
+
# Find default example index (prefer DEFAULT_EXAMPLE if it exists)
|
|
30
|
+
default_index = 0
|
|
31
|
+
if DEFAULT_EXAMPLE in examples:
|
|
32
|
+
default_index = examples.index(DEFAULT_EXAMPLE)
|
|
33
|
+
|
|
34
|
+
result = _disambiguate_user_choice(
|
|
35
|
+
examples,
|
|
36
|
+
option_to_str=lambda x: x,
|
|
37
|
+
prompt="Available examples:",
|
|
38
|
+
default_index=default_index,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if result is None:
|
|
42
|
+
# Fallback to DEFAULT_EXAMPLE if user cancelled
|
|
43
|
+
return DEFAULT_EXAMPLE
|
|
44
|
+
|
|
45
|
+
return result
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
class DownloadThread(threading.Thread):
|
|
@@ -1,10 +1,134 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from typing import Callable, TypeVar, Union
|
|
2
3
|
|
|
3
|
-
from fastled.string_diff import
|
|
4
|
+
from fastled.string_diff import string_diff
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _disambiguate_user_choice(
|
|
10
|
+
options: list[T],
|
|
11
|
+
option_to_str: Callable[[T], str] = str,
|
|
12
|
+
prompt: str = "Multiple matches found. Please choose:",
|
|
13
|
+
default_index: int = 0,
|
|
14
|
+
) -> Union[T, None]:
|
|
15
|
+
"""
|
|
16
|
+
Present multiple options to the user with a default selection.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
options: List of options to choose from
|
|
20
|
+
option_to_str: Function to convert option to display string
|
|
21
|
+
prompt: Prompt message to show user
|
|
22
|
+
default_index: Index of the default option (0-based)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Selected option or None if cancelled
|
|
26
|
+
"""
|
|
27
|
+
if not options:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
if len(options) == 1:
|
|
31
|
+
return options[0]
|
|
32
|
+
|
|
33
|
+
# Ensure default_index is valid
|
|
34
|
+
if default_index < 0 or default_index >= len(options):
|
|
35
|
+
default_index = 0
|
|
36
|
+
|
|
37
|
+
print(f"\n{prompt}")
|
|
38
|
+
for i, option in enumerate(options):
|
|
39
|
+
option_str = option_to_str(option)
|
|
40
|
+
if i == default_index:
|
|
41
|
+
print(f" [{i+1}]: [{option_str}]") # Default option shown in brackets
|
|
42
|
+
else:
|
|
43
|
+
print(f" [{i+1}]: {option_str}")
|
|
44
|
+
|
|
45
|
+
default_option_str = option_to_str(options[default_index])
|
|
46
|
+
user_input = input(
|
|
47
|
+
f"\nEnter number or name (default: [{default_option_str}]): "
|
|
48
|
+
).strip()
|
|
49
|
+
|
|
50
|
+
# Handle empty input - select default
|
|
51
|
+
if not user_input:
|
|
52
|
+
return options[default_index]
|
|
53
|
+
|
|
54
|
+
# Try to parse as number
|
|
55
|
+
try:
|
|
56
|
+
index = int(user_input) - 1
|
|
57
|
+
if 0 <= index < len(options):
|
|
58
|
+
return options[index]
|
|
59
|
+
except ValueError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# Try to match by name (case insensitive)
|
|
63
|
+
user_input_lower = user_input.lower()
|
|
64
|
+
for option in options:
|
|
65
|
+
option_str = option_to_str(option).lower()
|
|
66
|
+
if option_str == user_input_lower:
|
|
67
|
+
return option
|
|
68
|
+
|
|
69
|
+
# Try partial match
|
|
70
|
+
matches = []
|
|
71
|
+
for option in options:
|
|
72
|
+
option_str = option_to_str(option)
|
|
73
|
+
if user_input_lower in option_str.lower():
|
|
74
|
+
matches.append(option)
|
|
75
|
+
|
|
76
|
+
if len(matches) == 1:
|
|
77
|
+
return matches[0]
|
|
78
|
+
elif len(matches) > 1:
|
|
79
|
+
# Recursive disambiguation with the filtered matches
|
|
80
|
+
return _disambiguate_user_choice(
|
|
81
|
+
matches,
|
|
82
|
+
option_to_str,
|
|
83
|
+
f"Multiple partial matches for '{user_input}':",
|
|
84
|
+
0, # Reset default to first match
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Try fuzzy matching as fallback
|
|
88
|
+
# For better fuzzy matching on paths, extract just the last component (basename)
|
|
89
|
+
# to avoid the "examples/" prefix interfering with matching
|
|
90
|
+
from pathlib import Path as PathLib
|
|
91
|
+
|
|
92
|
+
option_basenames = []
|
|
93
|
+
for option in options:
|
|
94
|
+
option_str = option_to_str(option)
|
|
95
|
+
# Extract basename for fuzzy matching
|
|
96
|
+
basename = (
|
|
97
|
+
PathLib(option_str).name
|
|
98
|
+
if "/" in option_str or "\\" in option_str
|
|
99
|
+
else option_str
|
|
100
|
+
)
|
|
101
|
+
option_basenames.append(basename)
|
|
102
|
+
|
|
103
|
+
fuzzy_results = string_diff(user_input, option_basenames)
|
|
104
|
+
|
|
105
|
+
if fuzzy_results:
|
|
106
|
+
# Map fuzzy results back to original options
|
|
107
|
+
fuzzy_matches = []
|
|
108
|
+
for _, matched_basename in fuzzy_results:
|
|
109
|
+
for i, basename in enumerate(option_basenames):
|
|
110
|
+
if basename == matched_basename:
|
|
111
|
+
fuzzy_matches.append(options[i])
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
if len(fuzzy_matches) == 1:
|
|
115
|
+
return fuzzy_matches[0]
|
|
116
|
+
elif len(fuzzy_matches) > 1:
|
|
117
|
+
# Recursive disambiguation with fuzzy matches
|
|
118
|
+
return _disambiguate_user_choice(
|
|
119
|
+
fuzzy_matches,
|
|
120
|
+
option_to_str,
|
|
121
|
+
f"Multiple fuzzy matches for '{user_input}':",
|
|
122
|
+
0,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# No match found
|
|
126
|
+
print(f"No match found for '{user_input}'. Please try again.")
|
|
127
|
+
return _disambiguate_user_choice(options, option_to_str, prompt, default_index)
|
|
4
128
|
|
|
5
129
|
|
|
6
130
|
def select_sketch_directory(
|
|
7
|
-
sketch_directories: list[Path], cwd_is_fastled: bool
|
|
131
|
+
sketch_directories: list[Path], cwd_is_fastled: bool, is_followup: bool = False
|
|
8
132
|
) -> str | None:
|
|
9
133
|
if cwd_is_fastled:
|
|
10
134
|
exclude = ["src", "dev", "tests"]
|
|
@@ -17,21 +141,20 @@ def select_sketch_directory(
|
|
|
17
141
|
print(f"\nUsing sketch directory: {sketch_directories[0]}")
|
|
18
142
|
return str(sketch_directories[0])
|
|
19
143
|
elif len(sketch_directories) > 1:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return select_sketch_directory([p for _, p in top_hits], cwd_is_fastled)
|
|
144
|
+
# First scan with >4 directories: return None (too many to auto-select)
|
|
145
|
+
if not is_followup and len(sketch_directories) > 4:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
# Prompt user to disambiguate
|
|
149
|
+
result = _disambiguate_user_choice(
|
|
150
|
+
sketch_directories,
|
|
151
|
+
option_to_str=lambda x: str(x),
|
|
152
|
+
prompt="Multiple Directories found, choose one:",
|
|
153
|
+
default_index=0,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if result is None:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
return str(result)
|
|
37
160
|
return None
|
fastled/server_flask.py
CHANGED
|
@@ -85,6 +85,14 @@ def _run_flask_server(
|
|
|
85
85
|
|
|
86
86
|
# logger.error(f"Server error: {e}")
|
|
87
87
|
|
|
88
|
+
@app.after_request
|
|
89
|
+
def add_security_headers(response):
|
|
90
|
+
"""Add security headers required for cross-origin isolation and audio worklets"""
|
|
91
|
+
# Required for SharedArrayBuffer and audio worklets
|
|
92
|
+
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
|
|
93
|
+
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
|
|
94
|
+
return response
|
|
95
|
+
|
|
88
96
|
@app.before_request
|
|
89
97
|
def log_request_info():
|
|
90
98
|
"""Log details of each request before processing"""
|
|
@@ -333,7 +341,35 @@ def _run_flask_server(
|
|
|
333
341
|
# server.watch(str(fastled_js / "index.css"))
|
|
334
342
|
# Start the server
|
|
335
343
|
logger.info(f"Starting server on port {port}")
|
|
336
|
-
|
|
344
|
+
|
|
345
|
+
# Configure SSL if certificates are provided
|
|
346
|
+
if certfile and keyfile:
|
|
347
|
+
logger.info(
|
|
348
|
+
f"Configuring SSL with certfile: {certfile}, keyfile: {keyfile}"
|
|
349
|
+
)
|
|
350
|
+
# Monkey-patch the server to use SSL
|
|
351
|
+
# The livereload Server doesn't expose ssl_context directly,
|
|
352
|
+
# so we need to use the underlying tornado server
|
|
353
|
+
import ssl
|
|
354
|
+
|
|
355
|
+
from tornado import httpserver
|
|
356
|
+
from tornado.ioloop import IOLoop
|
|
357
|
+
from tornado.wsgi import WSGIContainer
|
|
358
|
+
|
|
359
|
+
# Create SSL context
|
|
360
|
+
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
361
|
+
ssl_ctx.load_cert_chain(str(certfile), str(keyfile))
|
|
362
|
+
|
|
363
|
+
# Wrap the WSGI app in Tornado's WSGIContainer
|
|
364
|
+
wsgi_container = WSGIContainer(server.app) # type: ignore[arg-type]
|
|
365
|
+
|
|
366
|
+
# Create HTTP server with SSL
|
|
367
|
+
http_server = httpserver.HTTPServer(wsgi_container, ssl_options=ssl_ctx)
|
|
368
|
+
http_server.listen(port)
|
|
369
|
+
logger.info(f"HTTPS server started on port {port}")
|
|
370
|
+
IOLoop.current().start()
|
|
371
|
+
else:
|
|
372
|
+
server.serve(port=port, debug=True)
|
|
337
373
|
except KeyboardInterrupt:
|
|
338
374
|
logger.info("Server stopped by keyboard interrupt")
|
|
339
375
|
import _thread
|
fastled/settings.py
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import platform
|
|
3
|
+
import sys
|
|
3
4
|
|
|
4
5
|
FILE_CHANGED_DEBOUNCE_SECONDS = 2.0
|
|
5
6
|
MACHINE = platform.machine().lower()
|
|
6
7
|
IS_ARM: bool = "arm" in MACHINE or "aarch64" in MACHINE
|
|
7
8
|
PLATFORM_TAG: str = "-arm64" if IS_ARM else ""
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_running_under_github_actions() -> bool:
|
|
12
|
+
"""Detect if we're running under github actions."""
|
|
13
|
+
return "GITHUB_ACTIONS" in os.environ
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_running_under_pytest() -> bool:
|
|
17
|
+
"""Detect if we're running under pytest."""
|
|
18
|
+
# Check if pytest is in the loaded modules
|
|
19
|
+
if "pytest" in sys.modules:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
# Check for pytest environment variables
|
|
23
|
+
if "PYTEST_CURRENT_TEST" in os.environ:
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_container_name() -> str:
|
|
30
|
+
"""Get the appropriate container name based on runtime context."""
|
|
31
|
+
base_name = "fastled-wasm-container"
|
|
32
|
+
|
|
33
|
+
if not _is_running_under_github_actions() and _is_running_under_pytest():
|
|
34
|
+
# Use test container name when running under pytest
|
|
35
|
+
return f"{base_name}-test{PLATFORM_TAG}"
|
|
36
|
+
else:
|
|
37
|
+
# Use regular container name
|
|
38
|
+
return f"{base_name}{PLATFORM_TAG}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_server_port() -> int:
|
|
42
|
+
"""Get the appropriate server port based on runtime context."""
|
|
43
|
+
if not _is_running_under_github_actions() and _is_running_under_pytest():
|
|
44
|
+
# Use test port when running under pytest to avoid conflicts
|
|
45
|
+
return 9022
|
|
46
|
+
else:
|
|
47
|
+
# Use regular port
|
|
48
|
+
return 9021
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
CONTAINER_NAME = _get_container_name()
|
|
9
52
|
DEFAULT_URL = str(os.environ.get("FASTLED_URL", "https://fastled.onrender.com"))
|
|
10
|
-
SERVER_PORT =
|
|
53
|
+
SERVER_PORT = _get_server_port()
|
|
54
|
+
AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
|
|
11
55
|
|
|
12
56
|
IMAGE_NAME = "niteris/fastled-wasm"
|
|
13
|
-
DEFAULT_CONTAINER_NAME =
|
|
57
|
+
DEFAULT_CONTAINER_NAME = _get_container_name()
|
|
14
58
|
# IMAGE_TAG = "latest"
|
|
15
59
|
|
|
16
60
|
DOCKER_FILE = (
|