diff --git a/deforestation_api/__main__.py b/deforestation_api/__main__.py index bd996b0..076d02d 100644 --- a/deforestation_api/__main__.py +++ b/deforestation_api/__main__.py @@ -1,8 +1,10 @@ +import pathlib from contextlib import asynccontextmanager import logging from fastapi import FastAPI - +from fastapi.openapi.docs import get_redoc_html +from deforestation_api.openapi import openapi from deforestation_api.dependencies.deforestationdata import ( fetch_deforestation_data, deforestation_data_fetcher, @@ -21,17 +23,28 @@ async def lifespan(deforestation_app: FastAPI): app = FastAPI( - title="Deforestation API", lifespan=lifespan, - version=settings.version, root_path=settings.api_root_path, - description=settings.api_description, + redoc_url=None, ) app.include_router(deforestation.router) app.include_router(healthcheck.router) logging.basicConfig(level=logging.INFO) +example_code_dir = pathlib.Path(__file__).parent / "example_code" +app.openapi_schema = openapi.custom_openapi(app, example_code_dir) + + +@app.get("/redoc", include_in_schema=False) +def redoc(): + return get_redoc_html( + openapi_url="/openapi.json", + title="Deforestation API", + redoc_favicon_url="https://www.openepi.io/favicon.ico", + ) + + if __name__ == "__main__": import uvicorn diff --git a/deforestation_api/example_code/curl/liveness.sh b/deforestation_api/example_code/curl/liveness.sh new file mode 100644 index 0000000..c5a2146 --- /dev/null +++ b/deforestation_api/example_code/curl/liveness.sh @@ -0,0 +1 @@ +curl -i -X GET $endpoint_url diff --git a/deforestation_api/example_code/curl/lossyear.sh b/deforestation_api/example_code/curl/lossyear.sh new file mode 100644 index 0000000..fea0a83 --- /dev/null +++ b/deforestation_api/example_code/curl/lossyear.sh @@ -0,0 +1,6 @@ +# Get yearly forest cover loss for the river basin at the given point +curl -i -X GET $endpoint_url?lon=30.0619&lat=-1.9441 + +# Get yearly forest cover loss for all river basins within the given bounding box +curl -i -X GET $endpoint_url?min_lon=28.850951&min_lat=30.909622&max_lon=-2.840114&max_lat=-1.041395 + diff --git a/deforestation_api/example_code/curl/ready.sh b/deforestation_api/example_code/curl/ready.sh new file mode 100644 index 0000000..c5a2146 --- /dev/null +++ b/deforestation_api/example_code/curl/ready.sh @@ -0,0 +1 @@ +curl -i -X GET $endpoint_url diff --git a/deforestation_api/example_code/javascript/liveness.js b/deforestation_api/example_code/javascript/liveness.js new file mode 100644 index 0000000..bbb6328 --- /dev/null +++ b/deforestation_api/example_code/javascript/liveness.js @@ -0,0 +1,7 @@ +const response = await fetch( + "$endpoint_url" +); +const json = await response.json(); +const message = json.message; + +console.log(`Message: ${message}`); diff --git a/deforestation_api/example_code/javascript/lossyear.js b/deforestation_api/example_code/javascript/lossyear.js new file mode 100644 index 0000000..263f10b --- /dev/null +++ b/deforestation_api/example_code/javascript/lossyear.js @@ -0,0 +1,26 @@ +// Get yearly forest cover loss for the river basin at the given point +const response_point = await fetch( + "$endpoint_url?" + new URLSearchParams({ + lat: -1.9441, + lon: 30.0619 + }) +); +const data_point = await response_point.json(); + +// Print the total forest cover loss within the returned river basin +console.log(data_point.features[0].properties.daterange_tot_treeloss); + + +// Get yearly forest cover loss for all river basins within the given bounding box +const response_bbox = await fetch( + "$endpoint_url?" + new URLSearchParams({ + min_lon: 28.850951, + min_lat: -2.840114, + max_lon: 30.909622, + max_lat: -1.041395 + }) +); +const data_bbox = await response_bbox.json(); + +// Print the total forest cover loss within the first returned river basin +console.log(data_bbox.features[0].properties.daterange_tot_treeloss); \ No newline at end of file diff --git a/deforestation_api/example_code/javascript/ready.js b/deforestation_api/example_code/javascript/ready.js new file mode 100644 index 0000000..8091e46 --- /dev/null +++ b/deforestation_api/example_code/javascript/ready.js @@ -0,0 +1,7 @@ +const response = await fetch( + "$endpoint_url" +); +const json = await response.json(); +const status = json.status; + +console.log(`Status: ${status}`); diff --git a/deforestation_api/example_code/python/liveness.py b/deforestation_api/example_code/python/liveness.py new file mode 100644 index 0000000..d509a3b --- /dev/null +++ b/deforestation_api/example_code/python/liveness.py @@ -0,0 +1,5 @@ +from httpx import Client + +with Client() as client: + response = client.get(url="$endpoint_url") + print(response.json()["message"]) diff --git a/deforestation_api/example_code/python/lossyear.py b/deforestation_api/example_code/python/lossyear.py new file mode 100644 index 0000000..45f75ce --- /dev/null +++ b/deforestation_api/example_code/python/lossyear.py @@ -0,0 +1,29 @@ +from httpx import Client + +with Client() as client: + # Get yearly forest cover loss for the river basin at the given point + response_point = client.get( + url="$endpoint_url", + params={"lon": 30.0619, "lat": -1.9441}, + ) + + data_point = response_point.json() + + # Print the total forest cover loss within the returned river basin + print(data_point["features"][0]["properties"]["daterange_tot_treeloss"]) + + # Get yearly forest cover loss for all river basins within the given bounding box + response_bbox = client.get( + url="$endpoint_url", + params={ + "min_lon": 28.850951, + "min_lat": -2.840114, + "max_lon": 30.909622, + "max_lat": -1.041395, + }, + ) + + data_bbox = response_bbox.json() + + # Print the total forest cover loss within the first returned river basin + print(data_bbox["features"][0]["properties"]["daterange_tot_treeloss"]) diff --git a/deforestation_api/example_code/python/ready.py b/deforestation_api/example_code/python/ready.py new file mode 100644 index 0000000..02196e2 --- /dev/null +++ b/deforestation_api/example_code/python/ready.py @@ -0,0 +1,5 @@ +from httpx import Client + +with Client() as client: + response = client.get(url="$endpoint_url") + print(response.json()["status"]) diff --git a/deforestation_api/openapi/__init__.py b/deforestation_api/openapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deforestation_api/openapi/openapi.py b/deforestation_api/openapi/openapi.py new file mode 100644 index 0000000..6a2f775 --- /dev/null +++ b/deforestation_api/openapi/openapi.py @@ -0,0 +1,62 @@ +import logging +import os +from pathlib import Path +from string import Template + +from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi +from fastapi.routing import APIRoute + +from deforestation_api.settings import settings + +supported_languages = {"cURL": "sh", "JavaScript": "js", "Python": "py"} + + +def custom_openapi(app: FastAPI, example_code_dir: Path): + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title="Deforestation API", + version=settings.version, + description=settings.api_description, + routes=app.routes, + ) + + openapi_schema["info"]["x-logo"] = { + "url": f"https://{settings.api_domain}/assets/icons/open-epi-logo.svg" + } + + routes_that_need_doc = [ + route for route in app.routes if isinstance(route, APIRoute) + ] + for route in routes_that_need_doc: + code_samples = [] + for lang in supported_languages: + file_with_code_sample = ( + example_code_dir + / lang.lower() + / f"{route.name}.{supported_languages[lang]}" + ) + if os.path.isfile(file_with_code_sample): + with open(file_with_code_sample) as f: + code_template = Template(f.read()) + code_samples.append( + { + "lang": lang, + "source": code_template.safe_substitute( + endpoint_url=f"{settings.api_url}{route.path}", + ), + } + ) + else: + logging.warning( + "No code sample found for route %s and language %s", + route.path, + lang, + ) + + if code_samples: + openapi_schema["paths"][route.path]["get"]["x-codeSamples"] = code_samples + + return openapi_schema diff --git a/deforestation_api/settings.py b/deforestation_api/settings.py index 639780f..cb3ba85 100644 --- a/deforestation_api/settings.py +++ b/deforestation_api/settings.py @@ -14,5 +14,14 @@ class Settings(BaseSettings): api_root_path: str = "/" api_description: str = 'This is a RESTful service that provides aggregated deforestation data over the period from 2001 to 2022 based on data provided by Global Land Analysis and Discovery (GLAD) laboratory at the University of Maryland, in partnership with Global Forest Watch (GFW). The data are freely available for use under a Creative Commons Attribution 4.0 International License.
Citation: Hansen, M. C., P. V. Potapov, R. Moore, M. Hancher, S. A. Turubanova, A. Tyukavina, D. Thau, S. V. Stehman, S. J. Goetz, T. R. Loveland, A. Kommareddy, A. Egorov, L. Chini, C. O. Justice, and J. R. G. Townshend. 2013. High-Resolution Global Maps of 21st-Century Forest Cover Change. Science 342 (15 November): 850-53. Data available on-line from: https://glad.earthengine.app/view/global-forest-change.

The data provided by the `basin` endpoint are aggregated over river basin polygons provided by HydroSHEDS. The basin data are feely available for non-commercial and commercial use under a licence agreement included in the HydroSHEDS Technical Documentation.' + api_domain: str = "localhost" + + @property + def api_url(self): + if self.api_domain == "localhost": + return f"http://{self.api_domain}:{self.uvicorn_port}" + else: + return f"https://{self.api_domain}{self.api_root_path}" + settings = Settings() diff --git a/deployment/kubernetes/deforestation-api.yaml b/deployment/kubernetes/deforestation-api.yaml index eadcb14..65fc5a4 100644 --- a/deployment/kubernetes/deforestation-api.yaml +++ b/deployment/kubernetes/deforestation-api.yaml @@ -16,7 +16,7 @@ spec: app: deforestation-api spec: containers: - - image: ghcr.io/openearthplatforminitiative/deforestation-api:0.1.3 + - image: ghcr.io/openearthplatforminitiative/deforestation-api:0.2.0 name: deforestation-api ports: - containerPort: 8080 @@ -24,7 +24,7 @@ spec: - name: API_ROOT_PATH value: "/deforestation" - name: VERSION - value: 0.1.3 + value: 0.2.0 --- apiVersion: v1 kind: Service