initial commit

This commit is contained in:
klein panic
2024-09-29 01:46:07 -04:00
commit ba6e6914d1
7576 changed files with 1356825 additions and 0 deletions

View File

@@ -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='&copy; <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
== '&copy; <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

View File

@@ -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)