Coverage for gws-app/gws/base/ows/client/provider.py: 95%
106 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
1"""Base OWS service provider."""
3from typing import Optional
5import gws
6import gws.lib.crs
7import gws.gis.source
8import gws.lib.net
9import gws.lib.mime
10import gws.lib.text
12from . import request, operation
14_MIME_TYPE_PARAMS = {'FORMAT', 'INFO_FORMAT', 'OUTPUTFORMAT'}
17class AuthorizationConfig(gws.Config):
18 """Credentials for accessing the service."""
20 type: str
21 """Authorization type ("basic" or "token")."""
22 username: str = ''
23 """User name."""
24 password: str = ''
25 """Password."""
26 header: str = 'Authorization'
27 """HTTP header that carries the token."""
28 prefix: str = ''
29 """Prefix expected before the token in the header value."""
30 token: str = ''
31 """Authorization token."""
34class Config(gws.Config):
35 """OWS provider configuration."""
37 capsCacheMaxAge: gws.Duration = '1d'
38 """How long a downloaded capabilities document is cached."""
39 forceCrs: Optional[gws.CrsName]
40 """Use this CRS for requests."""
41 alwaysXY: bool = False
42 """Force XY axis order for CRSs with lat/lon axis order."""
43 maxRequests: int = 0
44 """Max. concurrent requests to this source."""
45 operations: Optional[list[operation.Config]]
46 """Override operations reported in capabilities."""
47 authorization: Optional[AuthorizationConfig]
48 """Service authorization options."""
49 url: gws.Url
50 """Service URL."""
53class Object(gws.OwsServiceProvider):
54 """Base OWS service provider.
56 Holds the service URL, version, operations and source layers of a remote
57 OWS service, and prepares request arguments for its operations. Provides
58 subclasses with capabilities download and caching, merging of configured
59 and reported operations and format selection. Protocol-specific subclasses
60 set ``protocol`` and parse the capabilities.
61 """
63 def configure(self):
64 self.alwaysXY = self.cfg('alwaysXY', default=False)
65 self.forceCrs = gws.lib.crs.get(self.cfg('forceCrs'))
66 self.maxRequests = self.cfg('maxRequests')
67 self.operations = []
68 self.sourceLayers = []
69 self.url = self.cfg('url')
70 self.version = ''
72 self.configure_authorization()
74 def configure_authorization(self):
75 self.authorization = None
77 p = self.cfg('authorization')
78 if not p:
79 return
81 a = gws.OwsAuthorization(p)
83 if a.type == 'basic' and not a.username:
84 raise gws.ConfigurationError('basic authorization: "username" is required')
85 if a.type == 'token' and not a.header:
86 a.header = 'Authorization'
87 if a.type == 'token' and not a.token:
88 raise gws.ConfigurationError('token authorization: "token" is required')
89 if a.type not in ('basic', 'token'):
90 raise gws.ConfigurationError(f'invalid authorization type {a.type!r}')
92 self.authorization = a
94 def configure_operations(self, operations_from_caps):
95 """Set up the provider operations.
97 Operations from the configuration replace those from the capabilities
98 with the same verb; attributes missing in the configuration are taken from
99 the capabilities (see ``operation.create``).
101 Args:
102 operations_from_caps: Operations read from the capabilities.
103 """
105 ops = {}
106 cfgs = self.cfg('operations')
108 for op in operations_from_caps:
109 ops[op.verb] = operation.create(op.verb, op, cfgs)
111 if cfgs:
112 for c in cfgs:
113 if c.get('verb') not in ops:
114 ops[c.get('verb')] = operation.create(c.get('verb'), None, cfgs)
116 self.operations = list(ops.values())
118 def cache_hash(self):
119 """Compute a hash of the provider settings, used to build cache keys.
121 Returns:
122 A hash of the URL, the forced CRS and the axis order setting.
123 """
125 return gws.u.sha256(
126 [
127 self.url,
128 self.forceCrs.srid if self.forceCrs else '',
129 self.alwaysXY,
130 ]
131 )
133 def get_operation(self, verb, method=None):
134 for op in self.operations:
135 if op.verb == verb:
136 url = op.postUrl if method == gws.RequestMethod.POST else op.url
137 if url:
138 return op
140 def prepare_operation(self, op: gws.OwsOperation, method: gws.RequestMethod = None, params=None) -> request.Args:
141 """Prepare the request arguments for an operation.
143 For GET requests, the operation parameters are included. Parameter names
144 are upper-cased and checked against the operation's allowed values. MIME types
145 in ``FORMAT``, ``INFO_FORMAT`` and ``OUTPUTFORMAT`` are sent in the spelling of the service.
146 Adds a basic ``Authorization`` header or a token header if configured.
148 Args:
149 op: Operation.
150 method: Request method, ``GET`` by default.
151 params: Request parameters.
153 Returns:
154 Request arguments, to be passed to ``request.get``.
156 Raises:
157 ``gws.Error``: If a parameter value is not allowed by the operation.
158 """
160 args = request.Args(
161 method=method or gws.RequestMethod.GET,
162 headers={},
163 params={},
164 protocol=self.protocol,
165 verb=op.verb,
166 version=self.version,
167 )
169 if args.method == gws.RequestMethod.GET:
170 args.params.update(op.params)
172 args.url = op.url
173 if args.method == gws.RequestMethod.POST:
174 args.url = op.postUrl
176 if params:
177 for name, val in params.items():
178 name = name.upper()
179 if name in _MIME_TYPE_PARAMS and val in op.mimeTypeMap:
180 val = op.mimeTypeMap[val]
181 if name in op.allowedParameters and val not in op.allowedParameters[name]:
182 raise gws.Error(f'invalid parameter value {val!r} for {name!r}')
183 args.params[name] = val
185 if self.authorization and self.authorization.type == 'basic':
186 u = self.authorization.username or ''
187 p = self.authorization.password or ''
188 b = gws.lib.text.to_base64(f'{u}:{p}')
189 args.headers['Authorization'] = 'Basic ' + b
191 if self.authorization and self.authorization.type == 'token':
192 h = self.authorization.header or ''
193 p = self.authorization.prefix or ''
194 t = self.authorization.token or ''
195 args.headers[h] = f'{p} {t}'.strip()
197 return args
199 def get_capabilities(self):
200 """Download the capabilities document of the service.
202 The request goes to the service URL, unless the configuration has a ``GetCapabilities``
203 operation with its own attributes. The document is cached for ``capsCacheMaxAge``.
205 Returns:
206 The capabilities XML text.
208 Raises:
209 ``gws.ExternalServiceError``: If the request fails.
210 """
212 url, params = gws.lib.net.extract_params(self.url)
213 op = operation.create(
214 gws.OwsVerb.GetCapabilities,
215 gws.OwsOperation(mimeTypes=[gws.lib.mime.XML], params=params, url=url),
216 self.cfg('operations'),
217 )
218 args = self.prepare_operation(op)
219 return gws.u.get_cached_object(
220 f'ows_caps_{gws.u.sha256([args.url, args.params])}',
221 self.cfg('capsCacheMaxAge'),
222 lambda: request.get_text(args),
223 )
226##