diff --git a/softpack_core/service.py b/softpack_core/service.py index d2e079c..0d0876a 100644 --- a/softpack_core/service.py +++ b/softpack_core/service.py @@ -319,3 +319,19 @@ async def remove_recipe( # type: ignore[no-untyped-def] return {"error": e} return {"message": "Request Removed"} + + @staticmethod + @router.post("/getRecipeDescription") + async def recipe_description( # type: ignore[no-untyped-def] + request: Request, + ): + """Return the description for a recipe.""" + data = await request.json() + + if ( + not isinstance(data["recipe"], str) + or data["recipe"] not in app.spack.descriptions + ): + return {"error": "Invalid Input"} + + return {"description": app.spack.descriptions[data["recipe"]]} diff --git a/softpack_core/spack.py b/softpack_core/spack.py index be72f15..d10bd43 100644 --- a/softpack_core/spack.py +++ b/softpack_core/spack.py @@ -4,11 +4,11 @@ LICENSE file in the root directory of this source tree. """ -import json import subprocess import tempfile import threading from dataclasses import dataclass +from html.parser import HTMLParser from os import path from typing import Tuple @@ -27,6 +27,54 @@ class Package(PackageBase): versions: list[str] +class SpackHTMLParser(HTMLParser): + """Class to parse HTML output from `spack list --format=html`.""" + + def __init__(self) -> None: + """Init class.""" + super().__init__() + + self.onDT = False + self.onDD = True + self.nextIsVersion = False + self.nextIsDescription = False + self.recipe = "" + + self.versions: list[Package] = list() + self.descriptions: dict[str, str] = dict() + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + """Handle open tags.""" + if tag == "div": + self.recipe = next( + attr[1] for attr in attrs if attr[0] == "id" and isinstance(attr[1], str) + ) + elif tag == "dt": + self.onDT = True + elif tag == "dd": + self.onDD = True + + def handle_data(self, data: str) -> None: + """Handle data.""" + if self.onDT: + self.onDT = False + self.nextIsVersion = data == "Versions:" + self.nextIsDescription = data == "Description:" + elif self.onDD: + self.onDD = False + + if self.nextIsVersion: + self.nextIsVersion = False + self.versions.append( + Package( + name=self.recipe, versions=data.strip().split(", ") + ) + ) + elif self.nextIsDescription: + self.nextIsDescription = False + self.descriptions[self.recipe] = data.strip() + + class Spack: """Spack interface class.""" @@ -40,6 +88,7 @@ def __init__( ) -> None: """Constructor.""" self.stored_packages: list[Package] = [] + self.descriptions: dict[str, str] = dict() self.checkout_path = "" self.spack_exe = spack_exe self.cacheDir = cache @@ -81,24 +130,19 @@ def store_packages_from_spack( spack_exe (str): Path to the spack executable. checkout_path (str): Path to the cloned custom spack repo. """ - jsonData, didReadFromCache = self.__readPackagesFromCacheOnce() + htmlData, didReadFromCache = self.__readPackagesFromCacheOnce() if not didReadFromCache: - jsonData = self.__getPackagesFromSpack(spack_exe, checkout_path) - - self.__writeToCache(jsonData) - - self.stored_packages = list( - map( - lambda package: Package( - name=package.get("name"), - versions=[ - str(ver) for ver in list(package.get("versions")) - ], - ), - json.loads(jsonData), - ) - ) + htmlData = self.__getPackagesFromSpack(spack_exe, checkout_path) + + self.__writeToCache(htmlData) + + shp = SpackHTMLParser() + + shp.feed(htmlData.decode("utf-8")) + + self.stored_packages = shp.versions + self.descriptions = shp.descriptions self.packagesUpdated = True def __readPackagesFromCacheOnce(self) -> Tuple[bytes, bool]: @@ -156,7 +200,7 @@ def __getPackagesFromSpack( ) -> bytes: if checkout_path == "": result = subprocess.run( - [spack_exe, "list", "--format", "version_json"], + [spack_exe, "list", "--format", "html"], capture_output=True, ) @@ -169,7 +213,7 @@ def __getPackagesFromSpack( "repos:[" + checkout_path + "]", "list", "--format", - "version_json", + "html", ], capture_output=True, ) diff --git a/tests/integration/test_recipe_requests.py b/tests/integration/test_recipe_requests.py index e7f52a7..909b223 100644 --- a/tests/integration/test_recipe_requests.py +++ b/tests/integration/test_recipe_requests.py @@ -4,6 +4,7 @@ LICENSE file in the root directory of this source tree. """ +import pytest from fastapi.testclient import TestClient from softpack_core.app import app @@ -15,6 +16,8 @@ ) from tests.integration.utils import builder_called_correctly +pytestmark = pytest.mark.repo + def test_request_recipe(httpx_post, testable_env_input): client = TestClient(app.router) diff --git a/tests/integration/test_spack.py b/tests/integration/test_spack.py index 299a4a6..96b3893 100644 --- a/tests/integration/test_spack.py +++ b/tests/integration/test_spack.py @@ -40,6 +40,11 @@ def test_spack_packages(): assert len(packages[0].versions) != 0 + assert ( + spack.descriptions.get("jq") + == "jq is a lightweight and flexible command-line JSON processor." + ) + if app.settings.spack.repo == "https://github.com/custom-spack/repo": assert len(packages) == len(pkgs) else: @@ -68,7 +73,7 @@ def test_spack_package_updater(): spack.custom_repo = app.settings.spack.repo - timeout = time.time() + 60 * 2 + timeout = time.time() + 60 * 3 while True: new_pkgs = spack.stored_packages