Skip to content

Commit 38ffa12

Browse files
committed
feat: create small component for data.coordinates
1 parent 1d9327d commit 38ffa12

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

data/cities.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import data.db_connect as dbc
7+
from data.coordinates import Coordinates
78
from data.utils import sanitize_string, sanitize_code
89
from datetime import UTC, datetime
910
from data.cache import city_by_name_state_cache
@@ -35,6 +36,10 @@
3536
}
3637

3738

39+
def _validate_coordinates(coordinates: dict) -> dict:
40+
return Coordinates.from_dict(coordinates).to_dict()
41+
42+
3843
def get_cities() -> list:
3944
"""
4045
Returns a list of all cities
@@ -154,6 +159,8 @@ def add_city(city_data: dict) -> bool:
154159
city_data[STATE_CODE] = sanitize_code(city_data[STATE_CODE])
155160
if COUNTRY_CODE in city_data:
156161
city_data[COUNTRY_CODE] = sanitize_code(city_data[COUNTRY_CODE])
162+
if COORDINATES in city_data:
163+
city_data[COORDINATES] = _validate_coordinates(city_data[COORDINATES])
157164

158165
# Validate that state_code exists if provided
159166
if STATE_CODE in city_data and city_data[STATE_CODE]:
@@ -221,6 +228,8 @@ def update_city(name: str, state_code: str, update_data: dict) -> bool:
221228
# Sanitize string fields in update
222229
if COUNTRY_CODE in update_data:
223230
update_data[COUNTRY_CODE] = sanitize_code(update_data[COUNTRY_CODE])
231+
if COORDINATES in update_data:
232+
update_data[COORDINATES] = _validate_coordinates(update_data[COORDINATES])
224233

225234
# Prevent updating the name or state_code fields directly
226235
if CITY_NAME in update_data:
@@ -255,6 +264,8 @@ def update_city_by_name_and_country(
255264
# Sanitize string fields in update
256265
if STATE_CODE in update_data:
257266
update_data[STATE_CODE] = sanitize_code(update_data[STATE_CODE])
267+
if COORDINATES in update_data:
268+
update_data[COORDINATES] = _validate_coordinates(update_data[COORDINATES])
258269

259270
# Prevent updating the name or country_code fields directly
260271
if CITY_NAME in update_data:

data/coordinates.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""
2+
Validated coordinate field types for city data.
3+
"""
4+
5+
from abc import ABC, abstractmethod
6+
7+
LATITUDE = "latitude"
8+
LONGITUDE = "longitude"
9+
10+
MIN_LATITUDE = -90
11+
MAX_LATITUDE = 90
12+
MIN_LONGITUDE = -180
13+
MAX_LONGITUDE = 180
14+
15+
16+
class Coordinate(ABC):
17+
def __init__(self, value: int | float):
18+
numeric_value = self._validate_type(value)
19+
lower_bound, upper_bound = self.bounds()
20+
if numeric_value < lower_bound or numeric_value > upper_bound:
21+
raise ValueError(
22+
f"Bad value for {self.field_name()}: {numeric_value}. "
23+
f"Expected between {lower_bound} and {upper_bound}"
24+
)
25+
self.value = numeric_value
26+
27+
@classmethod
28+
@abstractmethod
29+
def bounds(cls) -> tuple[float, float]:
30+
"""Return the inclusive coordinate bounds."""
31+
32+
@classmethod
33+
def field_name(cls) -> str:
34+
return cls.__name__.lower()
35+
36+
def _validate_type(self, value: int | float) -> float:
37+
if isinstance(value, bool) or not isinstance(value, (int, float)):
38+
raise TypeError(f"Bad type for value: {type(value)}")
39+
return float(value)
40+
41+
def __float__(self) -> float:
42+
return self.value
43+
44+
def __str__(self) -> str:
45+
return str(self.value)
46+
47+
48+
class Latitude(Coordinate):
49+
@classmethod
50+
def bounds(cls) -> tuple[float, float]:
51+
return MIN_LATITUDE, MAX_LATITUDE
52+
53+
54+
class Longitude(Coordinate):
55+
@classmethod
56+
def bounds(cls) -> tuple[float, float]:
57+
return MIN_LONGITUDE, MAX_LONGITUDE
58+
59+
60+
class Coordinates:
61+
def __init__(
62+
self,
63+
latitude: int | float | Latitude,
64+
longitude: int | float | Longitude,
65+
):
66+
self.latitude = latitude if isinstance(latitude, Latitude) else Latitude(latitude)
67+
self.longitude = (
68+
longitude if isinstance(longitude, Longitude) else Longitude(longitude)
69+
)
70+
71+
@classmethod
72+
def from_dict(cls, coordinates: dict) -> "Coordinates":
73+
if not isinstance(coordinates, dict):
74+
raise TypeError(f"Bad type for coordinates: {type(coordinates)}")
75+
if LATITUDE not in coordinates:
76+
raise ValueError("Missing latitude in coordinates")
77+
if LONGITUDE not in coordinates:
78+
raise ValueError("Missing longitude in coordinates")
79+
return cls(coordinates[LATITUDE], coordinates[LONGITUDE])
80+
81+
def to_dict(self) -> dict:
82+
return {
83+
LATITUDE: float(self.latitude),
84+
LONGITUDE: float(self.longitude),
85+
}

0 commit comments

Comments
 (0)