Coverage for gws-app/gws/base/metadata/core.py: 96%

227 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 22:59 +0200

1from typing import Optional 

2import gws 

3import gws.lib.intl 

4import gws.lib.datetimex as dtx 

5 

6from . import inspire, iso 

7 

8 

9class LinkConfig(gws.Config): 

10 """Metadata link.""" 

11 

12 about: Optional[str] 

13 description: Optional[str] 

14 format: Optional[str] 

15 formatVersion: Optional[str] 

16 function: Optional[str] 

17 mimeType: Optional[gws.MimeType] 

18 scheme: Optional[str] 

19 title: Optional[str] 

20 type: Optional[str] 

21 url: Optional[str] 

22 

23 

24class Config(gws.Config): 

25 """Metadata configuration. (added in 8.2)""" 

26 

27 name: Optional[str] 

28 """Object name.""" 

29 title: Optional[str] 

30 """Object title.""" 

31 

32 abstract: Optional[str] 

33 """Object abstract, a brief description of the object.""" 

34 accessConstraints: Optional[str] 

35 """Access constraint for the object.""" 

36 accessConstraintsType: Optional[str] 

37 """Access constraint type for the object.""" 

38 attribution: Optional[str] 

39 """Attribution information for the object. (changed in 8.2)""" 

40 attributionUrl: Optional[str] 

41 """Attribution URL for the object. (added in 8.2)""" 

42 dateCreated: Optional[gws.DateStr] 

43 """Object creation date.""" 

44 dateUpdated: Optional[gws.DateStr] 

45 """Object last update date.""" 

46 fees: Optional[str] 

47 """Fees associated with accessing or using the object.""" 

48 image: Optional[str] 

49 """Image URL or path associated with the object.""" 

50 keywords: Optional[list[str]] 

51 """Keywords, optionally prefixed with a vocabulary, e.g. 'gemet:river'.""" 

52 license: Optional[str] 

53 """License information for the object.""" 

54 licenseUrl: Optional[gws.Url] 

55 """License URL.""" 

56 

57 contactAddress: Optional[str] 

58 """Contact address for the object.""" 

59 contactAddressType: Optional[str] 

60 """Type of contact address, such as 'postal' or 'email'.""" 

61 contactArea: Optional[str] 

62 """Contact area or state.""" 

63 contactCity: Optional[str] 

64 """Contact city.""" 

65 contactCountry: Optional[str] 

66 """Contact country.""" 

67 contactEmail: Optional[str] 

68 """Contact email address.""" 

69 contactFax: Optional[str] 

70 """Contact fax number.""" 

71 contactOrganization: Optional[str] 

72 """Contact organization or institution.""" 

73 contactPerson: Optional[str] 

74 """Contact person name.""" 

75 contactPhone: Optional[str] 

76 """Contact phone number.""" 

77 contactPosition: Optional[str] 

78 """Contact position or job title.""" 

79 contactProviderName: Optional[str] 

80 """Name of the provider of the contact information.""" 

81 contactProviderSite: Optional[str] 

82 """Website of the provider of the contact information.""" 

83 contactRole: Optional[iso.CI_RoleCode] 

84 """Role of the contact person, such as 'pointOfContact' or 'author'.""" 

85 contactUrl: Optional[str] 

86 """URL for additional contact information.""" 

87 contactZip: Optional[str] 

88 """Contact postal code.""" 

89 

90 authorityIdentifier: Optional[str] 

91 """Identifier (WMS)""" 

92 authorityName: Optional[str] 

93 """AuthorityURL name (WMS)""" 

94 authorityUrl: Optional[str] 

95 """AuthorityURL (WMS)""" 

96 

97 metaLinks: Optional[list[LinkConfig]] 

98 """MetadataURL (WMS, WFS) or metadata links (CSW).""" 

99 serviceMetadataURL: Optional[str] 

100 """Service metadata URL (WMTS).""" 

101 

102 catalogCitationUid: Optional[str] 

103 """CI_Citation.Identifier (CSW).""" 

104 catalogUid: Optional[str] 

105 """MD_Metadata.Identifier (CSW).""" 

106 

107 language: Optional[str] 

108 """Language code (ISO 639-1).""" 

109 

110 parentIdentifier: Optional[str] 

111 """MD_Metadata.parentIdentifier (ISO).""" 

112 wgsExtent: Optional[gws.Extent] 

113 """EX_Extent (ISO).""" 

114 crs: Optional[gws.CrsName] 

115 """MD_ReferenceSystem (ISO).""" 

116 temporalBegin: Optional[gws.DateStr] 

117 """EX_TemporalExtent (ISO).""" 

118 temporalEnd: Optional[gws.DateStr] 

119 """EX_TemporalExtent (ISO).""" 

120 

121 inspireMandatoryKeyword: Optional[inspire.IM_MandatoryKeyword] 

122 inspireDegreeOfConformity: Optional[inspire.IM_DegreeOfConformity] 

123 inspireResourceType: Optional[inspire.IM_ResourceType] 

124 inspireSpatialDataServiceType: Optional[inspire.IM_SpatialDataServiceType] 

125 inspireSpatialScope: Optional[inspire.IM_SpatialScope] 

126 inspireSpatialScopeName: Optional[str] 

127 inspireTheme: Optional[inspire.IM_Theme] 

128 

129 isoMaintenanceFrequencyCode: Optional[iso.MD_MaintenanceFrequencyCode] 

130 isoQualityConformanceExplanation: Optional[str] 

131 isoQualityConformanceQualityPass: Optional[bool] 

132 isoQualityConformanceSpecificationDate: Optional[str] 

133 isoQualityConformanceSpecificationTitle: Optional[str] 

134 isoQualityLineageSource: Optional[str] 

135 isoQualityLineageSourceScale: Optional[int] 

136 isoQualityLineageStatement: Optional[str] 

137 isoRestrictionCode: Optional[iso.MD_RestrictionCode] 

138 isoServiceFunction: Optional[iso.SV_ServiceFunction] 

139 isoScope: Optional[iso.MD_ScopeCode] 

140 isoScopeName: Optional[str] 

141 isoSpatialRepresentationType: Optional[iso.MD_SpatialRepresentationTypeCode] 

142 isoTopicCategories: Optional[list[iso.MD_TopicCategoryCode]] 

143 isoSpatialResolution: Optional[int] 

144 

145 

146## 

147 

148_KEYWORD_CODE_SPACES = { 

149 'iso': ['ISOTC211/19115', 'http://www.isotc211.org/2005/resources/Codelist/gmxCodelists.xml#MD_KeywordTypeCode'], 

150 'gemet': ['GEMET', 'http://www.eionet.europa.eu/gemet/2004/06/gemet-version.rdf'], 

151 'inspire_themes': ['GEMET - INSPIRE themes', 'http://inspire.ec.europa.eu/theme'], 

152 'gcmd': ['gcmd', 'http://gcmd.nasa.gov/Resources/valids/locations.html'], 

153} 

154 

155 

156class KeywordGroup(gws.Data): 

157 codeSpace: str 

158 """Code space for the keyword group, e.g. 'iso', 'gemet', 'inspire', 'gcmd'.""" 

159 typeName: str 

160 """Type name for the keyword group, e.g. 'isoTopicCategories', 'keywords'.""" 

161 keywords: list[str] 

162 """List of keywords in the group.""" 

163 

164 

165def keyword_groups(md: gws.Metadata) -> list[KeywordGroup]: 

166 d = {} 

167 

168 def add(kw): 

169 p = kw.split(':') 

170 if len(p) == 1: 

171 return add2('', '', kw) 

172 

173 def add2(code_space, type_name, kw): 

174 if code_space.lower() in _KEYWORD_CODE_SPACES: 

175 code_space = _KEYWORD_CODE_SPACES[code_space.lower()][0] 

176 key = (code_space, type_name) 

177 if key not in d: 

178 d[key] = KeywordGroup(codeSpace=code_space, typeName=type_name, keywords=[]) 

179 d[key].keywords.append(kw) 

180 

181 if md.keywords: 

182 for kw in md.keywords: 

183 add(kw) 

184 if md.inspireTheme: 

185 add2('inspire_themes', 'theme', md.inspireTheme) 

186 if md.isoTopicCategories: 

187 for cat in md.isoTopicCategories: 

188 add2('iso', 'isoTopicCategory', cat) 

189 if md.inspireMandatoryKeyword: 

190 add2('iso', 'serviceType', md.inspireMandatoryKeyword) 

191 

192 return list(d.values()) 

193 

194 

195class Props(gws.Props): 

196 """Represents metadata properties.""" 

197 

198 abstract: str 

199 attribution: str 

200 dateCreated: str 

201 dateUpdated: str 

202 keywords: list[str] 

203 language: str 

204 title: str 

205 

206 

207def new() -> gws.Metadata: 

208 """Create a new Metadata object with default values.""" 

209 

210 return _new() 

211 

212 

213def from_dict(d: dict) -> gws.Metadata: 

214 """Create a Metadata object from a dictionary. 

215 

216 Args: 

217 d: Dictionary containing metadata information. 

218 """ 

219 

220 return _update(_new(), d) 

221 

222 

223def from_args(*args, **kwargs) -> gws.Metadata: 

224 """Create a Metadata object from arguments (dicts or other Metadata objects).""" 

225 

226 return _update(_new(), *args, **kwargs) 

227 

228 

229def from_config(c: gws.Config) -> gws.Metadata: 

230 """Create a Metadata object from a configuration. 

231 

232 Args: 

233 c: Configuration object. 

234 """ 

235 

236 return _update(_new(), c) 

237 

238 

239def from_props(p: gws.Props) -> gws.Metadata: 

240 """Create a Metadata object from properties. 

241 

242 Args: 

243 p: Properties object. 

244 """ 

245 

246 return _update(_new(), p) 

247 

248 

249def update(md: gws.Metadata, *args, **kwargs) -> gws.Metadata: 

250 """Update a Metadata object from arguments (dicts or other Metadata objects).""" 

251 

252 _update(md, *args, **kwargs) 

253 return md 

254 

255 

256def normalize(md: gws.Metadata) -> gws.Metadata: 

257 """Normalize a Metadata object (e.g. fix dates and language codes).""" 

258 

259 nor = _new() 

260 _update(nor, md) 

261 return nor 

262 

263 

264def props(md: gws.Metadata) -> gws.Props: 

265 """Properties of a Metadata object.""" 

266 

267 dc = dtx.parse(md.dateCreated) 

268 du = dtx.parse(md.dateUpdated) 

269 

270 return gws.Props( 

271 abstract=md.abstract or '', 

272 attribution=md.attribution or '', 

273 dateCreated=dtx.to_iso_date_string(dc) if dc else '', 

274 dateUpdated=dtx.to_iso_date_string(du) if du else '', 

275 keywords=sorted(md.keywords or []), 

276 language=md.language or '', 

277 title=md.title or '', 

278 ) 

279 

280 

281## 

282 

283 

284def _new() -> gws.Metadata: 

285 md = gws.Metadata() 

286 for key, fn in _UPDATE_FNS.items(): 

287 fn(md, key, None) 

288 return md 

289 

290 

291def _update(md: gws.Metadata, *args, **kwargs): 

292 def add(a): 

293 for key, val in a.items(): 

294 fn = _UPDATE_FNS.get(key) 

295 if fn: 

296 fn(md, key, val) 

297 elif val is not None: 

298 setattr(md, key, val) 

299 

300 for a in args: 

301 if not a: 

302 continue 

303 if isinstance(a, gws.Data): 

304 a = gws.u.to_dict(a) 

305 add(a) 

306 

307 add(kwargs) 

308 _fix_language(md) 

309 

310 return md 

311 

312 

313def _update_set(md: gws.Metadata, key, val): 

314 s = set(getattr(md, key, None) or []) 

315 s.update(val or []) 

316 setattr(md, key, sorted(s)) 

317 

318 

319def _update_list(md: gws.Metadata, key, val): 

320 setattr(md, key, val or []) 

321 

322 

323def _update_datetime(md: gws.Metadata, key, val): 

324 if val: 

325 dt = dtx.parse(val) 

326 if dt: 

327 setattr(md, key, dt) 

328 

329 

330def _fix_language(md: gws.Metadata): 

331 lang = md.language or 'en' 

332 

333 md.language3 = gws.lib.intl.locale(lang).language3 

334 md.languageBib = gws.lib.intl.locale(lang).languageBib 

335 

336 if md.inspireTheme: 

337 md.inspireThemeNameLocal = inspire.theme_name(md.inspireTheme, md.language) or '' 

338 md.inspireThemeNameEn = inspire.theme_name(md.inspireTheme, 'en') or '' 

339 

340 

341_UPDATE_FNS = dict( 

342 keywords=_update_set, 

343 isoTopicCategories=_update_set, 

344 metaLinks=_update_list, 

345 dateCreated=_update_datetime, 

346 dateUpdated=_update_datetime, 

347 temporalBegin=_update_datetime, 

348 temporalEnd=_update_datetime, 

349)