iolanta 2.1.18__py3-none-any.whl → 2.1.20__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
 
iolanta/iolanta.py CHANGED
@@ -62,6 +62,21 @@ def _create_default_graph():
62
62
  return ConjunctiveGraph(identifier=namespaces.LOCAL.term("_inference"))
63
63
 
64
64
 
65
+ def _loadable_files(path: Path) -> Iterable[Path]:
66
+ """Paths of loadable files under path, skipping hidden files and dirs."""
67
+ if path.is_file():
68
+ if not path.name.startswith("."):
69
+ yield path
70
+ return
71
+ for entry in path.iterdir():
72
+ if entry.name.startswith("."):
73
+ continue
74
+ if entry.is_file():
75
+ yield entry
76
+ else:
77
+ yield from _loadable_files(entry)
78
+
79
+
65
80
  @dataclass
66
81
  class Iolanta: # noqa: WPS214, WPS338
67
82
  """Iolanta is a Semantic web browser."""
@@ -241,10 +256,7 @@ class Iolanta: # noqa: WPS214, WPS338
241
256
  if not isinstance(source, Path):
242
257
  source = Path(source)
243
258
 
244
- for source_file in list(source.rglob("*")) or [source]:
245
- if source_file.is_dir():
246
- continue
247
-
259
+ for source_file in _loadable_files(source):
248
260
  try: # noqa: WPS225
249
261
  ld_rdf = yaml_ld.to_rdf(source_file)
250
262
  except ConnectionError as name_resolution_error:
@@ -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.18
3
+ Version: 2.1.20
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
@@ -129,7 +129,12 @@ iolanta/facets/title/title.yamlld,sha256=FjfqNPl3EAzZNLYAZNQ6mHfNmFkfxMm1A43gygf
129
129
  iolanta/facets/wikibase_statement_title/__init__.py,sha256=_yk1akxgSJOiUBJIc8QGrD2vovvmx_iw_vJDuv1rD7M,91
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
- iolanta/iolanta.py,sha256=qHc4S1u86m8GLM4omdgzGMoB9QXNnsFH8K5U2rq0r7Y,14086
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.18.dist-info/METADATA,sha256=XZU3ors0xfTtQLxAWmGTGaUtUHjJB5AmMJ9JyM7SEsA,2349
169
- iolanta-2.1.18.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
170
- iolanta-2.1.18.dist-info/entry_points.txt,sha256=Z1f3OaNruE2a6eprkiLgcPw-lZCah_cRT-PTlDD-Y1s,2569
171
- iolanta-2.1.18.dist-info/RECORD,,
173
+ iolanta-2.1.20.dist-info/METADATA,sha256=NNHUo9jZBtoUWehayFzugGdCamVM1jlh9OYeU9L-Dwk,2349
174
+ iolanta-2.1.20.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
175
+ iolanta-2.1.20.dist-info/entry_points.txt,sha256=ArQ-tRMRs-X_w78E1-4f4oaE7beHskz9knYOPgkE-ms,2604
176
+ iolanta-2.1.20.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