From 830f300822c26777d85189aa731418e2ac4147d3 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Thu, 8 May 2025 08:54:57 -0400 Subject: [PATCH 1/6] Upgraded tooling and testing Due to changes in tooling from the originals used file formats have changed. pnpm 10.10.0 rye 0.44.0 ruff 0.11.8 CI is now testing on a matrix of pnpm, node, and python versions. This will hopefully cover edgecases where users are running various version. Still needs update to use python version in matrix with `rye`. Installs OS deps in workflow Adds 'packages' key in workspace form pnpm 9 Makes testing for BaseExternal configurable Adds redis and httpbin as service containers ruff lint changed dictionary comprehensions adds environment variables for httpbin Fixes runner to docker communications --- .github/workflows/backend.yml | 51 ++++++++++++++----- .github/workflows/frontend.yml | 15 +++--- hyperglass/api/__init__.py | 1 + hyperglass/cli/echo.py | 1 + hyperglass/cli/main.py | 11 ++-- hyperglass/compat/_sshtunnel.py | 24 ++++----- hyperglass/exceptions/_common.py | 2 +- hyperglass/execution/drivers/_construct.py | 4 +- hyperglass/execution/drivers/ssh.py | 6 +-- hyperglass/external/_base.py | 23 ++++++--- hyperglass/external/bgptools.py | 4 +- hyperglass/external/tests/test_base.py | 40 +++++++++++---- hyperglass/external/tests/test_bgptools.py | 1 - hyperglass/external/tests/test_rpki.py | 9 ++-- hyperglass/models/api/__init__.py | 1 + hyperglass/models/config/cache.py | 1 - hyperglass/models/tests/test_util.py | 6 +-- .../plugins/tests/test_bgp_community.py | 1 + hyperglass/state/tests/test_hooks.py | 6 +-- hyperglass/ui/pnpm-lock.yaml | 10 ++-- hyperglass/ui/pnpm-workspace.yaml | 5 ++ pyproject.toml | 10 ++-- requirements-dev.lock | 4 +- requirements.lock | 2 + 24 files changed, 152 insertions(+), 86 deletions(-) create mode 100644 hyperglass/ui/pnpm-workspace.yaml diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 7032fe17..92fb40c4 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -12,39 +12,56 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x] - pnpm-version: [8] + node-version: [20, 22] + pnpm-version: [9, 10] redis-version: [latest] - python-version: ["3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] os: [ubuntu-latest] runs-on: ${{ matrix.os }} + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + httpbin: + image: kennethreitz/httpbin + ports: + - 8080:80 + steps: - name: Git Checkout uses: actions/checkout@v3 + - name: Install OS dependencies + run: | + sudo apt-get install libcairo2-dev clang curl -y + - name: Install Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Setup Rye - uses: sksat/setup-rye@v0.23.1 - - - name: Install Node - uses: actions/setup-node@v4 + - name: Install the latest version of rye + uses: eifinger/setup-rye@v4 with: - node-version: ${{ matrix.node-version }} + enable-cache: true - name: Install PNPM uses: pnpm/action-setup@v3 with: version: ${{ matrix.pnpm-version }} + run_install: false - - name: Start Redis - uses: supercharge/redis-github-action@1.7.0 + - name: Install Node + uses: actions/setup-node@v4 with: - redis-version: ${{ matrix.redis-version }} + node-version: ${{ matrix.node-version }} - name: Prepare run: | @@ -56,7 +73,7 @@ jobs: - name: Install run: rye sync - - name: Activate virtualenv + - name: Activate virtual environment run: | . .venv/bin/activate echo PATH=$PATH >> $GITHUB_ENV @@ -64,8 +81,16 @@ jobs: - name: Lint (Rye) run: rye lint + - name: Format (Rye) + run: rye format -- --check + - name: Tests (PyTest) run: pytest hyperglass --ignore hyperglass/plugins/external + env: + HYPERGLASS_HTTPBIN_HOST: localhost + HYPERGLASS_HTTPBIN_PROTOCOL: http + HYPERGLASS_HTTPBIN_PORT: 8080 + HYPERGLASS_REDIS_HOST: localhost - name: Run hyperglass run: ".tests/ga-backend-app.sh" diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 55bf46fe..d6c9ee06 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -12,8 +12,8 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x] - pnpm-version: [8] + node-version: [20, 22] + pnpm-version: [9, 10] os: [ubuntu-latest] runs-on: ${{ matrix.os }} env: @@ -22,15 +22,16 @@ jobs: - name: Git Checkout uses: actions/checkout@v3 - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - name: Install PNPM uses: pnpm/action-setup@v3 with: version: ${{ matrix.pnpm-version }} + run_install: false + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} - name: Install Dependencies working-directory: ${{ env.working-directory }} diff --git a/hyperglass/api/__init__.py b/hyperglass/api/__init__.py index 5684629c..65458f96 100644 --- a/hyperglass/api/__init__.py +++ b/hyperglass/api/__init__.py @@ -1,4 +1,5 @@ """hyperglass API.""" + # Standard Library import logging diff --git a/hyperglass/cli/echo.py b/hyperglass/cli/echo.py index 642c1ce0..4f7ced0c 100644 --- a/hyperglass/cli/echo.py +++ b/hyperglass/cli/echo.py @@ -1,4 +1,5 @@ """Helper functions for CLI message printing.""" + # Standard Library import typing as t diff --git a/hyperglass/cli/main.py b/hyperglass/cli/main.py index 2ee2107f..7cc06f60 100644 --- a/hyperglass/cli/main.py +++ b/hyperglass/cli/main.py @@ -33,7 +33,7 @@ def run(): def _version( version: t.Optional[bool] = typer.Option( None, "--version", help="hyperglass version", callback=_version - ) + ), ) -> None: """hyperglass""" pass @@ -77,7 +77,6 @@ def _build_ui(timeout: int = typer.Option(180, help="Timeout in seconds")) -> No with echo._console.status( f"Starting new UI build with a {timeout} second timeout...", spinner="aesthetic" ): - _build_ui(timeout=120) @@ -140,7 +139,7 @@ def _clear_cache(): @cli.command(name="devices") def _devices( - search: t.Optional[str] = typer.Argument(None, help="Device ID or Name Search Pattern") + search: t.Optional[str] = typer.Argument(None, help="Device ID or Name Search Pattern"), ): """Show all configured devices""" # Third Party @@ -189,7 +188,7 @@ def _devices( @cli.command(name="directives") def _directives( - search: t.Optional[str] = typer.Argument(None, help="Directive ID or Name Search Pattern") + search: t.Optional[str] = typer.Argument(None, help="Directive ID or Name Search Pattern"), ): """Show all configured devices""" # Third Party @@ -280,7 +279,7 @@ def _plugins( def _params( path: t.Optional[str] = typer.Argument( None, help="Parameter Object Path, for example 'messages.no_input'" - ) + ), ): """Show configuration parameters""" # Standard Library @@ -312,7 +311,7 @@ def _params( ) raise typer.Exit(0) except AttributeError: - echo.error(f"{'params.'+path!r} does not exist") + echo.error(f"{'params.' + path!r} does not exist") raise typer.Exit(1) panel = Inspect( diff --git a/hyperglass/compat/_sshtunnel.py b/hyperglass/compat/_sshtunnel.py index ca6547f7..f852fa5c 100644 --- a/hyperglass/compat/_sshtunnel.py +++ b/hyperglass/compat/_sshtunnel.py @@ -114,8 +114,9 @@ def check_address(address): raise ValueError("ADDRESS not a valid socket domain socket ({0})".format(address)) else: raise ValueError( - "ADDRESS is not a tuple, string, or character buffer " - "({0})".format(type(address).__name__) + "ADDRESS is not a tuple, string, or character buffer ({0})".format( + type(address).__name__ + ) ) @@ -147,7 +148,7 @@ def check_addresses(address_list, is_remote=False): """ assert all(isinstance(x, (tuple, str)) for x in address_list) if is_remote and any(isinstance(x, str) for x in address_list): - raise AssertionError("UNIX domain sockets not allowed for remote" "addresses") + raise AssertionError("UNIX domain sockets not allowed for remoteaddresses") for address in address_list: check_address(address) @@ -272,7 +273,9 @@ def __init__(self, *args, **kwargs): def handle_error(self, request, client_address): (exc_class, exc, tb) = sys.exc_info() - self.logger.bind(source=request.getsockname()).error("Could not establish connection to remote side of the tunnel") + self.logger.bind(source=request.getsockname()).error( + "Could not establish connection to remote side of the tunnel" + ) self.tunnel_ok.put(False) @property @@ -937,7 +940,7 @@ def _consolidate_binds(local_binds, remote_binds): count = len(remote_binds) - len(local_binds) if count < 0: raise ValueError( - "Too many local bind addresses " "(local_bind_addresses > remote_bind_addresses)" + "Too many local bind addresses (local_bind_addresses > remote_bind_addresses)" ) local_binds.extend([("0.0.0.0", 0) for x in range(count)]) return local_binds @@ -1023,7 +1026,7 @@ def _create_tunnels(self): msg = template.format(self.ssh_host, self.ssh_port, e.args[0]) self.logger.error(msg) return - for (rem, loc) in zip(self._remote_binds, self._local_binds): + for rem, loc in zip(self._remote_binds, self._local_binds): try: self._make_ssh_forward_server(rem, loc) except BaseSSHTunnelForwarderError as e: @@ -1053,7 +1056,7 @@ def _get_binds(bind_address, bind_addresses, is_remote=False): bind_addresses = [bind_address] if not is_remote: # Add random port if missing in local bind - for (i, local_bind) in enumerate(bind_addresses): + for i, local_bind in enumerate(bind_addresses): if isinstance(local_bind, tuple) and len(local_bind) == 1: bind_addresses[i] = (local_bind[0], 0) check_addresses(bind_addresses, is_remote) @@ -1074,8 +1077,7 @@ def _process_deprecated(attrib, deprecated_attrib, kwargs): ) if attrib: raise ValueError( - "You can't use both '{0}' and '{1}'. " - "Please only use one of them".format( + "You can't use both '{0}' and '{1}'. Please only use one of them".format( deprecated_attrib, DEPRECATIONS[deprecated_attrib] ) ) @@ -1119,7 +1121,6 @@ def read_private_key_file(pkey_file, pkey_password=None, key_type=None, logger=l break except paramiko.PasswordRequiredException: - logger.error("Password is required for key {k}", k=pkey_file) break @@ -1319,7 +1320,6 @@ def _stop_transport(self) -> None: @property def local_bind_port(self): - # BACKWARDS COMPATIBILITY self._check_is_started() if len(self._server_list) != 1: @@ -1330,7 +1330,6 @@ def local_bind_port(self): @property def local_bind_host(self): - # BACKWARDS COMPATIBILITY self._check_is_started() if len(self._server_list) != 1: @@ -1341,7 +1340,6 @@ def local_bind_host(self): @property def local_bind_address(self): - # BACKWARDS COMPATIBILITY self._check_is_started() if len(self._server_list) != 1: diff --git a/hyperglass/exceptions/_common.py b/hyperglass/exceptions/_common.py index 99633247..c36a1ce5 100644 --- a/hyperglass/exceptions/_common.py +++ b/hyperglass/exceptions/_common.py @@ -72,7 +72,7 @@ def _parse_pydantic_errors(*errors: Dict[str, Any]) -> str: for err in errors: loc = " → ".join(str(loc) for loc in err["loc"]) - errs += (f'Field: {loc}\n Error: {err["msg"]}\n',) + errs += (f"Field: {loc}\n Error: {err['msg']}\n",) return "\n".join(errs) diff --git a/hyperglass/execution/drivers/_construct.py b/hyperglass/execution/drivers/_construct.py index 86032bc7..c1d1e2b5 100644 --- a/hyperglass/execution/drivers/_construct.py +++ b/hyperglass/execution/drivers/_construct.py @@ -94,7 +94,7 @@ def format(self, command: str) -> str: for key in [k for k in keys if k != "target" and k != "mask"]: if key not in attrs: raise ConfigError( - ("Command '{c}' has attribute '{k}', " "which is missing from device '{d}'"), + ("Command '{c}' has attribute '{k}', which is missing from device '{d}'"), level="danger", c=self.directive.name, k=key, @@ -224,4 +224,4 @@ def _bird_bgp_aspath(self, target: str) -> str: def _bird_bgp_community(self, target: str) -> str: """Convert from standard community format to BIRD format.""" parts = target.split(":") - return f'({",".join(parts)})' + return f"({','.join(parts)})" diff --git a/hyperglass/execution/drivers/ssh.py b/hyperglass/execution/drivers/ssh.py index 13e070ef..25d81b4f 100644 --- a/hyperglass/execution/drivers/ssh.py +++ b/hyperglass/execution/drivers/ssh.py @@ -44,9 +44,9 @@ def opener(): if proxy.credential._method == "encrypted_key": # If the key is encrypted, use the password field as the # private key password. - tunnel_kwargs[ - "ssh_private_key_password" - ] = proxy.credential.password.get_secret_value() + tunnel_kwargs["ssh_private_key_password"] = ( + proxy.credential.password.get_secret_value() + ) try: return open_tunnel(proxy._target, proxy.port, **tunnel_kwargs) diff --git a/hyperglass/external/_base.py b/hyperglass/external/_base.py index 60c35da1..8e2c1305 100644 --- a/hyperglass/external/_base.py +++ b/hyperglass/external/_base.py @@ -1,7 +1,7 @@ """Session handler for external http data sources.""" # Standard Library -import re +from urllib.parse import urlparse import json as _json import socket import typing as t @@ -167,14 +167,25 @@ def _test(self: "BaseExternal") -> bool: try: # Parse out just the hostname from a URL string. # E.g. `https://www.example.com` becomes `www.example.com` - test_host = re.sub(r"http(s)?\:\/\/", "", self.base_url) + parsed = urlparse(self.base_url) + host_parts = parsed.netloc.split(":") + try: + test_host, test_port = host_parts + test_port = int(test_port) + except ValueError: + test_host = host_parts[0] + match parsed.scheme: + case "http": + test_port = 80 + case "https": + test_port = 443 # Create a generic socket object test_socket = socket.socket() # Try opening a low-level socket to make sure it's even # listening on the port prior to trying to use it. - test_socket.connect((test_host, 443)) + test_socket.connect((test_host, test_port)) # Properly shutdown & close the socket. test_socket.shutdown(1) @@ -212,7 +223,7 @@ def _build_request(self: "BaseExternal", **kwargs: t.Any) -> t.Dict[str, t.Any]: if method.upper() not in supported_methods: raise self._exception( - f'Method must be one of {", ".join(supported_methods)}. ' f"Got: {str(method)}" + f"Method must be one of {', '.join(supported_methods)}. Got: {str(method)}" ) endpoint = "/".join( @@ -284,7 +295,7 @@ async def _arequest( # noqa: C901 status = httpx.codes(response.status_code) error = self._parse_response(response) raise self._exception( - f'{status.name.replace("_", " ")}: {error}', level="danger" + f"{status.name.replace('_', ' ')}: {error}", level="danger" ) from None except httpx.HTTPError as http_err: @@ -340,7 +351,7 @@ def _request( # noqa: C901 status = httpx.codes(response.status_code) error = self._parse_response(response) raise self._exception( - f'{status.name.replace("_", " ")}: {error}', level="danger" + f"{status.name.replace('_', ' ')}: {error}", level="danger" ) from None except httpx.HTTPError as http_err: diff --git a/hyperglass/external/bgptools.py b/hyperglass/external/bgptools.py index 1ea1cee8..f2fd7f6a 100644 --- a/hyperglass/external/bgptools.py +++ b/hyperglass/external/bgptools.py @@ -35,7 +35,7 @@ def default_ip_targets(*targets: str) -> t.Tuple[TargetData, t.Tuple[str, ...]]: default_data = {} query = () for target in targets: - detail: TargetDetail = {k: "None" for k in DEFAULT_KEYS} + detail: TargetDetail = dict.fromkeys(DEFAULT_KEYS, "None") try: valid: t.Union[IPv4Address, IPv6Address] = ip_address(target) @@ -139,7 +139,7 @@ async def network_info(*targets: str) -> TargetData: cache = use_state("cache") # Set default data structure. - query_data = {t: {k: "" for k in DEFAULT_KEYS} for t in query_targets} + query_data = {t: dict.fromkeys(DEFAULT_KEYS, "") for t in query_targets} # Get all cached bgp.tools data. cached = cache.get_map(CACHE_KEY) or {} diff --git a/hyperglass/external/tests/test_base.py b/hyperglass/external/tests/test_base.py index 5f235942..5424f5f8 100644 --- a/hyperglass/external/tests/test_base.py +++ b/hyperglass/external/tests/test_base.py @@ -1,6 +1,8 @@ """Test external http client.""" + # Standard Library import asyncio +import os # Third Party import pytest @@ -12,38 +14,54 @@ # Local from .._base import BaseExternal -config = Http(provider="generic", host="https://httpbin.org") + +@pytest.fixture +def httpbin_url(): + # get HYPERGLASS_TEST_HTTPBIN + httpbin_host: str = os.environ.get("HYPERGLASS_HTTPBIN_HOST", "httpbin.org") + httpbin_port: int = os.environ.get("HYPERGLASS_HTTPBIN_PORT", 443) + httpbin_protocol: str = os.environ.get("HYPERGLASS_HTTPBIN_PROTOCOL", "https") + + url = f"{httpbin_protocol}://{httpbin_host}" + if httpbin_port != 443 and httpbin_port != 80: + url = f"{url}:{httpbin_port}" + return url + + +@pytest.fixture +def httpbin_config(httpbin_url): + return Http(provider="generic", host=httpbin_url) -def test_base_external_sync(): - with BaseExternal(base_url="https://httpbin.org", config=config) as client: +def test_base_external_sync(httpbin_url, httpbin_config): + with BaseExternal(base_url=httpbin_url, config=httpbin_config) as client: res1 = client._get("/get") res2 = client._get("/get", params={"key": "value"}) res3 = client._post("/post", data={"strkey": "value", "intkey": 1}) - assert res1["url"] == "https://httpbin.org/get" + assert res1["url"] == f"{httpbin_url}/get" assert res2["args"].get("key") == "value" assert res3["json"].get("strkey") == "value" assert res3["json"].get("intkey") == 1 with pytest.raises(ExternalError): - with BaseExternal(base_url="https://httpbin.org", config=config, timeout=2) as client: + with BaseExternal(base_url=httpbin_url, config=httpbin_config, timeout=2) as client: client._get("/delay/4") -async def _run_test_base_external_async(): - async with BaseExternal(base_url="https://httpbin.org", config=config) as client: +async def _run_test_base_external_async(httpbin_url, httpbin_config): + async with BaseExternal(base_url=httpbin_url, config=httpbin_config) as client: res1 = await client._aget("/get") res2 = await client._aget("/get", params={"key": "value"}) res3 = await client._apost("/post", data={"strkey": "value", "intkey": 1}) - assert res1["url"] == "https://httpbin.org/get" + assert res1["url"] == f"{httpbin_url}/get" assert res2["args"].get("key") == "value" assert res3["json"].get("strkey") == "value" assert res3["json"].get("intkey") == 1 with pytest.raises(ExternalError): - async with BaseExternal(base_url="https://httpbin.org", config=config, timeout=2) as client: + async with BaseExternal(base_url=httpbin_url, config=httpbin_config, timeout=2) as client: await client._get("/delay/4") -def test_base_external_async(): - asyncio.run(_run_test_base_external_async()) +def test_base_external_async(httpbin_url, httpbin_config): + asyncio.run(_run_test_base_external_async(httpbin_url, httpbin_config)) diff --git a/hyperglass/external/tests/test_bgptools.py b/hyperglass/external/tests/test_bgptools.py index ca8f98da..542c1dc2 100644 --- a/hyperglass/external/tests/test_bgptools.py +++ b/hyperglass/external/tests/test_bgptools.py @@ -16,7 +16,6 @@ # Ignore asyncio deprecation warning about loop @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_network_info(): - checks = ( ("192.0.2.1", {"asn": "None", "rir": "Private Address"}), ("127.0.0.1", {"asn": "None", "rir": "Loopback Address"}), diff --git a/hyperglass/external/tests/test_rpki.py b/hyperglass/external/tests/test_rpki.py index 66da2e47..01438e71 100644 --- a/hyperglass/external/tests/test_rpki.py +++ b/hyperglass/external/tests/test_rpki.py @@ -1,4 +1,5 @@ """Test RPKI data fetching.""" + # Third Party import pytest @@ -18,8 +19,8 @@ def test_rpki(): result = rpki_state(prefix, asn) result_name = RPKI_NAME_MAP.get(result, "No Name") expected_name = RPKI_NAME_MAP.get(expected, "No Name") - assert ( - result == expected - ), "RPKI State for '{}' via AS{!s} '{}' ({}) instead of '{}' ({})".format( - prefix, asn, result, result_name, expected, expected_name + assert result == expected, ( + "RPKI State for '{}' via AS{!s} '{}' ({}) instead of '{}' ({})".format( + prefix, asn, result, result_name, expected, expected_name + ) ) diff --git a/hyperglass/models/api/__init__.py b/hyperglass/models/api/__init__.py index 70769f42..8f4bfcd2 100644 --- a/hyperglass/models/api/__init__.py +++ b/hyperglass/models/api/__init__.py @@ -1,4 +1,5 @@ """Query & Response Validation Models.""" + # Local from .query import Query from .response import ( diff --git a/hyperglass/models/config/cache.py b/hyperglass/models/config/cache.py index 0871b947..f3c3e93e 100644 --- a/hyperglass/models/config/cache.py +++ b/hyperglass/models/config/cache.py @@ -1,6 +1,5 @@ """Validation model for cache config.""" - # Local from ..main import HyperglassModel diff --git a/hyperglass/models/tests/test_util.py b/hyperglass/models/tests/test_util.py index bb133d63..f990d579 100644 --- a/hyperglass/models/tests/test_util.py +++ b/hyperglass/models/tests/test_util.py @@ -19,9 +19,9 @@ def test_check_legacy_fields(): test1_expected.keys() ), "legacy field not replaced" - assert set(check_legacy_fields(model="Device", data=test2).keys()) == set( - test2.keys() - ), "new field not left unmodified" + assert set(check_legacy_fields(model="Device", data=test2).keys()) == set(test2.keys()), ( + "new field not left unmodified" + ) with pytest.raises(ValueError): check_legacy_fields(model="Device", data=test3) diff --git a/hyperglass/plugins/tests/test_bgp_community.py b/hyperglass/plugins/tests/test_bgp_community.py index d5f88342..3541244a 100644 --- a/hyperglass/plugins/tests/test_bgp_community.py +++ b/hyperglass/plugins/tests/test_bgp_community.py @@ -1,4 +1,5 @@ """Test BGP Community validation.""" + # Standard Library import typing as t diff --git a/hyperglass/state/tests/test_hooks.py b/hyperglass/state/tests/test_hooks.py index aaa9d5c5..e82f1a43 100644 --- a/hyperglass/state/tests/test_hooks.py +++ b/hyperglass/state/tests/test_hooks.py @@ -96,7 +96,7 @@ def test_use_state_caching(state): instance = use_state(attr) if i == 0: first = instance - assert isinstance( - instance, model - ), f"{instance!r} is not an instance of '{model.__name__}'" + assert isinstance(instance, model), ( + f"{instance!r} is not an instance of '{model.__name__}'" + ) assert instance == first, f"{instance!r} is not equal to {first!r}" diff --git a/hyperglass/ui/pnpm-lock.yaml b/hyperglass/ui/pnpm-lock.yaml index f987d8d6..b50dc286 100644 --- a/hyperglass/ui/pnpm-lock.yaml +++ b/hyperglass/ui/pnpm-lock.yaml @@ -116,7 +116,7 @@ importers: version: 1.5.3 '@testing-library/jest-dom': specifier: ^6.4.2 - version: 6.4.2(vitest@1.3.1(@types/node@20.11.20)(@vitest/ui@1.3.1)(jsdom@24.0.0)) + version: 6.4.2(vitest@1.3.1) '@testing-library/react': specifier: ^14.2.1 version: 14.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -5400,7 +5400,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.2(vitest@1.3.1(@types/node@20.11.20)(@vitest/ui@1.3.1)(jsdom@24.0.0))': + '@testing-library/jest-dom@6.4.2(vitest@1.3.1)': dependencies: '@adobe/css-tools': 4.3.3 '@babel/runtime': 7.23.9 @@ -6467,7 +6467,7 @@ snapshots: debug: 4.3.4 enhanced-resolve: 5.15.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -6479,7 +6479,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -6500,7 +6500,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.1 is-core-module: 2.13.1 is-glob: 4.0.3 diff --git a/hyperglass/ui/pnpm-workspace.yaml b/hyperglass/ui/pnpm-workspace.yaml new file mode 100644 index 00000000..a469382b --- /dev/null +++ b/hyperglass/ui/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +onlyBuiltDependencies: + - '@biomejs/biome' + - esbuild +packages: + - . diff --git a/pyproject.toml b/pyproject.toml index c5cf96fe..44a1c486 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ dev-dependencies = [ "pytest>=8.0.1", "pytest-asyncio>=0.23.5", "pytest-dependency>=0.6.0", - "ruff>=0.2.1", + "ruff>=0.11", "stackprinter>=0.2.11", "taskipy>=1.12.2", ] @@ -99,6 +99,9 @@ upgrade = {cmd = "python3 version.py", help = "Upgrade hyperglass version"} pnpm = {cmd = "pnpm run --dir ./hyperglass/ui/", help = "Run a yarn command from the UI directory"} [tool.ruff] +line-length = 100 + +[tool.ruff.lint] exclude = [ ".git", "__pycache__", @@ -128,13 +131,12 @@ ignore = [ "B905", # zip without `strict` "W293", # blank line contains whitespace ] -line-length = 100 select = ["B", "C", "D", "E", "F", "I", "N", "S", "RET", "W"] -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "pep257" -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 10 [tool.ruff.lint.per-file-ignores] diff --git a/requirements-dev.lock b/requirements-dev.lock index 1796da39..ce49492b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -6,6 +6,8 @@ # features: [] # all-features: false # with-sources: false +# generate-hashes: false +# universal: false -e file:. aiofiles==23.2.1 @@ -201,7 +203,7 @@ rich-click==1.7.4 # via litestar rlpycairo==0.3.0 # via favicons -ruff==0.2.2 +ruff==0.11.8 scp==0.14.5 # via netmiko setuptools==69.1.0 diff --git a/requirements.lock b/requirements.lock index 016cc743..b1517092 100644 --- a/requirements.lock +++ b/requirements.lock @@ -6,6 +6,8 @@ # features: [] # all-features: false # with-sources: false +# generate-hashes: false +# universal: false -e file:. aiofiles==23.2.1 From b6b886c58b822c2132210f100707be462d8025c5 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Tue, 13 May 2025 18:39:10 -0400 Subject: [PATCH 2/6] Use system python --- .github/workflows/backend.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 92fb40c4..07019253 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -69,9 +69,13 @@ jobs: echo "HYPERGLASS_APP_PATH=$HOME/hyperglass" >> $GITHUB_ENV echo "HYPERGLASS_HOST=127.0.0.1" >> $GITHUB_ENV echo "HYPERGLASS_PORT=8001" >> $GITHUB_ENV + echo "PYTHON3_PATH=$(which python3)" >> $GITHUB_ENV - name: Install - run: rye sync + run: | + rye toolchain register $PYTHON_PATH + rye pin ${{ matrix.python-version }} + rye sync - name: Activate virtual environment run: | From c385222ede3666fe9ec1b94311f876e4161bd766 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Tue, 13 May 2025 18:43:39 -0400 Subject: [PATCH 3/6] Fixes PYTHON3_PATH env var --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 07019253..3f2b4bfb 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -73,7 +73,7 @@ jobs: - name: Install run: | - rye toolchain register $PYTHON_PATH + rye toolchain register $PYTHON3_PATH rye pin ${{ matrix.python-version }} rye sync From 4ea821da998d2d6136afc23aaaf647fdd120d0e8 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Tue, 13 May 2025 18:46:13 -0400 Subject: [PATCH 4/6] Fixes PYTHON3_PATH env var --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 3f2b4bfb..1e10e40e 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -69,7 +69,7 @@ jobs: echo "HYPERGLASS_APP_PATH=$HOME/hyperglass" >> $GITHUB_ENV echo "HYPERGLASS_HOST=127.0.0.1" >> $GITHUB_ENV echo "HYPERGLASS_PORT=8001" >> $GITHUB_ENV - echo "PYTHON3_PATH=$(which python3)" >> $GITHUB_ENV + echo "PYTHON3_PATH=$(which python)" >> $GITHUB_ENV - name: Install run: | From b7a00bbda68c69ca4779e333f566d70fc66a8aff Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Tue, 13 May 2025 18:50:46 -0400 Subject: [PATCH 5/6] Fixes PYTHON3_PATH env var --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 1e10e40e..6a1c7337 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -73,7 +73,7 @@ jobs: - name: Install run: | - rye toolchain register $PYTHON3_PATH + rye toolchain register $Python3_ROOT_DIR/bin/python rye pin ${{ matrix.python-version }} rye sync From d0bc6ebf50d7dfb29f5191b8681b3ecb4eee16ed Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Thu, 15 May 2025 16:39:12 -0400 Subject: [PATCH 6/6] removes 3.12 from testing httptools is not compatible with 3.13 --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 6a1c7337..84e2b23f 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -15,7 +15,7 @@ jobs: node-version: [20, 22] pnpm-version: [9, 10] redis-version: [latest] - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12"] os: [ubuntu-latest] runs-on: ${{ matrix.os }}