Skip to content

Commit 61586cd

Browse files
committed
feat: ✨ Coordinate format documented, date format specified, fields parameter explained and zipcode approach changed
1 parent 7230338 commit 61586cd

9 files changed

Lines changed: 209 additions & 105 deletions

File tree

app/api/routes/weather.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,10 @@ def get_weather_forecast(
2828
fields: str | None = None,
2929
):
3030
"""
31-
Fetch weather forecast data for the given coordinates.
31+
Fetch hourly weather forecast data for the given coordinates and date range.
3232
33-
Query params:
34-
- latitude, longitude: required floats
35-
- fields: optional comma-separated list of hourly variables to include.
36-
Defaults to all supported fields when omitted.
33+
Coordinates must be supplied as **decimal degrees** — not degree/minute/second notation. E.g. -122.4194, 37.7749.
34+
Dates must use the **YYYY-MM-DD** format (ISO 8601).
3735
"""
3836
controller = Controller()
3937
try:

app/api/routes/zipcode.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,24 @@
55

66
router = APIRouter(prefix="/zipcode", tags=["zipcode"])
77

8-
@router.get("/postal-code")
9-
def get_postal_code(
10-
country: str,
11-
postal_code: str,
12-
):
13-
"""
14-
Fetch data for the given postal code.
158

16-
Query params:
17-
- country: required string
18-
- postal_code: required string
9+
@router.get("/lookup-address")
10+
def lookup_address(address: str):
1911
"""
20-
controller = Controller()
21-
try:
22-
return controller.get_postal_code(country, postal_code)
23-
except Exception as e:
24-
raise AppError(str(e), status_code=500)
12+
Resolve a free-form address to one or more geographic locations including
13+
postal code, latitude, longitude, place name, state, county, and country.
2514
26-
@router.get("/location")
27-
def get_location(
28-
country: str,
29-
city: str,
30-
):
31-
"""
32-
Fetch data for the given location.
15+
Returns a list of matching results ordered by relevance (most relevant first).
16+
Each result is guaranteed to include `latitude` and `longitude`; `postal_code`
17+
is present when the geocoding service can determine it for the given address.
3318
34-
Query params:
35-
- country: required string
36-
- city: required string
19+
Example:
20+
address = "1600 Amphitheatre Parkway, Mountain View, CA, US"
3721
"""
3822
controller = Controller()
3923
try:
40-
return controller.get_location(country, city)
24+
return controller.lookup_address(address)
25+
except TimeoutError as e:
26+
raise AppError(str(e), status_code=504)
4127
except Exception as e:
4228
raise AppError(str(e), status_code=500)

app/services/controller.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,5 @@ def prepare_fillable(self, pdf_path: str):
1616
def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list = None):
1717
return self.external_apis_coordinator.get_weather(latitude, longitude, start_date, end_date, hourly_fields)
1818

19-
def get_postal_code(self, country: str, postal_code: str):
20-
return self.external_apis_coordinator.get_postal_code(country, postal_code)
21-
22-
def get_location(self, country: str, city: str):
23-
return self.external_apis_coordinator.get_location(country, city)
19+
def lookup_address(self, address: str) -> list[dict]:
20+
return self.external_apis_coordinator.lookup_address(address)

app/services/external_apis/weather_api.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,6 @@ def get_weather(self, latitude: float, longitude: float, start_date: str, end_da
5555
responses = openmeteo.weather_api(url, params=params)
5656

5757
response = responses[0]
58-
print(f"Coordinates: {response.Latitude()}°N {response.Longitude()}°E")
59-
print(f"Elevation: {response.Elevation()} m asl")
60-
print(f"Timezone difference to GMT+0: {response.UtcOffsetSeconds()}s")
6158

6259
hourly = response.Hourly()
6360

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,72 @@
1-
import pgeocode
2-
import numpy as np
3-
import pandas as pd
1+
from geopy.geocoders import Nominatim
2+
from geopy.exc import GeocoderTimedOut, GeocoderServiceError
3+
4+
from app.core.logging import get_logger
5+
6+
logger = get_logger(__name__)
7+
8+
_GEOLOCATOR = Nominatim(user_agent="fireform", timeout=10)
9+
410

511
class ZipCodeAPI:
612
def __init__(self):
713
pass
814

9-
def get_postal_code(self, country: str, postal_code: str):
10-
res = pgeocode.Nominatim(country).query_postal_code(postal_code)
11-
if isinstance(res, pd.Series):
12-
res = res.replace({np.nan: None}).to_dict()
13-
elif isinstance(res, pd.DataFrame):
14-
res = res.replace({np.nan: None}).to_dict(orient='records')
15-
return res
16-
17-
def get_location(self, country: str, city: str):
18-
res = pgeocode.Nominatim(country).query_location(city)
19-
if isinstance(res, pd.Series):
20-
res = res.replace({np.nan: None}).to_dict()
21-
elif isinstance(res, pd.DataFrame):
22-
res = res.replace({np.nan: None}).to_dict(orient='records')
23-
return res
15+
def lookup_address(self, address: str) -> list[dict]:
16+
"""
17+
Look up an address string via OpenStreetMap Nominatim and return a list
18+
of matching locations with their postal codes, coordinates, and names.
19+
20+
Args:
21+
address: Full address string, e.g. "1600 Amphitheatre Parkway, Mountain View, CA, US"
22+
23+
Returns:
24+
A list of dicts, each containing:
25+
- postal_code (str | None)
26+
- place_name (str)
27+
- latitude (float)
28+
- longitude (float)
29+
- display_name (str)
30+
- country (str | None)
31+
- state (str | None)
32+
- county (str | None)
33+
"""
34+
try:
35+
locations = _GEOLOCATOR.geocode(
36+
address,
37+
exactly_one=False,
38+
addressdetails=True,
39+
limit=10,
40+
)
41+
except GeocoderTimedOut:
42+
logger.error("Nominatim geocoding timed out for address: %s", address)
43+
raise TimeoutError(f"Geocoding service timed out for address: '{address}'")
44+
except GeocoderServiceError as exc:
45+
logger.error("Nominatim geocoding service error: %s", exc)
46+
raise RuntimeError(f"Geocoding service error: {exc}")
47+
48+
if not locations:
49+
logger.info("No results found for address: %s", address)
50+
return []
51+
52+
results = []
53+
for loc in locations:
54+
addr = loc.raw.get("address", {})
55+
results.append({
56+
"postal_code": addr.get("postcode"),
57+
"place_name": addr.get("city")
58+
or addr.get("town")
59+
or addr.get("village")
60+
or addr.get("hamlet")
61+
or addr.get("municipality")
62+
or "",
63+
"latitude": loc.latitude,
64+
"longitude": loc.longitude,
65+
"display_name": loc.address,
66+
"country": addr.get("country"),
67+
"state": addr.get("state"),
68+
"county": addr.get("county"),
69+
})
70+
logger.debug("Geocoded result: %s", results[-1])
71+
72+
return results

app/services/external_apis_coordinator.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,5 @@ def __init__(self):
99
def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list[str] | None = None):
1010
return self.weather_api.get_weather(latitude, longitude, start_date, end_date, hourly_fields)
1111

12-
def get_postal_code(self, country: str, postal_code: str):
13-
return self.zipcode_api.get_postal_code(country, postal_code)
14-
15-
def get_location(self, country: str, city: str):
16-
return self.zipcode_api.get_location(country, city)
12+
def lookup_address(self, address: str) -> list[dict]:
13+
return self.zipcode_api.lookup_address(address)

contracts/path/weather.yaml

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,108 @@
11
forecast:
22
get:
33
summary: Fetch weather forecast
4-
description: Fetch weather data for the given location and fields.
4+
description: >
5+
Fetch hourly weather data for the given location and date range.
6+
Coordinates must be provided as decimal degrees (e.g. `40.7128` for latitude, `-74.0060` for longitude).
57
tags:
68
- external-apis
79
parameters:
810
- name: latitude
911
in: query
1012
required: true
13+
description: >
14+
Latitude of the location in decimal degrees. Valid range: -90.0 to 90.0.
15+
Example: `40.7128` (New York City). Do NOT use degree/minute/second notation.
1116
schema:
1217
type: number
1318
format: float
19+
minimum: -90.0
20+
maximum: 90.0
21+
example: 40.7128
1422
- name: longitude
1523
in: query
1624
required: true
25+
description: >
26+
Longitude of the location in decimal degrees. Valid range: -180.0 to 180.0.
27+
Example: `-74.0060` (New York City). Do NOT use degree/minute/second notation.
1728
schema:
1829
type: number
1930
format: float
31+
minimum: -180.0
32+
maximum: 180.0
33+
example: -74.0060
2034
- name: start_date
2135
in: query
2236
required: true
37+
description: >
38+
Start date of the forecast period. Format: `YYYY-MM-DD` (ISO 8601).
39+
Example: `2024-06-01`.
2340
schema:
2441
type: string
2542
format: date
43+
example: "2024-06-01"
2644
- name: end_date
2745
in: query
2846
required: true
47+
description: >
48+
End date of the forecast period (inclusive). Format: `YYYY-MM-DD` (ISO 8601).
49+
Example: `2024-06-07`. Must be >= start_date.
2950
schema:
3051
type: string
3152
format: date
53+
example: "2024-06-07"
3254
- name: fields
3355
in: query
3456
required: false
57+
description: >
58+
Comma-separated list of hourly variables to include in the response.
59+
When omitted, all supported variables are returned.
60+
Supported values: `temperature_2m`, `relative_humidity_2m`, `rain`,
61+
`apparent_temperature`, `precipitation_probability`, `precipitation`,
62+
`wind_direction_10m`, `wind_speed_10m`, `soil_temperature_0cm`, `uv_index`.
63+
Example: `temperature_2m,rain,wind_speed_10m`.
3564
schema:
3665
type: string
37-
description: Comma-separated list of hourly variables to request.
66+
example: "temperature_2m,rain,wind_speed_10m"
3867
responses:
3968
'200':
4069
description: Weather data retrieved successfully
4170
content:
4271
application/json:
4372
schema:
4473
type: object
45-
additionalProperties: true
74+
properties:
75+
latitude:
76+
type: number
77+
description: Actual latitude used by the weather model (may differ slightly from input).
78+
longitude:
79+
type: number
80+
description: Actual longitude used by the weather model.
81+
elevation:
82+
type: number
83+
description: Elevation of the location in metres above sea level.
84+
timezone:
85+
type: string
86+
description: Timezone name (e.g. "GMT").
87+
timezone_abbreviation:
88+
type: string
89+
hourly:
90+
type: object
91+
description: >
92+
Object containing hourly time series. The `date` key holds an array
93+
of ISO 8601 timestamps; all other keys correspond to the requested
94+
fields and contain arrays of the same length.
95+
properties:
96+
date:
97+
type: array
98+
items:
99+
type: string
100+
format: date-time
101+
additionalProperties:
102+
type: array
103+
items:
104+
type: number
105+
'422':
106+
description: Validation error (e.g. coordinates out of range, invalid date format)
46107
'500':
47108
description: Internal server error

0 commit comments

Comments
 (0)