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

1"""Base OWS service provider.""" 

2 

3from typing import Optional 

4 

5import gws 

6import gws.lib.crs 

7import gws.gis.source 

8import gws.lib.net 

9import gws.lib.mime 

10import gws.lib.text 

11 

12from . import request, operation 

13 

14_MIME_TYPE_PARAMS = {'FORMAT', 'INFO_FORMAT', 'OUTPUTFORMAT'} 

15 

16 

17class AuthorizationConfig(gws.Config): 

18 """Credentials for accessing the service.""" 

19 

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.""" 

32 

33 

34class Config(gws.Config): 

35 """OWS provider configuration.""" 

36 

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.""" 

51 

52 

53class Object(gws.OwsServiceProvider): 

54 """Base OWS service provider. 

55 

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 """ 

62 

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 = '' 

71 

72 self.configure_authorization() 

73 

74 def configure_authorization(self): 

75 self.authorization = None 

76 

77 p = self.cfg('authorization') 

78 if not p: 

79 return 

80 

81 a = gws.OwsAuthorization(p) 

82 

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}') 

91 

92 self.authorization = a 

93 

94 def configure_operations(self, operations_from_caps): 

95 """Set up the provider operations. 

96 

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``). 

100 

101 Args: 

102 operations_from_caps: Operations read from the capabilities. 

103 """ 

104 

105 ops = {} 

106 cfgs = self.cfg('operations') 

107 

108 for op in operations_from_caps: 

109 ops[op.verb] = operation.create(op.verb, op, cfgs) 

110 

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) 

115 

116 self.operations = list(ops.values()) 

117 

118 def cache_hash(self): 

119 """Compute a hash of the provider settings, used to build cache keys. 

120 

121 Returns: 

122 A hash of the URL, the forced CRS and the axis order setting. 

123 """ 

124 

125 return gws.u.sha256( 

126 [ 

127 self.url, 

128 self.forceCrs.srid if self.forceCrs else '', 

129 self.alwaysXY, 

130 ] 

131 ) 

132 

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 

139 

140 def prepare_operation(self, op: gws.OwsOperation, method: gws.RequestMethod = None, params=None) -> request.Args: 

141 """Prepare the request arguments for an operation. 

142 

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. 

147 

148 Args: 

149 op: Operation. 

150 method: Request method, ``GET`` by default. 

151 params: Request parameters. 

152 

153 Returns: 

154 Request arguments, to be passed to ``request.get``. 

155 

156 Raises: 

157 ``gws.Error``: If a parameter value is not allowed by the operation. 

158 """ 

159 

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 ) 

168 

169 if args.method == gws.RequestMethod.GET: 

170 args.params.update(op.params) 

171 

172 args.url = op.url 

173 if args.method == gws.RequestMethod.POST: 

174 args.url = op.postUrl 

175 

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 

184 

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 

190 

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

196 

197 return args 

198 

199 def get_capabilities(self): 

200 """Download the capabilities document of the service. 

201 

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``. 

204 

205 Returns: 

206 The capabilities XML text. 

207 

208 Raises: 

209 ``gws.ExternalServiceError``: If the request fails. 

210 """ 

211 

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 ) 

224 

225 

226##