4646import importlib
4747import logging
4848import os
49+ import pkgutil
50+ from typing import Dict , Type , List
4951
5052from pygeometa .schemas .base import BaseOutputSchema
5153
5254LOGGER = logging .getLogger (__name__ )
5355THISDIR = os .path .dirname (os .path .realpath (__file__ ))
5456
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- }
57+ # runtime mapping: schema_key -> class object
58+ _DISCOVERED_SCHEMAS : Dict [str , Type [BaseOutputSchema ]] = {}
59+ _DISCOVERY_DONE = False
6860
6961
62+ def _discover_schemas ():
63+ """Discover local schema packages (folders with __init__.py) in ./schemas.
64+
65+ For each discovered package module, import it and choose the first class
66+ that is a subclass of BaseOutputSchema (excluding BaseOutputSchema itself).
67+ The mapping key will be:
68+ - module attribute SCHEMA_NAME if defined, else
69+ - the package (folder) name.
70+
71+ This function is idempotent and caches results in _DISCOVERED_SCHEMAS.
72+ """
73+ global _DISCOVERY_DONE , _DISCOVERED_SCHEMAS
74+ if _DISCOVERY_DONE :
75+ return
76+
77+ LOGGER .debug ("Discovering schema packages in %s" , THISDIR )
78+ pkg_name = __name__ # 'pygeometa.schemas'
79+ # Use pkgutil to iterate subpackages defined by filesystem packages adjacent to this file.
80+ # pkgutil.walk_packages won't work directly without package loader, so we handle dirs.
81+ try :
82+ entries = sorted (os .listdir (THISDIR ))
83+ except OSError :
84+ entries = []
85+
86+ for entry in entries :
87+ path = os .path .join (THISDIR , entry )
88+ # look for directories with __init__.py only
89+ if not os .path .isdir (path ):
90+ continue
91+ init_py = os .path .join (path , "__init__.py" )
92+ if not os .path .isfile (init_py ):
93+ continue
94+
95+ module_name = f"{ pkg_name } .{ entry } "
96+ try :
97+ module = importlib .import_module (module_name )
98+ except Exception as exc : # defensive: log and skip bad packages
99+ LOGGER .exception ("Failed to import schema package %s: %s" , module_name , exc )
100+ continue
101+
102+ # allow module to explicitly provide a name
103+ schema_key = getattr (module , "name" , entry )
104+
105+ # find first class that subclasses BaseOutputSchema
106+ found_cls = None
107+ for attr_name in dir (module ):
108+ try :
109+ attr = getattr (module , attr_name )
110+ except Exception :
111+ continue
112+ if isinstance (attr , type ) and issubclass (attr , BaseOutputSchema ) and attr is not BaseOutputSchema :
113+ found_cls = attr
114+ break
115+
116+ if found_cls is None :
117+ LOGGER .warning ("No BaseOutputSchema subclass found in %s; skipping" , module_name )
118+ continue
119+
120+ if schema_key in _DISCOVERED_SCHEMAS :
121+ LOGGER .warning ("Duplicate schema key '%s' (from %s). Keeping first discovered." , schema_key , module_name )
122+ continue
123+
124+ _DISCOVERED_SCHEMAS [schema_key ] = found_cls
125+ LOGGER .info ("Discovered schema '%s' -> %s.%s" , schema_key , module_name , found_cls .__name__ )
126+
127+ _DISCOVERY_DONE = True
128+
70129def get_supported_schemas (details : bool = False ,
71130 include_autodetect : bool = False ) -> list :
72131 """
73- Get supported schemas
132+ Get supported schemas.
74133
75134 :param details: provide read/write details
76135 :param include_autodetect: include magic auto detection mode
77136
78- :returns: list of supported schemas
137+ :returns: list of supported schemas (strings) or details matrix
79138 """
139+ _discover_schemas ()
80140
81141 def has_mode (plugin : BaseOutputSchema , mode : str ) -> bool :
82142 enabled = False
@@ -90,36 +150,47 @@ def has_mode(plugin: BaseOutputSchema, mode: str) -> bool:
90150
91151 return enabled
92152
93- schema_matrix = []
94-
95153 LOGGER .debug ('Generating list of supported schemas' )
96154
97155 if not details :
156+ schema_names = []
157+ for cls in _DISCOVERED_SCHEMAS .values ():
158+ try :
159+ schema_names .append (cls ().name )
160+ except Exception :
161+ continue
98162 if include_autodetect :
99- schemas_keys = list (SCHEMAS .keys ())
100- schemas_keys .append ('autodetect' )
101- return schemas_keys
102- else :
103- return SCHEMAS .keys ()
163+ schema_names .append ("autodetect" )
164+ return schema_names
104165
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' )
166+ schema_matrix = []
167+ for key , cls in _DISCOVERED_SCHEMAS .items ():
168+ nm = key
169+ try :
170+ schema_inst = cls ()
171+ nm = schema_inst .name
172+ can_read = has_mode (schema_inst , "import_" )
173+ can_write = has_mode (schema_inst , "write" )
174+ description = getattr (schema_inst , "description" , "" )
175+ except Exception :
176+ LOGGER .exception ("Error instantiating schema class for key %s" , key )
177+ can_read = False
178+ can_write = False
179+ description = ""
109180
110181 schema_matrix .append ({
111- 'id' : key ,
112- ' description' : schema . description ,
113- ' read' : can_read ,
114- ' write' : can_write
182+ "id" : nm ,
183+ " description" : description ,
184+ " read" : can_read ,
185+ " write" : can_write
115186 })
116187
117188 if include_autodetect :
118189 schema_matrix .append ({
119- 'id' : ' autodetect' ,
120- ' description' : ' Auto schema detection' ,
121- ' read' : True ,
122- ' write' : False
190+ "id" : " autodetect" ,
191+ " description" : " Auto schema detection" ,
192+ " read" : True ,
193+ " write" : False
123194 })
124195
125196 return schema_matrix
@@ -133,28 +204,25 @@ def load_schema(schema_name: str) -> BaseOutputSchema:
133204
134205 :returns: plugin object
135206 """
207+ _discover_schemas ()
208+ LOGGER .debug ("Available schemas: %s" , list (_DISCOVERED_SCHEMAS .keys ()))
136209
137- LOGGER .debug (f'Schemas: { SCHEMAS .keys ()} ' )
210+ # allow 'autodetect' to be handled elsewhere (kept for parity)
211+ if schema_name == "autodetect" :
212+ raise InvalidSchemaError ("Autodetect is not a concrete schema" )
138213
139- if schema_name not in SCHEMAS . keys () :
140- msg = f' Schema { schema_name } not found'
214+ if schema_name not in _DISCOVERED_SCHEMAS :
215+ msg = f" Schema { schema_name } not found"
141216 LOGGER .exception (msg )
142217 raise InvalidSchemaError (msg )
143218
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_ ()
219+ cls = _DISCOVERED_SCHEMAS [schema_name ]
220+ try :
221+ return cls ()
222+ except Exception as exc :
223+ msg = f"Failed to instantiate schema { schema_name } : { exc } "
224+ LOGGER .exception (msg )
225+ raise InvalidSchemaError (msg )
158226
159227
160228class InvalidSchemaError (Exception ):
0 commit comments