Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to FastAPI #54

Merged
merged 5 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 4 additions & 15 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ jobs:
steps:
- uses: actions/checkout@v2
- run: docker build --tag webproj .
- run: 'docker run -d -p 0.0.0.0:5000:80 --name restapi webproj'
- run: 'docker run -d -p 80:80 --name restapi webproj'
- run: docker ps
- run: sudo apt-get update

- name: Setup conda
uses: conda-incubator/setup-miniconda@v2
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-variant: Mambaforge
miniforge-version: latest
Expand All @@ -28,18 +28,7 @@ jobs:
- name: Pytest
run: pytest --cov

- run: >-
docker exec restapi curl -s
localhost/v1.0/trans/EPSG:4258/DK:S34S/55.0,12.0 > S34S.out
- run: curl -s localhost/v1.0/trans/EPSG:4258/DK:S34S/55.0,12.0 > S34S.out

- run: cat S34S.out
- run: diff test_s34s.out S34S.out

- name: Covert Coverage Results
run: |
coveragepy-lcov --data_file_path .coverage --output_file_path lcov.info

- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: lcov.info
39 changes: 19 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
# See https://github.com/tiangolo/meinheld-gunicorn-flask-docker
# for notes on how to use this particular Docker image.
#
# In summary: Make sure that a /app/main.py file with a Flask-object called
# 'app' is present and everything should run smoothly with all the bells and
# whistles of a properly configured HTTP server

FROM tiangolo/meinheld-gunicorn-flask:python3.9
FROM condaforge/miniforge3

# We store PROJ ressources in $WEBPROJ_LIB
ENV WEBPROJ_LIB /proj
RUN mkdir $WEBPROJ_LIB

RUN mkdir /proj

# Copy necessary files. Tests and README are needed by setup.py
COPY /webproj /webproj/webproj
COPY /tests /webproj/tests
COPY /app /webproj/app
COPY /tests /webproj/tests
COPY /setup.py /webproj/setup.py
COPY /environment.yaml /webproj/environment.yaml
COPY /README.md /webproj/README.md
COPY /app/main.py /app/main.py

WORKDIR /webproj

# Running upgrade for security
RUN apt update -y && apt upgrade -y
RUN apt-get update -y && apt-get upgrade -y

# Set up virtual environment
RUN conda env create -f environment.yaml
RUN conda run -n webproj python -m pip install --no-deps .

# Sync PROJ-data files
RUN conda run -n webproj pyproj sync --source-id dk_sdfe --target-dir $WEBPROJ_LIB
RUN conda run -n webproj pyproj sync --source-id dk_sdfi --target-dir $WEBPROJ_LIB

RUN pip install --upgrade pip
RUN pip install "pyproj<3.4.0"
RUN pip install "flask>=2.1.0,<2.2.0"
RUN pip install flask-restx flask-cors
run pip install "Werkzeug>=2.1.0,<2.2.0"
RUN pip install /webproj
RUN pyproj sync --source-id dk_sdfe -v
CMD ["conda", "run", "-n", "webproj", "uvicorn", "--proxy-headers", "app.main:app", "--host", "0.0.0.0", "--port", "80"]

EXPOSE 80
38 changes: 16 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ at https://docs.dataforsyningen.dk/#webproj.

## API

The API is a simple REST API that delivers data in JSON format. In
the current version two main entry points are provided: `/crs/` and
`/trans/`.
The API is a simple REST API that delivers data in JSON format. The two main
entry points are provided: `/crs/` and `/trans/`.

### Installation

Expand All @@ -31,14 +30,11 @@ Activate the new environment with
$ conda activate webproj
```

For production use, the API should be installed as a component in a
WSGI compatible http server. How to configure this depends on the used http server.


Remember to run projsync in order to install the datum grids.

```
$ projsync --source-id dk_sdfe
$ projsync --source-id dk_sdfi
```

### Tests
Expand All @@ -57,14 +53,18 @@ For a simple demonstration of the WEBPROJ REST API a webserver can
be started locally by running

```
(webproj) C:\dev\webproj>python webproj\api.py
(webproj) C:\dev\webproj>uvicorn app.main:app --host 127.0.0.1 --port 8000
```

This will spawn a Flask server that serves the API locally on
`http://127.0.0.1:5000/`.
This will spawn a web-server that serves the API locally on
`http://127.0.0.1:8000/`.

The API exposes a small set of features that are accessed via URL
entry points. For version 1.0 of the API we have:
entry points. OpenAPI documentation is auto-generated and is available
in a user-friendly web UI at `/documentation`. A machine-readable version
of the same documentation is available at `/openapi.json`.

For version 1.0 of the API we have:

#### `/crs/`

Expand All @@ -74,7 +74,7 @@ be transformed between.
##### Example

```
$ curl http://127.0.0.1:5000/v1.0/crs/
$ curl http://127.0.0.1:8000/v1.0/crs/
{
DK: [
"EPSG:25832",
Expand All @@ -94,7 +94,7 @@ Returns information about a specific coordinate reference system.
##### Example

```
$curl http://127.0.0.1:5000/v1.0/crs/EPSG:25832
$curl http://127.0.0.1:8000/v1.0/crs/EPSG:25832
{
country: "DK",
title: "ETRS89 / UTM Zone 32 Nord",
Expand All @@ -120,7 +120,7 @@ depending on the input the number of used output coordinate components varies.

```
# 2D
curl http://127.0.0.1:5000/v1.0/trans/EPSG:4258/EPSG:25832/12.0,56.0
curl http://127.0.0.1:8000/v1.0/trans/EPSG:4258/EPSG:25832/12.0,56.0
{
"v1:": 687071.4391094431,
"v2": 6210141.326748009,
Expand All @@ -129,7 +129,7 @@ curl http://127.0.0.1:5000/v1.0/trans/EPSG:4258/EPSG:25832/12.0,56.0
}

# 3D
curl http://127.0.0.1:5000/v1.0/trans/EPSG:4258/EPSG:25832/12.0,56.0,30.0
curl http://127.0.0.1:8000/v1.0/trans/EPSG:4258/EPSG:25832/12.0,56.0,30.0
{
"v1:": 687071.4391094431,
"v2": 6210141.326748009,
Expand All @@ -138,17 +138,11 @@ curl http://127.0.0.1:5000/v1.0/trans/EPSG:4258/EPSG:25832/12.0,56.0,30.0
}

# 4D
curl http://127.0.0.1:5000/v1.0/trans/EPSG:4258/EPSG:25832/12.0,56.0,30.0,2010.5
curl http://127.0.0.1:8000/v1.0/trans/EPSG:4258/EPSG:25832/12.0,56.0,30.0,2010.5
{
"v1:": 687071.4391094431,
"v2": 6210141.326748009,
"v3": 30.0,
"v4": 2010.5
}
```

## Web application

N/A.


8 changes: 0 additions & 8 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1 @@
from webproj import app


def run():
app.run(debug=True, host="0.0.0.0")


if __name__ == "__main__":
run()
18 changes: 7 additions & 11 deletions environment-dev.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
name: webproj
channels:
- conda-forge
- defaults
dependencies:
- python=3.9
- pip>=20.0
- werkzeug=2.1
- flask>=1.0
- flask-restx>=0.1.1
- flask-cors>=3.0
- pyproj>=3.3
- python
- pip
- fastapi
- httpx
- pyproj
- pydantic
- uvicorn
- black
- pytest
- coverage
- pytest-cov
- pip:
- coveragepy-lcov
14 changes: 6 additions & 8 deletions environment.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
name: webproj
channels:
- conda-forge
- defaults
dependencies:
- python=3.9
- pip>=20.0
- werkzeug=2.1
- flask>=1.0
- flask-restx>=0.1.1
- flask-cors>=3.0
- pyproj>=3.3
- python
- fastapi
- httpx
- pyproj
- pydantic
- uvicorn
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
license="MIT",
keywords="PROJ transformations geodesy projections",
author="Kristian Evers",
author_email="kreve@sdfe.dk",
url="https://github.com/Kortforsyningen/WEBPROJ",
author_email="kreve@sdfi.dk",
url="https://github.com/SDFIdk/WEBPROJ",
long_description=readme,
packages=["webproj", "tests", "app"],
install_requires=["flask", "flask-restx", "flask-cors", "pyproj"],
install_requires=["fastapi", "pyproj"],
test_suite="tests/test_api.py",
data_files=["webproj/data.json"],
include_package_data=True,
Expand Down
2 changes: 1 addition & 1 deletion test_s34s.out
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"v1": 106020.66701403809, "v2": 64281.27481858295, "v3": null, "v4": null}
{"v1":106020.66701403809,"v2":64281.27481858295,"v3":null,"v4":null}
39 changes: 16 additions & 23 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import sys
import re
import json
import pprint

import pytest
from fastapi.testclient import TestClient

from webproj import api
from webproj.api import app, TransformerFactory


def _get_and_decode_response(entry):
"""
Retrieves response from API and decodes the JSON data into a dict
"""
app = api.app.test_client()
response = app.get(entry)
decoded_response = json.loads(response.get_data().decode(sys.getdefaultencoding()))
client = TestClient(app)
response = client.get(entry)
decoded_response = response.json()
return decoded_response


Expand Down Expand Up @@ -53,9 +52,9 @@ def _assert_coordinate(entry, expected_json_output, tolerance=1e-6):
Check that a returned coordinate matches the expected result
within a pre-determined tolerance
"""
app = api.app.test_client()
response = app.get(entry)
result = json.loads(response.get_data().decode(sys.getdefaultencoding()))
client = TestClient(app)
response = client.get(entry)
result = response.json()
print(expected_json_output)
print(result)
for key in expected_json_output.keys():
Expand Down Expand Up @@ -90,8 +89,8 @@ def test_transformer_caching():
Check that caching works by comparing objects with the is operator
"""

transformer_a = api.TransformerFactory.create("EPSG:4095", "EPSG:4096")
transformer_b = api.TransformerFactory.create("EPSG:4095", "EPSG:4096")
transformer_a = TransformerFactory.create("EPSG:4095", "EPSG:4096")
transformer_b = TransformerFactory.create("EPSG:4095", "EPSG:4096")

assert transformer_a is transformer_b

Expand All @@ -100,7 +99,7 @@ def test_crs(api_all):
"""
Test that CRS descriptions are presented correctly
"""
for srid, crsinfo in api.CRS_LIST.items():
for srid, crsinfo in app.CRS_LIST.items():
_assert_result(f"/{api_all}/crs/{srid}", crsinfo)


Expand All @@ -110,7 +109,7 @@ def test_crs_index(api_all):
correctly.
"""
expected = {}
for srid, crsinfo in api.CRS_LIST.items():
for srid, crsinfo in app.CRS_LIST.items():
if crsinfo["country"] not in expected:
expected[crsinfo["country"]] = []
expected[crsinfo["country"]].append(srid)
Expand All @@ -122,14 +121,8 @@ def test_crs_that_doesnt_exist(api_all):
"""
Test that we get the proper response when requesting an unknown CRS
"""

errmsg = f"'unknowncrs' not available. You have requested this URI "
errmsg += (
f"[/{api_all}/crs/unknowncrs] but did you mean /{api_all}/crs/<string:crs>"
)

response = _get_and_decode_response(f"/{api_all}/crs/unknowncrs")
assert response["message"].startswith(errmsg)
assert response["detail"] == "'unknowncrs' not available."


def test_trans_2d(api_all):
Expand Down Expand Up @@ -216,7 +209,7 @@ def test_transformation_outside_crs_area_of_use(api_all):
"""
api_entry = f"/{api_all}/trans/EPSG:4258/DK:S34S/12.0,56.0"
expected = {
"message": "Input coordinate outside area of use of either source or destination CRS"
"detail": "Input coordinate outside area of use of either source or destination CRS"
}
_assert_result(api_entry, expected)

Expand Down Expand Up @@ -261,11 +254,11 @@ def test_transformation_between_global_and_regional_crs(api_all):

# test some failing cases DK -> GL
api_entry = f"/{api_all}/trans/EPSG:4258/EPSG:4909/55.0,12.0"
expected = {"message": "CRS's are not compatible across countries"}
expected = {"detail": "CRS's are not compatible across countries"}
_assert_result(api_entry, expected)

api_entry = f"/{api_all}/trans/EPSG:4909/EPSG:4258/75.0,-50.0"
expected = {"message": "CRS's are not compatible across countries"}
expected = {"detail": "CRS's are not compatible across countries"}
_assert_result(api_entry, expected)


Expand Down
2 changes: 1 addition & 1 deletion webproj/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import api

__version__ = api.version
__version__ = api.__VERSION__
app = api.app
Loading
Loading