Coverage for gws-app/gws/base/ows/client/provider.py: 41%
102 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Base OWS service provider."""
3from typing import Optional
4import base64
6import gws
7import gws.lib.crs
8import gws.gis.source
9import gws.lib.net
10import gws.lib.mime
12from . import request
14_IMAGE_VERBS = {
15 gws.OwsVerb.GetLegendGraphic,
16 gws.OwsVerb.GetMap,
17 gws.OwsVerb.GetTile,
18}
20_PREFER_IMAGE_MIME = [gws.lib.mime.PNG, gws.lib.mime.JPEG]
21_PREFER_XML_MIME = [gws.lib.mime.GML3, gws.lib.mime.GML, gws.lib.mime.XML]
24class OperationConfig(gws.Config):
25 """OWS operation that overrides or adds to those in the service capabilities."""
26 verb: gws.OwsVerb
27 """OWS request type."""
28 formats: Optional[list[str]]
29 """Formats supported by the operation."""
30 params: Optional[dict]
31 """Extra parameters added to GET requests for this operation."""
32 postUrl: Optional[gws.Url]
33 """URL for POST requests."""
34 url: Optional[gws.Url]
35 """URL for GET requests."""
38class AuthorizationConfig(gws.Config):
39 """Credentials for accessing the service."""
40 type: str
41 """Authorization type (only "basic" is supported)."""
42 username: str = ''
43 """User name."""
44 password: str = ''
45 """Password."""
48class Config(gws.Config):
49 """OWS provider configuration."""
51 capsCacheMaxAge: gws.Duration = '1d'
52 """How long a downloaded capabilities document is cached."""
53 forceCrs: Optional[gws.CrsName]
54 """Use this CRS for requests."""
55 alwaysXY: bool = False
56 """Force XY axis order for CRSs with lat/lon axis order."""
57 maxRequests: int = 0
58 """Max. concurrent requests to this source."""
59 operations: Optional[list[OperationConfig]]
60 """Override operations reported in capabilities."""
61 authorization: Optional[AuthorizationConfig]
62 """Service authorization options."""
63 url: gws.Url
64 """Service URL."""
67class Object(gws.OwsServiceProvider):
68 """Base OWS service provider.
70 Holds the service URL, version, operations and source layers of a remote
71 OWS service, and prepares request arguments for its operations. Provides
72 subclasses with capabilities download and caching, merging of configured
73 and reported operations and format selection. Protocol-specific subclasses
74 set ``protocol`` and parse the capabilities.
75 """
77 def configure(self):
78 self.alwaysXY = self.cfg('alwaysXY', default=False)
79 self.forceCrs = gws.lib.crs.get(self.cfg('forceCrs'))
80 self.maxRequests = self.cfg('maxRequests')
81 self.operations = []
82 self.sourceLayers = []
83 self.url = self.cfg('url')
84 self.version = ''
86 p = self.cfg('authorization')
87 self.authorization = gws.OwsAuthorization(p) if p else None
89 def configure_operations(self, operations_from_caps):
90 """Set up the provider operations.
92 Operations from the configuration replace those from the capabilities
93 with the same verb; attributes missing in the configuration are taken from
94 the capabilities. A preferred format is then chosen for each operation:
95 PNG or JPEG for image requests, GML3, GML or XML for others.
97 Args:
98 operations_from_caps: Operations read from the capabilities.
99 """
101 d = {}
103 for op in operations_from_caps:
104 d[op.verb] = op
106 for cfg in (self.cfg('operations') or []):
107 # add an operation from the config, borrowing missing attributes from a caps op
108 verb = cfg.get('verb')
109 caps_op = d.get(verb, {})
110 d[verb] = gws.OwsOperation(
111 verb=verb,
112 formats=gws.u.first_not_none(cfg.get('formats'), caps_op.get('formats'), []),
113 params=gws.u.first_not_none(cfg.get('params'), caps_op.get('params'), {}),
114 postUrl=gws.u.first_not_none(cfg.get('postUrl'), caps_op.get('postUrl'), ''),
115 url=gws.u.first_not_none(cfg.get('url'), caps_op.get('url'), '')
116 )
118 self.operations = list(d.values())
120 for op in self.operations:
121 op.preferredFormat = self._preferred_format(op)
123 def _preferred_format(self, op: gws.OwsOperation) -> Optional[str]:
124 """Pick the best supported format of an operation, or the first one if none is preferred."""
125 prefer_fmts = _PREFER_IMAGE_MIME if op.verb in _IMAGE_VERBS else _PREFER_XML_MIME
127 if not op.formats:
128 return prefer_fmts[0]
130 # select the "best" (leftmost in the preferred list) format
132 best_pos = 999
133 best_fmt = ''
135 for fmt in op.formats:
136 canon_fmt = gws.lib.mime.get(fmt)
137 if not canon_fmt or canon_fmt not in prefer_fmts:
138 continue
139 pos = prefer_fmts.index(canon_fmt)
140 if pos < best_pos:
141 best_pos = pos
142 best_fmt = fmt
144 return best_fmt or op.formats[0]
146 def cache_hash(self):
147 """Compute a hash of the provider settings, used to build cache keys.
149 Returns:
150 A hash of the URL, the forced CRS and the axis order setting.
151 """
153 return gws.u.sha256([
154 self.url,
155 self.forceCrs.srid if self.forceCrs else '',
156 self.alwaysXY,
157 ])
159 def get_operation(self, verb, method=None):
160 for op in self.operations:
161 if op.verb == verb:
162 url = op.postUrl if method == gws.RequestMethod.POST else op.url
163 if url:
164 return op
166 def prepare_operation(self, op: gws.OwsOperation, method: gws.RequestMethod = None, params=None) -> request.Args:
167 """Prepare the request arguments for an operation.
169 For GET requests, the operation parameters are included. Parameter names
170 are upper-cased and checked against the operation's allowed values.
171 Adds a basic ``Authorization`` header if configured.
173 Args:
174 op: Operation.
175 method: Request method, ``GET`` by default.
176 params: Request parameters.
178 Returns:
179 Request arguments, to be passed to ``request.get``.
181 Raises:
182 ``gws.Error``: If a parameter value is not allowed by the operation.
183 """
185 args = request.Args(
186 method=method or gws.RequestMethod.GET,
187 headers={},
188 params={},
189 protocol=self.protocol,
190 verb=op.verb,
191 version=self.version,
192 )
194 if args.method == gws.RequestMethod.GET:
195 args.params.update(op.params)
197 args.url = op.url
198 if args.method == gws.RequestMethod.POST:
199 args.url = op.postUrl
201 allowed = op.allowedParameters or {}
203 if params:
204 for name, val in params.items():
205 name = name.upper()
206 if name in allowed and val not in allowed[name]:
207 raise gws.Error(f'invalid parameter value {val!r} for {name!r}')
208 args.params[name] = val
210 if self.authorization and self.authorization.type == 'basic':
211 b = base64.encodebytes(
212 gws.u.to_bytes(self.authorization.username) + b':' + gws.u.to_bytes(self.authorization.password))
213 args.headers['Authorization'] = 'Basic ' + gws.u.to_str(b).strip()
215 return args
217 def get_capabilities(self):
218 """Download the capabilities document of the service.
220 The document is cached for ``capsCacheMaxAge``.
222 Returns:
223 The capabilities XML text.
225 Raises:
226 ``gws.ExternalServiceError``: If the request fails.
227 """
229 url, params = gws.lib.net.extract_params(self.url)
230 op = gws.OwsOperation(
231 formats=[gws.lib.mime.XML],
232 url=url,
233 params=params,
234 verb=gws.OwsVerb.GetCapabilities,
235 )
236 args = self.prepare_operation(op)
237 return gws.u.get_cached_object(
238 f'ows_caps_{gws.u.sha256([args.url, args.params])}',
239 self.cfg('capsCacheMaxAge'),
240 lambda: request.get_text(args),
241 )