fastled 1.2.81__py3-none-any.whl → 1.2.83__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 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.81"
17
+ __version__ = "1.2.83"
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 open_browser_process
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 = open_browser_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 open_browser_process
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
- browser_proc: Process | None = None
225
- if open_web_browser:
226
- browser_proc = open_browser_process(
227
- directory / "fastled_js", compile_server_port=port
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 browser_proc:
238
- browser_proc.kill()
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 browser_proc:
350
- browser_proc.kill()
414
+ if http_proc:
415
+ http_proc.kill()
351
416
 
352
417
 
353
418
  def run_client_server(args: Args) -> int:
@@ -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
- CLIENT_PORT = 80 # TODO: Don't use port 80.
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 = {CLIENT_PORT: port}
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 = CLIENT_PORT in ports
345
- port_is_free = _port_is_free(CLIENT_PORT)
346
- if client_port_mapped and port_is_free:
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 {CLIENT_PORT}, disabling port forwarding in interactive mode"
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 subprocess
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.keyz import get_ssl_config
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
- def _open_http_server_subprocess(
17
- fastled_js: Path, port: int, compile_server_port: int
18
- ) -> None:
19
- print("\n################################################################")
20
- print(f"# Opening browser to {fastled_js} on port {port}")
21
- print("################################################################\n")
22
- ssl = get_ssl_config()
23
- try:
24
- # Fallback to our Python server
25
- cmd = [
26
- PYTHON_EXE,
27
- "-m",
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 open_browser_process(
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
- port = find_free_port(DEFAULT_PORT)
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=_open_http_server_subprocess,
105
- args=(fastled_js, port, compile_server_port),
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 = open_browser_process(args.fastled_js, args.port, open_browser=True)
136
+ proc = spawn_http_server(args.fastled_js, args.port, open_browser=True)
140
137
  proc.join()
fastled/server_flask.py CHANGED
@@ -5,6 +5,8 @@ from pathlib import Path
5
5
  import requests
6
6
  from livereload import Server
7
7
 
8
+ _DRAWF_SOURCE_PREFIX = "drawfsource/js/fastled/src/"
9
+
8
10
 
9
11
  def _run_flask_server(
10
12
  fastled_js: Path,
@@ -22,7 +24,7 @@ def _run_flask_server(
22
24
  keyfile: Path to the SSL key file
23
25
  """
24
26
  try:
25
- from flask import Flask, send_from_directory
27
+ from flask import Flask, Response, send_from_directory
26
28
 
27
29
  app = Flask(__name__)
28
30
 
@@ -35,13 +37,78 @@ def _run_flask_server(
35
37
  def serve_index():
36
38
  return send_from_directory(fastled_js, "index.html")
37
39
 
38
- @app.route("/static/<path:path>")
39
- def proxy_static(path):
40
- """Proxy requests to /static/* to the compile server"""
40
+ @app.route("/sourcefiles/<path:path>")
41
+ def serve_source_files(path):
42
+ """Proxy requests to /sourcefiles/* to the compile server"""
43
+ from flask import request
44
+
45
+ # Forward the request to the compile server
46
+ target_url = f"http://localhost:{compile_server_port}/sourcefiles/{path}"
47
+
48
+ # Forward the request with the same method, headers, and body
49
+ resp = requests.request(
50
+ method=request.method,
51
+ url=target_url,
52
+ headers={key: value for key, value in request.headers if key != "Host"},
53
+ data=request.get_data(),
54
+ cookies=request.cookies,
55
+ allow_redirects=True,
56
+ stream=False,
57
+ )
58
+
59
+ # Create a Flask Response object from the requests response
60
+ response = Response(
61
+ resp.raw.read(), status=resp.status_code, headers=dict(resp.headers)
62
+ )
63
+
64
+ return response
65
+
66
+ def handle_dwarfsource(path: str) -> Response:
67
+ """Handle requests to /drawfsource/js/fastled/src/"""
68
+ from flask import request
69
+
70
+ print("\n##################################")
71
+ print(f"# Serving source file /drawfsource/ {path}")
72
+ print("##################################\n")
73
+
74
+ if not path.startswith(_DRAWF_SOURCE_PREFIX):
75
+ # unexpected
76
+ print(f"Unexpected path: {path}")
77
+ return Response("Malformed path", status=400)
78
+
79
+ path = path.replace("drawfsource/js/fastled/src/", "")
80
+
81
+ # Forward the request to the compile server
82
+ target_url = f"http://localhost:{compile_server_port}/sourcefiles/{path}"
83
+
84
+ # Forward the request with the same method, headers, and body
85
+ resp = requests.request(
86
+ method=request.method,
87
+ url=target_url,
88
+ headers={key: value for key, value in request.headers if key != "Host"},
89
+ data=request.get_data(),
90
+ cookies=request.cookies,
91
+ allow_redirects=True,
92
+ stream=False,
93
+ )
94
+
95
+ # Create a Flask Response object from the requests response
96
+ response = Response(
97
+ resp.raw.read(), status=resp.status_code, headers=dict(resp.headers)
98
+ )
99
+
100
+ return response
101
+
102
+ def handle_sourcefile(path: str) -> Response:
103
+ """Handle requests to /sourcefiles/*"""
41
104
  from flask import Response, request
42
105
 
106
+ print("\n##################################")
107
+ print(f"# Serving source file /sourcefiles/ {path}")
108
+ print("##################################\n")
109
+
43
110
  # Forward the request to the compile server
44
- target_url = f"http://localhost:{compile_server_port}/static/{path}"
111
+ target_url = f"http://localhost:{compile_server_port}/sourcefiles/{path}"
45
112
 
46
113
  # Forward the request with the same method, headers, and body
47
114
  resp = requests.request(
@@ -61,8 +128,12 @@ def _run_flask_server(
61
128
 
62
129
  return response
63
130
 
64
- @app.route("/<path:path>")
65
- def serve_files(path):
131
+ def handle_local_file_fetch(path: str) -> Response:
132
+
133
+ print("\n##################################")
134
+ print(f"# Servering generic file {path}")
135
+ print("##################################\n")
136
+
66
137
  response = send_from_directory(fastled_js, path)
67
138
  # Some servers don't set the Content-Type header for a bunch of files.
68
139
  if path.endswith(".js"):
@@ -94,6 +165,16 @@ def _run_flask_server(
94
165
  response.headers["Expires"] = "0"
95
166
  return response
96
167
 
168
+ @app.route("/<path:path>")
169
+ def serve_files(path: str):
170
+
171
+ if path.startswith("drawfsource/"):
172
+ return handle_dwarfsource(path)
173
+ elif path.startswith("sourcefiles/"):
174
+ return handle_sourcefile(path)
175
+ else:
176
+ return handle_local_file_fetch(path)
177
+
97
178
  server = Server(app.wsgi_app)
98
179
  # Watch index.html for changes
99
180
  server.watch(str(fastled_js / "index.html"))
@@ -112,7 +193,7 @@ def _run_flask_server(
112
193
  _thread.interrupt_main()
113
194
 
114
195
 
115
- def run(
196
+ def run_flask_in_thread(
116
197
  port: int,
117
198
  cwd: Path,
118
199
  compile_server_port: int,
@@ -169,7 +250,7 @@ def run_flask_server_process(
169
250
  ) -> Process:
170
251
  """Run the Flask server in a separate process."""
171
252
  process = Process(
172
- target=run,
253
+ target=run_flask_in_thread,
173
254
  args=(port, cwd, compile_server_port, certfile, keyfile),
174
255
  )
175
256
  process.start()
@@ -179,7 +260,7 @@ def run_flask_server_process(
179
260
  def main() -> None:
180
261
  """Main function."""
181
262
  args = parse_args()
182
- run(args.port, args.fastled_js, args.certfile, args.keyfile)
263
+ run_flask_in_thread(args.port, args.fastled_js, args.certfile, args.keyfile)
183
264
 
184
265
 
185
266
  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.server_fastapi_cli import run_fastapi_server_process
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 run_server_process(
6
+ def start_server_in_thread(
12
7
  port: int,
13
- cwd: Path,
8
+ fastled_js: Path,
14
9
  compile_server_port: int,
15
10
  certfile: Path | None = None,
16
11
  keyfile: Path | None = None,
17
- ) -> Process:
18
- """Run the server in a separate process."""
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
- compile_server_port=args.compile_server_port,
132
- certfile=cert,
133
- keyfile=key,
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.81
3
+ Version: 1.2.83
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=E0yN78HV0eXNtDsCcsGLoJTTdhAaXlsN6B17h7nzmjc,6896
1
+ fastled/__init__.py,sha256=GFW1WwdYsAv_gygWIQ047oPRT6sjzh7WgDAYVNyInSo,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=zQF4ioOkQzHkKAP0A-8mqjVt8aGD9Sj9ijmknsdLW0U,14859
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=0xWJg5b6n_Y-BKPI2ToG4bT7bB_ZuGNQy45JjyJDg6o,13659
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=MDauol0mxtXggV1Pv9ahC0Jjg_4wnnV6FjGEtdd9cxU,2763
14
- fastled/open_browser.py,sha256=6Iu1hVve0g1Hy4L0DcInmZJfeMM5-Dqcwlv9UqfCtCg,3983
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/server_fastapi.py,sha256=ytsL4poO-yugDIhvYJq6nCNdLZ4fQJ1AFqXkF-uEkqo,1488
21
- fastled/server_fastapi_cli.py,sha256=fJGLvbJx5ertsZER_lgg0GfkYTX-V2rxzbNO1lEapU0,1392
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=qPY72qJ4qOI3Llj3VG0CGvEnJ0ibMcprykAG6o9Sexk,9050
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=17f2A52TfBErJOEGC_Vs72t1mTDocLVTfnR9hWbXW8A,501
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.81.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
39
- fastled-1.2.81.dist-info/METADATA,sha256=dJ5jrJBU_ZPYVPE9HHC6S_61BN3-PxC297TXvEpYorc,22065
40
- fastled-1.2.81.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
41
- fastled-1.2.81.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
42
- fastled-1.2.81.dist-info/top_level.txt,sha256=Bbv5kpJpZhWNCvDF4K0VcvtBSDMa8B7PTOrZa9CezHY,8
43
- fastled-1.2.81.dist-info/RECORD,,
36
+ fastled-1.2.83.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
37
+ fastled-1.2.83.dist-info/METADATA,sha256=XPce-fPL-Jl0e7rpYRuf-JHiyIJZe44nZiARcxX4S8c,21940
38
+ fastled-1.2.83.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
39
+ fastled-1.2.83.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
40
+ fastled-1.2.83.dist-info/top_level.txt,sha256=Bbv5kpJpZhWNCvDF4K0VcvtBSDMa8B7PTOrZa9CezHY,8
41
+ fastled-1.2.83.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
- )
@@ -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()