diff --git a/docs/usage.md b/docs/usage.md index 73b9ed8f..b475f8f1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -28,3 +28,23 @@ safely. Proteus has a command-line interface that can be accessed by running `proteus` on the command line. Try `proteus --help` to see the available commands! + +### `proteus doctor` + +The `proteus doctor` commnd helps you to diagnose issues with your proteus installation. +It tells you about outdated or missing packages, and whether all environment variables have been set. + +```console +$ proteus doctor +Dependencies +fwl-proteus: ok +fwl-mors: Update available 24.10.27 -> 24.11.18 +fwl-calliope: ok +fwl-zephyrus: ok +aragog: Update available 0.1.0a0 -> 0.1.5a0 +AGNI: No package metadata was found for AGNI is not installed. + +Environment variables +FWL_DATA: Variable not set. +RAD_DIR: Variable not set. +``` diff --git a/src/proteus/cli.py b/src/proteus/cli.py index 77738f3e..94e66b1a 100644 --- a/src/proteus/cli.py +++ b/src/proteus/cli.py @@ -138,6 +138,7 @@ def spider(): from .utils.data import get_spider get_spider() + cli.add_command(get) get.add_command(spectral) get.add_command(surfaces) @@ -147,5 +148,15 @@ def spider(): get.add_command(petsc) get.add_command(spider) + +@click.command() +def doctor(): + """Diagnose your PROTEUS installation""" + from .doctor import doctor_entry + doctor_entry() + + +cli.add_command(doctor) + if __name__ == '__main__': cli() diff --git a/src/proteus/doctor.py b/src/proteus/doctor.py new file mode 100644 index 00000000..31eaed2a --- /dev/null +++ b/src/proteus/doctor.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import importlib.metadata +import os +from functools import partial +from importlib.metadata import PackageNotFoundError +from typing import Callable + +import click +import requests +from attr import dataclass + +from proteus.utils.coupler import _get_agni_version, get_proteus_directories + +DIRS = get_proteus_directories() + +HEADER_STYLE = {'fg': 'yellow', 'underline': True, 'bold': True} +OK_STYLE = {'fg': 'green'} +ERROR_STYLE = {'fg': 'red'} +DEFAULT_STYLE = {'fg': 'yellow'} + + +@dataclass +class BasePackage: + name: str + + def current_version(self) -> str: ... + + def latest_version(self) -> str: ... + + def get_status_message(self) -> str: + try: + current_version = self.current_version() + latest_version = self.latest_version() + except BaseException as exc: + message = click.style(str(exc), **ERROR_STYLE) + else: + if current_version != latest_version: + message = click.style( + f'Update available {current_version} -> {latest_version}', fg='yellow' + ) + else: + message = click.style('ok', **OK_STYLE) + + name = click.style(self.name, **OK_STYLE) + return f'{name}: {message}' + + +class PythonPackage(BasePackage): + def current_version(self) -> str: + return importlib.metadata.version(self.name) + + def latest_version(self) -> str: + response = requests.get(f'https://pypi.org/pypi/{self.name}/json') + if not response.ok: + response.raise_for_status() + + return response.json()['info']['version'] + + +@dataclass +class GitPackage(BasePackage): + owner: str + version_getter: Callable + + def current_version(self) -> str: + try: + return self.version_getter() + except FileNotFoundError as exc: + raise PackageNotFoundError(f'{self.name} is not installed.') from exc + + def latest_version(self) -> str: + response = requests.get( + f'https://api.github.com/repos/{self.owner}/{self.name}/releases/latest' + ) + return response.json()['name'] + + +PACKAGES = ( + PythonPackage(name='aragog'), + PythonPackage(name='fwl-calliope'), + PythonPackage(name='fwl-janus'), + PythonPackage(name='fwl-proteus'), + PythonPackage(name='fwl-mors'), + PythonPackage(name='fwl-zephyrus'), + GitPackage(name='AGNI', owner='nichollsh', version_getter=partial(_get_agni_version, DIRS)), +) + + +def get_env_var_status_message(var: str) -> str: + if os.environ.get(var): + message = click.style('ok', **OK_STYLE) + else: + message = click.style('Variable not set.', **ERROR_STYLE) + + name = click.style(var, **OK_STYLE) + return f'{name}: {message}' + + +VARIABLES = ( + 'FWL_DATA', + 'RAD_DIR', +) + + +def doctor_entry(): + click.secho('Packages', **HEADER_STYLE) + for package in PACKAGES: + message = package.get_status_message() + click.echo(message) + + click.secho('\nEnvironment variables', **HEADER_STYLE) + for var in VARIABLES: + message = get_env_var_status_message(var) + click.echo(message) diff --git a/src/proteus/utils/coupler.py b/src/proteus/utils/coupler.py index 38f8e23d..dc517f0e 100644 --- a/src/proteus/utils/coupler.py +++ b/src/proteus/utils/coupler.py @@ -498,7 +498,37 @@ def UpdatePlots( hf_all:pd.DataFrame, dirs:dict, config:Config, end=False, num_s # Close all figures plt.close() -def SetDirectories(config: Config): + +def get_proteus_directories(*, out_dir: str = 'proteus_out') -> dict[str, str]: + """Create dict of proteus directories from root dir. + + Parameters + ---------- + root_dir : str + Proteus root directory + out_dir : str, optional + Name out output directory + + Returns + ------- + dirs : dict[str, str] + Proteus directories dict + """ + root_dir = get_proteus_dir() + + return { + "agni": os.path.join(root_dir, "AGNI"), + "input": os.path.join(root_dir, "input"), + "output": os.path.join(root_dir, "output", out_dir), + "proteus": root_dir, + "spider": os.path.join(root_dir, "SPIDER"), + "tools": os.path.join(root_dir, "tools"), + "vulcan": os.path.join(root_dir, "VULCAN"), + "utils": os.path.join(root_dir, "src", "proteus", "utils") + } + + +def SetDirectories(config: Config) -> dict[str, str]: """Set directories dictionary Sets paths to the required directories, based on the configuration provided @@ -506,29 +536,15 @@ def SetDirectories(config: Config): Parameters ---------- - config : Config - PROTEUS options dictionary + config : Config + PROTEUS options dictionary Returns ---------- - dirs : dict - Dictionary of paths to important directories + dirs : dict + Dictionary of paths to important directories """ - - proteus_dir = get_proteus_dir() - proteus_src = os.path.join(proteus_dir,"src","proteus") - - # PROTEUS folders - dirs = { - "output": os.path.join(proteus_dir,"output",config.params.out.path), - "input": os.path.join(proteus_dir,"input"), - "proteus": proteus_dir, - "agni": os.path.join(proteus_dir,"AGNI"), - "vulcan": os.path.join(proteus_dir,"VULCAN"), - "spider": os.path.join(proteus_dir,"SPIDER"), - "utils": os.path.join(proteus_src,"utils"), - "tools": os.path.join(proteus_dir,"tools"), - } + dirs = get_proteus_directories(out_dir=config.params.out.path) # FWL data folder if os.environ.get('FWL_DATA') is None: