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 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1from typing import Optional
2import gws
3import gws.lib.intl
4import gws.lib.datetimex as dtx
6from . import inspire, iso
9class LinkConfig(gws.Config):
10 """Metadata link."""
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]
24class Config(gws.Config):
25 """Metadata configuration. (added in 8.2)"""
27 name: Optional[str]
28 """Object name."""
29 title: Optional[str]
30 """Object title."""
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."""
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."""
90 authorityIdentifier: Optional[str]
91 """Identifier (WMS)"""
92 authorityName: Optional[str]
93 """AuthorityURL name (WMS)"""
94 authorityUrl: Optional[str]
95 """AuthorityURL (WMS)"""
97 metaLinks: Optional[list[LinkConfig]]
98 """MetadataURL (WMS, WFS) or metadata links (CSW)."""
99 serviceMetadataURL: Optional[str]
100 """Service metadata URL (WMTS)."""
102 catalogCitationUid: Optional[str]
103 """CI_Citation.Identifier (CSW)."""
104 catalogUid: Optional[str]
105 """MD_Metadata.Identifier (CSW)."""
107 language: Optional[str]
108 """Language code (ISO 639-1)."""
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)."""
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]
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]
146##
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}
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."""
165def keyword_groups(md: gws.Metadata) -> list[KeywordGroup]:
166 d = {}
168 def add(kw):
169 p = kw.split(':')
170 if len(p) == 1:
171 return add2('', '', kw)
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)
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)
192 return list(d.values())
195class Props(gws.Props):
196 """Represents metadata properties."""
198 abstract: str
199 attribution: str
200 dateCreated: str
201 dateUpdated: str
202 keywords: list[str]
203 language: str
204 title: str
207def new() -> gws.Metadata:
208 """Create a new Metadata object with default values."""
210 return _new()
213def from_dict(d: dict) -> gws.Metadata:
214 """Create a Metadata object from a dictionary.
216 Args:
217 d: Dictionary containing metadata information.
218 """
220 return _update(_new(), d)
223def from_args(*args, **kwargs) -> gws.Metadata:
224 """Create a Metadata object from arguments (dicts or other Metadata objects)."""
226 return _update(_new(), *args, **kwargs)
229def from_config(c: gws.Config) -> gws.Metadata:
230 """Create a Metadata object from a configuration.
232 Args:
233 c: Configuration object.
234 """
236 return _update(_new(), c)
239def from_props(p: gws.Props) -> gws.Metadata:
240 """Create a Metadata object from properties.
242 Args:
243 p: Properties object.
244 """
246 return _update(_new(), p)
249def update(md: gws.Metadata, *args, **kwargs) -> gws.Metadata:
250 """Update a Metadata object from arguments (dicts or other Metadata objects)."""
252 _update(md, *args, **kwargs)
253 return md
256def normalize(md: gws.Metadata) -> gws.Metadata:
257 """Normalize a Metadata object (e.g. fix dates and language codes)."""
259 nor = _new()
260 _update(nor, md)
261 return nor
264def props(md: gws.Metadata) -> gws.Props:
265 """Properties of a Metadata object."""
267 dc = dtx.parse(md.dateCreated)
268 du = dtx.parse(md.dateUpdated)
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 )
281##
284def _new() -> gws.Metadata:
285 md = gws.Metadata()
286 for key, fn in _UPDATE_FNS.items():
287 fn(md, key, None)
288 return md
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)
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)
307 add(kwargs)
308 _fix_language(md)
310 return md
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))
319def _update_list(md: gws.Metadata, key, val):
320 setattr(md, key, val or [])
323def _update_datetime(md: gws.Metadata, key, val):
324 if val:
325 dt = dtx.parse(val)
326 if dt:
327 setattr(md, key, dt)
330def _fix_language(md: gws.Metadata):
331 lang = md.language or 'en'
333 md.language3 = gws.lib.intl.locale(lang).language3
334 md.languageBib = gws.lib.intl.locale(lang).languageBib
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 ''
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)