diff --git a/schema.graphql b/schema.graphql index a13964d..81d504b 100644 --- a/schema.graphql +++ b/schema.graphql @@ -40,6 +40,7 @@ type Environment { tags: [String!]! hidden: Boolean! cachedEnvs: [Environment!]! + interpreters: Interpreters! requested: DateTime buildStart: DateTime buildDone: DateTime @@ -80,6 +81,11 @@ type HiddenSuccess implements Success { message: String! } +type Interpreters { + r: String + python: String +} + type InvalidInputError implements Error { message: String! } diff --git a/softpack_core/artifacts.py b/softpack_core/artifacts.py index 9d6bbb0..f7263c1 100644 --- a/softpack_core/artifacts.py +++ b/softpack_core/artifacts.py @@ -5,6 +5,7 @@ """ import itertools +import json import shutil import tempfile from dataclasses import dataclass @@ -49,6 +50,14 @@ def from_name(cls, name: str) -> 'Package': return Package(name=name) +@strawberry.type +class Interpreters: + """A Strawberry model representing the interpreters in an environment.""" + + r: Optional[str] = None + python: Optional[str] = None + + @strawberry.enum class State(Enum): """Environment states.""" @@ -77,6 +86,7 @@ class Artifacts: module_file = "module" readme_file = "README.md" meta_file = "meta.yml" + spack_file = "spack.lock" built_by_softpack_file = ".built_by_softpack" built_by_softpack = Type.softpack.value generated_from_module_file = ".generated_from_module" @@ -168,6 +178,41 @@ def spec(self) -> Box: info["hidden"] = getattr(metadata, "hidden", False) info["force_hidden"] = getattr(metadata, "force_hidden", False) + info["interpreters"] = Interpreters() + + if Artifacts.spack_file in self.obj: + hasR = False + hasPython = False + + for pkg in cast(list[Package], info["packages"]): + if pkg.name == "r": + hasR = True + + if pkg.name == "python": + hasPython = True + + if hasR and hasPython: + break + + if not hasR or not hasPython: + data = json.loads(self.obj[Artifacts.spack_file].data) + + for hash in data.get("concrete_specs", {}): + spec = data["concrete_specs"][hash] + + if not hasR and spec.get("name", "") == "r": + hasR = True + info["interpreters"].r = spec.get("version", "") + + if not hasPython and spec.get("name", "") == "python": + hasPython = True + info["interpreters"].python = spec.get( + "version", "" + ) + + if hasR and hasPython: + break + return info def metadata(self) -> Box: diff --git a/softpack_core/schemas/environment.py b/softpack_core/schemas/environment.py index 3344fa9..72ff976 100644 --- a/softpack_core/schemas/environment.py +++ b/softpack_core/schemas/environment.py @@ -22,7 +22,14 @@ from strawberry.file_uploads import Upload from softpack_core.app import app -from softpack_core.artifacts import Artifacts, Package, State, Type, artifacts +from softpack_core.artifacts import ( + Artifacts, + Interpreters, + Package, + State, + Type, + artifacts, +) from softpack_core.module import GenerateEnvReadme, ToSoftpackYML from softpack_core.schemas.base import BaseSchema @@ -327,6 +334,7 @@ class Environment: tags: list[str] hidden: bool cachedEnvs: list["Environment"] = field(default_factory=list) + interpreters: Interpreters = field(default_factory=Interpreters) requested: Optional[datetime.datetime] = None build_start: Optional[datetime.datetime] = None @@ -409,6 +417,7 @@ def from_artifact(cls, obj: Artifacts.Object) -> Optional["Environment"]: type=spec.get("type", ""), tags=spec.tags, hidden=spec.hidden, + interpreters=spec.get("interpreters", Interpreters()), ) except KeyError: return None diff --git a/tests/integration/test_environment.py b/tests/integration/test_environment.py index c0ee77e..fad24cf 100644 --- a/tests/integration/test_environment.py +++ b/tests/integration/test_environment.py @@ -6,6 +6,7 @@ import datetime import io +import json from pathlib import Path from typing import Optional @@ -27,6 +28,7 @@ HiddenSuccess, InvalidInputError, Package, + PackageInput, State, UpdateEnvironmentSuccess, WriteArtifactSuccess, @@ -613,3 +615,166 @@ def test_environment_with_requested_recipe( result = Environment.create(testable_env_input) assert isinstance(result, CreateEnvironmentSuccess) httpx_post.assert_not_called() + + +def test_interpreters( + httpx_post, testable_env_input: EnvironmentInput +) -> None: + env = EnvironmentInput.from_path("users/me/my_env-1") + env.packages = [ + PackageInput.from_name("pkg@1"), + PackageInput.from_name("pkg@2"), + ] + + assert isinstance(Environment.create(env), CreateEnvironmentSuccess) + + artifacts.commit_and_push( + artifacts.create_file( + Path(Artifacts.environments_root, env.path, env.name), + Artifacts.spack_file, + json.dumps( + { + "concrete_specs": { + "long_hash": { + "name": "python", + "version": "1.2.3", + } + } + } + ), + False, + True, + ), + "add spack.lock for environment", + ) + + env = Environment.from_artifact(artifacts.get(env.path, env.name)) + + assert env.interpreters.python == "1.2.3" + assert env.interpreters.r is None + + artifacts.commit_and_push( + artifacts.create_file( + Path(Artifacts.environments_root, env.path, env.name), + Artifacts.spack_file, + json.dumps( + { + "concrete_specs": { + "long_hash": { + "name": "r", + "version": "4.5.6", + } + } + } + ), + False, + True, + ), + "add spack.lock for environment", + ) + + env = Environment.from_artifact(artifacts.get(env.path, env.name)) + + assert env.interpreters.python is None + assert env.interpreters.r == "4.5.6" + + artifacts.commit_and_push( + artifacts.create_file( + Path(Artifacts.environments_root, env.path, env.name), + Artifacts.spack_file, + json.dumps( + { + "concrete_specs": { + "short_hash": { + "name": "python", + "version": "3.11.4", + }, + "long_hash": { + "name": "r", + "version": "4.4.1", + }, + } + } + ), + False, + True, + ), + "add spack.lock for environment", + ) + + env = Environment.from_artifact(artifacts.get(env.path, env.name)) + + assert env.interpreters.python == "3.11.4" + assert env.interpreters.r == "4.4.1" + + env = EnvironmentInput.from_path("users/me/my_env-2") + env.packages = [ + PackageInput.from_name("r@1"), + ] + + assert isinstance(Environment.create(env), CreateEnvironmentSuccess) + + artifacts.commit_and_push( + artifacts.create_file( + Path(Artifacts.environments_root, env.path, env.name), + Artifacts.spack_file, + json.dumps( + { + "concrete_specs": { + "short_hash": { + "name": "python", + "version": "3.11.4", + }, + "long_hash": { + "name": "r", + "version": "4.4.1", + }, + } + } + ), + False, + True, + ), + "add spack.lock for environment", + ) + + env = Environment.from_artifact(artifacts.get(env.path, env.name)) + + assert env.interpreters.python == "3.11.4" + assert env.interpreters.r is None + + env = EnvironmentInput.from_path("users/me/my_env-3") + env.packages = [ + PackageInput.from_name("python@2"), + ] + + assert isinstance(Environment.create(env), CreateEnvironmentSuccess) + + artifacts.commit_and_push( + artifacts.create_file( + Path(Artifacts.environments_root, env.path, env.name), + Artifacts.spack_file, + json.dumps( + { + "concrete_specs": { + "short_hash": { + "name": "python", + "version": "3.11.4", + }, + "long_hash": { + "name": "r", + "version": "4.4.1", + }, + } + } + ), + False, + True, + ), + "add spack.lock for environment", + ) + + env = Environment.from_artifact(artifacts.get(env.path, env.name)) + + assert env.interpreters.python is None + assert env.interpreters.r == "4.4.1"