csv2geo 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.
csv2geo/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """
2
+ CSV2GEO Python SDK
3
+
4
+ Fast, accurate geocoding powered by 446M+ addresses worldwide.
5
+
6
+ Usage:
7
+ from csv2geo import Client
8
+
9
+ client = Client("your_api_key")
10
+ result = client.geocode("1600 Pennsylvania Ave, Washington DC")
11
+ print(result.lat, result.lng)
12
+ """
13
+
14
+ from .client import Client
15
+ from .models import GeocodeResult, Location, AddressComponents
16
+ from .exceptions import (
17
+ CSV2GEOError,
18
+ AuthenticationError,
19
+ RateLimitError,
20
+ InvalidRequestError,
21
+ APIError,
22
+ )
23
+
24
+ __version__ = "1.0.0"
25
+ __all__ = [
26
+ "Client",
27
+ "GeocodeResult",
28
+ "Location",
29
+ "AddressComponents",
30
+ "CSV2GEOError",
31
+ "AuthenticationError",
32
+ "RateLimitError",
33
+ "InvalidRequestError",
34
+ "APIError",
35
+ ]
csv2geo/client.py ADDED
@@ -0,0 +1,328 @@
1
+ """CSV2GEO API Client."""
2
+
3
+ import time
4
+ from typing import List, Optional, Union, Tuple
5
+ import requests
6
+
7
+ from .models import GeocodeResult, GeocodeResponse, BatchGeocodeResponse, Location
8
+ from .exceptions import (
9
+ CSV2GEOError,
10
+ AuthenticationError,
11
+ RateLimitError,
12
+ InvalidRequestError,
13
+ PermissionError,
14
+ APIError,
15
+ )
16
+
17
+
18
+ class Client:
19
+ """
20
+ CSV2GEO API Client.
21
+
22
+ Usage:
23
+ client = Client("your_api_key")
24
+
25
+ # Forward geocoding
26
+ result = client.geocode("1600 Pennsylvania Ave, Washington DC")
27
+ print(result.lat, result.lng)
28
+
29
+ # Reverse geocoding
30
+ result = client.reverse(38.8977, -77.0365)
31
+ print(result.formatted_address)
32
+
33
+ # Batch geocoding
34
+ results = client.geocode_batch([
35
+ "1600 Pennsylvania Ave, Washington DC",
36
+ "350 Fifth Avenue, New York, NY",
37
+ ])
38
+ for r in results:
39
+ print(r.best.formatted_address if r.best else "Not found")
40
+ """
41
+
42
+ DEFAULT_BASE_URL = "https://api.csv2geo.com/v1"
43
+ DEFAULT_TIMEOUT = 30
44
+ MAX_RETRIES = 3
45
+ RETRY_DELAY = 1 # seconds
46
+
47
+ def __init__(
48
+ self,
49
+ api_key: str,
50
+ base_url: str = None,
51
+ timeout: int = None,
52
+ auto_retry: bool = True,
53
+ ):
54
+ """
55
+ Initialize the CSV2GEO client.
56
+
57
+ Args:
58
+ api_key: Your CSV2GEO API key
59
+ base_url: API base URL (default: https://api.csv2geo.com/v1)
60
+ timeout: Request timeout in seconds (default: 30)
61
+ auto_retry: Automatically retry on rate limit (default: True)
62
+ """
63
+ if not api_key:
64
+ raise ValueError("API key is required")
65
+
66
+ self.api_key = api_key
67
+ self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
68
+ self.timeout = timeout or self.DEFAULT_TIMEOUT
69
+ self.auto_retry = auto_retry
70
+
71
+ self._session = requests.Session()
72
+ self._session.headers.update({
73
+ "Authorization": f"Bearer {self.api_key}",
74
+ "User-Agent": "csv2geo-python/1.0.0",
75
+ "Content-Type": "application/json",
76
+ })
77
+
78
+ # Rate limit tracking
79
+ self.rate_limit = None
80
+ self.rate_limit_remaining = None
81
+ self.rate_limit_reset = None
82
+
83
+ def _handle_response(self, response: requests.Response) -> dict:
84
+ """Handle API response and raise appropriate exceptions."""
85
+ # Update rate limit info from headers
86
+ self.rate_limit = response.headers.get("X-RateLimit-Limit")
87
+ self.rate_limit_remaining = response.headers.get("X-RateLimit-Remaining")
88
+ self.rate_limit_reset = response.headers.get("X-RateLimit-Reset")
89
+
90
+ if response.status_code == 200:
91
+ return response.json()
92
+
93
+ # Handle errors
94
+ try:
95
+ error_data = response.json().get("error", {})
96
+ code = error_data.get("code", "unknown")
97
+ message = error_data.get("message", "Unknown error")
98
+ status = error_data.get("status", response.status_code)
99
+ except (ValueError, KeyError):
100
+ code = "unknown"
101
+ message = response.text or "Unknown error"
102
+ status = response.status_code
103
+
104
+ if response.status_code == 401:
105
+ raise AuthenticationError(message, code=code, status=status)
106
+ elif response.status_code == 403:
107
+ raise PermissionError(message, code=code, status=status)
108
+ elif response.status_code == 429:
109
+ retry_after = int(response.headers.get("Retry-After", 60))
110
+ raise RateLimitError(
111
+ message, code=code, status=status, retry_after=retry_after
112
+ )
113
+ elif response.status_code == 400:
114
+ raise InvalidRequestError(message, code=code, status=status)
115
+ else:
116
+ raise APIError(message, code=code, status=status)
117
+
118
+ def _request(
119
+ self,
120
+ method: str,
121
+ endpoint: str,
122
+ params: dict = None,
123
+ json: dict = None,
124
+ retry_count: int = 0,
125
+ ) -> dict:
126
+ """Make an API request with retry logic."""
127
+ url = f"{self.base_url}{endpoint}"
128
+
129
+ try:
130
+ response = self._session.request(
131
+ method=method,
132
+ url=url,
133
+ params=params,
134
+ json=json,
135
+ timeout=self.timeout,
136
+ )
137
+ return self._handle_response(response)
138
+
139
+ except RateLimitError as e:
140
+ if self.auto_retry and retry_count < self.MAX_RETRIES:
141
+ wait_time = min(e.retry_after or self.RETRY_DELAY, 60)
142
+ time.sleep(wait_time)
143
+ return self._request(
144
+ method, endpoint, params, json, retry_count + 1
145
+ )
146
+ raise
147
+
148
+ except requests.exceptions.Timeout:
149
+ raise APIError("Request timed out", code="timeout")
150
+ except requests.exceptions.ConnectionError:
151
+ raise APIError("Connection failed", code="connection_error")
152
+
153
+ def geocode(
154
+ self,
155
+ address: str,
156
+ country: str = None,
157
+ ) -> Optional[GeocodeResult]:
158
+ """
159
+ Geocode a single address.
160
+
161
+ Args:
162
+ address: The address to geocode
163
+ country: Limit results to a specific country (ISO 3166-1 alpha-2)
164
+
165
+ Returns:
166
+ GeocodeResult or None if not found
167
+
168
+ Example:
169
+ result = client.geocode("1600 Pennsylvania Ave, Washington DC")
170
+ if result:
171
+ print(f"Lat: {result.lat}, Lng: {result.lng}")
172
+ """
173
+ params = {"q": address}
174
+ if country:
175
+ params["country"] = country
176
+
177
+ data = self._request("GET", "/geocode", params=params)
178
+ response = GeocodeResponse.from_dict(data)
179
+ return response.best
180
+
181
+ def geocode_full(
182
+ self,
183
+ address: str,
184
+ country: str = None,
185
+ ) -> GeocodeResponse:
186
+ """
187
+ Geocode a single address and return full response with all results.
188
+
189
+ Args:
190
+ address: The address to geocode
191
+ country: Limit results to a specific country (ISO 3166-1 alpha-2)
192
+
193
+ Returns:
194
+ GeocodeResponse with all matching results
195
+ """
196
+ params = {"q": address}
197
+ if country:
198
+ params["country"] = country
199
+
200
+ data = self._request("GET", "/geocode", params=params)
201
+ return GeocodeResponse.from_dict(data)
202
+
203
+ def reverse(
204
+ self,
205
+ lat: float,
206
+ lng: float,
207
+ ) -> Optional[GeocodeResult]:
208
+ """
209
+ Reverse geocode coordinates to an address.
210
+
211
+ Args:
212
+ lat: Latitude
213
+ lng: Longitude
214
+
215
+ Returns:
216
+ GeocodeResult or None if not found
217
+
218
+ Example:
219
+ result = client.reverse(38.8977, -77.0365)
220
+ if result:
221
+ print(result.formatted_address)
222
+ """
223
+ params = {"lat": lat, "lng": lng}
224
+ data = self._request("GET", "/reverse", params=params)
225
+ response = GeocodeResponse.from_dict(data)
226
+ return response.best
227
+
228
+ def reverse_full(
229
+ self,
230
+ lat: float,
231
+ lng: float,
232
+ ) -> GeocodeResponse:
233
+ """
234
+ Reverse geocode coordinates and return full response.
235
+
236
+ Args:
237
+ lat: Latitude
238
+ lng: Longitude
239
+
240
+ Returns:
241
+ GeocodeResponse with all matching results
242
+ """
243
+ params = {"lat": lat, "lng": lng}
244
+ data = self._request("GET", "/reverse", params=params)
245
+ return GeocodeResponse.from_dict(data)
246
+
247
+ def geocode_batch(
248
+ self,
249
+ addresses: List[str],
250
+ ) -> List[GeocodeResponse]:
251
+ """
252
+ Geocode multiple addresses in a single request.
253
+
254
+ Args:
255
+ addresses: List of addresses to geocode (max 10,000)
256
+
257
+ Returns:
258
+ List of GeocodeResponse objects
259
+
260
+ Example:
261
+ results = client.geocode_batch([
262
+ "1600 Pennsylvania Ave, Washington DC",
263
+ "350 Fifth Avenue, New York, NY",
264
+ ])
265
+ for r in results:
266
+ if r.best:
267
+ print(f"{r.query}: {r.best.lat}, {r.best.lng}")
268
+ """
269
+ if len(addresses) > 10000:
270
+ raise InvalidRequestError("Maximum 10,000 addresses per batch request")
271
+
272
+ data = self._request("POST", "/geocode", json={"addresses": addresses})
273
+ response = BatchGeocodeResponse.from_dict(data)
274
+ return response.results
275
+
276
+ def reverse_batch(
277
+ self,
278
+ coordinates: List[Union[Tuple[float, float], Location, dict]],
279
+ ) -> List[GeocodeResponse]:
280
+ """
281
+ Reverse geocode multiple coordinates in a single request.
282
+
283
+ Args:
284
+ coordinates: List of coordinates as (lat, lng) tuples, Location objects,
285
+ or dicts with 'lat' and 'lng' keys (max 10,000)
286
+
287
+ Returns:
288
+ List of GeocodeResponse objects
289
+
290
+ Example:
291
+ results = client.reverse_batch([
292
+ (38.8977, -77.0365),
293
+ (40.7484, -73.9857),
294
+ ])
295
+ for r in results:
296
+ if r.best:
297
+ print(r.best.formatted_address)
298
+ """
299
+ if len(coordinates) > 10000:
300
+ raise InvalidRequestError("Maximum 10,000 coordinates per batch request")
301
+
302
+ # Normalize coordinates to dict format
303
+ coords_list = []
304
+ for coord in coordinates:
305
+ if isinstance(coord, tuple):
306
+ coords_list.append({"lat": coord[0], "lng": coord[1]})
307
+ elif isinstance(coord, Location):
308
+ coords_list.append(coord.to_dict())
309
+ elif isinstance(coord, dict):
310
+ coords_list.append(coord)
311
+ else:
312
+ raise InvalidRequestError(
313
+ f"Invalid coordinate format: {type(coord)}"
314
+ )
315
+
316
+ data = self._request("POST", "/reverse", json={"coordinates": coords_list})
317
+ response = BatchGeocodeResponse.from_dict(data)
318
+ return response.results
319
+
320
+ def close(self):
321
+ """Close the client session."""
322
+ self._session.close()
323
+
324
+ def __enter__(self):
325
+ return self
326
+
327
+ def __exit__(self, exc_type, exc_val, exc_tb):
328
+ self.close()
csv2geo/exceptions.py ADDED
@@ -0,0 +1,39 @@
1
+ """Custom exceptions for CSV2GEO SDK."""
2
+
3
+
4
+ class CSV2GEOError(Exception):
5
+ """Base exception for CSV2GEO SDK."""
6
+
7
+ def __init__(self, message: str, code: str = None, status: int = None):
8
+ self.message = message
9
+ self.code = code
10
+ self.status = status
11
+ super().__init__(self.message)
12
+
13
+
14
+ class AuthenticationError(CSV2GEOError):
15
+ """Raised when API key is missing, invalid, or revoked."""
16
+ pass
17
+
18
+
19
+ class RateLimitError(CSV2GEOError):
20
+ """Raised when rate limit is exceeded."""
21
+
22
+ def __init__(self, message: str, retry_after: int = None, **kwargs):
23
+ super().__init__(message, **kwargs)
24
+ self.retry_after = retry_after
25
+
26
+
27
+ class InvalidRequestError(CSV2GEOError):
28
+ """Raised when request parameters are invalid."""
29
+ pass
30
+
31
+
32
+ class PermissionError(CSV2GEOError):
33
+ """Raised when API key lacks required permission."""
34
+ pass
35
+
36
+
37
+ class APIError(CSV2GEOError):
38
+ """Raised for general API errors."""
39
+ pass
csv2geo/models.py ADDED
@@ -0,0 +1,123 @@
1
+ """Data models for CSV2GEO API responses."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, List
5
+
6
+
7
+ @dataclass
8
+ class Location:
9
+ """Geographic coordinates."""
10
+ lat: float
11
+ lng: float
12
+
13
+ def __str__(self) -> str:
14
+ return f"{self.lat}, {self.lng}"
15
+
16
+ def to_dict(self) -> dict:
17
+ return {"lat": self.lat, "lng": self.lng}
18
+
19
+
20
+ @dataclass
21
+ class AddressComponents:
22
+ """Parsed address components."""
23
+ house_number: Optional[str] = None
24
+ street: Optional[str] = None
25
+ unit: Optional[str] = None
26
+ city: Optional[str] = None
27
+ state: Optional[str] = None
28
+ postcode: Optional[str] = None
29
+ country: Optional[str] = None
30
+
31
+ @classmethod
32
+ def from_dict(cls, data: dict) -> "AddressComponents":
33
+ return cls(
34
+ house_number=data.get("house_number"),
35
+ street=data.get("street"),
36
+ unit=data.get("unit"),
37
+ city=data.get("city"),
38
+ state=data.get("state"),
39
+ postcode=data.get("postcode"),
40
+ country=data.get("country"),
41
+ )
42
+
43
+
44
+ @dataclass
45
+ class GeocodeResult:
46
+ """A single geocoding result."""
47
+ formatted_address: str
48
+ lat: float
49
+ lng: float
50
+ accuracy: str
51
+ accuracy_score: float
52
+ components: AddressComponents
53
+
54
+ @property
55
+ def location(self) -> Location:
56
+ """Get location as a Location object."""
57
+ return Location(lat=self.lat, lng=self.lng)
58
+
59
+ @classmethod
60
+ def from_dict(cls, data: dict) -> "GeocodeResult":
61
+ location = data.get("location", {})
62
+ return cls(
63
+ formatted_address=data.get("formatted_address", ""),
64
+ lat=location.get("lat", 0.0),
65
+ lng=location.get("lng", 0.0),
66
+ accuracy=data.get("accuracy", ""),
67
+ accuracy_score=data.get("accuracy_score", 0.0),
68
+ components=AddressComponents.from_dict(data.get("components", {})),
69
+ )
70
+
71
+ def to_dict(self) -> dict:
72
+ return {
73
+ "formatted_address": self.formatted_address,
74
+ "location": {"lat": self.lat, "lng": self.lng},
75
+ "accuracy": self.accuracy,
76
+ "accuracy_score": self.accuracy_score,
77
+ }
78
+
79
+
80
+ @dataclass
81
+ class GeocodeResponse:
82
+ """Response from a geocode request."""
83
+ query: str
84
+ results: List[GeocodeResult]
85
+
86
+ @property
87
+ def best(self) -> Optional[GeocodeResult]:
88
+ """Get the best (first) result, or None if no results."""
89
+ return self.results[0] if self.results else None
90
+
91
+ @classmethod
92
+ def from_dict(cls, data: dict) -> "GeocodeResponse":
93
+ results = [
94
+ GeocodeResult.from_dict(r)
95
+ for r in data.get("results", [])
96
+ ]
97
+ return cls(
98
+ query=data.get("query", ""),
99
+ results=results,
100
+ )
101
+
102
+
103
+ @dataclass
104
+ class BatchGeocodeResponse:
105
+ """Response from a batch geocode request."""
106
+ results: List[GeocodeResponse]
107
+ total: int
108
+ successful: int
109
+ failed: int
110
+
111
+ @classmethod
112
+ def from_dict(cls, data: dict) -> "BatchGeocodeResponse":
113
+ meta = data.get("meta", {})
114
+ results = [
115
+ GeocodeResponse.from_dict(r)
116
+ for r in data.get("results", [])
117
+ ]
118
+ return cls(
119
+ results=results,
120
+ total=meta.get("total", len(results)),
121
+ successful=meta.get("successful", len(results)),
122
+ failed=meta.get("failed", 0),
123
+ )
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.4
2
+ Name: csv2geo
3
+ Version: 1.0.0
4
+ Summary: Python SDK for CSV2GEO Geocoding API
5
+ Author-email: Scale Campaign <admin@csv2geo.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://csv2geo.com
8
+ Project-URL: Documentation, https://acenji.github.io/csv2geo-api/docs/
9
+ Project-URL: Repository, https://github.com/acenji/csv2geo-api
10
+ Project-URL: Issues, https://github.com/acenji/csv2geo-api/issues
11
+ Keywords: geocoding,geocode,address,latitude,longitude,maps,csv2geo
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering :: GIS
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: requests>=2.25.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
30
+ Requires-Dist: responses>=0.23.0; extra == "dev"
31
+ Requires-Dist: black>=23.0.0; extra == "dev"
32
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
33
+ Requires-Dist: types-requests>=2.28.0; extra == "dev"
34
+
35
+ # CSV2GEO Python SDK
36
+
37
+ [![PyPI version](https://img.shields.io/pypi/v/csv2geo.svg)](https://pypi.org/project/csv2geo/)
38
+ [![Python versions](https://img.shields.io/pypi/pyversions/csv2geo.svg)](https://pypi.org/project/csv2geo/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
40
+
41
+ Official Python SDK for the [CSV2GEO Geocoding API](https://csv2geo.com) - fast, accurate geocoding powered by 446M+ addresses worldwide.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install csv2geo
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```python
52
+ from csv2geo import Client
53
+
54
+ # Initialize with your API key
55
+ client = Client("your_api_key")
56
+
57
+ # Forward geocoding (address → coordinates)
58
+ result = client.geocode("1600 Pennsylvania Ave, Washington DC")
59
+ if result:
60
+ print(f"Lat: {result.lat}, Lng: {result.lng}")
61
+ print(f"Address: {result.formatted_address}")
62
+
63
+ # Reverse geocoding (coordinates → address)
64
+ result = client.reverse(38.8977, -77.0365)
65
+ if result:
66
+ print(f"Address: {result.formatted_address}")
67
+ ```
68
+
69
+ ## Features
70
+
71
+ - **Forward geocoding** - Convert addresses to coordinates
72
+ - **Reverse geocoding** - Convert coordinates to addresses
73
+ - **Batch processing** - Geocode up to 10,000 addresses per request
74
+ - **Auto-retry** - Automatic retry on rate limits
75
+ - **Type hints** - Full type annotations for IDE support
76
+
77
+ ## API Reference
78
+
79
+ ### Initialize Client
80
+
81
+ ```python
82
+ from csv2geo import Client
83
+
84
+ client = Client(
85
+ api_key="your_api_key",
86
+ base_url="https://api.csv2geo.com/v1", # optional
87
+ timeout=30, # optional, seconds
88
+ auto_retry=True, # optional, retry on rate limit
89
+ )
90
+ ```
91
+
92
+ ### Forward Geocoding
93
+
94
+ ```python
95
+ # Simple - returns best match or None
96
+ result = client.geocode("1600 Pennsylvania Ave, Washington DC")
97
+
98
+ # With country filter
99
+ result = client.geocode("123 Main St", country="US")
100
+
101
+ # Full response with all matches
102
+ response = client.geocode_full("1600 Pennsylvania Ave")
103
+ for result in response.results:
104
+ print(f"{result.formatted_address}: {result.accuracy_score}")
105
+ ```
106
+
107
+ ### Reverse Geocoding
108
+
109
+ ```python
110
+ # Simple - returns best match or None
111
+ result = client.reverse(38.8977, -77.0365)
112
+
113
+ # Full response with all matches
114
+ response = client.reverse_full(38.8977, -77.0365)
115
+ ```
116
+
117
+ ### Batch Geocoding
118
+
119
+ ```python
120
+ # Geocode multiple addresses (up to 10,000)
121
+ addresses = [
122
+ "1600 Pennsylvania Ave, Washington DC",
123
+ "350 Fifth Avenue, New York, NY",
124
+ "1 Infinite Loop, Cupertino, CA",
125
+ ]
126
+
127
+ results = client.geocode_batch(addresses)
128
+ for response in results:
129
+ if response.best:
130
+ print(f"{response.query}: {response.best.lat}, {response.best.lng}")
131
+ else:
132
+ print(f"{response.query}: Not found")
133
+ ```
134
+
135
+ ### Batch Reverse Geocoding
136
+
137
+ ```python
138
+ # Reverse geocode multiple coordinates
139
+ coordinates = [
140
+ (38.8977, -77.0365),
141
+ (40.7484, -73.9857),
142
+ ]
143
+
144
+ results = client.reverse_batch(coordinates)
145
+ for response in results:
146
+ if response.best:
147
+ print(response.best.formatted_address)
148
+ ```
149
+
150
+ ### GeocodeResult Object
151
+
152
+ ```python
153
+ result = client.geocode("1600 Pennsylvania Ave, Washington DC")
154
+
155
+ # Coordinates
156
+ result.lat # 38.8977
157
+ result.lng # -77.0365
158
+ result.location # Location(lat=38.8977, lng=-77.0365)
159
+
160
+ # Address
161
+ result.formatted_address # "1600 PENNSYLVANIA AVE NW, WASHINGTON, DC 20500, US"
162
+ result.accuracy # "rooftop"
163
+ result.accuracy_score # 1.0
164
+
165
+ # Components
166
+ result.components.house_number # "1600"
167
+ result.components.street # "PENNSYLVANIA AVE NW"
168
+ result.components.city # "WASHINGTON"
169
+ result.components.state # "DC"
170
+ result.components.postcode # "20500"
171
+ result.components.country # "US"
172
+ ```
173
+
174
+ ## Error Handling
175
+
176
+ ```python
177
+ from csv2geo import Client, AuthenticationError, RateLimitError, InvalidRequestError
178
+
179
+ client = Client("your_api_key")
180
+
181
+ try:
182
+ result = client.geocode("123 Main St")
183
+ except AuthenticationError as e:
184
+ print(f"Invalid API key: {e.message}")
185
+ except RateLimitError as e:
186
+ print(f"Rate limited. Retry after {e.retry_after} seconds")
187
+ except InvalidRequestError as e:
188
+ print(f"Invalid request: {e.message}")
189
+ ```
190
+
191
+ ## Rate Limits
192
+
193
+ The client tracks rate limit headers automatically:
194
+
195
+ ```python
196
+ client.geocode("123 Main St")
197
+
198
+ print(client.rate_limit) # Max requests per minute
199
+ print(client.rate_limit_remaining) # Requests remaining
200
+ print(client.rate_limit_reset) # Unix timestamp when limit resets
201
+ ```
202
+
203
+ With `auto_retry=True` (default), the client automatically waits and retries when rate limited.
204
+
205
+ ## Context Manager
206
+
207
+ ```python
208
+ with Client("your_api_key") as client:
209
+ result = client.geocode("123 Main St")
210
+ print(result.lat, result.lng)
211
+ # Session automatically closed
212
+ ```
213
+
214
+ ## Get Your API Key
215
+
216
+ Sign up at [csv2geo.com](https://csv2geo.com) to get your API key.
217
+
218
+ ## Documentation
219
+
220
+ - [API Documentation](https://acenji.github.io/csv2geo-api/docs/)
221
+ - [OpenAPI Specification](https://github.com/acenji/csv2geo-api/blob/main/openapi.yaml)
222
+
223
+ ## License
224
+
225
+ MIT License - see [LICENSE](https://github.com/acenji/csv2geo-api/blob/main/LICENSE) for details.
@@ -0,0 +1,8 @@
1
+ csv2geo/__init__.py,sha256=R4rmKzyLdyvemq94lvnuv6BYBOpTnv6X9IcWz7mZsd4,715
2
+ csv2geo/client.py,sha256=onNIAI61GOoEqQoyVAj-7dscifofDuvkyEWZOlGa5o8,10211
3
+ csv2geo/exceptions.py,sha256=kNJTjEUAyyww90zyaBq28KR1O5CF_tId1zQsJhki4DQ,966
4
+ csv2geo/models.py,sha256=8ETXkGv1QAb_zKyMwNaVOD5JaadUBmB0gCyzHoGLym0,3388
5
+ csv2geo-1.0.0.dist-info/METADATA,sha256=8qSKeBVZgYYCtO-lk5GIO0EZmLg1wN7JZf1BMgcWe3U,6515
6
+ csv2geo-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
7
+ csv2geo-1.0.0.dist-info/top_level.txt,sha256=JtWH-UwBAgITeusXhvWkqPfjbuzexKsaf4q5JxeJtkw,8
8
+ csv2geo-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ csv2geo