From a875a9705697d334b2c4e14429670c0bb8d0b684 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:22:42 -0500 Subject: [PATCH 01/14] update, synchronize, and pin versions --- .pre-commit-config.yaml | 10 +++---- environment-docs.yml | 12 ++++---- environment.yml | 64 +++++++++++++++++++++++++---------------- requirements.txt | 21 +++++++------- requirements_dev.txt | 34 +++++++++++----------- requirements_extra.txt | 18 ++++++------ 6 files changed, 87 insertions(+), 72 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28e4396..b65a1d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,17 +15,17 @@ repos: - id: check-yaml - id: debug-statements - repo: https://github.com/psf/black - rev: 24.10.0 + rev: 25.1.0 hooks: - id: black args: [ '--target-version=py39' ] - repo: https://github.com/PyCQA/isort - rev: 5.13.2 + rev: 6.0.1 hooks: - id: isort args: [ '--profile=black' ] - repo: https://github.com/pycqa/flake8 - rev: 7.1.1 + rev: 7.1.2 hooks: - id: flake8 args: [ '--config=setup.cfg' ] @@ -42,10 +42,10 @@ repos: additional_dependencies: [ 'pyupgrade==v3.19.1' ] - id: nbqa-black args: [ '--target-version=py39' ] - additional_dependencies: [ 'black==24.10.0' ] + additional_dependencies: [ 'black==25.1.0' ] - id: nbqa-isort args: [ '--profile=black' ] - additional_dependencies: [ 'isort==5.13.2' ] + additional_dependencies: [ 'isort==6.0.1' ] - repo: https://github.com/pycqa/pydocstyle rev: 6.3.0 hooks: diff --git a/environment-docs.yml b/environment-docs.yml index a328507..1ab0697 100644 --- a/environment-docs.yml +++ b/environment-docs.yml @@ -2,10 +2,10 @@ name: birdy channels: - conda-forge - - defaults dependencies: - - ipython - - nbsphinx - - sphinx >=7.0.0 - - sphinx-codeautolink >=0.15.0 - - sphinx-copybutton >=0.5.2 + - python >=3.9,<3.13 + - ipython >8.5.0,!=9.0.0 + - nbsphinx >=0.9.5 + - sphinx >=7.0.0,<8.2.0 + - sphinx-codeautolink + - sphinx-copybutton diff --git a/environment.yml b/environment.yml index 48fdf71..a069ab8 100644 --- a/environment.yml +++ b/environment.yml @@ -2,30 +2,46 @@ name: birdy channels: - conda-forge - - defaults dependencies: - - pip - #- openssl - - boltons - - click - - funcsigs - - jinja2 - - lxml - - owslib>=0.19 - - packaging - - pyopenssl - - wrapt - # optional - - netcdf4 - - xarray + - python >=3.9,<3.13 + - pip >=23.3 + - boltons >=23.0 + - click >=8.1.0 + - jinja2 >=3.0.0 + - lxml >=5.0.0 + - owslib >=0.19.2 + - packaging >=23.0 + - pyopenssl >=23.0 + - python-dateutil >=2.8.1 + - requests >=2.24.0 + - wrapt >=1.14.0 + # extra + - fiona >=1.9.0 + - geojson >=3.0.0 + - ipyleaflet >=0.18.0 + - ipython >8.5.0,!=9.0.0 + - ipywidgets >=8.0.5 + - netcdf4 >=1.6.0 + - pymetalink >=6.5.3 + - rioxarray >=0.15.0 + - xarray >=2023.1.0 + # dev + - black >=24.2.0 + - bump-my-version >=1.0.0 + - pre-commit >=3.6.2 + - python-build >=1.0 + - twine >=4.0.0 + - watchdog >=4.0.0 # tests - - flake8>=3.8.4 - - nbval - - pytest - - pytest-notebook + - coverage >=7.5.0 + - flake8 >=7.0.0 + - nbval >=0.9.5 + - pytest >=8.0.0 + - pytest-notebook >=0.6.0 # sphinx for rtd - - ipykernel - - nbconvert - - nbsphinx - - pandoc - - sphinx + - ipykernel >=6.25.0 + - nbconvert >=7.3.0 + - nbsphinx >=0.9.5 + - sphinx >=7.0.0,<8.2.0 + - sphinx-codeautolink + - sphinx-copybutton diff --git a/requirements.txt b/requirements.txt index 984cab1..5e1dd69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,10 @@ -boltons -click -funcsigs -jinja2 -lxml -owslib>=0.19.2 -packaging -python-dateutil>=2.8.1 -requests>=2.0 -wrapt -pyOpenSSL +boltons >=23.0 +click >=8.1.0 +jinja2 >=3.0.0 +lxml >=5.0.0 +owslib >=0.19.2 +packaging >=23.0 +pyOpenSSL >=23.0 +python-dateutil >=2.8.1 +requests >=2.24.0 +wrapt >=1.14.0 diff --git a/requirements_dev.txt b/requirements_dev.txt index 8718c74..a55641f 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,18 +1,18 @@ -black>=24.2.0 -build>=1.0.0 -bumpversion>=0.5.3 -coverage>=4.5.1 -flake8>=7.0 -ipykernel -nbconvert -nbsphinx >=0.7.0 +black >=24.2.0 +build >=1.0 +bump-my-version >=1.0.0 +coverage >=7.5.0 +flake8 >=7.0 +ipykernel >=6.25.0 +nbconvert >=7.3.0 +nbsphinx >=0.9.5 nbval >=0.9.6 -pip >=23.3.0 -pre-commit>=3.6.2 -pytest-runner>=4.2 -pytest>=3.8.2 -sphinx-codeautolink>=0.15.0 -sphinx-copybutton>=0.5.2 -sphinx>=7.0.0 -twine>=4.0.0 -watchdog>=3.0.0 +pip >=23.3 +pre-commit >=3.6.2 +pytest >=8.0.0 +pytest-notebook >=0.6.0 +sphinx-codeautolink +sphinx-copybutton +sphinx >=7.0.0,<8.2.0 +twine >=4.0.0 +watchdog >=4.0.0 diff --git a/requirements_extra.txt b/requirements_extra.txt index 4033b8e..2aa8984 100644 --- a/requirements_extra.txt +++ b/requirements_extra.txt @@ -1,9 +1,9 @@ -fiona -geojson -ipyleaflet -ipython -ipywidgets -netCDF4 -pymetalink -xarray -rioxarray +fiona >=1.9.0 +geojson >=3.0.0 +ipyleaflet >=0.18.0 +ipython >8.5.0,!=9.0.0 +ipywidgets >=8.0.5 +netCDF4 >=1.6.0 +pymetalink >=6.5.3 +xarray >=2023.1.0 +rioxarray >=0.15.0 From 5e4e16988719e96bc11d618b47ff23047a897d52 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:23:55 -0500 Subject: [PATCH 02/14] support Python3.12 and 3.13 --- .github/workflows/main.yml | 16 +++++++++++++++- setup.py | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bdb8f5c..70a6912 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,13 @@ jobs: python-version: - "3.9" steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -38,7 +44,13 @@ jobs: python-version: - "3.9" steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install pandoc run: | sudo apt-get -y install pandoc @@ -70,13 +82,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] steps: - name: Harden Runner uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: diff --git a/setup.py b/setup.py index 6bb0fe8..df6d12a 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,8 @@ def parse_reqs(file): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Atmospheric Science", ] From 9914ae5218f83e7597b66041be6dc1208889331e Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:24:42 -0500 Subject: [PATCH 03/14] add missing authors --- .zenodo.json | 6 +++++- AUTHORS.rst | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index 3b2fa40..1c7dc5a 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -19,8 +19,12 @@ "affiliation": "CRIM" }, { - "name": "David Caron" + "name": "David Caron", "affiliation": "Jakarto" + }, + { + "name": "Nathan Collier", + "affiliation": "Oak Ridge National Laboratory" } ], diff --git a/AUTHORS.rst b/AUTHORS.rst index faa80b7..07a8ce1 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,11 +1,13 @@ Authors ******* -* David Huard -* Carsten Ehbrecht +* David Huard `@huard `_ +* Carsten Ehbrecht `@cehbrecht `_ Contributors ************ -* Trevor James Smith `@Zeitsperre `_ +* David Caron `@davidcaron `_ +* Francis Pelletier `@f-PLT <@f-PLT>`_ * Nathan Collier `@nocollier `_ +* Trevor James Smith `@Zeitsperre `_ From eb4af5736ae0539bb0a4542907c1f9767867f6e9 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:24:59 -0500 Subject: [PATCH 04/14] update ReadTheDocs config --- .readthedocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index c2f27db..6ca12f7 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,9 +9,9 @@ sphinx: configuration: docs/source/conf.py build: - os: ubuntu-22.04 + os: "ubuntu-24.04" tools: - python: "mambaforge-22.9" + python: "mambaforge-23.11" jobs: pre_build: - sphinx-apidoc -o docs/source/apidoc/ --private --module-first --separate birdy From 75e94e76b31cd4cf953c6d240aa8fb3c399c63ac Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:25:23 -0500 Subject: [PATCH 05/14] grouped dependabot updates --- .github/dependabot.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0e1bf72..b49b3d5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,10 +9,19 @@ updates: directory: / schedule: interval: monthly - open-pull-requests-limit: 5 + groups: + actions: + patterns: + - "*" - package-ecosystem: pip directory: / schedule: interval: monthly - open-pull-requests-limit: 5 + groups: + ci: + patterns: + - "CI/*" + python: + patterns: + - "requirements*.txt" From 8cf5fe20820896bb572bae7340d3e8d8be3f8731 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:25:41 -0500 Subject: [PATCH 06/14] use bump-my-version --- .bumpversion.toml | 24 ++++++++++++++++++++++++ MANIFEST.in | 4 ++++ setup.cfg | 17 ----------------- 3 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 .bumpversion.toml diff --git a/.bumpversion.toml b/.bumpversion.toml new file mode 100644 index 0000000..db3fce0 --- /dev/null +++ b/.bumpversion.toml @@ -0,0 +1,24 @@ +[tool.bumpversion] +current_version = "0.8.7" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +tag = false +allow_dirty = false +commit = true + +[[tool.bumpversion.files]] +filename = "birdy/__init__.py" +search = "__version__ = \"{current_version}\"" +replace = "__version__ = \"{new_version}\"" + +[[tool.bumpversion.files]] +filename = "docs/source/conf.py" +search = "release = \"{current_version}\"" +replace = "release = \"{new_version}\"" + +[[tool.bumpversion.files]] +filename = "README.rst" +search = "{current_version}" +replace = "{new_version}" diff --git a/MANIFEST.in b/MANIFEST.in index 2670216..5c1a3a3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,6 +11,10 @@ include docs Makefile make.bat recursive-include docs/source Makefile make.bat *.rst *.py *.ipynb *.svg *.ico *.txt *.yml *.cfg *.md recursive-include tests *.py *.nc *.tif *.geojson *.xml +exclude *.toml +exclude *.yaml +exclude *.yml + recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude * *.ipynb_checkpoints diff --git a/setup.cfg b/setup.cfg index 5f72e76..17b2015 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,23 +1,6 @@ -[bumpversion] -current_version = 0.8.7 -commit = True -tag = False - [metadata] description-file = README.rst -[bumpversion:file:birdy/__init__.py] -search = __version__ = "{current_version}" -replace = __version__ = "{new_version}" - -[bumpversion:file:docs/source/conf.py] -search = release = "{current_version}" -replace = release = "{new_version}" - -[bumpversion:file:README.rst] -search = {current_version} -replace = {new_version} - [tool:pytest] python_files = test_*.py testpaths = tests From 3f021dc26631dcae5b9fcf8ecbce19985d2158d8 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:52:55 -0500 Subject: [PATCH 07/14] add missing requirements --- environment.yml | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/environment.yml b/environment.yml index a069ab8..bce9608 100644 --- a/environment.yml +++ b/environment.yml @@ -14,6 +14,7 @@ dependencies: - pyopenssl >=23.0 - python-dateutil >=2.8.1 - requests >=2.24.0 + - urllib3 >=2.0.2 - wrapt >=1.14.0 # extra - fiona >=1.9.0 diff --git a/requirements.txt b/requirements.txt index 5e1dd69..f160293 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ packaging >=23.0 pyOpenSSL >=23.0 python-dateutil >=2.8.1 requests >=2.24.0 +urllib3 >=2.0.2 wrapt >=1.14.0 From 67ca854344f85a7074155224a27e96d2369958ce Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:09:25 -0500 Subject: [PATCH 08/14] wip - add numpydoc validation --- .pre-commit-config.yaml | 4 ++++ setup.cfg | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b65a1d2..8246502 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,6 +51,10 @@ repos: hooks: - id: pydocstyle args: [ '--convention=numpy' ] + - repo: https://github.com/numpy/numpydoc + rev: v1.8.0 + hooks: + - id: numpydoc-validation - repo: meta hooks: - id: check-hooks-apply diff --git a/setup.cfg b/setup.cfg index 17b2015..9a3340c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,3 +26,8 @@ exclude = [doc8] ignore-path = docs/build,docs/source/_templates,docs/source/_static max-line-length = 120 + +[tool:numpydoc_validation] +checks = all,EX01,SA01,ES01,RT01,GL06,GL07,GL08 +exclude = \.undocumented_method$,\.__repr__$ +override_SS05 = ^Process ,^Assess ,^Access , From 30ccf9353e1359c0393c18c44b291ac8f3630edc Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:09:47 -0500 Subject: [PATCH 09/14] wip - wip docstring fixup --- birdy/cli/base.py | 16 ++-- birdy/cli/misc.py | 4 +- birdy/cli/run.py | 2 +- birdy/client/base.py | 83 +++++++++++---------- birdy/client/converters.py | 41 +++++----- birdy/client/notebook.py | 16 ++-- birdy/client/outputs.py | 37 +++++---- birdy/client/utils.py | 89 ++++++++++++++-------- birdy/dependencies.py | 4 +- birdy/ipyleafletwfs/__init__.py | 5 +- birdy/ipyleafletwfs/base.py | 128 +++++++++++++++----------------- birdy/utils.py | 36 +++++---- tests/test_converters.py | 3 +- 13 files changed, 251 insertions(+), 213 deletions(-) diff --git a/birdy/cli/base.py b/birdy/cli/base.py index 022a5a3..b26fb98 100644 --- a/birdy/cli/base.py +++ b/birdy/cli/base.py @@ -25,12 +25,16 @@ class BirdyCLI(click.MultiCommand): Parameters ---------- - url: str - URL of the Web Processing Service. - caps_xml: str - A WPS GetCapabilities response for testing. - desc_xml: str - A WPS DescribeProcess response with "identifier=all" for testing. + name : str + Name of the process. + url : str + URL of the Web Processing Service. + caps_xml : str + A WPS GetCapabilities response for testing. + desc_xml : str + A WPS DescribeProcess response with "identifier=all" for testing. + attrs : dict + Additional attributes. """ def __init__(self, name=None, url=None, caps_xml=None, desc_xml=None, **attrs): diff --git a/birdy/cli/misc.py b/birdy/cli/misc.py index edc48e4..7449066 100644 --- a/birdy/cli/misc.py +++ b/birdy/cli/misc.py @@ -28,8 +28,8 @@ def get_ssl_verify(): # noqa: D103 urllib3.disable_warnings() click.echo( - "Warning: Unverified HTTPS request is being made." - " Adding certificate verification is strongly advised.\n" + "Warning: Unverified HTTPS request is being made. " + "Adding certificate verification is strongly advised.\n" ) verify = False else: diff --git a/birdy/cli/run.py b/birdy/cli/run.py index a9a0588..b990c7b 100644 --- a/birdy/cli/run.py +++ b/birdy/cli/run.py @@ -72,7 +72,7 @@ def cli(ctx, cert, send, sync, token): Birdy is a command line client for Web Processing Services. Documentation is available on readthedocs: - http://birdy.readthedocs.org/en/latest/ + https://birdy.readthedocs.org/en/latest/ """ ctx.obj = ctx.obj or dict() ctx.obj["verify"] = get_ssl_verify() diff --git a/birdy/client/base.py b/birdy/client/base.py index 410b7d5..21e0dcd 100644 --- a/birdy/client/base.py +++ b/birdy/client/base.py @@ -4,6 +4,7 @@ import types from collections import OrderedDict from textwrap import dedent +from typing import Callable from warnings import warn import owslib @@ -17,7 +18,9 @@ SYNC, WPS_DEFAULT_VERSION, ComplexData, + Input, WebProcessingService, + WPSExecution, ) from birdy.client import notebook, utils @@ -60,35 +63,35 @@ def __init__( Parameters ---------- - url: str - Link to WPS provider. config (Config): an instance - processes: - Specify a subset of processes to bind. Defaults to all processes. - converters: dict - Correspondence of {mimetype: class} to convert this mimetype to a python object. - username: str - passed to :class:`owslib.wps.WebProcessingService` - password: str - passed to :class:`owslib.wps.WebProcessingService` - headers: str - passed to :class:`owslib.wps.WebProcessingService` - auth: requests.auth.AuthBase - requests-style auth class to authenticate. - see https://2.python-requests.org/en/master/user/authentication/ - verify: bool - passed to :class:`owslib.wps.WebProcessingService` - cert: str - passed to :class:`owslib.wps.WebProcessingService` + url : str + Link to WPS provider. config (Config): an instance. + processes : any, optional + Specify a subset of processes to bind. Defaults to all processes. + converters : dict, optional + Correspondence of {mimetype: class} to convert this mimetype to a python object. + username : str, optional + Passed to :class:`owslib.wps.WebProcessingService`. + password : str, optional + Passed to :class:`owslib.wps.WebProcessingService`. + headers : str, optional + Passed to :class:`owslib.wps.WebProcessingService`. + auth : requests.auth.AuthBase + Requests-style auth class to authenticate. + See https://2.python-requests.org/en/master/user/authentication/. + verify : bool + Passed to :class:`owslib.wps.WebProcessingService`. + cert : str + Passed to :class:`owslib.wps.WebProcessingService`. verbose: bool - Deprecated. passed to :class:`owslib.wps.WebProcessingService` for owslib < 0.29 - progress: bool - If True, enable interactive user mode. - version: str - WPS version to use. - language: str - passed to :class:`owslib.wps.WebProcessingService` (ex: 'fr-CA', 'en_US'). - lineage: bool - If True, the Execute operation includes lineage information. + Deprecated. passed to :class:`owslib.wps.WebProcessingService` for owslib < 0.29. + progress : bool + If True, enable interactive user mode. + version : str + WPS version to use. + language : str + Passed to :class:`owslib.wps.WebProcessingService` (ex: 'fr-CA', 'en_US'). + lineage : bool + If True, the Execute operation includes lineage information. """ self._converters = converters self._interactive = progress @@ -227,20 +230,20 @@ def _setup_logging(self): fh.setFormatter(logging.Formatter("%(asctime)s: %(message)s")) self.logger.addHandler(fh) - def _method_factory(self, pid): + def _method_factory(self, pid: str) -> Callable: """Create a custom function signature with docstring, instantiate it and pass it to a wrapper. The wrapper will call the process on reception. Parameters ---------- - pid: str - Identifier of the WPS process. + pid : str + Identifier of the WPS process. Returns ------- func - A Python function calling the remote process, complete with docstring and signature. + A Python function calling the remote process, complete with docstring and signature. """ process = self._processes[pid] @@ -404,15 +407,15 @@ def _execute(self, pid, **kwargs): wps_response.attach(wps_outputs=self._outputs[pid], converters=self._converters) return wps_response - def _console_monitor(self, execution, sleep=3): + def _console_monitor(self, execution: WPSExecution, sleep: int = 3): """Monitor the execution of a process. Parameters ---------- - execution : WPSExecution instance - The execute response to monitor. - sleep: float - Number of seconds to wait before each status check. + execution : WPSExecution + The execute response to monitor. + sleep : int + Number of seconds to wait before each status check. """ import signal @@ -438,7 +441,7 @@ def sigint_handler(signum, frame): self.logger.info(f"{execution.process.identifier} failed.") -def sort_inputs_key(i): +def sort_inputs_key(i: Input): """Key function for sorting process inputs. The order is: @@ -448,8 +451,8 @@ def sort_inputs_key(i): Parameters ---------- - i: owslib.wps.Input - An owslib Input + i : owslib.wps.Input + An owslib Input Notes ----- diff --git a/birdy/client/converters.py b/birdy/client/converters.py index 9d4a9c7..5391b0a 100644 --- a/birdy/client/converters.py +++ b/birdy/client/converters.py @@ -4,7 +4,7 @@ from collections.abc import Sequence from importlib import import_module from pathlib import Path -from typing import Union +from typing import Any, Optional, Union from owslib.wps import Output from packaging.version import Version @@ -20,13 +20,18 @@ class BaseConverter: # noqa: D101 priority = None nested = False - def __init__(self, output=None, path=None, verify=True): + def __init__( + self, + output: Output = None, + path: Optional[Union[str, Path]] = None, + verify: bool = True, + ) -> None: """Instantiate the conversion class. Parameters ---------- - output: owslib.wps.Output - Output object to be converted. + output : owslib.wps.Output + Output object to be converted. """ self.path = path or tempfile.mkdtemp() self.output = output @@ -39,7 +44,7 @@ def __init__(self, output=None, path=None, verify=True): self.url = output self._file = Path(output) else: - raise NotImplementedError + raise NotImplementedError() @property def file(self): @@ -60,15 +65,15 @@ def data(self): def check_dependencies(self): # noqa: D102 pass - def _check_import(self, name, package=None): + def _check_import(self, name: str, package: Optional[str] = None): """Check if libraries can be imported. Parameters ---------- - name: str - module name to try to import - package: str - package of the module + name : str + module name to try to import. + package : str + package of the module. """ try: import_module(name, package) @@ -78,7 +83,7 @@ def _check_import(self, name, package=None): def convert(self): """To be subclassed.""" - raise NotImplementedError + raise NotImplementedError() class GenericConverter(BaseConverter): # noqa: D101 @@ -307,7 +312,7 @@ def _find_converter(mimetype=None, extension=None, converters=()): return select -def find_converter(obj, converters): +def find_converter(obj: Any, converters): """Find converters for a WPS output or a file on disk.""" if isinstance(obj, Output): mimetype = obj.mimeType @@ -332,18 +337,18 @@ def convert( Parameters ---------- output : owslib.wps.Output, Path, str - Item to convert to an object. + Item to convert to an object. path : str, Path - Path on disk where temporary files are stored. + Path on disk where temporary files are stored. converters : sequence of BaseConverter subclasses - Converter classes to search within for a match. + Converter classes to search within for a match. verify : bool - + Whether to perform verification. Default: True. Returns ------- - objs - Python object or file's content as bytes. + Any + Python object or file's content as bytes. """ # Get all converters if converters is None: diff --git a/birdy/client/notebook.py b/birdy/client/notebook.py index ee5ff86..7346003 100644 --- a/birdy/client/notebook.py +++ b/birdy/client/notebook.py @@ -10,11 +10,9 @@ from . import utils -# from traitlets import Dict, Unicode - def is_notebook(): - """Return whether or not this function is executed in a notebook environment.""" + """Return whether this function is executed in a notebook environment.""" if not IPython: return False @@ -166,15 +164,15 @@ def build_ui(self, input_widgets, of_widgets, go): return ui -def monitor(execution, sleep=3): +def monitor(execution: WPSExecution, sleep: int = 3): """Monitor the execution of a process using a notebook progress bar widget. Parameters ---------- execution : WPSExecution instance - The execute response to monitor. - sleep: float - Number of seconds to wait before each status check. + The execute response to monitor. + sleep : int + Number of seconds to wait before each status check. """ progress = widgets.IntProgress( value=0, @@ -225,10 +223,10 @@ def check(execution, progress, cancel): thread.start() -def input2widget(inpt): +def input2widget(inpt: Input): """Return a Notebook widget to enter values for the input.""" if not isinstance(inpt, Input): - raise ValueError + raise ValueError() typ = inpt.dataType opt = inpt.allowedValues diff --git a/birdy/client/outputs.py b/birdy/client/outputs.py index 8eea5a7..8f9599e 100644 --- a/birdy/client/outputs.py +++ b/birdy/client/outputs.py @@ -2,8 +2,9 @@ import tempfile from collections import namedtuple +from typing import Optional -from owslib.wps import WPSExecution +from owslib.wps import Output, WPSExecution from birdy.client import utils from birdy.client.converters import convert @@ -12,26 +13,27 @@ class WPSResult(WPSExecution): # noqa: D101 - def attach(self, wps_outputs, converters=None): + def attach(self, wps_outputs: Output, converters: Optional[dict] = None): """Attach the outputs according to converters. Parameters ---------- - wps_outputs: dict - converters: dict - Converter dictionary {name: object} + wps_outputs : owslib.wps.Output + The WPS outputs. + converters : dict, optional + Converter dictionary (`{name: object}`) """ self._wps_outputs = wps_outputs self._converters = converters self._path = tempfile.mkdtemp() - def get(self, asobj=False): + def get(self, asobj: bool = False): """Return the process response outputs. Parameters ---------- - asobj: bool - If True, object_converters will be used. + asobj : bool + If True, object_converters will be used. """ if not self.isComplete(): raise ProcessIsNotComplete("Please wait ...") @@ -41,23 +43,26 @@ def get(self, asobj=False): return self._make_output(asobj) def _make_output(self, convert_objects=False): - Output = namedtuple( + output = namedtuple( sanitize(self.process.identifier) + "Response", [sanitize(o.identifier) for o in self.processOutputs], ) - Output.__repr__ = utils.pretty_repr - return Output( + output.__repr__ = utils.pretty_repr + return output( *[self._process_output(o, convert_objects) for o in self.processOutputs] ) - def _process_output(self, output, convert_objects=False): - """Process the output response, whether it is actual data or a URL to a file. + def _process_output(self, output: Output, convert_objects: bool = False): + """Process the output response. + + Determine whether it is actual data or a URL to a file. Parameters ---------- - output: owslib.wps.Output - convert_objects: bool - If True, object_converters will be used. + output : owslib.wps.Output + The WPS outputs. + convert_objects : bool + If True, object_converters will be used. """ # Get the data for recognized types. if output.data: diff --git a/birdy/client/utils.py b/birdy/client/utils.py index d0fa6c6..d2afd3f 100644 --- a/birdy/client/utils.py +++ b/birdy/client/utils.py @@ -2,24 +2,25 @@ import datetime as dt from pathlib import Path +from typing import Any, Optional, Union from urllib.parse import urlparse import dateutil.parser -from owslib.wps import ComplexDataInput +from owslib.wps import ComplexDataInput, Process, WebProcessingService from ..utils import is_file, sanitize -def filter_case_insensitive(names, complete_list): +def filter_case_insensitive( + names: Union[str, list[str]], complete_list: list[str] +) -> tuple[list[str], list[str]]: """Filter a sequence of process names into a `known` and `unknown` list.""" contained = [] missing = [] - complete_list_lower = set(map(str.lower, complete_list)) + complete_list_lower = {name.lower() for name in complete_list} if isinstance(names, str): - names = [ - names, - ] + names = [names] for name in names: if name.lower() in complete_list_lower: @@ -30,14 +31,15 @@ def filter_case_insensitive(names, complete_list): return contained, missing -def pretty_repr(obj, linebreaks=True): +def pretty_repr(obj: Any, linebreaks: bool = True): """Output pretty repr for an Output. Parameters ---------- - obj : any type + obj : Any + An object. linebreaks : bool - If True, split attributes with linebreaks + If True, split attributes with linebreaks. """ class_name = obj.__class__.__name__ @@ -68,7 +70,9 @@ def pretty_repr(obj, linebreaks=True): return joiner.join([class_name + "(", attributes, ")"]) -def build_wps_client_doc(wps, processes): +def build_wps_client_doc( + wps: WebProcessingService, processes: dict[str, Process] +) -> str: """Create WPSClient docstring. Parameters @@ -99,7 +103,7 @@ def build_wps_client_doc(wps, processes): return "\n".join(doc) -def build_process_doc(process): +def build_process_doc(process: Process) -> str: """Create docstring from process metadata.""" doc = [process.abstract or "", ""] @@ -126,7 +130,7 @@ def build_process_doc(process): return "\n".join(doc) -def format_type(obj): +def format_type(obj: Any) -> str: """Create docstring entry for input parameter from an OWSlib object.""" nmax = 10 @@ -169,15 +173,18 @@ def format_type(obj): return doc -def is_embedded_in_request(url, value): - """Whether or not to encode the value as raw data content. +def is_embedded_in_request(url: str, value: Any) -> bool: + """Whether to encode the value as raw data content. - Returns True if - - value is a file:/// URI or a local path - - value is a File-like instance - - url is not localhost - - value is a File object - - value is already the string content + Returns + ------- + bool + True if: + - value is a file:/// URI or a local path + - value is a File-like instance + - url is not localhost + - value is a File object + - value is already the string content """ if hasattr(value, "read"): # File-like return True @@ -206,7 +213,13 @@ def is_embedded_in_request(url, value): return False -def to_owslib(value, data_type, encoding=None, mimetype=None, schema=None): +def to_owslib( + value: Any, + data_type: str, + encoding: Optional[Any] = None, + mimetype: Optional[Any] = None, + schema: Optional[Any] = None, +) -> Any: """Convert value into OWSlib objects.""" # owslib only accepts literaldata, complexdata and boundingboxdata @@ -221,7 +234,7 @@ def to_owslib(value, data_type, encoding=None, mimetype=None, schema=None): return str(value) -def from_owslib(value, data_type): +def from_owslib(value: Any, data_type: str) -> Any: """Convert a string into another data type.""" if value is None: return None @@ -250,7 +263,7 @@ def from_owslib(value, data_type): return value -def py_type(data_type): +def py_type(data_type: str) -> Any: """Return the python data type matching the WPS dataType.""" if data_type is None: return None @@ -283,20 +296,25 @@ def extend_instance(obj, cls): obj.__class__ = type(base_cls_name, (cls, base_cls), {}) -def add_output_format(output_dictionary, output_identifier, as_ref=None, mimetype=None): +def add_output_format( + output_dictionary: dict, + output_identifier: str, + as_ref: Optional[bool] = None, + mimetype: Optional[str] = None, +) -> None: """Add an output format to an already existing dictionary. Parameters ---------- - output_dictionary: dict + output_dictionary : dict The dictionary (created with create_output_dictionary()) to which this output format will be added. - output_identifier: str + output_identifier : str Identifier of the output. - as_ref: True, False or None + as_ref : bool, optional Determines if this output will be returned as a reference or not. None for process default. - mimetype: str or None + mimetype : str or None If the process supports multiple MIME types, it can be specified with this argument. None for process default. """ @@ -306,23 +324,28 @@ def add_output_format(output_dictionary, output_identifier, as_ref=None, mimetyp } -def create_output_dictionary(output_identifier, as_ref=None, mimetype=None): +def create_output_dictionary( + output_identifier: str, + as_ref: Optional[bool] = None, + mimetype: Optional[str] = None, +) -> dict: """Create an output format dictionary. Parameters ---------- - output_identifier: str + output_identifier : str Identifier of the output. - as_ref: True, False or None + as_ref : bool, optional Determines if this output will be returned as a reference or not. None for process default. - mimetype: str or None + mimetype : str, optional If the process supports multiple MIME types, it can be specified with this argument. None for process default. Returns ------- - output_dictionary: dict + dict + Output dictionary. """ output_dictionary = { output_identifier: { diff --git a/birdy/dependencies.py b/birdy/dependencies.py index 0dd6d00..54a6277 100644 --- a/birdy/dependencies.py +++ b/birdy/dependencies.py @@ -5,7 +5,9 @@ Module for managing optional dependencies. -Example usage:: +Example usage: + +.. code-block:: python >>> from birdy.dependencies import ipywidgets as widgets """ diff --git a/birdy/ipyleafletwfs/__init__.py b/birdy/ipyleafletwfs/__init__.py index 66ffc2d..cb30ebf 100644 --- a/birdy/ipyleafletwfs/__init__.py +++ b/birdy/ipyleafletwfs/__init__.py @@ -10,7 +10,9 @@ ------------ Ipyleaflet and Ipywidgets dependencies are included in the requirements_extra.txt, at the root of this repository. -To install:: +To install: + +.. code-block:: console $ pip install -r requirements_extra.txt @@ -24,6 +26,7 @@ Using the on-map 'Refresh WFS layer' button will make a new request for the current extent. .. warning:: + WFS requests and GeoJSON layers are costly operations to process and render. Trying to load lake layers at the nationwide extent may take a long time and probably crash. The more dense and complex the layer, the more zoomed-in the map extent should be. diff --git a/birdy/ipyleafletwfs/base.py b/birdy/ipyleafletwfs/base.py index b3af8ea..6caf9ad 100644 --- a/birdy/ipyleafletwfs/base.py +++ b/birdy/ipyleafletwfs/base.py @@ -1,6 +1,7 @@ # noqa: D100 import json +from typing import Any, Optional from owslib.wfs import WebFeatureService @@ -23,8 +24,8 @@ def _map_extent_to_bbox_filter(source_map): Parameters ---------- - source_map: Map instance - The map instance from which the extent will calculated + source_map : Map instance + The map instance from which the extent will be calculated. Returns ------- @@ -60,15 +61,15 @@ class IpyleafletWFS: Parameters ---------- - url: str - The url of the WFS service - wfs_version: str - The version of the WFS service to use. Defaults to 2.0.0. + url : str + The url of the WFS service. + wfs_version : str + The version of the WFS service to use. Defaults to 2.0.0. Returns ------- IpyleafletWFS - Instance from which the WFS layers can be created. + Instance from which the WFS layers can be created. """ def __init__(self, url, wfs_version="2.0.0"): @@ -99,7 +100,7 @@ def __init__(self, url, wfs_version="2.0.0"): # # # # # # # # # # # # # # # def build_layer( - self, layer_typename, source_map, layer_style=None, feature_property=None + self, layer_typename: str, source_map, layer_style=None, feature_property=None ): """Return an ipyleaflet GeoJSON layer from a geojson wfs request. @@ -109,24 +110,19 @@ def build_layer( Parameters ---------- - layer_typename: string - Typename of the layer to display. Listed as Layer_ID by get_layer_list(). - Must include namespace and layer name, separated by a colon. - - ex: public:canada_forest_layer - - source_map: Map instance + layer_typename : str + Typename of the layer to display. Listed as Layer_ID by `get_layer_list()`. + Must include namespace and layer name, separated by a colon. + e.g. public:canada_forest_layer + source_map : Map instance The map instance on which the layer is to be added. - - layer_style: dictionnary - ipyleaflet GeoJSON style format, for example + layer_style : dict + ipyleaflet GeoJSON style format, for example: `{ 'color': 'white', 'opacity': 1, 'dashArray': '9', 'fillOpacity': 0.1, 'weight': 1 }`. See ipyleaflet documentation for more information. - - feature_property: string - The property key to be used by the widget. Use the property_list() function + feature_property : str + The property key to be used by the widget. Use the `property_list()` function to get a list of the available properties. - """ # Check if dependency is installed if ipyl is None: @@ -178,7 +174,9 @@ def build_layer( # Create refresh button self._create_refresh_widget() - def create_wfsgeojson_layer(self, layer_typename, source_map, layer_style=None): + def create_wfsgeojson_layer( + self, layer_typename: str, source_map, layer_style: Optional[dict] = None + ): """Create a static ipyleaflett GeoJSON layer from a WFS service. Simple wrapper for a WFS => GeoJSON layer, using owslib. @@ -191,19 +189,16 @@ def create_wfsgeojson_layer(self, layer_typename, source_map, layer_style=None): Parameters ---------- - layer_typename: string - Typename of the layer to display. Listed as Layer_ID by get_layer_list(). + layer_typename: str + Typename of the layer to display. Listed as Layer_ID by `get_layer_list()`. Must include namespace and layer name, separated by a colon. - - ex: public:canada_forest_layer - - source_map: Map instance - The map instance from which the extent will be used to filter the request. - - layer_style: dictionnary - ipyleaflet GeoJSON style format, for example - `{ 'color': 'white', 'opacity': 1, 'dashArray': '9', 'fillOpacity': 0.1, 'weight': 1 }`. - See ipyleaflet documentation for more information. + e.g. `public:canada_forest_layer` + source_map : Map instance + The map instance from which the extent will be used to filter the request. + layer_style : dict, optional + ipyleaflet GeoJSON style format, for example: + `{ 'color': 'white', 'opacity': 1, 'dashArray': '9', 'fillOpacity': 0.1, 'weight': 1 }`. + See ipyleaflet documentation for more information. Returns ------- @@ -232,15 +227,15 @@ def create_wfsgeojson_layer(self, layer_typename, source_map, layer_style=None): return layer - def _refresh_layer(self, placeholder=None): + def _refresh_layer(self, placeholder: Optional[str] = None): """Refresh the wfs layer for the current map extent. Also updates the existing widgets. Parameters ---------- - placeholder: string - Parameter is only there so that button.on_click() will work properly. + placeholder : str, optional + Parameter is only there so that button.on_click() will work properly. """ if self._layer: self.build_layer( @@ -278,23 +273,22 @@ def remove_layer(self): # Layer information functions # # # # # # # # # # # # # # # # # - def feature_properties_by_id(self, feature_id): + def feature_properties_by_id(self, feature_id: int) -> Optional[Any]: """Return the properties of a feature. - The id field is usually the first field. Since the name is - always different, this is the only assumption that can be - made to automate this process. Hence, this will not work if - the layer in question does not follow this formatting. + The id field is usually the first field. + Since the name is always different, this is the only assumption that can be made to automate this process. + Hence, this will not work if the layer in question does not follow this formatting. Parameters ---------- - feature_id: int - The feature id. + feature_id : int + The feature id. Returns ------- - Dict - A dictionary of the layer's properties + dict + A dictionary of the layer's properties. """ for feature in self._geojson["features"]: # The id field is usually the first field. Since the name is @@ -312,18 +306,18 @@ def geojson(self): return self._geojson @property - def layer_list(self): + def layer_list(self) -> list: """Return a simple layer list available to the WFS service. Returns ------- - List - A List of the WFS layers available + list + A list of the WFS layers available. """ return sorted(self._wfs.contents.keys()) @property - def property_list(self): + def property_list(self) -> dict: """Return a list containing the properties of the first feature. Retrieves the available properties for use subsequent use @@ -331,8 +325,8 @@ def property_list(self): Returns ------- - Dict - A dictionary of the layer properties. + dict + A dictionary of the layer properties. """ return self._geojson["features"][0]["properties"] @@ -377,11 +371,6 @@ def clear_property_widgets(self): This function will remove the property widgets from a given map, without affecting other widgets. - - Parameters - ---------- - src_map: Map instance - The map instance from which the widgets are to be removed. """ if self._property_widgets: for widget in self._property_widgets: @@ -391,7 +380,10 @@ def clear_property_widgets(self): self._property_widgets = None def create_feature_property_widget( - self, widget_name, feature_property=None, widget_position="bottomright" + self, + widget_name: str, + feature_property: Optional[str] = None, + widget_position: str = "bottomright", ): """Create a visualization widget for a specific feature property. @@ -402,16 +394,14 @@ def create_feature_property_widget( Parameters ---------- - widget_name: string - Name of the widget. Must be unique or will overwrite existing widget. - - feature_property: string - The property key to be used by the widget. Use the property_list() function - to get a list of the available properties. If left empty, it will default to - the first property attribute in the list. - - widget_position: string - Position on the map for the widget. Choose between ‘bottomleft’, ‘bottomright’, ‘topleft’, or ‘topright’. + widget_name : str + Name of the widget. Must be unique or will overwrite existing widget. + feature_property : str, optional + The property key to be used by the widget. Use the property_list() function + to get a list of the available properties. If left empty, it will default to + the first property attribute in the list. + widget_position : {‘bottomleft’, ‘bottomright’, ‘topleft’, ‘topright’} + Position on the map for the widget. Notes ----- diff --git a/birdy/utils.py b/birdy/utils.py index 1369b95..ca8db35 100644 --- a/birdy/utils.py +++ b/birdy/utils.py @@ -5,6 +5,7 @@ import keyword import re from pathlib import Path +from typing import Any, Optional, Union from urllib.parse import urlparse # These mimetypes will be encoded in base64 when embedded in requests. @@ -27,12 +28,12 @@ DEFAULT_ENCODING = "utf-8" -def fix_url(url): +def fix_url(url: str) -> str: """If url is a local path, add a file:// scheme.""" return urlparse(url, scheme="file").geturl() -def is_url(url): +def is_url(url: Optional[str]) -> bool: """Return whether value is a valid URL.""" if url is None: return False @@ -43,7 +44,7 @@ def is_url(url): return True -def is_opendap_url(url): +def is_opendap_url(url: str): """Check if a provided url is an OpenDAP url. The DAP Standard specifies that a specific tag must be included in the @@ -70,22 +71,22 @@ def is_opendap_url(url): return False -def is_file(path): +def is_file(path: Optional[str]) -> bool: """Return True if `path` is a valid file.""" if not path: - ok = False + return False elif isinstance(path, Path): p = path else: p = Path(path[:255]) try: ok = p.is_file() - except Exception: + except (OSError, ValueError): ok = False return ok -def sanitize(name): +def sanitize(name: str): """Lower-case name and replace all non-ascii chars by `_`. If name is a Python keyword (like `return`) then add a trailing `_`. @@ -96,7 +97,7 @@ def sanitize(name): return new_name -def delist(data): +def delist(data: Any): """If data is a sequence with a single element, returns this element, otherwise return the sequence.""" if ( isinstance(data, collections.abc.Iterable) @@ -107,7 +108,7 @@ def delist(data): return data -def embed(value, mimetype=None, encoding=None): +def embed(value: Any, mimetype: Optional[str] = None, encoding: Optional[str] = None): """Return the content of the file, either as a string or base64 bytes. Returns @@ -156,21 +157,24 @@ def _encode(content, mimetype, encoding): # return u''.format(content) -def guess_type(url, supported): +def guess_type( + url: Union[str, Path], supported: Union[list[str], tuple[str]] +) -> tuple[str, str]: """Guess the mime type of the file link. If the mimetype is not recognized, default to the first supported value. Parameters ---------- - url : str, Path - Path or URL to file. - supported : list, tuple - Supported mimetypes. + url : str or Path + A path or URL to a file. + supported : list or tuple + Supported mimetypes. Returns ------- - mimetype, encoding + (str, str) + mimetype, encoding """ import mimetypes @@ -201,7 +205,7 @@ def guess_type(url, supported): mime = "application/geo+json" # FIXME: Verify whether this code is needed. Remove if not. - # # GeoTIFF (workaround since this mimetype isn't correctly understoud) + # # GeoTIFF (workaround since this mimetype isn't correctly understood) # if mime == "image/tiff" and (".tif" in url or ".tiff" in "url"): # mime = "image/tiff; subtype=geotiff" # diff --git a/tests/test_converters.py b/tests/test_converters.py index 4dcb3dc..c1a0c2d 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -5,7 +5,6 @@ import tempfile import pytest -import xarray as xr from common import resource_file from birdy.client import converters @@ -99,7 +98,9 @@ def test_jpeg_imageconverter(): # noqa: D103 def test_raster_tif(): + xr = pytest.importorskip("xarray") pytest.importorskip("rioxarray") + fn = resource_file("Olympus.tif") da = converters.convert(fn, path="/tmp") From 200e70a8be54e47b2a9d8ffd0ff35c702a7f2283 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:51:47 -0500 Subject: [PATCH 10/14] numpy docstyle compliant docstrings --- .pre-commit-config.yaml | 5 -- birdy/cli/__init__.py | 33 ++++--- birdy/cli/base.py | 5 +- birdy/cli/run.py | 17 +++- birdy/client/__init__.py | 72 ++++++++------- birdy/client/base.py | 94 +++++++++++--------- birdy/client/converters.py | 29 ++++-- birdy/client/notebook.py | 150 +++++++++++++++++++++++++++----- birdy/client/outputs.py | 15 ++-- birdy/client/utils.py | 137 +++++++++++++++++++++++++---- birdy/dependencies.py | 1 - birdy/exceptions.py | 2 - birdy/ipyleafletwfs/__init__.py | 1 - birdy/ipyleafletwfs/base.py | 68 ++++++++------- birdy/utils.py | 121 ++++++++++++++++++++++---- setup.cfg | 4 +- setup.py | 14 ++- tests/test_client.py | 11 ++- 18 files changed, 576 insertions(+), 203 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8246502..50f1b5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,11 +46,6 @@ repos: - id: nbqa-isort args: [ '--profile=black' ] additional_dependencies: [ 'isort==6.0.1' ] - - repo: https://github.com/pycqa/pydocstyle - rev: 6.3.0 - hooks: - - id: pydocstyle - args: [ '--convention=numpy' ] - repo: https://github.com/numpy/numpydoc rev: v1.8.0 hooks: diff --git a/birdy/cli/__init__.py b/birdy/cli/__init__.py index 39384f2..93a8654 100644 --- a/birdy/cli/__init__.py +++ b/birdy/cli/__init__.py @@ -1,4 +1,3 @@ -# noqa: D205, D400 """ Birdy CLI module ================ @@ -7,7 +6,9 @@ Example ------- -Here is an example with Emu_ WPS service:: +Here is an example with Emu_ WPS service: + +.. code-block:: console $ birdy -h $ birdy hello -h @@ -17,14 +18,18 @@ Configure WPS service URL ------------------------- By default Birdy talks to a WPS service on URL http://localhost:5000/wps. -You can change this URL by setting the enivronment variable ``WPS_SERVICE``:: +You can change this URL by setting the environment variable ``WPS_SERVICE``: + +.. code-block:: console $ export WPS_SERVICE=http://localhost:5000/wps Configure SSL verification for HTTPS ------------------------------------ -In case you have a WPS serive using HTTPS with a self-signed certificate you need to configure -the environment variable ``WPS_SSL_VERIFY``:: +In case you have a WPS service using HTTPS with a self-signed certificate you need to configure +the environment variable ``WPS_SSL_VERIFY``: + +.. code-block:: console # deactivate SSL server validation for a self-signed certificate. $ export WPS_SSL_VERIFY=false @@ -36,7 +41,9 @@ -------------------------- If the WPS service is secured by an OAuth2 access tokens -then you can provide an access token with the ``--token`` option:: +then you can provide an access token with the ``--token`` option: + +.. code-block:: console $ birdy --token abc123 hello --name stranger @@ -44,7 +51,9 @@ -------------------------------------------- If the WPS service is secured by x509 certificates you can add a certificate -with the ``--cert`` option to a request:: +with the ``--cert`` option to a request: + +.. code-block:: console # run hello with certificate $ birdy --cert cert.pem hello --name stranger @@ -66,19 +75,23 @@ Unless the process has multiple supported mime types, this can be left to None. Looking at the emu process `output_formats`, the JSON output's default's the ``as reference`` -parameter to False and returns the content directly:: +parameter to False and returns the content directly: + +.. code-block:: console $ birdy output_formats Output: netcdf=http://localhost:5000/outputs/d9abfdc4-08d6-11eb-9334-0800274cd70c/dummy.nc json=['{"testing": [1, 2]}'] -We can then use the output_formats option to redefine it:: +We can then use the output_formats option to redefine it: + +.. code-block:: console $ birdy output_formats --output_formats json True None Output: netcdf=http://localhost:5000/outputs/38e9aefe-08db-11eb-9334-0800274cd70c/dummy.nc json=http://localhost:5000/outputs/38e9aefe-08db-11eb-9334-0800274cd70c/dummy.json -.. _requests: http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification +.. _requests: https://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification """ diff --git a/birdy/cli/base.py b/birdy/cli/base.py index b26fb98..3087690 100644 --- a/birdy/cli/base.py +++ b/birdy/cli/base.py @@ -19,7 +19,8 @@ class BirdyCLI(click.MultiCommand): - """BirdyCLI is an implementation of :class:`click.MultiCommand`. + """ + BirdyCLI is an implementation of :class:`click.MultiCommand`. Adds each process of a Web Processing Service as command to the command-line interface. @@ -33,7 +34,7 @@ class BirdyCLI(click.MultiCommand): A WPS GetCapabilities response for testing. desc_xml : str A WPS DescribeProcess response with "identifier=all" for testing. - attrs : dict + **attrs : dict Additional attributes. """ diff --git a/birdy/cli/run.py b/birdy/cli/run.py index b990c7b..883bdff 100644 --- a/birdy/cli/run.py +++ b/birdy/cli/run.py @@ -40,7 +40,7 @@ def _set_language(ctx, param, value): "--send", "-S", is_flag=True, - help="Send client side certificate to WPS. Default: false", + help="Send client side certificate to WPS. Default: false.", ) @click.option( "--sync", @@ -71,6 +71,21 @@ def cli(ctx, cert, send, sync, token): """ Birdy is a command line client for Web Processing Services. + Parameters + ---------- + ctx : click.Context + Click context object. + cert : str + Client side certificate containing both certificate and private key. + send : bool + Send client side certificate to WPS. Default: false. + sync : bool + Execute process in sync mode. Default: async mode. + token : str + Token to access the WPS service. + + Notes + ----- Documentation is available on readthedocs: https://birdy.readthedocs.org/en/latest/ """ diff --git a/birdy/client/__init__.py b/birdy/client/__init__.py index 959485e..a167e7f 100644 --- a/birdy/client/__init__.py +++ b/birdy/client/__init__.py @@ -1,4 +1,3 @@ -# noqa: D205, D400 """ WPSClient Class =============== @@ -23,31 +22,32 @@ Example ------- -If a WPS server with a simple `hello` process is running on the local host on port 5000:: - - >>> from birdy import WPSClient - >>> emu = WPSClient('http://localhost:5000/') - >>> emu.hello - > - >>> print(emu.hello.__doc__) - - # Just says a friendly Hello. Returns a literal string output with Hello plus the inputed name. - - # Parameters - # ---------- - # name : string - # Please enter your name. - # - # Returns - # ------- - # output : string - # A friendly Hello from us. - # - # "" - # - # # Call the function. The output is a namedtuple - # >>> emu.hello('stranger') - # hello(output='Hello stranger') +If a WPS server with a simple `hello` process is running on the local host on port 5000: + +.. code-block:: python + + >>> from birdy import WPSClient + >>> emu = WPSClient('http://localhost:5000/') + >>> emu.hello + > + >>> print(emu.hello.__doc__) + # Just says a friendly Hello. Returns a literal string output with Hello plus the inputed name. + + # Parameters + # ---------- + # name : string + # Please enter your name. + # + # Returns + # ------- + # output : string + # A friendly Hello from us. + # + # "" + # + # # Call the function. The output is a namedtuple + # >>> emu.hello('stranger') + # hello(output='Hello stranger') Authentication -------------- @@ -56,7 +56,9 @@ The simplest form of authentication is HTTP Basic Auth. Although wps processes are not commonly protected by this authentication method, -here is a simple example of how to use it:: +here is a simple example of how to use it: + +.. code-block :: python >>> from birdy import WPSClient >>> from requests.auth import HTTPBasicAuth @@ -66,7 +68,9 @@ Because any `requests`-compatible class is accepted, custom authentication methods are implemented the same way as in `requests`. -For example, to connect to a magpie_ protected wps, you can use the requests-magpie_ module:: +For example, to connect to a magpie_ protected wps, you can use the requests-magpie_ module: + +.. code-block :: python >>> from birdy import WPSClient >>> from requests_magpie import MagpieAuth @@ -79,7 +83,9 @@ Birdy automatically manages process output to reflect its default values or Birdy's own defaults. However, it's possible to customize the output of a process. Each process has an input -named ``output_formats``, that takes a dictionary as a parameter:: +named ``output_formats``, that takes a dictionary as a parameter: + +.. code-block :: python # example format = { # 'output_identifier': { @@ -100,12 +106,16 @@ >>> } >>> } -Utility functions can also be used to create this dictionary:: +Utility functions can also be used to create this dictionary: + +.. code-block:: python >>> custom_format = create_output_dictionary('netcdf', True, 'application/json') >>> add_output_format(custom_format, 'json', False, None) -The created dictionary can then be used with a process:: +The created dictionary can then be used with a process: + +.. code-block:: python >>> cli = WPSClient("http://localhost:5000") >>> z = cli.output_formats(output_formats=custom_format).get() diff --git a/birdy/client/base.py b/birdy/client/base.py index 21e0dcd..9e83422 100644 --- a/birdy/client/base.py +++ b/birdy/client/base.py @@ -1,5 +1,3 @@ -# noqa: D100 - import logging import types from collections import OrderedDict @@ -31,7 +29,44 @@ # TODO: Support passing ComplexInput's data using POST. class WPSClient: - """Returns a class where every public method is a WPS process available at the given url. + """ + Returns a class where every public method is a WPS process available at the given url. + + Parameters + ---------- + url : str + Link to WPS provider. config (Config): an instance. + processes : any, optional + Specify a subset of processes to bind. Defaults to all processes. + converters : dict, optional + Correspondence of {mimetype: class} to convert this mimetype to a python object. + username : str, optional + Passed to :class:`owslib.wps.WebProcessingService`. + password : str, optional + Passed to :class:`owslib.wps.WebProcessingService`. + headers : str, optional + Passed to :class:`owslib.wps.WebProcessingService`. + auth : requests.auth.AuthBase + Requests-style auth class to authenticate. + See https://2.python-requests.org/en/master/user/authentication/. + verify : bool + Passed to :class:`owslib.wps.WebProcessingService`. + cert : str + Passed to :class:`owslib.wps.WebProcessingService`. + progress : bool + If True, enable interactive user mode. + version : str + WPS version to use. + caps_xml : str + A WPS GetCapabilities response for testing. + desc_xml : str + A WPS DescribeProcess response with "identifier=all" for testing. + language : str + Passed to :class:`owslib.wps.WebProcessingService` (e.g. 'fr-CA', 'en_US'). + lineage : bool + If True, the Execute operation includes lineage information. + **kwds : dict + Passed to :class:`owslib.wps.WebProcessingService`. Examples -------- @@ -59,40 +94,7 @@ def __init__( lineage=False, **kwds, ): - """Initialize WPSClient. - - Parameters - ---------- - url : str - Link to WPS provider. config (Config): an instance. - processes : any, optional - Specify a subset of processes to bind. Defaults to all processes. - converters : dict, optional - Correspondence of {mimetype: class} to convert this mimetype to a python object. - username : str, optional - Passed to :class:`owslib.wps.WebProcessingService`. - password : str, optional - Passed to :class:`owslib.wps.WebProcessingService`. - headers : str, optional - Passed to :class:`owslib.wps.WebProcessingService`. - auth : requests.auth.AuthBase - Requests-style auth class to authenticate. - See https://2.python-requests.org/en/master/user/authentication/. - verify : bool - Passed to :class:`owslib.wps.WebProcessingService`. - cert : str - Passed to :class:`owslib.wps.WebProcessingService`. - verbose: bool - Deprecated. passed to :class:`owslib.wps.WebProcessingService` for owslib < 0.29. - progress : bool - If True, enable interactive user mode. - version : str - WPS version to use. - language : str - Passed to :class:`owslib.wps.WebProcessingService` (ex: 'fr-CA', 'en_US'). - lineage : bool - If True, the Execute operation includes lineage information. - """ + """Initialize WPSClient.""" self._converters = converters self._interactive = progress self._mode = ASYNC if progress else SYNC @@ -442,7 +444,8 @@ def sigint_handler(signum, frame): def sort_inputs_key(i: Input): - """Key function for sorting process inputs. + """ + Key function for sorting process inputs. The order is: - Inputs that have minOccurs >= 1 and no default value @@ -452,7 +455,7 @@ def sort_inputs_key(i: Input): Parameters ---------- i : owslib.wps.Input - An owslib Input + An owslib Input. Notes ----- @@ -467,8 +470,17 @@ def sort_inputs_key(i: Input): return [not c for c in conditions] # False values are sorted first -def nb_form(wps, pid): - """Return a Notebook form to enter input values and launch process.""" +def nb_form(wps: WPSClient, pid: str) -> None: + """ + Return a Notebook form to enter input values and launch process. + + Parameters + ---------- + wps : WPSClient + The WPS client. + pid : str + The process identifier. + """ if wps._notebook: return notebook.interact( func=getattr(wps, sanitize(pid)), diff --git a/birdy/client/converters.py b/birdy/client/converters.py index 5391b0a..5b8f83e 100644 --- a/birdy/client/converters.py +++ b/birdy/client/converters.py @@ -1,10 +1,8 @@ -# noqa: D100 - import tempfile from collections.abc import Sequence from importlib import import_module from pathlib import Path -from typing import Any, Optional, Union +from typing import Optional, Union from owslib.wps import Output from packaging.version import Version @@ -312,8 +310,24 @@ def _find_converter(mimetype=None, extension=None, converters=()): return select -def find_converter(obj: Any, converters): - """Find converters for a WPS output or a file on disk.""" +def find_converter( + obj: Union[Output, str, Path], converters: Sequence[BaseConverter] +) -> list: + """ + Find converters for a WPS output or a file on disk. + + Parameters + ---------- + obj : owslib.wps.Output or str or Path + Object to convert. + converters : sequence of BaseConverter subclasses + Converter classes to search within for a match. + + Returns + ------- + list + A list of compatible converters ordered by priority. + """ if isinstance(obj, Output): mimetype = obj.mimeType extension = Path(obj.fileName or "").suffix[1:] @@ -321,7 +335,7 @@ def find_converter(obj: Any, converters): mimetype = None extension = Path(obj).suffix[1:] else: - raise NotImplementedError + raise NotImplementedError() return _find_converter(mimetype, extension, converters=converters) @@ -332,7 +346,8 @@ def convert( converters: Sequence[BaseConverter] = None, verify: bool = True, ): - """Convert a file to an object. + """ + Convert a file to an object. Parameters ---------- diff --git a/birdy/client/notebook.py b/birdy/client/notebook.py index 7346003..ae98c61 100644 --- a/birdy/client/notebook.py +++ b/birdy/client/notebook.py @@ -1,8 +1,6 @@ -# noqa: D100 - import threading -from owslib.wps import Input +from owslib.wps import Input, WPSExecution from birdy.dependencies import IPython from birdy.dependencies import ipywidgets as widgets @@ -29,7 +27,19 @@ def is_notebook(): def gui(func): - """Return a Notebook form to enter input values and launch process.""" + """ + Return a Notebook form to enter input values and launch process. + + Parameters + ---------- + func : function + A function. + + Returns + ------- + Form + A form to enter input values and launch the process. + """ if func.__self__._notebook: return Form(func) else: @@ -39,7 +49,14 @@ def gui(func): class Form: - """Create notebook form to launch WPS process.""" + """ + Create notebook form to launch WPS process. + + Parameters + ---------- + func : function + A function. + """ def __init__(self, func): self.result = None @@ -81,12 +98,13 @@ def execute(change): ui = self.build_ui(iw, ofw, go) IPython.display(ui, out) - def get(self, asobj=False): - """Return the process response outputs. + def get(self, asobj: bool = False): + """ + Return the process response outputs. Parameters ---------- - asobj: bool + asobj : bool If True, object_converters will be used. """ # Mimicks the `WPSResult.get` method, to provide a consistent look and feel to all user interfaces. @@ -94,16 +112,52 @@ def get(self, asobj=False): raise ValueError("The process has not yet been executed.") return self.result.get(asobj) - def input_widgets(self, inputs): - """Return input parameter widgets.""" + def input_widgets(self, inputs: dict) -> dict: + """ + Return input parameter widgets. + + Parameters + ---------- + inputs : dict + A dictionary of input parameters. + + Returns + ------- + dict + A dictionary of input widgets. + """ return {sanitize(key): input2widget(inpt) for key, inpt in inputs} - def input_widget_values(self, widgets): - """Return values from input widgets.""" + def input_widget_values(self, widgets: dict) -> dict: + """ + Return values from input widgets. + + Parameters + ---------- + widgets : dict + A dictionary of input widgets. + + Returns + ------- + dict + A dictionary of input values. + """ return {k: v.value for (k, v) in widgets.items()} - def output_formats_widgets(self, outputs): - """Return output formats parameter widgets for ComplexData outputs that have multiple supported formats.""" + def output_formats_widgets(self, outputs: dict) -> dict: + """ + Return output formats parameter widgets for ComplexData outputs that have multiple supported formats. + + Parameters + ---------- + outputs : dict + A dictionary of output parameters. + + Returns + ------- + dict + A dictionary of output format widgets. + """ of = {} style = {"description_width": "initial"} if any( @@ -123,8 +177,20 @@ def output_formats_widgets(self, outputs): return of - def output_format_widget_values(self, widgets): - """Return the `output_formats` dict from output_formats widgets.""" + def output_format_widget_values(self, widgets: dict) -> dict: + """ + Return the `output_formats` dict from output_formats widgets. + + Parameters + ---------- + widgets : dict + `output_formats` widgets. + + Returns + ------- + dict + A dictionary of `output_formats`. + """ out = {} for key, val in widgets.items(): utils.add_output_format( @@ -135,7 +201,23 @@ def output_format_widget_values(self, widgets): return {} def build_ui(self, input_widgets, of_widgets, go): - """Create the form.""" + """ + Create the form. + + Parameters + ---------- + input_widgets : dict + Input widgets. + of_widgets : dict + Output form widgets. + go : str + Footer. + + Returns + ------- + AppLayout + An instance of the UI. + """ iw = list(input_widgets.values()) ofw = list(of_widgets.values()) @@ -165,7 +247,8 @@ def build_ui(self, input_widgets, of_widgets, go): def monitor(execution: WPSExecution, sleep: int = 3): - """Monitor the execution of a process using a notebook progress bar widget. + """ + Monitor the execution of a process using a notebook progress bar widget. Parameters ---------- @@ -191,21 +274,21 @@ def monitor(execution: WPSExecution, sleep: int = 3): tooltip="Send `dismiss` request to WPS server.", ) - def cancel_handler(b): + def _cancel_handler(b): b.value = True b.disabled = True progress.description = "Interrupted" # TODO: Send dismiss signal to server cancel.value = False - cancel.on_click(cancel_handler) + cancel.on_click(_cancel_handler) box = widgets.HBox( [progress, cancel], layout=widgets.Layout(justify_content="space-between") ) IPython.display.display(box) - def check(execution, progress, cancel): + def _check(execution, progress, cancel): while not execution.isComplete() and not cancel.value: execution.checkStatus(sleepSecs=sleep) progress.value = execution.percentCompleted @@ -219,12 +302,24 @@ def check(execution, progress, cancel): else: progress.bar_style = "danger" - thread = threading.Thread(target=check, args=(execution, progress, cancel)) + thread = threading.Thread(target=_check, args=(execution, progress, cancel)) thread.start() def input2widget(inpt: Input): - """Return a Notebook widget to enter values for the input.""" + """ + Return a Notebook widget to enter values for the input. + + Parameters + ---------- + inpt : Input + A WPS Input instance. + + Returns + ------- + Any + Widget. + """ if not isinstance(inpt, Input): raise ValueError() @@ -286,5 +381,12 @@ def input2widget(inpt: Input): def output2widget(output): - """Return notebook widget based on output mime-type.""" + """ + Return notebook widget based on output mime-type. + + Parameters + ---------- + output : Any + Unused. + """ pass diff --git a/birdy/client/outputs.py b/birdy/client/outputs.py index 8f9599e..639ecae 100644 --- a/birdy/client/outputs.py +++ b/birdy/client/outputs.py @@ -1,5 +1,3 @@ -# noqa: D100 - import tempfile from collections import namedtuple from typing import Optional @@ -14,26 +12,28 @@ class WPSResult(WPSExecution): # noqa: D101 def attach(self, wps_outputs: Output, converters: Optional[dict] = None): - """Attach the outputs according to converters. + """ + Attach the outputs according to converters. Parameters ---------- wps_outputs : owslib.wps.Output The WPS outputs. converters : dict, optional - Converter dictionary (`{name: object}`) + Converter dictionary (`{name: object}`). """ self._wps_outputs = wps_outputs self._converters = converters self._path = tempfile.mkdtemp() def get(self, asobj: bool = False): - """Return the process response outputs. + """ + Return the process response outputs. Parameters ---------- asobj : bool - If True, object_converters will be used. + If True, object_converters will be used. Default is False. """ if not self.isComplete(): raise ProcessIsNotComplete("Please wait ...") @@ -53,7 +53,8 @@ def _make_output(self, convert_objects=False): ) def _process_output(self, output: Output, convert_objects: bool = False): - """Process the output response. + """ + Process the output response. Determine whether it is actual data or a URL to a file. diff --git a/birdy/client/utils.py b/birdy/client/utils.py index d2afd3f..b98a93d 100644 --- a/birdy/client/utils.py +++ b/birdy/client/utils.py @@ -14,7 +14,21 @@ def filter_case_insensitive( names: Union[str, list[str]], complete_list: list[str] ) -> tuple[list[str], list[str]]: - """Filter a sequence of process names into a `known` and `unknown` list.""" + """ + Filter a sequence of process names into a `known` and `unknown` list. + + Parameters + ---------- + names : str or list of str + Process names. + complete_list : list of str + List of all available process names. + + Returns + ------- + tuple + Tuple of two lists: `contained` and `missing`. + """ contained = [] missing = [] complete_list_lower = {name.lower() for name in complete_list} @@ -32,7 +46,8 @@ def filter_case_insensitive( def pretty_repr(obj: Any, linebreaks: bool = True): - """Output pretty repr for an Output. + """ + Output pretty repr for an Output. Parameters ---------- @@ -73,17 +88,20 @@ def pretty_repr(obj: Any, linebreaks: bool = True): def build_wps_client_doc( wps: WebProcessingService, processes: dict[str, Process] ) -> str: - """Create WPSClient docstring. + """ + Create WPSClient docstring. Parameters ---------- wps : owslib.wps.WebProcessingService + A WPS service. processes : Dict[str, owslib.wps.Process] + A dictionary of available processes. Returns ------- str - The formatted docstring for this WPSClient + The formatted docstring for this WPSClient. """ doc = [wps.identification.abstract or "", "", "Processes", "---------", ""] @@ -104,7 +122,19 @@ def build_wps_client_doc( def build_process_doc(process: Process) -> str: - """Create docstring from process metadata.""" + """ + Create docstring from process metadata. + + Parameters + ---------- + process : owslib.wps.Process + A WPS process. + + Returns + ------- + str + The formatted docstring for this process. + """ doc = [process.abstract or "", ""] # Inputs @@ -131,7 +161,19 @@ def build_process_doc(process: Process) -> str: def format_type(obj: Any) -> str: - """Create docstring entry for input parameter from an OWSlib object.""" + """ + Create docstring entry for input parameter from an OWSlib object. + + Parameters + ---------- + obj : Any + An OWSlib object. + + Returns + ------- + str + The formatted docstring entry for this object. + """ nmax = 10 doc = "" @@ -174,7 +216,15 @@ def format_type(obj: Any) -> str: def is_embedded_in_request(url: str, value: Any) -> bool: - """Whether to encode the value as raw data content. + """ + Whether to encode the value as raw data content. + + Parameters + ---------- + url : str + URL to the WPS server. + value : Any + Value to be sent to the WPS server. Returns ------- @@ -220,7 +270,27 @@ def to_owslib( mimetype: Optional[Any] = None, schema: Optional[Any] = None, ) -> Any: - """Convert value into OWSlib objects.""" + """ + Convert value into OWSlib objects. + + Parameters + ---------- + value : Any + Value to be converted. + data_type : str + The WPS dataType. + encoding : Any, optional + Encoding of the data. + mimetype : Any, optional + MIME type of the data. + schema : Any, optional + Schema of the data. + + Returns + ------- + Any + The converted value. + """ # owslib only accepts literaldata, complexdata and boundingboxdata if data_type == "ComplexData": @@ -235,7 +305,21 @@ def to_owslib( def from_owslib(value: Any, data_type: str) -> Any: - """Convert a string into another data type.""" + """ + Convert a string into another data type. + + Parameters + ---------- + value : Any + Value to be converted. + data_type : str + The WPS dataType. + + Returns + ------- + Any + The converted value. + """ if value is None: return None @@ -264,7 +348,19 @@ def from_owslib(value: Any, data_type: str) -> Any: def py_type(data_type: str) -> Any: - """Return the python data type matching the WPS dataType.""" + """ + Return the python data type matching the WPS dataType. + + Parameters + ---------- + data_type : str + The WPS dataType. + + Returns + ------- + Any + The python data type. + """ if data_type is None: return None if "string" in data_type: @@ -289,8 +385,17 @@ def py_type(data_type: str) -> Any: return str -def extend_instance(obj, cls): - """Apply mixins to a class instance after creation.""" +def extend_instance(obj: Any, cls: Any) -> None: + """ + Apply mixins to a class instance after creation. + + Parameters + ---------- + obj : Any + The object to be extended. + cls : Any + The class to be added. + """ base_cls = obj.__class__ base_cls_name = obj.__class__.__name__ obj.__class__ = type(base_cls_name, (cls, base_cls), {}) @@ -302,12 +407,13 @@ def add_output_format( as_ref: Optional[bool] = None, mimetype: Optional[str] = None, ) -> None: - """Add an output format to an already existing dictionary. + """ + Add an output format to an already existing dictionary. Parameters ---------- output_dictionary : dict - The dictionary (created with create_output_dictionary()) to which this + The dictionary (created with `create_output_dictionary()`) to which this output format will be added. output_identifier : str Identifier of the output. @@ -329,7 +435,8 @@ def create_output_dictionary( as_ref: Optional[bool] = None, mimetype: Optional[str] = None, ) -> dict: - """Create an output format dictionary. + """ + Create an output format dictionary. Parameters ---------- diff --git a/birdy/dependencies.py b/birdy/dependencies.py index 54a6277..5efce44 100644 --- a/birdy/dependencies.py +++ b/birdy/dependencies.py @@ -1,4 +1,3 @@ -# noqa: D205,D400 """ Dependencies Module =================== diff --git a/birdy/exceptions.py b/birdy/exceptions.py index 1094bf0..d7958e8 100644 --- a/birdy/exceptions.py +++ b/birdy/exceptions.py @@ -1,5 +1,3 @@ -# noqa: D100 - import click from owslib.util import ServiceException diff --git a/birdy/ipyleafletwfs/__init__.py b/birdy/ipyleafletwfs/__init__.py index cb30ebf..8c415fa 100644 --- a/birdy/ipyleafletwfs/__init__.py +++ b/birdy/ipyleafletwfs/__init__.py @@ -1,4 +1,3 @@ -# noqa: D205,D400 """ IpyleafletWFS Module ==================== diff --git a/birdy/ipyleafletwfs/base.py b/birdy/ipyleafletwfs/base.py index 6caf9ad..1799da1 100644 --- a/birdy/ipyleafletwfs/base.py +++ b/birdy/ipyleafletwfs/base.py @@ -1,5 +1,3 @@ -# noqa: D100 - import json from typing import Any, Optional @@ -17,7 +15,8 @@ def _map_extent_to_bbox_filter(source_map): - """Return formatted coordinates, from ipylealet format to owslib.wfs format. + """ + Return formatted coordinates from ipylealet format to owslib.wfs format. This function takes the result of ipyleaflet's Map.bounds() function and formats it so it can be used as a bbox filter in an owslib WFS request. @@ -43,7 +42,8 @@ def _map_extent_to_bbox_filter(source_map): class IpyleafletWFS: - """Create a connection to a WFS service capable of geojson output. + """ + Create a connection to a WFS service capable of geojson output. This class is a small wrapper for ipylealet to facilitate the use of a WFS service, as well as provide some automation. @@ -102,7 +102,8 @@ def __init__(self, url, wfs_version="2.0.0"): def build_layer( self, layer_typename: str, source_map, layer_style=None, feature_property=None ): - """Return an ipyleaflet GeoJSON layer from a geojson wfs request. + """ + Return an ipyleaflet GeoJSON layer from a geojson wfs request. Requires the WFS service to be capable of geojson output. @@ -113,16 +114,16 @@ def build_layer( layer_typename : str Typename of the layer to display. Listed as Layer_ID by `get_layer_list()`. Must include namespace and layer name, separated by a colon. - e.g. public:canada_forest_layer + e.g. public:canada_forest_layer. source_map : Map instance - The map instance on which the layer is to be added. + The map instance on which the layer is to be added. layer_style : dict - ipyleaflet GeoJSON style format, for example: - `{ 'color': 'white', 'opacity': 1, 'dashArray': '9', 'fillOpacity': 0.1, 'weight': 1 }`. - See ipyleaflet documentation for more information. + A layer style dictionary in ipyleaflet GeoJSON style format, for example: + `{ 'color': 'white', 'opacity': 1, 'dashArray': '9', 'fillOpacity': 0.1, 'weight': 1 }`. + See ipyleaflet documentation for more information. feature_property : str - The property key to be used by the widget. Use the `property_list()` function - to get a list of the available properties. + The property key to be used by the widget. Use the `property_list()` function + to get a list of the available properties. """ # Check if dependency is installed if ipyl is None: @@ -177,7 +178,8 @@ def build_layer( def create_wfsgeojson_layer( self, layer_typename: str, source_map, layer_style: Optional[dict] = None ): - """Create a static ipyleaflett GeoJSON layer from a WFS service. + """ + Create a static ipyleaflet GeoJSON layer from a WFS service. Simple wrapper for a WFS => GeoJSON layer, using owslib. @@ -189,20 +191,21 @@ def create_wfsgeojson_layer( Parameters ---------- - layer_typename: str - Typename of the layer to display. Listed as Layer_ID by `get_layer_list()`. - Must include namespace and layer name, separated by a colon. - e.g. `public:canada_forest_layer` + layer_typename : str + Typename of the layer to display. Listed as Layer_ID by `get_layer_list()`. + Must include namespace and layer name, separated by a colon. + e.g. `public:canada_forest_layer`. source_map : Map instance The map instance from which the extent will be used to filter the request. layer_style : dict, optional - ipyleaflet GeoJSON style format, for example: + A layer style dictionary in ipyleaflet GeoJSON style format, for example: `{ 'color': 'white', 'opacity': 1, 'dashArray': '9', 'fillOpacity': 0.1, 'weight': 1 }`. See ipyleaflet documentation for more information. Returns ------- - GeoJSON layer: an instance of an ipyleaflet GeoJSON layer. + ipyleaflet.GeoJSON + An instance of an ipyleaflet GeoJSON layer. """ # Check if dependency is installed if ipyl is None: @@ -228,14 +231,15 @@ def create_wfsgeojson_layer( return layer def _refresh_layer(self, placeholder: Optional[str] = None): - """Refresh the wfs layer for the current map extent. + """ + Refresh the WFS layer for the current map extent. Also updates the existing widgets. Parameters ---------- placeholder : str, optional - Parameter is only there so that button.on_click() will work properly. + Parameter is only there so that `button.on_click()` will work properly. """ if self._layer: self.build_layer( @@ -274,7 +278,8 @@ def remove_layer(self): # # # # # # # # # # # # # # # # def feature_properties_by_id(self, feature_id: int) -> Optional[Any]: - """Return the properties of a feature. + """ + Return the properties of a feature. The id field is usually the first field. Since the name is always different, this is the only assumption that can be made to automate this process. @@ -307,7 +312,8 @@ def geojson(self): @property def layer_list(self) -> list: - """Return a simple layer list available to the WFS service. + """ + Return a simple layer list available to the WFS service. Returns ------- @@ -318,7 +324,8 @@ def layer_list(self) -> list: @property def property_list(self) -> dict: - """Return a list containing the properties of the first feature. + """ + Return a list containing the properties of the first feature. Retrieves the available properties for use subsequent use by the feature property widget. @@ -367,10 +374,10 @@ def _create_refresh_widget(self): self._source_map.add_control(self._refresh_widget) def clear_property_widgets(self): - """Remove all property widgets from a map. + """ + Remove all property widgets from a map. - This function will remove the property widgets from a given map, without - affecting other widgets. + This function will remove the property widgets from a given map, without affecting other widgets. """ if self._property_widgets: for widget in self._property_widgets: @@ -385,7 +392,8 @@ def create_feature_property_widget( feature_property: Optional[str] = None, widget_position: str = "bottomright", ): - """Create a visualization widget for a specific feature property. + """ + Create a visualization widget for a specific feature property. Will create a widget for the layer and source map. Once the widget is created, click on a map feature to have the information appear in the corresponding box. @@ -398,8 +406,8 @@ def create_feature_property_widget( Name of the widget. Must be unique or will overwrite existing widget. feature_property : str, optional The property key to be used by the widget. Use the property_list() function - to get a list of the available properties. If left empty, it will default to - the first property attribute in the list. + to get a list of the available properties. + If left empty, it will default to the first property attribute in the list. widget_position : {‘bottomleft’, ‘bottomright’, ‘topleft’, ‘topright’} Position on the map for the widget. diff --git a/birdy/utils.py b/birdy/utils.py index ca8db35..bc5a042 100644 --- a/birdy/utils.py +++ b/birdy/utils.py @@ -1,5 +1,3 @@ -# noqa: D100 - import base64 import collections import keyword @@ -29,12 +27,36 @@ def fix_url(url: str) -> str: - """If url is a local path, add a file:// scheme.""" + """ + If url is a local path, add a file:// scheme. + + Parameters + ---------- + url : str + URL or local path. + + Returns + ------- + str + URL with a file:// scheme. + """ return urlparse(url, scheme="file").geturl() def is_url(url: Optional[str]) -> bool: - """Return whether value is a valid URL.""" + """ + Return whether value is a valid URL. + + Parameters + ---------- + url : str + URL or local path. + + Returns + ------- + bool + True if value is a valid URL. + """ if url is None: return False parsed_url = urlparse(url) @@ -44,8 +66,9 @@ def is_url(url: Optional[str]) -> bool: return True -def is_opendap_url(url: str): - """Check if a provided url is an OpenDAP url. +def is_opendap_url(url: str) -> bool: + """ + Check if a provided url is an OpenDAP url. The DAP Standard specifies that a specific tag must be included in the Content-Description header of every request. This tag is one of: @@ -53,7 +76,19 @@ def is_opendap_url(url: str): So we can check if the header starts with `dods`. - Note that this might not work with every DAP server implementation. + Parameters + ---------- + url : str + URL. + + Returns + ------- + bool + True if the URL is an OpenDAP URL. + + Notes + ----- + This might not work with every DAP server implementation. """ import requests from requests.exceptions import ConnectionError, InvalidSchema, MissingSchema @@ -72,7 +107,19 @@ def is_opendap_url(url: str): def is_file(path: Optional[str]) -> bool: - """Return True if `path` is a valid file.""" + """ + Return True if `path` is a valid file. + + Parameters + ---------- + path : str or Path + Path to a file. + + Returns + ------- + bool + True if `path` is a valid file. + """ if not path: return False elif isinstance(path, Path): @@ -86,10 +133,21 @@ def is_file(path: Optional[str]) -> bool: return ok -def sanitize(name: str): - """Lower-case name and replace all non-ascii chars by `_`. +def sanitize(name: str) -> str: + """ + Lower-case name and replace all non-ascii chars by `_`. If name is a Python keyword (like `return`) then add a trailing `_`. + + Parameters + ---------- + name : str + Name to sanitize. + + Returns + ------- + str + Sanitized name. """ new_name = re.sub(r"\W|^(?=\d)", "_", name.lower()) if keyword.iskeyword(new_name): @@ -97,8 +155,20 @@ def sanitize(name: str): return new_name -def delist(data: Any): - """If data is a sequence with a single element, returns this element, otherwise return the sequence.""" +def delist(data: Any) -> Any: + """ + If data is a sequence with a single element, returns this element, otherwise return the sequence. + + Parameters + ---------- + data : Any + Data to check. + + Returns + ------- + Any + Single element or sequence. + """ if ( isinstance(data, collections.abc.Iterable) and not isinstance(data, str) @@ -108,13 +178,25 @@ def delist(data: Any): return data -def embed(value: Any, mimetype: Optional[str] = None, encoding: Optional[str] = None): - """Return the content of the file, either as a string or base64 bytes. +def embed( + value: Any, mimetype: Optional[str] = None, encoding: Optional[str] = None +) -> tuple[bytes, str] | tuple[str, str | Any] | tuple[Any, str | Any]: + """ + Return the content of the file, either as a string or base64 bytes. + + Parameters + ---------- + value : Any + File path, URL, or file-like object. + mimetype : str, optional + Mimetype of the file. + encoding : str, optional + Encoding of the file. Returns ------- - str - encoded content string and actual encoding + tuple + Encoded content string and actual encoding. """ if hasattr( value, "read" @@ -160,7 +242,8 @@ def _encode(content, mimetype, encoding): def guess_type( url: Union[str, Path], supported: Union[list[str], tuple[str]] ) -> tuple[str, str]: - """Guess the mime type of the file link. + """ + Guess the mime type of the file link. If the mimetype is not recognized, default to the first supported value. @@ -173,8 +256,8 @@ def guess_type( Returns ------- - (str, str) - mimetype, encoding + tuple + Mimetype and encoding. """ import mimetypes diff --git a/setup.cfg b/setup.cfg index 9a3340c..eefc511 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,6 @@ ignore-path = docs/build,docs/source/_templates,docs/source/_static max-line-length = 120 [tool:numpydoc_validation] -checks = all,EX01,SA01,ES01,RT01,GL06,GL07,GL08 -exclude = \.undocumented_method$,\.__repr__$ +checks = all,ES01,EX01,GL06,GL07,GL08,RT01,SA01,SS01 +exclude = \.undocumented_method$,\.__repr__$,\._\w+ override_SS05 = ^Process ,^Assess ,^Access , diff --git a/setup.py b/setup.py index df6d12a..05ff215 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,19 @@ def parse_reqs(file): - """Parse dependencies from requirements file with regex.""" + """ + Parse dependencies from requirements file with regex. + + Parameters + ---------- + file : str + Path to requirements file. + + Returns + ------- + list + The list of dependencies. + """ egg_regex = re.compile(r"#egg=(\w+)") reqs = list() for req in open(file): diff --git a/tests/test_client.py b/tests/test_client.py index e67df71..8c6e389 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -326,12 +326,15 @@ def test_sort_inputs(process): # noqa: D103 def test_sort_inputs_conditions(): - """Test for the input sorting function. + """ + Test for the input sorting function. + Notes + ----- The order should be: - - Inputs that have minOccurs >= 1 and no default value - - Inputs that have minOccurs >= 1 and a default value - - Every other input + - Inputs that have minOccurs >= 1 and no default value + - Inputs that have minOccurs >= 1 and a default value + - Every other input """ i = mock.Mock() i.minOccurs = 1 From 8f9e8c4b3a9b2e00a9de9bd801c224fd6af8dcff Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:11:06 -0500 Subject: [PATCH 11/14] ensure Python3.9 support --- birdy/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/birdy/utils.py b/birdy/utils.py index bc5a042..ba84ac3 100644 --- a/birdy/utils.py +++ b/birdy/utils.py @@ -180,7 +180,7 @@ def delist(data: Any) -> Any: def embed( value: Any, mimetype: Optional[str] = None, encoding: Optional[str] = None -) -> tuple[bytes, str] | tuple[str, str | Any] | tuple[Any, str | Any]: +) -> Union[tuple[bytes, str], tuple[str, Union[str, Any]], tuple[Any, Union[str, Any]]]: """ Return the content of the file, either as a string or base64 bytes. From f8a19907de3fd86abb07ed0e28ef11eb7fb60c0b Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:11:15 -0500 Subject: [PATCH 12/14] update actions --- .github/workflows/main.yml | 30 +++++++++--------------------- .github/workflows/publish-pypi.yml | 8 +++++--- .github/workflows/tag-testpypi.yml | 8 +++++--- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70a6912..19b5e9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,17 +16,17 @@ jobs: strategy: matrix: python-version: - - "3.9" + - "3.x" steps: - name: Harden Runner uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: egress-policy: audit - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Set up Python${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: ${{ matrix.python-version }} - name: Install flake8 and black @@ -48,25 +48,19 @@ jobs: uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: egress-policy: audit - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Install pandoc run: | sudo apt-get -y install pandoc - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: ${{ matrix.python-version }} - - name: Install requirements 📦 - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi - if [ -f requirements_extra.txt ]; then pip install -r requirements_extra.txt; fi - name: Install Birdy 🐦 run: | - python -m pip install --editable . + python -m pip install --editable ".[docs]" - name: Check dependencies run: | python -m pip list @@ -88,22 +82,16 @@ jobs: uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: egress-policy: audit - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: ${{ matrix.python-version }} - - name: Install requirements 📦 - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi - if [ -f requirements_extra.txt ]; then pip install -r requirements_extra.txt; fi - name: Install Birdy 🐦 run: | - python -m pip install --editable . + python -m pip install --editable ".[dev]" - name: Check dependencies run: | python -m pip list diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index d507d16..f2bf5fe 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -20,9 +20,11 @@ jobs: uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: egress-policy: audit - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Set up Python3 - uses: actions/setup-python@v5 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: "3.x" - name: Install packaging libraries @@ -32,4 +34,4 @@ jobs: run: | python -m build --sdist --wheel . - name: Publish distribution 📦 to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.2 diff --git a/.github/workflows/tag-testpypi.yml b/.github/workflows/tag-testpypi.yml index c6ad02e..f3ef7fe 100644 --- a/.github/workflows/tag-testpypi.yml +++ b/.github/workflows/tag-testpypi.yml @@ -20,9 +20,11 @@ jobs: uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: egress-policy: audit - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Set up Python3 - uses: actions/setup-python@v5 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: "3.x" - name: Install packaging libraries @@ -32,7 +34,7 @@ jobs: run: | python -m build --sdist --wheel . - name: Publish distribution 📦 to Test PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.2 with: repository-url: https://test.pypi.org/legacy/ skip-existing: true From fe0d2cde3d380d74a688a743d8f7617018ea6a8d Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:16:39 -0500 Subject: [PATCH 13/14] add docs requirements --- requirements_docs.txt | 5 +++++ setup.py | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 requirements_docs.txt diff --git a/requirements_docs.txt b/requirements_docs.txt new file mode 100644 index 0000000..4983fba --- /dev/null +++ b/requirements_docs.txt @@ -0,0 +1,5 @@ +ipython >8.5.0,!=9.0.0 +nbsphinx >=0.9.5 +sphinx >=7.0.0,<8.2.0 +sphinx-codeautolink +sphinx-copybutton diff --git a/setup.py b/setup.py index 05ff215..1af64b5 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ def parse_reqs(file): requirements = parse_reqs("requirements.txt") dev_requirements = parse_reqs("requirements_dev.txt") +docs_requirements = parse_reqs("requirements_docs.txt") extra_requirements = parse_reqs("requirements_extra.txt") classifiers = [ @@ -86,6 +87,7 @@ def parse_reqs(file): install_requires=requirements, extras_require={ "dev": dev_requirements, # pip install ".[dev]" + "docs": docs_requirements, # pip install ".[docs]" "extra": extra_requirements, # pip install ".[extra]" }, entry_points={"console_scripts": ["birdy=birdy.cli.run:cli"]}, From 39ea8ee6858e47117173d7b5f959f057195d7478 Mon Sep 17 00:00:00 2001 From: Zeitsperre <10819524+Zeitsperre@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:29:17 -0500 Subject: [PATCH 14/14] update installation and readme directions, add badges --- README.rst | 32 ++++++++++++++++++----------- docs/source/development.rst | 40 +++++++++++++++++++++++++----------- docs/source/installation.rst | 24 +++++++++++++++------- 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/README.rst b/README.rst index 3cd3199..406b5c7 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Birdy ===== -|PyPI| |Docs| |Build| |License| |Gitter| +|pypi| |conda| |versions| |docs| |build| |license| |gitter| Birdy (the bird) *Birdy is not a bird but likes to play with them.* @@ -23,25 +23,33 @@ Full `documentation `_ is on ReadTheDoc .. _Birdhouse: http://bird-house.github.io/en/latest/ -.. |PyPI| image:: https://img.shields.io/pypi/v/birdhouse-birdy.svg - :target: https://pypi.python.org/pypi/birdhouse-birdy - :alt: Python Package Index Build +.. |build| image:: https://github.com/bird-house/birdy/actions/workflows/main.yml/badge.svg + :target: https://github.com/bird-house/birdy/actions/workflows/main.yml + :alt: Build Status -.. |Docs| image:: https://img.shields.io/badge/docs-latest-brightgreen.svg +.. |conda| image:: https://anaconda.org/conda-forge/birdy/badges/version.svg + :target: https://anaconda.org/conda-forge/birdy + :alt: Anaconda Version + +.. |docs| image:: https://img.shields.io/badge/docs-latest-brightgreen.svg :target: http://birdy.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -.. |Build| image:: https://github.com/bird-house/birdy/actions/workflows/main.yml/badge.svg - :target: https://github.com/bird-house/birdy/actions/workflows/main.yml - :alt: Build Status +.. |gitter| image:: https://badges.gitter.im/bird-house/birdhouse.svg + :target: https://gitter.im/bird-house/birdhouse?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + :alt: Join the chat at https://gitter.im/bird-house/birdhouse -.. |License| image:: https://img.shields.io/github/license/bird-house/birdy.svg +.. |license| image:: https://img.shields.io/github/license/bird-house/birdy.svg :target: https://github.com/bird-house/birdy/blob/master/LICENSE.txt :alt: GitHub license -.. |Gitter| image:: https://badges.gitter.im/bird-house/birdhouse.svg - :target: https://gitter.im/bird-house/birdhouse?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge - :alt: Join the chat at https://gitter.im/bird-house/birdhouse +.. |pypi| image:: https://img.shields.io/pypi/v/birdhouse-birdy.svg + :target: https://pypi.python.org/pypi/birdhouse-birdy + :alt: Python Package Index Build + +.. |versions| image:: https://img.shields.io/pypi/pyversions/birdhouse-birdy.svg + :target: https://pypi.python.org/pypi/birdhouse-birdy + :alt: Supported Python Versions .. |Binder| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/bird-house/birdy.git/v0.8.7?filepath=notebooks diff --git a/docs/source/development.rst b/docs/source/development.rst index 5a6f973..aa4f66d 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -7,24 +7,33 @@ Development Get Started! ============ -Check out code from the birdy GitHub repo and start the installation:: +Check out code from the birdy GitHub repo and start the installation: + +.. code-block:: console $ git clone https://github.com/bird-house/birdy.git $ cd birdy $ conda env create -f environment.yml $ pip install --editable . -Install additional dependencies:: +Install additional dependencies: + +.. code-block:: console $ pip install -r requirements_dev.txt -When you're done making changes, check that your changes pass `black`, `flake8` and the tests:: +When you're done making changes, check that your changes pass `black`, `flake8` and the tests: + +.. code-block:: console $ flake8 birdy tests $ black --check --target-version py39 birdy tests + $ pytest -v tests -Or use the Makefile:: +Or use the Makefile: + +.. code-block:: console $ make lint $ make test @@ -35,7 +44,9 @@ Add pre-commit hooks Before committing your changes, we ask that you install `pre-commit` in your environment. `Pre-commit` runs git hooks that ensure that your code resembles that of the project -and catches and corrects any small errors or inconsistencies when you `git commit`:: +and catches and corrects any small errors or inconsistencies when you `git commit`: + +.. code-block:: console $ conda install -c conda-forge pre-commit $ pre-commit install @@ -44,7 +55,9 @@ Write Documentation =================== You can find the documentation in the `docs/source` folder. To generate the Sphinx -documentation locally you can use the `Makefile`:: +documentation locally you can use the `Makefile`: + +.. code-block:: console $ make docs @@ -53,11 +66,12 @@ Bump a new version Make a new version of Birdy in the following steps: -* Make sure everything is commit to GitHub. -* Update ``CHANGES.rst`` with the next version. -* Dry Run: ``bumpversion --dry-run --verbose --new-version 0.3.1 patch`` -* Do it: ``bumpversion --new-version 0.3.1 patch`` -* ... or: ``bumpversion --new-version 0.4.0 minor`` +* Make sure everything has been committed to the `master` branch of the GitHub repository. +* In a new branch, update ``CHANGES.rst`` with the information needed for the next version. +* Dry Run: ``bump-my-version bump --dry-run --verbose patch|minor|major`` +* Do it: ``bump-my-version bump patch`` + * ... or: ``bumpversion bump minor`` +* Tag it: ``git tag -a v{new_version} -m "Version {new_version}"`` * Push it: ``git push --tags`` See the bumpversion_ documentation for details. @@ -67,7 +81,9 @@ See the bumpversion_ documentation for details. Build a source distribution and wheel ===================================== -To build a source distribution (`.sdist`) and wheel (`.whl`) locally, run the following command:: +To build a source distribution (`.sdist`) and wheel (`.whl`) locally, run the following command: + +.. code-block:: console $ python -m build diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 3588197..eac6c44 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -7,7 +7,7 @@ Installation Install from PyPI ================= -|pypi| +|pypi| |pypi downloads| .. code-block:: console @@ -16,7 +16,7 @@ Install from PyPI Install from Anaconda ===================== -|conda install| |conda version| |conda downloads| +|conda version| |conda downloads| .. code-block:: console @@ -32,16 +32,26 @@ Check out code from the birdy GitHub repo and start the installation: $ git clone https://github.com/bird-house/birdy.git $ cd birdy $ conda env create -f environment.yml - $ python setup.py install +For a non-interactive installation: + +.. code-block:: console + + $ python -m pip install . + +Or, for an editable (development) installation: + +.. code-block:: console + + $ python -m pip install --editable . .. |pypi| image:: https://img.shields.io/pypi/v/birdhouse-birdy.svg :target: https://pypi.python.org/pypi/birdhouse-birdy - :alt: Python Package Index Build + :alt: Python Package Index Version -.. |conda install| image:: https://anaconda.org/conda-forge/birdy/badges/installer/conda.svg - :target: https://anaconda.org/conda-forge/birdy - :alt: Anaconda Install +.. |pypi downloads| image:: https://img.shields.io/pypi/dm/birdhouse-birdy + :target: https://pypi.python.org/pypi/birdhouse-birdy + :alt: Python Package Index Downloads .. |conda version| image:: https://anaconda.org/conda-forge/birdy/badges/version.svg :target: https://anaconda.org/conda-forge/birdy