├── landlab_rest
├── api
│ ├── __init__.py
│ └── graphs.py
├── _version.py
├── __init__.py
├── cli.py
├── start.py
└── app.py
├── news
├── .gitignore
├── 12.misc.1
├── 15.docs
├── 13.misc
├── 12.misc
├── 12.misc.2
├── 12.misc.3
├── 11.bugfix
├── 14.misc
└── 10.feature
├── .gitattributes
├── setup.py
├── MANIFEST.in
├── env.yaml
├── docs
├── source
│ ├── _templates
│ │ ├── sidebarintro.html
│ │ └── links.html
│ ├── _static
│ │ └── powered-by-logo-header.png
│ ├── environment.yml
│ ├── index.rst
│ └── conf.py
└── Makefile
├── requirements.txt
├── tests
├── conftest.py
├── test_cli.py
├── test_response.py
├── test_radial.py
├── test_hex.py
└── test_raster.py
├── AUTHORS.rst
├── setup.cfg
├── .github
└── workflows
│ ├── black.yml
│ ├── flake8.yml
│ └── test.yml
├── Dockerfile
├── LICENSE.rst
├── .gitignore
├── CHANGES.rst
├── README.rst
├── requirements.py
├── Makefile
└── pyproject.toml
/landlab_rest/api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/news/.gitignore:
--------------------------------------------------------------------------------
1 | !.gitignore
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | landlab_rest/_version.py export-subst
2 |
--------------------------------------------------------------------------------
/landlab_rest/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.2.1.dev0"
2 |
--------------------------------------------------------------------------------
/news/12.misc.1:
--------------------------------------------------------------------------------
1 | Fixed unit tests for *landlab* v2.
2 |
3 |
--------------------------------------------------------------------------------
/news/15.docs:
--------------------------------------------------------------------------------
1 | Added badges to the README file.
2 |
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup()
4 |
--------------------------------------------------------------------------------
/news/13.misc:
--------------------------------------------------------------------------------
1 | Updated the Dockerfile to use micromamba
2 |
3 |
--------------------------------------------------------------------------------
/news/12.misc:
--------------------------------------------------------------------------------
1 | Move package metadata into *pyproject.toml*.
2 |
3 |
--------------------------------------------------------------------------------
/news/12.misc.2:
--------------------------------------------------------------------------------
1 | Set up *towncrier* to manage the changelog.
2 |
3 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include versioneer.py
2 | include landlab_rest/_version.py
3 |
--------------------------------------------------------------------------------
/news/12.misc.3:
--------------------------------------------------------------------------------
1 | Use *zest.releaser* for managing releases, drop *versioneer*.
2 |
--------------------------------------------------------------------------------
/news/11.bugfix:
--------------------------------------------------------------------------------
1 | Updated *landlab-rest* to work with, and only with, *landlab* v2.
2 |
3 |
--------------------------------------------------------------------------------
/env.yaml:
--------------------------------------------------------------------------------
1 | name: base
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - python
6 | - landlab
--------------------------------------------------------------------------------
/news/14.misc:
--------------------------------------------------------------------------------
1 | Setup continuous integration to run using GitHub actions instead of Travis-CI.
2 |
3 |
--------------------------------------------------------------------------------
/docs/source/_templates/sidebarintro.html:
--------------------------------------------------------------------------------
1 |
About bmipy
2 |
3 | A RESTful interface to landlab graphs
4 |
5 |
--------------------------------------------------------------------------------
/landlab_rest/__init__.py:
--------------------------------------------------------------------------------
1 | from ._version import __version__
2 | from .app import create_app
3 |
4 | __all__ = ["__version__", "create_app"]
5 |
--------------------------------------------------------------------------------
/docs/source/_static/powered-by-logo-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/landlab/landlab-rest/master/docs/source/_static/powered-by-logo-header.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Requirements extracted from pyproject.toml
2 | # [project.dependencies]
3 | cherrypy
4 | click
5 | flask
6 | flask-cors
7 | landlab >= 2
8 |
--------------------------------------------------------------------------------
/news/10.feature:
--------------------------------------------------------------------------------
1 | Added commandline options for specifying ssl parameters. New options are
2 | ``--ssl-cert``, ``--ssl-key``, and ``--ssl-chain`` that give paths to various
3 | ssl files.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from landlab_rest import create_app
4 |
5 | app = create_app()
6 |
7 |
8 | @pytest.fixture
9 | def client():
10 | with app.test_client() as c:
11 | yield c
12 |
--------------------------------------------------------------------------------
/AUTHORS.rst:
--------------------------------------------------------------------------------
1 | Credits
2 | =======
3 |
4 | Development Lead
5 | ----------------
6 |
7 | * Eric Hutton (@mcflugen)
8 |
9 | Contributors
10 | ------------
11 |
12 | * Jenny Knuth
13 | * Mark Piper
14 | * Greg Tucker
15 |
16 |
--------------------------------------------------------------------------------
/docs/source/environment.yml:
--------------------------------------------------------------------------------
1 | name: landlab_rest_docs
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - python==3.6
6 | - pandoc
7 | - pip
8 | - sphinx>=1.5.1
9 | - sphinx_rtd_theme
10 | - tornado
11 | - entrypoints
12 | - pip:
13 | - sphinxcontrib_github_alt
14 |
--------------------------------------------------------------------------------
/docs/source/_templates/links.html:
--------------------------------------------------------------------------------
1 | Useful Links
2 |
7 |
8 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
3 |
4 | [flake8]
5 | exclude = docs
6 | ignore =
7 | E203
8 | E501
9 | W503
10 | max-line-length = 88
11 |
12 | [aliases]
13 | test = pytest
14 |
15 | [zest.releaser]
16 | tag-format = v{version}
17 | python-file-with-version = sequence/_version.py
18 |
19 | [coverage:run]
20 | relative_files = True
21 |
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | from click.testing import CliRunner
2 |
3 | from landlab_rest.cli import main
4 |
5 |
6 | def test_cli_version():
7 | runner = CliRunner()
8 | result = runner.invoke(main, ["--version"])
9 | assert result.exit_code == 0
10 | assert "version" in result.output
11 |
12 |
13 | def test_cli_help():
14 | runner = CliRunner()
15 | result = runner.invoke(main, ["--help"])
16 | assert result.exit_code == 0
17 | assert "help" in result.output
18 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. landlab_rest documentation master file, created by
2 | sphinx-quickstart on Wed Apr 17 14:11:40 2019.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to landlab_rest's documentation!
7 | ========================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 |
14 |
15 | Indices and tables
16 | ==================
17 |
18 | * :ref:`genindex`
19 | * :ref:`modindex`
20 | * :ref:`search`
21 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SOURCEDIR = source
8 | BUILDDIR = build
9 |
10 | # Put it first so that "make" without argument is like "make help".
11 | help:
12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
13 |
14 | .PHONY: help Makefile
15 |
16 | # Catch-all target: route all unknown targets to Sphinx using the new
17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
18 | %: Makefile
19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/.github/workflows/black.yml:
--------------------------------------------------------------------------------
1 | name: Black
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 |
7 | lint:
8 | name: Check code format
9 | # We want to run on external PRs, but not on our own internal PRs as they'll be run
10 | # by the push to the branch. Without this if check, checks are duplicated since
11 | # internal PRs match both the push and pull_request events.
12 | if:
13 | github.event_name == 'push' || github.event.pull_request.head.repo.full_name !=
14 | github.repository
15 |
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | - uses: actions/setup-python@v2
20 | - uses: psf/black@stable
21 | with:
22 | options: "--check --verbose --diff"
23 | src: "setup.py landlab_rest tests"
24 |
--------------------------------------------------------------------------------
/.github/workflows/flake8.yml:
--------------------------------------------------------------------------------
1 | name: Flake8
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 |
7 | lint:
8 | name: Check for lint
9 | # We want to run on external PRs, but not on our own internal PRs as they'll be run
10 | # by the push to the branch. Without this if check, checks are duplicated since
11 | # internal PRs match both the push and pull_request events.
12 | if:
13 | github.event_name == 'push' || github.event.pull_request.head.repo.full_name !=
14 | github.repository
15 |
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up Python 3.8
20 | uses: actions/setup-python@v2
21 | with:
22 | python-version: 3.8
23 |
24 | - name: Lint
25 | run: |
26 | pip install flake8
27 | flake8 landlab_rest tests
28 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mambaorg/micromamba:0.23.0
2 |
3 | ARG MAMBA_USER=mambauser
4 | ARG MAMBA_USER_ID=1000
5 | ARG MAMBA_USER_GID=1000
6 | ENV MAMBA_USER=$MAMBA_USER
7 |
8 | USER root
9 |
10 | COPY --chown=$MAMBA_USER:$MAMBA_USER env.yaml /tmp/env.yaml
11 | RUN micromamba install -y -f /tmp/env.yaml && micromamba clean --all --yes
12 | ARG MAMBA_DOCKERFILE_ACTIVATE=1
13 |
14 | # File Author / Maintainer
15 | MAINTAINER Eric Hutton
16 |
17 | # RUN export PATH=/usr/local/python/bin:$PATH
18 |
19 | # install landlab-rest package
20 | ADD . /landlab-rest
21 | RUN pip install /landlab-rest
22 | RUN cd /landlab-rest/grid-sketchbook-master
23 |
24 | # Expose ports
25 | EXPOSE 80
26 |
27 | # Set the default directory where CMD will execute
28 | WORKDIR /landlab-rest
29 |
30 | # Set the default command to execute
31 | # when creating a new container
32 | CMD start-sketchbook
33 |
--------------------------------------------------------------------------------
/landlab_rest/cli.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import click
3 |
4 | from .start import start
5 |
6 |
7 | @click.command()
8 | @click.version_option(prog_name="landlab-sketchbook")
9 | @click.option("-p", "--port", default=80, help="port to run on")
10 | @click.option("--host", default="0.0.0.0", help="host IP address")
11 | @click.option("--ssl-cert", default=None, help="path to host SSL certificate")
12 | @click.option("--ssl-key", default=None, help="path to host SSL key")
13 | @click.option("--ssl-chain", default=None, help="path to host SSL certificate chain")
14 | @click.option("--silent", is_flag=True, help="only emit messages on error")
15 | def main(host, port, ssl_cert, ssl_key, ssl_chain, silent):
16 | if not silent:
17 | click.secho("🚀 launching landlab sketchbook on {0}:{1}".format(host, port))
18 | start(host, port, ssl_cert, ssl_key, ssl_chain)
19 |
--------------------------------------------------------------------------------
/landlab_rest/start.py:
--------------------------------------------------------------------------------
1 | import cherrypy
2 | from landlab_rest import create_app
3 |
4 | app = create_app()
5 |
6 |
7 | def start(host, port, ssl_cert, ssl_key, ssl_chain):
8 |
9 | # Mount the application
10 | cherrypy.tree.graft(app, "/")
11 |
12 | # Unsubscribe the default server
13 | cherrypy.server.unsubscribe()
14 |
15 | # Instantiate a new server object
16 | server = cherrypy._cpserver.Server()
17 |
18 | # Configure the server object
19 | server.socket_host = host
20 | server.socket_port = port
21 | server.thread_pool = 30
22 |
23 | # For SSL Support
24 | if ssl_cert is not None and ssl_key is not None:
25 | server.ssl_module = "builtin"
26 | server.ssl_certificate = ssl_cert
27 | server.ssl_private_key = ssl_key
28 | server.ssl_certificate_chain = ssl_chain
29 |
30 | # Subscribe this server
31 | server.subscribe()
32 |
33 | # Start the server engine (Option 1 *and* 2)
34 |
35 | cherrypy.engine.start()
36 | cherrypy.engine.block()
37 |
--------------------------------------------------------------------------------
/landlab_rest/app.py:
--------------------------------------------------------------------------------
1 | import importlib
2 |
3 | from flask_cors import CORS
4 |
5 | from flask import Blueprint, Flask, jsonify, url_for
6 |
7 |
8 | def register_blueprints(app):
9 | rv = []
10 |
11 | for name in ["graphs"]:
12 | m = importlib.import_module(".api.{bp}".format(bp=name), package="landlab_rest")
13 | for item in dir(m):
14 | item = getattr(m, item)
15 | if isinstance(item, Blueprint):
16 | app.register_blueprint(item, url_prefix="/" + item.name)
17 | rv.append(item)
18 |
19 | return rv
20 |
21 |
22 | def create_app():
23 | app = Flask(__name__, instance_relative_config=True)
24 | CORS(app)
25 |
26 | @app.route("/")
27 | def site_map():
28 | COLLECTIONS = ["graphs"]
29 |
30 | map = {"@type": "api", "href": url_for(".site_map")}
31 | links = []
32 | for rel in COLLECTIONS:
33 | href = url_for(".".join([rel, "show"]))
34 | links.append({"rel": rel, "href": href})
35 | map["links"] = links
36 | return jsonify(map)
37 |
38 | register_blueprints(app)
39 |
40 | return app
41 |
--------------------------------------------------------------------------------
/LICENSE.rst:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright © `2022` `Landlab`
5 |
6 | Permission is hereby granted, free of charge, to any person
7 | obtaining a copy of this software and associated documentation
8 | files (the "Software"), to deal in the Software without
9 | restriction, including without limitation the rights to use,
10 | copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the
12 | Software is furnished to do so, subject to the following
13 | conditions:
14 |
15 | The above copyright notice and this permission notice shall be
16 | included in all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
--------------------------------------------------------------------------------
/tests/test_response.py:
--------------------------------------------------------------------------------
1 | import urllib
2 |
3 | import pytest
4 |
5 |
6 | @pytest.mark.parametrize("graph_type", ("hex", "radial", "raster"))
7 | def test_response_status(client, graph_type):
8 | response = client.get("/graphs/{0}".format(graph_type))
9 | assert response.status_code == 200
10 |
11 |
12 | @pytest.mark.parametrize("graph_type", ("hex", "radial", "raster"))
13 | def test_response_type(client, graph_type):
14 | response = client.get("/graphs/{0}".format(graph_type))
15 | data = response.get_json()
16 | assert data["_type"] == "graph"
17 | assert "graph" in data
18 |
19 |
20 | @pytest.mark.parametrize("graph_type", ("hex", "radial", "raster"))
21 | def test_response_href(client, graph_type):
22 | response = client.get("/graphs/{0}".format(graph_type))
23 | data = response.get_json()
24 |
25 | parts = urllib.parse.urlsplit(data["href"])
26 | assert parts[0] == ""
27 | assert parts[1] == ""
28 | assert parts[2] == "/graphs/{0}".format(graph_type)
29 | assert urllib.parse.parse_qs(parts[3])
30 | assert parts[4] == ""
31 |
32 |
33 | @pytest.mark.parametrize(
34 | "graph_type,expected",
35 | (
36 | ("hex", "DualHexGraph"),
37 | ("radial", "DualRadialGraph"),
38 | ("raster", "DualUniformRectilinearGraph"),
39 | ),
40 | )
41 | def test_response_repr(client, graph_type, expected):
42 | response = client.get("/graphs/{0}".format(graph_type))
43 | data = response.get_json()
44 |
45 | assert data["repr"].startswith(expected)
46 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changelog for landlab-rest
2 | ==========================
3 |
4 | .. towncrier release notes start
5 |
6 | 0.2.0 (2019-04-26)
7 | ------------------
8 |
9 | New Features
10 | ````````````
11 |
12 | - Use *flask-cors* to allow API calls from other servers. (`#8 `_)
13 |
14 |
15 | 0.1.1 (2019-04-18)
16 | ------------------
17 |
18 | Bug Fixes
19 | `````````
20 |
21 | - Fixed a bug in the startup script. (`#7 `_)
22 |
23 |
24 | 0.1.0 (2019-04-18)
25 | ------------------
26 |
27 | New Features
28 | ````````````
29 |
30 | - Added the ``start-sketchbook`` command to start the service. (`#6 `_)
31 |
32 |
33 | Documentation Enhancements
34 | ``````````````````````````
35 |
36 | - Added documentation for building and running *landlab-rest* as a service within a docker
37 | container. (`#1 `_)
38 |
39 |
40 | Other Changes and Additions
41 | ```````````````````````````
42 |
43 | - Added a docker file to create an image able to run the *landlab-rest* service. (`#1 `_)
44 | - Removed Python 2 compatibility. *landlab-rest* is now Python 3 only. (`#4 `_)
45 | - Set up continuous integration on Travis.org. (`#5 `_)
46 | - Added unit tests. (`#5 `_)
47 | - Added smoke tests for the command line interface, ``start-sketchbook``. (`#7 `_)
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build-and-test:
7 | name: Run the tests
8 | # We want to run on external PRs, but not on our own internal PRs as they'll be run
9 | # by the push to the branch. Without this if check, checks are duplicated since
10 | # internal PRs match both the push and pull_request events.
11 | if:
12 | github.event_name == 'push' || github.event.pull_request.head.repo.full_name !=
13 | github.repository
14 |
15 | runs-on: ${{ matrix.os }}
16 |
17 | defaults:
18 | run:
19 | shell: bash -l {0}
20 |
21 | strategy:
22 | matrix:
23 | os: [ubuntu-latest, macos-latest, windows-latest]
24 | python-version: ["3.8", "3.9", "3.10"]
25 |
26 | steps:
27 | - uses: actions/checkout@v2
28 |
29 | - uses: conda-incubator/setup-miniconda@v2
30 | with:
31 | auto-update-conda: true
32 | python-version: ${{ matrix.python-version }}
33 | channels: conda-forge
34 | channel-priority: true
35 |
36 | - name: Show conda installation info
37 | run: |
38 | conda info
39 | conda list
40 |
41 | - name: Install dependencies
42 | run: |
43 | conda install mamba -c conda-forge
44 | mamba install --file=requirements.txt
45 |
46 | - name: Build and install package
47 | run: |
48 | pip install -e .
49 |
50 | - name: Install testing requirements
51 | run: pip install -e .[dev]
52 |
53 | - name: Test
54 | run: |
55 | python -c 'import landlab_rest; print(landlab_rest.__version__)'
56 | pytest --cov=landlab_rest --cov-report=xml:$(pwd)/coverage.xml -vvv
57 |
58 | - name: Coveralls
59 | if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9'
60 | uses: AndreMiras/coveralls-python-action@v20201129
61 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://github.com/landlab/landlab-rest/actions/workflows/test.yml/badge.svg
2 | :target: https://github.com/landlab/landlab-rest/actions/workflows/test.yml
3 |
4 | .. image:: https://github.com/landlab/landlab-rest/actions/workflows/flake8.yml/badge.svg
5 | :target: https://github.com/landlab/landlab-rest/actions/workflows/flake8.yml
6 |
7 | .. image:: https://github.com/landlab/landlab-rest/actions/workflows/black.yml/badge.svg
8 | :target: https://github.com/landlab/landlab-rest/actions/workflows/black.yml
9 |
10 | landlab REST
11 | ============
12 |
13 | A RESTful interface to landlab graphs.
14 |
15 | Quickstart
16 | ----------
17 |
18 | Use `conda` to install the necessary requirements and `landlab_rest`,
19 |
20 | .. code::
21 |
22 | $ conda install --file=requirements.txt -c conda-forge
23 | $ pip install .
24 |
25 | Start the server,
26 |
27 | .. code::
28 |
29 | $ start-sketchbook
30 |
31 | Look at the line containing `Serving on` to see what host and port the
32 | server is running on. Alternatively, you can use the `--host` and `--port`
33 | options to specify a specific host and port (`--help` for help).
34 |
35 | Now you should be able to send requests to the server. For instance,
36 | to get a `RasterModelGrid`,
37 |
38 | .. code::
39 |
40 | $ curl https://0.0.0.0:8080/graphs/raster
41 |
42 | For a list of supported graphs
43 |
44 | .. code::
45 |
46 | $ curl https://0.0.0.0:8080/graphs/
47 |
48 | You can pass parameters like,
49 |
50 | .. code::
51 |
52 | $ curl 'https://0.0.0.0:8080/graphs/raster?shape=4,5&spacing=2.,1.'
53 |
54 |
55 | Docker
56 | ------
57 |
58 | To build a new docker image that will be a landlab-rest server,
59 |
60 | .. code::
61 |
62 | docker build . -t landlab-rest
63 |
64 |
65 | After building, run the server,
66 |
67 | .. code::
68 |
69 | docker run -it -p 80:80 landlab-rest
70 |
71 | Once running, you can then send requests to the server. For example,
72 |
73 | .. code::
74 |
75 | $ curl https://0.0.0.0/graphs/raster
76 |
--------------------------------------------------------------------------------
/requirements.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | import argparse
3 | import os
4 |
5 |
6 | def requirements(extras, include_required=False):
7 |
8 | sections = Requirements().get_sections(
9 | extras, include_required=not extras or include_required
10 | )
11 |
12 | print("# Requirements extracted from pyproject.toml")
13 | for section, packages in sections.items():
14 | print(f"# {section}")
15 | print(os.linesep.join(sorted(packages)))
16 |
17 |
18 | class Requirements:
19 | def __init__(self):
20 | tomllib = _find_tomllib()
21 |
22 | with open("pyproject.toml", "rb") as fp:
23 | project = tomllib.load(fp)["project"]
24 | self._dependencies = Requirements._clean_dependency_names(
25 | project.get("dependencies", [])
26 | )
27 | self._optional_dependencies = {
28 | name: Requirements._clean_dependency_names(packages)
29 | for name, packages in project.get("optional-dependencies", {}).items()
30 | }
31 |
32 | @staticmethod
33 | def _clean_dependency_names(packages):
34 | return [package.split(";")[0].strip() for package in packages]
35 |
36 | @property
37 | def required(self):
38 | return tuple(self._dependencies)
39 |
40 | def get_optional(self, extra):
41 | return tuple(self._optional_dependencies[extra])
42 |
43 | def get_sections(self, sections=None, include_required=False):
44 | sections = sections or []
45 | reqs = {}
46 | if include_required:
47 | reqs["[project.dependencies]"] = self.required
48 | for section in sections:
49 | reqs[f"[project.optional-dependencies] {section}"] = self.get_optional(
50 | section
51 | )
52 | return reqs
53 |
54 |
55 | def _find_tomllib():
56 | try:
57 | import tomllib
58 | except ModuleNotFoundError:
59 | import tomli as tomllib
60 | return tomllib
61 |
62 |
63 | if __name__ == "__main__":
64 | parser = argparse.ArgumentParser(
65 | description="Extract requirements information from pyproject.toml"
66 | )
67 | parser.add_argument("extras", type=str, nargs="*")
68 | args = parser.parse_args()
69 |
70 | requirements(args.extras)
71 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean clean-test clean-pyc clean-build docs help
2 | .DEFAULT_GOAL := help
3 |
4 | define BROWSER_PYSCRIPT
5 | import os, webbrowser, sys
6 |
7 | try:
8 | from urllib import pathname2url
9 | except:
10 | from urllib.request import pathname2url
11 |
12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
13 | endef
14 | export BROWSER_PYSCRIPT
15 |
16 | define PRINT_HELP_PYSCRIPT
17 | import re, sys
18 |
19 | for line in sys.stdin:
20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
21 | if match:
22 | target, help = match.groups()
23 | print("%-20s %s" % (target, help))
24 | endef
25 | export PRINT_HELP_PYSCRIPT
26 |
27 | BROWSER := python -c "$$BROWSER_PYSCRIPT"
28 |
29 | help:
30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
31 |
32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
33 |
34 | clean-build: ## remove build artifacts
35 | rm -fr build/
36 | rm -fr dist/
37 | rm -fr .eggs/
38 | find . -name '*.egg-info' -exec rm -fr {} +
39 | find . -name '*.egg' -exec rm -f {} +
40 |
41 | clean-pyc: ## remove Python file artifacts
42 | find . -name '*.pyc' -exec rm -f {} +
43 | find . -name '*.pyo' -exec rm -f {} +
44 | find . -name '*~' -exec rm -f {} +
45 | find . -name '__pycache__' -exec rm -fr {} +
46 |
47 | clean-test: ## remove test and coverage artifacts
48 | rm -fr .tox/
49 | rm -f .coverage
50 | rm -fr htmlcov/
51 | rm -fr .pytest_cache
52 |
53 | lint: ## check style with flake8
54 | flake8 landlab_rest tests
55 |
56 | pretty:
57 | find landlab_rest -name '*.py' | xargs isort
58 | find tests -name '*.py' | xargs isort
59 | black setup.py tests landlab_rest
60 |
61 | test: ## run tests quickly with the default Python
62 | pytest
63 |
64 | test-all: ## run tests on every Python version with tox
65 | tox
66 |
67 | coverage: ## check code coverage quickly with the default Python
68 | coverage run --source landlab_rest -m pytest
69 | coverage report -m
70 | coverage html
71 | $(BROWSER) htmlcov/index.html
72 |
73 | docs: ## generate Sphinx HTML documentation, including API docs
74 | rm -f docs/source/landlab_rest.rst
75 | rm -f docs/source/modules.rst
76 | sphinx-apidoc -o docs/source landlab_rest
77 | $(MAKE) -C docs clean
78 | $(MAKE) -C docs html
79 | $(BROWSER) docs/build/html/index.html
80 |
81 | servedocs: docs ## compile the docs watching for changes
82 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
83 |
84 | release: dist ## package and upload a release
85 | twine upload dist/*
86 |
87 | dist: clean ## builds source and wheel package
88 | python setup.py sdist
89 | python setup.py bdist_wheel
90 | ls -l dist
91 |
92 | install: clean ## install the package to the active Python's site-packages
93 | python setup.py develop
94 |
--------------------------------------------------------------------------------
/tests/test_radial.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | import xarray as xr
4 | from numpy.testing import assert_array_almost_equal
5 |
6 |
7 | def test_radial_default(client):
8 | graph = xr.Dataset.from_dict(client.get("/graphs/radial").get_json()["graph"])
9 |
10 | assert graph.dims == {
11 | "cell": 13,
12 | "corner": 36,
13 | "face": 48,
14 | "link": 60,
15 | "node": 25,
16 | "patch": 36,
17 | "max_cell_faces": 6,
18 | "max_patch_links": 3,
19 | "Two": 2,
20 | }
21 |
22 |
23 | def test_radial_default_shape(client):
24 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/radial").get_json()["graph"])
25 | graph_2 = xr.Dataset.from_dict(
26 | client.get("/graphs/radial?shape=3,4").get_json()["graph"]
27 | )
28 |
29 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
30 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
31 |
32 |
33 | def test_radial_default_spacing(client):
34 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/radial").get_json()["graph"])
35 | graph_2 = xr.Dataset.from_dict(
36 | client.get("/graphs/radial?spacing=1").get_json()["graph"]
37 | )
38 |
39 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
40 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
41 |
42 |
43 | def test_radial_default_origin(client):
44 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/radial").get_json()["graph"])
45 | graph_2 = xr.Dataset.from_dict(
46 | client.get("/graphs/radial?origin=0.0,0.0").get_json()["graph"]
47 | )
48 |
49 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
50 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
51 |
52 |
53 | @pytest.mark.parametrize("spacing", (0.5, 1.0, 2.0, 4.0))
54 | def test_radial_spacing(client, spacing):
55 | url = "/graphs/radial?spacing={0}".format(spacing)
56 | graph = xr.Dataset.from_dict(client.get(url).get_json()["graph"])
57 | unit = xr.Dataset.from_dict(
58 | client.get("/graphs/radial?spacing=1.0").get_json()["graph"]
59 | )
60 |
61 | assert_array_almost_equal(
62 | np.sqrt(graph.x_of_node**2 + graph.y_of_node**2),
63 | np.sqrt(unit.x_of_node**2 + unit.y_of_node**2) * spacing,
64 | 5,
65 | )
66 |
67 |
68 | @pytest.mark.parametrize("y0", (-1.0, 1.0, 2.0, 4.0))
69 | @pytest.mark.parametrize("x0", (-1.0, 1.0, 2.0, 4.0))
70 | def test_radial_origin(client, x0, y0):
71 | url = "/graphs/radial?origin={0},{1}".format(y0, x0)
72 | graph = xr.Dataset.from_dict(client.get(url).get_json()["graph"])
73 | centered = xr.Dataset.from_dict(
74 | client.get("/graphs/radial?origin=0.0,0.0").get_json()["graph"]
75 | )
76 |
77 | assert_array_almost_equal(graph.x_of_node, centered.x_of_node + x0)
78 | assert_array_almost_equal(graph.y_of_node, centered.y_of_node + y0)
79 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["cython", "numpy", "setuptools", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "landlab-rest"
7 | description = "A RESTful interface to landlab graphs."
8 | authors = [
9 | {email = "mcflugen@gmail.com"},
10 | {name = "The landlab team"}
11 | ]
12 | maintainers = [
13 | {email = "mcflugen@gmail.com"},
14 | {name = "The landlab team"}
15 | ]
16 | keywords = [
17 | "bmi",
18 | "component modeling",
19 | "earth science",
20 | "gridding engine",
21 | "model coupling",
22 | "numerical modeling",
23 | ]
24 | license = {file = "LICENSE.rst"}
25 | classifiers = [
26 | "Development Status :: 4 - Beta",
27 | "Intended Audience :: Science/Research",
28 | "License :: OSI Approved :: MIT License",
29 | "Operating System :: OS Independent",
30 | "Programming Language :: Python :: 3.8",
31 | "Programming Language :: Python :: 3.9",
32 | "Programming Language :: Python :: 3.10",
33 | "Programming Language :: Python :: Implementation :: CPython",
34 | "Topic :: Scientific/Engineering :: Physics",
35 | ]
36 | requires-python = ">=3.8"
37 | dependencies = [
38 | "cherrypy",
39 | "click",
40 | "flask",
41 | "flask-cors",
42 | "landlab >= 2",
43 | ]
44 | dynamic = ["readme", "version"]
45 |
46 | [project.urls]
47 | homepage = "https://github.com/landlab"
48 | documentation = "https://github.com/landlab/landlab-rest#readme"
49 | repository = "https://github.com/landlab/landlab-rest"
50 | changelog = "https://github.com/landlab/landlab-rest/blob/develop/CHANGES.rst"
51 |
52 | [project.optional-dependencies]
53 | dev = [
54 | "black",
55 | "coveralls",
56 | "flake8",
57 | "flake8-bugbear",
58 | "isort",
59 | "pre-commit",
60 | "pytest",
61 | "pytest-cov",
62 | "towncrier",
63 | "zest.releaser[recommended]",
64 | ]
65 |
66 | [project.scripts]
67 | start-sketchbook = "landlab_rest.cli:main"
68 |
69 | [tool.setuptools.packages.find]
70 | where = ["."]
71 |
72 | [tool.setuptools.dynamic]
73 | readme = {file = ["README.rst", "AUTHORS.rst", "CHANGES.rst"]}
74 | version = {attr = "landlab_rest._version.__version__"}
75 |
76 | [tool.pytest.ini_options]
77 | minversion = "6.0"
78 | testpaths = ["landlab_rest", "tests"]
79 | norecursedirs = [".*", "*.egg*", "build", "dist", "examples"]
80 | addopts = """
81 | --ignore setup.py
82 | --tb native
83 | --strict
84 | --durations 16
85 | --doctest-modules
86 | -vvv
87 | """
88 | doctest_optionflags = [
89 | "NORMALIZE_WHITESPACE",
90 | "IGNORE_EXCEPTION_DETAIL",
91 | "ALLOW_UNICODE"
92 | ]
93 |
94 | [tool.isort]
95 | multi_line_output = 3
96 | include_trailing_comma = true
97 | force_grid_wrap = 0
98 | combine_as_imports = true
99 | line_length = 88
100 |
101 | [tool.towncrier]
102 | directory = "news"
103 | package = "landlab_rest"
104 | filename = "CHANGES.rst"
105 | single_file = true
106 | underlines = "-`^"
107 | issue_format = "`#{issue} `_"
108 | title_format = "{version} ({project_date})"
109 |
110 | [[tool.towncrier.type]]
111 | directory = "feature"
112 | name = "New Features"
113 | showcontent = true
114 |
115 | [[tool.towncrier.type]]
116 | directory = "bugfix"
117 | name = "Bug Fixes"
118 | showcontent = true
119 |
120 | [[tool.towncrier.type]]
121 | directory = "docs"
122 | name = "Documentation Enhancements"
123 | showcontent = true
124 |
125 | [[tool.towncrier.type]]
126 | directory = "misc"
127 | name = "Other Changes and Additions"
128 | showcontent = true
129 |
--------------------------------------------------------------------------------
/tests/test_hex.py:
--------------------------------------------------------------------------------
1 | import urllib
2 |
3 | import pytest
4 | import xarray as xr
5 | from numpy.testing import assert_array_almost_equal
6 |
7 |
8 | def test_hex_default(client):
9 | graph = xr.Dataset.from_dict(client.get("/graphs/hex").get_json()["graph"])
10 |
11 | assert graph.dims == {
12 | "cell": 4,
13 | "corner": 18,
14 | "face": 21,
15 | "link": 33,
16 | "node": 16,
17 | "patch": 18,
18 | "max_cell_faces": 6,
19 | "max_patch_links": 3,
20 | "Two": 2,
21 | }
22 |
23 |
24 | def test_hex_default_shape(client):
25 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/hex").get_json()["graph"])
26 | graph_2 = xr.Dataset.from_dict(
27 | client.get("/graphs/hex?shape=4,4").get_json()["graph"]
28 | )
29 |
30 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
31 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
32 |
33 |
34 | def test_hex_default_spacing(client):
35 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/hex").get_json()["graph"])
36 | graph_2 = xr.Dataset.from_dict(
37 | client.get("/graphs/hex?spacing=1").get_json()["graph"]
38 | )
39 |
40 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
41 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
42 |
43 |
44 | def test_hex_default_origin(client):
45 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/hex").get_json()["graph"])
46 | graph_2 = xr.Dataset.from_dict(
47 | client.get("/graphs/hex?origin=0.0,0.0").get_json()["graph"]
48 | )
49 |
50 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
51 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
52 |
53 |
54 | @pytest.mark.parametrize("node_layout", ("rect", "hex"))
55 | @pytest.mark.parametrize("orientation", ("horizontal", "vertical"))
56 | @pytest.mark.parametrize("y0", (-1.0, 1.0, 2.0, 4.0))
57 | @pytest.mark.parametrize("x0", (-1.0, 1.0, 2.0, 4.0))
58 | def test_hex_origin(client, x0, y0, orientation, node_layout):
59 | query = dict(
60 | origin="{0},{1}".format(y0, x0),
61 | orientation=orientation,
62 | node_layout=node_layout,
63 | )
64 | url = urllib.parse.urlunsplit(
65 | ("", "", "/graphs/hex", urllib.parse.urlencode(query), "")
66 | )
67 | graph = xr.Dataset.from_dict(client.get(url).get_json()["graph"])
68 |
69 | query["origin"] = "0.0,0.0"
70 | url = urllib.parse.urlunsplit(
71 | ("", "", "/graphs/hex", urllib.parse.urlencode(query), "")
72 | )
73 | centered = xr.Dataset.from_dict(client.get(url).get_json()["graph"])
74 |
75 | assert_array_almost_equal(graph.x_of_node, centered.x_of_node + x0)
76 | assert_array_almost_equal(graph.y_of_node, centered.y_of_node + y0)
77 |
78 |
79 | def test_hex_default_orientation(client):
80 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/hex").get_json()["graph"])
81 | graph_2 = xr.Dataset.from_dict(
82 | client.get("/graphs/hex?orientation=horizontal").get_json()["graph"]
83 | )
84 |
85 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
86 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
87 |
88 |
89 | def test_hex_default_node_layout(client):
90 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/hex").get_json()["graph"])
91 | graph_2 = xr.Dataset.from_dict(
92 | client.get("/graphs/hex?node_layout=rect").get_json()["graph"]
93 | )
94 |
95 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
96 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
97 |
--------------------------------------------------------------------------------
/tests/test_raster.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | import xarray as xr
4 | from numpy.testing import assert_array_almost_equal, assert_array_equal
5 |
6 |
7 | @pytest.mark.parametrize("graph_type", ("hex", "radial", "raster"))
8 | def test_graph_data(client, graph_type):
9 | graph = xr.Dataset.from_dict(
10 | client.get("/graphs/{0}".format(graph_type)).get_json()["graph"]
11 | )
12 |
13 | assert set(graph.dims) == {
14 | "cell",
15 | "corner",
16 | "face",
17 | "link",
18 | "max_cell_faces",
19 | "max_patch_links",
20 | "node",
21 | "patch",
22 | "Two",
23 | }
24 | assert set(graph.variables) == {
25 | "corner",
26 | "corners_at_face",
27 | "faces_at_cell",
28 | "links_at_patch",
29 | "mesh",
30 | "node",
31 | "node_at_cell",
32 | "nodes_at_face",
33 | "nodes_at_link",
34 | "x_of_corner",
35 | "x_of_node",
36 | "y_of_corner",
37 | "y_of_node",
38 | }
39 |
40 |
41 | def test_raster_default(client):
42 | graph = xr.Dataset.from_dict(client.get("/graphs/raster").get_json()["graph"])
43 |
44 | assert graph.dims == {
45 | "cell": 1,
46 | "corner": 4,
47 | "face": 4,
48 | "link": 12,
49 | "node": 9,
50 | "patch": 4,
51 | "max_cell_faces": 4,
52 | "max_patch_links": 4,
53 | "Two": 2,
54 | }
55 |
56 | assert_array_equal(
57 | graph.nodes_at_link,
58 | [
59 | [0, 1],
60 | [1, 2],
61 | [0, 3],
62 | [1, 4],
63 | [2, 5],
64 | [3, 4],
65 | [4, 5],
66 | [3, 6],
67 | [4, 7],
68 | [5, 8],
69 | [6, 7],
70 | [7, 8],
71 | ],
72 | )
73 | assert_array_equal(graph.x_of_node, [0.0, 1.0, 2.0, 0.0, 1.0, 2.0, 0.0, 1.0, 2.0])
74 | assert_array_equal(graph.y_of_node, [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0])
75 | assert_array_equal(graph.x_of_corner, [0.5, 1.5, 0.5, 1.5])
76 | assert_array_equal(graph.y_of_corner, [0.5, 0.5, 1.5, 1.5])
77 | assert_array_equal(
78 | graph.links_at_patch, [[3, 5, 2, 0], [4, 6, 3, 1], [8, 10, 7, 5], [9, 11, 8, 6]]
79 | )
80 | assert_array_equal(graph.corners_at_face, [[0, 1], [0, 2], [1, 3], [2, 3]])
81 |
82 |
83 | def test_raster_default_shape(client):
84 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/raster").get_json()["graph"])
85 | graph_2 = xr.Dataset.from_dict(
86 | client.get("/graphs/raster?shape=3,3").get_json()["graph"]
87 | )
88 |
89 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
90 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
91 |
92 |
93 | def test_raster_default_spacing(client):
94 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/raster").get_json()["graph"])
95 | graph_2 = xr.Dataset.from_dict(
96 | client.get("/graphs/raster?spacing=1").get_json()["graph"]
97 | )
98 |
99 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
100 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
101 |
102 |
103 | def test_raster_default_origin(client):
104 | graph_1 = xr.Dataset.from_dict(client.get("/graphs/raster").get_json()["graph"])
105 | graph_2 = xr.Dataset.from_dict(
106 | client.get("/graphs/raster?origin=0.0,0.0").get_json()["graph"]
107 | )
108 |
109 | assert_array_almost_equal(graph_1.x_of_node, graph_2.x_of_node)
110 | assert_array_almost_equal(graph_1.y_of_node, graph_2.y_of_node)
111 |
112 |
113 | @pytest.mark.parametrize("n_cols", (4, 8, 16, 32))
114 | @pytest.mark.parametrize("n_rows", (4, 8, 16, 32))
115 | def test_raster_shape(client, n_rows, n_cols):
116 | url = "/graphs/raster?shape={0},{1}".format(n_rows, n_cols)
117 | graph = xr.Dataset.from_dict(client.get(url).get_json()["graph"])
118 | assert graph.dims["node"] == n_rows * n_cols
119 |
120 |
121 | @pytest.mark.parametrize("dy", (1.0, 2.0, 4.0))
122 | @pytest.mark.parametrize("dx", (1.0, 2.0, 4.0))
123 | def test_raster_spacing(client, dx, dy):
124 | url = "/graphs/raster?spacing={0},{1}".format(dy, dx)
125 | graph = xr.Dataset.from_dict(client.get(url).get_json()["graph"])
126 |
127 | expected_x, expected_y = np.meshgrid([0.0, 1.0, 2.0], [0.0, 1.0, 2.0])
128 |
129 | assert_array_almost_equal(graph.x_of_node, expected_x.reshape((-1,)) * dx)
130 | assert_array_almost_equal(graph.y_of_node, expected_y.reshape((-1,)) * dy)
131 |
132 |
133 | @pytest.mark.parametrize("y0", (-1.0, 1.0, 2.0, 4.0))
134 | @pytest.mark.parametrize("x0", (-1.0, 1.0, 2.0, 4.0))
135 | def test_raster_origin(client, x0, y0):
136 | url = "/graphs/raster?origin={0},{1}".format(y0, x0)
137 | graph = xr.Dataset.from_dict(client.get(url).get_json()["graph"])
138 |
139 | expected_x, expected_y = np.meshgrid([0.0, 1.0, 2.0], [0.0, 1.0, 2.0])
140 |
141 | assert_array_almost_equal(graph.x_of_node, expected_x.reshape((-1,)) + x0)
142 | assert_array_almost_equal(graph.y_of_node, expected_y.reshape((-1,)) + y0)
143 |
--------------------------------------------------------------------------------
/landlab_rest/api/graphs.py:
--------------------------------------------------------------------------------
1 | import json
2 | import urllib
3 |
4 | import landlab
5 | from flask import Blueprint, Response, jsonify, request
6 |
7 | graphs_page = Blueprint("graphs", __name__)
8 |
9 |
10 | def as_resource(resp):
11 | return Response(
12 | json.dumps(resp, sort_keys=True, indent=2, separators=(",", ": ")),
13 | mimetype="application/x-resource+json; charset=utf-8",
14 | )
15 |
16 |
17 | def as_collection(resp):
18 | return Response(
19 | json.dumps(resp, sort_keys=True, indent=2, separators=(",", ": ")),
20 | mimetype="application/x-collection+json; charset=utf-8",
21 | )
22 |
23 |
24 | def jsonify_collection(items):
25 | collection = []
26 | for item in items:
27 | collection.append(item.to_resource())
28 | return Response(
29 | json.dumps(collection, sort_keys=True, indent=2, separators=(",", ": ")),
30 | mimetype="application/x-collection+json; charset=utf-8",
31 | )
32 |
33 |
34 | def to_resource(grid, href=None, repr_=None):
35 | return {"_type": "graph", "href": href, "graph": grid_as_dict(grid), "repr": repr_}
36 |
37 |
38 | def grid_as_dict(grid):
39 | grid.ds.update(
40 | {
41 | "corner": grid.corners.reshape(-1),
42 | "x_of_corner": (("corner",), grid.x_of_corner),
43 | "y_of_corner": (("corner",), grid.y_of_corner),
44 | "faces_at_cell": (("cell", "max_cell_faces"), grid.faces_at_cell),
45 | "corners_at_face": (("face", "Two"), grid.corners_at_face),
46 | }
47 | )
48 | return grid.ds.to_dict()
49 |
50 |
51 | @graphs_page.route("/")
52 | def show():
53 | graphs = ["hex", "raster", "radial"]
54 | graphs.sort()
55 | return jsonify(graphs)
56 |
57 |
58 | @graphs_page.route("/raster")
59 | def raster():
60 | args = dict(
61 | shape=request.args.get("shape", "3,3"),
62 | spacing=request.args.get("spacing", "1.0,1.0"),
63 | origin=request.args.get("origin", "0.0,0.0"),
64 | )
65 |
66 | shape = tuple(int(n) for n in args["shape"].split(","))
67 | spacing = tuple(float(n) for n in args["spacing"].split(","))
68 | origin = tuple(float(n) for n in args["origin"].split(","))
69 |
70 | grid = landlab.graph.DualUniformRectilinearGraph(
71 | shape, spacing=spacing, origin=origin
72 | )
73 | return as_resource(
74 | to_resource(
75 | grid,
76 | href=urllib.parse.urlunsplit(
77 | ("", "", "/graphs/raster", urllib.parse.urlencode(args), "")
78 | ),
79 | repr_="DualUniformRectilinearGraph({shape}, spacing={spacing}, origin={origin})".format(
80 | shape=repr(shape), spacing=repr(spacing), origin=repr(origin)
81 | ),
82 | )
83 | )
84 |
85 |
86 | @graphs_page.route("/hex")
87 | def hex():
88 | args = dict(
89 | shape=request.args.get("shape", "4,4"),
90 | spacing=request.args.get("spacing", "1.0"),
91 | yx_of_origin=request.args.get("origin", "0.0,0.0"),
92 | orientation=request.args.get("orientation", "horizontal"),
93 | node_layout=request.args.get("node_layout", "rect"),
94 | )
95 |
96 | shape = tuple(int(n) for n in args["shape"].split(","))
97 | spacing = float(args["spacing"])
98 | yx_of_origin = tuple(float(n) for n in args["yx_of_origin"].split(","))
99 | xy_of_origin = yx_of_origin[1], yx_of_origin[0]
100 |
101 | grid = landlab.graph.DualHexGraph(
102 | shape,
103 | spacing=spacing,
104 | xy_of_lower_left=xy_of_origin,
105 | orientation=args["orientation"],
106 | node_layout=args["node_layout"],
107 | sort=True,
108 | )
109 |
110 | return as_resource(
111 | to_resource(
112 | grid,
113 | href=urllib.parse.urlunsplit(
114 | ("", "", "/graphs/hex", urllib.parse.urlencode(args), "")
115 | ),
116 | repr_="DualHexGraph({shape}, spacing={spacing}, xy_of_lower_left={xy_of_origin}, orientation={orientation}, node_layout={node_layout})".format(
117 | shape=repr(shape),
118 | spacing=repr(spacing),
119 | xy_of_origin=repr(xy_of_origin),
120 | orientation=repr(args["orientation"]),
121 | node_layout=repr(args["node_layout"]),
122 | ),
123 | )
124 | )
125 |
126 |
127 | @graphs_page.route("/radial")
128 | def radial():
129 | args = dict(
130 | shape=request.args.get("shape", "3,4"),
131 | spacing=request.args.get("spacing", "1.0"),
132 | yx_of_origin=request.args.get("origin", "0.0,0.0"),
133 | )
134 |
135 | shape = tuple(int(n) for n in args["shape"].split(","))
136 | spacing = float(args["spacing"])
137 | yx_of_origin = tuple(float(n) for n in args["yx_of_origin"].split(","))
138 | xy_of_origin = yx_of_origin[1], yx_of_origin[0]
139 |
140 | grid = landlab.graph.DualRadialGraph(
141 | shape, spacing=spacing, xy_of_center=xy_of_origin, sort=True
142 | )
143 |
144 | return as_resource(
145 | to_resource(
146 | grid,
147 | href=urllib.parse.urlunsplit(
148 | ("", "", "/graphs/radial", urllib.parse.urlencode(args), "")
149 | ),
150 | repr_=f"DualRadialGraph({shape!r}, spacing={spacing!r}, xy_of_center={xy_of_origin!r})",
151 | )
152 | )
153 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # http://www.sphinx-doc.org/en/master/config
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | # import os
14 | # import sys
15 | # sys.path.insert(0, os.path.abspath('.'))
16 |
17 |
18 | # -- Project information -----------------------------------------------------
19 |
20 | project = 'landlab_rest'
21 | copyright = '2019, Eric Hutton'
22 | author = 'Eric Hutton'
23 |
24 |
25 | # -- General configuration ---------------------------------------------------
26 |
27 | # Add any Sphinx extension module names here, as strings. They can be
28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
29 | # ones.
30 | extensions = [
31 | 'sphinx.ext.autodoc',
32 | 'sphinx.ext.intersphinx',
33 | 'sphinx.ext.todo',
34 | 'sphinx.ext.viewcode',
35 | 'sphinx.ext.githubpages',
36 | 'sphinx.ext.napoleon',
37 | ]
38 |
39 | # Add any paths that contain templates here, relative to this directory.
40 | templates_path = ['_templates']
41 |
42 | # List of patterns, relative to source directory, that match files and
43 | # directories to ignore when looking for source files.
44 | # This pattern also affects html_static_path and html_extra_path.
45 | exclude_patterns = []
46 |
47 |
48 | # -- Options for HTML output ----------------------------------------------
49 |
50 | # The theme to use for HTML and HTML Help pages. See the documentation for
51 | # a list of builtin themes.
52 | html_theme = 'alabaster'
53 |
54 | # Theme options are theme-specific and customize the look and feel of a theme
55 | # further. For a list of options available for each theme, see the
56 | # documentation.
57 | #html_theme_options = {}
58 |
59 | # Add any paths that contain custom themes here, relative to this directory.
60 | #html_theme_path = []
61 |
62 | # The name for this set of Sphinx documents. If None, it defaults to
63 | # " v documentation".
64 | #html_title = None
65 |
66 | # A shorter title for the navigation bar. Default is the same as html_title.
67 | #html_short_title = None
68 |
69 | # The name of an image file (relative to this directory) to place at the top
70 | # of the sidebar.
71 | html_logo = "_static/powered-by-logo-header.png"
72 |
73 | # The name of an image file (within the static path) to use as favicon of the
74 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
75 | # pixels large.
76 | #html_favicon = None
77 |
78 | # Add any paths that contain custom static files (such as style sheets) here,
79 | # relative to this directory. They are copied after the builtin static files,
80 | # so a file named "default.css" will overwrite the builtin "default.css".
81 | html_static_path = ['_static']
82 |
83 | # Add any extra paths that contain custom files (such as robots.txt or
84 | # .htaccess) here, relative to this directory. These files are copied
85 | # directly to the root of the documentation.
86 | #html_extra_path = []
87 |
88 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
89 | # using the given strftime format.
90 | #html_last_updated_fmt = '%b %d, %Y'
91 |
92 | # If true, SmartyPants will be used to convert quotes and dashes to
93 | # typographically correct entities.
94 | #html_use_smartypants = True
95 |
96 | # Custom sidebar templates, maps document names to template names.
97 | html_sidebars = {
98 | "index": [
99 | "sidebarintro.html",
100 | "links.html",
101 | "sourcelink.html",
102 | "searchbox.html",
103 | ],
104 | "**": [
105 | "sidebarintro.html",
106 | "links.html",
107 | "sourcelink.html",
108 | "searchbox.html",
109 | ]
110 | }
111 |
112 | # Additional templates that should be rendered to pages, maps page names to
113 | # template names.
114 | #html_additional_pages = {}
115 |
116 | # If false, no module index is generated.
117 | #html_domain_indices = True
118 |
119 | # If false, no index is generated.
120 | #html_use_index = True
121 |
122 | # If true, the index is split into individual pages for each letter.
123 | #html_split_index = False
124 |
125 | # If true, links to the reST sources are added to the pages.
126 | #html_show_sourcelink = True
127 |
128 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
129 | #html_show_sphinx = True
130 |
131 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
132 | #html_show_copyright = True
133 |
134 | # If true, an OpenSearch description file will be output, and all pages will
135 | # contain a tag referring to it. The value of this option must be the
136 | # base URL from which the finished HTML is served.
137 | #html_use_opensearch = ''
138 |
139 | # This is the file name suffix for HTML files (e.g. ".xhtml").
140 | #html_file_suffix = None
141 |
142 | # Language to be used for generating the HTML full-text search index.
143 | # Sphinx supports the following languages:
144 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
145 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
146 | #html_search_language = 'en'
147 |
148 | # A dictionary with options for the search language support, empty by default.
149 | # Now only 'ja' uses this config value
150 | #html_search_options = {'type': 'default'}
151 |
152 | # The name of a javascript file (relative to the configuration directory) that
153 | # implements a search results scorer. If empty, the default will be used.
154 | #html_search_scorer = 'scorer.js'
155 |
156 | # Output file base name for HTML help builder.
157 | htmlhelp_basename = 'landlab_restdoc'
158 |
159 |
160 | # -- Extension configuration -------------------------------------------------
161 |
162 | # -- Options for intersphinx extension ---------------------------------------
163 |
164 | # Example configuration for intersphinx: refer to the Python standard library.
165 | intersphinx_mapping = {'https://docs.python.org/': None}
166 |
167 | # -- Options for todo extension ----------------------------------------------
168 |
169 | # If true, `todo` and `todoList` produce output, else they produce nothing.
170 | todo_include_todos = True
171 |
--------------------------------------------------------------------------------