├── 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 | --------------------------------------------------------------------------------