Skip to content

Commit cb025b6

Browse files
authored
ENH: Pass custom headers to tile server requests (#266)
* feat: Pass headers to tile server requests * test: Test request headers * feat: Update place.py to accept headers * test: Update expected sums * test: Remove pyplot warning > contextily/tests/test_cx.py:806: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`). Consider using `matplotlib.pyplot.close()`. * test: Increase patch test coverage * test: Test to confirm user provided user-agent overrides
1 parent a0f0fc5 commit cb025b6

File tree

4 files changed

+403
-8
lines changed

4 files changed

+403
-8
lines changed

contextily/place.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ class Place(object):
4545
`rasterio` and all bands are loaded into the basemap.
4646
IMPORTANT: tiles are assumed to be in the Spherical Mercator
4747
projection (EPSG:3857), unless the `crs` keyword is specified.
48+
headers : dict[str, str] or None
49+
[Optional. Default: None]
50+
Headers to include with requests to the tile server.
4851
geocoder : geopy.geocoders
4952
[Optional. Default: geopy.geocoders.Nominatim()] Geocoder method to process `search`
5053
@@ -77,12 +80,14 @@ def __init__(
7780
path=None,
7881
zoom_adjust=None,
7982
source=None,
83+
headers: dict[str, str] | None = None,
8084
geocoder=gp.geocoders.Nominatim(user_agent=_default_user_agent),
8185
):
8286
self.path = path
8387
if source is None:
8488
source = providers.OpenStreetMap.HOT
8589
self.source = source
90+
self.headers = headers
8691
self.zoom_adjust = zoom_adjust
8792

8893
# Get geocoded values
@@ -119,6 +124,8 @@ def _get_map(self):
119124
kwargs = {"ll": True}
120125
if self.source is not None:
121126
kwargs["source"] = self.source
127+
if self.headers is not None:
128+
kwargs["headers"] = self.headers
122129

123130
try:
124131
if isinstance(self.path, str):

contextily/plotting.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def add_basemap(
1919
ax,
2020
zoom=ZOOM,
2121
source=None,
22+
headers=None,
2223
interpolation=INTERPOLATION,
2324
attribution=None,
2425
attribution_size=ATTRIBUTION_SIZE,
@@ -50,6 +51,9 @@ def add_basemap(
5051
the file is read with `rasterio` and all bands are loaded into the basemap.
5152
IMPORTANT: tiles are assumed to be in the Spherical Mercator projection
5253
(EPSG:3857), unless the `crs` keyword is specified.
54+
headers : dict or None
55+
[Optional. Default: None]
56+
Headers to include with requests to the tile server.
5357
interpolation : str
5458
[Optional. Default='bilinear'] Interpolation algorithm to be passed
5559
to `imshow`. See `matplotlib.pyplot.imshow` for further details.
@@ -138,6 +142,7 @@ def add_basemap(
138142
top,
139143
zoom=zoom,
140144
source=source,
145+
headers=headers,
141146
ll=False,
142147
zoom_adjust=zoom_adjust,
143148
)

contextily/tile.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def bounds2raster(
7373
path,
7474
zoom="auto",
7575
source=None,
76+
headers: dict[str, str] | None = None,
7677
ll=False,
7778
wait=0,
7879
max_retries=2,
@@ -106,6 +107,9 @@ def bounds2raster(
106107
`rasterio` and all bands are loaded into the basemap.
107108
IMPORTANT: tiles are assumed to be in the Spherical Mercator
108109
projection (EPSG:3857), unless the `crs` keyword is specified.
110+
headers : dict[str, str] or None
111+
[Optional. Default: None]
112+
Headers to include with requests to the tile server.
109113
ll : Boolean
110114
[Optional. Default: False] If True, `w`, `s`, `e`, `n` are
111115
assumed to be lon/lat as opposed to Spherical Mercator.
@@ -136,6 +140,9 @@ def bounds2raster(
136140
extent : tuple
137141
Bounding box [minX, maxX, minY, maxY] of the returned image
138142
"""
143+
if headers is None:
144+
headers = {}
145+
139146
if not ll:
140147
# Convert w, s, e, n into lon/lat
141148
w, s = _sm2ll(w, s)
@@ -148,6 +155,7 @@ def bounds2raster(
148155
n,
149156
zoom=zoom,
150157
source=source,
158+
headers=headers,
151159
ll=True,
152160
n_connections=n_connections,
153161
use_cache=use_cache,
@@ -187,6 +195,7 @@ def bounds2img(
187195
n,
188196
zoom="auto",
189197
source=None,
198+
headers: dict[str, str] | None = None,
190199
ll=False,
191200
wait=0,
192201
max_retries=2,
@@ -219,6 +228,9 @@ def bounds2img(
219228
`rasterio` and all bands are loaded into the basemap.
220229
IMPORTANT: tiles are assumed to be in the Spherical Mercator
221230
projection (EPSG:3857), unless the `crs` keyword is specified.
231+
headers : dict[str, str] or None
232+
[Optional. Default: None]
233+
Headers to include with requests to the tile server.
222234
ll : Boolean
223235
[Optional. Default: False] If True, `w`, `s`, `e`, `n` are
224236
assumed to be lon/lat as opposed to Spherical Mercator.
@@ -253,6 +265,9 @@ def bounds2img(
253265
extent : tuple
254266
Bounding box [minX, maxX, minY, maxY] of the returned image
255267
"""
268+
if headers is None:
269+
headers = {}
270+
256271
if not ll:
257272
# Convert w, s, e, n into lon/lat
258273
w, s = _sm2ll(w, s)
@@ -281,7 +296,7 @@ def bounds2img(
281296
)
282297
fetch_tile_fn = memory.cache(_fetch_tile) if use_cache else _fetch_tile
283298
arrays = Parallel(n_jobs=n_connections, prefer=preferred_backend)(
284-
delayed(fetch_tile_fn)(tile_url, wait, max_retries) for tile_url in tile_urls
299+
delayed(fetch_tile_fn)(tile_url, wait, max_retries, headers) for tile_url in tile_urls
285300
)
286301
# merge downloaded tiles
287302
merged, extent = _merge_tiles(tiles, arrays)
@@ -309,8 +324,8 @@ def _process_source(source):
309324
return provider
310325

311326

312-
def _fetch_tile(tile_url, wait, max_retries):
313-
array = _retryer(tile_url, wait, max_retries)
327+
def _fetch_tile(tile_url, wait, max_retries, headers: dict[str, str]):
328+
array = _retryer(tile_url, wait, max_retries, headers)
314329
return array
315330

316331

@@ -428,7 +443,7 @@ def _warper(img, transform, s_crs, t_crs, resampling):
428443
return img, bounds, transform
429444

430445

431-
def _retryer(tile_url, wait, max_retries):
446+
def _retryer(tile_url, wait, max_retries, headers: dict[str, str]):
432447
"""
433448
Retry a url many times in attempt to get a tile and read the image
434449
@@ -443,13 +458,15 @@ def _retryer(tile_url, wait, max_retries):
443458
max_retries : int
444459
total number of rejected requests allowed before contextily
445460
will stop trying to fetch more tiles from a rate-limited API.
461+
headers: dict[str, str]
462+
headers to include with request.
446463
447464
Returns
448465
-------
449466
array of the tile
450467
"""
451468
try:
452-
request = requests.get(tile_url, headers={"user-agent": USER_AGENT})
469+
request = requests.get(tile_url, headers={"user-agent": USER_AGENT, **headers})
453470
request.raise_for_status()
454471
with io.BytesIO(request.content) as image_stream:
455472
image = Image.open(image_stream).convert("RGBA")
@@ -468,7 +485,7 @@ def _retryer(tile_url, wait, max_retries):
468485
if max_retries > 0:
469486
time.sleep(wait)
470487
max_retries -= 1
471-
request = _retryer(tile_url, wait, max_retries)
488+
request = _retryer(tile_url, wait, max_retries, headers)
472489
else:
473490
raise requests.HTTPError("Connection reset by peer too many times. "
474491
f"Last message was: {request.status_code} "

0 commit comments

Comments
 (0)