fastled 1.3.15__py3-none-any.whl → 1.3.17__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 -247
- fastled/project_init.py +129 -129
- fastled/server_flask.py +2 -1
- fastled/site/build.py +449 -449
- fastled/string_diff.py +82 -82
- fastled/types.py +2 -2
- fastled/util.py +6 -3
- fastled/version.py +41 -41
- {fastled-1.3.15.dist-info → fastled-1.3.17.dist-info}/METADATA +478 -477
- {fastled-1.3.15.dist-info → fastled-1.3.17.dist-info}/RECORD +20 -20
- {fastled-1.3.15.dist-info → fastled-1.3.17.dist-info}/WHEEL +0 -0
- {fastled-1.3.15.dist-info → fastled-1.3.17.dist-info}/entry_points.txt +0 -0
- {fastled-1.3.15.dist-info → fastled-1.3.17.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.3.15.dist-info → fastled-1.3.17.dist-info}/top_level.txt +0 -0
fastled/print_filter.py
CHANGED
@@ -1,247 +1,52 @@
|
|
1
|
-
import re
|
2
|
-
import
|
3
|
-
from
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
return text
|
54
|
-
|
55
|
-
|
56
|
-
class PrintFilterFastled(PrintFilter):
|
57
|
-
"""Provides filtering for FastLED output so that source files match up with local names."""
|
58
|
-
|
59
|
-
def __init__(self, echo: bool = True) -> None:
|
60
|
-
super().__init__(echo)
|
61
|
-
self.build_started = False
|
62
|
-
# self.compile_link_active = False
|
63
|
-
# self.compile_link_filter:
|
64
|
-
|
65
|
-
def filter(self, text: str) -> str:
|
66
|
-
lines = text.splitlines()
|
67
|
-
out: list[str] = []
|
68
|
-
for line in lines:
|
69
|
-
## DEBUG DO NOT SUBMIT
|
70
|
-
# print(line)
|
71
|
-
if "# WASM is building" in line:
|
72
|
-
self.build_started = True
|
73
|
-
line = _handle_fastled_src(
|
74
|
-
line
|
75
|
-
) # Always convert fastled/src to src for file matchups.
|
76
|
-
if self.build_started or " error: " in line:
|
77
|
-
line = _handle_ino_cpp(line)
|
78
|
-
out.append(line)
|
79
|
-
text = "\n".join(out)
|
80
|
-
return text
|
81
|
-
|
82
|
-
|
83
|
-
class CompileOrLink(Enum):
|
84
|
-
COMPILE = "compile"
|
85
|
-
LINK = "link"
|
86
|
-
|
87
|
-
|
88
|
-
@dataclass
|
89
|
-
class BuildArtifact:
|
90
|
-
timestamp: float
|
91
|
-
input_artifact: str | None
|
92
|
-
output_artifact: str | None
|
93
|
-
build_flags: str
|
94
|
-
compile_or_link: CompileOrLink
|
95
|
-
hash: int
|
96
|
-
|
97
|
-
def flags_pretty(self) -> str:
|
98
|
-
"""
|
99
|
-
Returns the flags in a pretty format.
|
100
|
-
This is used for printing the flags to the console.
|
101
|
-
"""
|
102
|
-
flags = self.build_flags
|
103
|
-
flags = flags.replace(" -I", "\n-I")
|
104
|
-
flags = flags.replace(" -D", "\n-D")
|
105
|
-
flags = flags.replace(" -l", "\n-l")
|
106
|
-
flags = flags.replace(" -L", "\n-L")
|
107
|
-
flags = flags.replace(" -o", "\n-o")
|
108
|
-
flags = flags.replace(" -W", "\n-W")
|
109
|
-
flags = flags.replace(" -f", "\n-f")
|
110
|
-
flags = flags.replace(" -g", "\n-g")
|
111
|
-
|
112
|
-
# break into lines and sort
|
113
|
-
lines = flags.splitlines()
|
114
|
-
first_line = lines[0]
|
115
|
-
lines.pop(0) # remove first line
|
116
|
-
lines = sorted(lines)
|
117
|
-
# remove duplicates
|
118
|
-
lines = list(dict.fromkeys(lines))
|
119
|
-
# remove empty lines
|
120
|
-
lines = [line for line in lines if line.strip() != ""]
|
121
|
-
# remove leading and trailing whitespace
|
122
|
-
lines = [line.strip() for line in lines]
|
123
|
-
lines = sorted(lines)
|
124
|
-
lines = [first_line] + lines # add first line back to the beginning
|
125
|
-
# stringify
|
126
|
-
flags = "\n".join(lines)
|
127
|
-
return flags
|
128
|
-
|
129
|
-
def __str__(self) -> str:
|
130
|
-
return f"{self.brief()} {self.build_flags} {self.compile_or_link} {self.hash}"
|
131
|
-
|
132
|
-
def brief(self) -> str:
|
133
|
-
return f"{self.timestamp:.2f} {self.output_artifact}"
|
134
|
-
|
135
|
-
def begin_flags(self) -> str:
|
136
|
-
"""
|
137
|
-
Returns the flags that are used to begin a build.
|
138
|
-
This is the flags that are used for the first compile or link.
|
139
|
-
"""
|
140
|
-
|
141
|
-
out: str = (
|
142
|
-
"\n################ NEW COMPILE/LINK FLAG GROUP #####################\n\n"
|
143
|
-
)
|
144
|
-
out += f"{self.flags_pretty()}\n"
|
145
|
-
return out
|
146
|
-
|
147
|
-
def end_flags(self) -> str:
|
148
|
-
"""
|
149
|
-
Returns the flags that are used to end a build.
|
150
|
-
This is the flags that are used for the last compile or link.
|
151
|
-
"""
|
152
|
-
out: str = (
|
153
|
-
"\n################ END COMPILE/LINK FLAG GROUP #####################\n"
|
154
|
-
)
|
155
|
-
return out
|
156
|
-
|
157
|
-
@staticmethod
|
158
|
-
def parse(input_str: str) -> "BuildArtifact | None":
|
159
|
-
"""
|
160
|
-
Parse a single build-log line of the form:
|
161
|
-
"<timestamp> ... <some .cpp or .h file> ... <flags>"
|
162
|
-
|
163
|
-
Returns a BuildArtifact, or None if parsing failed.
|
164
|
-
"""
|
165
|
-
return _parse(input_str)
|
166
|
-
|
167
|
-
|
168
|
-
class TokenFilter(ABC):
|
169
|
-
@abstractmethod
|
170
|
-
def extract(self, tokens: list[str]) -> str | None:
|
171
|
-
"""
|
172
|
-
Scan `tokens`, remove any tokens this filter is responsible for,
|
173
|
-
and return the extracted string (or None if not found/invalid).
|
174
|
-
"""
|
175
|
-
...
|
176
|
-
|
177
|
-
|
178
|
-
class TimestampFilter(TokenFilter):
|
179
|
-
def extract(self, tokens: list[str]) -> str | None:
|
180
|
-
if not tokens:
|
181
|
-
return None
|
182
|
-
candidate = tokens[0]
|
183
|
-
try:
|
184
|
-
_ = float(candidate)
|
185
|
-
return tokens.pop(0)
|
186
|
-
except ValueError:
|
187
|
-
return None
|
188
|
-
|
189
|
-
|
190
|
-
class InputArtifactFilter(TokenFilter):
|
191
|
-
def extract(self, tokens: list[str]) -> str | None:
|
192
|
-
for i, tok in enumerate(tokens):
|
193
|
-
if tok.endswith(".cpp") or tok.endswith(".h"):
|
194
|
-
return tokens.pop(i)
|
195
|
-
return None
|
196
|
-
|
197
|
-
|
198
|
-
class OutputArtifactFilter(TokenFilter):
|
199
|
-
def extract(self, tokens: list[str]) -> str | None:
|
200
|
-
for i, tok in enumerate(tokens):
|
201
|
-
if tok == "-o" and i + 1 < len(tokens):
|
202
|
-
tokens.pop(i) # drop '-o'
|
203
|
-
return tokens.pop(i) # drop & return artifact
|
204
|
-
return None
|
205
|
-
|
206
|
-
|
207
|
-
class ActionFilter(TokenFilter):
|
208
|
-
def extract(self, tokens: list[str]) -> str | None:
|
209
|
-
if "-c" in tokens:
|
210
|
-
return CompileOrLink.COMPILE.value
|
211
|
-
return CompileOrLink.LINK.value
|
212
|
-
|
213
|
-
|
214
|
-
def _parse(line: str) -> BuildArtifact | None:
|
215
|
-
tokens = line.strip().split()
|
216
|
-
if not tokens:
|
217
|
-
return None
|
218
|
-
|
219
|
-
# instantiate in the order we need them
|
220
|
-
filters: list[TokenFilter] = [
|
221
|
-
TimestampFilter(),
|
222
|
-
InputArtifactFilter(),
|
223
|
-
OutputArtifactFilter(),
|
224
|
-
ActionFilter(),
|
225
|
-
]
|
226
|
-
|
227
|
-
# apply each filter
|
228
|
-
raw_ts = filters[0].extract(tokens)
|
229
|
-
raw_in = filters[1].extract(tokens)
|
230
|
-
raw_out = filters[2].extract(tokens)
|
231
|
-
raw_act = filters[3].extract(tokens)
|
232
|
-
|
233
|
-
if raw_ts is None or raw_in is None or raw_act is None:
|
234
|
-
return None
|
235
|
-
|
236
|
-
# the rest of `tokens` are the flags
|
237
|
-
flags_str = " ".join(tokens)
|
238
|
-
h = zlib.adler32(flags_str.encode("utf-8"))
|
239
|
-
|
240
|
-
return BuildArtifact(
|
241
|
-
timestamp=float(raw_ts),
|
242
|
-
input_artifact=raw_in,
|
243
|
-
output_artifact=raw_out,
|
244
|
-
build_flags=flags_str,
|
245
|
-
compile_or_link=CompileOrLink(raw_act),
|
246
|
-
hash=h,
|
247
|
-
)
|
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