Coverage for lode / reader / logic / rdf_logic.py: 12%

142 statements  

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

1# rdf_logic.py 

2 

3from rdflib import URIRef, Node, Literal as RDFlibLiteral, BNode 

4from rdflib.namespace import RDF, RDFS, OWL, SKOS, XSD 

5from rdflib.collection import Collection as RDFLibCollection 

6 

7from lode.models import * 

8from lode.reader.logic.base_logic import BaseLogic 

9 

10 

11class RdfLogic(BaseLogic): 

12 """ 

13 RDF/RDFS parsing logic. 

14 

15 Behavior: 

16 - Phase1: classify URIRef subjects of mapped predicates whose config 

17 declares inferred_class or a single target_classes (mirrors OwlLogic). 

18 BNodes with inferred_class are classified too (e.g. reified Statements 

19 via rdf:subject/predicate/object). 

20 - Phase2: create instances from rdf:type using type_mapping; static 

21 setters declared in 'is: class' entries are applied immediately. 

22 - Phase3: populate via setters/handlers from the property mapping. 

23 - Phase4: no group axioms in pure RDF/RDFS. 

24 - Phase5: fallback - all unmapped predicates -> Property, all unmapped 

25 URI subjects/objects -> Resource, then RDFS defaults applied to every 

26 Property (rdfs:Resource for domain, rdfs:Class for range) climbing the 

27 rdfs:subPropertyOf hierarchy first. 

28 - Phase6: inherited from BaseLogic, reifies unmapped triples as Statements. 

29 """ 

30 

31 # ========== READER PHASES ========== 

32 

33 def phase1_classify_from_predicates(self): 

34 """Classifies subjects of mapped predicates. 

35 - BNodes with inferred_class predicates -> classified 

36 - URIRefs with inferred_class or unambiguous target_classes -> classified 

37 """ 

38 classified = {} 

39 

40 for pred in self._property_mapping: 

41 for uri in self.graph.subjects(pred, None): 

42 if uri in self._instance_cache: 

43 continue 

44 python_class = self._strategy.classify_by_predicate(uri, self.graph) 

45 if not python_class: 

46 continue 

47 if isinstance(uri, BNode): 

48 if uri not in classified: 

49 classified[uri] = python_class 

50 elif isinstance(uri, URIRef): 

51 self.get_or_create(uri, python_class, populate=False) 

52 

53 for uri, py_class in classified.items(): 

54 self.get_or_create(uri, py_class, populate=False) 

55 

56 def phase2_create_from_types(self): 

57 type_mapping = self._strategy.get_type_mapping() 

58 

59 for rdf_type, config in type_mapping.items(): 

60 py_class = config.get('target_class') 

61 if not py_class: 

62 continue 

63 for uri in self.graph.subjects(RDF.type, rdf_type): 

64 self.get_or_create(uri, py_class, populate=False) 

65 for instance in self._instance_cache.get(uri, set()): 

66 if 'setters' in config: 

67 self._apply_setters_immediate(instance, config['setters']) 

68 if 'handler' in config: 

69 getattr(self, config['handler'])(instance, uri) 

70 

71 def phase3_populate_properties(self): 

72 """Populates each cached instance via configured setters and handlers.""" 

73 for uri in list(self._instance_cache.keys()): 

74 for instance in list(self._instance_cache[uri]): 

75 self.populate_instance(instance, uri) 

76 

77 def phase4_process_group_axioms(self): 

78 """No group axioms in pure RDF/RDFS.""" 

79 pass 

80 

81 def phase5_fallback(self): 

82 """ 

83 Generic fallback for RDF/RDFS: 

84 1. Every predicate not yet cached and outside structural namespaces 

85 is created as Property. 

86 2. Every URI subject/object not yet cached is created as Resource. 

87 3. RDFS defaults (rdfs:Resource for domain, rdfs:Class for range) 

88 are applied to every Property after climbing the rdfs:subPropertyOf 

89 chain to inherit declared values when present. 

90 """ 

91 exclude_predicates = {RDF.first, RDF.rest, RDF.nil} 

92 

93 # 1. predicati -> Property 

94 for pred in set(self.graph.predicates()): 

95 if pred in self._instance_cache or pred in exclude_predicates: 

96 continue 

97 if self._is_structural(pred): 

98 continue 

99 self.get_or_create(pred, Property, populate=False) 

100 

101 # 2. soggetti/oggetti URI -> Resource 

102 for subj in set(self.graph.subjects()): 

103 if isinstance(subj, URIRef) and subj not in self._instance_cache: 

104 if self._is_structural(subj) or self._is_rdf_collection(subj): 

105 continue 

106 self.get_or_create(subj, Resource, populate=False) 

107 

108 for obj in set(self.graph.objects()): 

109 if isinstance(obj, URIRef) and obj not in self._instance_cache: 

110 if self._is_structural(obj) or self._is_rdf_collection(obj): 

111 continue 

112 self.get_or_create(obj, Resource, populate=False) 

113 

114 # 3. defaults RDFS su Property 

115 for uri, instances in list(self._instance_cache.items()): 

116 for instance in list(instances): 

117 if isinstance(instance, Property): 

118 self._apply_rdfs_defaults(instance) 

119 

120 # ========== HELPERS ========== 

121 

122 def _is_structural(self, uri: Node) -> bool: 

123 """True if URI belongs to RDF/RDFS/OWL/SKOS/XSD vocabulary namespaces.""" 

124 if not isinstance(uri, URIRef): 

125 return False 

126 s = str(uri) 

127 return any(s.startswith(ns) for ns in (str(RDF), str(RDFS), str(OWL), str(SKOS), str(XSD))) 

128 

129 def _apply_setters_immediate(self, instance, setters_config): 

130 """Applies static setter values from config directly (mirror of 

131 OwlLogic._apply_setters_immediate). Used in phase2 for 'is: class' 

132 entries that declare setters with constant values. 

133 """ 

134 for setter_item in setters_config: 

135 if isinstance(setter_item, dict): 

136 for setter_name, value in setter_item.items(): 

137 if hasattr(instance, setter_name): 

138 getattr(instance, setter_name)(value) 

139 else: 

140 if hasattr(instance, setter_item): 

141 getattr(instance, setter_item)() 

142 

143 def _apply_rdfs_defaults(self, instance): 

144 """Applies rdfs:Resource (domain) and rdfs:Class (range) defaults 

145 when the property does not declare them, after walking up 

146 rdfs:subPropertyOf to inherit values from ancestors. 

147 """ 

148 if not instance.get_has_domain(): 

149 inherited = self._get_inherited_property_values(instance, "get_has_domain") 

150 if inherited: 

151 for d in inherited: 

152 instance.set_has_domain(d) 

153 else: 

154 instance.set_has_domain(self.get_or_create(RDFS.Resource, Concept)) 

155 

156 if not instance.get_has_range(): 

157 inherited = self._get_inherited_property_values(instance, "get_has_range") 

158 if inherited: 

159 for r in inherited: 

160 instance.set_has_range(r) 

161 else: 

162 instance.set_has_range(self.get_or_create(RDFS.Class, Concept)) 

163 

164 def _get_inherited_property_values(self, property_instance, getter_name: str) -> list: 

165 """Walks up rdfs:subPropertyOf collecting the first non-empty value 

166 of getter_name found on an ancestor. 

167 """ 

168 def collect(node): 

169 if node is property_instance: 

170 return None 

171 getter = getattr(node, getter_name, None) 

172 if getter: 

173 values = getter() 

174 if values: 

175 return values if isinstance(values, list) else [values] 

176 return None 

177 

178 result = self._traverse_hierarchy( 

179 property_instance, 

180 next_getter="get_is_sub_property_of", 

181 direction="up", 

182 collect=collect, 

183 ) 

184 return result if result is not None else [] 

185 

186 

187 def create_python_container(self, instance, uri): 

188 """Populates a Container instance from RDF container/list syntax. 

189 

190 Handles all four RDF container types: 

191 - rdf:Bag, rdf:Alt, rdf:Seq -> rdf:_N indexed members 

192 - rdf:List -> rdf:first/rdf:rest linked list 

193 

194 URI members are resolved as Resource, RDFlibLiterals as Literal. 

195 All consumed triples are registered in _triples_map so phase6 does not 

196 reify them as Statements. 

197 """ 

198 if instance not in self._triples_map: 

199 self._triples_map[instance] = set() 

200 

201 # rdf:List: rdf:first/rdf:rest chain 

202 if (uri, RDF.first, None) in self.graph: 

203 try: 

204 for item in RDFLibCollection(self.graph, uri): 

205 member = self._resolve_container_member(item) 

206 if member: 

207 instance.set_has_member(member) 

208 # mark structural triples as consumed 

209 node = uri 

210 while node and node != RDF.nil: 

211 first = self.graph.value(node, RDF.first) 

212 rest = self.graph.value(node, RDF.rest) 

213 if first is not None: 

214 self._triples_map[instance].add((node, RDF.first, first)) 

215 if rest is not None: 

216 self._triples_map[instance].add((node, RDF.rest, rest)) 

217 node = rest 

218 except Exception as e: 

219 print(f"Errore create_python_container (List): {e}") 

220 return 

221 

222 # rdf:Bag/Alt/Seq: rdf:_N indexed members 

223 indexed = [] 

224 for pred, obj in self.graph.predicate_objects(uri): 

225 pred_str = str(pred) 

226 prefix = str(RDF) + "_" 

227 if pred_str.startswith(prefix): 

228 try: 

229 idx = int(pred_str[len(prefix):]) 

230 indexed.append((idx, pred, obj)) 

231 except ValueError: 

232 continue 

233 

234 indexed.sort(key=lambda t: t[0]) 

235 for _, pred, obj in indexed: 

236 member = self._resolve_container_member(obj) 

237 if member: 

238 instance.set_has_member(member) 

239 self._triples_map[instance].add((uri, pred, obj)) 

240 

241 def _resolve_container_member(self, node): 

242 if isinstance(node, RDFlibLiteral): 

243 return self._create_literal(node) 

244 return self.get_or_create(node, Resource)