csv2geo 1.0.0__tar.gz
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-1.0.0/PKG-INFO +225 -0
- csv2geo-1.0.0/README.md +191 -0
- csv2geo-1.0.0/csv2geo/__init__.py +35 -0
- csv2geo-1.0.0/csv2geo/client.py +328 -0
- csv2geo-1.0.0/csv2geo/exceptions.py +39 -0
- csv2geo-1.0.0/csv2geo/models.py +123 -0
- csv2geo-1.0.0/csv2geo.egg-info/PKG-INFO +225 -0
- csv2geo-1.0.0/csv2geo.egg-info/SOURCES.txt +11 -0
- csv2geo-1.0.0/csv2geo.egg-info/dependency_links.txt +1 -0
- csv2geo-1.0.0/csv2geo.egg-info/requires.txt +9 -0
- csv2geo-1.0.0/csv2geo.egg-info/top_level.txt +2 -0
- csv2geo-1.0.0/pyproject.toml +64 -0
- csv2geo-1.0.0/setup.cfg +4 -0
csv2geo-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/csv2geo/)
|
|
38
|
+
[](https://pypi.org/project/csv2geo/)
|
|
39
|
+
[](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.
|
csv2geo-1.0.0/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# CSV2GEO Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/csv2geo/)
|
|
4
|
+
[](https://pypi.org/project/csv2geo/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Official Python SDK for the [CSV2GEO Geocoding API](https://csv2geo.com) - fast, accurate geocoding powered by 446M+ addresses worldwide.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install csv2geo
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from csv2geo import Client
|
|
19
|
+
|
|
20
|
+
# Initialize with your API key
|
|
21
|
+
client = Client("your_api_key")
|
|
22
|
+
|
|
23
|
+
# Forward geocoding (address → coordinates)
|
|
24
|
+
result = client.geocode("1600 Pennsylvania Ave, Washington DC")
|
|
25
|
+
if result:
|
|
26
|
+
print(f"Lat: {result.lat}, Lng: {result.lng}")
|
|
27
|
+
print(f"Address: {result.formatted_address}")
|
|
28
|
+
|
|
29
|
+
# Reverse geocoding (coordinates → address)
|
|
30
|
+
result = client.reverse(38.8977, -77.0365)
|
|
31
|
+
if result:
|
|
32
|
+
print(f"Address: {result.formatted_address}")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Forward geocoding** - Convert addresses to coordinates
|
|
38
|
+
- **Reverse geocoding** - Convert coordinates to addresses
|
|
39
|
+
- **Batch processing** - Geocode up to 10,000 addresses per request
|
|
40
|
+
- **Auto-retry** - Automatic retry on rate limits
|
|
41
|
+
- **Type hints** - Full type annotations for IDE support
|
|
42
|
+
|
|
43
|
+
## API Reference
|
|
44
|
+
|
|
45
|
+
### Initialize Client
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from csv2geo import Client
|
|
49
|
+
|
|
50
|
+
client = Client(
|
|
51
|
+
api_key="your_api_key",
|
|
52
|
+
base_url="https://api.csv2geo.com/v1", # optional
|
|
53
|
+
timeout=30, # optional, seconds
|
|
54
|
+
auto_retry=True, # optional, retry on rate limit
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Forward Geocoding
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# Simple - returns best match or None
|
|
62
|
+
result = client.geocode("1600 Pennsylvania Ave, Washington DC")
|
|
63
|
+
|
|
64
|
+
# With country filter
|
|
65
|
+
result = client.geocode("123 Main St", country="US")
|
|
66
|
+
|
|
67
|
+
# Full response with all matches
|
|
68
|
+
response = client.geocode_full("1600 Pennsylvania Ave")
|
|
69
|
+
for result in response.results:
|
|
70
|
+
print(f"{result.formatted_address}: {result.accuracy_score}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Reverse Geocoding
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Simple - returns best match or None
|
|
77
|
+
result = client.reverse(38.8977, -77.0365)
|
|
78
|
+
|
|
79
|
+
# Full response with all matches
|
|
80
|
+
response = client.reverse_full(38.8977, -77.0365)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Batch Geocoding
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# Geocode multiple addresses (up to 10,000)
|
|
87
|
+
addresses = [
|
|
88
|
+
"1600 Pennsylvania Ave, Washington DC",
|
|
89
|
+
"350 Fifth Avenue, New York, NY",
|
|
90
|
+
"1 Infinite Loop, Cupertino, CA",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
results = client.geocode_batch(addresses)
|
|
94
|
+
for response in results:
|
|
95
|
+
if response.best:
|
|
96
|
+
print(f"{response.query}: {response.best.lat}, {response.best.lng}")
|
|
97
|
+
else:
|
|
98
|
+
print(f"{response.query}: Not found")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Batch Reverse Geocoding
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# Reverse geocode multiple coordinates
|
|
105
|
+
coordinates = [
|
|
106
|
+
(38.8977, -77.0365),
|
|
107
|
+
(40.7484, -73.9857),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
results = client.reverse_batch(coordinates)
|
|
111
|
+
for response in results:
|
|
112
|
+
if response.best:
|
|
113
|
+
print(response.best.formatted_address)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### GeocodeResult Object
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
result = client.geocode("1600 Pennsylvania Ave, Washington DC")
|
|
120
|
+
|
|
121
|
+
# Coordinates
|
|
122
|
+
result.lat # 38.8977
|
|
123
|
+
result.lng # -77.0365
|
|
124
|
+
result.location # Location(lat=38.8977, lng=-77.0365)
|
|
125
|
+
|
|
126
|
+
# Address
|
|
127
|
+
result.formatted_address # "1600 PENNSYLVANIA AVE NW, WASHINGTON, DC 20500, US"
|
|
128
|
+
result.accuracy # "rooftop"
|
|
129
|
+
result.accuracy_score # 1.0
|
|
130
|
+
|
|
131
|
+
# Components
|
|
132
|
+
result.components.house_number # "1600"
|
|
133
|
+
result.components.street # "PENNSYLVANIA AVE NW"
|
|
134
|
+
result.components.city # "WASHINGTON"
|
|
135
|
+
result.components.state # "DC"
|
|
136
|
+
result.components.postcode # "20500"
|
|
137
|
+
result.components.country # "US"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Error Handling
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from csv2geo import Client, AuthenticationError, RateLimitError, InvalidRequestError
|
|
144
|
+
|
|
145
|
+
client = Client("your_api_key")
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
result = client.geocode("123 Main St")
|
|
149
|
+
except AuthenticationError as e:
|
|
150
|
+
print(f"Invalid API key: {e.message}")
|
|
151
|
+
except RateLimitError as e:
|
|
152
|
+
print(f"Rate limited. Retry after {e.retry_after} seconds")
|
|
153
|
+
except InvalidRequestError as e:
|
|
154
|
+
print(f"Invalid request: {e.message}")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Rate Limits
|
|
158
|
+
|
|
159
|
+
The client tracks rate limit headers automatically:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
client.geocode("123 Main St")
|
|
163
|
+
|
|
164
|
+
print(client.rate_limit) # Max requests per minute
|
|
165
|
+
print(client.rate_limit_remaining) # Requests remaining
|
|
166
|
+
print(client.rate_limit_reset) # Unix timestamp when limit resets
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
With `auto_retry=True` (default), the client automatically waits and retries when rate limited.
|
|
170
|
+
|
|
171
|
+
## Context Manager
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
with Client("your_api_key") as client:
|
|
175
|
+
result = client.geocode("123 Main St")
|
|
176
|
+
print(result.lat, result.lng)
|
|
177
|
+
# Session automatically closed
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Get Your API Key
|
|
181
|
+
|
|
182
|
+
Sign up at [csv2geo.com](https://csv2geo.com) to get your API key.
|
|
183
|
+
|
|
184
|
+
## Documentation
|
|
185
|
+
|
|
186
|
+
- [API Documentation](https://acenji.github.io/csv2geo-api/docs/)
|
|
187
|
+
- [OpenAPI Specification](https://github.com/acenji/csv2geo-api/blob/main/openapi.yaml)
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT License - see [LICENSE](https://github.com/acenji/csv2geo-api/blob/main/LICENSE) for details.
|
|
@@ -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
|
+
]
|
|
@@ -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()
|
|
@@ -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
|
|
@@ -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
|
+
[](https://pypi.org/project/csv2geo/)
|
|
38
|
+
[](https://pypi.org/project/csv2geo/)
|
|
39
|
+
[](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,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
csv2geo/__init__.py
|
|
4
|
+
csv2geo/client.py
|
|
5
|
+
csv2geo/exceptions.py
|
|
6
|
+
csv2geo/models.py
|
|
7
|
+
csv2geo.egg-info/PKG-INFO
|
|
8
|
+
csv2geo.egg-info/SOURCES.txt
|
|
9
|
+
csv2geo.egg-info/dependency_links.txt
|
|
10
|
+
csv2geo.egg-info/requires.txt
|
|
11
|
+
csv2geo.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "csv2geo"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK for CSV2GEO Geocoding API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Scale Campaign", email = "admin@csv2geo.com"}
|
|
13
|
+
]
|
|
14
|
+
keywords = ["geocoding", "geocode", "address", "latitude", "longitude", "maps", "csv2geo"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
requires-python = ">=3.8"
|
|
30
|
+
dependencies = [
|
|
31
|
+
"requests>=2.25.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=7.0.0",
|
|
37
|
+
"pytest-cov>=4.0.0",
|
|
38
|
+
"responses>=0.23.0",
|
|
39
|
+
"black>=23.0.0",
|
|
40
|
+
"mypy>=1.0.0",
|
|
41
|
+
"types-requests>=2.28.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://csv2geo.com"
|
|
46
|
+
Documentation = "https://acenji.github.io/csv2geo-api/docs/"
|
|
47
|
+
Repository = "https://github.com/acenji/csv2geo-api"
|
|
48
|
+
Issues = "https://github.com/acenji/csv2geo-api/issues"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
where = ["."]
|
|
52
|
+
|
|
53
|
+
[tool.black]
|
|
54
|
+
line-length = 88
|
|
55
|
+
target-version = ["py38", "py39", "py310", "py311", "py312"]
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
python_version = "3.8"
|
|
59
|
+
warn_return_any = true
|
|
60
|
+
warn_unused_configs = true
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
python_files = ["test_*.py"]
|
csv2geo-1.0.0/setup.cfg
ADDED