Coverage for lode / reader / reader.py: 48%

120 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-10-05 16:02 +0000

1# reader.py - ORCHESTRATOR GENERICO 

2from lode.reader.loader import Loader 

3from lode.reader.config_manager import get_configuration 

4from lode.models import * 

5 

6class Reader: 

7 """ 

8 Generic RDF Reader/Orchestrator. 

9  

10 Responsibilities: 

11 - Calls the loader to parse input RDF (via rdflib) 

12 - Orchestrates Python model population phases 

13 - Delegates specific logics for extraction and population to other modules 

14 """ 

15 

16 def __init__(self): 

17 self._instance_cache = {} 

18 self._logic = None # Logic specializzata (OWL, SKOS, RDF, RDFS) 

19 self._graph = None 

20 self._configuration = None 

21 self._imported_uris = set() # needed to get the model before imported/closure models 

22 self._ontology_iri = None 

23 

24 def get_warnings(self) -> list: 

25 if not getattr(self, '_warnings_enabled', False): 

26 return [] 

27 if self._logic and hasattr(self._logic, '_warnings'): 

28 return self._logic._warnings 

29 return [] 

30 

31 def load_instances(self, graph_path: str, read_as: str, imported=None, closure=None, partial_import=None, warnings=False): 

32 """Carica e processa grafo RDF""" 

33 self._warnings_enabled = warnings 

34 

35 # 1. Parse generico 

36 loader = Loader(graph_path, imported=imported, closure=closure, partial_import=partial_import) 

37 self._graph = loader.get_graph() 

38 self._ontology_iri = loader.ontology_iri 

39 

40 # 1.5 saves the imported/closure uris to be reused in viewer so the model metadata are not overwritten  

41 self._imported_uris = getattr(loader, "imported_uris", set()) 

42 

43 # 2. Seleziona strategia 

44 self._configuration = get_configuration(read_as) 

45 

46 # 3. Crea Logic specializzata 

47 self._logic = self._configuration.create_logic(self._graph, self._instance_cache) 

48 

49 # 4. Esecuzione fasi 

50 self._extract_instances() 

51 

52 

53 def get_instance(self, uri: str, instance_type=None): 

54 """Ottiene istanze per URI""" 

55 uri_identifier = None 

56 

57 for identifier in self._instance_cache.keys(): 

58 if str(identifier) == uri: 

59 uri_identifier = identifier 

60 break 

61 

62 if uri_identifier is None: 

63 return None 

64 

65 instances = self._instance_cache[uri_identifier] 

66 

67 if instance_type is None: 

68 return instances 

69 

70 for instance in instances: 

71 if isinstance(instance, instance_type): 

72 return instance 

73 

74 return None 

75 

76 def get_instances(self) -> dict: 

77 """Raggruppa istanze per tipo""" 

78 grouped = {} 

79 

80 for uri_identifier, instances in self._instance_cache.items(): 

81 if isinstance(uri_identifier, str) and uri_identifier.startswith("LITERAL::"): 

82 continue 

83 

84 instances_list = instances if isinstance(instances, set) else [instances] 

85 

86 for instance in instances_list: 

87 class_name = instance.__class__.__name__ 

88 if class_name not in grouped: 

89 grouped[class_name] = [] 

90 grouped[class_name].append(instance) 

91 

92 return grouped 

93 

94 def get_triples_for_instance(self, instance): 

95 """Ottiene le triple RDF associate a un'istanza Python""" 

96 if self._logic and hasattr(self._logic, '_triples_map'): 

97 return self._logic._triples_map.get(instance, set()) 

98 return set() 

99 

100 def get_all_triples_map(self) -> dict: 

101 """Ottiene la mappa completa instance → triple""" 

102 if self._logic and hasattr(self._logic, '_triples_map'): 

103 return self._logic._triples_map 

104 return {} 

105 

106 def get_provenance_subgraph(self, instance): 

107 """Return the rdflib.Graph subgraph that provenanced `instance`.""" 

108 if self._logic and hasattr(self._logic, 'build_provenance_subgraph'): 

109 return self._logic.build_provenance_subgraph(instance) 

110 from rdflib import Graph 

111 return Graph() 

112 

113 

114 # function reused by the api to push instances 

115 def to_dict(self, instance) -> dict: 

116 """Serializza un'istanza Python in dict JSON, includendo le triple RDF""" 

117 result = { 

118 "instance": str(instance), 

119 "type": type(instance).__name__, 

120 "uri": str(instance.has_identifier) if instance.has_identifier else None, 

121 "properties": {}, 

122 "rdf_triples": [] 

123 } 

124 

125 # Serializza le proprietà dell'istanza 

126 for attr_name in instance.__dict__.keys(): 

127 value = getattr(instance, attr_name) 

128 # removes empty values from the properties 

129 if value is not None and not (isinstance(value, (list, set)) and not value): 

130 result["properties"][attr_name] = str(value) 

131 

132 # Aggiungi le triple RDF associate 

133 triples = self.get_triples_for_instance(instance) 

134 for s, p, o in triples: 

135 triple_dict = { 

136 "subject": str(s), 

137 "predicate": str(p), 

138 "object": str(o) 

139 } 

140 result["rdf_triples"].append(triple_dict) 

141 

142 return result 

143 

144 def _serialize_value(self, value): 

145 """Helper per serializzare diversi tipi di valori""" 

146 if isinstance(value, (str, int, float, bool)): 

147 return value 

148 elif isinstance(value, (list, set)): 

149 return [self._serialize_value(v) for v in value] 

150 elif hasattr(value, 'uri'): 

151 return str(value.uri) 

152 else: 

153 return str(value) 

154 

155 def get_viewer(self): 

156 """Ottiene il viewer appropriato per il formato corrente.""" 

157 if not self._configuration: 

158 raise ValueError("No configuration loaded. Call load_instances() first.") 

159 

160 return self._configuration.create_viewer(self) 

161 

162 def clear_cache(self): 

163 """Pulisce la cache""" 

164 self._instance_cache.clear() 

165 if self._logic: 

166 self._logic.clear_cache() 

167 

168 # ==================== ESTRAZIONE (ORCHESTRAZIONE) ==================== 

169 

170 def _extract_instances(self): 

171 """Estrazione in 6 fasi orchestrate""" 

172 # print("\n" + "="*60) 

173 # print("ESTRAZIONE INSTANCES") 

174 # print("="*60) 

175 

176 # FASE 0: Pre-crea datatypes (comune a tutti) 

177 # self._phase0_create_datatypes() 

178 

179 # FASE 1-4: Delegate alla Logic specifica 

180 self._logic.phase1_classify_from_predicates() 

181 self._logic.phase2_create_from_types() 

182 self._logic.phase3_populate_properties() 

183 self._logic.phase4_process_group_axioms() 

184 

185 # FASE 5: Fallback (comune) 

186 self._logic.phase5_fallback() 

187 

188 # FASE 6: Statements 

189 self._logic.phase6_create_statements() 

190 

191 # FASE 7: Namespaces  

192 self._logic.populate_namespaces() 

193 

194 # Post-pipeline warning checks 

195 if self._warnings_enabled: 

196 from lode.reader.warnings import owl_warnings 

197 owl_warnings.run_post_checks(self._logic) 

198 

199 if self._logic._warnings: 

200 print(f"WARNINGS ({len(self._logic._warnings)}):") 

201 for w in self._logic._warnings: 

202 print(f" [{w['code']}] {w['message']}") 

203 

204 self._logic.save_warnings() 

205 

206 # def _phase0_create_datatypes(self): 

207 # """Pre-crea tutti i Datatype (comune a tutti i formati)""" 

208 # print("\n--- FASE 0: Datatypes ---") 

209 

210 # created = 0 

211 

212 # # 1. URI XSD 

213 # for s, p, o in self._graph: 

214 # if isinstance(s, URIRef) and str(s).startswith(str(XSD)): 

215 # if s not in self._instance_cache: 

216 # self._logic.create_empty_instance(s, Datatype) 

217 # created += 1 

218 

219 # if isinstance(o, URIRef) and str(o).startswith(str(XSD)): 

220 # if o not in self._instance_cache: 

221 # self._logic.create_empty_instance(o, Datatype) 

222 # created += 1 

223 

224 # # 2. rdfs:Literal 

225 # if RDFS.Literal not in self._instance_cache: 

226 # self._logic.create_empty_instance(RDFS.Literal, Datatype) 

227 # created += 1 

228 

229 # # 3. BNode Datatypes 

230 # for bnode in self._graph.subjects(RDF.type, RDFS.Datatype): 

231 # if isinstance(bnode, BNode) and bnode not in self._instance_cache: 

232 # self._logic.create_empty_instance(bnode, Datatype) 

233 # created += 1 

234 

235 # print(f" Creati {created} datatypes") 

236 

237 def _phase5_fallback(self): 

238 """Fallback per risorse non categorizzate (comune)""" 

239 #print("\n--- FASE 5: Fallback ---") 

240 

241 fallback_class = self._configuration.get_fallback_class() 

242 if not fallback_class: 

243 print(" Nessun fallback configurato") 

244 return 

245 

246 all_subjects = set(self._graph.subjects()) 

247 fallback_count = 0 

248 

249 for subj in all_subjects: 

250 if subj not in self._instance_cache: 

251 self._logic.get_or_create(subj, fallback_class) 

252 fallback_count += 1 

253 

254 #print(f" Fallback: {fallback_count} risorse -> {fallback_class.__name__}") 

255 

256 # def get_ontology_metadata(self) -> Model: 

257 # """ 

258 # Extracts metadata and returns a populated Model object. 

259 # """ 

260 # if self._graph is None: 

261 # return Model() 

262 

263 # # 1. Create the Model instance 

264 # ontology_model = Model() 

265 

266 # # 2. Find the owl:Ontology node 

267 # ontology_node = self._graph.value(predicate=RDF.type, object=OWL.Ontology) 

268 

269 # if ontology_node: 

270 # # --- IDENTIFIER (IRI) --- 

271 # ontology_model.set_has_identifier(str(ontology_node)) 

272 

273 # # --- TITLE (Label) --- 

274 # title = ( 

275 # self._graph.value(ontology_node, DCTERMS.title) or 

276 # self._graph.value(ontology_node, DC.title) or 

277 # self._graph.value(ontology_node, RDFS.label) 

278 # ) 

279 # if title: 

280 # ontology_model.set_has_label(str(title)) 

281 

282 # # --- DESCRIPTION (Comment) --- 

283 # comment = ( 

284 # self._graph.value(ontology_node, DCTERMS.description) or 

285 # self._graph.value(ontology_node, RDFS.comment) 

286 # ) 

287 # if comment: 

288 # ontology_model.set_has_comment(str(comment)) 

289 

290 # # --- VERSION INFO --- 

291 # version_info = self._graph.value(ontology_node, OWL.versionInfo) 

292 # if version_info: 

293 # ontology_model.set_has_version_info(str(version_info)) 

294 

295 # # --- VERSION IRI --- 

296 # version_iri = self._graph.value(ontology_node, OWL.versionIRI) 

297 # if version_iri: 

298 # v_model = Model() 

299 # v_model.set_has_identifier(str(version_iri)) 

300 # ontology_model.set_has_version(v_model) 

301 

302 # # --- IMPORTS --- 

303 # for imported_iri in self._graph.objects(ontology_node, OWL.imports): 

304 # imported_model = Model() 

305 # imported_model.set_has_identifier(str(imported_iri)) 

306 # ontology_model.set_imports(imported_model) 

307 

308 # # --- CREATORS (Custom handling) --- 

309 # for pred in [DCTERMS.creator, DC.creator]: 

310 # for creator in self._graph.objects(ontology_node, pred): 

311 # ontology_model.set_has_creator(creator) 

312 

313 

314 # # --- CONTRIBUTORS (Custom handling) --- 

315 # for pred in [DCTERMS.contributor, DC.contributor]: 

316 # for contributor in self._graph.objects(ontology_node, pred): 

317 # ontology_model.set_has_contributor(contributor) 

318 

319 # return ontology_model