4747import logging
4848import os
4949
50+ from typing import Dict , Type
5051from pygeometa .schemas .base import BaseOutputSchema
5152
5253LOGGER = logging .getLogger (__name__ )
5354THISDIR = os .path .dirname (os .path .realpath (__file__ ))
5455
55- SCHEMAS = {
56- 'dcat' : 'pygeometa.schemas.dcat.DCATOutputSchema' ,
57- 'iso19139' : 'pygeometa.schemas.iso19139.ISO19139OutputSchema' ,
58- 'iso19139-2' : 'pygeometa.schemas.iso19139_2.ISO19139_2OutputSchema' ,
59- 'iso19139-hnap' : 'pygeometa.schemas.iso19139_hnap.ISO19139HNAPOutputSchema' , # noqa
60- 'oarec-record' : 'pygeometa.schemas.ogcapi_records.OGCAPIRecordOutputSchema' , # noqa
61- 'schema-org' : 'pygeometa.schemas.schema_org.SchemaOrgOutputSchema' ,
62- 'stac-item' : 'pygeometa.schemas.stac.STACItemOutputSchema' ,
63- 'wmo-cmp' : 'pygeometa.schemas.wmo_cmp.WMOCMPOutputSchema' ,
64- 'wmo-wcmp2' : 'pygeometa.schemas.wmo_wcmp2.WMOWCMP2OutputSchema' ,
65- 'wmo-wigos' : 'pygeometa.schemas.wmo_wigos.WMOWIGOSOutputSchema' ,
66- 'cwl' : 'pygeometa.schemas.cwl.CWLOutputSchema'
67- }
56+ # runtime mapping: schema_key -> class object
57+ _DISCOVERED_SCHEMAS : Dict [str , Type [BaseOutputSchema ]] = {}
58+ _DISCOVERY_DONE = False
59+
60+
61+ def _discover_schemas ():
62+ """Discover local schema packages (folders with __init__.py) in ./schemas.
63+
64+ For each discovered package module, import it and choose the first class
65+ that is a subclass of BaseOutputSchema (excluding BaseOutputSchema itself).
66+ The mapping key will be:
67+ - module attribute SCHEMA_NAME if defined, else
68+ - the package (folder) name.
69+
70+ This function is idempotent and caches results in _DISCOVERED_SCHEMAS.
71+ """
72+ global _DISCOVERY_DONE , _DISCOVERED_SCHEMAS # noqa
73+ if _DISCOVERY_DONE :
74+ return
75+
76+ LOGGER .debug ("Discovering schema packages in %s" , THISDIR )
77+ pkg_name = __name__ # 'pygeometa.schemas'
78+
79+ try :
80+ entries = sorted (os .listdir (THISDIR ))
81+ except OSError :
82+ entries = []
83+
84+ for entry in entries :
85+ path = os .path .join (THISDIR , entry )
86+ # look for directories with __init__.py only
87+ if not os .path .isdir (path ):
88+ continue
89+ init_py = os .path .join (path , "__init__.py" )
90+ if not os .path .isfile (init_py ):
91+ continue
92+
93+ module_name = f"{ pkg_name } .{ entry } "
94+ try :
95+ # import normally so package semantics and relative imports work
96+ module = importlib .import_module (module_name )
97+ except Exception as exc :
98+ LOGGER .exception (f"Failed to import schema package { module_name } : { exc } " ) # noqa
99+ continue
100+
101+ # Key selection: prefer explicit module-provided `name`,
102+ # else use dotted name
103+ schema_key = getattr (module , "name" , None ) or module .__name__
104+
105+ # Collect candidate classes in module that are subclasses
106+ # of BaseOutputSchema
107+ candidates = []
108+ for attr_name in dir (module ):
109+ try :
110+ attr = getattr (module , attr_name )
111+ except Exception :
112+ # some modules raise on attribute access; skip those
113+ continue
114+ if not isinstance (attr , type ):
115+ continue
116+ # Exclude BaseOutputSchema itself
117+ if attr is BaseOutputSchema :
118+ continue
119+ try :
120+ if issubclass (attr , BaseOutputSchema ):
121+ candidates .append (attr )
122+ except TypeError :
123+ # issubclass can raise if attr is not a class; ignore
124+ continue
125+
126+ if not candidates :
127+ LOGGER .warning (f"No BaseOutputSchema subclass found in { module_name } ; skipping" ) # noqa
128+ continue
129+
130+ # pick the most concrete subclass (leaf in the inheritance chain)
131+ # the class with the longest method-resolution order (MRO)
132+ chosen_cls = sorted (candidates , key = lambda c : len (getattr (c , "__mro__" , ())), reverse = True )[0 ] # noqa
133+
134+ # Ensure uniqueness of keys
135+ if schema_key in _DISCOVERED_SCHEMAS :
136+ existing = _DISCOVERED_SCHEMAS [schema_key ]
137+ if existing is chosen_cls :
138+ LOGGER .debug ("Schema key {schema_key} already registered with same class; skipping" ) # noqa
139+ continue
140+ LOGGER .warning (f"""Duplicate schema key '{ schema_key } ' (module { module_name } , class { chosen_cls .__name__ } ). # noqa
141+ Keeping first discovered: { existing .__module__ } .{ existing .__name__ } """ ) # noqa
142+ continue
143+
144+ _DISCOVERED_SCHEMAS [schema_key ] = chosen_cls
145+ LOGGER .info (f"Discovered schema '{ schema_key } ' -> { chosen_cls .__module__ } .{ chosen_cls .__name__ } " ) # noqa
146+
147+ _DISCOVERY_DONE = True
68148
69149
70150def get_supported_schemas (details : bool = False ,
71151 include_autodetect : bool = False ) -> list :
72152 """
73- Get supported schemas
153+ Get supported schemas.
74154
75155 :param details: provide read/write details
76156 :param include_autodetect: include magic auto detection mode
77157
78- :returns: list of supported schemas
158+ :returns: list of supported schemas (strings) or details matrix
79159 """
160+ _discover_schemas ()
80161
81162 def has_mode (plugin : BaseOutputSchema , mode : str ) -> bool :
82163 enabled = False
@@ -90,36 +171,47 @@ def has_mode(plugin: BaseOutputSchema, mode: str) -> bool:
90171
91172 return enabled
92173
93- schema_matrix = []
94-
95174 LOGGER .debug ('Generating list of supported schemas' )
96175
97176 if not details :
177+ schema_names = []
178+ for cls in _DISCOVERED_SCHEMAS .values ():
179+ try :
180+ schema_names .append (cls ().name )
181+ except Exception :
182+ continue
98183 if include_autodetect :
99- schemas_keys = list (SCHEMAS .keys ())
100- schemas_keys .append ('autodetect' )
101- return schemas_keys
102- else :
103- return SCHEMAS .keys ()
184+ schema_names .append ("autodetect" )
185+ return schema_names
104186
105- for key in SCHEMAS .keys ():
106- schema = load_schema (key )
107- can_read = has_mode (schema , 'import_' )
108- can_write = has_mode (schema , 'write' )
187+ schema_matrix = []
188+ for key , cls in _DISCOVERED_SCHEMAS .items ():
189+ nm = key
190+ try :
191+ schema_inst = cls ()
192+ nm = schema_inst .name
193+ can_read = has_mode (schema_inst , "import_" )
194+ can_write = has_mode (schema_inst , "write" )
195+ description = getattr (schema_inst , "description" , "" )
196+ except Exception :
197+ LOGGER .exception (f"Error instantiating schema class for key { key } " ) # noqa
198+ can_read = False
199+ can_write = False
200+ description = ""
109201
110202 schema_matrix .append ({
111- 'id' : key ,
112- ' description' : schema . description ,
113- ' read' : can_read ,
114- ' write' : can_write
203+ "id" : nm ,
204+ " description" : description ,
205+ " read" : can_read ,
206+ " write" : can_write
115207 })
116208
117209 if include_autodetect :
118210 schema_matrix .append ({
119- 'id' : ' autodetect' ,
120- ' description' : ' Auto schema detection' ,
121- ' read' : True ,
122- ' write' : False
211+ "id" : " autodetect" ,
212+ " description" : " Auto schema detection" ,
213+ " read" : True ,
214+ " write" : False
123215 })
124216
125217 return schema_matrix
@@ -133,28 +225,30 @@ def load_schema(schema_name: str) -> BaseOutputSchema:
133225
134226 :returns: plugin object
135227 """
228+ _discover_schemas ()
229+ LOGGER .debug ("Available schemas: %s" , list (_DISCOVERED_SCHEMAS .keys ()))
136230
137- LOGGER .debug (f'Schemas: { SCHEMAS .keys ()} ' )
231+ # allow 'autodetect' to be handled elsewhere (kept for parity)
232+ if schema_name == "autodetect" :
233+ raise InvalidSchemaError ("Autodetect is not a concrete schema" )
138234
139- if schema_name not in SCHEMAS .keys ():
140- msg = f'Schema { schema_name } not found'
235+ cls = None
236+ for v in _DISCOVERED_SCHEMAS .values ():
237+ if v ().name == schema_name :
238+ cls = v
239+ break
240+
241+ if not cls :
242+ msg = f"Schema { schema_name } not found"
141243 LOGGER .exception (msg )
142244 raise InvalidSchemaError (msg )
143245
144- name = SCHEMAS [schema_name ]
145-
146- if '.' in name : # dotted path
147- packagename , classname = name .rsplit ('.' , 1 )
148- else :
149- raise InvalidSchemaError (f'Schema path { name } not found' )
150-
151- LOGGER .debug (f'package name: { packagename } ' )
152- LOGGER .debug (f'class name: { classname } ' )
153-
154- module = importlib .import_module (packagename )
155- class_ = getattr (module , classname )
156-
157- return class_ ()
246+ try :
247+ return cls ()
248+ except Exception as exc :
249+ msg = f"Failed to instantiate schema { schema_name } : { exc } "
250+ LOGGER .exception (msg )
251+ raise InvalidSchemaError (msg )
158252
159253
160254class InvalidSchemaError (Exception ):
0 commit comments