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/.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" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bdb8f5c..19b5e9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,11 +16,17 @@ jobs: strategy: matrix: python-version: - - "3.9" + - "3.x" steps: - - uses: actions/checkout@v4 + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - 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 @@ -38,23 +44,23 @@ jobs: python-version: - "3.9" steps: - - uses: actions/checkout@v4 + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - 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 @@ -70,26 +76,22 @@ 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 + - 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28e4396..50f1b5a 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,15 +42,14 @@ 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' ] - - repo: https://github.com/pycqa/pydocstyle - rev: 6.3.0 + additional_dependencies: [ 'isort==6.0.1' ] + - repo: https://github.com/numpy/numpydoc + rev: v1.8.0 hooks: - - id: pydocstyle - args: [ '--convention=numpy' ] + - id: numpydoc-validation - repo: meta hooks: - id: check-hooks-apply 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 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 `_ 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/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/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 022a5a3..3087690 100644 --- a/birdy/cli/base.py +++ b/birdy/cli/base.py @@ -19,18 +19,23 @@ 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. 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..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,8 +71,23 @@ 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: - 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/__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 410b7d5..9e83422 100644 --- a/birdy/client/base.py +++ b/birdy/client/base.py @@ -1,9 +1,8 @@ -# noqa: D100 - import logging import types from collections import OrderedDict from textwrap import dedent +from typing import Callable from warnings import warn import owslib @@ -17,7 +16,9 @@ SYNC, WPS_DEFAULT_VERSION, ComplexData, + Input, WebProcessingService, + WPSExecution, ) from birdy.client import notebook, utils @@ -28,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 -------- @@ -56,40 +94,7 @@ def __init__( lineage=False, **kwds, ): - """Initialize WPSClient. - - 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` - 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 @@ -227,20 +232,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 +409,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,8 +443,9 @@ def sigint_handler(signum, frame): self.logger.info(f"{execution.process.identifier} failed.") -def sort_inputs_key(i): - """Key function for sorting process inputs. +def sort_inputs_key(i: Input): + """ + Key function for sorting process inputs. The order is: - Inputs that have minOccurs >= 1 and no default value @@ -448,8 +454,8 @@ def sort_inputs_key(i): Parameters ---------- - i: owslib.wps.Input - An owslib Input + i : owslib.wps.Input + An owslib Input. Notes ----- @@ -464,8 +470,17 @@ def sort_inputs_key(i): 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 9d4a9c7..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 Union +from typing import Optional, Union from owslib.wps import Output from packaging.version import Version @@ -20,13 +18,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 +42,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 +63,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 +81,7 @@ def _check_import(self, name, package=None): def convert(self): """To be subclassed.""" - raise NotImplementedError + raise NotImplementedError() class GenericConverter(BaseConverter): # noqa: D101 @@ -307,8 +310,24 @@ def _find_converter(mimetype=None, extension=None, converters=()): return select -def find_converter(obj, 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:] @@ -316,7 +335,7 @@ def find_converter(obj, converters): mimetype = None extension = Path(obj).suffix[1:] else: - raise NotImplementedError + raise NotImplementedError() return _find_converter(mimetype, extension, converters=converters) @@ -327,23 +346,24 @@ def convert( converters: Sequence[BaseConverter] = None, verify: bool = True, ): - """Convert a file to an object. + """ + Convert a file to an object. 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..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 @@ -10,11 +8,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 @@ -31,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: @@ -41,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 @@ -83,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. @@ -96,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( @@ -125,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( @@ -137,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()) @@ -166,15 +246,16 @@ def build_ui(self, input_widgets, of_widgets, go): return ui -def monitor(execution, sleep=3): - """Monitor the execution of a process using a notebook progress bar widget. +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, @@ -193,21 +274,21 @@ def monitor(execution, sleep=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 @@ -221,14 +302,26 @@ 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): - """Return a Notebook widget to enter values for the input.""" +def input2widget(inpt: 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 + raise ValueError() typ = inpt.dataType opt = inpt.allowedValues @@ -288,5 +381,12 @@ def input2widget(inpt): 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 8eea5a7..639ecae 100644 --- a/birdy/client/outputs.py +++ b/birdy/client/outputs.py @@ -1,9 +1,8 @@ -# noqa: D100 - 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 +11,29 @@ class WPSResult(WPSExecution): # noqa: D101 - def attach(self, wps_outputs, converters=None): - """Attach the outputs according to converters. + 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): - """Return the process response outputs. + 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. Default is False. """ if not self.isComplete(): raise ProcessIsNotComplete("Please wait ...") @@ -41,23 +43,27 @@ 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..b98a93d 100644 --- a/birdy/client/utils.py +++ b/birdy/client/utils.py @@ -2,24 +2,39 @@ 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): - """Filter a sequence of process names into a `known` and `unknown` 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. + + 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 = 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 +45,16 @@ def filter_case_insensitive(names, complete_list): return contained, missing -def pretty_repr(obj, linebreaks=True): - """Output pretty repr for an Output. +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,18 +85,23 @@ def pretty_repr(obj, linebreaks=True): return joiner.join([class_name + "(", attributes, ")"]) -def build_wps_client_doc(wps, processes): - """Create WPSClient docstring. +def build_wps_client_doc( + wps: WebProcessingService, processes: dict[str, Process] +) -> str: + """ + 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", "---------", ""] @@ -99,8 +121,20 @@ def build_wps_client_doc(wps, processes): return "\n".join(doc) -def build_process_doc(process): - """Create docstring from process metadata.""" +def build_process_doc(process: Process) -> str: + """ + 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 @@ -126,8 +160,20 @@ def build_process_doc(process): return "\n".join(doc) -def format_type(obj): - """Create docstring entry for input parameter from an OWSlib object.""" +def format_type(obj: Any) -> str: + """ + 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 = "" @@ -169,15 +215,26 @@ 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. + + Parameters + ---------- + url : str + URL to the WPS server. + value : Any + Value to be sent to the WPS server. - 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,8 +263,34 @@ def is_embedded_in_request(url, value): return False -def to_owslib(value, data_type, encoding=None, mimetype=None, schema=None): - """Convert value into OWSlib objects.""" +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. + + 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": @@ -221,8 +304,22 @@ def to_owslib(value, data_type, encoding=None, mimetype=None, schema=None): return str(value) -def from_owslib(value, data_type): - """Convert a string into another data type.""" +def from_owslib(value: Any, data_type: str) -> Any: + """ + 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 @@ -250,8 +347,20 @@ def from_owslib(value, data_type): return value -def py_type(data_type): - """Return the python data type matching the WPS dataType.""" +def py_type(data_type: str) -> Any: + """ + 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: @@ -276,27 +385,42 @@ def py_type(data_type): 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), {}) -def add_output_format(output_dictionary, output_identifier, as_ref=None, mimetype=None): - """Add an output format to an already existing dictionary. +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 - The dictionary (created with create_output_dictionary()) to which this + 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 +430,29 @@ def add_output_format(output_dictionary, output_identifier, as_ref=None, mimetyp } -def create_output_dictionary(output_identifier, as_ref=None, mimetype=None): - """Create an output format dictionary. +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..5efce44 100644 --- a/birdy/dependencies.py +++ b/birdy/dependencies.py @@ -1,11 +1,12 @@ -# noqa: D205,D400 """ Dependencies Module =================== Module for managing optional dependencies. -Example usage:: +Example usage: + +.. code-block:: python >>> from birdy.dependencies import ipywidgets as widgets """ 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 66ffc2d..8c415fa 100644 --- a/birdy/ipyleafletwfs/__init__.py +++ b/birdy/ipyleafletwfs/__init__.py @@ -1,4 +1,3 @@ -# noqa: D205,D400 """ IpyleafletWFS Module ==================== @@ -10,7 +9,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 +25,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..1799da1 100644 --- a/birdy/ipyleafletwfs/base.py +++ b/birdy/ipyleafletwfs/base.py @@ -1,6 +1,5 @@ -# noqa: D100 - import json +from typing import Any, Optional from owslib.wfs import WebFeatureService @@ -16,15 +15,16 @@ 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. 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 ------- @@ -42,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. @@ -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,9 +100,10 @@ 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. + """ + Return an ipyleaflet GeoJSON layer from a geojson wfs request. Requires the WFS service to be capable of geojson output. @@ -109,24 +111,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 - The map instance on which the layer is to be added. - - 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. - - 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. - + 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 : dict + 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. """ # Check if dependency is installed if ipyl is None: @@ -178,8 +175,11 @@ def build_layer( # Create refresh button self._create_refresh_widget() - def create_wfsgeojson_layer(self, layer_typename, source_map, layer_style=None): - """Create a static ipyleaflett GeoJSON layer from a WFS service. + def create_wfsgeojson_layer( + self, layer_typename: str, source_map, layer_style: Optional[dict] = None + ): + """ + Create a static ipyleaflet GeoJSON layer from a WFS service. Simple wrapper for a WFS => GeoJSON layer, using owslib. @@ -191,23 +191,21 @@ 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(). - 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. + 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 + 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: @@ -232,15 +230,16 @@ def create_wfsgeojson_layer(self, layer_typename, source_map, layer_style=None): return layer - def _refresh_layer(self, placeholder=None): - """Refresh the wfs layer for the current map extent. + 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 +277,23 @@ def remove_layer(self): # Layer information functions # # # # # # # # # # # # # # # # # - def feature_properties_by_id(self, feature_id): - """Return the properties of a feature. + 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,27 +311,29 @@ def geojson(self): return self._geojson @property - def layer_list(self): - """Return a simple layer list available to the WFS service. + 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): - """Return a list containing the properties of the first feature. + def property_list(self) -> dict: + """ + Return a list containing the properties of the first feature. Retrieves the available properties for use subsequent use by the feature property widget. Returns ------- - Dict - A dictionary of the layer properties. + dict + A dictionary of the layer properties. """ return self._geojson["features"][0]["properties"] @@ -373,15 +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. - - This function will remove the property widgets from a given map, without - affecting other widgets. + """ + Remove all property widgets from a map. - Parameters - ---------- - src_map: Map instance - The map instance from which the widgets are to be removed. + 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: @@ -391,9 +387,13 @@ 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. + """ + 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. @@ -402,16 +402,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..ba84ac3 100644 --- a/birdy/utils.py +++ b/birdy/utils.py @@ -1,10 +1,9 @@ -# noqa: D100 - import base64 import collections 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,13 +26,37 @@ DEFAULT_ENCODING = "utf-8" -def fix_url(url): - """If url is a local path, add a file:// scheme.""" +def fix_url(url: str) -> str: + """ + 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): - """Return whether value is a valid URL.""" +def is_url(url: Optional[str]) -> bool: + """ + 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) @@ -43,8 +66,9 @@ def is_url(url): return True -def is_opendap_url(url): - """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: @@ -52,7 +76,19 @@ def is_opendap_url(url): 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 @@ -70,25 +106,48 @@ def is_opendap_url(url): return False -def is_file(path): - """Return True if `path` is a valid file.""" +def is_file(path: Optional[str]) -> bool: + """ + 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: - 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): - """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): @@ -96,8 +155,20 @@ def sanitize(name): return new_name -def delist(data): - """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) @@ -107,13 +178,25 @@ def delist(data): return data -def embed(value, mimetype=None, encoding=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 +) -> 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. + + 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" @@ -156,21 +239,25 @@ def _encode(content, mimetype, encoding): # return u''.format(content) -def guess_type(url, supported): - """Guess the mime type of the file link. +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 + tuple + Mimetype and encoding. """ import mimetypes @@ -201,7 +288,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/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 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..bce9608 100644 --- a/environment.yml +++ b/environment.yml @@ -2,30 +2,47 @@ 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 + - urllib3 >=2.0.2 + - 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..f160293 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ -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 +urllib3 >=2.0.2 +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_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/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 diff --git a/setup.cfg b/setup.cfg index 5f72e76..eefc511 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 @@ -43,3 +26,8 @@ exclude = [doc8] ignore-path = docs/build,docs/source/_templates,docs/source/_static max-line-length = 120 + +[tool:numpydoc_validation] +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 6bb0fe8..1af64b5 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): @@ -33,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 = [ @@ -46,6 +59,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", ] @@ -72,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"]}, 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 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")