diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 3bfabfc..96caa82 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,11 +1,6 @@ # This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - name: Upload Python Package on: @@ -18,9 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install dependencies @@ -30,7 +27,7 @@ jobs: - name: Build package run: python -m build - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3725811 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[test]" + - name: Run tests + run: pytest diff --git a/.gitignore b/.gitignore index 98916bf..a88c730 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ *.swp *__pycache__ python/*.pyc -gfdlvitals.egg-info/ +*.egg-info/ +.venv/ testing/test_data testing/db testing/db-esm2 diff --git a/.readthedocs.yml b/.readthedocs.yml index f94758f..97c3631 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,8 +9,13 @@ version: 2 sphinx: configuration: docs/source/conf.py -# Optionally set the version of Python and requirements required to build your docs +build: + os: ubuntu-22.04 + tools: + python: "3.11" + python: - version: 3.8 install: + - method: pip + path: . - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt index 910fbf5..c2e0ad0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,10 +1,11 @@ -cftime<=1.2.1 +cftime ipython jupyter matplotlib>=3.3.3 -nc-time-axis<=1.2.0 -netCDF4 +nc-time-axis +netCDF4 numpy pandas scipy +sphinx-rtd-theme xarray diff --git a/docs/source/conf.py b/docs/source/conf.py index 88b7b68..f6dd15b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,9 +11,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import os -import sys -print(os.getcwd()) -sys.path.insert(0, os.path.abspath('../../')) +from importlib.metadata import version as _get_version # -- Project information ----------------------------------------------------- @@ -23,7 +21,8 @@ author = 'John Krasting' # The full version, including alpha/beta/rc tags -release = '3.0a1' +release = _get_version("gfdlvitals") +version = release # -- General configuration --------------------------------------------------- @@ -67,4 +66,6 @@ #-- Build api from sphinx.ext.apidoc import main -main(['-f', '-M', '-e', '-T', '../../gfdlvitals', '-o', 'api' ]) \ No newline at end of file +import gfdlvitals +_pkg_dir = os.path.dirname(gfdlvitals.__file__) +main(['-f', '-M', '-e', '-T', _pkg_dir, '-o', 'api' ]) \ No newline at end of file diff --git a/docs/source/plotting.rst b/docs/source/plotting.rst index 8648871..66e5779 100644 --- a/docs/source/plotting.rst +++ b/docs/source/plotting.rst @@ -15,6 +15,7 @@ options to smooth the data, overlay a trend line, and align offset time axes. .. ipython:: python + :okexcept: import gfdlvitals diff --git a/docs/source/vitals_data_frame.rst b/docs/source/vitals_data_frame.rst index f05be4c..eeb12d4 100644 --- a/docs/source/vitals_data_frame.rst +++ b/docs/source/vitals_data_frame.rst @@ -138,6 +138,7 @@ In the example below, the test historical dataset is artifically split into two 20-year epochs for comparison. .. ipython:: python + :okexcept: df_hist_t0 = df_hist[-40:-20] df_hist_t1 = df_hist[-20::] diff --git a/gfdlvitals/__init__.py b/gfdlvitals/__init__.py index 9d672db..5c412ad 100644 --- a/gfdlvitals/__init__.py +++ b/gfdlvitals/__init__.py @@ -1,6 +1,11 @@ """gfdlvitals - a package for computing global mean metrics""" -from .version import __version__ +from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version("gfdlvitals") +except PackageNotFoundError: + __version__ = "unknown" from . import averagers from . import cli diff --git a/gfdlvitals/cli.py b/gfdlvitals/cli.py index 6cb7bf0..7fd399b 100755 --- a/gfdlvitals/cli.py +++ b/gfdlvitals/cli.py @@ -5,10 +5,11 @@ import os import shutil import subprocess +import sys import tempfile import gfdlvitals -__all__ = ["arguments", "process_year", "run"] +__all__ = ["arguments", "process_year", "run", "main"] def arguments(args=None): @@ -195,3 +196,9 @@ def run(args): # -- Clean up os.chdir(cwd) shutil.rmtree(tempdir) + + +def main(): + """Entry point for the gfdlvitals command""" + sys.stdout.reconfigure(line_buffering=True) + run(sys.argv[1:]) diff --git a/scripts/db2nc b/gfdlvitals/cli_db2nc.py old mode 100755 new mode 100644 similarity index 90% rename from scripts/db2nc rename to gfdlvitals/cli_db2nc.py index 337e1d5..55534d1 --- a/scripts/db2nc +++ b/gfdlvitals/cli_db2nc.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python3 - -""" Command line utility to convert SQLite to NetCDF """ +"""Command line utility to convert SQLite to NetCDF""" import argparse import os @@ -12,7 +10,7 @@ def arguments(): - """Function captures the command-line aguments passed to this script""" + """Function captures the command-line arguments passed to this script""" description = """ Program for converting .db file format to NetCDF format. @@ -24,12 +22,10 @@ def arguments(): description=description, formatter_class=argparse.RawTextHelpFormatter ) - # -- Input tile parser.add_argument( "infile", type=str, help="Input file. Format must be sqlite (*.db)" ) - # -- Output file parser.add_argument( "-o", "--outfile", @@ -122,9 +118,6 @@ def write_nc( ncfile = nc.Dataset(outfile, "w", format=ncformat) ncfile.setncattr("source_file", dbfile) ncfile.setncattr("created", timestamp_str) - # ncfile.setncattr('experiment',expName) - # ncfile.setncattr('type',plotType) - # ncfile.setncattr('region',region) _ = ncfile.createDimension("time", 0) time = ncfile.createVariable("time", "f4", ("time",)) time.calendar = "noleap" @@ -134,9 +127,6 @@ def write_nc( if table not in ["long_name", "units", "cell_measure"]: data_array = np.ma.ones(len(years)) + 1.0e20 data_array.mask = True - # if 'Land' in plotType: - # extract_list = ['avg','sum'] - # else: extract_list = ["value"] for k in extract_list: count = 0 @@ -178,16 +168,15 @@ def write_nc( ncfile.close() -if __name__ == "__main__": - - # read command line arguments +def main(): + """Entry point for the db2nc command""" args = arguments() - # get the full path of the db file infile = os.path.realpath(args.infile) - # get list of variables and years in a file _tables, _years = tables_and_years(infile) write_nc( infile, args.outfile, _tables, _years, clobber=args.force, verbose=args.verbose ) -sys.exit() + +if __name__ == "__main__": + main() diff --git a/scripts/plotdb b/gfdlvitals/cli_plotdb.py old mode 100755 new mode 100644 similarity index 93% rename from scripts/plotdb rename to gfdlvitals/cli_plotdb.py index bb56658..5434a79 --- a/scripts/plotdb +++ b/gfdlvitals/cli_plotdb.py @@ -1,15 +1,10 @@ -#!/usr/bin/env python - -""" CLI script for plotting SQLite files """ +"""CLI script for plotting SQLite files""" import argparse import os -import matplotlib.pyplot as plt - import gfdlvitals -COUNT = 1 def arguments(): """ @@ -79,7 +74,11 @@ def arguments(): return args - -if __name__ == "__main__": +def main(): + """Entry point for the plotdb command""" cliargs = arguments() gfdlvitals.plot.run_plotdb(cliargs) + + +if __name__ == "__main__": + main() diff --git a/gfdlvitals/plot.py b/gfdlvitals/plot.py index eed6e9b..fd7685c 100644 --- a/gfdlvitals/plot.py +++ b/gfdlvitals/plot.py @@ -5,7 +5,7 @@ import nc_time_axis import numpy as np -import pkg_resources as pkgr +from importlib.resources import files import matplotlib import matplotlib.pyplot as plt @@ -21,7 +21,7 @@ def set_font(): """Sets font style to Roboto""" # Add Roboto font - fonts_dir = pkgr.resource_filename("gfdlvitals", "resources/fonts") + fonts_dir = str(files("gfdlvitals").joinpath("resources/fonts")) font_dirs = [fonts_dir] font_files = font_manager.findSystemFonts(fontpaths=font_dirs) diff --git a/gfdlvitals/sample.py b/gfdlvitals/sample.py index ab6135b..72ecedd 100644 --- a/gfdlvitals/sample.py +++ b/gfdlvitals/sample.py @@ -1,6 +1,6 @@ """ Sample db files for demonstration """ -import pkg_resources as pkgr +from importlib.resources import files -historical = pkgr.resource_filename("gfdlvitals", "resources/historical.db") -picontrol = pkgr.resource_filename("gfdlvitals", "resources/picontrol.db") +historical = str(files("gfdlvitals").joinpath("resources/historical.db")) +picontrol = str(files("gfdlvitals").joinpath("resources/picontrol.db")) diff --git a/gfdlvitals/util/gmeantools.py b/gfdlvitals/util/gmeantools.py index 63c06f0..4c15835 100644 --- a/gfdlvitals/util/gmeantools.py +++ b/gfdlvitals/util/gmeantools.py @@ -3,10 +3,11 @@ import math import pickle import sqlite3 +import sys import warnings import numpy as np -import pkg_resources as pkgr +from importlib.resources import files __all__ = [ "get_web_vars_dict", @@ -29,9 +30,7 @@ def get_web_vars_dict(): dict LM3 variable module mappings and metadata """ - mapping_file = pkgr.resource_filename( - "gfdlvitals", "resources/LM3_variable_dictionary.pkl" - ) + mapping_file = str(files("gfdlvitals").joinpath("resources/LM3_variable_dictionary.pkl")) return pickle.load( open( mapping_file, @@ -285,16 +284,12 @@ def write_sqlite_data( # check if result is a nan and replace with a defined missing value if varmean is not None: if math.isnan(float(varmean)): - warnings.warn( - f"Could not update {sqlfile} variable {varname} with mean={varmean}" - ) + print(f" WARNING: {varname} mean is NaN in {sqlfile}, writing missing value", file=sys.stderr) varmean = missing_value if varsum is not None: if math.isnan(float(varsum)): - warnings.warn( - f"Could not update {sqlfile} variable {varname} with mean={varsum}" - ) + print(f" WARNING: {varname} sum is NaN in {sqlfile}, writing missing value", file=sys.stderr) varsum = missing_value conn = sqlite3.connect(sqlfile) diff --git a/gfdlvitals/util/netcdf.py b/gfdlvitals/util/netcdf.py index 7cf4ffe..5b33a4c 100644 --- a/gfdlvitals/util/netcdf.py +++ b/gfdlvitals/util/netcdf.py @@ -73,10 +73,11 @@ def in_mem_xr(data): In-memory xarray dataset object """ + time_coder = xr.coders.CFDatetimeCoder(use_cftime=True) if isinstance(data, netCDF4._netCDF4.Dataset): - dfile = xr.open_dataset(xr.backends.NetCDF4DataStore(data), use_cftime=True) + dfile = xr.open_dataset(xr.backends.NetCDF4DataStore(data), decode_times=time_coder, decode_timedelta=False) else: - dfile = xr.open_dataset(data, use_cftime=True) + dfile = xr.open_dataset(data, decode_times=time_coder, decode_timedelta=False) return dfile diff --git a/gfdlvitals/util/xrtools.py b/gfdlvitals/util/xrtools.py index 03bcbac..5dfecb6 100644 --- a/gfdlvitals/util/xrtools.py +++ b/gfdlvitals/util/xrtools.py @@ -97,6 +97,6 @@ def xr_weighted_avg(dset, weights): _dset_weighted[x] = _dset_weighted[x].astype(dset[x].dtype) _dset_weighted[x].attrs = dset[x].attrs - result = result.merge(_dset_weighted) + result = result.merge(_dset_weighted, compat="override") return result diff --git a/gfdlvitals/version.py b/gfdlvitals/version.py deleted file mode 100644 index 8357025..0000000 --- a/gfdlvitals/version.py +++ /dev/null @@ -1,3 +0,0 @@ -"""momlevel: version information""" - -__version__ = "3.0.12" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..649d02f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,77 @@ +[build-system] +requires = ["setuptools>=64", "setuptools-git-versioning>=2.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "gfdlvitals" +dynamic = ["version"] +description = "Tools for calculating scalar diagnostics from GFDL models" +readme = {content-type = "text/x-rst", text = """ +**gfdlvitals** is a Python package in the public domain +that provides tools for calculating, storing, and analyzing +scalar diagnostics from GFDL's CM4-class models. + +More Information +---------------- +- Source code: ``_ +- Documentation: ``_ +"""} +requires-python = ">=3.9" +authors = [{name = "John Krasting", email = "john.krasting@noaa.gov"}] +keywords = ["climate", "model", "analysis", "scalar", "diagnostics", "gfdl", "AM4", "CM4", "ESM4", "OM4", "LM4"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering", + "License :: Public Domain", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "numpy", + "scipy", + "h5netcdf", + "pandas", + "netCDF4", + "matplotlib", + "xarray", + "cftime", + "nc-time-axis", + "xgcm", + "regionmask", + "xoverturning", +] + +[project.optional-dependencies] +test = ["pytest"] + +[project.urls] +Homepage = "https://github.com/jkrasting/gfdlvitals" +Documentation = "https://gfdlvitals.readthedocs.io/en/latest/" + +[project.scripts] +gfdlvitals = "gfdlvitals.cli:main" +db2nc = "gfdlvitals.cli_db2nc:main" +plotdb = "gfdlvitals.cli_plotdb:main" + +[tool.setuptools.packages.find] +include = ["gfdlvitals*"] + +[tool.setuptools.package-data] +gfdlvitals = [ + "resources/LM3_variable_dictionary.pkl", + "resources/historical.db", + "resources/picontrol.db", + "resources/fonts/Roboto/*", + "resources/fonts/Roboto_Condensed/*", +] + +[tool.setuptools-git-versioning] +enabled = true +dev_template = "{tag}.dev{ccount}+{sha}" +dirty_template = "{tag}.dev{ccount}+{sha}.dirty" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/scripts/gfdlvitals b/scripts/gfdlvitals deleted file mode 100755 index d59c671..0000000 --- a/scripts/gfdlvitals +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 - -""" Command Line Script for running gfdlvitals """ - -import sys -import gfdlvitals.cli as cli - -if __name__ == "__main__": - cli.run(sys.argv[1::]) - sys.exit() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index c2be97e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -[metadata] -name = gfdlvitals -description = Tools for calculating scalar diagnostics from GFDL models -url = https://github.com/jkrasting/gfdlvitals -author = John Krasting -author_email = john.krasting@noaa.gov -keywords = climate model analysis scalar diagnostics gfdl AM4 CM4 ESM4 OM4 LM4 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Science/Research - Topic :: Scientific/Engineering - License :: Public Domain - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 -long_description_content_type=text/x-rst -long_description = - **gfdlvitals** is a Python package in the public domain - that provides tools for calculating, storing, and analyzing - scalar diagnostics from GFDL's CM4-class models. - - More Information - ---------------- - - Source code: ``_ - - Documentation: ``_ - -[options] -packages = find_namespace: -zip_safe = False -include_package_data = True -python_requires = >=3.7 -install_requires = - setuptools - numpy - scipy - h5netcdf - pandas - netCDF4 - matplotlib - xarray - cftime - nc-time-axis -scripts = - scripts/gfdlvitals - scripts/db2nc - scripts/plotdb -setup_requires = - setuptools - -[options.package_data] -gfdlvitals = - resources/LM3_variable_dictionary.pkl - resources/historical.db - resources/picontrol.db - resources/fonts/Roboto/* - resources/fonts/Roboto_Condensed/* diff --git a/setup.py b/setup.py deleted file mode 100644 index 68b182c..0000000 --- a/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -""" setup script """ -import setuptools - -exec(open("gfdlvitals/version.py").read()) - -setuptools.setup(version=__version__) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..e4e1c33 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,40 @@ +"""Basic tests for gfdlvitals package""" + +import os + + +def test_import(): + import gfdlvitals + + +def test_version_exists(): + import gfdlvitals + assert isinstance(gfdlvitals.__version__, str) + assert len(gfdlvitals.__version__) > 0 + + +def test_submodule_imports(): + from gfdlvitals import averagers + from gfdlvitals import cli + from gfdlvitals import models + from gfdlvitals import sample + from gfdlvitals import util + + +def test_vitals_dataframe(): + from gfdlvitals import VitalsDataFrame + df = VitalsDataFrame({"a": [1, 2, 3]}) + assert len(df) == 3 + + +def test_sample_files_exist(): + from gfdlvitals import sample + assert os.path.isfile(sample.historical) + assert os.path.isfile(sample.picontrol) + + +def test_open_db(): + from gfdlvitals import open_db, sample, VitalsDataFrame + df = open_db(sample.historical) + assert isinstance(df, VitalsDataFrame) + assert len(df) > 0