iolanta 2.1.19__py3-none-any.whl → 2.1.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
iolanta/cli/main.py CHANGED
@@ -54,6 +54,9 @@ def decode_datatype(datatype: str) -> URIRef:
54
54
  if datatype.startswith("http"):
55
55
  return URIRef(datatype)
56
56
 
57
+ if "/" in datatype:
58
+ return URIRef(f"https://iolanta.tech/{datatype}")
59
+
57
60
  return URIRef(f"https://iolanta.tech/datatypes/{datatype}")
58
61
 
59
62
 
@@ -52,13 +52,14 @@ class TextualDefaultFacet(Facet[Widget]): # noqa: WPS214
52
52
  for row in property_rows
53
53
  ]
54
54
 
55
+ iolanta_lang = str(self.iolanta.language)
55
56
  property_pairs = [
56
57
  (property_iri, object_node)
57
58
  for property_iri, object_node in property_pairs
58
59
  if (
59
60
  not isinstance(object_node, Literal)
60
61
  or not (language := object_node.language) # noqa: W503
61
- or (language == self.iolanta.language) # noqa: W503
62
+ or (language == iolanta_lang)
62
63
  )
63
64
  ]
64
65
 
@@ -0,0 +1,5 @@
1
+ """Knowledge Graph linter facet."""
2
+
3
+ from iolanta.kglint.facet import KGLint
4
+
5
+ __all__ = ['KGLint']
@@ -0,0 +1,183 @@
1
+ """KGLint facet: deterministic KG linter with assertions and labels."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ import funcy
7
+ from rdflib import BNode, Literal, Node, URIRef
8
+
9
+ from iolanta import Facet
10
+ from iolanta.kglint.models import (
11
+ Assertion,
12
+ AssertionCode,
13
+ LabelEntry,
14
+ NodeBlank,
15
+ NodeLiteral,
16
+ NodeObject,
17
+ NodeURI,
18
+ Report,
19
+ TripleRef,
20
+ )
21
+ from iolanta.namespaces import DATATYPES
22
+
23
+ QNAME_RE = re.compile(r'^\S+:\S+$')
24
+
25
+
26
+ def _serialize(node: Node) -> str:
27
+ """Serialize node to stable string (N-Triples style)."""
28
+ return node.n3()
29
+
30
+
31
+ def _looks_like_qname(label: str) -> bool:
32
+ """True if label has the form prefix:localName (QName-shaped)."""
33
+ return bool(QNAME_RE.match(label))
34
+
35
+
36
+ class KGLint(Facet[str]):
37
+ """Deterministic Knowledge Graph linter: assertions + URI/blank labels."""
38
+
39
+ META = Path(__file__).parent / 'kglint.yamlld'
40
+
41
+ def _render_label(self, node: Node) -> str:
42
+ """Human-readable label for a node (title facet)."""
43
+ return str(self.render(node, as_datatype=DATATYPES.title))
44
+
45
+ @funcy.cached_property
46
+ @funcy.post_processing(list)
47
+ def _triples(self):
48
+ """All triples in the graph (cached)."""
49
+ for row in self.stored_query('triples.sparql', graph=self.this):
50
+ yield (row['subject'], row['predicate'], row['object'])
51
+
52
+ @funcy.cached_property
53
+ @funcy.post_processing(set)
54
+ def _nodes(self):
55
+ """URIRef and BNode terms that appear in triples (cached)."""
56
+ for s, p, o in self._triples:
57
+ for term in (s, p, o):
58
+ if isinstance(term, (URIRef, BNode)):
59
+ yield term
60
+
61
+ @funcy.cached_property
62
+ @funcy.post_processing(dict)
63
+ def label_by_node(self):
64
+ """Rendered label per node (cached)."""
65
+ for node in self._nodes:
66
+ yield node, self._render_label(node)
67
+
68
+ @funcy.cached_property
69
+ @funcy.post_processing(funcy.group_values)
70
+ def _triples_by_node(self):
71
+ """Map each node to list of triples it participates in (cached)."""
72
+ nodes = self._nodes
73
+ for s, p, o in self._triples:
74
+ for term in (s, p, o):
75
+ if term in nodes:
76
+ yield term, (s, p, o)
77
+
78
+ def _node_to_object(self, node: Node) -> NodeObject:
79
+ """Convert RDF node to JSON node object (literal, uri, or blank)."""
80
+ match node:
81
+ case Literal():
82
+ return NodeLiteral(
83
+ value=str(node),
84
+ datatype=str(node.datatype) if node.datatype else None,
85
+ language=node.language or None,
86
+ )
87
+ case URIRef():
88
+ return NodeURI(value=str(node), label=self.label_by_node[node])
89
+ case BNode():
90
+ return NodeBlank(value=node.n3(), label=self.label_by_node[node])
91
+
92
+ def _label_entries(self):
93
+ """Yield label entries (node + triples) in deterministic order."""
94
+ label_by_node = self.label_by_node
95
+ triples_by_node = self._triples_by_node
96
+ for node in sorted(self._nodes, key=_serialize):
97
+ triples_refs = [
98
+ TripleRef(
99
+ s=self._node_to_object(s),
100
+ p=self._node_to_object(p),
101
+ o=self._node_to_object(o),
102
+ )
103
+ for s, p, o in sorted(
104
+ triples_by_node[node],
105
+ key=lambda t: (_serialize(t[0]), _serialize(t[1]), _serialize(t[2])),
106
+ )
107
+ ]
108
+ if isinstance(node, URIRef):
109
+ entry_node = NodeURI(value=str(node), label=label_by_node[node])
110
+ else:
111
+ entry_node = NodeBlank(value=node.n3(), label=label_by_node[node])
112
+ yield LabelEntry(node=entry_node, triples=triples_refs)
113
+
114
+ def _assertions(self):
115
+ """Yield all assertions (literal/uri/blank checks)."""
116
+ label_by_node = self.label_by_node
117
+ for s, p, o in self._triples:
118
+ if isinstance(o, Literal):
119
+ label = self._render_label(o)
120
+ if label.startswith('http'):
121
+ yield Assertion(
122
+ severity='warning',
123
+ code=AssertionCode.LITERAL_LOOKS_LIKE_URI,
124
+ target=TripleRef(
125
+ s=self._node_to_object(s),
126
+ p=self._node_to_object(p),
127
+ o=self._node_to_object(o),
128
+ ),
129
+ message=(
130
+ 'This RDF literal seems to be actually a URL. '
131
+ 'Good chance is that it should not be a literal.'
132
+ ),
133
+ )
134
+ elif _looks_like_qname(label):
135
+ yield Assertion(
136
+ severity='warning',
137
+ code=AssertionCode.LITERAL_LOOKS_LIKE_QNAME,
138
+ target=TripleRef(
139
+ s=self._node_to_object(s),
140
+ p=self._node_to_object(p),
141
+ o=self._node_to_object(o),
142
+ ),
143
+ message=(
144
+ 'This RDF literal seems to be actually a QName '
145
+ '(prefixed URI). Good chance is that it should '
146
+ 'not be a literal.'
147
+ ),
148
+ )
149
+ for node in self._nodes:
150
+ label = label_by_node[node]
151
+ if isinstance(node, URIRef):
152
+ if str(node) == label:
153
+ yield Assertion(
154
+ severity='error',
155
+ code=AssertionCode.URI_LABEL_IDENTICAL,
156
+ target=NodeURI(value=str(node), label=label),
157
+ message=(
158
+ 'For this URI, the label is the same as the URI. '
159
+ 'We were unable to render that URI.'
160
+ ),
161
+ )
162
+ elif isinstance(node, BNode):
163
+ if str(node) == label:
164
+ yield Assertion(
165
+ severity='warning',
166
+ code=AssertionCode.BLANK_LABEL_IDENTICAL,
167
+ target=NodeBlank(value=node.n3(), label=label),
168
+ message=(
169
+ 'For this blank node, the label is the same as '
170
+ 'the blank node. We were unable to render that '
171
+ 'blank node.'
172
+ ),
173
+ )
174
+
175
+ def show(self) -> str:
176
+ """Build report (assertions + labels) and return JSON string."""
177
+ labels_list = list(self._label_entries())
178
+ assertions_list = sorted(
179
+ self._assertions(),
180
+ key=lambda a: (a.severity, a.code, a.target.model_dump_json()),
181
+ )
182
+ report = Report(assertions=assertions_list, labels=labels_list)
183
+ return report.model_dump_json(indent=2, exclude_none=True)
@@ -0,0 +1,23 @@
1
+ "@context":
2
+ "@import": https://json-ld.org/contexts/dollar-convenience.jsonld
3
+ iolanta: https://iolanta.tech/
4
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#"
5
+
6
+ iolanta:outputs:
7
+ "@type": "@id"
8
+
9
+ $: rdfs:label
10
+ →:
11
+ "@type": "@id"
12
+ "@id": iolanta:outputs
13
+
14
+ ↦: iolanta:matches
15
+
16
+ $id: pkg:pypi/iolanta#kglint
17
+ $: KGLint
18
+ →:
19
+ $id: https://iolanta.tech/kglint/json
20
+ $: KGLint JSON
21
+ ↦:
22
+ - ASK WHERE { GRAPH $this { ?s ?p ?o } }
23
+ - ASK WHERE { $this <https://iolanta.tech/has-sub-graph> ?subgraph }
@@ -0,0 +1,80 @@
1
+ """Pydantic models for kglint report output."""
2
+
3
+ from enum import StrEnum
4
+ from typing import Annotated, Literal as LiteralType
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class AssertionCode(StrEnum):
10
+ """Deterministic assertion codes emitted by the linter."""
11
+
12
+ LITERAL_LOOKS_LIKE_URI = 'literal-looks-like-uri'
13
+ LITERAL_LOOKS_LIKE_QNAME = 'literal-looks-like-qname'
14
+ URI_LABEL_IDENTICAL = 'uri-label-identical'
15
+ BLANK_LABEL_IDENTICAL = 'blank-label-identical'
16
+
17
+
18
+ class NodeLiteral(BaseModel):
19
+ """Literal term as object: value, datatype, language."""
20
+
21
+ type: LiteralType['literal'] = 'literal'
22
+ value: str
23
+ datatype: str | None = None
24
+ language: str | None = None
25
+
26
+
27
+ class NodeURI(BaseModel):
28
+ """URI term as object: value and rendered label."""
29
+
30
+ type: LiteralType['uri'] = 'uri'
31
+ value: str
32
+ label: str
33
+
34
+
35
+ class NodeBlank(BaseModel):
36
+ """Blank node term as object: value and rendered label."""
37
+
38
+ type: LiteralType['blank'] = 'blank'
39
+ value: str
40
+ label: str
41
+
42
+
43
+ NodeObject = Annotated[
44
+ NodeLiteral | NodeURI | NodeBlank,
45
+ Field(discriminator='type'),
46
+ ]
47
+
48
+
49
+ class TripleRef(BaseModel):
50
+ """A triple (s, p, o as node objects). Used in assertions and label entries."""
51
+
52
+ type: LiteralType['triple'] = 'triple'
53
+ s: NodeObject
54
+ p: NodeObject
55
+ o: NodeObject
56
+
57
+
58
+ class Assertion(BaseModel):
59
+ """Single validation assertion (error or warning)."""
60
+
61
+ severity: LiteralType['error', 'warning']
62
+ code: AssertionCode
63
+ target: NodeURI | NodeBlank | TripleRef
64
+ message: str
65
+
66
+
67
+ class LabelEntry(BaseModel):
68
+ """URI/blank node (as object with type, value, label) and participating triples."""
69
+
70
+ node: NodeURI | NodeBlank
71
+ triples: list[TripleRef] = Field(default_factory=list)
72
+
73
+
74
+
75
+
76
+ class Report(BaseModel):
77
+ """KGLint report: assertions and labels."""
78
+
79
+ assertions: list[Assertion] = Field(default_factory=list)
80
+ labels: list[LabelEntry] = Field(default_factory=list)
@@ -0,0 +1,5 @@
1
+ SELECT * WHERE {
2
+ GRAPH $graph {
3
+ ?subject ?predicate ?object .
4
+ }
5
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iolanta
3
- Version: 2.1.19
3
+ Version: 2.1.21
4
4
  Summary: Semantic Web browser
5
5
  License: MIT
6
6
  Author: Anatoly Scherbakov
@@ -6,7 +6,7 @@ iolanta/cli/formatters/choose.py,sha256=LWzsO_9IBSSgYNIyLlItkp8TNvpW3v6YCQ8-6kbI
6
6
  iolanta/cli/formatters/csv.py,sha256=ceJ_DTz0beqeK-d6FPBQqqjXrziEfF0FRSLoGZCt_fs,760
7
7
  iolanta/cli/formatters/json.py,sha256=Og5B9UrSM_0NWqW5Afpsy6WH8ZfYgPMVXYvT3i-43Jc,748
8
8
  iolanta/cli/formatters/pretty.py,sha256=IypZRAr2vNqcXFY6NOIc75mpyfpFWh56aCBlOPDDieQ,2901
9
- iolanta/cli/main.py,sha256=RNNrj8Rnh6F9ISbxCPc_LHQrgwPcwvQ2vL6NU2rxNus,8333
9
+ iolanta/cli/main.py,sha256=9TgrPQ2ytKhK--Z4jikY1ngW7HcVh5nc8q8z4KcBwcs,8416
10
10
  iolanta/cli/models.py,sha256=cjbpowdzI4wAP0DUk3qoVHyimk6AZwlXi9CGmusZTuM,159
11
11
  iolanta/cli/pretty_print.py,sha256=M6E3TmhzA6JY5GeUVmDZLmOh5u70-393PVit4voFKDI,977
12
12
  iolanta/context.py,sha256=bZR-tbZIrDQ-Vby01PMDZ6ifxM-0YMK68RJvAsyqCTs,507
@@ -86,7 +86,7 @@ iolanta/facets/textual_class/facets.py,sha256=MiGTapgt30ME2fapwdrD_yQj4mhkdmyMAj
86
86
  iolanta/facets/textual_class/sparql/instances.sparql,sha256=Wx3xThlEgwz8gqH5Fnv789p1CiBRJGLOhRpChtFq1Us,139
87
87
  iolanta/facets/textual_class/textual-class.yamlld,sha256=HBQCC3Kg41rFfPne9Gx1CXsQ3nQPXmwUXh9hr3AQU5A,586
88
88
  iolanta/facets/textual_default/__init__.py,sha256=snxA0FEY9qfAxNv3MlZLrJsXugD4dvs5hLStZWV7soM,158
89
- iolanta/facets/textual_default/facets.py,sha256=D3W9kMxel_BqA8mWoxfWc3IJlHMwdM5ndyZ5SBeYHnA,5721
89
+ iolanta/facets/textual_default/facets.py,sha256=_9tjsEojtG6kSuIbz-2_qc-HUbufs5PheDOEj53_H1A,5745
90
90
  iolanta/facets/textual_default/sparql/inverse-properties.sparql,sha256=2m2q3C6jMQ_8o-0cPgIYrT77l6UnY3oqI37-Ed7p4-c,65
91
91
  iolanta/facets/textual_default/sparql/label.sparql,sha256=IWAkkgMKtIZ7Zpg8LUJ5fDZ9tiI8fiRYZo-zT61Fong,121
92
92
  iolanta/facets/textual_default/sparql/nodes-for-property.sparql,sha256=J9vg0Pz2HXDlPCeZ6IS2C0wODrpYDuNeD6DYT6UdNsU,68
@@ -130,6 +130,11 @@ iolanta/facets/wikibase_statement_title/__init__.py,sha256=_yk1akxgSJOiUBJIc8QGr
130
130
  iolanta/facets/wikibase_statement_title/facets.py,sha256=gNRqiwOTMxyyHYvb_vIrfd-4ipb8wfyJ4GgPcQdyy9E,726
131
131
  iolanta/facets/wikibase_statement_title/sparql/statement-title.sparql,sha256=n07DQWxKqB5c3CA4kacq2HSN0R0dLgnMnLP1AxMo5YA,320
132
132
  iolanta/iolanta.py,sha256=f_BlYPxXs8b2E3BYB_3wsxDcycIt8daqQ-WPecfTEbY,14450
133
+ iolanta/kglint/__init__.py,sha256=m16W7nQvAGylFgc40x_QrJJGTH2fNi8vjKVf0OurYQU,99
134
+ iolanta/kglint/facet.py,sha256=84zuYAcEXcJNvyBNLFhGhFYm6s7SwQ98mAm1794dC3w,7057
135
+ iolanta/kglint/kglint.yamlld,sha256=gKiNhn-IHBCTzfHBVSfGoppprxUQEIoc4GGwsZrycVI,515
136
+ iolanta/kglint/models.py,sha256=Lox7XRttiwKyY5_QdE8k--y3T0oqNEBUNlQzUZUG06k,1949
137
+ iolanta/kglint/sparql/triples.sparql,sha256=VsCmYN5AX7jSIiFm-SqLcRcOvUVj8yyZI4PSzKROtQw,82
133
138
  iolanta/labeled_triple_set/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
134
139
  iolanta/labeled_triple_set/data/labeled_triple_set.yamlld,sha256=P3oAPSPsirpbcRXej-VekuYFTpWqrkysYsxghZc3bTk,1008
135
140
  iolanta/labeled_triple_set/labeled_triple_set.py,sha256=o4IgvTvPd0mzBtpgHYd4n1xpujYdAvWBr6gIYwp5vnA,4061
@@ -165,7 +170,7 @@ iolanta/sparqlspace/sparqlspace.py,sha256=Y8_ZPXwuGEXbEes6XQjaQWA2Zv9y8SWxMPDFdq
165
170
  iolanta/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
171
  iolanta/widgets/description.py,sha256=98Qd3FwT9r8sYqKjl9ZEptaVX9jJ2ULWf0uy3j52p5o,800
167
172
  iolanta/widgets/mixin.py,sha256=nDRCOc-gizCf1a5DAcYs4hW8eZEd6pHBPFsfm0ncv7E,251
168
- iolanta-2.1.19.dist-info/METADATA,sha256=TQncwX9PC67AS7oD4fdRIfy3K7Ayw1FcBGHAPOf4q6k,2349
169
- iolanta-2.1.19.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
170
- iolanta-2.1.19.dist-info/entry_points.txt,sha256=Z1f3OaNruE2a6eprkiLgcPw-lZCah_cRT-PTlDD-Y1s,2569
171
- iolanta-2.1.19.dist-info/RECORD,,
173
+ iolanta-2.1.21.dist-info/METADATA,sha256=mbHvn8r2Iol03WVyF1jClkt7DFaL5FGU04yP-K61pN4,2349
174
+ iolanta-2.1.21.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
175
+ iolanta-2.1.21.dist-info/entry_points.txt,sha256=ArQ-tRMRs-X_w78E1-4f4oaE7beHskz9knYOPgkE-ms,2604
176
+ iolanta-2.1.21.dist-info/RECORD,,
@@ -12,6 +12,7 @@ construct-result-csv-facet=iolanta.facets.query.construct_result_csv:ConstructRe
12
12
  construct-result-json-facet=iolanta.facets.query.construct_result_json:ConstructResultJsonFacet
13
13
  construct-result-table-facet=iolanta.facets.query.construct_result_table:ConstructResultTableFacet
14
14
  icon=iolanta.facets.icon:IconFacet
15
+ kglint=iolanta.kglint.facet:KGLint
15
16
  labeled-triple-set=iolanta.labeled_triple_set.labeled_triple_set:LabeledTripleSet
16
17
  mermaid-graph=iolanta.mermaid.facet:Mermaid
17
18
  mermaid-roadmap=iolanta.facets.mermaid_roadmap.facet:MermaidRoadmap