diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml deleted file mode 100644 index bf22430..0000000 --- a/.github/workflows/black.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Black - -on: [push, pull_request] - -jobs: - - lint: - name: Check code format - # We want to run on external PRs, but not on our own internal PRs as they'll be run - # by the push to the branch. Without this if check, checks are duplicated since - # internal PRs match both the push and pull_request events. - if: - github.event_name == 'push' || github.event.pull_request.head.repo.full_name != - github.repository - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: psf/black@stable - with: - options: "--check --verbose --diff" - src: "setup.py src/bmi_wavewatch3 tests" diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 2c04eb4..ed0d4d3 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -23,4 +23,3 @@ jobs: echo "Please see https://landlab.readthedocs.io/en/master/development/contribution/index.html?highlight=towncrier#news-entries for guidance." false fi - diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ffd19b1..e051f46 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,9 +26,9 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - + runs-on: ubuntu-latest - + defaults: run: shell: bash -l {0} @@ -44,10 +44,10 @@ jobs: - name: Show conda installation info run: conda info - + - name: Install dependencies run: | pip install nox - name: Build documentation - run: nox -s docs + run: nox -s build-docs diff --git a/.github/workflows/flake8.yml b/.github/workflows/lint.yml similarity index 98% rename from .github/workflows/flake8.yml rename to .github/workflows/lint.yml index 8fb6378..d9bd825 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Flake8 +name: Lint on: [push, pull_request] diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 48681cb..7e38419 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -28,7 +28,7 @@ jobs: uses: actions/setup-python@v2 with: python-version: "3.9" - + - name: Install twine run: | pip install --upgrade pip wheel setuptools @@ -36,11 +36,11 @@ jobs: python --version pip --version twine --version - + - name: Create source distribution run: | python setup.py sdist --dist-dir wheelhouse - + - name: Upload source distribution run: | twine upload --skip-existing wheelhouse/*.tar.gz diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0bbd9c..a43d7ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: uses: actions/setup-python@v2 with: python-version: "3.9" - + - name: Install twine run: | pip install --upgrade pip wheel setuptools @@ -36,11 +36,11 @@ jobs: python --version pip --version twine --version - + - name: Create source distribution run: | python setup.py sdist --dist-dir wheelhouse - + - name: Upload source distribution run: | twine upload --skip-existing wheelhouse/*.tar.gz diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 229b31e..87a2301 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,4 +43,3 @@ jobs: - name: Test package run: nox -s tests - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3cab59d..93af851 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,11 +21,13 @@ repos: require_serial: true types_or: [python, pyi, jupyter] additional_dependencies: [".[jupyter]"] + - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + rev: 5.0.4 hooks: - id: flake8 # additional_dependencies: [flake8-bugbear] + - repo: https://gitlab.com/iamlikeme/nbhooks rev: 1.0.0 hooks: @@ -36,8 +38,37 @@ repos: files: \.ipynb$ language: python language_version: python3 + - repo: https://github.com/asottile/pyupgrade rev: v2.34.0 hooks: - id: pyupgrade args: [--py38-plus] + +- repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + files: \.py$ + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-builtin-literals + - id: check-added-large-files + - id: check-case-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: forbid-new-submodules + - id: trailing-whitespace + +- repo: https://github.com/PyCQA/pydocstyle + rev: 6.1.1 + hooks: + - id: pydocstyle + files: src/bmi_wavewatch3/.*\.py$ + args: + # - --convention=numpy + - --select=D417 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 4e4d1c3..464aa04 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,8 +1,8 @@ version: 2 sphinx: - builder: html - configuration: docs/source/conf.py + builder: dirhtml + configuration: docs/conf.py fail_on_warning: false formats: diff --git a/AUTHORS.rst b/AUTHORS.rst index 60f3cc3..9715b39 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,3 +1,4 @@ +======= Credits ======= diff --git a/LICENSE.rst b/LICENSE.rst index 833d46d..7496260 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -23,4 +23,3 @@ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.rst b/README.rst index aa63a2e..ce6c406 100644 --- a/README.rst +++ b/README.rst @@ -39,23 +39,43 @@ All data sources provide both global and regional grids. .. end-abstract -Installation ------------- +Quickstart +---------- -.. start-installation +.. start-quickstart -*bmi_wavewatch3* can be installed by running ``pip install bmi-wavewatch3``. It requires Python >= 3.8 to run. +To get started you will first need to install the *bmi-wavewatch3* package, which is currently distributed +on `PyPI`_ and `conda-forge`_. -If you simply can't wait for the latest release, you can install *bmi_wavewatch3* -directly from GitHub, +.. tab:: pip + + .. code-block:: bash + + pip install bmi-wavewatch3 + +.. tab:: mamba + + .. code-block:: bash + + conda install mamba + mamba install bmi-wavewatch3 -c conda-forge + +.. tab:: conda + + .. code-block:: bash + + conda install bmi-wavewatch3 -c conda-forge + +You can now begin to download WAVEWATCH III data with the *ww3* command, .. code-block:: bash - $ pip install git+https://github.com/csdms/bmi-wavewatch3 + ww3 fetch "2010-05-22" -*bmi_wavewatch3* is also available through *conda*, ``conda install bmi-wavewatch3 -c conda-forge``. +.. _PyPI: https://pypi.org/project/bmi-wavewatch3/ +.. _conda-forge: https://github.com/conda-forge/bmi-wavewatch3-feedstock -.. end-installation +.. end-quickstart Usage ----- @@ -67,11 +87,11 @@ To get started, you can download *WAVEWATCH III* data by date with the *ww3* com .. code-block:: bash - $ ww3 fetch "2010-05-22" + ww3 fetch "2010-05-22" You can also do this through Python, -.. code-block:: python +.. code-block:: pycon >>> from bmi_wavewatch3 import WaveWatch3 >>> WaveWatch3.fetch("2010-05-22") @@ -79,7 +99,7 @@ You can also do this through Python, The *bmi_wavewatch3* package provides the ``WaveWatch3`` class for downloading data and presenting it as an *xarray* *Dataset*. -.. code-block:: python +.. code-block:: pycon >>> from bmi_wavewatch3 import WaveWatch3 >>> ww3 = WaveWatch3("2010-05-22") @@ -89,7 +109,7 @@ presenting it as an *xarray* *Dataset*. Use the ``inc`` method to advance in time month-by-month, -.. code-block:: python +.. code-block:: pycon >>> ww3.date '2010-05-22' @@ -121,7 +141,7 @@ separated from the date with a ``T`` (i.e. times can be given as ``YYYY-MM-DDTHH .. code:: bash - $ ww3 plot --grid=at_4m --data-var=swh "2010-09-15T15" + ww3 plot --grid=at_4m --data-var=swh "2010-09-15T15" .. image:: https://raw.githubusercontent.com/csdms/bmi-wavewatch3/main/docs/source/_static/hurricane_julia-light.png :width: 100% @@ -145,14 +165,14 @@ Plot data from Python This example is similar to the previous but uses the *bmi_wavewatch3* Python interface. -.. code:: python +.. code:: pycon >>> from bmi_wavewatch3 import WaveWatch3 >>> ww3 = WaveWatch3("2009-11-08") The data can be accessed as an *xarray* *Dataset* through the ``data`` attribute. -.. code:: python +.. code:: pycon >>> ww3.data @@ -182,7 +202,7 @@ The data can be accessed as an *xarray* *Dataset* through the ``data`` attribute The ``step`` attribute points to the current time slice into the data (i.e number of three hour increments since the start of the month), -.. code:: python +.. code:: pycon >>> ww3.step 56 @@ -210,6 +230,3 @@ since the start of the month), .. _WAVEWATCH III thredds: https://www.ncei.noaa.gov/thredds-ocean/catalog/ncep/nww3/catalog.html .. _Singlegrid data: https://polar.ncep.noaa.gov/waves/hindcasts/nww3/ .. _Multigrid data: https://polar.ncep.noaa.gov/waves/hindcasts/multi_1/ - - - diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/bmi-wavewatch3-logo-dark.svg b/docs/_static/bmi-wavewatch3-logo-dark.svg new file mode 100644 index 0000000..db45400 --- /dev/null +++ b/docs/_static/bmi-wavewatch3-logo-dark.svg @@ -0,0 +1,477 @@ + + + + + + + + 2022-08-01T11:58:26.564827 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/bmi-wavewatch3-logo-light.svg b/docs/_static/bmi-wavewatch3-logo-light.svg new file mode 100644 index 0000000..7066d7c --- /dev/null +++ b/docs/_static/bmi-wavewatch3-logo-light.svg @@ -0,0 +1,477 @@ + + + + + + + + 2022-08-01T11:58:13.913316 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/_static/hurricane_julia-dark.png b/docs/_static/hurricane_julia-dark.png similarity index 100% rename from docs/source/_static/hurricane_julia-dark.png rename to docs/_static/hurricane_julia-dark.png diff --git a/docs/source/_static/hurricane_julia-light.png b/docs/_static/hurricane_julia-light.png similarity index 100% rename from docs/source/_static/hurricane_julia-light.png rename to docs/_static/hurricane_julia-light.png diff --git a/docs/source/_static/powered-by-logo-header.png b/docs/_static/powered-by-logo-header.png similarity index 100% rename from docs/source/_static/powered-by-logo-header.png rename to docs/_static/powered-by-logo-header.png diff --git a/docs/source/_static/ww3_global_swh-dark.png b/docs/_static/ww3_global_swh-dark.png similarity index 100% rename from docs/source/_static/ww3_global_swh-dark.png rename to docs/_static/ww3_global_swh-dark.png diff --git a/docs/source/_static/ww3_global_swh-light.png b/docs/_static/ww3_global_swh-light.png similarity index 100% rename from docs/source/_static/ww3_global_swh-light.png rename to docs/_static/ww3_global_swh-light.png diff --git a/docs/api/.gitignore b/docs/api/.gitignore new file mode 100644 index 0000000..b0445ce --- /dev/null +++ b/docs/api/.gitignore @@ -0,0 +1,2 @@ +# auto-generated with sphinx-apidoc +bmi_wavewatch3*.rst diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000..f7b5b00 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,18 @@ +Developer Documentation +----------------------- + +The following pages provide technical documentation for the API for +the *bmi_wavewatch3* package. + +There are two main interfaces to *bmi_wavewatch3*, + +* :class:`~bmi_wavewatch3.WaveWatch3`: This is the main API for users wanting access + to WAVEWATCH III through Python. +* :class:`~bmi_wavewatch3.BmiWaveWatch3`: This class provides a Basic Model Interface and + is meant to be used when coupling with other BMI-enabled models/data. + +.. toctree:: + :maxdepth: 4 + :hidden: + + bmi_wavewatch3 diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..e122f91 --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/docs/changes.rst b/docs/changes.rst new file mode 100644 index 0000000..d9e113e --- /dev/null +++ b/docs/changes.rst @@ -0,0 +1 @@ +.. include:: ../CHANGES.rst diff --git a/docs/source/conf.py b/docs/conf.py similarity index 92% rename from docs/source/conf.py rename to docs/conf.py index 74dafe0..49c27ce 100644 --- a/docs/source/conf.py +++ b/docs/conf.py @@ -62,6 +62,7 @@ "sphinx.ext.viewcode", "sphinx.ext.napoleon", "sphinx.ext.autosummary", + "sphinx_copybutton", "sphinx_inline_tabs", "sphinxcontrib.towncrier", ] @@ -139,4 +140,13 @@ towncrier_draft_autoversion_mode = "draft" # or: 'sphinx-release', 'sphinx-version' towncrier_draft_include_empty = True -towncrier_draft_working_directory = pathlib.Path(docs_dir).parent.parent +towncrier_draft_working_directory = pathlib.Path(docs_dir).parent + +# -- Options for intersphinx extension --------------------------------------- + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), + "xarray": ("https://docs.xarray.dev/en/stable/", None), +} diff --git a/docs/contributing/developer_install.rst b/docs/contributing/developer_install.rst new file mode 100644 index 0000000..12d8713 --- /dev/null +++ b/docs/contributing/developer_install.rst @@ -0,0 +1,60 @@ +.. _developer_install: + +================= +Developer Install +================= + +.. important:: + + The following commands will install *bmi-wavewatch3* into your current environment. Although + not necessary, we **highly recommend** you install *bmi-wavewatch3* into its own + :ref:`virtual environment `. + +If you will be modifying code or contributing new code to *bmi-wavewatch3*, you will first +need to get *bmi-wavewatch3*'s source code and then install *bmi-wavewatch3* from that code. + +-------------- +Source Install +-------------- + +*bmi-wavewatch3* is actively being developed on GitHub, where the code is freely available. +If you would like to modify or contribute code, you can either clone our +repository + +.. code-block:: bash + + git clone git://github.com/csdms/bmi-wavewatch3.git + +or download the `tarball `_ +(a zip file is available for Windows users): + +.. code-block:: bash + + curl -OL https://github.com/csdms/bmi-wavewatch3/tarball/master + +Once you have a copy of the source code, you can install it into your current +Python environment, + + +.. tab:: pip + + .. code-block:: bash + + cd bmi-wavewatch3 + pip install -e . + +.. tab:: mamba + + .. code-block:: bash + + cd bmi-wavewatch3 + mamba install --file=requirements.txt + pip install -e . + +.. tab:: conda + + .. code-block:: bash + + cd bmi-wavewatch3 + conda install --file=requirements.txt + pip install -e . diff --git a/docs/contributing/environments.rst b/docs/contributing/environments.rst new file mode 100644 index 0000000..d937ed2 --- /dev/null +++ b/docs/contributing/environments.rst @@ -0,0 +1,43 @@ +.. _virtual_environments: + +==================== +Virtual Environments +==================== + +A virtual environment is a self-contained directory tree that contains a Python installation for a particular +version of Python along with additional packages. It solves the problem of one application's +package requirements conflicting with another's. + +Two popular tools used for creating virtual environments are the built-in *venv* module and *conda* +(or the *much* faster and more reliable *mamba*). For virtual environments created using *conda*/*mamba*, +you can use either *conda*/*mamba* or *pip* to install additional packages, while *venv*-created environments +should stick with *pip*. + +.. tab:: mamba + + .. code-block:: bash + + conda install mamba -c conda-forge + mamba create -n ww3 + mamba activate ww3 + +.. tab:: conda + + .. code-block:: bash + + conda create -n ww3 + conda activate ww3 + +.. tab:: venv + + .. code-block:: bash + + python -m venv .venv + source .venv/bin/activate + +Note that you will need to activate this environment every time you want to use it in a new shell. + +Helpful links on managing virtual environments: + +* `conda environments `_. +* `venv environments `_. diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst new file mode 100644 index 0000000..3198882 --- /dev/null +++ b/docs/contributing/index.rst @@ -0,0 +1,25 @@ +============ +Contributing +============ + +Thank you for your interest in *bmi-wavewatch3*! ✨ + +*bmi-wavewatch3* is a volunteer maintained open source project, and we welcome contributions +of all forms. This section of *bmi-wavewatch3*'s documentation serves as a resource to help +you to contribute to the project. + + +:ref:`Workflow `: + Describes how to work on this project. Start here if you're a new contributor. +:ref:`Developer Install `: + Describes how to install *bmi-wavewatch3* when working with the source code. +:ref:`Virtual Environments `: + Describes how to use virtual environments. + +.. toctree:: + :maxdepth: 1 + :hidden: + + workflow + developer_install + environments diff --git a/docs/contributing/workflow.rst b/docs/contributing/workflow.rst new file mode 100644 index 0000000..c6a93e2 --- /dev/null +++ b/docs/contributing/workflow.rst @@ -0,0 +1,85 @@ +.. _workflow: + +======== +Workflow +======== + +This page describes the tooling used during development of this project. It also serves +as a reference for the various commands that you would use when working on this project. + +-------- +Overview +-------- + +This project uses the `GitHub Flow`_ for collaboration. The codebase contains primarily Python code. + +- `nox`_ is used for automating development tasks. +- `sphinx-autobuild`_ is used to provide live-reloading pages when working on the docs. +- `pre-commit`_ is used for running the linters. + +------------- +Initial Setup +------------- + +To work on this project, you need to have Python 3.8+. + +* Clone this project using git: + + .. code:: bash + + git clone https://github.com/csdms/bmi-wavewatch3.git + cd bmi-wavewatch3 + +* Install the project's development workflow runner: + + .. code:: bash + + pip install nox + +You're all set for working on this project. + +-------- +Commands +-------- + +Code Linting +============ + +.. code:: bash + + nox -s lint + + +Run the linters, as configured with `pre-commit`_. + +Local Development Server +======================== + +.. code:: bash + + nox -s live-docs + + +Serve this project's documentation locally, using `sphinx-autobuild`_. This will open +the generated documentation page in your browser. + +The server also watches for changes made to the documentation (`docs/source`), which +will trigger a rebuild. Once the build is completed, the server will +reload any open pages using *livereload*. + +Documentation Generation +======================== + +.. code:: bash + + nox -s build-docs + +Generate the documentation for *bmi-wavewatch3* into the `docs/build` folder. This (mostly) +does the same thing as ``nox -s docs-live``, except it invokes ``sphinx-build`` instead +of `sphinx-autobuild`_. + + +.. _GitHub Flow: https://guides.github.com/introduction/flow/ +.. _nox: https://nox.readthedocs.io/en/stable/ +.. _sphinx-autobuild: https://github.com/executablebooks/sphinx-autobuild +.. _pre-commit: https://pre-commit.com/ diff --git a/docs/source/index.rst b/docs/index.rst similarity index 92% rename from docs/source/index.rst rename to docs/index.rst index 2e8de4b..be36121 100644 --- a/docs/source/index.rst +++ b/docs/index.rst @@ -13,7 +13,7 @@ :class: only-dark -.. include:: ../../README.rst +.. include:: ../README.rst :start-after: .. start-abstract :end-before: .. end-abstract @@ -36,16 +36,17 @@ :maxdepth: 2 :hidden: - installation + quickstart usage plotting - + .. toctree:: :caption: Development :maxdepth: 2 :hidden: + Contributing + API Reference Release Notes Contributors License - API Reference diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 0000000..68c5792 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1 @@ +.. include:: ../LICENSE.rst diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 747ffb7..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/plotting.rst b/docs/plotting.rst new file mode 100644 index 0000000..1cc4946 --- /dev/null +++ b/docs/plotting.rst @@ -0,0 +1,7 @@ +=============== +Plotting Output +=============== + +.. include:: ../README.rst + :start-after: .. start-plotting + :end-before: .. end-plotting diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000..78b9195 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,17 @@ +========== +Quickstart +========== + +.. important:: + + The following commands will install *bmi-wavewatch3* into your current environment. Although + not necessary, we **highly recommend** you install *bmi-wavewatch3* into its own + :ref:`virtual environment `. + +.. include:: ../README.rst + :start-after: .. start-quickstart + :end-before: .. end-quickstart + +If you would like the very latest development version of *bmi-wavewatch3* or want to modify +or contribute code to the *bmi-wavewatch3* project, you will need to do a +:ref:`developer installation ` of *bmi-wavewatch3* from source. diff --git a/docs/source/_static/bmi-wavewatch3-logo-dark.svg b/docs/source/_static/bmi-wavewatch3-logo-dark.svg deleted file mode 100644 index 1f34f4d..0000000 --- a/docs/source/_static/bmi-wavewatch3-logo-dark.svg +++ /dev/null @@ -1,477 +0,0 @@ - - - - - - - - 2022-08-01T11:58:26.564827 - image/svg+xml - - - Matplotlib v3.5.2, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/source/_static/bmi-wavewatch3-logo-light.svg b/docs/source/_static/bmi-wavewatch3-logo-light.svg deleted file mode 100644 index 77c96f8..0000000 --- a/docs/source/_static/bmi-wavewatch3-logo-light.svg +++ /dev/null @@ -1,477 +0,0 @@ - - - - - - - - 2022-08-01T11:58:13.913316 - image/svg+xml - - - Matplotlib v3.5.2, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/source/_templates/sidebaroutro.html b/docs/source/_templates/sidebaroutro.html deleted file mode 100644 index bbf0fec..0000000 --- a/docs/source/_templates/sidebaroutro.html +++ /dev/null @@ -1,3 +0,0 @@ - - Powered by CSDMS - diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst deleted file mode 100644 index eb5ff7d..0000000 --- a/docs/source/api/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Developer Documentation ------------------------ - -.. toctree:: - :maxdepth: 2 - - modules \ No newline at end of file diff --git a/docs/source/authors.rst b/docs/source/authors.rst deleted file mode 100644 index 7739272..0000000 --- a/docs/source/authors.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../AUTHORS.rst diff --git a/docs/source/changes.rst b/docs/source/changes.rst deleted file mode 100644 index d76c92b..0000000 --- a/docs/source/changes.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../CHANGES.rst diff --git a/docs/source/installation.rst b/docs/source/installation.rst deleted file mode 100644 index 9139ac7..0000000 --- a/docs/source/installation.rst +++ /dev/null @@ -1,6 +0,0 @@ -Installation -============ - -.. include:: ../../README.rst - :start-after: .. start-installation - :end-before: .. end-installation \ No newline at end of file diff --git a/docs/source/license.rst b/docs/source/license.rst deleted file mode 100644 index 2f7644a..0000000 --- a/docs/source/license.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../LICENSE.rst diff --git a/docs/source/plotting.rst b/docs/source/plotting.rst deleted file mode 100644 index ddeee96..0000000 --- a/docs/source/plotting.rst +++ /dev/null @@ -1,6 +0,0 @@ -Plotting Output -=============== - -.. include:: ../../README.rst - :start-after: .. start-plotting - :end-before: .. end-plotting \ No newline at end of file diff --git a/docs/source/usage.rst b/docs/source/usage.rst deleted file mode 100644 index fc8a42f..0000000 --- a/docs/source/usage.rst +++ /dev/null @@ -1,6 +0,0 @@ -Usage -===== - -.. include:: ../../README.rst - :start-after: .. start-usage - :end-before: .. end-usage \ No newline at end of file diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..ee291e3 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,7 @@ +===== +Usage +===== + +.. include:: ../README.rst + :start-after: .. start-usage + :end-before: .. end-usage diff --git a/news/14.docs b/news/14.docs index b94c5b2..dcd6505 100644 --- a/news/14.docs +++ b/news/14.docs @@ -1,2 +1 @@ Added documentation, using the *furo* theme, to appear on `readthedocs.io `_. - diff --git a/news/14.misc b/news/14.misc index d7e1a8f..0dc3ef9 100644 --- a/news/14.misc +++ b/news/14.misc @@ -1,4 +1,2 @@ Added a GitHub Actions workflow that checks to see that pull requests contain at least one news fragment. - - diff --git a/news/15.misc b/news/15.misc index ea4e0a2..3e33982 100644 --- a/news/15.misc +++ b/news/15.misc @@ -1,3 +1,2 @@ Added a *nox* configuration file, *noxfile.py*, that scripts some routine package maintenance tasks. - diff --git a/news/16.docs b/news/16.docs new file mode 100644 index 0000000..6d1c0d0 --- /dev/null +++ b/news/16.docs @@ -0,0 +1,2 @@ +Added a "contributing" sections to the documentation that describes how +developers can install and contribute to the project. diff --git a/noxfile.py b/noxfile.py index 475734b..c68afb4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,6 +5,9 @@ import nox +PROJECT = "bmi_wavewatch3" +ROOT = pathlib.Path(__file__).parent + @nox.session def tests(session: nox.Session) -> None: @@ -29,14 +32,12 @@ def cli(session: nox.Session) -> None: session.run("ww3", "url", "--help") -@nox.session +@nox.session(reuse_venv=True) def lint(session: nox.Session) -> None: """Look for lint.""" session.install("pre-commit") session.run("pre-commit", "run", "--all-files") - # towncrier(session) - @nox.session def towncrier(session: nox.Session) -> None: @@ -45,16 +46,60 @@ def towncrier(session: nox.Session) -> None: session.run("towncrier", "check", "--compare-with", "origin/main") -@nox.session -def docs(session: nox.Session) -> None: +@nox.session(name="build-docs", reuse_venv=True) +def build_docs(session: nox.Session) -> None: """Build the docs.""" - session.install(".[doc]") + with session.chdir(ROOT): + session.install(".[doc]") + + clean_docs(session) + + with session.chdir(ROOT): + session.run( + "sphinx-apidoc", + "-e", + "-force", + "--no-toc", + "--module-first", + "--templatedir", + "docs/_templates", + "-o", + "docs/api", + "src/bmi_wavewatch3", + ) + session.run( + "sphinx-build", + "-b", + "dirhtml", + "-W", + "docs", + "build/html", + ) + + +@nox.session(python=False, name="clean-docs") +def clean_docs(session: nox.Session) -> None: + """Clean up the docs folder.""" + with session.chdir(ROOT / "docs"): + if os.path.exists("build/html"): + shutil.rmtree("build/html") + + for p in pathlib.Path("api").rglob("bmi_wavewatch3*.rst"): + p.unlink() + - session.chdir("docs/source") - if os.path.exists("build"): - shutil.rmtree("build") - session.run("sphinx-apidoc", "--force", "-o", "api", "../../src/bmi_wavewatch3") - session.run("sphinx-build", "-b", "html", "-W", ".", "build/html") +@nox.session(name="live-docs", reuse_venv=True) +def live_docs(session: nox.Session) -> None: + session.install(".[doc]") + session.run( + "sphinx-autobuild", + "-v", + "-b", + "dirhtml", + "docs", + "build/html", + "--open-browser", + ) @nox.session @@ -115,7 +160,9 @@ def clean(session): shutil.rmtree(f"src/{PROJECT}.egg-info", ignore_errors=True) shutil.rmtree(".pytest_cache", ignore_errors=True) shutil.rmtree(".venv", ignore_errors=True) - for p in chain(ROOT.rglob("*.py[co]"), ROOT.rglob("__pycache__")): + for p in chain( + ROOT.rglob("*.py[co]"), ROOT.rglob("__pycache__"), ROOT.rglob(".DS_Store") + ): if p.is_dir(): p.rmdir() else: diff --git a/pyproject.toml b/pyproject.toml index 16351aa..077f77e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,8 @@ testing = [ ] doc = [ "sphinx", + "sphinx-autobuild", + "sphinx-copybutton", "pygments>=2.4", "sphinx-inline-tabs", "furo", diff --git a/setup.py b/setup.py index b024da8..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ from setuptools import setup - setup() diff --git a/src/bmi_wavewatch3/__init__.py b/src/bmi_wavewatch3/__init__.py index 45ffa8b..5758061 100644 --- a/src/bmi_wavewatch3/__init__.py +++ b/src/bmi_wavewatch3/__init__.py @@ -1,3 +1,8 @@ +"""Access WAVEWATCH III data through Python. + +The *bmi_wavewatch3* Python package provides both a command line interface +and a programming interface for downloading and working with *WAVEWATCH III* data. +""" from ._version import __version__ from .bmi import BmiWaveWatch3 from .downloader import WaveWatch3Downloader @@ -5,7 +10,6 @@ from .source import SOURCES from .wavewatch3 import WaveWatch3 - __all__ = [ "__version__", "BmiWaveWatch3", diff --git a/src/bmi_wavewatch3/bmi.py b/src/bmi_wavewatch3/bmi.py index ebca254..5b2b8bc 100644 --- a/src/bmi_wavewatch3/bmi.py +++ b/src/bmi_wavewatch3/bmi.py @@ -1,3 +1,4 @@ +"""A Basic Model Interface to the *wavewatch3* package.""" from collections import namedtuple from typing import Tuple @@ -20,7 +21,6 @@ class BmiWaveWatch3(Bmi): - """BMI-mediated access to WaveWatch III data.""" _name = "bmi-wavewatch3" @@ -44,7 +44,7 @@ def finalize(self) -> None: self._data = None def get_component_name(self) -> str: - """Name of the component. + """Return the name of the component. Returns ------- @@ -54,7 +54,7 @@ def get_component_name(self) -> str: return self._name def get_current_time(self) -> float: - """Current time of the model. + """Return the current time of the model. Returns ------- @@ -64,7 +64,7 @@ def get_current_time(self) -> float: return float(self._data.step[self._time_index]) * 1e-9 / 60 / 60 def get_end_time(self) -> float: - """End time of the model. + """Return the end time of the model. Returns ------- @@ -420,7 +420,7 @@ def get_start_time(self) -> float: return 0.0 def get_time_step(self) -> float: - """Current time step of the model. + """Return the current time step of the model. The model time step should be of type float. @@ -478,7 +478,7 @@ def get_value_at_indices( An input or output variable name, a CSDMS Standard Name. dest : ndarray A numpy array into which to place the values. - indices : array_like + inds : array_like The indices into the variable array. Returns @@ -693,9 +693,9 @@ def set_value(self, name: str, values: numpy.ndarray) -> None: Parameters ---------- - var_name : str + name : str An input or output variable name, a CSDMS Standard Name. - src : array_like + values : array_like The new value for the specified variable. """ raise NotImplementedError("set_value") @@ -707,9 +707,9 @@ def set_value_at_indices( Parameters ---------- - var_name : str + name : str An input or output variable name, a CSDMS Standard Name. - indices : array_like + inds : array_like The indices into the variable array. src : array_like The new value for the specified variable. @@ -749,7 +749,7 @@ def _grids_from_dataset(dataset): def _var_grid(dataset): - var_to_grid = dict() + var_to_grid = {} for name, var in dataset.data_vars.items(): shape = var.GRIB_Ny, var.GRIB_Nx yx_spacing = ( diff --git a/src/bmi_wavewatch3/cli.py b/src/bmi_wavewatch3/cli.py index f21dd4c..1a77ca1 100644 --- a/src/bmi_wavewatch3/cli.py +++ b/src/bmi_wavewatch3/cli.py @@ -1,3 +1,4 @@ +"""Command line interface to *bmi_wavewatch3*.""" import inspect import itertools import os @@ -9,23 +10,40 @@ from functools import partial from multiprocessing import Pool, RLock -import click +# import click import matplotlib.pyplot as plt +import rich_click as click from tqdm.auto import tqdm + from .downloader import WaveWatch3Downloader from .errors import ChoiceError, DateValueError from .source import SOURCES from .wavewatch3 import WaveWatch3 +click.rich_click.ERRORS_SUGGESTION = ( + "Try running the '--help' flag for more information." +) +click.rich_click.ERRORS_EPILOGUE = ( + "To find out more, visit https://github.com/csdms/bmi-wavewatch3" +) +click.rich_click.STYLE_ERRORS_SUGGESTION = "yellow italic" +click.rich_click.SHOW_ARGUMENTS = True +click.rich_click.GROUP_ARGUMENTS_OPTIONS = False +click.rich_click.SHOW_METAVARS_COLUMN = True +click.rich_click.USE_MARKDOWN = True + out = partial(click.secho, bold=True, file=sys.stderr) err = partial(click.secho, fg="red", file=sys.stderr) -DownloadResult = namedtuple("DownloadResult", ["remote", "local", "success", "status"]) +_DownloadResult = namedtuple( + "_DownloadResult", ["remote", "local", "success", "status"] +) -def validate_date(ctx, param, value): +def _validate_date(ctx, param, value): + """Check if a provided date is valid.""" source = SOURCES[ctx.parent.params["source"]] for date_str in [value] if isinstance(value, str) else value: @@ -36,7 +54,8 @@ def validate_date(ctx, param, value): return value -def validate_quantity(ctx, param, value): +def _validate_quantity(ctx, param, value): + """Check if a provided quantity is valid.""" source = SOURCES[ctx.parent.params["source"]] if not value: return sorted(source.QUANTITIES) @@ -49,7 +68,8 @@ def validate_quantity(ctx, param, value): return value -def validate_data_var(ctx, param, value): +def _validate_data_var(ctx, param, value): + """Check if a provided data variable is valid.""" data_var_to_quantity = { "dirpw": "dp", "swh": "hs", @@ -73,7 +93,8 @@ def validate_data_var(ctx, param, value): return value -def validate_grid(ctx, param, value): +def _validate_grid(ctx, param, value): + """Check if a provided WAVEWATCH III grid is valid.""" source = SOURCES[ctx.parent.params["source"]] if not value: return inspect.signature(source).parameters["grid"].default @@ -85,7 +106,7 @@ def validate_grid(ctx, param, value): return value -@click.group(chain=True) +@click.group(chain=False) @click.version_option() @click.option( "--cd", @@ -111,20 +132,26 @@ def validate_grid(ctx, param, value): def ww3(cd, silent, verbose, source) -> None: """Download WAVEWATCH III data. - \b - Examples: + Dates can be provided either as iso-formatted dates (*YYYY-MM-DD*) or + as date times (**YYYY-MM-DDTHH**). + + # Examples: - Download WAVEWATCH III data by date, + The following will download the WAVEWATCH III datasets that contain + the given dates, - $ ww3 fetch 2010-05-22 2010-05-22 + ```bash + $ ww3 fetch "2010-05-22" "2010-05-22" + ``` """ os.chdir(cd) @ww3.command() -@click.option("--all", is_flag=True, help="info on all sources") +@click.option("--all", is_flag=True, help="Get info for all available sources") @click.pass_context def info(ctx, all): + """Get information about a WAVEWATCHIII data source.""" source = ctx.parent.params["source"] sources = SOURCES if all else {source: SOURCES[source]} @@ -149,14 +176,14 @@ def info(ctx, all): @ww3.command() -@click.argument("date", nargs=-1, callback=validate_date) -@click.option("--grid", default=None, help="Grid to download", callback=validate_grid) +@click.argument("date", nargs=-1, callback=_validate_date) +@click.option("--grid", default=None, help="Grid to download", callback=_validate_grid) @click.option( "--quantity", "-q", multiple=True, help="Quantity to download", - callback=validate_quantity, + callback=_validate_quantity, ) @click.pass_context def url(ctx, date, grid, quantity): @@ -169,7 +196,7 @@ def url(ctx, date, grid, quantity): @ww3.command() -@click.argument("date", nargs=-1, callback=validate_date) +@click.argument("date", nargs=-1, callback=_validate_date) @click.option("--dry-run", is_flag=True, help="do not actually download data") @click.option( "--force", @@ -178,13 +205,13 @@ def url(ctx, date, grid, quantity): help="force download even if local file already exists", ) @click.option("--file", type=click.File("r", lazy=False), help="read dates from a file") -@click.option("--grid", default=None, help="Grid to download", callback=validate_grid) +@click.option("--grid", default=None, help="Grid to download", callback=_validate_grid) @click.option( "--quantity", "-q", multiple=True, help="Quantity to download", - callback=validate_quantity, + callback=_validate_quantity, ) @click.pass_context def fetch(ctx, date, dry_run, force, file, grid, quantity): @@ -203,7 +230,7 @@ def fetch(ctx, date, dry_run, force, file, grid, quantity): out(url) if not dry_run: - results = _retreive_urls(urls, disable=silent, force=force) + results = _retrieve_urls(urls, disable=silent, force=force) if not silent: [ @@ -231,7 +258,7 @@ def fetch(ctx, date, dry_run, force, file, grid, quantity): @click.option("--yes", is_flag=True, help="remove files without prompting") @click.pass_context def clean(ctx, dry_run, cache_dir, yes): - """Remove cached date files.""" + """Remove cached data files.""" verbose = ctx.parent.params["verbose"] silent = ctx.parent.params["silent"] @@ -271,13 +298,13 @@ def clean(ctx, dry_run, cache_dir, yes): @ww3.command() -@click.argument("date", callback=validate_date) -@click.option("--grid", default=None, help="Grid to download", callback=validate_grid) +@click.argument("date", callback=_validate_date, metavar="YYYY-MM-DD[THH]") +@click.option("--grid", default=None, help="Grid to download", callback=_validate_grid) @click.option( "--data-var", help="Data variable to plot", default="swh", - callback=validate_data_var, + callback=_validate_data_var, ) @click.pass_context def plot(ctx, date, grid, data_var): @@ -317,20 +344,20 @@ def plot(ctx, date, grid, data_var): plt.show() -def _retreive_urls(urls, disable=False, force=False): +def _retrieve_urls(urls, disable=False, force=False): tqdm.set_lock(RLock()) p = Pool(initializer=tqdm.set_lock, initargs=(tqdm.get_lock(),)) return p.map( - partial(_retreive, disable=disable, force=force), list(enumerate(urls)) + partial(_retrieve, disable=disable, force=force), list(enumerate(urls)) ) -def _retreive(position_and_url, disable=False, force=False): +def _retrieve(position_and_url, disable=False, force=False): position, url = position_and_url name = pathlib.Path(urllib.parse.urlparse(url).path).name if not pathlib.Path(name).is_file() or force: - with TqdmUpTo( + with _TqdmUpTo( unit="B", unit_scale=True, unit_divisor=1024, @@ -341,7 +368,7 @@ def _retreive(position_and_url, disable=False, force=False): leave=False, ) as t: try: - WaveWatch3Downloader.retreive( + WaveWatch3Downloader.retrieve( url, filename=name, reporthook=t.update_to, force=force ) except (urllib.error.HTTPError, urllib.error.URLError) as error: @@ -352,13 +379,16 @@ def _retreive(position_and_url, disable=False, force=False): else: success, status = True, "cached" - return DownloadResult( + return _DownloadResult( local=pathlib.Path(name).absolute(), remote=url, success=success, status=status ) -class TqdmUpTo(tqdm): +class _TqdmUpTo(tqdm): + """Progress bar to use when retrieving data.""" + def update_to(self, b=1, bsize=1, tsize=None): + """Update the progress bar as more data are downloaded.""" if tsize is not None: self.total = tsize return self.update(b * bsize - self.n) # also sets self.n = b * bsize diff --git a/src/bmi_wavewatch3/downloader.py b/src/bmi_wavewatch3/downloader.py index 7a56ec1..26ec9d8 100644 --- a/src/bmi_wavewatch3/downloader.py +++ b/src/bmi_wavewatch3/downloader.py @@ -1,3 +1,4 @@ +"""Download and cache a WAVEWATCH III data file.""" import gzip import pathlib import urllib @@ -5,17 +6,49 @@ class WaveWatch3Downloader: + """Download a WAVEWATCH III data file from a URL.""" + def __init__(self, url, force=False): - self._url = url + """Create a downloader. + + Parameters + ---------- + url : str + Location of the file to download. + force : bool, optional + Download the file even if a cached file already exists. + """ + # self._url = url self._force = force - self._filepath = None + # self._filepath = None @staticmethod def url_file_part(url): + """Return the file part of a url.""" return pathlib.Path(urllib.parse.urlparse(url).path).name @staticmethod def retreive(url, filename=None, reporthook=None, force=False): + """Fetch a file from a URL. + + Parameters + ---------- + url : str + Location of the file to download. + filename : str, optional + Name of the local file to save the download file to. If not + provided, use the name of the remote file. + reporthook : func, optional + Function used for reporting download status. The function + signature is the same as that of :func:`urllib.request.urlretrieve` + force : bool, optional + Download the file even if a cached file already exists. + + Returns + ------- + pathlib.Path + Absolute path to the downloaded file. + """ if filename is None: filename = WaveWatch3Downloader.url_file_part(url) if not pathlib.Path(filename).is_file() or force: @@ -32,16 +65,29 @@ def retreive(url, filename=None, reporthook=None, force=False): @staticmethod def unzip(filepath): + """Unzip a file. + + Parameters + ---------- + filepath : str or path-like + Path to the file to unzip. + + Returns + ------- + str + Name of the unzipped file. + """ filepath = pathlib.Path(filepath) with gzip.open(filepath, "rb") as zip_file: with open(filepath.stem, "wb") as fp: fp.write(zip_file.read()) return pathlib.Path(filepath.stem) - @property - def filepath(self): - return self._filepath + # @property + # def filepath(self): + # """Return the name of the local file.""" + # return self._filepath - @property - def url(self): - return self._url + # @property + # def url(self): + # return self._url diff --git a/src/bmi_wavewatch3/errors.py b/src/bmi_wavewatch3/errors.py index 1f4bc5d..b8c5d90 100644 --- a/src/bmi_wavewatch3/errors.py +++ b/src/bmi_wavewatch3/errors.py @@ -1,20 +1,31 @@ +"""Exceptions raised from the *bmi_wavewatch3* package.""" + + class WaveWatch3Error(Exception): + """Base exception for *bmi_wavewatch3* errors.""" + pass class ChoiceError(WaveWatch3Error): + """Raise this exceptions when an invalid choice is made.""" + def __init__(self, choice, choices): self._choice = choice self._choices = tuple(choices) def __str__(self): + """Return the exception as a string.""" choices = ", ".join([repr(c) for c in self._choices]) return f"{self._choice!r}: invalid choice (not one of {choices})" class DateValueError(WaveWatch3Error): + """Raise this exception when an invalid date is encountered.""" + def __init__(self, msg): self._msg = msg def __str__(self): + """Return the exception as a string.""" return self._msg diff --git a/src/bmi_wavewatch3/source.py b/src/bmi_wavewatch3/source.py index 4da3ac6..6078c14 100644 --- a/src/bmi_wavewatch3/source.py +++ b/src/bmi_wavewatch3/source.py @@ -1,3 +1,4 @@ +"""Define the various sources for WAVEWATCH III data.""" import datetime import pathlib import urllib @@ -32,10 +33,12 @@ def __init__(self, date, quantity, grid="glo_30m"): @property def path(self): + """Return the path to the remote WAVEWATCH III data file.""" raise NotImplementedError("path") @property def filename(self): + """Return the name of WAVEWATCH III data file.""" raise NotImplementedError("filename") def __str__(self): @@ -122,12 +125,25 @@ def validate_date(cls, date): datetime.datetime.fromisoformat(date) except ValueError as error: raise DateValueError(str(error)) - date_in_range_or_raise(date, lower=cls.MIN_DATE, upper=cls.MAX_DATE) + _date_in_range_or_raise(date, lower=cls.MIN_DATE, upper=cls.MAX_DATE) return date -def date_in_range_or_raise(date, lower=None, upper=None): +def _date_in_range_or_raise(date, lower=None, upper=None): + """Check if a date is within a given range. + + Parameters + ---------- + date : str + An iso-formatted date. + lower : str, optional + The lower bound for the date. If not provided, there + is no lower bound. + upper : str, optional + The upper bound for the date. If not provided, there + is no upper bound. + """ date = datetime.datetime.fromisoformat(date) lower = datetime.datetime.fromisoformat(lower) if lower else lower @@ -144,7 +160,8 @@ def date_in_range_or_raise(date, lower=None, upper=None): class WaveWatch3SourcePhase1(_WaveWatch3Source): - """ + """The WAVEWATCH III Phase 1 data source. + https://polar.ncep.noaa.gov/waves/hindcasts/nopp-phase1.php """ @@ -178,6 +195,7 @@ def __init__(self, date, quantity, grid="glo_30m"): @property def path(self): + """Return the path to the remote WAVEWATCH III data file.""" path = ( self.PREFIX / pathlib.PurePosixPath(f"{self.year}{self.month:02d}") / "grib" ) @@ -185,11 +203,13 @@ def path(self): @property def filename(self): + """Return the name of WAVEWATCH III data file.""" return f"multi_reanal.{self.grid}.{self.quantity}.{self.year}{self.month:02d}.grb2.gz" class WaveWatch3SourcePhase2(_WaveWatch3Source): - """ + """The WAVEWATCH III Phase 2 data source. + https://polar.ncep.noaa.gov/waves/hindcasts/nopp-phase2.php """ @@ -222,6 +242,7 @@ def __init__(self, date, quantity, grid="glo_30m_ext"): @property def path(self): + """Return the path to the remote WAVEWATCH III data file.""" path = ( self.PREFIX / pathlib.PurePosixPath(f"{self.year}{self.month:02d}") @@ -231,13 +252,15 @@ def path(self): @property def filename(self): + """Return the name of WAVEWATCH III data file.""" return ( f"multi_reanal.{self.grid}.{self.quantity}.{self.year}{self.month:02d}.grb2" ) class WaveWatch3SourceMultigrid(_WaveWatch3Source): - """ + """The WAVEWATCH III multi-grid data source. + https://polar.ncep.noaa.gov/waves/hindcasts/prod-multi_1.php """ @@ -250,6 +273,7 @@ class WaveWatch3SourceMultigrid(_WaveWatch3Source): @property def path(self): + """Return the path to the remote WAVEWATCH III data file.""" path = ( self.PREFIX / pathlib.PurePosixPath(f"{self.year}{self.month:02d}") @@ -259,11 +283,13 @@ def path(self): @property def filename(self): + """Return the name of WAVEWATCH III data file.""" return f"multi_1.{self.grid}.{self.quantity}.{self.year}{self.month:02d}.grb2" class WaveWatch3SourceMultigridExt(_WaveWatch3Source): - """ + """The WAVEWATCH III multi-grid extended data source. + https://polar.ncep.noaa.gov/waves/hindcasts/prod-multi_1.php """ @@ -289,6 +315,7 @@ class WaveWatch3SourceMultigridExt(_WaveWatch3Source): @property def path(self): + """Return the path to the remote WAVEWATCH III data file.""" path = ( self.PREFIX / pathlib.PurePosixPath(f"{self.year}{self.month:02d}") @@ -298,10 +325,13 @@ def path(self): @property def filename(self): + """Return the name of WAVEWATCH III data file.""" return f"multi_1.{self.grid}.{self.quantity}.{self.year}{self.month:02d}.grb2" class WaveWatch3SourceThredds(_WaveWatch3Source): + """The WAVEWATCH III thredds data source.""" + SCHEME = "https" NETLOC = "www.ncei.noaa.gov" PREFIX = "/thredds-ocean/fileServer/ncep/nww3" @@ -311,17 +341,20 @@ class WaveWatch3SourceThredds(_WaveWatch3Source): @property def path(self): + """Return the path to the remote WAVEWATCH III data file.""" path = self.PREFIX / pathlib.PurePosixPath(f"{self.year}", f"{self.month:02d}") path /= self.grid if self.year < 2017 else "gribs" return path / self.filename @property def filename(self): + """Return the name of WAVEWATCH III data file.""" return f"multi_1.{self.grid}.{self.quantity}.{self.year}{self.month:02d}.grb2" class WaveWatch3SourceSinglegrid(_WaveWatch3Source): - """ + """The WAVEWATCH III single grid data source. + https://polar.ncep.noaa.gov/waves/hindcasts/prod-nww3.php """ @@ -339,10 +372,12 @@ def __init__(self, date, quantity, grid="nww3"): @property def path(self): + """Return the path to the remote WAVEWATCH III data file.""" return pathlib.PurePosixPath(self.PREFIX) / self.filename @property def filename(self): + """Return the name of WAVEWATCH III data file.""" return f"{self.grid}.{self.quantity}.{self.year}{self.month:02d}.grb" diff --git a/src/bmi_wavewatch3/wavewatch3.py b/src/bmi_wavewatch3/wavewatch3.py index e0f7dfa..e7eda37 100644 --- a/src/bmi_wavewatch3/wavewatch3.py +++ b/src/bmi_wavewatch3/wavewatch3.py @@ -1,3 +1,5 @@ +"""Programmatically access WAVEWATCH III datasets.""" + import contextlib import datetime import os @@ -15,6 +17,8 @@ class WaveWatch3: + """Access WAVEWATCH III data.""" + def __init__( self, date, @@ -58,13 +62,14 @@ def __init__( @property def data(self): - """Current WAVEWATCH III data as an xarray.Dataset.""" + """Return the current WAVEWATCH III data as an :class:`xarray.Dataset`.""" if self._data is None: self._load_data() return self._data @property def step(self): + """Return the current location in the data.""" return self._step @step.setter @@ -84,17 +89,17 @@ def step(self, step): @property def source(self): - """Source from which data will be downloaded.""" + """Return the source from which data will be downloaded.""" return self._source @property def grid(self): - """The WAVEWATCH III grid region.""" + """Return the WAVEWATCH III grid region.""" return self._urls[0].grid @property def date(self): - """Current date as an isoformatted string.""" + """Return the current date as an iso-formatted string.""" return self._date.isoformat(timespec="hours") @date.setter @@ -111,24 +116,26 @@ def date(self, date): @property def year(self): - """The current year.""" + """Return the current year.""" return self._date.year @property def month(self): - """The current month.""" + """Return the current month.""" return self._date.month @property def day(self): + """Return the current day.""" return self._date.day @property def hour(self): + """Return the current hour.""" return self._date.hour def inc(self, months=1): - """Increment to current date by some number of months. + """Increment the current date by some number of months. Parameters ---------- @@ -138,7 +145,7 @@ def inc(self, months=1): self.date = (self._date + relativedelta(months=months)).isoformat() def _load_data(self): - """Load the current data into an xarray Dataset.""" + """Load the current data into an :class:`xarray.Dataset`.""" self._fetch_data() self._data = xr.open_mfdataset( [self._cache / url.filename for url in self._urls], @@ -152,11 +159,11 @@ def _load_data(self): def _fetch_data(self): """Download data in parallel.""" self._cache.mkdir(parents=True, exist_ok=True) - with as_cwd(self._cache): + with _as_cwd(self._cache): Pool().map(WaveWatch3Downloader.retreive, [str(url) for url in self._urls]) def __repr__(self): - """String representation of a WaveWatch3 instance.""" + """Return a string representation of a WaveWatch3 instance.""" return f"WaveWatch3({self.date!r}, grid={self.grid!r}, source={self.source!r})" def __eq__(self, other): @@ -183,6 +190,8 @@ def fetch(date, folder=".", force=False, grid="glo_30m", source="multigrid"): already exists in the destination folder. grid : str, optional The WAVEWATCH III grid to download. + source : str, optional + The name of the WAVEWATCH III data source. Returns ------- @@ -204,7 +213,7 @@ def fetch(date, folder=".", force=False, grid="glo_30m", source="multigrid"): for quantity in Source.QUANTITIES ] - with as_cwd(folder): + with _as_cwd(folder): Pool().map( partial(WaveWatch3Downloader.retreive, force=force), [str(url) for url in urls], @@ -214,11 +223,13 @@ def fetch(date, folder=".", force=False, grid="glo_30m", source="multigrid"): @contextlib.contextmanager -def as_cwd(path): +def _as_cwd(path): """Change directory context. - Args: - path (str): Path-like object to a directory. + Parameters + ---------- + path : str or Path-like + Directory to make the current working directory. """ prev_cwd = pathlib.Path.cwd() os.chdir(path)