Skip to content

Commit

Permalink
Retrieve recipe descriptions from Spack and serve them via an endpoin…
Browse files Browse the repository at this point in the history
…t. (#60)
  • Loading branch information
mjkw31 authored Oct 21, 2024
1 parent 0de65ab commit 2a7ecf5
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 20 deletions.
16 changes: 16 additions & 0 deletions softpack_core/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]]}
82 changes: 63 additions & 19 deletions softpack_core/spack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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."""

Expand All @@ -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
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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,
)

Expand All @@ -169,7 +213,7 @@ def __getPackagesFromSpack(
"repos:[" + checkout_path + "]",
"list",
"--format",
"version_json",
"html",
],
capture_output=True,
)
Expand Down
3 changes: 3 additions & 0 deletions tests/integration/test_recipe_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion tests/integration/test_spack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 2a7ecf5

Please sign in to comment.