fastled 1.2.80__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 CHANGED
@@ -9,12 +9,12 @@ from .compile_server import CompileServer
9
9
  from .live_client import LiveClient
10
10
  from .settings import DOCKER_FILE, IMAGE_NAME
11
11
  from .site.build import build
12
- from .types import BuildMode, CompileResult, CompileServerError
12
+ from .types import BuildMode, CompileResult, CompileServerError, FileResponse
13
13
 
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.80"
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 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,
@@ -218,5 +222,6 @@ __all__ = [
218
222
  "CompileResult",
219
223
  "CompileServerError",
220
224
  "BuildMode",
225
+ "FileResponse",
221
226
  "DOCKER_FILE",
222
227
  ]
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:
fastled/compile_server.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from fastled.types import BuildMode, CompileResult, Platform
3
+ from fastled.types import BuildMode, CompileResult, FileResponse, Platform
4
4
 
5
5
 
6
6
  class CompileServer:
@@ -53,6 +53,10 @@ class CompileServer:
53
53
 
54
54
  project_init(example=example, outputdir=outputdir)
55
55
 
56
+ def fetch_source_file(self, filepath: str) -> FileResponse | Exception:
57
+ """Get the source file from the server."""
58
+ return self.impl.fetch_source_file(filepath)
59
+
56
60
  @property
57
61
  def name(self) -> str:
58
62
  return self.impl.container_name
@@ -17,7 +17,8 @@ from fastled.docker_manager import (
17
17
  )
18
18
  from fastled.settings import DEFAULT_CONTAINER_NAME, IMAGE_NAME, SERVER_PORT
19
19
  from fastled.sketch import looks_like_fastled_repo
20
- from fastled.types import BuildMode, CompileResult, CompileServerError
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,
@@ -204,6 +179,29 @@ class CompileServerImpl:
204
179
  return False
205
180
  return False
206
181
 
182
+ def fetch_source_file(self, filepath: str) -> FileResponse | Exception:
183
+ """Get the source file from the server."""
184
+ if not self._port:
185
+ raise RuntimeError("Server has not been started yet")
186
+ try:
187
+ httpx_client = httpx.Client()
188
+ url = f"http://localhost:{self._port}/sourcefiles/{filepath}"
189
+ response = httpx_client.get(url, follow_redirects=True)
190
+ if response.status_code == 200:
191
+ content = response.text
192
+ mimetype: str = response.headers.get("Content-Type", "text/plain")
193
+ return FileResponse(
194
+ content=content,
195
+ mimetype=mimetype,
196
+ filename=filepath,
197
+ )
198
+ else:
199
+ return CompileServerError(
200
+ f"Error fetching file {filepath}: {response.status_code}"
201
+ )
202
+ except httpx.RequestError as e:
203
+ return CompileServerError(f"Error fetching file {filepath}: {e}")
204
+
207
205
  def _start(self) -> int:
208
206
  print("Compiling server starting")
209
207
 
@@ -227,7 +225,7 @@ class CompileServerImpl:
227
225
  image_name=IMAGE_NAME, tag="latest", upgrade=upgrade
228
226
  )
229
227
  DISK_CACHE.put("last-update", now_str)
230
- CLIENT_PORT = 80 # TODO: Don't use port 80.
228
+ INTERNAL_DOCKER_PORT = 80
231
229
 
232
230
  print("Docker image now validated")
233
231
  port = SERVER_PORT
@@ -239,7 +237,7 @@ class CompileServerImpl:
239
237
  print("Disabling port forwarding in interactive mode")
240
238
  ports = {}
241
239
  else:
242
- ports = {CLIENT_PORT: port}
240
+ ports = {INTERNAL_DOCKER_PORT: port}
243
241
  volumes = []
244
242
  if self.fastled_src_dir:
245
243
  print(
@@ -318,11 +316,11 @@ class CompileServerImpl:
318
316
  print("Compile server starting")
319
317
  return port
320
318
  else:
321
- client_port_mapped = CLIENT_PORT in ports
322
- port_is_free = _port_is_free(CLIENT_PORT)
323
- 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:
324
322
  warnings.warn(
325
- 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"
326
324
  )
327
325
  ports = {}
328
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
@@ -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 run(
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=run,
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
- run(args.port, args.fastled_js, args.certfile, args.keyfile)
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.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/types.py CHANGED
@@ -151,3 +151,12 @@ class Platform(Enum):
151
151
  raise ValueError(
152
152
  f"Platform must be one of {valid_modes}, got {platform_str}"
153
153
  )
154
+
155
+
156
+ @dataclass
157
+ class FileResponse:
158
+ """File response from the server."""
159
+
160
+ filename: str
161
+ content: str
162
+ mimetype: str
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.80
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=yIAkMRI7Rv2hmTe-0r8VSV-sAkdZTUxJ9dGUD8mUL00,6862
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=zQF4ioOkQzHkKAP0A-8mqjVt8aGD9Sj9ijmknsdLW0U,14859
7
- fastled/compile_server.py,sha256=rkXvrvdav5vDG8lv_OlBX3YSCHtnHMt25nXbfeg_r78,2960
8
- fastled/compile_server_impl.py,sha256=_DRdt-eWTdOr2mXvO7h6dKqDSonsm523bIqCZr4wf2k,12615
6
+ fastled/client_server.py,sha256=xQIWJEejZtafCnO2fnO4k_owIzPydvXpEJTjVHpzMlA,17183
7
+ fastled/compile_server.py,sha256=sRiXYzw7lv9vcWJWGPUkzOGZPmvZGV_TGwbHYoRc15s,3155
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=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
- fastled/types.py,sha256=mNchhIW5m6hBBv63OYE0V_u5yGnS505eWjw7HCazb_s,4694
29
- fastled/util.py,sha256=17f2A52TfBErJOEGC_Vs72t1mTDocLVTfnR9hWbXW8A,501
26
+ fastled/types.py,sha256=k1j1y5h1zpRonp1mqRXy797mSbLqzf5K1QEgl8f27jQ,4822
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.80.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
39
- fastled-1.2.80.dist-info/METADATA,sha256=jAnVkAuPkXEoU6dKzqjSu2phFz6WgwsFfvuILLfAuyo,22065
40
- fastled-1.2.80.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
41
- fastled-1.2.80.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
42
- fastled-1.2.80.dist-info/top_level.txt,sha256=Bbv5kpJpZhWNCvDF4K0VcvtBSDMa8B7PTOrZa9CezHY,8
43
- fastled-1.2.80.dist-info/RECORD,,
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
- )
@@ -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()