fastled 1.4.8__tar.gz → 1.4.10__tar.gz

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.
Files changed (133) hide show
  1. {fastled-1.4.8 → fastled-1.4.10}/PKG-INFO +1 -1
  2. fastled-1.4.10/requirements.docker.txt +1 -0
  3. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/__version__.py +1 -1
  4. fastled-1.4.10/src/fastled/interruptible_http.py +148 -0
  5. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/open_browser.py +10 -3
  6. fastled-1.4.10/src/fastled/playwright/chrome_extension_downloader.py +207 -0
  7. {fastled-1.4.8/src/fastled → fastled-1.4.10/src/fastled/playwright}/playwright_browser.py +278 -181
  8. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/web_compile.py +42 -45
  9. {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/PKG-INFO +1 -1
  10. {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/SOURCES.txt +3 -1
  11. {fastled-1.4.8 → fastled-1.4.10}/tests/integration/test_playwright_integration.py +1 -1
  12. fastled-1.4.8/requirements.docker.txt +0 -1
  13. {fastled-1.4.8 → fastled-1.4.10}/.aiderignore +0 -0
  14. {fastled-1.4.8 → fastled-1.4.10}/.cursorrules +0 -0
  15. {fastled-1.4.8 → fastled-1.4.10}/.dockerignore +0 -0
  16. {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/build_multi_docker_image.yml +0 -0
  17. {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/build_webpage.yml +0 -0
  18. {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/lint.yml +0 -0
  19. {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/publish_release.yml +0 -0
  20. {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/template_build_docker_image.yml +0 -0
  21. {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/test_build_exe.yml +0 -0
  22. {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/test_macos.yml +0 -0
  23. {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/test_ubuntu.yml +0 -0
  24. {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/test_win.yml +0 -0
  25. {fastled-1.4.8 → fastled-1.4.10}/.gitignore +0 -0
  26. {fastled-1.4.8 → fastled-1.4.10}/.pylintrc +0 -0
  27. {fastled-1.4.8 → fastled-1.4.10}/.vscode/launch.json +0 -0
  28. {fastled-1.4.8 → fastled-1.4.10}/.vscode/settings.json +0 -0
  29. {fastled-1.4.8 → fastled-1.4.10}/.vscode/tasks.json +0 -0
  30. {fastled-1.4.8 → fastled-1.4.10}/DEBUGGER.md +0 -0
  31. {fastled-1.4.8 → fastled-1.4.10}/Dockerfile +0 -0
  32. {fastled-1.4.8 → fastled-1.4.10}/FAQ.md +0 -0
  33. {fastled-1.4.8 → fastled-1.4.10}/LICENSE +0 -0
  34. {fastled-1.4.8 → fastled-1.4.10}/MANIFEST.in +0 -0
  35. {fastled-1.4.8 → fastled-1.4.10}/README.md +0 -0
  36. {fastled-1.4.8 → fastled-1.4.10}/RELEASE.md +0 -0
  37. {fastled-1.4.8 → fastled-1.4.10}/TODO.md +0 -0
  38. {fastled-1.4.8 → fastled-1.4.10}/build_exe.py +0 -0
  39. {fastled-1.4.8 → fastled-1.4.10}/build_local_docker.py +0 -0
  40. {fastled-1.4.8 → fastled-1.4.10}/build_site.py +0 -0
  41. {fastled-1.4.8 → fastled-1.4.10}/clean +0 -0
  42. {fastled-1.4.8 → fastled-1.4.10}/compiler/debug.sh +0 -0
  43. {fastled-1.4.8 → fastled-1.4.10}/compiler/run.py +0 -0
  44. {fastled-1.4.8 → fastled-1.4.10}/demo/100dots.html +0 -0
  45. {fastled-1.4.8 → fastled-1.4.10}/demo/demo_threejs.html +0 -0
  46. {fastled-1.4.8 → fastled-1.4.10}/demo/micdemo.html +0 -0
  47. {fastled-1.4.8 → fastled-1.4.10}/demo/mp3upload.html +0 -0
  48. {fastled-1.4.8 → fastled-1.4.10}/demo/webgl_postprocessing_unreal_bloom.html +0 -0
  49. {fastled-1.4.8 → fastled-1.4.10}/docker-compose.yml +0 -0
  50. {fastled-1.4.8 → fastled-1.4.10}/entrypoint.sh +0 -0
  51. {fastled-1.4.8 → fastled-1.4.10}/install +0 -0
  52. {fastled-1.4.8 → fastled-1.4.10}/install_linux.sh +0 -0
  53. {fastled-1.4.8 → fastled-1.4.10}/lint +0 -0
  54. {fastled-1.4.8 → fastled-1.4.10}/pyproject.toml +0 -0
  55. {fastled-1.4.8 → fastled-1.4.10}/requirements.testing.txt +0 -0
  56. {fastled-1.4.8 → fastled-1.4.10}/setup.cfg +0 -0
  57. {fastled-1.4.8 → fastled-1.4.10}/setup.py +0 -0
  58. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/__init__.py +0 -0
  59. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/__main__.py +0 -0
  60. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/app.py +0 -0
  61. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/args.py +0 -0
  62. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/assets/example.txt +0 -0
  63. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/assets/localhost-key.pem +0 -0
  64. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/assets/localhost.pem +0 -0
  65. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/cli.py +0 -0
  66. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/cli_test.py +0 -0
  67. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/cli_test_interactive.py +0 -0
  68. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/client_server.py +0 -0
  69. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/compile_server.py +0 -0
  70. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/compile_server_impl.py +0 -0
  71. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/docker_manager.py +0 -0
  72. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/filewatcher.py +0 -0
  73. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/find_good_connection.py +0 -0
  74. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/keyboard.py +0 -0
  75. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/keyz.py +0 -0
  76. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/live_client.py +0 -0
  77. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/parse_args.py +0 -0
  78. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/paths.py +0 -0
  79. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/print_filter.py +0 -0
  80. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/project_init.py +0 -0
  81. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/select_sketch_directory.py +0 -0
  82. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/server_flask.py +0 -0
  83. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/server_start.py +0 -0
  84. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/settings.py +0 -0
  85. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/site/build.py +0 -0
  86. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/site/examples.py +0 -0
  87. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/sketch.py +0 -0
  88. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/spinner.py +0 -0
  89. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/string_diff.py +0 -0
  90. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/test/can_run_local_docker_tests.py +0 -0
  91. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/test/examples.py +0 -0
  92. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/types.py +0 -0
  93. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/util.py +0 -0
  94. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/version.py +0 -0
  95. {fastled-1.4.8 → fastled-1.4.10}/src/fastled/zip_files.py +0 -0
  96. {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/dependency_links.txt +0 -0
  97. {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/entry_points.txt +0 -0
  98. {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/requires.txt +0 -0
  99. {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/top_level.txt +0 -0
  100. {fastled-1.4.8 → fastled-1.4.10}/test +0 -0
  101. {fastled-1.4.8 → fastled-1.4.10}/tests/integration/test_build_examples.py +0 -0
  102. {fastled-1.4.8 → fastled-1.4.10}/tests/integration/test_examples.py +0 -0
  103. {fastled-1.4.8 → fastled-1.4.10}/tests/integration/test_libcompile.py +0 -0
  104. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/html/index.html +0 -0
  105. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_api.py +0 -0
  106. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_bad_ino.py +0 -0
  107. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_banner_string.py +0 -0
  108. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_cli.py +0 -0
  109. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_cli_no_platformio.py +0 -0
  110. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_compile_server.py +0 -0
  111. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_debug_fetch_source_files.py +0 -0
  112. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_docker_linux_on_windows.py +0 -0
  113. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_embedded_data.py +0 -0
  114. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_filechanger.py +0 -0
  115. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_flask_headers.py +0 -0
  116. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_http_server.py +0 -0
  117. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/bad/bad.ino +0 -0
  118. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
  119. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
  120. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
  121. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/embedded/wasm.ino +0 -0
  122. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/wasm/wasm.ino +0 -0
  123. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_manual_api_invocation.py +0 -0
  124. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_no_platformio_compile.py +0 -0
  125. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_project_init.py +0 -0
  126. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_server_and_client_seperatly.py +0 -0
  127. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_session_compile.py +0 -0
  128. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_string_diff.py +0 -0
  129. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_string_diff_comprehensive.py +0 -0
  130. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_version.py +0 -0
  131. {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_webcompile.py +0 -0
  132. {fastled-1.4.8 → fastled-1.4.10}/upload_package.sh +0 -0
  133. {fastled-1.4.8 → fastled-1.4.10}/vscode-plugin/readme +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.4.8
3
+ Version: 1.4.10
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -0,0 +1 @@
1
+ fastled-wasm-server>=1.1.18
@@ -1,6 +1,6 @@
1
1
  # IMPORTANT! There's a bug in github which will REJECT any version update
2
2
  # that has any other change in the repo. Please bump the version as the
3
3
  # ONLY change in a commit, or else the pypi update and the release will fail.
4
- __version__ = "1.4.8"
4
+ __version__ = "1.4.10"
5
5
 
6
6
  __version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
@@ -0,0 +1,148 @@
1
+ """
2
+ Interruptible HTTP requests that can be cancelled with Ctrl+C.
3
+
4
+ This module provides cross-platform HTTP request functionality that can be
5
+ interrupted with Ctrl+C by using asyncio cancellation and periodic checks.
6
+ """
7
+
8
+ import asyncio
9
+
10
+ import httpx
11
+
12
+
13
+ class InterruptibleHTTPRequest:
14
+ """A wrapper for making HTTP requests that can be interrupted by Ctrl+C."""
15
+
16
+ def __init__(self):
17
+ self.cancelled = False
18
+
19
+ async def _make_request_async(
20
+ self,
21
+ url: str,
22
+ files: dict,
23
+ headers: dict,
24
+ transport: httpx.HTTPTransport | None = None,
25
+ timeout: float = 240,
26
+ follow_redirects: bool = True,
27
+ ) -> httpx.Response:
28
+ """Make an async HTTP request."""
29
+ # Convert sync transport to async transport if provided
30
+ async_transport = None
31
+ if transport is not None:
32
+ # For IPv4 connections, create async transport with local address
33
+ async_transport = httpx.AsyncHTTPTransport(local_address="0.0.0.0")
34
+
35
+ async with httpx.AsyncClient(
36
+ transport=async_transport,
37
+ timeout=timeout,
38
+ ) as client:
39
+ response = await client.post(
40
+ url,
41
+ follow_redirects=follow_redirects,
42
+ files=files,
43
+ headers=headers,
44
+ )
45
+ return response
46
+
47
+ def make_request_interruptible(
48
+ self,
49
+ url: str,
50
+ files: dict,
51
+ headers: dict,
52
+ transport: httpx.HTTPTransport | None = None,
53
+ timeout: float = 240,
54
+ follow_redirects: bool = True,
55
+ ) -> httpx.Response:
56
+ """Make an HTTP request that can be interrupted by Ctrl+C."""
57
+ try:
58
+ # Create a new event loop if we're not in one
59
+ try:
60
+ loop = asyncio.get_running_loop()
61
+ # We're already in an event loop, use run_in_executor
62
+ return asyncio.run_coroutine_threadsafe(
63
+ self._run_with_keyboard_check(
64
+ url, files, headers, transport, timeout, follow_redirects
65
+ ),
66
+ loop,
67
+ ).result()
68
+ except RuntimeError:
69
+ # No running loop, create one
70
+ return asyncio.run(
71
+ self._run_with_keyboard_check(
72
+ url, files, headers, transport, timeout, follow_redirects
73
+ )
74
+ )
75
+ except KeyboardInterrupt:
76
+ print("\nHTTP request cancelled by user")
77
+ raise
78
+
79
+ async def _run_with_keyboard_check(
80
+ self,
81
+ url: str,
82
+ files: dict,
83
+ headers: dict,
84
+ transport: httpx.HTTPTransport | None = None,
85
+ timeout: float = 240,
86
+ follow_redirects: bool = True,
87
+ ) -> httpx.Response:
88
+ """Run the request with periodic keyboard interrupt checks."""
89
+ task = asyncio.create_task(
90
+ self._make_request_async(
91
+ url, files, headers, transport, timeout, follow_redirects
92
+ )
93
+ )
94
+
95
+ # Poll for keyboard interrupt while waiting for the request
96
+ # This approach allows the task to be cancelled when KeyboardInterrupt
97
+ # is raised in the calling thread
98
+ while not task.done():
99
+ try:
100
+ # Wait for either completion or a short timeout
101
+ response = await asyncio.wait_for(asyncio.shield(task), timeout=0.1)
102
+ return response
103
+ except asyncio.TimeoutError:
104
+ # Continue waiting - the short timeout allows for more responsive
105
+ # cancellation when KeyboardInterrupt is raised
106
+ continue
107
+ except KeyboardInterrupt:
108
+ task.cancel()
109
+ print("\nHTTP request cancelled by user")
110
+ raise
111
+
112
+ return await task
113
+
114
+
115
+ def make_interruptible_post_request(
116
+ url: str,
117
+ files: dict | None = None,
118
+ headers: dict | None = None,
119
+ transport: httpx.HTTPTransport | None = None,
120
+ timeout: float = 240,
121
+ follow_redirects: bool = True,
122
+ ) -> httpx.Response:
123
+ """
124
+ Convenience function to make an interruptible POST request.
125
+
126
+ Args:
127
+ url: The URL to make the request to
128
+ files: Files to upload (optional)
129
+ headers: HTTP headers (optional)
130
+ transport: HTTP transport to use (optional)
131
+ timeout: Request timeout in seconds
132
+ follow_redirects: Whether to follow redirects
133
+
134
+ Returns:
135
+ The HTTP response
136
+
137
+ Raises:
138
+ KeyboardInterrupt: If the request was cancelled by Ctrl+C
139
+ """
140
+ request_handler = InterruptibleHTTPRequest()
141
+ return request_handler.make_request_interruptible(
142
+ url=url,
143
+ files=files or {},
144
+ headers=headers or {},
145
+ transport=transport,
146
+ timeout=timeout,
147
+ follow_redirects=follow_redirects,
148
+ )
@@ -6,7 +6,7 @@ import weakref
6
6
  from multiprocessing import Process
7
7
  from pathlib import Path
8
8
 
9
- from fastled.playwright_browser import open_with_playwright
9
+ from fastled.playwright.playwright_browser import open_with_playwright
10
10
  from fastled.server_flask import run_flask_in_thread
11
11
 
12
12
  # Global reference to keep Playwright browser alive
@@ -128,7 +128,9 @@ def spawn_http_server(
128
128
  if should_use_playwright:
129
129
  if app:
130
130
  # For --app mode, try to install browsers if needed
131
- from fastled.playwright_browser import install_playwright_browsers
131
+ from fastled.playwright.playwright_browser import (
132
+ install_playwright_browsers,
133
+ )
132
134
 
133
135
  install_playwright_browsers()
134
136
 
@@ -136,8 +138,13 @@ def spawn_http_server(
136
138
  print(
137
139
  "Auto-resize enabled: Browser window will automatically adjust to content size"
138
140
  )
141
+ print(
142
+ "🔧 C++ DevTools Support extension will be loaded for DWARF debugging"
143
+ )
139
144
  global _playwright_browser_proxy
140
- _playwright_browser_proxy = open_with_playwright(url)
145
+ _playwright_browser_proxy = open_with_playwright(
146
+ url, enable_extensions=True
147
+ )
141
148
  else:
142
149
  print(f"Opening browser to {url}")
143
150
  import webbrowser
@@ -0,0 +1,207 @@
1
+ """
2
+ Chrome extension downloader utility for FastLED WASM compiler.
3
+
4
+ This module provides functionality to download Chrome extensions from the
5
+ Chrome Web Store and prepare them for use with Playwright browser.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import shutil
11
+ import tempfile
12
+ import warnings
13
+ import zipfile
14
+ from pathlib import Path
15
+
16
+ import httpx
17
+
18
+
19
+ class ChromeExtensionDownloader:
20
+ """Downloads Chrome extensions from the Chrome Web Store."""
21
+
22
+ # Chrome Web Store CRX download URL
23
+ CRX_URL = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=114.0&acceptformat=crx2,crx3&x=id%3D{extension_id}%26uc"
24
+
25
+ # Modern user agent string
26
+ USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
27
+
28
+ def __init__(self, cache_dir: Path | None = None):
29
+ """Initialize the Chrome extension downloader.
30
+
31
+ Args:
32
+ cache_dir: Directory to store downloaded extensions.
33
+ Defaults to ~/.fastled/chrome-extensions
34
+ """
35
+ if cache_dir is None:
36
+ cache_dir = Path.home() / ".fastled" / "chrome-extensions"
37
+
38
+ self.cache_dir = Path(cache_dir)
39
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
40
+
41
+ self.headers = {
42
+ "User-Agent": self.USER_AGENT,
43
+ "Referer": "https://chrome.google.com",
44
+ }
45
+
46
+ def extract_extension_id(self, url: str) -> str:
47
+ """Extract extension ID from Chrome Web Store URL.
48
+
49
+ Args:
50
+ url: Chrome Web Store URL
51
+
52
+ Returns:
53
+ Extension ID string
54
+
55
+ Raises:
56
+ ValueError: If URL is not a valid Chrome Web Store URL
57
+ """
58
+ # Match new Chrome Web Store URLs (chromewebstore.google.com)
59
+ new_pattern = r"chromewebstore\.google\.com/detail/[^/]+/([a-z]{32})"
60
+ match = re.search(new_pattern, url)
61
+
62
+ if match:
63
+ return match.group(1)
64
+
65
+ # Match old Chrome Web Store URLs (chrome.google.com/webstore)
66
+ old_pattern = r"chrome\.google\.com/webstore/detail/[^/]+/([a-z]{32})"
67
+ match = re.search(old_pattern, url)
68
+
69
+ if match:
70
+ return match.group(1)
71
+
72
+ # Try direct extension ID
73
+ if re.match(r"^[a-z]{32}$", url):
74
+ return url
75
+
76
+ raise ValueError(f"Invalid Chrome Web Store URL or extension ID: {url}")
77
+
78
+ def download_crx(self, extension_id: str) -> bytes:
79
+ """Download CRX file from Chrome Web Store.
80
+
81
+ Args:
82
+ extension_id: Chrome extension ID
83
+
84
+ Returns:
85
+ CRX file content as bytes
86
+
87
+ Raises:
88
+ httpx.RequestError: If download fails
89
+ """
90
+ download_url = self.CRX_URL.format(extension_id=extension_id)
91
+
92
+ with httpx.Client(follow_redirects=True) as client:
93
+ response = client.get(download_url, headers=self.headers)
94
+ response.raise_for_status()
95
+
96
+ return response.content
97
+
98
+ def extract_crx_to_directory(self, crx_content: bytes, extract_dir: Path) -> None:
99
+ """Extract CRX file content to a directory.
100
+
101
+ CRX files are essentially ZIP files with a header that needs to be removed.
102
+
103
+ Args:
104
+ crx_content: CRX file content as bytes
105
+ extract_dir: Directory to extract the extension to
106
+ """
107
+ # CRX files have a header before the ZIP content
108
+ # We need to find the ZIP header (starts with 'PK')
109
+ zip_start = crx_content.find(b"PK\x03\x04")
110
+ if zip_start == -1:
111
+ zip_start = crx_content.find(b"PK\x05\x06") # Empty ZIP
112
+
113
+ if zip_start == -1:
114
+ raise ValueError("Could not find ZIP header in CRX file")
115
+
116
+ # Extract the ZIP portion
117
+ zip_content = crx_content[zip_start:]
118
+
119
+ # Create temporary file to extract from
120
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
121
+ temp_zip.write(zip_content)
122
+ temp_zip_path = temp_zip.name
123
+
124
+ try:
125
+ # Extract the ZIP file
126
+ extract_dir.mkdir(parents=True, exist_ok=True)
127
+ with zipfile.ZipFile(temp_zip_path, "r") as zip_ref:
128
+ zip_ref.extractall(extract_dir)
129
+ finally:
130
+ # Clean up temporary file
131
+ os.unlink(temp_zip_path)
132
+
133
+ def get_extension_path(
134
+ self, url_or_id: str, extension_name: str | None = None
135
+ ) -> Path:
136
+ """Download and extract Chrome extension, returning the path to the extracted directory.
137
+
138
+ Args:
139
+ url_or_id: Chrome Web Store URL or extension ID
140
+ extension_name: Optional name for the extension directory
141
+
142
+ Returns:
143
+ Path to the extracted extension directory
144
+ """
145
+ extension_id = self.extract_extension_id(url_or_id)
146
+
147
+ if extension_name is None:
148
+ extension_name = extension_id
149
+
150
+ extension_dir = self.cache_dir / extension_name
151
+
152
+ # Check if extension is already downloaded and extracted
153
+ if extension_dir.exists() and (extension_dir / "manifest.json").exists():
154
+ print(f"✅ Chrome extension already cached: {extension_dir}")
155
+ return extension_dir
156
+
157
+ print(f"🔽 Downloading Chrome extension {extension_id}...")
158
+
159
+ try:
160
+ # Download the CRX file
161
+ crx_content = self.download_crx(extension_id)
162
+
163
+ # Clean up existing directory if it exists
164
+ if extension_dir.exists():
165
+ shutil.rmtree(extension_dir)
166
+
167
+ # Extract the CRX file
168
+ self.extract_crx_to_directory(crx_content, extension_dir)
169
+
170
+ # Verify extraction worked
171
+ if not (extension_dir / "manifest.json").exists():
172
+ raise ValueError("Extension extraction failed - no manifest.json found")
173
+
174
+ print(f"✅ Chrome extension downloaded and extracted: {extension_dir}")
175
+ return extension_dir
176
+
177
+ except Exception as e:
178
+ warnings.warn(f"Failed to download Chrome extension {extension_id}: {e}")
179
+ if extension_dir.exists():
180
+ shutil.rmtree(extension_dir)
181
+ raise
182
+
183
+
184
+ def download_cpp_devtools_extension() -> Path | None:
185
+ """Download the C++ DevTools Support (DWARF) extension.
186
+
187
+ Returns:
188
+ Path to the extracted extension directory, or None if download failed
189
+ """
190
+ # C++ DevTools Support (DWARF) extension
191
+ extension_url = "https://chromewebstore.google.com/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb"
192
+
193
+ try:
194
+ downloader = ChromeExtensionDownloader()
195
+ return downloader.get_extension_path(extension_url, "cpp-devtools-support")
196
+ except Exception as e:
197
+ warnings.warn(f"Failed to download C++ DevTools Support extension: {e}")
198
+ return None
199
+
200
+
201
+ if __name__ == "__main__":
202
+ # Test the downloader with the C++ DevTools Support extension
203
+ extension_path = download_cpp_devtools_extension()
204
+ if extension_path:
205
+ print(f"Extension downloaded to: {extension_path}")
206
+ else:
207
+ print("Failed to download extension")