fastled 1.3.18__py3-none-any.whl → 1.3.20__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/__version__.py +1 -1
- fastled/app.py +177 -177
- fastled/client_server.py +519 -519
- fastled/docker_manager.py +1068 -1068
- fastled/open_browser.py +137 -137
- fastled/parse_args.py +301 -301
- fastled/print_filter.py +52 -52
- fastled/project_init.py +129 -129
- fastled/server_flask.py +25 -76
- fastled/site/build.py +449 -449
- fastled/string_diff.py +82 -82
- fastled/version.py +41 -41
- {fastled-1.3.18.dist-info → fastled-1.3.20.dist-info}/METADATA +478 -478
- {fastled-1.3.18.dist-info → fastled-1.3.20.dist-info}/RECORD +18 -18
- {fastled-1.3.18.dist-info → fastled-1.3.20.dist-info}/WHEEL +0 -0
- {fastled-1.3.18.dist-info → fastled-1.3.20.dist-info}/entry_points.txt +0 -0
- {fastled-1.3.18.dist-info → fastled-1.3.20.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.3.18.dist-info → fastled-1.3.20.dist-info}/top_level.txt +0 -0
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
@@ -1,129 +1,129 @@
|
|
1
|
-
import _thread
|
2
|
-
import threading
|
3
|
-
import time
|
4
|
-
import zipfile
|
5
|
-
from pathlib import Path
|
6
|
-
|
7
|
-
import httpx
|
8
|
-
|
9
|
-
from fastled.settings import DEFAULT_URL
|
10
|
-
from fastled.spinner import Spinner
|
11
|
-
|
12
|
-
DEFAULT_EXAMPLE = "wasm"
|
13
|
-
|
14
|
-
|
15
|
-
def get_examples(host: str | None = None) -> list[str]:
|
16
|
-
host = host or DEFAULT_URL
|
17
|
-
url_info = f"{host}/info"
|
18
|
-
response = httpx.get(url_info, timeout=4)
|
19
|
-
response.raise_for_status()
|
20
|
-
examples: list[str] = response.json()["examples"]
|
21
|
-
return sorted(examples)
|
22
|
-
|
23
|
-
|
24
|
-
def _prompt_for_example() -> str:
|
25
|
-
examples = get_examples()
|
26
|
-
while True:
|
27
|
-
print("Available examples:")
|
28
|
-
for i, example in enumerate(examples):
|
29
|
-
print(f" [{i+1}]: {example}")
|
30
|
-
answer = input("Enter the example number or name: ").strip()
|
31
|
-
if answer.isdigit():
|
32
|
-
example_num = int(answer) - 1
|
33
|
-
if example_num < 0 or example_num >= len(examples):
|
34
|
-
print("Invalid example number")
|
35
|
-
continue
|
36
|
-
return examples[example_num]
|
37
|
-
elif answer in examples:
|
38
|
-
return answer
|
39
|
-
|
40
|
-
|
41
|
-
class DownloadThread(threading.Thread):
|
42
|
-
def __init__(self, url: str, json: str):
|
43
|
-
super().__init__(daemon=True)
|
44
|
-
self.url = url
|
45
|
-
self.json = json
|
46
|
-
self.bytes_downloaded = 0
|
47
|
-
self.content: bytes | None = None
|
48
|
-
self.error: Exception | None = None
|
49
|
-
self.success = False
|
50
|
-
|
51
|
-
def run(self) -> None:
|
52
|
-
timeout = httpx.Timeout(5.0, connect=5.0, read=120.0, write=30.0)
|
53
|
-
try:
|
54
|
-
with httpx.Client(timeout=timeout) as client:
|
55
|
-
with client.stream("POST", self.url, json=self.json) as response:
|
56
|
-
response.raise_for_status()
|
57
|
-
content = b""
|
58
|
-
for chunk in response.iter_bytes():
|
59
|
-
content += chunk
|
60
|
-
self.bytes_downloaded += len(chunk)
|
61
|
-
self.content = content
|
62
|
-
self.success = True
|
63
|
-
except KeyboardInterrupt:
|
64
|
-
self.error = RuntimeError("Download cancelled")
|
65
|
-
_thread.interrupt_main()
|
66
|
-
except Exception as e:
|
67
|
-
self.error = e
|
68
|
-
|
69
|
-
|
70
|
-
def project_init(
|
71
|
-
example: str | None = "PROMPT", # prompt for example
|
72
|
-
outputdir: Path | None = None,
|
73
|
-
host: str | None = None,
|
74
|
-
) -> Path:
|
75
|
-
"""
|
76
|
-
Initialize a new FastLED project.
|
77
|
-
"""
|
78
|
-
host = host or DEFAULT_URL
|
79
|
-
outputdir = Path(outputdir) if outputdir is not None else Path("fastled")
|
80
|
-
outputdir.mkdir(exist_ok=True, parents=True)
|
81
|
-
if example == "PROMPT" or example is None:
|
82
|
-
try:
|
83
|
-
example = _prompt_for_example()
|
84
|
-
except httpx.HTTPStatusError:
|
85
|
-
print(
|
86
|
-
f"Failed to fetch examples, using default example '{DEFAULT_EXAMPLE}'"
|
87
|
-
)
|
88
|
-
example = DEFAULT_EXAMPLE
|
89
|
-
assert example is not None
|
90
|
-
endpoint_url = f"{host}/project/init"
|
91
|
-
json = example
|
92
|
-
print(f"Initializing project with example '{example}', url={endpoint_url}")
|
93
|
-
|
94
|
-
# Start download thread
|
95
|
-
download_thread = DownloadThread(endpoint_url, json)
|
96
|
-
# spinner = Spinner("Downloading project...")
|
97
|
-
with Spinner(f"Downloading project {example}..."):
|
98
|
-
download_thread.start()
|
99
|
-
while download_thread.is_alive():
|
100
|
-
time.sleep(0.1)
|
101
|
-
|
102
|
-
print() # New line after progress
|
103
|
-
download_thread.join()
|
104
|
-
|
105
|
-
# Check for errors
|
106
|
-
if not download_thread.success:
|
107
|
-
assert download_thread.error is not None
|
108
|
-
raise download_thread.error
|
109
|
-
|
110
|
-
content = download_thread.content
|
111
|
-
assert content is not None
|
112
|
-
tmpzip = outputdir / "fastled.zip"
|
113
|
-
outputdir.mkdir(exist_ok=True)
|
114
|
-
tmpzip.write_bytes(content)
|
115
|
-
with zipfile.ZipFile(tmpzip, "r") as zip_ref:
|
116
|
-
zip_ref.extractall(outputdir)
|
117
|
-
tmpzip.unlink()
|
118
|
-
out = outputdir / example
|
119
|
-
print(f"Project initialized at {out}")
|
120
|
-
assert out.exists()
|
121
|
-
return out
|
122
|
-
|
123
|
-
|
124
|
-
def unit_test() -> None:
|
125
|
-
project_init()
|
126
|
-
|
127
|
-
|
128
|
-
if __name__ == "__main__":
|
129
|
-
unit_test()
|
1
|
+
import _thread
|
2
|
+
import threading
|
3
|
+
import time
|
4
|
+
import zipfile
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import httpx
|
8
|
+
|
9
|
+
from fastled.settings import DEFAULT_URL
|
10
|
+
from fastled.spinner import Spinner
|
11
|
+
|
12
|
+
DEFAULT_EXAMPLE = "wasm"
|
13
|
+
|
14
|
+
|
15
|
+
def get_examples(host: str | None = None) -> list[str]:
|
16
|
+
host = host or DEFAULT_URL
|
17
|
+
url_info = f"{host}/info"
|
18
|
+
response = httpx.get(url_info, timeout=4)
|
19
|
+
response.raise_for_status()
|
20
|
+
examples: list[str] = response.json()["examples"]
|
21
|
+
return sorted(examples)
|
22
|
+
|
23
|
+
|
24
|
+
def _prompt_for_example() -> str:
|
25
|
+
examples = get_examples()
|
26
|
+
while True:
|
27
|
+
print("Available examples:")
|
28
|
+
for i, example in enumerate(examples):
|
29
|
+
print(f" [{i+1}]: {example}")
|
30
|
+
answer = input("Enter the example number or name: ").strip()
|
31
|
+
if answer.isdigit():
|
32
|
+
example_num = int(answer) - 1
|
33
|
+
if example_num < 0 or example_num >= len(examples):
|
34
|
+
print("Invalid example number")
|
35
|
+
continue
|
36
|
+
return examples[example_num]
|
37
|
+
elif answer in examples:
|
38
|
+
return answer
|
39
|
+
|
40
|
+
|
41
|
+
class DownloadThread(threading.Thread):
|
42
|
+
def __init__(self, url: str, json: str):
|
43
|
+
super().__init__(daemon=True)
|
44
|
+
self.url = url
|
45
|
+
self.json = json
|
46
|
+
self.bytes_downloaded = 0
|
47
|
+
self.content: bytes | None = None
|
48
|
+
self.error: Exception | None = None
|
49
|
+
self.success = False
|
50
|
+
|
51
|
+
def run(self) -> None:
|
52
|
+
timeout = httpx.Timeout(5.0, connect=5.0, read=120.0, write=30.0)
|
53
|
+
try:
|
54
|
+
with httpx.Client(timeout=timeout) as client:
|
55
|
+
with client.stream("POST", self.url, json=self.json) as response:
|
56
|
+
response.raise_for_status()
|
57
|
+
content = b""
|
58
|
+
for chunk in response.iter_bytes():
|
59
|
+
content += chunk
|
60
|
+
self.bytes_downloaded += len(chunk)
|
61
|
+
self.content = content
|
62
|
+
self.success = True
|
63
|
+
except KeyboardInterrupt:
|
64
|
+
self.error = RuntimeError("Download cancelled")
|
65
|
+
_thread.interrupt_main()
|
66
|
+
except Exception as e:
|
67
|
+
self.error = e
|
68
|
+
|
69
|
+
|
70
|
+
def project_init(
|
71
|
+
example: str | None = "PROMPT", # prompt for example
|
72
|
+
outputdir: Path | None = None,
|
73
|
+
host: str | None = None,
|
74
|
+
) -> Path:
|
75
|
+
"""
|
76
|
+
Initialize a new FastLED project.
|
77
|
+
"""
|
78
|
+
host = host or DEFAULT_URL
|
79
|
+
outputdir = Path(outputdir) if outputdir is not None else Path("fastled")
|
80
|
+
outputdir.mkdir(exist_ok=True, parents=True)
|
81
|
+
if example == "PROMPT" or example is None:
|
82
|
+
try:
|
83
|
+
example = _prompt_for_example()
|
84
|
+
except httpx.HTTPStatusError:
|
85
|
+
print(
|
86
|
+
f"Failed to fetch examples, using default example '{DEFAULT_EXAMPLE}'"
|
87
|
+
)
|
88
|
+
example = DEFAULT_EXAMPLE
|
89
|
+
assert example is not None
|
90
|
+
endpoint_url = f"{host}/project/init"
|
91
|
+
json = example
|
92
|
+
print(f"Initializing project with example '{example}', url={endpoint_url}")
|
93
|
+
|
94
|
+
# Start download thread
|
95
|
+
download_thread = DownloadThread(endpoint_url, json)
|
96
|
+
# spinner = Spinner("Downloading project...")
|
97
|
+
with Spinner(f"Downloading project {example}..."):
|
98
|
+
download_thread.start()
|
99
|
+
while download_thread.is_alive():
|
100
|
+
time.sleep(0.1)
|
101
|
+
|
102
|
+
print() # New line after progress
|
103
|
+
download_thread.join()
|
104
|
+
|
105
|
+
# Check for errors
|
106
|
+
if not download_thread.success:
|
107
|
+
assert download_thread.error is not None
|
108
|
+
raise download_thread.error
|
109
|
+
|
110
|
+
content = download_thread.content
|
111
|
+
assert content is not None
|
112
|
+
tmpzip = outputdir / "fastled.zip"
|
113
|
+
outputdir.mkdir(exist_ok=True)
|
114
|
+
tmpzip.write_bytes(content)
|
115
|
+
with zipfile.ZipFile(tmpzip, "r") as zip_ref:
|
116
|
+
zip_ref.extractall(outputdir)
|
117
|
+
tmpzip.unlink()
|
118
|
+
out = outputdir / example
|
119
|
+
print(f"Project initialized at {out}")
|
120
|
+
assert out.exists()
|
121
|
+
return out
|
122
|
+
|
123
|
+
|
124
|
+
def unit_test() -> None:
|
125
|
+
project_init()
|
126
|
+
|
127
|
+
|
128
|
+
if __name__ == "__main__":
|
129
|
+
unit_test()
|
fastled/server_flask.py
CHANGED
@@ -28,16 +28,19 @@ else:
|
|
28
28
|
|
29
29
|
def _is_dwarf_source(path: str) -> bool:
|
30
30
|
"""Check if the path is a dwarf source file."""
|
31
|
+
logger.debug(f"_is_dwarf_source called with path: {path}")
|
31
32
|
if "dwarfsource" in path:
|
32
33
|
logger.debug(f"Path '{path}' contains 'dwarfsource'")
|
33
34
|
return True
|
34
|
-
|
35
|
-
|
36
|
-
path
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
_is_dwarf_source = False
|
36
|
+
for p in ["fastledsource", "sketchsource", "dwarfsource", "drawfsource"]:
|
37
|
+
if p in path:
|
38
|
+
_is_dwarf_source = True
|
39
|
+
logger.debug(f"Path '{p}' contains '{path}'")
|
40
|
+
break
|
41
|
+
if not _is_dwarf_source:
|
42
|
+
logger.debug(f"Path '{path}' is not a dwarf source file")
|
43
|
+
return _is_dwarf_source
|
41
44
|
|
42
45
|
|
43
46
|
def _run_flask_server(
|
@@ -139,70 +142,15 @@ def _run_flask_server(
|
|
139
142
|
logger.error(f"Error forwarding request: {e}", exc_info=True)
|
140
143
|
return Response(f"Error: {str(e)}", status=500)
|
141
144
|
|
142
|
-
def
|
143
|
-
"""Handle requests to
|
144
|
-
/fastledsource/js/fastledsource/git/fastled/src/
|
145
|
-
or
|
146
|
-
/sketchsource/js/src/Blink.ino
|
147
|
-
|
148
|
-
The names are a bit mangled due to the way C++ prefixing works near the root directory.
|
149
|
-
"""
|
150
|
-
from flask import request
|
151
|
-
|
152
|
-
start_time = time.time()
|
153
|
-
logger.info(f"Processing request: {request.method} {request.url}")
|
154
|
-
# Forward the request to the compile server
|
155
|
-
target_url = f"http://localhost:{compile_server_port}/dwarfsource/{path}"
|
156
|
-
logger.info(f"Requesting: {target_url}")
|
157
|
-
logger.info(f"Processing dwarfsource request for {path}")
|
158
|
-
|
159
|
-
# Log request headers
|
160
|
-
request_headers = {
|
161
|
-
key: value for key, value in request.headers if key != "Host"
|
162
|
-
}
|
163
|
-
logger.debug(f"Request headers: {request_headers}")
|
164
|
-
|
165
|
-
try:
|
166
|
-
# Forward the request with the same method, headers, and body
|
167
|
-
with httpx.Client() as client:
|
168
|
-
resp = client.request(
|
169
|
-
method=request.method,
|
170
|
-
url=target_url,
|
171
|
-
headers=request_headers,
|
172
|
-
content=request.get_data(),
|
173
|
-
cookies=request.cookies,
|
174
|
-
follow_redirects=True,
|
175
|
-
)
|
176
|
-
|
177
|
-
logger.info(f"Response status: {resp.status_code}")
|
178
|
-
logger.debug(f"Response headers: {dict(resp.headers)}")
|
179
|
-
|
180
|
-
# Create a Flask Response object from the httpx response
|
181
|
-
payload = resp.content
|
182
|
-
assert isinstance(payload, bytes)
|
183
|
-
|
184
|
-
# Check if the payload is empty
|
185
|
-
if len(payload) == 0:
|
186
|
-
logger.error("Empty payload received from compile server")
|
187
|
-
return Response("Empty payload", status=400)
|
188
|
-
|
189
|
-
response = Response(
|
190
|
-
payload, status=resp.status_code, headers=dict(resp.headers)
|
191
|
-
)
|
192
|
-
|
193
|
-
elapsed_time = time.time() - start_time
|
194
|
-
logger.info(f"Request completed in {elapsed_time:.3f} seconds")
|
195
|
-
|
196
|
-
return response
|
197
|
-
|
198
|
-
except Exception as e:
|
199
|
-
logger.error(f"Error handling dwarfsource request: {e}", exc_info=True)
|
200
|
-
return Response(f"Error: {str(e)}", status=500)
|
201
|
-
|
202
|
-
def handle_sourcefile(path: str) -> Response:
|
145
|
+
def handle_dwarfsource(path: str) -> Response:
|
203
146
|
"""Handle requests to /sourcefiles/*"""
|
204
147
|
from flask import Response, request
|
205
148
|
|
149
|
+
# Request body should be
|
150
|
+
# {
|
151
|
+
# "path": "string"
|
152
|
+
# }
|
153
|
+
|
206
154
|
start_time = time.time()
|
207
155
|
logger.info("\n##################################")
|
208
156
|
logger.info(f"# Serving source file /sourcefiles/ {path}")
|
@@ -211,7 +159,7 @@ def _run_flask_server(
|
|
211
159
|
logger.info(f"Processing sourcefile request for {path}")
|
212
160
|
|
213
161
|
# Forward the request to the compile server
|
214
|
-
target_url = f"http://localhost:{compile_server_port}/
|
162
|
+
target_url = f"http://localhost:{compile_server_port}/dwarfsource"
|
215
163
|
logger.info(f"Forwarding to: {target_url}")
|
216
164
|
|
217
165
|
# Log request headers
|
@@ -220,14 +168,18 @@ def _run_flask_server(
|
|
220
168
|
}
|
221
169
|
logger.debug(f"Request headers: {request_headers}")
|
222
170
|
|
171
|
+
body: dict[str, str] = {
|
172
|
+
"path": path,
|
173
|
+
}
|
174
|
+
|
223
175
|
try:
|
224
176
|
# Forward the request with the same method, headers, and body
|
225
177
|
with httpx.Client() as client:
|
226
178
|
resp = client.request(
|
227
|
-
method=
|
179
|
+
method="POST",
|
228
180
|
url=target_url,
|
229
181
|
headers=request_headers,
|
230
|
-
|
182
|
+
json=body,
|
231
183
|
cookies=request.cookies,
|
232
184
|
follow_redirects=True,
|
233
185
|
)
|
@@ -341,11 +293,8 @@ def _run_flask_server(
|
|
341
293
|
is_debug_src_code_request = _is_dwarf_source(path)
|
342
294
|
logger.info(f"is debug_src_code_request: {is_debug_src_code_request}")
|
343
295
|
if is_debug_src_code_request:
|
344
|
-
logger.info(f"Handling as
|
345
|
-
return
|
346
|
-
elif path.startswith("sourcefiles/"):
|
347
|
-
logger.info(f"Handling as sourcefiles: {path}")
|
348
|
-
return handle_sourcefile(path)
|
296
|
+
logger.info(f"Handling as dwarf source file request: {path}")
|
297
|
+
return handle_dwarfsource(path)
|
349
298
|
else:
|
350
299
|
logger.info(f"Handling as local file: {path}")
|
351
300
|
return handle_local_file_fetch(path)
|