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

#2714 geocode #2781

Merged
merged 7 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from datetime import date
from typing import List, Optional

from data_service.controller.geocode_controller import Geocoder

from data_service.model.case import observe_case_class
from data_service.model.case_exclusion_metadata import CaseExclusionMetadata
from data_service.model.case_page import CasePage
Expand All @@ -17,6 +19,7 @@
PropertyFilter,
FilterOperator,
)
from data_service.model.geojson import Feature
from data_service.util.errors import (
NotFoundError,
PreconditionUnsatisfiedError,
Expand All @@ -38,11 +41,12 @@ class to actually work with the cases collection, so that different
storage technology can be chosen.
All methods return a tuple of (response, HTTP status code)"""

def __init__(self, store, outbreak_date: date):
def __init__(self, store, outbreak_date: date, geocoder: Geocoder):
"""store is an adapter to the external storage technology.
outbreak_date is the earliest date on which this instance should accept cases."""
self.store = store
self.outbreak_date = outbreak_date
self.geocoder = geocoder
observe_case_class(case_observer)

def get_case(self, id: str):
Expand Down Expand Up @@ -305,8 +309,20 @@ def validate_updated_case(self, id: str, update: DocumentUpdate):

def create_case_if_valid(self, maybe_case: dict):
"""Attempts to create a case from an input dictionary and validate it against
the application rules. Raises ValidationError or PreconditionUnsatisfiedError on invalid input."""
the application rules. Raises ValidationError or PreconditionUnsatisfiedError on invalid input.
Raises DependencyFailedError if it has to geocode a case and the location service fails."""
if 'location' in maybe_case:
loc = maybe_case['location']
if 'query' in loc:
features = self.geocoder.locate_feature(loc['query'])
feature = features[0]
else:
# if you aren't asking for a query, you must be telling me what it is
feature = Feature.from_dict(loc)
else:
feature = None
case = Case.from_dict(maybe_case)
case.location = feature
self.check_case_preconditions(case)
return case

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import iso3166
import requests

from typing import Any, Dict, List, Union

from data_service.model.geojson import Feature, Point
from data_service.util.errors import DependencyFailedError


class Geocoder:
"""Call the location-service to identify locations."""

def __init__(self, location_service: str):
self.location_endpoint = f"{location_service}/geocode"

def locate_feature(self, query: str) -> List[Feature]:
"""Make a query to the location-service and turn its response
into GeoJSON. Additionally turns the ISO 3166-1 alpha-2 codes
used by the location-service into ISO 3166-1 alpha-3 codes.
Raises DependencyFailedError if the location-service is unreachable
or responds with any status code other than 200, or if it returns
an empty list of locations, or a location that cannot be turned
into a feature."""
response = requests.get(self.location_endpoint, {"q": query})
if response.status_code != 200:
raise DependencyFailedError(
f"Geocoding service responded with status {response.status_code} for query {query}"
)
locations = response.json()
if len(locations) == 0:
raise DependencyFailedError(
f"Geocoding service returned no locations for query {query}"
)
features = [self.create_feature(l, query) for l in locations]
return features

def create_feature(self, location: Dict[str, Union[str, float]], query: str):
"""Turn a location-service response into a GeoJSON feature."""
p = Point()
try:
geometry = location["geometry"]
p.coordinates = [geometry["latitude"], geometry["longitude"]]
except KeyError:
raise DependencyFailedError(f"location {location} doesn't have coordinates")
f = Feature()
f.geometry = p
try:
f.properties = {"country": self.iso_two_to_three(location["country"])}
except KeyError:
raise DependencyFailedError(f"location {location} doesn't have a country")
dict_set_if_present(
f.properties, "admin1", location.get("administrativeAreaLevel1", None)
)
dict_set_if_present(
f.properties, "admin2", location.get("administrativeAreaLevel2", None)
)
dict_set_if_present(
f.properties, "admin3", location.get("administrativeAreaLevel3", None)
)
dict_set_if_present(f.properties, "place", location.get("place", None))
dict_set_if_present(f.properties, "name", location.get("name", None))
dict_set_if_present(
f.properties, "resolution", location.get("geoResolution", None)
)
f.properties["query"] = query
return f

def iso_two_to_three(self, iso2: str) -> str:
"""Given an ISO-3166-1 two-letter country code like US, return a three-letter code like USA.
Raises DependencyFailedError if given a two-letter code that I cannot find."""
country = iso3166.countries.get(iso2)
if country is None:
raise DependencyFailedError(f"Country code {iso2} is not known")
return country.alpha3


def dict_set_if_present(dest: Dict, key: str, value: Any):
"""Set a key in a dictionary only if the value is not None."""
if value is not None:
dest[key] = value
6 changes: 5 additions & 1 deletion data-serving/reusable-data-service/data_service/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import date
from flask import Flask, jsonify, request
from data_service.controller.case_controller import CaseController
from data_service.controller.geocode_controller import Geocoder
from data_service.controller.schema_controller import SchemaController
from data_service.stores.mongo_store import MongoStore
from data_service.util.errors import (
Expand Down Expand Up @@ -180,7 +181,10 @@ def set_up_controllers():
outbreak_date = os.environ.get("OUTBREAK_DATE")
if outbreak_date is None:
raise ValueError("Define $OUTBREAK_DATE in the environment")
case_controller = CaseController(store, date.fromisoformat(outbreak_date))
location_service_base = os.environ.get("LOCATION_SERVICE")
if location_service_base is None:
raise ValueError("Define $LOCATION_SERVICE in the environment")
case_controller = CaseController(store, date.fromisoformat(outbreak_date), geocoder=Geocoder(location_service_base))
schema_controller = SchemaController(store)


Expand Down
Loading