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

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

2 

3from typing import Optional 

4import base64 

5 

6import gws 

7import gws.lib.crs 

8import gws.gis.source 

9import gws.lib.net 

10import gws.lib.mime 

11 

12from . import request 

13 

14_IMAGE_VERBS = { 

15 gws.OwsVerb.GetLegendGraphic, 

16 gws.OwsVerb.GetMap, 

17 gws.OwsVerb.GetTile, 

18} 

19 

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] 

22 

23 

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

36 

37 

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

46 

47 

48class Config(gws.Config): 

49 """OWS provider configuration.""" 

50 

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

65 

66 

67class Object(gws.OwsServiceProvider): 

68 """Base OWS service provider. 

69 

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

76 

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

85 

86 p = self.cfg('authorization') 

87 self.authorization = gws.OwsAuthorization(p) if p else None 

88 

89 def configure_operations(self, operations_from_caps): 

90 """Set up the provider operations. 

91 

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. 

96 

97 Args: 

98 operations_from_caps: Operations read from the capabilities. 

99 """ 

100 

101 d = {} 

102 

103 for op in operations_from_caps: 

104 d[op.verb] = op 

105 

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 ) 

117 

118 self.operations = list(d.values()) 

119 

120 for op in self.operations: 

121 op.preferredFormat = self._preferred_format(op) 

122 

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 

126 

127 if not op.formats: 

128 return prefer_fmts[0] 

129 

130 # select the "best" (leftmost in the preferred list) format 

131 

132 best_pos = 999 

133 best_fmt = '' 

134 

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 

143 

144 return best_fmt or op.formats[0] 

145 

146 def cache_hash(self): 

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

148 

149 Returns: 

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

151 """ 

152 

153 return gws.u.sha256([ 

154 self.url, 

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

156 self.alwaysXY, 

157 ]) 

158 

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 

165 

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

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

168 

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. 

172 

173 Args: 

174 op: Operation. 

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

176 params: Request parameters. 

177 

178 Returns: 

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

180 

181 Raises: 

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

183 """ 

184 

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 ) 

193 

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

195 args.params.update(op.params) 

196 

197 args.url = op.url 

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

199 args.url = op.postUrl 

200 

201 allowed = op.allowedParameters or {} 

202 

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 

209 

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

214 

215 return args 

216 

217 def get_capabilities(self): 

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

219 

220 The document is cached for ``capsCacheMaxAge``. 

221 

222 Returns: 

223 The capabilities XML text. 

224 

225 Raises: 

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

227 """ 

228 

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 )