fastled 1.2.81__py3-none-any.whl → 1.2.82__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 +7 -3
- fastled/client_server.py +78 -13
- fastled/compile_server_impl.py +7 -32
- fastled/live_client.py +5 -0
- fastled/open_browser.py +38 -41
- fastled/server_flask.py +29 -3
- fastled/server_start.py +10 -134
- fastled/util.py +26 -0
- {fastled-1.2.81.dist-info → fastled-1.2.82.dist-info}/METADATA +1 -5
- {fastled-1.2.81.dist-info → fastled-1.2.82.dist-info}/RECORD +14 -16
- fastled/server_fastapi.py +0 -60
- fastled/server_fastapi_cli.py +0 -61
- {fastled-1.2.81.dist-info → fastled-1.2.82.dist-info}/WHEEL +0 -0
- {fastled-1.2.81.dist-info → fastled-1.2.82.dist-info}/entry_points.txt +0 -0
- {fastled-1.2.81.dist-info → fastled-1.2.82.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.2.81.dist-info → fastled-1.2.82.dist-info}/top_level.txt +0 -0
fastled/__init__.py
CHANGED
@@ -14,7 +14,7 @@ from .types import BuildMode, CompileResult, CompileServerError, FileResponse
|
|
14
14
|
# IMPORTANT! There's a bug in github which will REJECT any version update
|
15
15
|
# that has any other change in the repo. Please bump the version as the
|
16
16
|
# ONLY change in a commit, or else the pypi update and the release will fail.
|
17
|
-
__version__ = "1.2.
|
17
|
+
__version__ = "1.2.82"
|
18
18
|
|
19
19
|
|
20
20
|
class Api:
|
@@ -65,6 +65,9 @@ class Api:
|
|
65
65
|
keep_running=True,
|
66
66
|
build_mode=BuildMode.QUICK,
|
67
67
|
profile=False,
|
68
|
+
http_port: (
|
69
|
+
int | None
|
70
|
+
) = None, # None means auto select a free port. -1 means no server.
|
68
71
|
) -> LiveClient:
|
69
72
|
return LiveClient(
|
70
73
|
sketch_directory=sketch_directory,
|
@@ -75,6 +78,7 @@ class Api:
|
|
75
78
|
keep_running=keep_running,
|
76
79
|
build_mode=build_mode,
|
77
80
|
profile=profile,
|
81
|
+
http_port=http_port,
|
78
82
|
)
|
79
83
|
|
80
84
|
@staticmethod
|
@@ -197,12 +201,12 @@ class Test:
|
|
197
201
|
compile_server_port: int | None = None,
|
198
202
|
open_browser: bool = True,
|
199
203
|
) -> Process:
|
200
|
-
from fastled.open_browser import
|
204
|
+
from fastled.open_browser import spawn_http_server
|
201
205
|
|
202
206
|
compile_server_port = compile_server_port or -1
|
203
207
|
if isinstance(directory, str):
|
204
208
|
directory = Path(directory)
|
205
|
-
proc: Process =
|
209
|
+
proc: Process = spawn_http_server(
|
206
210
|
directory,
|
207
211
|
port=port,
|
208
212
|
compile_server_port=compile_server_port,
|
fastled/client_server.py
CHANGED
@@ -2,6 +2,7 @@ import shutil
|
|
2
2
|
import tempfile
|
3
3
|
import threading
|
4
4
|
import time
|
5
|
+
import warnings
|
5
6
|
from multiprocessing import Process
|
6
7
|
from pathlib import Path
|
7
8
|
|
@@ -9,7 +10,7 @@ from fastled.compile_server import CompileServer
|
|
9
10
|
from fastled.docker_manager import DockerManager
|
10
11
|
from fastled.filewatcher import DebouncedFileWatcherProcess, FileWatcherProcess
|
11
12
|
from fastled.keyboard import SpaceBarWatcher
|
12
|
-
from fastled.open_browser import
|
13
|
+
from fastled.open_browser import spawn_http_server
|
13
14
|
from fastled.parse_args import Args
|
14
15
|
from fastled.settings import DEFAULT_URL
|
15
16
|
from fastled.sketch import looks_like_sketch_directory
|
@@ -165,6 +166,45 @@ def _try_start_server_or_get_url(
|
|
165
166
|
return (DEFAULT_URL, None)
|
166
167
|
|
167
168
|
|
169
|
+
def _try_make_compile_server() -> CompileServer | None:
|
170
|
+
if not DockerManager.is_docker_installed():
|
171
|
+
return None
|
172
|
+
try:
|
173
|
+
print(
|
174
|
+
"\nNo host specified, but Docker is installed, attempting to start a compile server using Docker."
|
175
|
+
)
|
176
|
+
from fastled.util import find_free_port
|
177
|
+
|
178
|
+
free_port = find_free_port(start_port=9723, end_port=9743)
|
179
|
+
if free_port is None:
|
180
|
+
return None
|
181
|
+
compile_server = CompileServer(auto_updates=False)
|
182
|
+
print("Waiting for the local compiler to start...")
|
183
|
+
if not compile_server.ping():
|
184
|
+
print("Failed to start local compiler.")
|
185
|
+
raise CompileServerError("Failed to start local compiler.")
|
186
|
+
return compile_server
|
187
|
+
except KeyboardInterrupt:
|
188
|
+
import _thread
|
189
|
+
|
190
|
+
_thread.interrupt_main()
|
191
|
+
raise
|
192
|
+
except Exception as e:
|
193
|
+
warnings.warn(f"Error starting local compile server: {e}")
|
194
|
+
return None
|
195
|
+
|
196
|
+
|
197
|
+
def _is_local_host(host: str) -> bool:
|
198
|
+
return (
|
199
|
+
host.startswith("http://localhost")
|
200
|
+
or host.startswith("http://127.0.0.1")
|
201
|
+
or host.startswith("http://0.0.0.0")
|
202
|
+
or host.startswith("http://[::]")
|
203
|
+
or host.startswith("http://[::1]")
|
204
|
+
or host.startswith("http://[::ffff:127.0.0.1]")
|
205
|
+
)
|
206
|
+
|
207
|
+
|
168
208
|
def run_client(
|
169
209
|
directory: Path,
|
170
210
|
host: str | CompileServer | None,
|
@@ -173,14 +213,24 @@ def run_client(
|
|
173
213
|
build_mode: BuildMode = BuildMode.QUICK,
|
174
214
|
profile: bool = False,
|
175
215
|
shutdown: threading.Event | None = None,
|
216
|
+
http_port: (
|
217
|
+
int | None
|
218
|
+
) = None, # None means auto select a free port, http_port < 0 means no server.
|
176
219
|
) -> int:
|
220
|
+
compile_server: CompileServer | None = None
|
221
|
+
|
222
|
+
if host is None:
|
223
|
+
# attempt to start a compile server if docker is installed.
|
224
|
+
compile_server = _try_make_compile_server()
|
225
|
+
if compile_server is None:
|
226
|
+
host = DEFAULT_URL
|
227
|
+
elif isinstance(host, CompileServer):
|
228
|
+
# if the host is a compile server, use that
|
229
|
+
compile_server = host
|
177
230
|
|
178
|
-
compile_server: CompileServer | None = (
|
179
|
-
host if isinstance(host, CompileServer) else None
|
180
|
-
)
|
181
231
|
shutdown = shutdown or threading.Event()
|
182
232
|
|
183
|
-
def get_url() -> str:
|
233
|
+
def get_url(host=host, compile_server=compile_server) -> str:
|
184
234
|
if compile_server is not None:
|
185
235
|
return compile_server.url()
|
186
236
|
if isinstance(host, str):
|
@@ -196,6 +246,11 @@ def run_client(
|
|
196
246
|
if parsed_url.port is not None:
|
197
247
|
port = parsed_url.port
|
198
248
|
else:
|
249
|
+
if _is_local_host(url):
|
250
|
+
raise ValueError(
|
251
|
+
"Cannot use local host without a port. Please specify a port."
|
252
|
+
)
|
253
|
+
# Assume default port for www
|
199
254
|
port = 80
|
200
255
|
|
201
256
|
try:
|
@@ -221,10 +276,20 @@ def run_client(
|
|
221
276
|
if not result.success:
|
222
277
|
print("\nCompilation failed.")
|
223
278
|
|
224
|
-
|
225
|
-
if open_web_browser:
|
226
|
-
|
227
|
-
|
279
|
+
use_http_server = http_port is None or http_port >= 0
|
280
|
+
if not use_http_server and open_web_browser:
|
281
|
+
warnings.warn(
|
282
|
+
f"Warning: --http-port={http_port} specified but open_web_browser is False, ignoring --http-port."
|
283
|
+
)
|
284
|
+
use_http_server = False
|
285
|
+
|
286
|
+
http_proc: Process | None = None
|
287
|
+
if use_http_server:
|
288
|
+
http_proc = spawn_http_server(
|
289
|
+
directory / "fastled_js",
|
290
|
+
port=http_port,
|
291
|
+
compile_server_port=port,
|
292
|
+
open_browser=open_web_browser,
|
228
293
|
)
|
229
294
|
else:
|
230
295
|
print("\nCompilation successful.")
|
@@ -234,8 +299,8 @@ def run_client(
|
|
234
299
|
return 0
|
235
300
|
|
236
301
|
if not keep_running or shutdown.is_set():
|
237
|
-
if
|
238
|
-
|
302
|
+
if http_proc:
|
303
|
+
http_proc.kill()
|
239
304
|
return 0 if result.success else 1
|
240
305
|
except KeyboardInterrupt:
|
241
306
|
print("\nExiting from main")
|
@@ -346,8 +411,8 @@ def run_client(
|
|
346
411
|
debounced_sketch_watcher.stop()
|
347
412
|
if compile_server:
|
348
413
|
compile_server.stop()
|
349
|
-
if
|
350
|
-
|
414
|
+
if http_proc:
|
415
|
+
http_proc.kill()
|
351
416
|
|
352
417
|
|
353
418
|
def run_client_server(args: Args) -> int:
|
fastled/compile_server_impl.py
CHANGED
@@ -18,6 +18,7 @@ from fastled.docker_manager import (
|
|
18
18
|
from fastled.settings import DEFAULT_CONTAINER_NAME, IMAGE_NAME, SERVER_PORT
|
19
19
|
from fastled.sketch import looks_like_fastled_repo
|
20
20
|
from fastled.types import BuildMode, CompileResult, CompileServerError, FileResponse
|
21
|
+
from fastled.util import port_is_free
|
21
22
|
|
22
23
|
SERVER_OPTIONS = [
|
23
24
|
"--allow-shutdown", # Allow the server to be shut down without a force kill.
|
@@ -36,32 +37,6 @@ def _try_get_fastled_src(path: Path) -> Path | None:
|
|
36
37
|
return None
|
37
38
|
|
38
39
|
|
39
|
-
def _port_is_free(port: int) -> bool:
|
40
|
-
"""Check if a port is free."""
|
41
|
-
import socket
|
42
|
-
|
43
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
44
|
-
try:
|
45
|
-
_ = sock.bind(("localhost", port)) and sock.bind(("0.0.0.0", port))
|
46
|
-
return True
|
47
|
-
except OSError:
|
48
|
-
return False
|
49
|
-
|
50
|
-
|
51
|
-
def _find_free_port() -> int:
|
52
|
-
"""Find a free port on the system."""
|
53
|
-
|
54
|
-
start_port = 49152
|
55
|
-
tries = 10
|
56
|
-
|
57
|
-
for port in range(start_port, start_port + tries):
|
58
|
-
if _port_is_free(port):
|
59
|
-
return port
|
60
|
-
raise RuntimeError(
|
61
|
-
f"No free port found in the range {start_port}-{start_port + tries - 1}"
|
62
|
-
)
|
63
|
-
|
64
|
-
|
65
40
|
class CompileServerImpl:
|
66
41
|
def __init__(
|
67
42
|
self,
|
@@ -250,7 +225,7 @@ class CompileServerImpl:
|
|
250
225
|
image_name=IMAGE_NAME, tag="latest", upgrade=upgrade
|
251
226
|
)
|
252
227
|
DISK_CACHE.put("last-update", now_str)
|
253
|
-
|
228
|
+
INTERNAL_DOCKER_PORT = 80
|
254
229
|
|
255
230
|
print("Docker image now validated")
|
256
231
|
port = SERVER_PORT
|
@@ -262,7 +237,7 @@ class CompileServerImpl:
|
|
262
237
|
print("Disabling port forwarding in interactive mode")
|
263
238
|
ports = {}
|
264
239
|
else:
|
265
|
-
ports = {
|
240
|
+
ports = {INTERNAL_DOCKER_PORT: port}
|
266
241
|
volumes = []
|
267
242
|
if self.fastled_src_dir:
|
268
243
|
print(
|
@@ -341,11 +316,11 @@ class CompileServerImpl:
|
|
341
316
|
print("Compile server starting")
|
342
317
|
return port
|
343
318
|
else:
|
344
|
-
client_port_mapped =
|
345
|
-
|
346
|
-
if client_port_mapped and
|
319
|
+
client_port_mapped = INTERNAL_DOCKER_PORT in ports
|
320
|
+
free_port = port_is_free(INTERNAL_DOCKER_PORT)
|
321
|
+
if client_port_mapped and free_port:
|
347
322
|
warnings.warn(
|
348
|
-
f"Can't expose port {
|
323
|
+
f"Can't expose port {INTERNAL_DOCKER_PORT}, disabling port forwarding in interactive mode"
|
349
324
|
)
|
350
325
|
ports = {}
|
351
326
|
self.docker.run_container_interactive(
|
fastled/live_client.py
CHANGED
@@ -16,6 +16,9 @@ class LiveClient:
|
|
16
16
|
auto_start: bool = True,
|
17
17
|
auto_updates: bool = True,
|
18
18
|
open_web_browser: bool = True,
|
19
|
+
http_port: (
|
20
|
+
int | None
|
21
|
+
) = None, # None means auto select a free port. -1 means no server.
|
19
22
|
keep_running: bool = True,
|
20
23
|
build_mode: BuildMode = BuildMode.QUICK,
|
21
24
|
profile: bool = False,
|
@@ -23,6 +26,7 @@ class LiveClient:
|
|
23
26
|
self.sketch_directory = sketch_directory
|
24
27
|
self.host = host
|
25
28
|
self.open_web_browser = open_web_browser
|
29
|
+
self.http_port = http_port
|
26
30
|
self.keep_running = keep_running
|
27
31
|
self.build_mode = build_mode
|
28
32
|
self.profile = profile
|
@@ -47,6 +51,7 @@ class LiveClient:
|
|
47
51
|
build_mode=self.build_mode,
|
48
52
|
profile=self.profile,
|
49
53
|
shutdown=self.shutdown,
|
54
|
+
http_port=self.http_port,
|
50
55
|
)
|
51
56
|
return rtn
|
52
57
|
|
fastled/open_browser.py
CHANGED
@@ -1,51 +1,36 @@
|
|
1
|
-
import
|
1
|
+
import atexit
|
2
|
+
import random
|
2
3
|
import sys
|
3
4
|
import time
|
5
|
+
import weakref
|
4
6
|
from multiprocessing import Process
|
5
7
|
from pathlib import Path
|
6
8
|
|
7
|
-
from fastled.
|
9
|
+
from fastled.server_flask import run_flask_in_thread
|
8
10
|
|
9
11
|
DEFAULT_PORT = 8089 # different than live version.
|
10
12
|
PYTHON_EXE = sys.executable
|
11
13
|
|
14
|
+
# Use a weak reference set to track processes without preventing garbage collection
|
15
|
+
_WEAK_CLEANUP_SET = weakref.WeakSet()
|
12
16
|
|
13
|
-
# print(f"SSL Config: {SSL_CONFIG.certfile}, {SSL_CONFIG.keyfile}")
|
14
17
|
|
18
|
+
def add_cleanup(proc: Process) -> None:
|
19
|
+
"""Add a process to the cleanup list using weak references"""
|
20
|
+
_WEAK_CLEANUP_SET.add(proc)
|
15
21
|
|
16
|
-
|
17
|
-
|
18
|
-
)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
"fastled.server_start",
|
29
|
-
str(fastled_js),
|
30
|
-
"--port",
|
31
|
-
str(port),
|
32
|
-
"--compile-server-port",
|
33
|
-
str(compile_server_port),
|
34
|
-
]
|
35
|
-
# Pass SSL flags if available
|
36
|
-
if ssl:
|
37
|
-
raise NotImplementedError("SSL is not implemented yet")
|
38
|
-
print(f"Running server on port {port}.")
|
39
|
-
print(f"Command: {subprocess.list2cmdline(cmd)}")
|
40
|
-
# Suppress output
|
41
|
-
subprocess.run(
|
42
|
-
cmd,
|
43
|
-
stdout=subprocess.DEVNULL,
|
44
|
-
# stderr=subprocess.DEVNULL,
|
45
|
-
) # type ignore
|
46
|
-
except KeyboardInterrupt:
|
47
|
-
print("Exiting from server...")
|
48
|
-
sys.exit(0)
|
22
|
+
# Register a cleanup function that checks if the process is still alive
|
23
|
+
def cleanup_if_alive():
|
24
|
+
if proc.is_alive():
|
25
|
+
try:
|
26
|
+
proc.terminate()
|
27
|
+
proc.join(timeout=1.0)
|
28
|
+
if proc.is_alive():
|
29
|
+
proc.kill()
|
30
|
+
except Exception:
|
31
|
+
pass
|
32
|
+
|
33
|
+
atexit.register(cleanup_if_alive)
|
49
34
|
|
50
35
|
|
51
36
|
def is_port_free(port: int) -> bool:
|
@@ -88,7 +73,7 @@ def wait_for_server(port: int, timeout: int = 10) -> None:
|
|
88
73
|
raise TimeoutError("Could not connect to server")
|
89
74
|
|
90
75
|
|
91
|
-
def
|
76
|
+
def spawn_http_server(
|
92
77
|
fastled_js: Path,
|
93
78
|
compile_server_port: int,
|
94
79
|
port: int | None = None,
|
@@ -98,14 +83,26 @@ def open_browser_process(
|
|
98
83
|
if port is not None and not is_port_free(port):
|
99
84
|
raise ValueError(f"Port {port} was specified but in use")
|
100
85
|
if port is None:
|
101
|
-
|
86
|
+
offset = random.randint(0, 100)
|
87
|
+
port = find_free_port(DEFAULT_PORT + offset)
|
88
|
+
|
89
|
+
# port: int,
|
90
|
+
# cwd: Path,
|
91
|
+
# compile_server_port: int,
|
92
|
+
# certfile: Path | None = None,
|
93
|
+
# keyfile: Path | None = None,
|
102
94
|
|
103
95
|
proc = Process(
|
104
|
-
target=
|
105
|
-
args=(
|
96
|
+
target=run_flask_in_thread,
|
97
|
+
args=(port, fastled_js, compile_server_port),
|
106
98
|
daemon=True,
|
107
99
|
)
|
100
|
+
add_cleanup(proc)
|
108
101
|
proc.start()
|
102
|
+
|
103
|
+
# Add to cleanup set with weak reference
|
104
|
+
add_cleanup(proc)
|
105
|
+
|
109
106
|
wait_for_server(port)
|
110
107
|
if open_browser:
|
111
108
|
print(f"Opening browser to http://localhost:{port}")
|
@@ -136,5 +133,5 @@ if __name__ == "__main__":
|
|
136
133
|
)
|
137
134
|
args = parser.parse_args()
|
138
135
|
|
139
|
-
proc =
|
136
|
+
proc = spawn_http_server(args.fastled_js, args.port, open_browser=True)
|
140
137
|
proc.join()
|
fastled/server_flask.py
CHANGED
@@ -61,6 +61,32 @@ def _run_flask_server(
|
|
61
61
|
|
62
62
|
return response
|
63
63
|
|
64
|
+
@app.route("/sourcefiles/<path:path>")
|
65
|
+
def serve_source_files(path):
|
66
|
+
"""Proxy requests to /sourcefiles/* to the compile server"""
|
67
|
+
from flask import Response, request
|
68
|
+
|
69
|
+
# Forward the request to the compile server
|
70
|
+
target_url = f"http://localhost:{compile_server_port}/sourcefiles/{path}"
|
71
|
+
|
72
|
+
# Forward the request with the same method, headers, and body
|
73
|
+
resp = requests.request(
|
74
|
+
method=request.method,
|
75
|
+
url=target_url,
|
76
|
+
headers={key: value for key, value in request.headers if key != "Host"},
|
77
|
+
data=request.get_data(),
|
78
|
+
cookies=request.cookies,
|
79
|
+
allow_redirects=True,
|
80
|
+
stream=False,
|
81
|
+
)
|
82
|
+
|
83
|
+
# Create a Flask Response object from the requests response
|
84
|
+
response = Response(
|
85
|
+
resp.raw.read(), status=resp.status_code, headers=dict(resp.headers)
|
86
|
+
)
|
87
|
+
|
88
|
+
return response
|
89
|
+
|
64
90
|
@app.route("/<path:path>")
|
65
91
|
def serve_files(path):
|
66
92
|
response = send_from_directory(fastled_js, path)
|
@@ -112,7 +138,7 @@ def _run_flask_server(
|
|
112
138
|
_thread.interrupt_main()
|
113
139
|
|
114
140
|
|
115
|
-
def
|
141
|
+
def run_flask_in_thread(
|
116
142
|
port: int,
|
117
143
|
cwd: Path,
|
118
144
|
compile_server_port: int,
|
@@ -169,7 +195,7 @@ def run_flask_server_process(
|
|
169
195
|
) -> Process:
|
170
196
|
"""Run the Flask server in a separate process."""
|
171
197
|
process = Process(
|
172
|
-
target=
|
198
|
+
target=run_flask_in_thread,
|
173
199
|
args=(port, cwd, compile_server_port, certfile, keyfile),
|
174
200
|
)
|
175
201
|
process.start()
|
@@ -179,7 +205,7 @@ def run_flask_server_process(
|
|
179
205
|
def main() -> None:
|
180
206
|
"""Main function."""
|
181
207
|
args = parse_args()
|
182
|
-
|
208
|
+
run_flask_in_thread(args.port, args.fastled_js, args.certfile, args.keyfile)
|
183
209
|
|
184
210
|
|
185
211
|
if __name__ == "__main__":
|
fastled/server_start.py
CHANGED
@@ -1,145 +1,21 @@
|
|
1
|
-
import argparse
|
2
|
-
import importlib.resources as pkg_resources
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from multiprocessing import Process
|
5
1
|
from pathlib import Path
|
6
2
|
|
7
|
-
from fastled.
|
8
|
-
from fastled.server_flask import run_flask_server_process
|
3
|
+
from fastled.server_flask import run_flask_in_thread
|
9
4
|
|
10
5
|
|
11
|
-
def
|
6
|
+
def start_server_in_thread(
|
12
7
|
port: int,
|
13
|
-
|
8
|
+
fastled_js: Path,
|
14
9
|
compile_server_port: int,
|
15
10
|
certfile: Path | None = None,
|
16
11
|
keyfile: Path | None = None,
|
17
|
-
) ->
|
18
|
-
"""
|
19
|
-
if True:
|
20
|
-
# Use Flask server
|
21
|
-
process = run_flask_server_process(
|
22
|
-
port=port,
|
23
|
-
cwd=cwd,
|
24
|
-
compile_server_port=compile_server_port,
|
25
|
-
certfile=certfile,
|
26
|
-
keyfile=keyfile,
|
27
|
-
)
|
28
|
-
else:
|
29
|
-
# Use FastAPI server
|
30
|
-
process = run_fastapi_server_process(
|
31
|
-
port=port,
|
32
|
-
cwd=cwd,
|
33
|
-
certfile=certfile,
|
34
|
-
keyfile=keyfile,
|
35
|
-
)
|
36
|
-
return process
|
12
|
+
) -> None:
|
13
|
+
"""Start the server in a separate thread."""
|
37
14
|
|
38
|
-
|
39
|
-
def get_asset_path(filename: str) -> Path | None:
|
40
|
-
"""Locate a file from the fastled.assets package resources."""
|
41
|
-
try:
|
42
|
-
resource = pkg_resources.files("fastled.assets").joinpath(filename)
|
43
|
-
# Convert to Path for file-system access
|
44
|
-
path = Path(str(resource))
|
45
|
-
return path if path.exists() else None
|
46
|
-
except (ModuleNotFoundError, AttributeError):
|
47
|
-
return None
|
48
|
-
|
49
|
-
|
50
|
-
def start_process(
|
51
|
-
path: Path,
|
52
|
-
port: int,
|
53
|
-
compile_server_port: int,
|
54
|
-
certfile: Path | None = None, # reserved for future use
|
55
|
-
keyfile: Path | None = None, # reserved for future use
|
56
|
-
) -> Process:
|
57
|
-
"""Run the server, using package assets if explicit paths are not provided"""
|
58
|
-
# Use package resources if no explicit path
|
59
|
-
|
60
|
-
# _run_flask_server(path, port, certfile, keyfile)
|
61
|
-
# run_fastapi_server_process(port=port, path=path, certfile=certfile, keyfile=keyfile)
|
62
|
-
proc = run_server_process(
|
63
|
-
port=port, cwd=path, compile_server_port=compile_server_port
|
64
|
-
)
|
65
|
-
# try:
|
66
|
-
# proc.join()
|
67
|
-
# except KeyboardInterrupt:
|
68
|
-
# import _thread
|
69
|
-
|
70
|
-
# _thread.interrupt_main()
|
71
|
-
return proc
|
72
|
-
|
73
|
-
|
74
|
-
@dataclass
|
75
|
-
class Args:
|
76
|
-
fastled_js: Path
|
77
|
-
port: int
|
78
|
-
compile_server_port: int
|
79
|
-
cert: Path | None
|
80
|
-
key: Path | None
|
81
|
-
|
82
|
-
|
83
|
-
def parse_args() -> Args:
|
84
|
-
parser = argparse.ArgumentParser(
|
85
|
-
description="Open a browser to the fastled_js directory"
|
86
|
-
)
|
87
|
-
parser.add_argument(
|
88
|
-
"fastled_js", type=Path, help="Path to the fastled_js directory"
|
89
|
-
)
|
90
|
-
parser.add_argument(
|
91
|
-
"--port",
|
92
|
-
"-p",
|
93
|
-
type=int,
|
94
|
-
default=5500,
|
95
|
-
help="Port to run the server on (default: 5500)",
|
96
|
-
)
|
97
|
-
parser.add_argument(
|
98
|
-
"--compile-server-port",
|
99
|
-
type=int,
|
100
|
-
required=True,
|
101
|
-
help="Used to forward requests to the compile server",
|
102
|
-
)
|
103
|
-
parser.add_argument(
|
104
|
-
"--cert", type=Path, help="(Optional) Path to SSL certificate (PEM format)"
|
105
|
-
)
|
106
|
-
parser.add_argument(
|
107
|
-
"--key", type=Path, help="(Optional) Path to SSL private key (PEM format)"
|
108
|
-
)
|
109
|
-
args = parser.parse_args()
|
110
|
-
out: Args = Args(
|
111
|
-
fastled_js=args.fastled_js,
|
112
|
-
port=args.port,
|
113
|
-
compile_server_port=args.compile_server_port,
|
114
|
-
cert=args.cert,
|
115
|
-
key=args.key,
|
116
|
-
)
|
117
|
-
if args.fastled_js is None:
|
118
|
-
raise ValueError("fastled_js directory is required")
|
119
|
-
return out
|
120
|
-
|
121
|
-
|
122
|
-
def main() -> None:
|
123
|
-
args: Args = parse_args()
|
124
|
-
fastled_js: Path = args.fastled_js
|
125
|
-
port: int = args.port
|
126
|
-
cert: Path | None = args.cert
|
127
|
-
key: Path | None = args.key
|
128
|
-
proc = start_process(
|
129
|
-
path=fastled_js,
|
15
|
+
run_flask_in_thread(
|
130
16
|
port=port,
|
131
|
-
|
132
|
-
|
133
|
-
|
17
|
+
cwd=fastled_js,
|
18
|
+
compile_server_port=compile_server_port,
|
19
|
+
certfile=certfile,
|
20
|
+
keyfile=keyfile,
|
134
21
|
)
|
135
|
-
try:
|
136
|
-
proc.join()
|
137
|
-
except KeyboardInterrupt:
|
138
|
-
import _thread
|
139
|
-
|
140
|
-
_thread.interrupt_main()
|
141
|
-
pass
|
142
|
-
|
143
|
-
|
144
|
-
if __name__ == "__main__":
|
145
|
-
main()
|
fastled/util.py
CHANGED
@@ -17,3 +17,29 @@ def banner_string(msg: str) -> str:
|
|
17
17
|
"""
|
18
18
|
border = "#" * (len(msg) + 4)
|
19
19
|
return f"\n{border}\n# {msg}\n{border}\n"
|
20
|
+
|
21
|
+
|
22
|
+
def port_is_free(port: int) -> bool:
|
23
|
+
"""Check if a port is free."""
|
24
|
+
import socket
|
25
|
+
|
26
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
27
|
+
try:
|
28
|
+
_ = sock.bind(("localhost", port)) and sock.bind(("0.0.0.0", port))
|
29
|
+
return True
|
30
|
+
except OSError:
|
31
|
+
return False
|
32
|
+
|
33
|
+
|
34
|
+
def find_free_port(start_port: int, end_port: int) -> int | None:
|
35
|
+
"""Find a free port on the system."""
|
36
|
+
|
37
|
+
for port in range(start_port, end_port):
|
38
|
+
if port_is_free(port):
|
39
|
+
return port
|
40
|
+
import warnings
|
41
|
+
|
42
|
+
warnings.warn(
|
43
|
+
f"No free port found in the range {start_port}-{end_port}. Using {start_port}."
|
44
|
+
)
|
45
|
+
return None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fastled
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.82
|
4
4
|
Summary: FastLED Wasm Compiler
|
5
5
|
Home-page: https://github.com/zackees/fastled-wasm
|
6
6
|
Maintainer: Zachary Vorhies
|
@@ -13,15 +13,11 @@ License-File: LICENSE
|
|
13
13
|
Requires-Dist: docker>=7.1.0
|
14
14
|
Requires-Dist: httpx>=0.28.1
|
15
15
|
Requires-Dist: watchdog>=6.0.0
|
16
|
-
Requires-Dist: download>=0.3.5
|
17
16
|
Requires-Dist: filelock>=3.16.1
|
18
17
|
Requires-Dist: disklru>=2.0.1
|
19
18
|
Requires-Dist: appdirs>=1.4.4
|
20
19
|
Requires-Dist: rapidfuzz>=3.10.1
|
21
20
|
Requires-Dist: progress>=1.6
|
22
|
-
Requires-Dist: fastapi>=0.115.12
|
23
|
-
Requires-Dist: uvicorn>=0.34.2
|
24
|
-
Requires-Dist: pywebview>=5.4
|
25
21
|
Requires-Dist: watchfiles>=1.0.5
|
26
22
|
Requires-Dist: Flask>=3.0.0
|
27
23
|
Requires-Dist: livereload
|
@@ -1,32 +1,30 @@
|
|
1
|
-
fastled/__init__.py,sha256=
|
1
|
+
fastled/__init__.py,sha256=lMAq0izhqcQRZ-ei8s5OqQYEeOi5J6HlGvpxsICiQ4o,7044
|
2
2
|
fastled/app.py,sha256=0W8Mbplo5UCRzj7nMVgkmCBddQGufsUQjkUUT4pMp74,4611
|
3
3
|
fastled/cli.py,sha256=FjVr31ht0UPlAcmX-84NwfAGMQHTkrCe4o744jCAxiw,375
|
4
4
|
fastled/cli_test.py,sha256=qJB9yLRFR3OwOwdIWSQ0fQsWLnA37v5pDccufiP_hTs,512
|
5
5
|
fastled/cli_test_interactive.py,sha256=BjNhveZOk5aCffHbcrxPQQjWmAuj4ClVKKcKX5eY6yM,542
|
6
|
-
fastled/client_server.py,sha256=
|
6
|
+
fastled/client_server.py,sha256=xQIWJEejZtafCnO2fnO4k_owIzPydvXpEJTjVHpzMlA,17183
|
7
7
|
fastled/compile_server.py,sha256=sRiXYzw7lv9vcWJWGPUkzOGZPmvZGV_TGwbHYoRc15s,3155
|
8
|
-
fastled/compile_server_impl.py,sha256=
|
8
|
+
fastled/compile_server_impl.py,sha256=t7y19DDB0auH1MmYLCi4yjc_ZyDUJ-WHy14QWTJ-pq8,13043
|
9
9
|
fastled/docker_manager.py,sha256=SC_qV6grNTGh0QD1ubKrULQblrN-2PORocISlaZg9NQ,35156
|
10
10
|
fastled/filewatcher.py,sha256=3qS3L7zMQhFuVrkeGn1djsB_cB6x_E2YGJmmQWVAU_w,10033
|
11
11
|
fastled/keyboard.py,sha256=UTAsqCn1UMYnB8YDzENiLTj4GeL45tYfEcO7_5fLFEg,3556
|
12
12
|
fastled/keyz.py,sha256=LO-8m_7CpNDiZLM-FXhQ30f9gN1bUYz5lOsUPTIbI-c,4020
|
13
|
-
fastled/live_client.py,sha256=
|
14
|
-
fastled/open_browser.py,sha256=
|
13
|
+
fastled/live_client.py,sha256=yoAul8tVgbbPf1oEC79SUZSHkLECxlrXxgWR9XaBIp4,2957
|
14
|
+
fastled/open_browser.py,sha256=DFyMrc1qic4Go7eLNPqMaLuMvTaE73NixdfSKV0yyp8,3709
|
15
15
|
fastled/parse_args.py,sha256=waNeATOEz8D50Py5-9p6HcVSa21piTOAWOXS3ag8PYo,9428
|
16
16
|
fastled/paths.py,sha256=VsPmgu0lNSCFOoEC0BsTYzDygXqy15AHUfN-tTuzDZA,99
|
17
17
|
fastled/print_filter.py,sha256=ZpebuqfWEraSBD3Dm0PVZhQVBnU_NSILniwBHwjC1qM,2342
|
18
18
|
fastled/project_init.py,sha256=bBt4DwmW5hZkm9ICt9Qk-0Nr_0JQM7icCgH5Iv-bCQs,3984
|
19
19
|
fastled/select_sketch_directory.py,sha256=-eudwCns3AKj4HuHtSkZAFwbnf005SNL07pOzs9VxnE,1383
|
20
|
-
fastled/
|
21
|
-
fastled/
|
22
|
-
fastled/server_flask.py,sha256=Temr1H-JXMOo0KGOs21BYRz0F7shH6Dsh_7T_bVRpsM,5851
|
23
|
-
fastled/server_start.py,sha256=muMreRRYvjme-gETWDWzmT4mRGAXpDJkFy_oJ0lJZFY,3895
|
20
|
+
fastled/server_flask.py,sha256=IW8zgpv7qoV28qnd7BysnLnNmKHtT_JTRAjIpPUQw7U,6919
|
21
|
+
fastled/server_start.py,sha256=W9yKStkRlRNuXeV6j_6O7HjjFPyVLBHMcF9Uy2QjDWQ,479
|
24
22
|
fastled/settings.py,sha256=oezRvRUJWwauO-kpC4LDbKg6Q-ij4d09UtR2vkjSAPU,575
|
25
23
|
fastled/sketch.py,sha256=tHckjDj8P6BI_LWzUFM071a9qcqPs-r-qFWIe50P5Xw,3391
|
26
24
|
fastled/spinner.py,sha256=VHxmvB92P0Z_zYxRajb5HiNmkHHvZ5dG7hKtZltzpcs,867
|
27
25
|
fastled/string_diff.py,sha256=NbtYxvBFxTUdmTpMLizlgZj2ULJ-7etj72GBdWDTGws,2496
|
28
26
|
fastled/types.py,sha256=k1j1y5h1zpRonp1mqRXy797mSbLqzf5K1QEgl8f27jQ,4822
|
29
|
-
fastled/util.py,sha256=
|
27
|
+
fastled/util.py,sha256=hw3gxS1qGc5LL_QN88_VIjut6T0-61ImDQpxGp11DXY,1189
|
30
28
|
fastled/web_compile.py,sha256=QTYHtcm55zsFxPhdA-qSPfL5Q4lhL3h3oNmir3m-Y3s,11345
|
31
29
|
fastled/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
|
32
30
|
fastled/assets/localhost-key.pem,sha256=Q-CNO_UoOd8fFNN4ljcnqwUeCMhzTplRjLO2x0pYRlU,1704
|
@@ -35,9 +33,9 @@ fastled/site/build.py,sha256=2YKU_UWKlJdGnjdbAbaL0co6kceFMSTVYwH1KCmgPZA,13987
|
|
35
33
|
fastled/site/examples.py,sha256=s6vj2zJc6BfKlnbwXr1QWY1mzuDBMt6j5MEBOWjO_U8,155
|
36
34
|
fastled/test/can_run_local_docker_tests.py,sha256=LEuUbHctRhNNFWcvnz2kEGmjDJeXO4c3kNpizm3yVJs,400
|
37
35
|
fastled/test/examples.py,sha256=GfaHeY1E8izBl6ZqDVjz--RHLyVR4NRnQ5pBesCFJFY,1673
|
38
|
-
fastled-1.2.
|
39
|
-
fastled-1.2.
|
40
|
-
fastled-1.2.
|
41
|
-
fastled-1.2.
|
42
|
-
fastled-1.2.
|
43
|
-
fastled-1.2.
|
36
|
+
fastled-1.2.82.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
37
|
+
fastled-1.2.82.dist-info/METADATA,sha256=Kl4WgIrfZKi7_wsVnVOfQ_dLRiWxSGbeDA16HJDsJB4,21940
|
38
|
+
fastled-1.2.82.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
39
|
+
fastled-1.2.82.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
|
40
|
+
fastled-1.2.82.dist-info/top_level.txt,sha256=Bbv5kpJpZhWNCvDF4K0VcvtBSDMa8B7PTOrZa9CezHY,8
|
41
|
+
fastled-1.2.82.dist-info/RECORD,,
|
fastled/server_fastapi.py
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
|
3
|
-
from fastapi import FastAPI, HTTPException
|
4
|
-
from fastapi.responses import FileResponse
|
5
|
-
|
6
|
-
# your existing MIME mapping, e.g.
|
7
|
-
MAPPING = {
|
8
|
-
"js": "application/javascript",
|
9
|
-
"css": "text/css",
|
10
|
-
"wasm": "application/wasm",
|
11
|
-
"json": "application/json",
|
12
|
-
"png": "image/png",
|
13
|
-
"jpg": "image/jpeg",
|
14
|
-
"jpeg": "image/jpeg",
|
15
|
-
"gif": "image/gif",
|
16
|
-
"svg": "image/svg+xml",
|
17
|
-
"ico": "image/x-icon",
|
18
|
-
"html": "text/html",
|
19
|
-
}
|
20
|
-
|
21
|
-
|
22
|
-
"""Run FastAPI server with live reload or HTTPS depending on args."""
|
23
|
-
app = FastAPI(debug=True)
|
24
|
-
base = Path(".")
|
25
|
-
|
26
|
-
|
27
|
-
def no_cache_headers() -> dict[str, str]:
|
28
|
-
return {
|
29
|
-
"Cache-Control": "no-cache, no-store, must-revalidate",
|
30
|
-
"Pragma": "no-cache",
|
31
|
-
"Expires": "0",
|
32
|
-
}
|
33
|
-
|
34
|
-
|
35
|
-
@app.get("/")
|
36
|
-
async def serve_index():
|
37
|
-
index_path = base / "index.html"
|
38
|
-
if not index_path.exists():
|
39
|
-
raise HTTPException(status_code=404, detail="index.html not found")
|
40
|
-
return FileResponse(
|
41
|
-
index_path,
|
42
|
-
media_type=MAPPING.get("html"),
|
43
|
-
headers=no_cache_headers(),
|
44
|
-
)
|
45
|
-
|
46
|
-
|
47
|
-
@app.get("/{path:path}")
|
48
|
-
async def serve_files(path: str):
|
49
|
-
file_path = base / path
|
50
|
-
if not file_path.exists() or not file_path.is_file():
|
51
|
-
raise HTTPException(status_code=404, detail=f"{path} not found")
|
52
|
-
|
53
|
-
ext = path.rsplit(".", 1)[-1].lower()
|
54
|
-
media_type = MAPPING.get(ext)
|
55
|
-
|
56
|
-
return FileResponse(
|
57
|
-
file_path,
|
58
|
-
media_type=media_type,
|
59
|
-
headers=no_cache_headers(),
|
60
|
-
)
|
fastled/server_fastapi_cli.py
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
from multiprocessing import Process
|
2
|
-
from pathlib import Path
|
3
|
-
|
4
|
-
import uvicorn
|
5
|
-
|
6
|
-
# MAPPING = {
|
7
|
-
# "js": "application/javascript",
|
8
|
-
# "css": "text/css",
|
9
|
-
# "wasm": "application/wasm",
|
10
|
-
# "json": "application/json",
|
11
|
-
# "png": "image/png",
|
12
|
-
# "jpg": "image/jpeg",
|
13
|
-
# "jpeg": "image/jpeg",
|
14
|
-
# "gif": "image/gif",
|
15
|
-
# "svg": "image/svg+xml",
|
16
|
-
# "ico": "image/x-icon",
|
17
|
-
# "html": "text/html",
|
18
|
-
# }
|
19
|
-
|
20
|
-
|
21
|
-
def _run_fastapi_server(
|
22
|
-
port: int,
|
23
|
-
cwd: Path,
|
24
|
-
certfile: Path | None = None,
|
25
|
-
keyfile: Path | None = None,
|
26
|
-
) -> None:
|
27
|
-
# Uvicorn “reload” will watch your Python files for changes.
|
28
|
-
import os
|
29
|
-
|
30
|
-
os.chdir(cwd)
|
31
|
-
uvicorn.run(
|
32
|
-
"fastled.server_fastapi:app",
|
33
|
-
host="127.0.0.1",
|
34
|
-
port=port,
|
35
|
-
reload=False,
|
36
|
-
# reload_includes=["index.html"],
|
37
|
-
ssl_certfile=certfile,
|
38
|
-
ssl_keyfile=keyfile,
|
39
|
-
)
|
40
|
-
|
41
|
-
|
42
|
-
def run_fastapi_server_process(
|
43
|
-
port: int,
|
44
|
-
cwd: Path | None = None,
|
45
|
-
certfile: Path | None = None,
|
46
|
-
keyfile: Path | None = None,
|
47
|
-
) -> Process:
|
48
|
-
"""Run the FastAPI server in a separate process."""
|
49
|
-
cwd = cwd or Path(".")
|
50
|
-
process = Process(
|
51
|
-
target=_run_fastapi_server,
|
52
|
-
args=(port, cwd, certfile, keyfile),
|
53
|
-
)
|
54
|
-
process.start()
|
55
|
-
return process
|
56
|
-
|
57
|
-
|
58
|
-
if __name__ == "__main__":
|
59
|
-
# Example usage
|
60
|
-
proc = run_fastapi_server_process(port=8000)
|
61
|
-
proc.join()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|