Skip to content

Commit 819893c

Browse files
feat: loop up request IP address in geoip database and store result
Store geoip results alongside map_frame in database
1 parent e5dc057 commit 819893c

11 files changed

+142
-5
lines changed

sketch_map_tool/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import os
3+
from pathlib import Path
34

45
from pydantic import computed_field, field_validator
56
from pydantic_settings import (
@@ -26,6 +27,7 @@ class Config(BaseSettings):
2627
cleanup_map_frames_interval: str = "12 months"
2728
data_dir: str = str(get_project_root() / "data") # TODO: make this a Path
2829
esri_api_key: str = ""
30+
geo_ip_database: Path | None = None
2931
log_level: str = "INFO"
3032
max_nr_simultaneous_uploads: int = 100
3133
model_type_sam: str = "vit_b"

sketch_map_tool/database/client_celery.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ def insert_map_frame(
4040
format_: PaperFormat,
4141
orientation: str,
4242
layer: str,
43+
ip: str | None = None,
44+
user_agent: str | None = None,
45+
geo_ip_city: str | None = None,
46+
geo_ip_country: str | None = None,
47+
geo_ip_country_iso_code: str | None = None,
48+
geo_ip_centroid_wgs84: str | None = None,
4349
):
4450
"""Insert map frame alongside map generation parameters into the database.
4551
@@ -59,7 +65,13 @@ def insert_map_frame(
5965
layer VARCHAR,
6066
version VARCHAR,
6167
created TIMESTAMP WITH TIME ZONE DEFAULT now(),
62-
downloaded TIMESTAMP WITH TIME ZONE
68+
downloaded TIMESTAMP WITH TIME ZONE,
69+
ip VARCHAR DEFAULT NULL,
70+
user_agent VARCHAR DEFAULT NULL,
71+
geo_ip_city VARCHAR DEFAULT NULL,
72+
geo_ip_country VARCHAR DEFAULT NULL,
73+
geo_ip_country_iso_code VARCHAR DEFAULT NULL,
74+
geo_ip_centroid_wgs84 VARCHAR DEFAULT NULL
6375
)
6476
"""
6577
insert_query = """
@@ -73,7 +85,13 @@ def insert_map_frame(
7385
format,
7486
orientation,
7587
layer,
76-
version
88+
version,
89+
ip,
90+
user_agent,
91+
geo_ip_city,
92+
geo_ip_country,
93+
geo_ip_country_iso_code,
94+
geo_ip_centroid_wgs84
7795
)
7896
VALUES (
7997
%s,
@@ -85,6 +103,12 @@ def insert_map_frame(
85103
%s,
86104
%s,
87105
%s,
106+
%s,
107+
%s,
108+
%s,
109+
%s,
110+
%s,
111+
%s,
88112
%s)
89113
"""
90114
with db_conn.cursor() as curs:
@@ -102,6 +126,12 @@ def insert_map_frame(
102126
orientation,
103127
layer,
104128
__version__,
129+
ip,
130+
user_agent,
131+
geo_ip_city,
132+
geo_ip_country,
133+
geo_ip_country_iso_code,
134+
geo_ip_centroid_wgs84,
105135
),
106136
)
107137

@@ -112,6 +142,7 @@ def cleanup_map_frames():
112142
Only set file and bbox to null. Keep metadata.
113143
This function is called by a periodic celery task.
114144
"""
145+
# TODO: Should request IP, user_agent and geo ip data also be removed?
115146
query = """
116147
UPDATE
117148
map_frame

sketch_map_tool/helpers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import logging
12
from io import BytesIO
23
from pathlib import Path
34
from typing import assert_never
45
from zipfile import ZipFile
56

67
import cv2
8+
import geoip2.database
79
import numpy as np
810
from billiard.exceptions import TimeLimitExceeded
911
from celery.result import AsyncResult, GroupResult
12+
from geoip2.errors import AddressNotFoundError
1013
from geojson import Feature, FeatureCollection
1114
from numpy.typing import NDArray
1215
from reportlab.graphics.shapes import Drawing
@@ -109,3 +112,19 @@ def extract_errors(
109112
if len(errors_) > 0:
110113
errors = errors + [e.translate() for e in errors_]
111114
return errors
115+
116+
117+
def geo_ip_lookup(ip: str | None, database: Path | None) -> tuple[str | None, ...]:
118+
if ip is not None and database is not None:
119+
try:
120+
with geoip2.database.Reader(str(database)) as reader:
121+
geo_ip_city = reader.city(ip)
122+
country = geo_ip_city.country.name
123+
country_iso_code = geo_ip_city.country.iso_code
124+
city = geo_ip_city.city.name
125+
location = geo_ip_city.location
126+
centroid_wgs84 = f"POINT ({location.longitude} {location.latitude})"
127+
return (country, country_iso_code, city, centroid_wgs84)
128+
except (FileNotFoundError, AddressNotFoundError) as error:
129+
logging.error(error, exc_info=error)
130+
return (None, None, None, None)

sketch_map_tool/routes.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
)
1717
from werkzeug import Response
1818

19-
from sketch_map_tool import celery_app, config, definitions, tasks
19+
from sketch_map_tool import celery_app, definitions, tasks
2020
from sketch_map_tool import flask_app as app
21+
from sketch_map_tool.config import CONFIG
2122
from sketch_map_tool.database import client_flask as db_client_flask
2223
from sketch_map_tool.definitions import REQUEST_TYPES
2324
from sketch_map_tool.exceptions import (
@@ -31,6 +32,7 @@
3132
from sketch_map_tool.helpers import (
3233
N_,
3334
extract_errors,
35+
geo_ip_lookup,
3436
merge,
3537
to_array,
3638
zip_,
@@ -81,7 +83,7 @@ def create(lang="en") -> str:
8183
return render_template(
8284
"create.html.jinja",
8385
lang=lang,
84-
esri_api_key=config.CONFIG.esri_api_key,
86+
esri_api_key=CONFIG.esri_api_key,
8587
)
8688

8789

@@ -97,9 +99,25 @@ def create_results_post(lang="en") -> Response:
9799
size = Size(**(json.loads(request.form["size"])))
98100
scale = float(request.form["scale"])
99101
layer = validate_layer(request.form["layer"])
102+
103+
user_agent = request.headers.get("User-Agent", None)
104+
ip = request.headers.get("X-Forwarded-For", request.remote_addr)
105+
geo_ip_response = geo_ip_lookup(ip, CONFIG.geo_ip_database)
106+
100107
# Tasks
101108
task_sketch_map = tasks.generate_sketch_map.apply_async(
102-
args=(bbox, bbox_wgs84, format_, orientation, size, scale, layer)
109+
args=(
110+
bbox,
111+
bbox_wgs84,
112+
format_,
113+
orientation,
114+
size,
115+
scale,
116+
layer,
117+
ip,
118+
user_agent,
119+
*geo_ip_response,
120+
)
103121
)
104122
return redirect(
105123
url_for(

sketch_map_tool/tasks.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ def generate_sketch_map(
9898
size: Size,
9999
scale: float,
100100
layer: str,
101+
ip: str | None = None,
102+
user_agent: str | None = None,
103+
geo_ip_city: str | None = None,
104+
geo_ip_country: str | None = None,
105+
geo_ip_country_iso_code: str | None = None,
106+
geo_ip_centroid_wgs84: str | None = None,
101107
) -> BytesIO | AsyncResult:
102108
"""Generate and returns a sketch map as PDF and stores the map frame in DB."""
103109
if layer.startswith("oam"):
@@ -125,6 +131,12 @@ def generate_sketch_map(
125131
format_,
126132
orientation,
127133
layer,
134+
ip,
135+
user_agent,
136+
geo_ip_city,
137+
geo_ip_country,
138+
geo_ip_country_iso_code,
139+
geo_ip_centroid_wgs84,
128140
)
129141
return map_pdf
130142

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
('United Kingdom', 'GB', 'Boxford', 'POINT (-1.25 51.75)')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ERROR root:helpers.py:129 The address 42.2.228.64 is not in the database.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
('Germany', 'DE', None, 'POINT (10.5 51.5)')
22 KB
Binary file not shown.

tests/fixtures/geoip/README.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
GeoIP2-City-Test.mmdb
2+
3+
Source of database
4+
5+
https://github.com/maxmind/MaxMind-DB/blob/94975ab8c103b822a91f7082b5b70794e5b9fb12/test-data/GeoIP2-City-Test.mmdb
6+
7+
Source of data
8+
9+
https://github.com/maxmind/MaxMind-DB/blob/94975ab8c103b822a91f7082b5b70794e5b9fb12/source-data/GeoIP2-City-Test.json

0 commit comments

Comments
 (0)