initial commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,271 @@
|
||||
from urllib.error import URLError
|
||||
|
||||
import pytest
|
||||
|
||||
import xyzservices.providers as xyz
|
||||
from xyzservices import Bunch, TileProvider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_provider():
|
||||
return TileProvider(
|
||||
url="https://myserver.com/tiles/{z}/{x}/{y}.png",
|
||||
attribution="(C) xyzservices",
|
||||
name="my_public_provider",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def retina_provider():
|
||||
return TileProvider(
|
||||
url="https://myserver.com/tiles/{z}/{x}/{y}{r}.png",
|
||||
attribution="(C) xyzservices",
|
||||
name="my_public_provider2",
|
||||
r="@2x",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def silent_retina_provider():
|
||||
return TileProvider(
|
||||
url="https://myserver.com/tiles/{z}/{x}/{y}{r}.png",
|
||||
attribution="(C) xyzservices",
|
||||
name="my_public_retina_provider3",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def private_provider():
|
||||
return TileProvider(
|
||||
url="https://myserver.com/tiles/{z}/{x}/{y}?access_token={accessToken}",
|
||||
attribution="(C) xyzservices",
|
||||
accessToken="<insert your access token here>",
|
||||
name="my_private_provider",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def html_attr_provider():
|
||||
return TileProvider(
|
||||
url="https://myserver.com/tiles/{z}/{x}/{y}.png",
|
||||
attribution="(C) xyzservices",
|
||||
html_attribution='© <a href="https://xyzservices.readthedocs.io">xyzservices</a>', # noqa
|
||||
name="my_public_provider_html",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subdomain_provider():
|
||||
return TileProvider(
|
||||
url="https://{s}.myserver.com/tiles/{z}/{x}/{y}.png",
|
||||
attribution="(C) xyzservices",
|
||||
subdomains="abcd",
|
||||
name="my_subdomain_provider",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_bunch(
|
||||
basic_provider,
|
||||
retina_provider,
|
||||
silent_retina_provider,
|
||||
private_provider,
|
||||
html_attr_provider,
|
||||
subdomain_provider,
|
||||
):
|
||||
return Bunch(
|
||||
basic_provider=basic_provider,
|
||||
retina_provider=retina_provider,
|
||||
silent_retina_provider=silent_retina_provider,
|
||||
private_provider=private_provider,
|
||||
bunched=Bunch(
|
||||
html_attr_provider=html_attr_provider, subdomain_provider=subdomain_provider
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_expect_name_url_attribution():
|
||||
msg = (
|
||||
"The attributes `name`, `url`, and `attribution` are "
|
||||
"required to initialise a `TileProvider`. Please provide "
|
||||
"values for: "
|
||||
)
|
||||
with pytest.raises(AttributeError, match=msg + "`name`, `url`, `attribution`"):
|
||||
TileProvider({})
|
||||
with pytest.raises(AttributeError, match=msg + "`url`, `attribution`"):
|
||||
TileProvider({"name": "myname"})
|
||||
with pytest.raises(AttributeError, match=msg + "`attribution`"):
|
||||
TileProvider({"url": "my_url", "name": "my_name"})
|
||||
with pytest.raises(AttributeError, match=msg + "`attribution`"):
|
||||
TileProvider(url="my_url", name="my_name")
|
||||
|
||||
|
||||
def test_build_url(
|
||||
basic_provider,
|
||||
retina_provider,
|
||||
silent_retina_provider,
|
||||
private_provider,
|
||||
subdomain_provider,
|
||||
):
|
||||
expected = "https://myserver.com/tiles/{z}/{x}/{y}.png"
|
||||
assert basic_provider.build_url() == expected
|
||||
|
||||
expected = "https://myserver.com/tiles/3/1/2.png"
|
||||
assert basic_provider.build_url(1, 2, 3) == expected
|
||||
assert basic_provider.build_url(1, 2, 3, scale_factor="@2x") == expected
|
||||
assert silent_retina_provider.build_url(1, 2, 3) == expected
|
||||
|
||||
expected = "https://myserver.com/tiles/3/1/2@2x.png"
|
||||
assert retina_provider.build_url(1, 2, 3) == expected
|
||||
assert silent_retina_provider.build_url(1, 2, 3, scale_factor="@2x") == expected
|
||||
|
||||
expected = "https://myserver.com/tiles/3/1/2@5x.png"
|
||||
assert retina_provider.build_url(1, 2, 3, scale_factor="@5x") == expected
|
||||
|
||||
expected = "https://myserver.com/tiles/{z}/{x}/{y}?access_token=my_token"
|
||||
assert private_provider.build_url(accessToken="my_token") == expected
|
||||
|
||||
with pytest.raises(ValueError, match="Token is required for this provider"):
|
||||
private_provider.build_url()
|
||||
|
||||
expected = "https://{s}.myserver.com/tiles/{z}/{x}/{y}.png"
|
||||
assert subdomain_provider.build_url(fill_subdomain=False)
|
||||
|
||||
expected = "https://a.myserver.com/tiles/{z}/{x}/{y}.png"
|
||||
assert subdomain_provider.build_url()
|
||||
|
||||
|
||||
def test_requires_token(private_provider, basic_provider):
|
||||
assert private_provider.requires_token() is True
|
||||
assert basic_provider.requires_token() is False
|
||||
|
||||
|
||||
def test_html_repr(basic_provider, retina_provider):
|
||||
provider_strings = [
|
||||
'<div class="xyz-wrap">',
|
||||
'<div class="xyz-header">',
|
||||
'<div class="xyz-obj">xyzservices.TileProvider</div>',
|
||||
'<div class="xyz-name">my_public_provider</div>',
|
||||
'<div class="xyz-details">',
|
||||
'<dl class="xyz-attrs">',
|
||||
"<dt><span>url</span></dt><dd>https://myserver.com/tiles/{z}/{x}/{y}.png</dd>",
|
||||
"<dt><span>attribution</span></dt><dd>(C) xyzservices</dd>",
|
||||
]
|
||||
|
||||
for html_string in provider_strings:
|
||||
assert html_string in basic_provider._repr_html_()
|
||||
|
||||
bunch = Bunch({"first": basic_provider, "second": retina_provider})
|
||||
|
||||
bunch_strings = [
|
||||
'<div class="xyz-obj">xyzservices.Bunch</div>',
|
||||
'<div class="xyz-name">2 items</div>',
|
||||
'<ul class="xyz-collapsible">',
|
||||
'<li class="xyz-child">',
|
||||
"<span>xyzservices.TileProvider</span>",
|
||||
'<div class="xyz-inside">',
|
||||
]
|
||||
|
||||
bunch_repr = bunch._repr_html_()
|
||||
for html_string in provider_strings + bunch_strings:
|
||||
assert html_string in bunch_repr
|
||||
assert bunch_repr.count('<li class="xyz-child">') == 2
|
||||
assert bunch_repr.count('<div class="xyz-wrap">') == 3
|
||||
assert bunch_repr.count('<div class="xyz-header">') == 3
|
||||
|
||||
|
||||
def test_copy(basic_provider):
|
||||
basic2 = basic_provider.copy()
|
||||
assert isinstance(basic2, TileProvider)
|
||||
|
||||
|
||||
def test_callable():
|
||||
# only testing the callable functionality to override a keyword, as we
|
||||
# cannot test the actual providers that need an API key
|
||||
original_key = str(xyz.OpenWeatherMap.CloudsClassic["apiKey"])
|
||||
updated_provider = xyz.OpenWeatherMap.CloudsClassic(apiKey="mykey")
|
||||
assert isinstance(updated_provider, TileProvider)
|
||||
assert "url" in updated_provider
|
||||
assert updated_provider["apiKey"] == "mykey"
|
||||
# check that original provider dict is not modified
|
||||
assert xyz.OpenWeatherMap.CloudsClassic["apiKey"] == original_key
|
||||
|
||||
|
||||
def test_html_attribution_fallback(basic_provider, html_attr_provider):
|
||||
# TileProvider.html_attribution falls back to .attribution if the former not present
|
||||
assert basic_provider.html_attribution == basic_provider.attribution
|
||||
assert (
|
||||
html_attr_provider.html_attribution
|
||||
== '© <a href="https://xyzservices.readthedocs.io">xyzservices</a>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="timeout error", raises=URLError)
|
||||
def test_from_qms():
|
||||
provider = TileProvider.from_qms("OpenStreetMap Standard aka Mapnik")
|
||||
assert isinstance(provider, TileProvider)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="timeout error", raises=URLError)
|
||||
def test_from_qms_not_found_error():
|
||||
with pytest.raises(ValueError):
|
||||
TileProvider.from_qms("LolWut")
|
||||
|
||||
|
||||
def test_flatten(
|
||||
basic_provider, retina_provider, silent_retina_provider, private_provider
|
||||
):
|
||||
nested_bunch = Bunch(
|
||||
first_bunch=Bunch(first=basic_provider, second=retina_provider),
|
||||
second_bunch=Bunch(first=silent_retina_provider, second=private_provider),
|
||||
)
|
||||
|
||||
assert len(nested_bunch) == 2
|
||||
assert len(nested_bunch.flatten()) == 4
|
||||
|
||||
|
||||
def test_filter(test_bunch):
|
||||
assert len(test_bunch.filter(keyword="private").flatten()) == 1
|
||||
assert len(test_bunch.filter(keyword="public").flatten()) == 4
|
||||
assert len(test_bunch.filter(keyword="{s}").flatten()) == 1
|
||||
assert len(test_bunch.filter(name="retina").flatten()) == 1
|
||||
assert len(test_bunch.filter(requires_token=True).flatten()) == 1
|
||||
assert len(test_bunch.filter(requires_token=False).flatten()) == 5
|
||||
assert len(test_bunch.filter(requires_token=False)) == 4 # check nested structure
|
||||
assert len(test_bunch.filter(keyword="{s}", requires_token=False).flatten()) == 1
|
||||
assert len(test_bunch.filter(name="nonsense").flatten()) == 0
|
||||
|
||||
def custom(provider):
|
||||
if hasattr(provider, "subdomains") and provider.subdomains == "abcd":
|
||||
return True
|
||||
if hasattr(provider, "r"):
|
||||
return True
|
||||
return False
|
||||
|
||||
assert len(test_bunch.filter(function=custom).flatten()) == 2
|
||||
|
||||
|
||||
def test_query_name():
|
||||
options = [
|
||||
"CartoDB Positron",
|
||||
"cartodbpositron",
|
||||
"cartodb-positron",
|
||||
"carto db/positron",
|
||||
"CARTO_DB_POSITRON",
|
||||
"CartoDB.Positron",
|
||||
"Carto,db,positron",
|
||||
]
|
||||
|
||||
for option in options:
|
||||
queried = xyz.query_name(option)
|
||||
assert isinstance(queried, TileProvider)
|
||||
assert queried.name == "CartoDB.Positron"
|
||||
|
||||
with pytest.raises(ValueError, match="No matching provider found"):
|
||||
xyz.query_name("i don't exist")
|
||||
|
||||
# Name with underscore GH124
|
||||
option_with_underscore = "NASAGIBS.ASTER_GDEM_Greyscale_Shaded_Relief"
|
||||
queried = xyz.query_name(option_with_underscore)
|
||||
assert isinstance(queried, TileProvider)
|
||||
assert queried.name == option_with_underscore
|
||||
@@ -0,0 +1,242 @@
|
||||
import os
|
||||
|
||||
import mercantile
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
import xyzservices.providers as xyz
|
||||
|
||||
flat_free = xyz.filter(requires_token=False).flatten()
|
||||
|
||||
|
||||
def check_provider(provider):
|
||||
for key in ["attribution", "name"]:
|
||||
assert key in provider
|
||||
assert provider.url.startswith("http")
|
||||
for option in ["{z}", "{y}", "{x}"]:
|
||||
assert option in provider.url
|
||||
|
||||
|
||||
def get_tile(provider):
|
||||
bounds = provider.get("bounds", [[-180, -90], [180, 90]])
|
||||
lat = (bounds[0][0] + bounds[1][0]) / 2
|
||||
lon = (bounds[0][1] + bounds[1][1]) / 2
|
||||
zoom = (provider.get("min_zoom", 0) + provider.get("max_zoom", 20)) // 2
|
||||
tile = mercantile.tile(lon, lat, zoom)
|
||||
z = tile.z
|
||||
x = tile.x
|
||||
y = tile.y
|
||||
return (z, x, y)
|
||||
|
||||
|
||||
def get_response(url):
|
||||
s = requests.Session()
|
||||
a = requests.adapters.HTTPAdapter(max_retries=3)
|
||||
s.mount("http://", a)
|
||||
s.mount("https://", a)
|
||||
try:
|
||||
r = s.get(url, timeout=30)
|
||||
except requests.ConnectionError:
|
||||
pytest.xfail("Timeout.")
|
||||
return r.status_code
|
||||
|
||||
|
||||
def get_test_result(provider, allow_403=True):
|
||||
if provider.get("status"):
|
||||
pytest.xfail("Provider is known to be broken.")
|
||||
|
||||
z, x, y = get_tile(provider)
|
||||
|
||||
try:
|
||||
r = get_response(provider.build_url(z=z, x=x, y=y))
|
||||
assert r == requests.codes.ok
|
||||
except AssertionError:
|
||||
if r == 403 and allow_403:
|
||||
pytest.xfail("Provider not available due to API restrictions (Error 403).")
|
||||
|
||||
elif r == 503:
|
||||
pytest.xfail("Service temporarily unavailable (Error 503).")
|
||||
|
||||
elif r == 502:
|
||||
pytest.xfail("Bad Gateway (Error 502).")
|
||||
|
||||
# check another tiles
|
||||
elif r == 404:
|
||||
# in some cases, the computed tile is not available. trying known tiles.
|
||||
options = [
|
||||
(12, 2154, 1363),
|
||||
(6, 13, 21),
|
||||
(16, 33149, 22973),
|
||||
(0, 0, 0),
|
||||
(2, 6, 7),
|
||||
(6, 21, 31),
|
||||
(6, 21, 32),
|
||||
(6, 21, 33),
|
||||
(6, 22, 31),
|
||||
(6, 22, 32),
|
||||
(6, 22, 33),
|
||||
(6, 23, 31),
|
||||
(6, 23, 32),
|
||||
(6, 23, 33),
|
||||
(9, 259, 181),
|
||||
(12, 2074, 1410),
|
||||
]
|
||||
results = []
|
||||
for o in options:
|
||||
z, x, y = o
|
||||
r = get_response(provider.build_url(z=z, x=x, y=y))
|
||||
results.append(r)
|
||||
if not any(x == requests.codes.ok for x in results):
|
||||
raise ValueError(f"Response code: {r}")
|
||||
else:
|
||||
raise ValueError(f"Response code: {r}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider_name", xyz.flatten())
|
||||
def test_minimal_provider_metadata(provider_name):
|
||||
provider = xyz.flatten()[provider_name]
|
||||
check_provider(provider)
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
@pytest.mark.parametrize("name", flat_free)
|
||||
def test_free_providers(name):
|
||||
provider = flat_free[name]
|
||||
if "Stadia" in name:
|
||||
pytest.skip("Stadia doesn't support tile download in this way.")
|
||||
get_test_result(provider)
|
||||
|
||||
|
||||
# test providers requiring API keys. Store API keys in GitHub secrets and load them as
|
||||
# environment variables in CI Action. Note that env variable is loaded as empty on PRs
|
||||
# from a fork.
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
@pytest.mark.parametrize("provider_name", xyz.Thunderforest)
|
||||
def test_thunderforest(provider_name):
|
||||
try:
|
||||
token = os.environ["THUNDERFOREST"]
|
||||
except KeyError:
|
||||
pytest.xfail("Missing API token.")
|
||||
if token == "":
|
||||
pytest.xfail("Token empty.")
|
||||
|
||||
provider = xyz.Thunderforest[provider_name](apikey=token)
|
||||
get_test_result(provider, allow_403=False)
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
@pytest.mark.parametrize("provider_name", xyz.Jawg)
|
||||
def test_jawg(provider_name):
|
||||
try:
|
||||
token = os.environ["JAWG"]
|
||||
except KeyError:
|
||||
pytest.xfail("Missing API token.")
|
||||
if token == "":
|
||||
pytest.xfail("Token empty.")
|
||||
|
||||
provider = xyz.Jawg[provider_name](accessToken=token)
|
||||
get_test_result(provider, allow_403=False)
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
def test_mapbox():
|
||||
try:
|
||||
token = os.environ["MAPBOX"]
|
||||
except KeyError:
|
||||
pytest.xfail("Missing API token.")
|
||||
if token == "":
|
||||
pytest.xfail("Token empty.")
|
||||
|
||||
provider = xyz.MapBox(accessToken=token)
|
||||
get_test_result(provider, allow_403=False)
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
@pytest.mark.parametrize("provider_name", xyz.MapTiler)
|
||||
def test_maptiler(provider_name):
|
||||
try:
|
||||
token = os.environ["MAPTILER"]
|
||||
except KeyError:
|
||||
pytest.xfail("Missing API token.")
|
||||
if token == "":
|
||||
pytest.xfail("Token empty.")
|
||||
|
||||
provider = xyz.MapTiler[provider_name](key=token)
|
||||
get_test_result(provider, allow_403=False)
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
@pytest.mark.parametrize("provider_name", xyz.TomTom)
|
||||
def test_tomtom(provider_name):
|
||||
try:
|
||||
token = os.environ["TOMTOM"]
|
||||
except KeyError:
|
||||
pytest.xfail("Missing API token.")
|
||||
if token == "":
|
||||
pytest.xfail("Token empty.")
|
||||
|
||||
provider = xyz.TomTom[provider_name](apikey=token)
|
||||
get_test_result(provider, allow_403=False)
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
@pytest.mark.parametrize("provider_name", xyz.OpenWeatherMap)
|
||||
def test_openweathermap(provider_name):
|
||||
try:
|
||||
token = os.environ["OPENWEATHERMAP"]
|
||||
except KeyError:
|
||||
pytest.xfail("Missing API token.")
|
||||
if token == "":
|
||||
pytest.xfail("Token empty.")
|
||||
|
||||
provider = xyz.OpenWeatherMap[provider_name](apiKey=token)
|
||||
get_test_result(provider, allow_403=False)
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
@pytest.mark.parametrize("provider_name", xyz.HEREv3)
|
||||
def test_herev3(provider_name):
|
||||
try:
|
||||
token = os.environ["HEREV3"]
|
||||
except KeyError:
|
||||
pytest.xfail("Missing API token.")
|
||||
if token == "":
|
||||
pytest.xfail("Token empty.")
|
||||
|
||||
provider = xyz.HEREv3[provider_name](apiKey=token)
|
||||
get_test_result(provider, allow_403=False)
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
@pytest.mark.parametrize("provider_name", xyz.Stadia)
|
||||
def test_stadia(provider_name):
|
||||
try:
|
||||
token = os.environ["STADIA"]
|
||||
except KeyError:
|
||||
pytest.xfail("Missing API token.")
|
||||
if token == "":
|
||||
pytest.xfail("Token empty.")
|
||||
|
||||
provider = xyz.Stadia[provider_name](api_key=token)
|
||||
provider["url"] = provider["url"] + "?api_key={api_key}"
|
||||
get_test_result(provider, allow_403=False)
|
||||
|
||||
|
||||
@pytest.mark.request
|
||||
@pytest.mark.parametrize("provider_name", xyz.OrdnanceSurvey)
|
||||
def test_os(provider_name):
|
||||
try:
|
||||
token = os.environ["ORDNANCESURVEY"]
|
||||
except KeyError:
|
||||
pytest.xfail("Missing API token.")
|
||||
if token == "":
|
||||
pytest.xfail("Token empty.")
|
||||
|
||||
provider = xyz.OrdnanceSurvey[provider_name](key=token)
|
||||
get_test_result(provider, allow_403=False)
|
||||
|
||||
|
||||
# NOTE: AzureMaps are not tested as their free account is limited to
|
||||
# 5000 downloads (total, not per month)
|
||||
Reference in New Issue
Block a user