tilearray 0.1.0__py3-none-any.whl
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.
- tilearray/__init__.py +89 -0
- tilearray/_version.py +3 -0
- tilearray/array.py +899 -0
- tilearray/decode.py +353 -0
- tilearray/errors.py +41 -0
- tilearray/fetch.py +772 -0
- tilearray/fetch_presets.py +77 -0
- tilearray/pressure.py +129 -0
- tilearray/py.typed +0 -0
- tilearray/service/__init__.py +33 -0
- tilearray/service/base.py +282 -0
- tilearray/service/config.py +455 -0
- tilearray/service/requests.py +70 -0
- tilearray/service/wcs.py +657 -0
- tilearray/service/wms.py +127 -0
- tilearray/service/wmts.py +590 -0
- tilearray/service/xyz.py +145 -0
- tilearray/tiles.py +142 -0
- tilearray/types.py +235 -0
- tilearray-0.1.0.dist-info/METADATA +345 -0
- tilearray-0.1.0.dist-info/RECORD +23 -0
- tilearray-0.1.0.dist-info/WHEEL +4 -0
- tilearray-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""Configuration helpers for constructing service instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
from requests import RequestException
|
|
10
|
+
|
|
11
|
+
from ..fetch import (
|
|
12
|
+
_DEFAULT_MULTIPLICATIVE_DECREASE,
|
|
13
|
+
FetchPolicy,
|
|
14
|
+
HostRateLimiter,
|
|
15
|
+
)
|
|
16
|
+
from ..types import CRS, Format, ServiceTypeEnum
|
|
17
|
+
from .base import BaseService, get_service
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ServiceConfig(BaseModel):
|
|
21
|
+
"""Serializable configuration describing how to build a service instance."""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
24
|
+
|
|
25
|
+
base_url: str = Field(..., description="Base endpoint URL for the service")
|
|
26
|
+
service_type: ServiceTypeEnum = Field(
|
|
27
|
+
..., description="Type of service to instantiate"
|
|
28
|
+
)
|
|
29
|
+
crs: CRS | None = Field(
|
|
30
|
+
None, description="Preferred coordinate reference system for requests"
|
|
31
|
+
)
|
|
32
|
+
output_format: Format | None = Field(
|
|
33
|
+
None, description="Preferred data format for tile requests"
|
|
34
|
+
)
|
|
35
|
+
headers: dict[str, str] = Field(
|
|
36
|
+
default_factory=dict, description="Additional HTTP headers to include"
|
|
37
|
+
)
|
|
38
|
+
params: dict[str, Any] = Field(
|
|
39
|
+
default_factory=dict, description="Additional query parameters to include"
|
|
40
|
+
)
|
|
41
|
+
chunk_size: tuple[int, int] | None = Field(
|
|
42
|
+
None,
|
|
43
|
+
description="Default chunk size (width, height) to use when building arrays",
|
|
44
|
+
)
|
|
45
|
+
grid_shape: tuple[int, int] | None = Field(
|
|
46
|
+
None,
|
|
47
|
+
description="Default grid shape (rows, cols) to use when planning tiles",
|
|
48
|
+
)
|
|
49
|
+
cache_dir: str | Path | None = Field(
|
|
50
|
+
None, description="Optional cache directory for fetched tiles"
|
|
51
|
+
)
|
|
52
|
+
resolution: tuple[float, float] | None = Field(
|
|
53
|
+
None,
|
|
54
|
+
description="Native resolution of the service responses (units per pixel in X and Y)",
|
|
55
|
+
)
|
|
56
|
+
max_concurrent_requests: int = Field(
|
|
57
|
+
default=3,
|
|
58
|
+
ge=1,
|
|
59
|
+
description="Maximum in-flight tile HTTP requests for this service",
|
|
60
|
+
)
|
|
61
|
+
initial_concurrent_requests: int = Field(
|
|
62
|
+
default=2,
|
|
63
|
+
ge=1,
|
|
64
|
+
description="Starting in-flight limit when adaptive_concurrency is enabled",
|
|
65
|
+
)
|
|
66
|
+
min_concurrent_requests: int = Field(
|
|
67
|
+
default=1,
|
|
68
|
+
ge=1,
|
|
69
|
+
description="Floor in-flight limit after AIMD multiplicative decrease",
|
|
70
|
+
)
|
|
71
|
+
multiplicative_decrease: float = Field(
|
|
72
|
+
default=_DEFAULT_MULTIPLICATIVE_DECREASE,
|
|
73
|
+
gt=0.0,
|
|
74
|
+
lt=1.0,
|
|
75
|
+
description="AIMD multiplicative decrease factor on pressure (0–1 exclusive)",
|
|
76
|
+
)
|
|
77
|
+
adaptive_concurrency: bool = Field(
|
|
78
|
+
default=False,
|
|
79
|
+
description=(
|
|
80
|
+
"Enable per-host AIMD concurrency (additive increase on success, "
|
|
81
|
+
"multiplicative decrease on 429/timeouts)"
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
max_connections: int | None = Field(
|
|
85
|
+
default=None,
|
|
86
|
+
ge=1,
|
|
87
|
+
description="httpx connection pool size (defaults to max_concurrent_requests)",
|
|
88
|
+
)
|
|
89
|
+
fetch_retries: int = Field(
|
|
90
|
+
default=3,
|
|
91
|
+
ge=0,
|
|
92
|
+
description="Retry attempts for retryable HTTP/network tile fetch failures",
|
|
93
|
+
)
|
|
94
|
+
fetch_timeout: float = Field(
|
|
95
|
+
default=30.0,
|
|
96
|
+
gt=0,
|
|
97
|
+
description="Per-tile HTTP read/connect timeout in seconds",
|
|
98
|
+
)
|
|
99
|
+
rate_limit_per_second: float | None = Field(
|
|
100
|
+
default=2.0,
|
|
101
|
+
description=(
|
|
102
|
+
"Conservative per-host request rate for tile fetches (requests/s); "
|
|
103
|
+
"set to None to disable the built-in limiter"
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
rate_limiter: HostRateLimiter | None = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
description="Optional custom per-host rate limiter hook",
|
|
109
|
+
)
|
|
110
|
+
forbidden_circuit_breaker: bool = Field(
|
|
111
|
+
default=False,
|
|
112
|
+
description=(
|
|
113
|
+
"Trip a pressure circuit breaker (freeze at floor) when classified "
|
|
114
|
+
"breaker events (e.g. gateway 403 or 429) cluster within "
|
|
115
|
+
"forbidden_window_seconds"
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
forbidden_window_seconds: float = Field(
|
|
119
|
+
default=5.0,
|
|
120
|
+
gt=0,
|
|
121
|
+
description="Sliding window for counting clustered breaker-classified events",
|
|
122
|
+
)
|
|
123
|
+
forbidden_threshold: int = Field(
|
|
124
|
+
default=6,
|
|
125
|
+
ge=1,
|
|
126
|
+
description="Breaker event count within the window that trips the circuit breaker",
|
|
127
|
+
)
|
|
128
|
+
forbidden_cooldown_seconds: float = Field(
|
|
129
|
+
default=15.0,
|
|
130
|
+
gt=0,
|
|
131
|
+
description="Cooldown at floor after the circuit breaker trips",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def build_service(self) -> BaseService:
|
|
135
|
+
"""Create the appropriate service implementation for this configuration."""
|
|
136
|
+
|
|
137
|
+
return get_service(
|
|
138
|
+
self.base_url,
|
|
139
|
+
service_type=self.service_type,
|
|
140
|
+
**self.service_kwargs(),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
# Helper accessors
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
def fetch_policy(self) -> FetchPolicy:
|
|
147
|
+
"""Build a :class:`~tilearray.fetch.FetchPolicy` from this configuration."""
|
|
148
|
+
|
|
149
|
+
return FetchPolicy(
|
|
150
|
+
max_concurrent=self.max_concurrent_requests,
|
|
151
|
+
max_connections=self.max_connections,
|
|
152
|
+
retries=self.fetch_retries,
|
|
153
|
+
timeout=self.fetch_timeout,
|
|
154
|
+
rate_limit_per_second=self.rate_limit_per_second,
|
|
155
|
+
rate_limiter=self.rate_limiter,
|
|
156
|
+
adaptive_concurrency=self.adaptive_concurrency,
|
|
157
|
+
initial_concurrent=self.initial_concurrent_requests,
|
|
158
|
+
min_concurrent=self.min_concurrent_requests,
|
|
159
|
+
multiplicative_decrease=self.multiplicative_decrease,
|
|
160
|
+
forbidden_circuit_breaker=self.forbidden_circuit_breaker,
|
|
161
|
+
forbidden_window_seconds=self.forbidden_window_seconds,
|
|
162
|
+
forbidden_threshold=self.forbidden_threshold,
|
|
163
|
+
forbidden_cooldown_seconds=self.forbidden_cooldown_seconds,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def service_kwargs(self) -> dict[str, Any]:
|
|
167
|
+
"""Keyword arguments used when instantiating the service."""
|
|
168
|
+
|
|
169
|
+
kwargs: dict[str, Any] = {}
|
|
170
|
+
if self.crs is not None:
|
|
171
|
+
kwargs["crs"] = self.crs
|
|
172
|
+
if self.output_format is not None:
|
|
173
|
+
kwargs["output_format"] = self.output_format
|
|
174
|
+
if self.headers:
|
|
175
|
+
kwargs["headers"] = dict(self.headers)
|
|
176
|
+
if self.params:
|
|
177
|
+
kwargs["params"] = dict(self.params)
|
|
178
|
+
return kwargs
|
|
179
|
+
|
|
180
|
+
def tile_kwargs(self) -> dict[str, Any]:
|
|
181
|
+
"""Keyword arguments provided to the tile request planner."""
|
|
182
|
+
|
|
183
|
+
kwargs: dict[str, Any] = {}
|
|
184
|
+
if self.crs is not None:
|
|
185
|
+
kwargs["crs"] = self.crs
|
|
186
|
+
if self.output_format is not None:
|
|
187
|
+
kwargs["output_format"] = self.output_format
|
|
188
|
+
if self.params:
|
|
189
|
+
kwargs["params"] = dict(self.params)
|
|
190
|
+
if self.headers:
|
|
191
|
+
kwargs["headers"] = dict(self.headers)
|
|
192
|
+
if self.resolution is not None:
|
|
193
|
+
kwargs["resolution"] = self.resolution
|
|
194
|
+
return kwargs
|
|
195
|
+
|
|
196
|
+
def array_defaults(self) -> dict[str, Any]:
|
|
197
|
+
"""Default array-level configuration supplied by the service."""
|
|
198
|
+
|
|
199
|
+
defaults: dict[str, Any] = {}
|
|
200
|
+
if self.chunk_size is not None:
|
|
201
|
+
defaults["chunk_size"] = self.chunk_size
|
|
202
|
+
if self.grid_shape is not None:
|
|
203
|
+
defaults["grid_shape"] = self.grid_shape
|
|
204
|
+
if self.cache_dir is not None:
|
|
205
|
+
defaults["cache_dir"] = self.cache_dir
|
|
206
|
+
if self.resolution is not None:
|
|
207
|
+
defaults["resolution"] = self.resolution
|
|
208
|
+
return defaults
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class WCSConfig(ServiceConfig):
|
|
212
|
+
"""Configuration helper for Web Coverage Services."""
|
|
213
|
+
|
|
214
|
+
coverage_id: str = Field(..., description="Coverage identifier to request")
|
|
215
|
+
version: str = Field(default="2.0.1", description="WCS protocol version")
|
|
216
|
+
service_type: ServiceTypeEnum = Field(
|
|
217
|
+
default=ServiceTypeEnum.WCS, init=False, description="Service type constant"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def from_url(cls, url: str, coverage_id: str, **kwargs: Any) -> WCSConfig:
|
|
222
|
+
"""Convenience constructor mirroring high-level usage patterns."""
|
|
223
|
+
|
|
224
|
+
return cls(base_url=url, coverage_id=coverage_id, **kwargs)
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def for_ea_dsp(
|
|
228
|
+
cls,
|
|
229
|
+
url: str,
|
|
230
|
+
coverage_id: str,
|
|
231
|
+
**kwargs: Any,
|
|
232
|
+
) -> WCSConfig:
|
|
233
|
+
"""
|
|
234
|
+
WCS preset for Environment Agency Data Service Platform endpoints.
|
|
235
|
+
|
|
236
|
+
Enables per-host AIMD concurrency (starts at 8, ceiling 10) plus a
|
|
237
|
+
sustained-403 circuit breaker for large cold mosaics on Azure App Gateway.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
from ..fetch_presets import ea_dsp_fetch_defaults
|
|
241
|
+
|
|
242
|
+
defaults = ea_dsp_fetch_defaults()
|
|
243
|
+
defaults.update(kwargs)
|
|
244
|
+
return cls.from_url(url, coverage_id=coverage_id, **defaults)
|
|
245
|
+
|
|
246
|
+
def build_service(self) -> BaseService:
|
|
247
|
+
"""Construct a ``WCSService`` instance from this configuration."""
|
|
248
|
+
|
|
249
|
+
from .wcs import WCSService
|
|
250
|
+
|
|
251
|
+
kwargs = self.service_kwargs()
|
|
252
|
+
kwargs.setdefault("coverage_id", self.coverage_id)
|
|
253
|
+
kwargs.setdefault("version", self.version)
|
|
254
|
+
service = WCSService(self.base_url, **kwargs)
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
service.describe_coverage(self.coverage_id)
|
|
258
|
+
except ValueError as exc:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f"Coverage '{self.coverage_id}' is not available from {self.base_url}" # noqa: G004
|
|
261
|
+
) from exc
|
|
262
|
+
except RequestException as exc:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Failed to validate coverage '{self.coverage_id}' against {self.base_url}" # noqa: G004
|
|
265
|
+
) from exc
|
|
266
|
+
|
|
267
|
+
return service
|
|
268
|
+
|
|
269
|
+
def service_kwargs(self) -> dict[str, Any]:
|
|
270
|
+
kwargs = super().service_kwargs()
|
|
271
|
+
kwargs.setdefault("coverage_id", self.coverage_id)
|
|
272
|
+
kwargs.setdefault("version", self.version)
|
|
273
|
+
return kwargs
|
|
274
|
+
|
|
275
|
+
def tile_kwargs(self) -> dict[str, Any]:
|
|
276
|
+
kwargs = super().tile_kwargs()
|
|
277
|
+
kwargs["coverage_id"] = self.coverage_id
|
|
278
|
+
return kwargs
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class XYZConfig(ServiceConfig):
|
|
282
|
+
"""Configuration helper for XYZ / slippy-map tile templates."""
|
|
283
|
+
|
|
284
|
+
zoom: int = Field(..., ge=0, description="Fixed zoom level for tile requests")
|
|
285
|
+
tile_size: int = Field(default=256, gt=0, description="Tile edge length in pixels")
|
|
286
|
+
service_type: ServiceTypeEnum = Field(
|
|
287
|
+
default=ServiceTypeEnum.XYZ, init=False, description="Service type constant"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def from_url(cls, url: str, *, zoom: int, **kwargs: Any) -> XYZConfig:
|
|
292
|
+
"""Convenience constructor for XYZ tile templates."""
|
|
293
|
+
|
|
294
|
+
return cls(base_url=url, zoom=zoom, **kwargs)
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def for_openstreetmap(
|
|
298
|
+
cls,
|
|
299
|
+
*,
|
|
300
|
+
zoom: int,
|
|
301
|
+
user_agent: str | None = None,
|
|
302
|
+
contact_url: str | None = None,
|
|
303
|
+
**kwargs: Any,
|
|
304
|
+
) -> XYZConfig:
|
|
305
|
+
"""
|
|
306
|
+
XYZ preset for OpenStreetMap raster tiles (OSMF tile usage policy).
|
|
307
|
+
|
|
308
|
+
Sets an identifiable User-Agent and polite concurrency / rate limits.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
from ..fetch_presets import OSM_TILE_TEMPLATE, osm_fetch_defaults
|
|
312
|
+
|
|
313
|
+
defaults = osm_fetch_defaults(
|
|
314
|
+
user_agent=user_agent,
|
|
315
|
+
contact_url=contact_url
|
|
316
|
+
or "https://github.com/MatthewJWhittle/tilearray/issues",
|
|
317
|
+
)
|
|
318
|
+
defaults.update(kwargs)
|
|
319
|
+
return cls.from_url(OSM_TILE_TEMPLATE, zoom=zoom, **defaults)
|
|
320
|
+
|
|
321
|
+
def build_service(self) -> BaseService:
|
|
322
|
+
"""Construct an ``XYZService`` instance from this configuration."""
|
|
323
|
+
|
|
324
|
+
from .xyz import XYZService
|
|
325
|
+
|
|
326
|
+
kwargs = self.service_kwargs()
|
|
327
|
+
kwargs.setdefault("zoom", self.zoom)
|
|
328
|
+
kwargs.setdefault("tile_size", self.tile_size)
|
|
329
|
+
return XYZService(self.base_url, **kwargs)
|
|
330
|
+
|
|
331
|
+
def service_kwargs(self) -> dict[str, Any]:
|
|
332
|
+
kwargs = super().service_kwargs()
|
|
333
|
+
kwargs.setdefault("zoom", self.zoom)
|
|
334
|
+
kwargs.setdefault("tile_size", self.tile_size)
|
|
335
|
+
return kwargs
|
|
336
|
+
|
|
337
|
+
def tile_kwargs(self) -> dict[str, Any]:
|
|
338
|
+
kwargs = super().tile_kwargs()
|
|
339
|
+
kwargs["zoom"] = self.zoom
|
|
340
|
+
kwargs["tile_size"] = self.tile_size
|
|
341
|
+
return kwargs
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class WMSConfig(ServiceConfig):
|
|
345
|
+
"""Configuration helper for Web Map Services."""
|
|
346
|
+
|
|
347
|
+
layers: str = Field(..., description="Comma-separated WMS layer names")
|
|
348
|
+
version: str = Field(default="1.3.0", description="WMS protocol version")
|
|
349
|
+
styles: str = Field(default="", description="WMS style names")
|
|
350
|
+
service_type: ServiceTypeEnum = Field(
|
|
351
|
+
default=ServiceTypeEnum.WMS, init=False, description="Service type constant"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
@classmethod
|
|
355
|
+
def from_url(cls, url: str, layers: str, **kwargs: Any) -> WMSConfig:
|
|
356
|
+
"""Convenience constructor for WMS endpoints."""
|
|
357
|
+
|
|
358
|
+
return cls(base_url=url, layers=layers, **kwargs)
|
|
359
|
+
|
|
360
|
+
def build_service(self) -> BaseService:
|
|
361
|
+
"""Construct a ``WMSService`` instance from this configuration."""
|
|
362
|
+
|
|
363
|
+
from .wms import WMSService
|
|
364
|
+
|
|
365
|
+
kwargs = self.service_kwargs()
|
|
366
|
+
kwargs.setdefault("layers", self.layers)
|
|
367
|
+
kwargs.setdefault("version", self.version)
|
|
368
|
+
kwargs.setdefault("styles", self.styles)
|
|
369
|
+
return WMSService(self.base_url, **kwargs)
|
|
370
|
+
|
|
371
|
+
def service_kwargs(self) -> dict[str, Any]:
|
|
372
|
+
kwargs = super().service_kwargs()
|
|
373
|
+
kwargs.setdefault("layers", self.layers)
|
|
374
|
+
kwargs.setdefault("version", self.version)
|
|
375
|
+
kwargs.setdefault("styles", self.styles)
|
|
376
|
+
return kwargs
|
|
377
|
+
|
|
378
|
+
def tile_kwargs(self) -> dict[str, Any]:
|
|
379
|
+
kwargs = super().tile_kwargs()
|
|
380
|
+
kwargs["layers"] = self.layers
|
|
381
|
+
kwargs["styles"] = self.styles
|
|
382
|
+
return kwargs
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class WMTSConfig(ServiceConfig):
|
|
386
|
+
"""Configuration helper for Web Map Tile Services."""
|
|
387
|
+
|
|
388
|
+
layer: str = Field(..., description="WMTS layer identifier")
|
|
389
|
+
tile_matrix_set: str = Field(..., description="Tile matrix set identifier")
|
|
390
|
+
tile_matrix: int = Field(..., ge=0, description="Tile matrix / zoom level")
|
|
391
|
+
style: str = Field(default="", description="WMTS style identifier")
|
|
392
|
+
url_template: str | None = Field(
|
|
393
|
+
default=None,
|
|
394
|
+
description="REST GetTile URL template from GetCapabilities",
|
|
395
|
+
)
|
|
396
|
+
version: str = Field(default="1.0.0", description="WMTS protocol version")
|
|
397
|
+
service_type: ServiceTypeEnum = Field(
|
|
398
|
+
default=ServiceTypeEnum.WMTS, init=False, description="Service type constant"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
@classmethod
|
|
402
|
+
def from_url(
|
|
403
|
+
cls,
|
|
404
|
+
url: str,
|
|
405
|
+
*,
|
|
406
|
+
layer: str,
|
|
407
|
+
tile_matrix_set: str,
|
|
408
|
+
tile_matrix: int,
|
|
409
|
+
**kwargs: Any,
|
|
410
|
+
) -> WMTSConfig:
|
|
411
|
+
"""Convenience constructor for WMTS endpoints."""
|
|
412
|
+
|
|
413
|
+
return cls(
|
|
414
|
+
base_url=url,
|
|
415
|
+
layer=layer,
|
|
416
|
+
tile_matrix_set=tile_matrix_set,
|
|
417
|
+
tile_matrix=tile_matrix,
|
|
418
|
+
**kwargs,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def build_service(self) -> BaseService:
|
|
422
|
+
"""Construct a ``WMTSService`` instance from this configuration."""
|
|
423
|
+
|
|
424
|
+
from .wmts import WMTSService
|
|
425
|
+
|
|
426
|
+
kwargs = self.service_kwargs()
|
|
427
|
+
kwargs.setdefault("layer", self.layer)
|
|
428
|
+
kwargs.setdefault("tile_matrix_set", self.tile_matrix_set)
|
|
429
|
+
kwargs.setdefault("tile_matrix", self.tile_matrix)
|
|
430
|
+
kwargs.setdefault("style", self.style)
|
|
431
|
+
kwargs.setdefault("version", self.version)
|
|
432
|
+
if self.url_template is not None:
|
|
433
|
+
kwargs.setdefault("url_template", self.url_template)
|
|
434
|
+
return WMTSService(self.base_url, **kwargs)
|
|
435
|
+
|
|
436
|
+
def service_kwargs(self) -> dict[str, Any]:
|
|
437
|
+
kwargs = super().service_kwargs()
|
|
438
|
+
kwargs.setdefault("layer", self.layer)
|
|
439
|
+
kwargs.setdefault("tile_matrix_set", self.tile_matrix_set)
|
|
440
|
+
kwargs.setdefault("tile_matrix", self.tile_matrix)
|
|
441
|
+
kwargs.setdefault("style", self.style)
|
|
442
|
+
kwargs.setdefault("version", self.version)
|
|
443
|
+
if self.url_template is not None:
|
|
444
|
+
kwargs.setdefault("url_template", self.url_template)
|
|
445
|
+
return kwargs
|
|
446
|
+
|
|
447
|
+
def tile_kwargs(self) -> dict[str, Any]:
|
|
448
|
+
kwargs = super().tile_kwargs()
|
|
449
|
+
kwargs["layer"] = self.layer
|
|
450
|
+
kwargs["tile_matrix_set"] = self.tile_matrix_set
|
|
451
|
+
kwargs["tile_matrix"] = self.tile_matrix
|
|
452
|
+
kwargs["style"] = self.style
|
|
453
|
+
if self.url_template is not None:
|
|
454
|
+
kwargs["url_template"] = self.url_template
|
|
455
|
+
return kwargs
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Shared helpers for composing HTTP tile requests from service configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..types import CRS, BoundingBox, Format, TileRequest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def merge_str_mappings(*sources: object) -> dict[str, str]:
|
|
11
|
+
"""Merge string-keyed mappings from config/options overlays."""
|
|
12
|
+
|
|
13
|
+
merged: dict[str, str] = {}
|
|
14
|
+
for source in sources:
|
|
15
|
+
if isinstance(source, dict):
|
|
16
|
+
merged.update({str(key): str(value) for key, value in source.items()})
|
|
17
|
+
return merged
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def merge_params(*sources: object) -> dict[str, Any]:
|
|
21
|
+
"""Merge query-parameter mappings from config/options overlays."""
|
|
22
|
+
|
|
23
|
+
merged: dict[str, Any] = {}
|
|
24
|
+
for source in sources:
|
|
25
|
+
if isinstance(source, dict):
|
|
26
|
+
merged.update(source)
|
|
27
|
+
return merged
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compose_tile_request(
|
|
31
|
+
*,
|
|
32
|
+
config: dict[str, object],
|
|
33
|
+
options: dict[str, object],
|
|
34
|
+
url: str,
|
|
35
|
+
bbox: BoundingBox,
|
|
36
|
+
width: int,
|
|
37
|
+
height: int,
|
|
38
|
+
crs: CRS,
|
|
39
|
+
params: dict[str, Any] | None = None,
|
|
40
|
+
output_format: Format | None = None,
|
|
41
|
+
) -> TileRequest:
|
|
42
|
+
"""
|
|
43
|
+
Build a :class:`TileRequest` with headers and params wired from config.
|
|
44
|
+
|
|
45
|
+
Service implementations supply URL/bbox/format specifics; this helper
|
|
46
|
+
ensures headers (User-Agent, auth hooks, etc.) and params always land
|
|
47
|
+
on the outgoing request.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
headers = merge_str_mappings(config.get("headers"), options.get("headers"))
|
|
51
|
+
merged_params = merge_params(config.get("params"), params, options.get("params"))
|
|
52
|
+
|
|
53
|
+
fmt = output_format
|
|
54
|
+
if fmt is None:
|
|
55
|
+
fmt_option = options.get("output_format")
|
|
56
|
+
if isinstance(fmt_option, Format):
|
|
57
|
+
fmt = fmt_option
|
|
58
|
+
elif isinstance(fmt_option, str):
|
|
59
|
+
fmt = Format(fmt_option)
|
|
60
|
+
|
|
61
|
+
return TileRequest(
|
|
62
|
+
url=url,
|
|
63
|
+
params=merged_params,
|
|
64
|
+
headers=headers or None,
|
|
65
|
+
output_format=fmt,
|
|
66
|
+
crs=crs,
|
|
67
|
+
bbox=bbox,
|
|
68
|
+
width=width,
|
|
69
|
+
height=height,
|
|
70
|
+
)
|