quantaroute-geocoding 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of quantaroute-geocoding might be problematic. Click here for more details.

@@ -0,0 +1,32 @@
1
+ """
2
+ QuantaRoute Geocoding Python SDK
3
+
4
+ A Python library for geocoding addresses to DigiPin codes with both online API
5
+ and offline processing capabilities.
6
+ """
7
+
8
+ __version__ = "1.0.0"
9
+ __author__ = "QuantaRoute"
10
+ __email__ = "support@quantaroute.com"
11
+
12
+ from .client import QuantaRouteClient
13
+ from .offline import OfflineProcessor
14
+ from .csv_processor import CSVProcessor
15
+ from .exceptions import (
16
+ QuantaRouteError,
17
+ APIError,
18
+ RateLimitError,
19
+ AuthenticationError,
20
+ ValidationError
21
+ )
22
+
23
+ __all__ = [
24
+ "QuantaRouteClient",
25
+ "OfflineProcessor",
26
+ "CSVProcessor",
27
+ "QuantaRouteError",
28
+ "APIError",
29
+ "RateLimitError",
30
+ "AuthenticationError",
31
+ "ValidationError"
32
+ ]
@@ -0,0 +1,281 @@
1
+ """
2
+ Command-line interface for QuantaRoute Geocoding
3
+ """
4
+
5
+ import click
6
+ import os
7
+ from typing import Optional
8
+
9
+ from .csv_processor import CSVProcessor
10
+ from .client import QuantaRouteClient
11
+ from .offline import OfflineProcessor
12
+ from .exceptions import QuantaRouteError
13
+
14
+
15
+ @click.group()
16
+ @click.version_option(version="1.0.0")
17
+ def main():
18
+ """QuantaRoute Geocoding CLI - Process addresses and coordinates with DigiPin"""
19
+ pass
20
+
21
+
22
+ @main.command()
23
+ @click.argument('input_file', type=click.Path(exists=True))
24
+ @click.argument('output_file', type=click.Path())
25
+ @click.option('--api-key', envvar='QUANTAROUTE_API_KEY', help='QuantaRoute API key')
26
+ @click.option('--address-column', default='address', help='Name of address column')
27
+ @click.option('--city-column', help='Name of city column')
28
+ @click.option('--state-column', help='Name of state column')
29
+ @click.option('--pincode-column', help='Name of pincode column')
30
+ @click.option('--country-column', help='Name of country column')
31
+ @click.option('--batch-size', default=50, help='Batch size for API requests')
32
+ @click.option('--delay', default=1.0, help='Delay between batches (seconds)')
33
+ @click.option('--offline', is_flag=True, help='Use offline processing (limited functionality)')
34
+ def geocode(
35
+ input_file: str,
36
+ output_file: str,
37
+ api_key: Optional[str],
38
+ address_column: str,
39
+ city_column: Optional[str],
40
+ state_column: Optional[str],
41
+ pincode_column: Optional[str],
42
+ country_column: Optional[str],
43
+ batch_size: int,
44
+ delay: float,
45
+ offline: bool
46
+ ):
47
+ """Geocode addresses from CSV file to DigiPin codes"""
48
+
49
+ if not offline and not api_key:
50
+ click.echo("Error: API key is required for online geocoding. Set QUANTAROUTE_API_KEY environment variable or use --api-key option.")
51
+ return
52
+
53
+ try:
54
+ processor = CSVProcessor(
55
+ api_key=api_key,
56
+ use_offline=offline,
57
+ batch_size=batch_size,
58
+ delay_between_batches=delay
59
+ )
60
+
61
+ def progress_callback(processed, total, success, errors):
62
+ click.echo(f"Progress: {processed}/{total} ({processed/total*100:.1f}%) - Success: {success}, Errors: {errors}")
63
+
64
+ click.echo(f"Processing {input_file}...")
65
+
66
+ result = processor.process_geocoding_csv(
67
+ input_file=input_file,
68
+ output_file=output_file,
69
+ address_column=address_column,
70
+ city_column=city_column,
71
+ state_column=state_column,
72
+ pincode_column=pincode_column,
73
+ country_column=country_column,
74
+ progress_callback=progress_callback
75
+ )
76
+
77
+ click.echo(f"\nProcessing complete!")
78
+ click.echo(f"Total rows: {result['total_rows']}")
79
+ click.echo(f"Successful: {result['success_count']}")
80
+ click.echo(f"Errors: {result['error_count']}")
81
+ click.echo(f"Success rate: {result['success_rate']:.1%}")
82
+ click.echo(f"Output saved to: {result['output_file']}")
83
+
84
+ except QuantaRouteError as e:
85
+ click.echo(f"Error: {str(e)}", err=True)
86
+ except Exception as e:
87
+ click.echo(f"Unexpected error: {str(e)}", err=True)
88
+
89
+
90
+ @main.command()
91
+ @click.argument('input_file', type=click.Path(exists=True))
92
+ @click.argument('output_file', type=click.Path())
93
+ @click.option('--api-key', envvar='QUANTAROUTE_API_KEY', help='QuantaRoute API key')
94
+ @click.option('--latitude-column', default='latitude', help='Name of latitude column')
95
+ @click.option('--longitude-column', default='longitude', help='Name of longitude column')
96
+ @click.option('--offline', is_flag=True, help='Use offline processing')
97
+ def coords_to_digipin(
98
+ input_file: str,
99
+ output_file: str,
100
+ api_key: Optional[str],
101
+ latitude_column: str,
102
+ longitude_column: str,
103
+ offline: bool
104
+ ):
105
+ """Convert coordinates to DigiPin codes from CSV file"""
106
+
107
+ if not offline and not api_key:
108
+ click.echo("Error: API key is required for online processing. Set QUANTAROUTE_API_KEY environment variable or use --api-key option.")
109
+ return
110
+
111
+ try:
112
+ processor = CSVProcessor(
113
+ api_key=api_key,
114
+ use_offline=offline
115
+ )
116
+
117
+ def progress_callback(processed, total, success, errors):
118
+ click.echo(f"Progress: {processed}/{total} ({processed/total*100:.1f}%) - Success: {success}, Errors: {errors}")
119
+
120
+ click.echo(f"Processing {input_file}...")
121
+
122
+ result = processor.process_coordinates_to_digipin_csv(
123
+ input_file=input_file,
124
+ output_file=output_file,
125
+ latitude_column=latitude_column,
126
+ longitude_column=longitude_column,
127
+ progress_callback=progress_callback
128
+ )
129
+
130
+ click.echo(f"\nProcessing complete!")
131
+ click.echo(f"Total rows: {result['total_rows']}")
132
+ click.echo(f"Successful: {result['success_count']}")
133
+ click.echo(f"Errors: {result['error_count']}")
134
+ click.echo(f"Success rate: {result['success_rate']:.1%}")
135
+ click.echo(f"Output saved to: {result['output_file']}")
136
+
137
+ except QuantaRouteError as e:
138
+ click.echo(f"Error: {str(e)}", err=True)
139
+ except Exception as e:
140
+ click.echo(f"Unexpected error: {str(e)}", err=True)
141
+
142
+
143
+ @main.command()
144
+ @click.argument('input_file', type=click.Path(exists=True))
145
+ @click.argument('output_file', type=click.Path())
146
+ @click.option('--api-key', envvar='QUANTAROUTE_API_KEY', help='QuantaRoute API key')
147
+ @click.option('--digipin-column', default='digipin', help='Name of DigiPin column')
148
+ @click.option('--offline', is_flag=True, help='Use offline processing')
149
+ def digipin_to_coords(
150
+ input_file: str,
151
+ output_file: str,
152
+ api_key: Optional[str],
153
+ digipin_column: str,
154
+ offline: bool
155
+ ):
156
+ """Convert DigiPin codes to coordinates from CSV file"""
157
+
158
+ if not offline and not api_key:
159
+ click.echo("Error: API key is required for online processing. Set QUANTAROUTE_API_KEY environment variable or use --api-key option.")
160
+ return
161
+
162
+ try:
163
+ processor = CSVProcessor(
164
+ api_key=api_key,
165
+ use_offline=offline
166
+ )
167
+
168
+ def progress_callback(processed, total, success, errors):
169
+ click.echo(f"Progress: {processed}/{total} ({processed/total*100:.1f}%) - Success: {success}, Errors: {errors}")
170
+
171
+ click.echo(f"Processing {input_file}...")
172
+
173
+ result = processor.process_digipin_to_coordinates_csv(
174
+ input_file=input_file,
175
+ output_file=output_file,
176
+ digipin_column=digipin_column,
177
+ progress_callback=progress_callback
178
+ )
179
+
180
+ click.echo(f"\nProcessing complete!")
181
+ click.echo(f"Total rows: {result['total_rows']}")
182
+ click.echo(f"Successful: {result['success_count']}")
183
+ click.echo(f"Errors: {result['error_count']}")
184
+ click.echo(f"Success rate: {result['success_rate']:.1%}")
185
+ click.echo(f"Output saved to: {result['output_file']}")
186
+
187
+ except QuantaRouteError as e:
188
+ click.echo(f"Error: {str(e)}", err=True)
189
+ except Exception as e:
190
+ click.echo(f"Unexpected error: {str(e)}", err=True)
191
+
192
+
193
+ @main.command()
194
+ @click.option('--api-key', envvar='QUANTAROUTE_API_KEY', required=True, help='QuantaRoute API key')
195
+ def usage():
196
+ """Check API usage statistics"""
197
+
198
+ try:
199
+ client = QuantaRouteClient(api_key)
200
+ usage_data = client.get_usage()
201
+
202
+ click.echo("API Usage Statistics:")
203
+ click.echo("-" * 30)
204
+
205
+ usage_info = usage_data.get('usage', {})
206
+ click.echo(f"Current Usage: {usage_info.get('currentUsage', 0)}")
207
+ click.echo(f"Monthly Limit: {usage_info.get('monthlyLimit', 'Unknown')}")
208
+ click.echo(f"Tier: {usage_info.get('tier', 'Unknown')}")
209
+ click.echo(f"Reset Date: {usage_info.get('resetDate', 'Unknown')}")
210
+
211
+ rate_limit = usage_data.get('rateLimit', {})
212
+ click.echo(f"\nRate Limit:")
213
+ click.echo(f"Requests per minute: {rate_limit.get('limit', 'Unknown')}")
214
+ click.echo(f"Remaining: {rate_limit.get('remaining', 'Unknown')}")
215
+
216
+ except QuantaRouteError as e:
217
+ click.echo(f"Error: {str(e)}", err=True)
218
+ except Exception as e:
219
+ click.echo(f"Unexpected error: {str(e)}", err=True)
220
+
221
+
222
+ @main.command()
223
+ @click.argument('latitude', type=float)
224
+ @click.argument('longitude', type=float)
225
+ @click.option('--api-key', envvar='QUANTAROUTE_API_KEY', help='QuantaRoute API key')
226
+ @click.option('--offline', is_flag=True, help='Use offline processing')
227
+ def single_coord_to_digipin(latitude: float, longitude: float, api_key: Optional[str], offline: bool):
228
+ """Convert single coordinate pair to DigiPin"""
229
+
230
+ try:
231
+ if offline:
232
+ processor = OfflineProcessor()
233
+ result = processor.coordinates_to_digipin(latitude, longitude)
234
+ else:
235
+ if not api_key:
236
+ click.echo("Error: API key is required for online processing.")
237
+ return
238
+
239
+ client = QuantaRouteClient(api_key)
240
+ result = client.coordinates_to_digipin(latitude, longitude)
241
+
242
+ click.echo(f"Coordinates: {latitude}, {longitude}")
243
+ click.echo(f"DigiPin: {result['digipin']}")
244
+
245
+ except QuantaRouteError as e:
246
+ click.echo(f"Error: {str(e)}", err=True)
247
+ except Exception as e:
248
+ click.echo(f"Unexpected error: {str(e)}", err=True)
249
+
250
+
251
+ @main.command()
252
+ @click.argument('digipin_code')
253
+ @click.option('--api-key', envvar='QUANTAROUTE_API_KEY', help='QuantaRoute API key')
254
+ @click.option('--offline', is_flag=True, help='Use offline processing')
255
+ def single_digipin_to_coords(digipin_code: str, api_key: Optional[str], offline: bool):
256
+ """Convert single DigiPin to coordinates"""
257
+
258
+ try:
259
+ if offline:
260
+ processor = OfflineProcessor()
261
+ result = processor.digipin_to_coordinates(digipin_code)
262
+ else:
263
+ if not api_key:
264
+ click.echo("Error: API key is required for online processing.")
265
+ return
266
+
267
+ client = QuantaRouteClient(api_key)
268
+ result = client.reverse_geocode(digipin_code)
269
+
270
+ coords = result['coordinates']
271
+ click.echo(f"DigiPin: {digipin_code}")
272
+ click.echo(f"Coordinates: {coords['latitude']}, {coords['longitude']}")
273
+
274
+ except QuantaRouteError as e:
275
+ click.echo(f"Error: {str(e)}", err=True)
276
+ except Exception as e:
277
+ click.echo(f"Unexpected error: {str(e)}", err=True)
278
+
279
+
280
+ if __name__ == '__main__':
281
+ main()
@@ -0,0 +1,312 @@
1
+ """
2
+ QuantaRoute Geocoding API Client
3
+ """
4
+
5
+ import requests
6
+ import time
7
+ from typing import Dict, List, Optional, Union
8
+ from .exceptions import APIError, RateLimitError, AuthenticationError, ValidationError
9
+
10
+
11
+ class QuantaRouteClient:
12
+ """
13
+ Client for QuantaRoute Geocoding API
14
+
15
+ Provides methods to interact with the QuantaRoute Geocoding API for
16
+ address geocoding, reverse geocoding, and DigiPin operations.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ api_key: str,
22
+ base_url: str = "https://api.quantaroute.com",
23
+ timeout: int = 30,
24
+ max_retries: int = 3
25
+ ):
26
+ """
27
+ Initialize the QuantaRoute client
28
+
29
+ Args:
30
+ api_key: Your QuantaRoute API key
31
+ base_url: Base URL for the API (default: https://api.quantaroute.com)
32
+ timeout: Request timeout in seconds (default: 30)
33
+ max_retries: Maximum number of retries for failed requests (default: 3)
34
+ """
35
+ self.api_key = api_key
36
+ self.base_url = base_url.rstrip('/')
37
+ self.timeout = timeout
38
+ self.max_retries = max_retries
39
+ self.session = requests.Session()
40
+ self.session.headers.update({
41
+ 'x-api-key': api_key,
42
+ 'User-Agent': 'quantaroute-geocoding-python/1.0.0',
43
+ 'Content-Type': 'application/json'
44
+ })
45
+
46
+ def _make_request(
47
+ self,
48
+ method: str,
49
+ endpoint: str,
50
+ data: Optional[Dict] = None,
51
+ params: Optional[Dict] = None
52
+ ) -> Dict:
53
+ """Make HTTP request with retry logic"""
54
+ url = f"{self.base_url}{endpoint}"
55
+
56
+ for attempt in range(self.max_retries + 1):
57
+ try:
58
+ if method.upper() == 'GET':
59
+ response = self.session.get(url, params=params, timeout=self.timeout)
60
+ else:
61
+ response = self.session.post(url, json=data, params=params, timeout=self.timeout)
62
+
63
+ # Handle rate limiting
64
+ if response.status_code == 429:
65
+ retry_after = int(response.headers.get('Retry-After', 60))
66
+ if attempt < self.max_retries:
67
+ time.sleep(retry_after)
68
+ continue
69
+ raise RateLimitError(
70
+ "Rate limit exceeded",
71
+ retry_after=retry_after
72
+ )
73
+
74
+ # Handle authentication errors
75
+ if response.status_code == 401:
76
+ raise AuthenticationError()
77
+
78
+ # Handle other client/server errors
79
+ if not response.ok:
80
+ try:
81
+ error_data = response.json()
82
+ message = error_data.get('message', f'HTTP {response.status_code}')
83
+ error_code = error_data.get('code')
84
+ except:
85
+ message = f'HTTP {response.status_code}: {response.text}'
86
+ error_code = None
87
+
88
+ raise APIError(message, response.status_code, error_code)
89
+
90
+ return response.json()
91
+
92
+ except requests.exceptions.RequestException as e:
93
+ if attempt < self.max_retries:
94
+ time.sleep(2 ** attempt) # Exponential backoff
95
+ continue
96
+ raise APIError(f"Request failed: {str(e)}")
97
+
98
+ raise APIError("Max retries exceeded")
99
+
100
+ def geocode(
101
+ self,
102
+ address: str,
103
+ city: Optional[str] = None,
104
+ state: Optional[str] = None,
105
+ pincode: Optional[str] = None,
106
+ country: Optional[str] = None
107
+ ) -> Dict:
108
+ """
109
+ Geocode an address to get DigiPin and coordinates
110
+
111
+ Args:
112
+ address: The address to geocode
113
+ city: City name (optional)
114
+ state: State name (optional)
115
+ pincode: Postal code (optional)
116
+ country: Country name (optional, defaults to India)
117
+
118
+ Returns:
119
+ Dict containing DigiPin, coordinates, and address information
120
+ """
121
+ if not address or not address.strip():
122
+ raise ValidationError("Address is required")
123
+
124
+ data = {
125
+ 'address': address.strip(),
126
+ 'city': city,
127
+ 'state': state,
128
+ 'pincode': pincode,
129
+ 'country': country or 'India'
130
+ }
131
+
132
+ # Remove None values
133
+ data = {k: v for k, v in data.items() if v is not None}
134
+
135
+ response = self._make_request('POST', '/v1/digipin/geocode', data)
136
+ return response.get('data', {})
137
+
138
+ def reverse_geocode(self, digipin: str) -> Dict:
139
+ """
140
+ Reverse geocode a DigiPin to get coordinates and address
141
+
142
+ Args:
143
+ digipin: The DigiPin code to reverse geocode
144
+
145
+ Returns:
146
+ Dict containing coordinates and address information
147
+ """
148
+ if not digipin or not digipin.strip():
149
+ raise ValidationError("DigiPin is required")
150
+
151
+ data = {'digipin': digipin.strip()}
152
+ response = self._make_request('POST', '/v1/digipin/reverse', data)
153
+ return response.get('data', {})
154
+
155
+ def coordinates_to_digipin(self, latitude: float, longitude: float) -> Dict:
156
+ """
157
+ Convert coordinates to DigiPin
158
+
159
+ Args:
160
+ latitude: Latitude coordinate
161
+ longitude: Longitude coordinate
162
+
163
+ Returns:
164
+ Dict containing DigiPin and coordinates
165
+ """
166
+ if not isinstance(latitude, (int, float)) or not isinstance(longitude, (int, float)):
167
+ raise ValidationError("Latitude and longitude must be numbers")
168
+
169
+ if not (-90 <= latitude <= 90):
170
+ raise ValidationError("Latitude must be between -90 and 90")
171
+
172
+ if not (-180 <= longitude <= 180):
173
+ raise ValidationError("Longitude must be between -180 and 180")
174
+
175
+ data = {
176
+ 'latitude': float(latitude),
177
+ 'longitude': float(longitude)
178
+ }
179
+
180
+ response = self._make_request('POST', '/v1/digipin/coordinates-to-digipin', data)
181
+ return response.get('data', {})
182
+
183
+ def validate_digipin(self, digipin: str) -> Dict:
184
+ """
185
+ Validate a DigiPin format and check if it's a real location
186
+
187
+ Args:
188
+ digipin: The DigiPin code to validate
189
+
190
+ Returns:
191
+ Dict containing validation results
192
+ """
193
+ if not digipin or not digipin.strip():
194
+ raise ValidationError("DigiPin is required")
195
+
196
+ response = self._make_request('GET', f'/v1/digipin/validate/{digipin.strip()}')
197
+ return response.get('data', {})
198
+
199
+ def batch_geocode(self, addresses: List[Dict]) -> Dict:
200
+ """
201
+ Geocode multiple addresses in batch
202
+
203
+ Args:
204
+ addresses: List of address dictionaries
205
+
206
+ Returns:
207
+ Dict containing batch processing results
208
+ """
209
+ if not addresses or not isinstance(addresses, list):
210
+ raise ValidationError("Addresses must be a non-empty list")
211
+
212
+ if len(addresses) > 100:
213
+ raise ValidationError("Maximum 100 addresses allowed per batch")
214
+
215
+ # Validate each address
216
+ for i, addr in enumerate(addresses):
217
+ if not isinstance(addr, dict) or 'address' not in addr:
218
+ raise ValidationError(f"Address {i+1} must be a dict with 'address' key")
219
+
220
+ data = {'addresses': addresses}
221
+ response = self._make_request('POST', '/v1/digipin/batch', data)
222
+ return response.get('data', {})
223
+
224
+ def get_usage(self) -> Dict:
225
+ """
226
+ Get API usage statistics
227
+
228
+ Returns:
229
+ Dict containing usage information
230
+ """
231
+ response = self._make_request('GET', '/v1/digipin/usage')
232
+ return response.get('data', {})
233
+
234
+ def autocomplete(self, query: str, limit: int = 5) -> List[Dict]:
235
+ """
236
+ Get autocomplete suggestions for addresses
237
+
238
+ Args:
239
+ query: Search query (minimum 3 characters)
240
+ limit: Maximum number of suggestions (default: 5, max: 10)
241
+
242
+ Returns:
243
+ List of address suggestions
244
+ """
245
+ if not query or len(query.strip()) < 3:
246
+ raise ValidationError("Query must be at least 3 characters long")
247
+
248
+ if limit > 10:
249
+ limit = 10
250
+
251
+ params = {
252
+ 'q': query.strip(),
253
+ 'limit': limit
254
+ }
255
+
256
+ response = self._make_request('GET', '/v1/digipin/autocomplete', params=params)
257
+ return response.get('data', [])
258
+
259
+ def register_webhook(self, url: str, events: List[str], secret: Optional[str] = None) -> Dict:
260
+ """
261
+ Register a webhook endpoint
262
+
263
+ Args:
264
+ url: Webhook URL
265
+ events: List of events to subscribe to
266
+ secret: Optional secret for signature verification
267
+
268
+ Returns:
269
+ Dict containing webhook registration details
270
+ """
271
+ if not url or not url.startswith(('http://', 'https://')):
272
+ raise ValidationError("Valid webhook URL is required")
273
+
274
+ if not events or not isinstance(events, list):
275
+ raise ValidationError("Events list is required")
276
+
277
+ data = {
278
+ 'url': url,
279
+ 'events': events
280
+ }
281
+
282
+ if secret:
283
+ data['secret'] = secret
284
+
285
+ response = self._make_request('POST', '/v1/digipin/webhooks', data)
286
+ return response.get('data', {})
287
+
288
+ def list_webhooks(self) -> List[Dict]:
289
+ """
290
+ List registered webhooks
291
+
292
+ Returns:
293
+ List of webhook configurations
294
+ """
295
+ response = self._make_request('GET', '/v1/digipin/webhooks')
296
+ return response.get('data', [])
297
+
298
+ def delete_webhook(self, webhook_id: str) -> Dict:
299
+ """
300
+ Delete a webhook
301
+
302
+ Args:
303
+ webhook_id: ID of the webhook to delete
304
+
305
+ Returns:
306
+ Dict containing deletion confirmation
307
+ """
308
+ if not webhook_id:
309
+ raise ValidationError("Webhook ID is required")
310
+
311
+ response = self._make_request('DELETE', f'/v1/digipin/webhooks/{webhook_id}')
312
+ return response