Skip to content

Commit 31cbf0e

Browse files
sbscullySamuel Scully
andauthored
Allow API key to be set by environment variable, bounds checks (#66)
* Allow API key to be set by env var * Reject out of bounds coordinates Co-authored-by: Samuel Scully <[email protected]>
1 parent 84f6097 commit 31cbf0e

File tree

5 files changed

+103
-16
lines changed

5 files changed

+103
-16
lines changed

opencage/geocoder.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,17 @@ class InvalidInputError(OpenCageGeocodeError):
3232
"""
3333
There was a problem with the input you provided.
3434
35-
:var bad_value: The value that caused the problem
35+
:var message: Error message describing the bad input.
36+
:var bad_value: The value that caused the problem.
3637
"""
3738

38-
def __init__(self, bad_value):
39+
def __init__(self, message, bad_value=None):
3940
super().__init__()
41+
self.message = message
4042
self.bad_value = bad_value
4143

4244
def __unicode__(self):
43-
return "Input must be a unicode string, not " + repr(self.bad_value)[:100]
45+
return self.message
4446

4547
__str__ = __unicode__
4648

@@ -135,13 +137,19 @@ class OpenCageGeocode:
135137

136138
def __init__(
137139
self,
138-
key,
140+
key=None,
139141
protocol='https',
140142
domain=DEFAULT_DOMAIN,
141143
sslcontext=None,
142144
user_agent_comment=None):
143145
"""Constructor."""
144-
self.key = key
146+
self.key = key if key is not None else os.environ.get('OPENCAGE_API_KEY')
147+
148+
if self.key is None:
149+
raise ValueError(
150+
"API key not provided. "
151+
"Either pass a 'key' parameter or set the OPENCAGE_API_KEY environment variable."
152+
)
145153

146154
if protocol and protocol not in ('http', 'https'):
147155
protocol = 'https'
@@ -243,7 +251,11 @@ def reverse_geocode(self, lat, lng, **kwargs):
243251
:raises RateLimitExceededError: if you have exceeded the number of queries you can make.
244252
: Exception says when you can try again
245253
:raises UnknownError: if something goes wrong with the OpenCage API
254+
:raises InvalidInputError: if the latitude or longitude is out of bounds
246255
"""
256+
257+
self._validate_lat_lng(lat, lng)
258+
247259
return self.geocode(_query_for_reverse_geocoding(lat, lng), **kwargs)
248260

249261
async def reverse_geocode_async(self, lat, lng, **kwargs):
@@ -258,7 +270,11 @@ async def reverse_geocode_async(self, lat, lng, **kwargs):
258270
:rtype: dict
259271
:raises RateLimitExceededError: if exceeded number of queries you can make. You can try again
260272
:raises UnknownError: if something goes wrong with the OpenCage API
273+
:raises InvalidInputError: if the latitude or longitude is out of bounds
261274
"""
275+
276+
self._validate_lat_lng(lat, lng)
277+
262278
return await self.geocode_async(_query_for_reverse_geocoding(lat, lng), **kwargs)
263279

264280
@backoff.on_exception(
@@ -340,12 +356,33 @@ async def _opencage_async_request(self, params):
340356

341357
def _parse_request(self, query, params):
342358
if not isinstance(query, str):
343-
raise InvalidInputError(bad_value=query)
359+
error_message = "Input must be a unicode string, not " + repr(query)[:100]
360+
raise InvalidInputError(error_message, bad_value=query)
344361

345362
data = {'q': query, 'key': self.key}
346363
data.update(params) # Add user parameters
347364
return data
348365

366+
def _validate_lat_lng(self, lat, lng):
367+
"""
368+
Validate latitude and longitude values.
369+
370+
Raises InvalidInputError if the values are out of bounds.
371+
"""
372+
try:
373+
lat_float = float(lat)
374+
if not -90 <= lat_float <= 90:
375+
raise InvalidInputError(f"Latitude must be a number between -90 and 90, not {lat}", bad_value=lat)
376+
except ValueError:
377+
raise InvalidInputError(f"Latitude must be a number between -90 and 90, not {lat}", bad_value=lat)
378+
379+
try:
380+
lng_float = float(lng)
381+
if not -180 <= lng_float <= 180:
382+
raise InvalidInputError(f"Longitude must be a number between -180 and 180, not {lng}", bad_value=lng)
383+
except ValueError:
384+
raise InvalidInputError(f"Longitude must be a number between -180 and 180, not {lng}", bad_value=lng)
385+
349386

350387
def _query_for_reverse_geocoding(lat, lng):
351388
"""

test/cli/test_cli_run.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def test_input_errors(capfd):
9696

9797
_, err = capfd.readouterr()
9898
# assert err == ''
99-
assert err.count("\n") == 6
99+
assert err.count("\n") == 7
100100
assert "Line 1 - Missing input column 2 in ['50.101010']" in err
101101
assert "Line 1 - Expected two comma-separated values for reverse geocoding, got ['50.101010']" in err
102102
assert "Line 3 - Empty line" in err
@@ -109,7 +109,7 @@ def test_input_errors(capfd):
109109
length=4,
110110
lines=[
111111
'50.101010,,',
112-
'-100,60.1,de,48153',
112+
'-100,60.1,,',
113113
',,',
114114
'a,b,,'
115115
]

test/test_error_invalid_input.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,36 @@ def test_must_be_unicode_string():
3434
geocoder.geocode(latin1_string)
3535
assert str(excinfo.value) == f"Input must be a unicode string, not {latin1_string!r}"
3636
assert excinfo.value.bad_value == latin1_string
37+
38+
39+
@responses.activate
40+
def test_reject_out_of_bounds_coordinates():
41+
"""Test that reverse geocoding rejects out-of-bounds latitude and longitude values."""
42+
responses.add(
43+
responses.GET,
44+
geocoder.url,
45+
body='{"results":{}}',
46+
status=200
47+
)
48+
49+
# Valid coordinates should work
50+
geocoder.reverse_geocode(45.0, 90.0)
51+
geocoder.reverse_geocode(-45.0, -90.0)
52+
53+
# Invalid latitude values (outside -90 to 90)
54+
with pytest.raises(InvalidInputError) as excinfo:
55+
geocoder.reverse_geocode(91.0, 45.0)
56+
assert "Latitude must be a number between -90 and 90" in str(excinfo.value)
57+
58+
with pytest.raises(InvalidInputError) as excinfo:
59+
geocoder.reverse_geocode(-91.0, 45.0)
60+
assert "Latitude must be a number between -90 and 90" in str(excinfo.value)
61+
62+
# Invalid longitude values (outside -180 to 180)
63+
with pytest.raises(InvalidInputError) as excinfo:
64+
geocoder.reverse_geocode(45.0, 181.0)
65+
assert "Longitude must be a number between -180 and 180" in str(excinfo.value)
66+
67+
with pytest.raises(InvalidInputError) as excinfo:
68+
geocoder.reverse_geocode(45.0, -181.0)
69+
assert "Longitude must be a number between -180 and 180" in str(excinfo.value)

test/test_geocoder_args.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# encoding: utf-8
2+
3+
from opencage.geocoder import OpenCageGeocode
4+
5+
import os
6+
7+
8+
def test_protocol_http():
9+
"""Test that HTTP protocol can be set correctly"""
10+
geocoder = OpenCageGeocode('abcde', protocol='http')
11+
assert geocoder.url == 'http://api.opencagedata.com/geocode/v1/json'
12+
13+
14+
def test_api_key_env_var():
15+
"""Test that API key can be set by an environment variable"""
16+
17+
os.environ['OPENCAGE_API_KEY'] = 'from-env-var'
18+
geocoder = OpenCageGeocode()
19+
assert geocoder.key == 'from-env-var'
20+
21+
22+
def test_custom_domain():
23+
"""Test that custom domain can be set"""
24+
geocoder = OpenCageGeocode('abcde', domain='example.com')
25+
assert geocoder.url == 'https://example.com/geocode/v1/json'

test/test_setting_protocol.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)