diff --git a/docs/Users_Guide/release-notes.rst b/docs/Users_Guide/release-notes.rst index 21b271caf..27cc996ef 100644 --- a/docs/Users_Guide/release-notes.rst +++ b/docs/Users_Guide/release-notes.rst @@ -93,3 +93,20 @@ METplotpy Upgrade Instructions development with Python 3.12. View the requirements.txt/nco_requirements.txt file at the top level of the repository for version numbers for the corresponding third-party packages. + + In June 2025, Plotly made significant updates to the kaleido package with the 1.0.0 + release by removing Google Chrome code. Now, users will need to have Google Chrome + installed in directories specified in this Plotly documentation (based on operating + system): + +https://plotly.com/python/static-image-export/ + +The METplotpy code downloads Chrome at runtime by invoking the kaleido.get_chrome_sync() +method call. + +If users do not wish to have Chrome downloaded at run time and already have Chrome installed +in one of the expected locations (specified in the Plotly link above), then the PRE_LOAD_CHROME environment variable +will need to be set to 'True' (case insensitive string). + +Refer to the Kaleido README for more information on the changes: +https://github.com/plotly/Kaleido diff --git a/internal/scripts/installation/modulefiles/3.1.0_ursa b/internal/scripts/installation/modulefiles/3.1.0_ursa new file mode 100644 index 000000000..9ffb356e9 --- /dev/null +++ b/internal/scripts/installation/modulefiles/3.1.0_ursa @@ -0,0 +1,25 @@ +#%Module###################################################################### +## +## METplotpy +## +proc ModulesHelp { } { + puts stderr "Sets up the paths and environment variables to use the METplotpy-3.1.0. + *** For help see the official MET webpage at http://www.dtcenter.org/met/users ***" +} + +prereq intel-oneapi-compilers/2025.1.1 + +setenv METPLOTPY_SOURCE /contrib/METplotpy/METplotpy-3.1.0 +setenv METPLOTPY_BASE /contrib/METplotpy/METplotpy-3.1.0 + +prepend-path PATH /scratch3/BMC/dtc/METplus/miniconda/miniconda3/envs/metplus_v6.1_py3.12/bin +prepend-path PATH /contrib/METplotpy/METplotpy-3.1.0/metplotpy/contributed +prepend-path PATH /contrib/METplotpy/METplotpy-3.1.0/metplotpy/plots/performance_diagram +prepend-path PATH /contrib/METplotpy/METplotpy-3.1.0/metplotpy/plots +prepend-path PATH /contrib/METplotpy/METplotpy-3.1.0/metplotpy +prepend-path PATH /contrib/METplotpy/METplotpy-3.1.0 +prepend-path PYTHONPATH /contrib/METplotpy/METplotpy-3.1.0/metplotpy/contributed +prepend-path PYTHONPATH /contrib/METplotpy/METplotpy-3.1.0/metplotpy/plots/performance_diagram +prepend-path PYTHONPATH /contrib/METplotpy/METplotpy-3.1.0/metplotpy/plots +prepend-path PYTHONPATH /contrib/METplotpy/METplotpy-3.1.0/metplotpy +prepend-path PYTHONPATH /contrib/METplotpy/METplotpy-3.1.0 diff --git a/metplotpy/plots/base_plot.py b/metplotpy/plots/base_plot.py index ac16e2759..23d28571a 100644 --- a/metplotpy/plots/base_plot.py +++ b/metplotpy/plots/base_plot.py @@ -20,18 +20,53 @@ import numpy as np import yaml from typing import Union - -import plotly.io as pio - +import kaleido import metplotpy.plots.util +from metplotpy.plots.util import strtobool from .config import Config from metplotpy.plots.context_filter import ContextFilter # kaleido 0.x will be deprecated after September 2025 and Chrome will no longer # be included with kaleido from version 1.0.0. Explicitly get Chrome via call to kaleido. -import kaleido -kaleido.get_chrome_sync() +# In some instances, we do NOT want Chrome to be installed at run-time. If the +# PRE_LOAD_CHROME environment variable exists, or set to TRUE, +# then Chrome will be assumed to have been pre-loaded. Otherwise, +# invoke get_chrome_sync() to install Chrome in the +# /path-to-python-libs/pythonx.yz/site-packages/... directory + +# Check if the PRE_LOAD_CHROME env variable exists +aquire_chrome = False + +turn_on_logging = strtobool('LOG_BASE_PLOT') +# Log when Chrome is downloaded at runtime +if turn_on_logging is True: + log = logging.getLogger("base_plot") + log.setLevel(logging.INFO) + + formatter = logging.Formatter("%(asctime)s [%(levelname)s] | %(name)s | %(message)s") + + # set the WRITE_LOG env var to True to save the log message to a + # separate log file + write_log = strtobool('WRITE_LOG') + if write_log is True: + file_handler = logging.FileHandler("./base_plot.log") + file_handler.setFormatter(formatter) + log.addHandler(file_handler) + +# Only load Chrome at run-time if PRE_LOAD_CHROME is False or not defined. +# Some applications may not want to load Chrome at runtime and +# will set the PRE_LOAD_CHROME to True to indicate that it is already +# loaded/downloaded prior to runtime. +chrome_env =strtobool ('PRE_LOAD_CHROME') +if ('PRE_LOAD_CHROME' not in os.environ)or (chrome_env is False): + aquire_chrome=True + kaleido.get_chrome_sync() + + +# Log when kaleido is downloading Chrome +if aquire_chrome is True and turn_on_logging is True: + log.info("Plotly kaleido is loading Chrome at run time") class BasePlot: """A class that provides methods for building Plotly plot's common features diff --git a/metplotpy/plots/util.py b/metplotpy/plots/util.py index 7b2c32897..2664f32df 100644 --- a/metplotpy/plots/util.py +++ b/metplotpy/plots/util.py @@ -623,3 +623,50 @@ def prepare_ctc_roc(subset_df, is_ascending): thresh = pd.concat([thresh, pd.Series([''])], ignore_index=True) return pody, pofd, thresh + + +def strtobool(env_var:str)->bool: + """ + Since distutils.util.strtobool was deprecated in Python 3.12, implement + our own version. + + In the distutils.util.strtobool, a simple one line command was used to determine + whether an environment variable was set to True or False. In this + example, the default value is set to False in the event that the environment + variable is not defined: + + turn_on_logging = strtobool(os.getenv('LOG_BASE_PLOT', 'False') ) + + Environment variables can be set as string or bool. Evaluate whether a string + value for true or false (support case-insensitive text) is True/False and + set the default value. + + Args: + @parm env_vars: string name of the environment variable to evaluate + + turn_on_logging = strtobool(os.getenv('LOG_BASE_PLOT') ) + """ + + true_list = ['true', 't', '1',] + false_list = ['false', 'f', '0' ] + # if the environment variable does not exist, then return False + try: + val = os.environ[env_var] + except KeyError: + return False + + # If the environment variable is None, return false + if val is None: + return False + else: + # Check for variations of truth values + lower = val.lower() + if lower in true_list: + return True + elif lower in false_list: + return False + else: + msg = "Value does not represent a truth value (i.e. true or false)" + raise ValueError(msg) + + diff --git a/nco_requirements.txt b/nco_requirements.txt index 7330a6f7e..4156ef860 100644 --- a/nco_requirements.txt +++ b/nco_requirements.txt @@ -1,4 +1,4 @@ -kaleido>=0.2.1 +kaleido>=1.0.0 matplotlib>=3.10.0 metpy>=1.6.3 netcdf4>=1.7.2 @@ -6,7 +6,7 @@ numpy==2.2.2 scipy>=1.15.1 pandas>=2.2.3 pint>=0.24.4 -plotly>=6.0.0 +plotly>=6.1.1 pytest>=8.3.4 pyyaml>=6.0.2 xarray>=2025.1.2 diff --git a/test/util/test_util.py b/test/util/test_util.py index b22bcc04c..4d853708a 100644 --- a/test/util/test_util.py +++ b/test/util/test_util.py @@ -1,6 +1,6 @@ import os import pandas as pd - +import pytest import metplotpy.plots.util as util import gc @@ -170,3 +170,48 @@ def test_filter_by_fixed_vars(): for filtered in filtered_list: assert filtered in expected_list + + +def test_strtobool(): + """ + Test that strtobool is returning the corresponding bool value to an environment + + """ + + # Non-existent env var should return boolean False + assert (util.strtobool('NON_EXISTENT') is False) + + # Variations of false should return boolean False + os.environ['FALSE_1'] = 'f' + os.environ['FALSE_2'] = 'FaLSE' + os.environ['FALSE_3'] = '0' + os.environ['FALSE_4'] = 'F' + false_1 = 'FALSE_1' + false_2 = 'FALSE_2' + false_3 = 'FALSE_3' + false_4 = 'FALSE_4' + assert(util.strtobool(false_1) is False) + assert(util.strtobool(false_2) is False) + assert(util.strtobool(false_3) is False) + assert(util.strtobool(false_4) is False) + + + # Variations of true should return boolean True + os.environ['TRUE_1'] = 't' + os.environ['TRUE_2'] = 'T' + os.environ['TRUE_3'] = 'tRuE' + os.environ['TRUE_4'] = '1' + true_1 = 'TRUE_1' + true_2 = 'TRUE_2' + true_3 = 'TRUE_3' + true_4 = 'TRUE_4' + assert (util.strtobool(true_1) is True) + assert (util.strtobool(true_2) is True) + assert (util.strtobool(true_3) is True) + assert (util.strtobool(true_4) is True) + + # non-truth values should raise a ValueError + os.environ['BOGUS'] = 'Whatever' + var = 'BOGUS' + with pytest.raises(ValueError): + util.strtobool(var)