[nominatim-web/fastapi] Initial commit of FastAPI rewrite




commit 7e60663987cf7579bc4271f12b45c70aed056a32
Author: Bartłomiej Piotrowski <bpiotrowski gnome org>
Date:   Fri Apr 2 12:58:50 2021 +0200

    Initial commit of FastAPI rewrite

 .gitignore         |   1 +
 Dockerfile         |  26 ++++++++--
 app/main.py        | 148 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 docker-compose.yml |  24 +++++++++
 nginx.conf         |  93 ---------------------------------
 redis.lua          |  43 ----------------
 requirements.txt   |  15 ++++++
 7 files changed, 211 insertions(+), 139 deletions(-)
---
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bee8a64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/Dockerfile b/Dockerfile
index 37b7b32..848bdb0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,23 @@
-FROM openresty/openresty:1.15.8.3-2-buster
-ADD nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
-ADD redis.lua /usr/local/openresty/nginx/redis.lua
+FROM python:3.9 as builder
+
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends \
+        build-essential libpq-dev
+
+ADD requirements.txt /requirements.txt
+RUN python -m venv /venv && \
+    /venv/bin/pip install -r requirements.txt \
+    && rm -f /requirements.txt
+
+FROM python:3.9-slim
+
+EXPOSE 8000
+
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends libpq5 curl && \
+    apt-get clean && rm -rf /var/lib/apt/lists/*
+
+COPY ./app /app
+COPY --from=builder /venv /venv
+
+CMD ["/venv/bin/uvicorn", "--app-dir", "/", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/app/main.py b/app/main.py
new file mode 100644
index 0000000..473cad5
--- /dev/null
+++ b/app/main.py
@@ -0,0 +1,148 @@
+from fastapi import FastAPI, Request, Response
+import redis
+import requests
+import hashlib
+import psycopg2
+import psycopg2.pool
+import datetime
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+    api_url: str = "https://nominatim.openstreetmap.org";
+    redis_host: str = "localhost"
+    redis_port: int = 6379
+    postgres_url: str = "dbname=nominatim host=127.0.0.1 user=postgres"
+
+
+settings = Settings()
+
+redis_conn = redis.Redis(
+    host=settings.redis_host, port=settings.redis_port, decode_responses=True
+)
+postgres = psycopg2.pool.SimpleConnectionPool(1, 10, settings.postgres_url)
+
+app = FastAPI()
+
+
+@app.on_event("startup")
+def initialize():
+    with postgres.getconn() as conn:
+        with conn.cursor() as cur:
+            cur.execute(
+                "CREATE TABLE IF NOT EXISTS search ( key VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP 
NOT NULL, value text);"
+            )
+
+            cur.execute(
+                "CREATE TABLE IF NOT EXISTS reverse ( key VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP 
NOT NULL, value text);"
+            )
+        conn.commit()
+
+
+@app.get("/status")
+def healthcheck():
+    return {"status": "OK"}
+
+
+@app.get("/search")
+def search(request: Request):
+    lang = request.query_params["accept-language"]
+    limit = request.query_params["limit"]
+    country = request.query_params["country"]
+    state = request.query_params["state"]
+    city = request.query_params["city"]
+    street = request.query_params["street"]
+
+    location = hashlib.md5(f"{country}:{state}:{city}:{street}".encode()).hexdigest()
+
+    if query := request.query_params.get("q"):
+        query = hashlib.md5(query.encode()).hexdigest()
+    else:
+        query = "none"
+
+    key = f"search:{lang}:{limit}:{location}:{query}"
+
+    if resp := redis_conn.get(key):
+        return Response(content=resp, media_type="application/json")
+
+    with postgres.getconn() as conn:
+        with conn.cursor() as cur:
+            cur.execute("SELECT * FROM search WHERE key = %s", (key,))
+            if resp := cur.fetchone():
+                redis_conn.setex(key, 600, resp[2])
+                return Response(content=resp[2], media_type="application/json")
+
+    r = requests.get(f"{settings.api_url}/search", params=request.query_params)
+    redis_conn.setex(key, 600, r.text)
+
+    if r.status_code == 200:
+        with postgres.getconn() as conn:
+            with conn.cursor() as cur:
+                now = datetime.datetime.now()
+                cur.execute(
+                    "INSERT INTO search(key,created_at,value) VALUES (%s, %s, %s)",
+                    (key, now, r.text),
+                )
+            conn.commit()
+
+    return Response(
+        content=r.text, media_type="application/json", status_code=r.status_code
+    )
+
+
+@app.get("/reverse")
+def reverse(request: Request):
+    lat = "{:.5f}".format(float(request.query_params["lat"]))
+    lon = "{:.5f}".format(float(request.query_params["lon"]))
+    lang = request.query_params["accept-language"]
+
+    key = f"rev:{lang}:{lat}x{lon}"
+    if resp := redis_conn.get(key):
+        return Response(content=resp, media_type="application/json")
+
+    with postgres.getconn() as conn:
+        with conn.cursor() as cur:
+            cur.execute("SELECT * FROM reverse WHERE key = %s", (key,))
+            if resp := cur.fetchone():
+                redis_conn.setex(key, 600, resp[2])
+                return Response(content=resp[2], media_type="application/json")
+
+    query_params = {
+        "lat": lat,
+        "lon": lon,
+        "addressdetails": 1,
+        "accept-language": lang,
+        "format": "json",
+    }
+
+    r = requests.get(f"{settings.api_url}/reverse", params=query_params)
+    redis_conn.setex(key, 600, r.text)
+
+    if r.status_code == 200:
+        with postgres.getconn() as conn:
+            with conn.cursor() as cur:
+                now = datetime.datetime.now()
+                cur.execute(
+                    "INSERT INTO reverse(key,created_at,value) VALUES (%s, %s, %s)",
+                    (key, now, r.text),
+                )
+            conn.commit()
+
+    return Response(
+        content=r.text, media_type="application/json", status_code=r.status_code
+    )
+
+
+@app.get("/expire")
+def expire():
+    limit = datetime.datetime.now() - datetime.timedelta(days=30)
+
+    with postgres.getconn() as conn:
+        with conn.cursor() as cur:
+            cur.execute("DELETE FROM search WHERE created_at < %s", (limit,))
+            search = cur.rowcount
+            cur.execute("DELETE FROM reverse WHERE created_at < %s", (limit,))
+            reverse = cur.rowcount
+        conn.commit()
+
+    return {"search": search, "reverse": reverse}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..22c0383
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,24 @@
+version: "2"
+services:
+  redis:
+    image: redis:6.2.1
+
+  postgres:
+    image: postgres
+    environment:
+      - POSTGRES_DB=nominatim
+      - POSTGRES_USER=postgres
+      - POSTGRES_HOST_AUTH_METHOD=trust
+
+  backend:
+    build:
+      dockerfile: Dockerfile
+      context: .
+    ports:
+      - "8000:8000"
+    links:
+      - redis:redis
+      - postgres:postgres
+    depends_on:
+      - redis
+      - postgres
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..fd7bdd0
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,15 @@
+certifi==2020.12.5
+chardet==4.0.0
+click==7.1.2
+fastapi==0.63.0
+h11==0.12.0
+hiredis==2.0.0
+idna==2.10
+psycopg2==2.8.6
+pydantic==1.8.1
+redis==3.5.3
+requests==2.25.1
+starlette==0.13.6
+typing-extensions==3.7.4.3
+urllib3==1.26.4
+uvicorn==0.13.4


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]