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.
- {fastled-1.4.8 → fastled-1.4.10}/PKG-INFO +1 -1
- fastled-1.4.10/requirements.docker.txt +1 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/__version__.py +1 -1
- fastled-1.4.10/src/fastled/interruptible_http.py +148 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/open_browser.py +10 -3
- fastled-1.4.10/src/fastled/playwright/chrome_extension_downloader.py +207 -0
- {fastled-1.4.8/src/fastled → fastled-1.4.10/src/fastled/playwright}/playwright_browser.py +278 -181
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/web_compile.py +42 -45
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/PKG-INFO +1 -1
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/SOURCES.txt +3 -1
- {fastled-1.4.8 → fastled-1.4.10}/tests/integration/test_playwright_integration.py +1 -1
- fastled-1.4.8/requirements.docker.txt +0 -1
- {fastled-1.4.8 → fastled-1.4.10}/.aiderignore +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.cursorrules +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.dockerignore +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/build_multi_docker_image.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/build_webpage.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/lint.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/publish_release.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/template_build_docker_image.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/test_build_exe.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/test_macos.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/test_ubuntu.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.github/workflows/test_win.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.gitignore +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.pylintrc +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.vscode/launch.json +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.vscode/settings.json +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/.vscode/tasks.json +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/DEBUGGER.md +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/Dockerfile +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/FAQ.md +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/LICENSE +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/MANIFEST.in +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/README.md +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/RELEASE.md +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/TODO.md +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/build_exe.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/build_local_docker.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/build_site.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/clean +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/compiler/debug.sh +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/compiler/run.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/demo/100dots.html +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/demo/demo_threejs.html +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/demo/micdemo.html +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/demo/mp3upload.html +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/demo/webgl_postprocessing_unreal_bloom.html +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/docker-compose.yml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/entrypoint.sh +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/install +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/install_linux.sh +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/lint +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/pyproject.toml +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/requirements.testing.txt +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/setup.cfg +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/setup.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/__init__.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/__main__.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/app.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/args.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/assets/example.txt +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/assets/localhost-key.pem +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/assets/localhost.pem +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/cli.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/cli_test.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/cli_test_interactive.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/client_server.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/compile_server.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/compile_server_impl.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/docker_manager.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/filewatcher.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/find_good_connection.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/keyboard.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/keyz.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/live_client.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/parse_args.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/paths.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/print_filter.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/project_init.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/select_sketch_directory.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/server_flask.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/server_start.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/settings.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/site/build.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/site/examples.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/sketch.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/spinner.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/string_diff.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/test/can_run_local_docker_tests.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/test/examples.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/types.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/util.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/version.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled/zip_files.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/dependency_links.txt +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/entry_points.txt +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/requires.txt +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/src/fastled.egg-info/top_level.txt +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/test +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/integration/test_build_examples.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/integration/test_examples.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/integration/test_libcompile.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/html/index.html +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_api.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_bad_ino.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_banner_string.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_cli.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_cli_no_platformio.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_compile_server.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_debug_fetch_source_files.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_docker_linux_on_windows.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_embedded_data.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_filechanger.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_flask_headers.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_http_server.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/bad/bad.ino +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/embedded/wasm.ino +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_ino/wasm/wasm.ino +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_manual_api_invocation.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_no_platformio_compile.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_project_init.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_server_and_client_seperatly.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_session_compile.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_string_diff.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_string_diff_comprehensive.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_version.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/tests/unit/test_webcompile.py +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/upload_package.sh +0 -0
- {fastled-1.4.8 → fastled-1.4.10}/vscode-plugin/readme +0 -0
@@ -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.
|
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
|
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(
|
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")
|