iolanta 2.1.1__py3-none-any.whl → 2.1.6__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 +83 -43
- iolanta/declension/__init__.py +0 -0
- iolanta/declension/data/declension.yamlld +39 -0
- iolanta/declension/facet.py +44 -0
- iolanta/declension/sparql/declension.sparql +8 -0
- iolanta/facets/cli/record.py +2 -2
- iolanta/facets/facet.py +1 -22
- iolanta/facets/foaf_person_title/facet.py +2 -2
- iolanta/facets/generic/bool_literal.py +3 -3
- iolanta/facets/generic/date_literal.py +1 -1
- iolanta/facets/generic/default.py +2 -2
- iolanta/facets/html/code_literal.py +3 -3
- iolanta/facets/icon.py +1 -1
- iolanta/facets/locator/sparql/get-query-to-facet.sparql +5 -0
- iolanta/facets/locator.py +5 -11
- iolanta/facets/qname.py +2 -2
- iolanta/facets/textual_browser/app.py +7 -3
- iolanta/facets/textual_browser/facet.py +1 -1
- iolanta/facets/textual_browser/page_switcher.py +13 -18
- iolanta/facets/textual_class/facets.py +3 -3
- iolanta/facets/textual_class/sparql/instances.sparql +4 -1
- iolanta/facets/textual_default/facets.py +5 -5
- iolanta/facets/textual_default/widgets.py +1 -1
- iolanta/facets/textual_graph/facets.py +3 -3
- iolanta/facets/textual_graph_triples.py +1 -1
- iolanta/facets/textual_graphs/__init__.py +6 -0
- iolanta/facets/textual_graphs/data/textual_graphs.yamlld +23 -0
- iolanta/facets/textual_graphs/facets.py +138 -0
- iolanta/facets/textual_graphs/sparql/graphs.sparql +5 -0
- iolanta/facets/textual_link/facet.py +4 -4
- iolanta/facets/textual_nanopublication/facet.py +1 -1
- iolanta/facets/textual_no_facet_found.py +3 -1
- iolanta/facets/textual_ontology/facets.py +3 -3
- iolanta/facets/textual_provenance/facets.py +1 -1
- iolanta/facets/title/facets.py +1 -4
- iolanta/facets/wikibase_statement_title/facets.py +2 -2
- iolanta/iolanta.py +2 -2
- iolanta/labeled_triple_set/__init__.py +0 -0
- iolanta/labeled_triple_set/data/labeled_triple_set.yamlld +42 -0
- iolanta/labeled_triple_set/labeled_triple_set.py +137 -0
- iolanta/labeled_triple_set/sparql/triples.sparql +5 -0
- iolanta/mcp/__init__.py +0 -0
- iolanta/mcp/cli.py +39 -0
- iolanta/mcp/prompts/nanopublication_assertion_authoring_rules.md +63 -0
- iolanta/mcp/prompts/rules.md +83 -0
- iolanta/mermaid/facet.py +0 -3
- iolanta/mermaid/mermaid.yamlld +7 -24
- iolanta/mermaid/models.py +38 -9
- iolanta/mermaid/sparql/ask-has-triples.sparql +3 -0
- iolanta/sparqlspace/processor.py +6 -3
- {iolanta-2.1.1.dist-info → iolanta-2.1.6.dist-info}/METADATA +7 -7
- {iolanta-2.1.1.dist-info → iolanta-2.1.6.dist-info}/RECORD +54 -36
- {iolanta-2.1.1.dist-info → iolanta-2.1.6.dist-info}/WHEEL +1 -1
- {iolanta-2.1.1.dist-info → iolanta-2.1.6.dist-info}/entry_points.txt +4 -0
|
@@ -32,7 +32,7 @@ class TextualDefaultFacet(Facet[Widget]): # noqa: WPS214
|
|
|
32
32
|
"""Properties of current node & their values."""
|
|
33
33
|
property_rows = self.stored_query(
|
|
34
34
|
self.query_file_name,
|
|
35
|
-
iri=self.
|
|
35
|
+
iri=self.this,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
property_pairs = [
|
|
@@ -64,11 +64,11 @@ class TextualDefaultFacet(Facet[Widget]): # noqa: WPS214
|
|
|
64
64
|
property_values = [
|
|
65
65
|
LiteralPropertyValue(
|
|
66
66
|
property_value=property_value,
|
|
67
|
-
subject=self.
|
|
67
|
+
subject=self.this,
|
|
68
68
|
property_iri=property_iri,
|
|
69
69
|
) if isinstance(property_value, Literal) else PropertyValue(
|
|
70
70
|
property_value=property_value,
|
|
71
|
-
subject=self.
|
|
71
|
+
subject=self.this,
|
|
72
72
|
property_iri=property_iri,
|
|
73
73
|
property_qname=self.iolanta.node_as_qname(property_iri),
|
|
74
74
|
)
|
|
@@ -135,7 +135,7 @@ class TextualDefaultFacet(Facet[Widget]): # noqa: WPS214
|
|
|
135
135
|
def show(self) -> Widget:
|
|
136
136
|
"""Render the content."""
|
|
137
137
|
return VerticalScroll(
|
|
138
|
-
PageTitle(self.
|
|
138
|
+
PageTitle(self.this),
|
|
139
139
|
Static(self.description or ''),
|
|
140
140
|
self.properties,
|
|
141
141
|
)
|
|
@@ -149,7 +149,7 @@ class InverseProperties(TextualDefaultFacet):
|
|
|
149
149
|
def show(self) -> Widget:
|
|
150
150
|
"""Render the content."""
|
|
151
151
|
return VerticalScroll(
|
|
152
|
-
PageTitle(self.
|
|
152
|
+
PageTitle(self.this, extra='[i]& its inverse RDF properties[/i]'),
|
|
153
153
|
Static(self.description or ''),
|
|
154
154
|
self.properties,
|
|
155
155
|
)
|
|
@@ -138,7 +138,7 @@ class PropertyValue(Widget, can_focus=True, inherit_bindings=False):
|
|
|
138
138
|
self.property_iri = property_iri
|
|
139
139
|
super().__init__()
|
|
140
140
|
self.renderable = Text( # noqa: WPS601
|
|
141
|
-
f'⏳ {
|
|
141
|
+
f'⏳ {property_value}',
|
|
142
142
|
style='#696969',
|
|
143
143
|
)
|
|
144
144
|
|
|
@@ -14,17 +14,17 @@ class GraphFacet(Facet[Widget]):
|
|
|
14
14
|
"""Show the widget."""
|
|
15
15
|
triples = [
|
|
16
16
|
Triple(triple['subject'], triple['predicate'], triple['object'])
|
|
17
|
-
for triple in self.stored_query('triples.sparql', graph=self.
|
|
17
|
+
for triple in self.stored_query('triples.sparql', graph=self.this)
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
triple_count = len(triples)
|
|
21
21
|
|
|
22
22
|
triples_view = self.iolanta.render(
|
|
23
|
-
self.
|
|
23
|
+
self.this,
|
|
24
24
|
as_datatype=DATATYPES['textual-graph-triples'],
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
return VerticalScroll(
|
|
28
|
-
PageTitle(self.
|
|
28
|
+
PageTitle(self.this, extra=f'({triple_count} triples)'),
|
|
29
29
|
triples_view,
|
|
30
30
|
)
|
|
@@ -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
|
+
rdfg: http://www.w3.org/2009/rdfg#
|
|
6
|
+
|
|
7
|
+
$: rdfs:label
|
|
8
|
+
iolanta:is-preferred-over:
|
|
9
|
+
"@type": "@id"
|
|
10
|
+
|
|
11
|
+
iolanta:outputs:
|
|
12
|
+
"@type": "@id"
|
|
13
|
+
|
|
14
|
+
$id: rdfg:Graph
|
|
15
|
+
iolanta:facet:
|
|
16
|
+
$id: pkg:pypi/iolanta#textual-graphs
|
|
17
|
+
$: Named Graphs
|
|
18
|
+
|
|
19
|
+
iolanta:outputs: https://iolanta.tech/cli/textual
|
|
20
|
+
|
|
21
|
+
iolanta:is-preferred-over:
|
|
22
|
+
- pkg:pypi/iolanta#textual-properties
|
|
23
|
+
- pkg:pypi/iolanta#textual-graph-triples
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Iterable, NamedTuple
|
|
4
|
+
|
|
5
|
+
import funcy
|
|
6
|
+
from rdflib import Literal, Node
|
|
7
|
+
from textual.containers import Vertical
|
|
8
|
+
from textual.coordinate import Coordinate
|
|
9
|
+
from textual.widgets import DataTable
|
|
10
|
+
|
|
11
|
+
from iolanta.facets.facet import Facet
|
|
12
|
+
from iolanta.facets.page_title import PageTitle
|
|
13
|
+
from iolanta.models import NotLiteralNode
|
|
14
|
+
from iolanta.namespaces import DATATYPES
|
|
15
|
+
from iolanta.widgets.mixin import IolantaWidgetMixin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GraphRow(NamedTuple):
|
|
19
|
+
"""A row in the graphs table."""
|
|
20
|
+
|
|
21
|
+
graph: NotLiteralNode
|
|
22
|
+
count: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GraphsTable(IolantaWidgetMixin, DataTable):
|
|
26
|
+
"""Render graphs as a table with graph URI and triple count."""
|
|
27
|
+
|
|
28
|
+
BINDINGS = [
|
|
29
|
+
('enter', 'goto', 'Goto'),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
def __init__(self, rows: Iterable[GraphRow]):
|
|
33
|
+
"""Construct."""
|
|
34
|
+
super().__init__(show_header=True, cell_padding=1)
|
|
35
|
+
self.graph_rows = list(rows)
|
|
36
|
+
|
|
37
|
+
def render_human_readable_cells(self):
|
|
38
|
+
"""Replace the cells with their human readable titles."""
|
|
39
|
+
terms_and_coordinates = sorted(
|
|
40
|
+
self.node_to_coordinates.items(),
|
|
41
|
+
key=lambda node_and_coordinates_pair: len(
|
|
42
|
+
node_and_coordinates_pair[1],
|
|
43
|
+
),
|
|
44
|
+
reverse=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
for term, coordinates in terms_and_coordinates:
|
|
48
|
+
title = str(self.iolanta.render(term, as_datatype=DATATYPES.title))
|
|
49
|
+
for coordinate in coordinates:
|
|
50
|
+
self.app.call_from_thread(
|
|
51
|
+
self.update_cell_at,
|
|
52
|
+
coordinate,
|
|
53
|
+
value=title,
|
|
54
|
+
update_width=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@funcy.cached_property
|
|
58
|
+
@funcy.post_processing(dict)
|
|
59
|
+
def coordinate_to_node(self):
|
|
60
|
+
"""Return a mapping of coordinates to their corresponding nodes."""
|
|
61
|
+
for row_number, (graph, _count) in enumerate(self.graph_rows):
|
|
62
|
+
yield Coordinate(row_number, 0), graph
|
|
63
|
+
|
|
64
|
+
@funcy.cached_property
|
|
65
|
+
def node_to_coordinates(self) -> defaultdict[Node, list[Coordinate]]:
|
|
66
|
+
"""Map node to coordinates where it appears."""
|
|
67
|
+
node_to_coordinate = [
|
|
68
|
+
(node, coordinate)
|
|
69
|
+
for coordinate, node in self.coordinate_to_node.items()
|
|
70
|
+
]
|
|
71
|
+
return funcy.group_values(node_to_coordinate)
|
|
72
|
+
|
|
73
|
+
def format_as_loading(self, node: Node) -> str:
|
|
74
|
+
"""Intermediate version of a value while it is loading."""
|
|
75
|
+
if isinstance(node, Literal):
|
|
76
|
+
node_text = f'⌛ {node}'
|
|
77
|
+
else:
|
|
78
|
+
node_text = self.iolanta.node_as_qname(node)
|
|
79
|
+
node_text = f'⌛ {node_text}'
|
|
80
|
+
|
|
81
|
+
return node_text
|
|
82
|
+
|
|
83
|
+
def on_mount(self):
|
|
84
|
+
"""Fill the table and start rendering."""
|
|
85
|
+
self.add_columns('Graph', 'Triples Count')
|
|
86
|
+
self.cell_padding = 1
|
|
87
|
+
|
|
88
|
+
for graph, count in self.graph_rows:
|
|
89
|
+
self.add_row(
|
|
90
|
+
self.format_as_loading(graph),
|
|
91
|
+
str(count),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
self.run_worker(
|
|
95
|
+
self.render_human_readable_cells,
|
|
96
|
+
thread=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def action_goto(self):
|
|
100
|
+
"""Navigate to the selected graph."""
|
|
101
|
+
if self.cursor_coordinate:
|
|
102
|
+
node = self.coordinate_to_node.get(self.cursor_coordinate)
|
|
103
|
+
if node is not None:
|
|
104
|
+
self.app.action_goto(node)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class GraphsBody(Vertical):
|
|
108
|
+
"""Container for graphs table."""
|
|
109
|
+
|
|
110
|
+
DEFAULT_CSS = """
|
|
111
|
+
GraphsBody {
|
|
112
|
+
height: auto;
|
|
113
|
+
max-height: 100%;
|
|
114
|
+
}
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Graphs(Facet):
|
|
119
|
+
"""Render named graphs as a table."""
|
|
120
|
+
|
|
121
|
+
META = Path(__file__).parent / 'data' / 'textual_graphs.yamlld'
|
|
122
|
+
|
|
123
|
+
def show(self):
|
|
124
|
+
"""Construct the table."""
|
|
125
|
+
rows = self.stored_query('graphs.sparql')
|
|
126
|
+
|
|
127
|
+
return GraphsBody(
|
|
128
|
+
PageTitle(self.this),
|
|
129
|
+
GraphsTable(
|
|
130
|
+
[
|
|
131
|
+
GraphRow(
|
|
132
|
+
graph=row['graph'],
|
|
133
|
+
count=int(row['count'].value),
|
|
134
|
+
)
|
|
135
|
+
for row in rows
|
|
136
|
+
],
|
|
137
|
+
),
|
|
138
|
+
)
|
|
@@ -11,13 +11,13 @@ class TextualLinkFacet(Facet[str | Text]):
|
|
|
11
11
|
|
|
12
12
|
def show(self) -> str | Text:
|
|
13
13
|
"""Render the link, or literal text, whatever."""
|
|
14
|
-
if isinstance(self.
|
|
15
|
-
return f'[b grey37]{self.
|
|
14
|
+
if isinstance(self.this, Literal):
|
|
15
|
+
return f'[b grey37]{self.this}[/b grey37]'
|
|
16
16
|
|
|
17
17
|
label = self.render(
|
|
18
|
-
self.
|
|
18
|
+
self.this,
|
|
19
19
|
as_datatype=DATATYPES.title,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
-
invocation = f"app.goto('{self.
|
|
22
|
+
invocation = f"app.goto('{self.this}')"
|
|
23
23
|
return f'[@click="{invocation}"]{label}[/]'
|
|
@@ -12,6 +12,7 @@ from iolanta.widgets.description import Description
|
|
|
12
12
|
TEXT = """
|
|
13
13
|
**😕 Iolanta is unable to visualize this resource**
|
|
14
14
|
|
|
15
|
+
* The reference type ({reference_type}) might be incorrect;
|
|
15
16
|
* The URI might be incorrect;
|
|
16
17
|
* Or, no edges might exist which involve it;
|
|
17
18
|
* Or maybe Iolanta does not know of such edges.
|
|
@@ -44,7 +45,7 @@ class TextualNoFacetFound(Facet):
|
|
|
44
45
|
@property
|
|
45
46
|
def raw_content(self):
|
|
46
47
|
"""Content of the file, if applicable."""
|
|
47
|
-
url = URL(self.
|
|
48
|
+
url = URL(self.this)
|
|
48
49
|
if url.scheme != 'file':
|
|
49
50
|
return None
|
|
50
51
|
|
|
@@ -94,6 +95,7 @@ class TextualNoFacetFound(Facet):
|
|
|
94
95
|
TEXT.format(
|
|
95
96
|
content=self.raw_content or '',
|
|
96
97
|
subgraphs=self.subgraphs_description or '',
|
|
98
|
+
reference_type=type(self.this).__name__,
|
|
97
99
|
),
|
|
98
100
|
),
|
|
99
101
|
),
|
|
@@ -48,7 +48,7 @@ class OntologyFacet(Facet[Widget]):
|
|
|
48
48
|
@cached_property
|
|
49
49
|
def grouped_terms(self) -> dict[NotLiteralNode | None, list[TermAndStatus]]:
|
|
50
50
|
"""Group terms by VANN categories."""
|
|
51
|
-
rows = self.stored_query('terms.sparql', iri=self.
|
|
51
|
+
rows = self.stored_query('terms.sparql', iri=self.this)
|
|
52
52
|
grouped = [
|
|
53
53
|
(
|
|
54
54
|
row.get('group'),
|
|
@@ -80,7 +80,7 @@ class OntologyFacet(Facet[Widget]):
|
|
|
80
80
|
|
|
81
81
|
vocabs = funcy.lpluck(
|
|
82
82
|
'vocab',
|
|
83
|
-
self.stored_query('visualization-vocab.sparql', iri=self.
|
|
83
|
+
self.stored_query('visualization-vocab.sparql', iri=self.this),
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
for vocab in vocabs:
|
|
@@ -94,7 +94,7 @@ class OntologyFacet(Facet[Widget]):
|
|
|
94
94
|
)
|
|
95
95
|
|
|
96
96
|
return Vertical(
|
|
97
|
-
PageTitle(self.
|
|
97
|
+
PageTitle(self.this),
|
|
98
98
|
TermsContent(
|
|
99
99
|
Static(
|
|
100
100
|
Columns(
|
iolanta/facets/title/facets.py
CHANGED
|
@@ -12,14 +12,14 @@ class WikibaseStatementTitle(Facet[str]):
|
|
|
12
12
|
"""Render the title."""
|
|
13
13
|
rows = self.stored_query(
|
|
14
14
|
'statement-title.sparql',
|
|
15
|
-
statement=self.
|
|
15
|
+
statement=self.this,
|
|
16
16
|
language=self.language,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
row = funcy.first(rows)
|
|
20
20
|
if not row:
|
|
21
21
|
return self.render(
|
|
22
|
-
self.
|
|
22
|
+
self.this,
|
|
23
23
|
as_datatype=URIRef('https://iolanta.tech/qname'),
|
|
24
24
|
)
|
|
25
25
|
|
iolanta/iolanta.py
CHANGED
|
@@ -292,7 +292,7 @@ class Iolanta: # noqa: WPS214
|
|
|
292
292
|
facet_class = self.facet_resolver.resolve(found['facet'])
|
|
293
293
|
|
|
294
294
|
facet = facet_class(
|
|
295
|
-
|
|
295
|
+
this=node,
|
|
296
296
|
iolanta=self,
|
|
297
297
|
as_datatype=found['output_datatype'],
|
|
298
298
|
)
|
|
@@ -328,7 +328,7 @@ class Iolanta: # noqa: WPS214
|
|
|
328
328
|
|
|
329
329
|
facet_instances = [
|
|
330
330
|
facet_class(
|
|
331
|
-
|
|
331
|
+
this=node,
|
|
332
332
|
iolanta=self,
|
|
333
333
|
as_datatype=output_datatype,
|
|
334
334
|
)
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"@context":
|
|
2
|
+
"@import": https://json-ld.org/contexts/dollar-convenience.jsonld
|
|
3
|
+
vann: https://purl.org/vocab/vann/
|
|
4
|
+
foaf: https://xmlns.com/foaf/0.1/
|
|
5
|
+
owl: https://www.w3.org/2002/07/owl#
|
|
6
|
+
iolanta: https://iolanta.tech/
|
|
7
|
+
rdfs: "https://www.w3.org/2000/01/rdf-schema#"
|
|
8
|
+
rdf: https://www.w3.org/1999/02/22-rdf-syntax-ns#
|
|
9
|
+
dcterms: https://purl.org/dc/terms/
|
|
10
|
+
dcam: https://purl.org/dc/dcam/
|
|
11
|
+
|
|
12
|
+
iolanta:outputs:
|
|
13
|
+
"@type": "@id"
|
|
14
|
+
|
|
15
|
+
iolanta:when-no-facet-found:
|
|
16
|
+
"@type": "@id"
|
|
17
|
+
|
|
18
|
+
$: rdfs:label
|
|
19
|
+
→:
|
|
20
|
+
"@type": "@id"
|
|
21
|
+
"@id": iolanta:outputs
|
|
22
|
+
|
|
23
|
+
⊆:
|
|
24
|
+
"@type": "@id"
|
|
25
|
+
"@id": rdfs:subClassOf
|
|
26
|
+
|
|
27
|
+
⪯:
|
|
28
|
+
"@type": "@id"
|
|
29
|
+
"@id": iolanta:is-preferred-over
|
|
30
|
+
|
|
31
|
+
↦: iolanta:matches
|
|
32
|
+
iolanta:hasDefaultFacet:
|
|
33
|
+
"@type": "@id"
|
|
34
|
+
|
|
35
|
+
$id: pkg:pypi/iolanta#labeled-triple-set
|
|
36
|
+
$: Labeled Triple Set
|
|
37
|
+
→:
|
|
38
|
+
$id: https://iolanta.tech/datatypes/labeled-triple-set
|
|
39
|
+
$: Labeled Triple Set
|
|
40
|
+
↦:
|
|
41
|
+
- ASK WHERE { GRAPH $this { ?s ?p ?o } }
|
|
42
|
+
- ASK WHERE { $this <https://iolanta.tech/has-sub-graph> ?subgraph }
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated, Iterable
|
|
3
|
+
from typing import Literal as TypingLiteral
|
|
4
|
+
|
|
5
|
+
from pydantic import (
|
|
6
|
+
AnyUrl,
|
|
7
|
+
BaseModel,
|
|
8
|
+
Field,
|
|
9
|
+
TypeAdapter,
|
|
10
|
+
field_serializer,
|
|
11
|
+
field_validator,
|
|
12
|
+
validator,
|
|
13
|
+
)
|
|
14
|
+
from rdflib import BNode, Literal, Node, URIRef
|
|
15
|
+
|
|
16
|
+
from iolanta import Facet
|
|
17
|
+
from iolanta.models import NotLiteralNode
|
|
18
|
+
from iolanta.namespaces import DATATYPES
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WithFeedback(BaseModel):
|
|
22
|
+
feedback: Annotated[list[str], Field(default_factory=list)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LabeledURI(WithFeedback, BaseModel):
|
|
26
|
+
type: TypingLiteral['uri'] = 'uri'
|
|
27
|
+
uri: AnyUrl
|
|
28
|
+
label: str
|
|
29
|
+
|
|
30
|
+
def construct_feedback(self) -> Iterable[str]:
|
|
31
|
+
if str(self.label) == str(self.uri):
|
|
32
|
+
yield (
|
|
33
|
+
'For this URI, the label is the same as the URI. We were '
|
|
34
|
+
'unable to render that URI.'
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LabeledBlank(WithFeedback, BaseModel):
|
|
39
|
+
type: TypingLiteral['blank'] = 'blank'
|
|
40
|
+
identifier: str
|
|
41
|
+
label: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LabeledLiteral(WithFeedback, BaseModel):
|
|
45
|
+
type: TypingLiteral['literal'] = 'literal'
|
|
46
|
+
value: str
|
|
47
|
+
language: str | None
|
|
48
|
+
datatype: str | None
|
|
49
|
+
label: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LabeledTriple(BaseModel):
|
|
53
|
+
subject: LabeledURI | LabeledBlank
|
|
54
|
+
predicate: LabeledURI
|
|
55
|
+
object_: LabeledURI | LabeledBlank | LabeledLiteral
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def construct_uri_feedback(uri: AnyUrl, label: str) -> Iterable[str]:
|
|
59
|
+
if str(uri) == str(label):
|
|
60
|
+
yield (
|
|
61
|
+
'For this URI, the label is the same as the URI. We were '
|
|
62
|
+
'unable to render that URI.'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def construct_blank_feedback(bnode, label) -> Iterable[str]:
|
|
67
|
+
if str(bnode) == str(label):
|
|
68
|
+
yield (
|
|
69
|
+
'For this blank node, the label is the same as the blank node. '
|
|
70
|
+
'We were unable to render that blank node.'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def construct_literal_feedback(literal, label):
|
|
75
|
+
if label.startswith('http'):
|
|
76
|
+
yield (
|
|
77
|
+
'This RDF literal seems to be actually a URL. Good chance is '
|
|
78
|
+
'that it should not be a literal.'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
elif ':' in label[:5]:
|
|
82
|
+
yield (
|
|
83
|
+
'This RDF literal seems to be actually a QName (prefixed URI). '
|
|
84
|
+
'Good chance is that it should not be a literal.'
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class LabeledTripleSet(Facet[list[LabeledTriple]]):
|
|
89
|
+
"""A set of labeled triples."""
|
|
90
|
+
|
|
91
|
+
META = Path(__file__).parent / 'data' / 'labeled_triple_set.yamlld'
|
|
92
|
+
|
|
93
|
+
def render_label(self, node: NotLiteralNode) -> str:
|
|
94
|
+
return self.render(node, as_datatype=DATATYPES.title)
|
|
95
|
+
|
|
96
|
+
def parse_term(self, term: Node):
|
|
97
|
+
match term:
|
|
98
|
+
case URIRef() as uriref:
|
|
99
|
+
uri = AnyUrl(uriref)
|
|
100
|
+
label = self.render_label(uriref)
|
|
101
|
+
|
|
102
|
+
return LabeledURI(
|
|
103
|
+
uri=uri,
|
|
104
|
+
label=label,
|
|
105
|
+
feedback=list(construct_uri_feedback(uri=uri, label=label)),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
case BNode() as bnode:
|
|
109
|
+
label = self.render_label(bnode)
|
|
110
|
+
return LabeledBlank(
|
|
111
|
+
identifier=bnode,
|
|
112
|
+
label=label,
|
|
113
|
+
feedback=list(construct_blank_feedback(bnode=bnode, label=label)),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
case Literal() as literal:
|
|
117
|
+
label = self.render_label(literal)
|
|
118
|
+
return LabeledLiteral(
|
|
119
|
+
value=literal,
|
|
120
|
+
language=literal.language,
|
|
121
|
+
datatype=literal.datatype,
|
|
122
|
+
label=label,
|
|
123
|
+
feedback=list(construct_literal_feedback(literal=literal, label=label)),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def show(self):
|
|
127
|
+
rows = self.stored_query('triples.sparql', graph=self.this)
|
|
128
|
+
triples = [
|
|
129
|
+
LabeledTriple(
|
|
130
|
+
subject=self.parse_term(row['subject']),
|
|
131
|
+
predicate=self.parse_term(row['predicate']),
|
|
132
|
+
object_=self.parse_term(row['object']),
|
|
133
|
+
)
|
|
134
|
+
for row in rows
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
return TypeAdapter(list[LabeledTriple]).dump_json(triples, indent=2).decode()
|
iolanta/mcp/__init__.py
ADDED
|
File without changes
|
iolanta/mcp/cli.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from fastmcp import FastMCP
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from iolanta.cli.main import render_and_return
|
|
6
|
+
|
|
7
|
+
mcp = FastMCP("Iolanta MCP Server")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
def render_uri(
|
|
12
|
+
uri: Annotated[str, 'URL, or file system path, to render'],
|
|
13
|
+
as_format: Annotated[str, 'Format to render as. Examples: `labeled-triple-set`, `mermaid`']
|
|
14
|
+
) -> str:
|
|
15
|
+
"""Render a URI."""
|
|
16
|
+
result = render_and_return(uri, as_format)
|
|
17
|
+
return str(result)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp.prompt(description="How to author Linked Data with Iolanta")
|
|
21
|
+
def ld_authoring_rules() -> str:
|
|
22
|
+
"""How to author Linked Data with Iolanta."""
|
|
23
|
+
rules_path = Path(__file__).parent / 'prompts' / 'rules.md'
|
|
24
|
+
return rules_path.read_text()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@mcp.prompt(description="How to author nanopublication assertions with Iolanta")
|
|
28
|
+
def nanopublication_assertion_authoring_rules() -> str:
|
|
29
|
+
"""How to author nanopublication assertions with Iolanta."""
|
|
30
|
+
rules_path = Path(__file__).parent / 'prompts' / 'nanopublication_assertion_authoring_rules.md'
|
|
31
|
+
return rules_path.read_text()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def app():
|
|
35
|
+
mcp.run()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
app()
|