1+ """Main logic."""
2+
3+ import gzip
14import importlib .resources
2- from pg_nearest_city import base_nearest_city
5+ import logging
6+ from contextlib import asynccontextmanager
7+ from typing import Optional
8+
39import psycopg
4- import gzip
510from psycopg import AsyncCursor
611
7- from typing import Optional , Union
8- from pg_nearest_city .base_nearest_city import BaseNearestCity
9- from pg_nearest_city .base_nearest_city import Location
10- from pg_nearest_city .base_nearest_city import InitializationStatus
11- from contextlib import asynccontextmanager
12-
13- import logging
14- from typing import Optional
12+ from pg_nearest_city import base_nearest_city
13+ from pg_nearest_city .base_nearest_city import (
14+ BaseNearestCity ,
15+ InitializationStatus ,
16+ Location ,
17+ )
1518
1619logger = logging .getLogger ("pg_nearest_city" )
1720
21+
1822class AsyncNearestCity :
23+ """Reverse geocoding to the nearest city over 1000 population."""
24+
1925 @classmethod
2026 @asynccontextmanager
2127 async def connect (cls , db : psycopg .AsyncConnection | base_nearest_city .DbConfig ):
22- """Create a managed NearestCity instance with automatic initialization and cleanup.
23-
28+ """Managed NearestCity instance with automatic initialization and cleanup.
29+
2430 Args:
2531 db: Either a DbConfig for a new connection or an existing psycopg Connection
2632 """
@@ -34,20 +40,24 @@ async def connect(cls, db: psycopg.AsyncConnection | base_nearest_city.DbConfig)
3440 conn = await psycopg .AsyncConnection .connect (db .get_connection_string ())
3541
3642 geocoder = cls (conn )
37-
43+
3844 try :
3945 await geocoder .initialize ()
4046 yield geocoder
4147 finally :
4248 if not is_external_connection :
4349 await conn .close ()
4450
51+ def __init__ (
52+ self ,
53+ connection : psycopg .AsyncConnection ,
54+ logger : Optional [logging .Logger ] = None ,
55+ ):
56+ """Initialize reverse geocoder with an existing AsyncConnection.
4557
46- def __init__ (self , connection : psycopg .AsyncConnection , logger : Optional [logging .Logger ] = None ):
47- """Initialize reverse geocoder with an existing AsyncConnection
48-
4958 Args:
5059 db: An existing psycopg AsyncConnection
60+ connection: psycopg.AsyncConnection
5161 logger: Optional custom logger. If not provided, uses package logger
5262 """
5363 # Allow users to provide their own logger while having a sensible default
@@ -69,51 +79,52 @@ async def initialize(self) -> None:
6979 async with self .connection .cursor () as cur :
7080 self ._logger .info ("Starting database initialization check" )
7181 status = await self ._check_initialization_status (cur )
72-
82+
7383 if status .is_fully_initialized :
7484 self ._logger .info ("Database already properly initialized" )
7585 return
76-
86+
7787 if status .has_table and not status .is_fully_initialized :
7888 missing = status .get_missing_components ()
7989 self ._logger .warning (
80- "Database needs repair. Missing components: %s" ,
81- ", " .join (missing )
90+ "Database needs repair. Missing components: %s" ,
91+ ", " .join (missing ),
8292 )
8393 self ._logger .info ("Reinitializing from scratch" )
8494 await cur .execute ("DROP TABLE IF EXISTS pg_nearest_city_geocoding;" )
85-
95+
8696 self ._logger .info ("Creating geocoding table" )
8797 await self ._create_geocoding_table (cur )
88-
98+
8999 self ._logger .info ("Importing city data" )
90100 await self ._import_cities (cur )
91-
101+
92102 self ._logger .info ("Processing Voronoi polygons" )
93103 await self ._import_voronoi_polygons (cur )
94-
104+
95105 self ._logger .info ("Creating spatial index" )
96106 await self ._create_spatial_index (cur )
97-
107+
98108 await self .connection .commit ()
99-
109+
100110 self ._logger .debug ("Verifying final initialization state" )
101111 final_status = await self ._check_initialization_status (cur )
102112 if not final_status .is_fully_initialized :
103113 missing = final_status .get_missing_components ()
104114 self ._logger .error (
105115 "Initialization failed final validation. Missing: %s" ,
106- ", " .join (missing )
116+ ", " .join (missing ),
107117 )
108118 raise RuntimeError (
109- f"Initialization failed final validation. Missing components: { ', ' .join (missing )} "
119+ "Initialization failed final validation. "
120+ f"Missing components: { ', ' .join (missing )} "
110121 )
111-
122+
112123 self ._logger .info ("Initialization complete and verified" )
113-
124+
114125 except Exception as e :
115126 self ._logger .error ("Database initialization failed: %s" , str (e ))
116- raise RuntimeError (f"Database initialization failed: { str (e )} " )
127+ raise RuntimeError (f"Database initialization failed: { str (e )} " ) from e
117128
118129 async def query (self , lat : float , lon : float ) -> Optional [Location ]:
119130 """Find the nearest city to the given coordinates using Voronoi regions.
@@ -129,7 +140,6 @@ async def query(self, lat: float, lon: float) -> Optional[Location]:
129140 ValueError: If coordinates are out of valid ranges
130141 RuntimeError: If database query fails
131142 """
132-
133143 # Validate coordinate ranges
134144 BaseNearestCity .validate_coordinates (lon , lat )
135145
@@ -151,26 +161,27 @@ async def query(self, lat: float, lon: float) -> Optional[Location]:
151161 )
152162 except Exception as e :
153163 self ._logger .error (f"Reverse geocoding failed: { str (e )} " )
154- raise RuntimeError (f"Reverse geocoding failed: { str (e )} " )
164+ raise RuntimeError (f"Reverse geocoding failed: { str (e )} " ) from e
155165
156-
157- async def _check_initialization_status (self , cur : psycopg .AsyncCursor ) -> InitializationStatus :
166+ async def _check_initialization_status (
167+ self , cur : psycopg .AsyncCursor
168+ ) -> InitializationStatus :
158169 """Check the status and integrity of the geocoding database.
159170
160- Performs essential validation checks to ensure the database is properly initialized
161- and contains valid data.
171+ Performs essential validation checks to ensure the database is
172+ properly initialized and contains valid data.
162173 """
163174 status = InitializationStatus ()
164-
175+
165176 # Check table existence
166- await cur .execute (BaseNearestCity ._get_table_existance_query ())
177+ await cur .execute (BaseNearestCity ._get_tableexistence_query ())
167178 table_exists = await cur .fetchone ()
168179 status .has_table = bool (table_exists and table_exists [0 ])
169-
180+
170181 # If table doesn't exist, we can't check other properties
171182 if not status .has_table :
172183 return status
173-
184+
174185 # Check table structure
175186 await cur .execute (BaseNearestCity ._get_table_structure_query ())
176187 columns = {col : dtype for col , dtype in await cur .fetchall ()}
@@ -186,20 +197,20 @@ async def _check_initialization_status(self, cur: psycopg.AsyncCursor) -> Initia
186197 # If table doesn't have valid structure, we can't check other properties
187198 if not status .has_valid_structure :
188199 return status
189-
200+
190201 # Check data completeness
191202 await cur .execute (BaseNearestCity ._get_data_completeness_query ())
192203 counts = await cur .fetchone ()
193204 total_cities , cities_with_voronoi = counts
194-
205+
195206 status .has_data = total_cities > 0
196207 status .has_complete_voronoi = cities_with_voronoi == total_cities
197-
208+
198209 # Check spatial index
199210 await cur .execute (BaseNearestCity ._get_spatial_index_check_query ())
200211 has_index = await cur .fetchone ()
201212 status .has_spatial_index = bool (has_index and has_index [0 ])
202-
213+
203214 return status
204215
205216 async def _import_cities (self , cur : AsyncCursor ):
@@ -218,21 +229,22 @@ async def _import_cities(self, cur: AsyncCursor):
218229 self ._logger .info (f"Imported { copied_bytes :,} bytes of city data" )
219230
220231 async def _create_geocoding_table (self , cur : AsyncCursor ):
221- """Create the main table"""
232+ """Create the main table. """
222233 await cur .execute ("""
223234 CREATE TABLE pg_nearest_city_geocoding (
224235 city varchar,
225236 country varchar,
226237 lat decimal,
227238 lon decimal,
228- geom geometry(Point,4326) GENERATED ALWAYS AS (ST_SetSRID(ST_MakePoint(lon, lat), 4326)) STORED,
239+ geom geometry(Point,4326)
240+ GENERATED ALWAYS AS (ST_SetSRID(ST_MakePoint(lon, lat), 4326))
241+ STORED,
229242 voronoi geometry(Polygon,4326)
230243 );
231244 """ )
232245
233246 async def _import_voronoi_polygons (self , cur : AsyncCursor ):
234247 """Import and integrate Voronoi polygons into the main table."""
235-
236248 if not self .voronoi_file .exists ():
237249 raise FileNotFoundError (f"Voronoi file not found: { self .voronoi_file } " )
238250
@@ -268,7 +280,7 @@ async def _import_voronoi_polygons(self, cur: AsyncCursor):
268280 async def _create_spatial_index (self , cur : AsyncCursor ):
269281 """Create a spatial index on the Voronoi polygons for efficient queries."""
270282 await cur .execute ("""
271- CREATE INDEX geocoding_voronoi_idx
272- ON pg_nearest_city_geocoding
283+ CREATE INDEX geocoding_voronoi_idx
284+ ON pg_nearest_city_geocoding
273285 USING GIST (voronoi);
274286 """ )
0 commit comments