commit b92027a7808b5c42cedf63837dad58f670ca55c7 Author: Marc Koch Date: Sun Sep 7 19:39:23 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a21812c --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright © 2025 Marc Koch + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8f2fa6 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# gnome-extension-download-url + +A simple script to get the download URL of a GNOME Shell extension from its UUID. + +## Installation +You can download the via pipx: +```bash +pipx install --index-url https://git.extrasolar.space/api/packages/marc/pypi/simple/ --pip-args="--extra-index-url https://pypi.org/simple" gnome-extension-download-ur +``` + +## Usage +```bash +gnome-extension-download-url [EXTENSION EXTENSION_VERSION] [GNOME_SHELL_VERSION] [-h|--help] +``` +- `EXTENSION`: the extension UUID (e.g. gsconnect@andyholmes.github.io) +- `EXTENSION_VERSION`: the version of the extension (optional, default: latest, e.g. 66 or latest) +- `GNOME_SHELL_VERSION`: the version of GNOME shell (optional, default: latest, e.g. 46, 3.36 or latest) +- `-h`, `--help`: show help message diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d68634a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["uv_build>=0.8.15,<0.9.0"] +build-backend = "uv_build" + +[project] +name = "gnome-extension-download-url" +authors = [ + {name = "Marc Koch", email = "marc-koch@posteo.de"}, +] +version = "1.0.0" +description = "Get the download URL for a GNOME shell extension from extensions.gnome.org" +readme = "README.md" +license = "MIT" +keywords = ["gnome"] +license-files = ["LICENSE"] +requires-python = ">=3.13" +dependencies = [ + "requests>=2.32.5", +] + +[dependency-groups] +dev = [ + "pytest>=8.4.2", + "pytest-mock>=3.15.0", +] + +[project.urls] +Homepage = "https://git.extrasolar.space/marc/gnome-extension-download-url" + +[project.scripts] +gnome-extension-download-url = "gnome_extension_download_url.__main__:main" + +[tool.uv] +package = true + +[tool.uv.build-backend] +module-root = "src" +module-name = "gnome_extension_download_url" +source-exclude = ["tests"] + +[[tool.uv.index]] +name = "extrasolar" +url = "https://git.extrasolar.space/api/packages/marc/pypi" +publish-url = "https://git.extrasolar.space/api/packages/marc/pypi" diff --git a/src/gnome_extension_download_url/__init__.py b/src/gnome_extension_download_url/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gnome_extension_download_url/__main__.py b/src/gnome_extension_download_url/__main__.py new file mode 100644 index 0000000..b873ff1 --- /dev/null +++ b/src/gnome_extension_download_url/__main__.py @@ -0,0 +1,153 @@ +import sys + +import requests + +HELP_MSG = \ + """Get the download URL for a GNOME extension. + + Usage: gnome-extension-download-url [EXTENSION EXTENSION_VERSION] [GNOME_SHELL_VERSION] [-h|--help] + + EXTENSION: the extension UUID (e.g. gsconnect@andyholmes.github.io) + EXTENSION_VERSION: the version of the extension (optional, default: latest, e.g. 66 or latest) + GNOME_SHELL_VERSION: the version of GNOME shell (optional, default: latest, e.g. 46, 3.36 or latest) + -h, --help: show this help message + """ + + +def print_help(error: bool = False): + """ + Print help message + :param error: if True, print to stderr + """ + print(HELP_MSG, file=sys.stderr if error else sys.stdout) + + +def check_gnome_shell_version(target_ver: str, shell_versions: list): + """ + Check if the GNOME shell version is available in the list of supported versions + :param target_ver: gnome shell version to check + :param shell_versions: list of supported gnome shell versions + :return: True if valid, False otherwise + """ + + # Split the version into its parts + parts = [int(p) for p in target_ver.split(".")] + major, minor, point = (parts + [-1, -1, -1])[:3] + + # Check if the version is in the list of supported versions + for ver in shell_versions: + if (ver["major"], ver["minor"], ver["point"]) == (major, minor, point): + return True + return False + + +def get_gnome_extension_download_url(ext_uuid: str, ext_ver: str = None, + gnome_shell_ver: str = None): + """ + Get the download url from a GNOME extension + :param ext_uuid: the extension UUID (e.g. gsconnect@andyholmes.github.io) + :param ext_ver: extension version + :param gnome_shell_ver: gnome shell version + """ + + # Convert "latest" versions to None + ext_ver = None if ext_ver == "latest" else ext_ver + gnome_shell_ver = None if gnome_shell_ver == "latest" else gnome_shell_ver + + # Parameter validation + if ext_uuid == "": + raise Exception("Extension UUID cannot be empty") + if ext_ver is not None: + if not str(ext_ver).isdigit(): + raise Exception(f"Invalid GNOME extension version: {ext_ver}") + if gnome_shell_ver is not None: + version_parts = str(gnome_shell_ver).split(".") + if len(version_parts) > 3 or not all( + v.isdigit() and int(v) >= 0 for v in version_parts): + raise Exception(f"Invalid GNOME shell version: {gnome_shell_ver}") + + # Prepare URL + url = f"https://extensions.gnome.org/api/v1/extensions/{ext_uuid}/versions" + + # If an extension version is specified, try to get it directly + if ext_ver: + url = url + f"/{ext_ver}" + result = requests.get(url) + data = result.json() + if result.status_code == 404 or data.get("count") == 0: + if ext_ver is not None: + raise Exception( + f"Extension '{ext_uuid}' version {ext_ver} not found") + raise Exception(f"Extension '{ext_uuid}' not found") + elif result.status_code != 200: + raise Exception( + f"Error fetching extension '{ext_uuid}': {result.status_code}") + if gnome_shell_ver is not None and not check_gnome_shell_version( + gnome_shell_ver, data["shell_versions"]): + raise Exception(f"Extension version {ext_ver} is not compatible " + f"with GNOME shell version {gnome_shell_ver}") + return url + "/?format=zip" + + # If no extension version is specified, get the list of versions + versions = [] + next_page = True + next_page_url = url + while next_page: + result = requests.get(next_page_url) + data = result.json() + if result.status_code == 404 or data.get("count") == 0: + raise Exception(f"Extension '{ext_uuid}' not found") + elif result.status_code != 200: + raise Exception( + f"Error fetching extension '{ext_uuid}': {result.status_code}") + next_page_url = data["next"] + next_page = data["next"] is not None + versions.extend(data["results"]) + versions_sorted = sorted(versions, key=lambda ver: ver["version"], + reverse=True) + + # If a GNOME shell version is specified, find the latest compatible version + if gnome_shell_ver: + for version in versions_sorted: + if check_gnome_shell_version(gnome_shell_ver, + version["shell_versions"]): + return url + f"/{version['version']}/?format=zip" + raise Exception("No compatible version found for GNOME shell " + f"version {gnome_shell_ver}") + + # If no GNOME shell version is specified, return the latest version + latest_version = versions_sorted[0]["version"] + return url + f"/{latest_version}/?format=zip" + + +def main(): + """ + Main function + """ + try: + if len(sys.argv) > 4: + print("Too many arguments. Use --help for usage.", file=sys.stderr) + exit(1) + extension_uuid = sys.argv[1] + if extension_uuid in ["-h", "--help"]: + print_help() + exit(0) + except IndexError: + print_help(error=True) + exit(1) + extension_version = sys.argv[2] \ + if len(sys.argv) > 2 and sys.argv[2] != "" else None + gnome_shell_version = sys.argv[3] \ + if len(sys.argv) > 3 and sys.argv[3] != "" else None + + try: + print(get_gnome_extension_download_url( + extension_uuid, extension_version, gnome_shell_version)) + exit(0) + except Exception as e: + print(e, file=sys.stderr) + exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/request_values_1.json b/tests/fixtures/request_values_1.json new file mode 100644 index 0000000..ee4c42e --- /dev/null +++ b/tests/fixtures/request_values_1.json @@ -0,0 +1,207 @@ +{ + "count": 66, + "next": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=2", + "previous": null, + "results": [ + { + "extension": "gsconnect@andyholmes.github.io", + "version": 1, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:31.576000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 2, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:31.657000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 3, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:31.749000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 4, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:31.785000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 5, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:31.849000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 6, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:31.881000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 7, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:31.925000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 8, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:31.971000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 9, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.036000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 10, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.123000", + "session_modes": [] + } + ] +} diff --git a/tests/fixtures/request_values_2.json b/tests/fixtures/request_values_2.json new file mode 100644 index 0000000..a2ddc87 --- /dev/null +++ b/tests/fixtures/request_values_2.json @@ -0,0 +1,217 @@ +{ + "count": 66, + "next": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=3", + "previous": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/", + "results": [ + { + "extension": "gsconnect@andyholmes.github.io", + "version": 11, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + }, + { + "major": 3, + "minor": 28, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.209000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 12, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 24, + "point": -1 + }, + { + "major": 3, + "minor": 26, + "point": -1 + }, + { + "major": 3, + "minor": 28, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.345000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 13, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.443000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 14, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.518000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 15, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.563000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 16, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.624000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 17, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.703000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 18, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.764000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 19, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.832000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 20, + "version_name": null, + "status": 1, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.923000", + "session_modes": [] + } + ] +} diff --git a/tests/fixtures/request_values_3.json b/tests/fixtures/request_values_3.json new file mode 100644 index 0000000..02dc041 --- /dev/null +++ b/tests/fixtures/request_values_3.json @@ -0,0 +1,247 @@ +{ + "count": 66, + "next": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=4", + "previous": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=2", + "results": [ + { + "extension": "gsconnect@andyholmes.github.io", + "version": 21, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + }, + { + "major": 3, + "minor": 32, + "point": -1 + } + ], + "created": "2023-03-26T13:29:32.993000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 22, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + }, + { + "major": 3, + "minor": 32, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.074000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 23, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + }, + { + "major": 3, + "minor": 32, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.168000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 24, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + }, + { + "major": 3, + "minor": 32, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.346000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 25, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + }, + { + "major": 3, + "minor": 32, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.412000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 26, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + }, + { + "major": 3, + "minor": 32, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.489000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 27, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + }, + { + "major": 3, + "minor": 34, + "point": -1 + }, + { + "major": 3, + "minor": 32, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.569000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 28, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 28, + "point": -1 + }, + { + "major": 3, + "minor": 30, + "point": -1 + }, + { + "major": 3, + "minor": 34, + "point": -1 + }, + { + "major": 3, + "minor": 32, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.648000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 29, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 34, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.732000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 30, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 34, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.809000", + "session_modes": [] + } + ] +} diff --git a/tests/fixtures/request_values_4.json b/tests/fixtures/request_values_4.json new file mode 100644 index 0000000..e36857f --- /dev/null +++ b/tests/fixtures/request_values_4.json @@ -0,0 +1,157 @@ +{ + "count": 66, + "next": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=5", + "previous": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=3", + "results": [ + { + "extension": "gsconnect@andyholmes.github.io", + "version": 31, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 34, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.889000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 32, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 34, + "point": -1 + } + ], + "created": "2023-03-26T13:29:33.971000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 33, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 34, + "point": -1 + } + ], + "created": "2023-03-26T13:29:34.055000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 34, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:34.193000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 35, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:34.278000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 36, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:34.413000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 37, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:34.497000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 38, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:34.625000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 39, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:34.720000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 40, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:34.827000", + "session_modes": [] + } + ] +} diff --git a/tests/fixtures/request_values_5.json b/tests/fixtures/request_values_5.json new file mode 100644 index 0000000..3634847 --- /dev/null +++ b/tests/fixtures/request_values_5.json @@ -0,0 +1,162 @@ +{ + "count": 66, + "next": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=6", + "previous": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=4", + "results": [ + { + "extension": "gsconnect@andyholmes.github.io", + "version": 41, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:34.927000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 42, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:35.233000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 43, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + }, + { + "major": 3, + "minor": 38, + "point": -1 + } + ], + "created": "2023-03-26T13:29:35.313000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 44, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 38, + "point": -1 + } + ], + "created": "2023-03-26T13:29:35.508000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 45, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 38, + "point": -1 + } + ], + "created": "2023-03-26T13:29:35.595000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 46, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 40, + "minor": -1, + "point": -1 + } + ], + "created": "2023-03-26T13:29:35.683000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 47, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 40, + "minor": -1, + "point": -1 + } + ], + "created": "2023-03-26T13:29:35.772000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 48, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 41, + "minor": -1, + "point": -1 + } + ], + "created": "2023-03-26T13:29:35.898000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 49, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 41, + "minor": -1, + "point": -1 + } + ], + "created": "2023-03-26T13:29:36.009000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 50, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 42, + "minor": -1, + "point": -1 + } + ], + "created": "2023-03-26T13:29:36.110000", + "session_modes": [] + } + ] +} diff --git a/tests/fixtures/request_values_6.json b/tests/fixtures/request_values_6.json new file mode 100644 index 0000000..647f1d2 --- /dev/null +++ b/tests/fixtures/request_values_6.json @@ -0,0 +1,167 @@ +{ + "count": 66, + "next": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=7", + "previous": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=5", + "results": [ + { + "extension": "gsconnect@andyholmes.github.io", + "version": 51, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 38, + "point": -1 + } + ], + "created": "2023-03-26T13:29:36.350000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 52, + "version_name": null, + "status": 2, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + }, + { + "major": 3, + "minor": 38, + "point": -1 + } + ], + "created": "2023-03-26T13:29:36.499000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 53, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 3, + "minor": 36, + "point": -1 + } + ], + "created": "2023-03-26T13:29:36.587000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 54, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 43, + "minor": -1, + "point": -1 + } + ], + "created": "2023-03-26T13:29:36.649000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 55, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 44, + "minor": -1, + "point": -1 + } + ], + "created": "2023-03-26T13:29:37.102000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 56, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 45, + "minor": -1, + "point": -1 + } + ], + "created": "2023-11-05T16:50:37.190000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 57, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 46, + "minor": -1, + "point": -1 + } + ], + "created": "2024-04-17T06:45:00.793000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 58, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 46, + "minor": -1, + "point": -1 + }, + { + "major": 47, + "minor": -1, + "point": -1 + } + ], + "created": "2024-10-24T16:27:57.217000", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 59, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 42, + "minor": -1, + "point": -1 + } + ], + "created": "2025-03-29T13:51:46.664324", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 60, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 43, + "minor": -1, + "point": -1 + } + ], + "created": "2025-03-29T13:55:52.914186", + "session_modes": [] + } + ] +} diff --git a/tests/fixtures/request_values_7.json b/tests/fixtures/request_values_7.json new file mode 100644 index 0000000..8d38095 --- /dev/null +++ b/tests/fixtures/request_values_7.json @@ -0,0 +1,117 @@ +{ + "count": 66, + "next": null, + "previous": "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/?page=6", + "results": [ + { + "extension": "gsconnect@andyholmes.github.io", + "version": 61, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 45, + "minor": -1, + "point": -1 + } + ], + "created": "2025-03-29T13:59:03.367888", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 62, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 46, + "minor": -1, + "point": -1 + }, + { + "major": 47, + "minor": -1, + "point": -1 + }, + { + "major": 48, + "minor": -1, + "point": -1 + } + ], + "created": "2025-03-29T14:07:52.146643", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 63, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 42, + "minor": -1, + "point": -1 + } + ], + "created": "2025-07-16T18:22:29.769501", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 64, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 43, + "minor": -1, + "point": -1 + } + ], + "created": "2025-07-16T18:22:53.589928", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 65, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 45, + "minor": -1, + "point": -1 + } + ], + "created": "2025-07-16T18:23:10.447182", + "session_modes": [] + }, + { + "extension": "gsconnect@andyholmes.github.io", + "version": 66, + "version_name": null, + "status": 3, + "shell_versions": [ + { + "major": 46, + "minor": -1, + "point": -1 + }, + { + "major": 47, + "minor": -1, + "point": -1 + }, + { + "major": 48, + "minor": -1, + "point": -1 + } + ], + "created": "2025-07-16T18:23:25.070141", + "session_modes": [] + } + ] +} diff --git a/tests/fixtures/version_request_values.py b/tests/fixtures/version_request_values.py new file mode 100644 index 0000000..ab1b606 --- /dev/null +++ b/tests/fixtures/version_request_values.py @@ -0,0 +1,16 @@ +import json +from pathlib import Path + + +def get_version_request_values(): + version_data = [] + fixture_dir = Path(__file__).parent + for file in fixture_dir.glob("request_values_*.json"): + with open(file, "r") as f: + version_data.extend(json.loads(f.read())["results"]) + version_data.sort(key=lambda v: v["version"], reverse=True) + return version_data + +# Singleton instance to be used in tests +# All extension version information ordered by version descending +values = get_version_request_values() diff --git a/tests/test_check_gnome_shell_version.py b/tests/test_check_gnome_shell_version.py new file mode 100644 index 0000000..341728b --- /dev/null +++ b/tests/test_check_gnome_shell_version.py @@ -0,0 +1,47 @@ +from src.gnome_extension_download_url.__main__ import check_gnome_shell_version + + +def test_check_gnome_shell_version_ok(): + assert check_gnome_shell_version("3.38", [ + {"major": 3, "minor": 36, "point": -1}, + {"major": 3, "minor": 37, "point": -1}, + {"major": 3, "minor": 38, "point": -1}, + ]) + assert check_gnome_shell_version("3.38.1", [ + {"major": 3, "minor": 36, "point": -1}, + {"major": 3, "minor": 37, "point": -1}, + {"major": 3, "minor": 38, "point": -1}, + {"major": 3, "minor": 38, "point": 1}, + ]) + assert check_gnome_shell_version("46", [ + {"major": 3, "minor": 36, "point": -1}, + {"major": 3, "minor": 37, "point": -1}, + {"major": 3, "minor": 38, "point": -1}, + {"major": 40, "minor": -1, "point": -1}, + {"major": 41, "minor": -1, "point": -1}, + {"major": 42, "minor": -1, "point": -1}, + {"major": 43, "minor": -1, "point": -1}, + {"major": 44, "minor": -1, "point": -1}, + {"major": 45, "minor": -1, "point": -1}, + {"major": 46, "minor": -1, "point": -1}, + ]) + + +def test_check_gnome_shell_version_not_ok(): + assert not check_gnome_shell_version("3.38", []) + assert not check_gnome_shell_version("3.38.1", [ + {"major": 3, "minor": 36, "point": -1}, + {"major": 3, "minor": 37, "point": -1}, + {"major": 3, "minor": 38, "point": -1}, + ]) + assert not check_gnome_shell_version("46", [ + {"major": 3, "minor": 36, "point": -1}, + {"major": 3, "minor": 37, "point": -1}, + {"major": 3, "minor": 38, "point": -1}, + {"major": 40, "minor": -1, "point": -1}, + {"major": 41, "minor": -1, "point": -1}, + {"major": 42, "minor": -1, "point": -1}, + {"major": 43, "minor": -1, "point": -1}, + {"major": 44, "minor": -1, "point": -1}, + {"major": 45, "minor": -1, "point": -1}, + ]) \ No newline at end of file diff --git a/tests/test_get_gnome_extension_download_url.py b/tests/test_get_gnome_extension_download_url.py new file mode 100644 index 0000000..6ac7bc8 --- /dev/null +++ b/tests/test_get_gnome_extension_download_url.py @@ -0,0 +1,122 @@ +import json +import re +from urllib.parse import urlparse, parse_qs + +import pytest +import requests + +from src.gnome_extension_download_url.__main__ import \ + get_gnome_extension_download_url +from tests.fixtures.version_request_values import \ + values as version_request_values + + +class MockResponse: + + def __init__(self, *args): + self.status_code = 200 + self.url = args[0] + self._path = urlparse(self.url).path + self._query = urlparse(self.url).query + self._extension_version = self._get_extension_version_from_path() + + def json(self): + if not self.url: + raise Exception("URL not set yet in MockResponse object.") + + # If a specific extension version is requested + if self._extension_version: + version = [v for v in version_request_values if + v["version"] == int(self._extension_version)] + if len(version) > 0: + assert len(version) == 1 + return version[0] + # If version not found + else: + self.status_code = 404 + return { + "detail": "No ExtensionVersion matches the given query."} + + # If no specific version is requested, return paginated results + with open( + f"tests/fixtures/request_values_{self._get_page_parameter()}.json", + "r") as f: + return json.loads(f.read()) + + def _get_page_parameter(self): + query = urlparse(self.url).query + params = parse_qs(query) + return int(params.get("page", [1])[0]) + + def _get_extension_version_from_path(self): + version_expression = r"^.+/versions/(\d[\d\.]*)$" + match = re.match(version_expression, self._path) + return match.group(1) if match else None + + +@pytest.mark.parametrize("ext_uuid, ext_ver, gnome_shell_ver, exception_msg", [ + ("", None, None, "Extension UUID cannot be empty"), + ("extension-uuid", "invalid-version", None, + "Invalid GNOME extension version: invalid-version"), + ("extension-uuid", None, "invalid-version", + "Invalid GNOME shell version: invalid-version"), + ("extension-uuid", "66", "invalid-version", + "Invalid GNOME shell version: invalid-version"), + ("extension-uuid", "invalid-version", "3.38", + "Invalid GNOME extension version: invalid-version"), + ("extension-uuid", "-1", None, "Invalid GNOME extension version: -1"), +]) +def test_get_gnome_extension_download_url_invalid_args( + ext_uuid, ext_ver, gnome_shell_ver, exception_msg, mocker): + mock_requests_get = mocker.patch("requests.get") + + try: + get_gnome_extension_download_url( + ext_uuid, ext_ver, gnome_shell_ver) + except Exception as e: + assert str(e.__str__()) == exception_msg + mock_requests_get.assert_not_called() + + +@pytest.mark.parametrize("ext_ver, gnome_shell_ver, expected_url", [ + (None, None, + "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/66/?format=zip"), + ("latest", None, + "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/66/?format=zip"), + (None, "latest", + "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/66/?format=zip"), + ("latest", "latest", + "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/66/?format=zip"), + ("43", None, + "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/43/?format=zip"), + ("43", "3.38", + "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/43/?format=zip"), + ("latest", "3.36", + "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/53/?format=zip"), + ("latest", "43", + "https://extensions.gnome.org/api/v1/extensions/gsconnect@andyholmes.github.io/versions/64/?format=zip"), +]) +def test_get_gnome_extension_download_url_valid_versions( + ext_ver, gnome_shell_ver, expected_url, monkeypatch): + monkeypatch.setattr(requests, "get", MockResponse) + download_url = get_gnome_extension_download_url( + "gsconnect@andyholmes.github.io", ext_ver, gnome_shell_ver) + + assert download_url == expected_url + + +@pytest.mark.parametrize("ext_ver, gnome_shell_ver, expected_result", [ + (0, None, "Extension 'gsconnect@andyholmes.github.io' version 0 not found"), + ("66", "0", + "Extension version 66 is not compatible with GNOME shell version 0"), + ("latest", "0", "No compatible version found for GNOME shell version 0"), +]) +def test_get_gnome_extension_download_url_invalid_versions( + ext_ver, gnome_shell_ver, expected_result, monkeypatch): + monkeypatch.setattr(requests, "get", MockResponse) + + try: + get_gnome_extension_download_url( + "gsconnect@andyholmes.github.io", ext_ver, gnome_shell_ver) + except Exception as e: + assert str(e) == expected_result diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..8d2c302 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,106 @@ +import pytest + +from src.gnome_extension_download_url.__main__ import main, HELP_MSG + + +def test_main_empty_args(capsys, mocker): + mocker.patch("sys.argv", ["/path/to/gnome-extension-download-url"]) + try: + main() + except SystemExit as e: + assert e.code == 1 + captured = capsys.readouterr() + assert captured.err == f"{HELP_MSG}\n" + + +def test_main_help(capsys, mocker): + mocker.patch("sys.argv", ["/path/to/gnome-extension-download-url", "--help"]) + try: + main() + except SystemExit as e: + assert e.code == 0 + captured = capsys.readouterr() + assert captured.out == f"{HELP_MSG}\n" + + +def test_main_too_many_arguments(capsys, mocker): + mocker.patch("sys.argv", [ + "/path/to/gnome-extension-download-url", + "1", "2", "3", "4" + ]) + try: + main() + except SystemExit as e: + assert e.code == 1 + captured = capsys.readouterr() + assert captured.err == "Too many arguments. Use --help for usage.\n" + + +def test_main_valid_args_only_extension_uuid(capsys, mocker): + mock_get_gnome_extension_download_url = mocker.patch( + "src.gnome_extension_download_url.__main__.get_gnome_extension_download_url", + return_value="http://example.com/download") + mocker.patch("sys.argv", [ + "/path/to/gnome-extension-download-url", + "extension-uuid", + ]) + try: + main() + except SystemExit as e: + assert e.code == 0 + captured = capsys.readouterr() + assert captured.out == "http://example.com/download\n" + mock_get_gnome_extension_download_url.assert_called_once_with( + "extension-uuid", None, None) + + +@pytest.mark.parametrize("extension_version", ["", "latest", "66"]) +def test_main_valid_args_extension_uuid_and_extension_version( + extension_version, capsys, mocker): + mock_get_gnome_extension_download_url= mocker.patch( + "src.gnome_extension_download_url.__main__.get_gnome_extension_download_url", + return_value="http://example.com/download") + mocker.patch("sys.argv", [ + "/path/to/gnome-extension-download-url", + "extension-uuid", + extension_version, + ]) + try: + main() + except SystemExit as e: + assert e.code == 0 + captured = capsys.readouterr() + assert captured.out == "http://example.com/download\n" + mock_get_gnome_extension_download_url.assert_called_once_with( + "extension-uuid", extension_version if extension_version != "" else None, + None) + + +@pytest.mark.parametrize("extension_version, gnome_shell_version", + [ + ("", ""), ("", "latest"), ("", "66"), + ("latest", ""), ("latest", "latest"), + ("latest", "3.38"), + ("66", ""), ("66", "latest"), ("66", "46") + ]) +def test_main_valid_args_extension_uuid_extension_version_and_gnome_shell_version( + extension_version, gnome_shell_version, capsys, mocker): + mock_get_gnome_extension_download_url= mocker.patch( + "src.gnome_extension_download_url.__main__.get_gnome_extension_download_url", + return_value="http://example.com/download") + mocker.patch("sys.argv", [ + "/path/to/gnome-extension-download-url", + "extension-uuid", + extension_version, + gnome_shell_version, + ]) + try: + main() + except SystemExit as e: + assert e.code == 0 + captured = capsys.readouterr() + assert captured.out == "http://example.com/download\n" + mock_get_gnome_extension_download_url.assert_called_once_with( + "extension-uuid", + extension_version if extension_version != "" else None, + gnome_shell_version if gnome_shell_version != "" else None) \ No newline at end of file diff --git a/tests/test_print_help.py b/tests/test_print_help.py new file mode 100644 index 0000000..75c39a4 --- /dev/null +++ b/tests/test_print_help.py @@ -0,0 +1,15 @@ +from src.gnome_extension_download_url.__main__ import print_help, HELP_MSG + + +def test_print_help_no_error(capsys): + print_help() + captured = capsys.readouterr() + assert captured.out == f"{HELP_MSG}\n" + assert captured.err == "" + + +def test_print_help_error(capsys): + print_help(error=True) + captured = capsys.readouterr() + assert captured.err == f"{HELP_MSG}\n" + assert captured.out == "" \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..df69744 --- /dev/null +++ b/uv.lock @@ -0,0 +1,172 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "gnome-extension-download-url" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-mock" }, +] + +[package.metadata] +requires-dist = [{ name = "requests", specifier = ">=2.32.5" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-mock", specifier = ">=3.15.0" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/99/3323ee5c16b3637b4d941c362182d3e749c11e400bea31018c42219f3a98/pytest_mock-3.15.0.tar.gz", hash = "sha256:ab896bd190316b9d5d87b277569dfcdf718b2d049a2ccff5f7aca279c002a1cf", size = 33838, upload-time = "2025-09-04T20:57:48.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/b3/7fefc43fb706380144bcd293cc6e446e6f637ddfa8b83f48d1734156b529/pytest_mock-3.15.0-py3-none-any.whl", hash = "sha256:ef2219485fb1bd256b00e7ad7466ce26729b30eadfc7cbcdb4fa9a92ca68db6f", size = 10050, upload-time = "2025-09-04T20:57:47.274Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +]