iolanta 1.2.7__py3-none-any.whl → 1.2.9__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
@@ -93,18 +93,22 @@ def render_command( # noqa: WPS231, WPS238, WPS210, C901
93
93
  enqueue=True,
94
94
  )
95
95
 
96
- iolanta: Iolanta = Iolanta(
97
- language=Literal(language),
98
- logger=logger,
99
- )
100
-
101
96
  node_url = URL(url)
102
97
  if node_url.scheme:
103
98
  node = URIRef(url)
99
+
100
+ iolanta: Iolanta = Iolanta(
101
+ language=Literal(language),
102
+ logger=logger,
103
+ )
104
104
  else:
105
105
  path = Path(url).absolute()
106
106
  node = URIRef(f'file://{path}')
107
- iolanta.add(path)
107
+ iolanta: Iolanta = Iolanta(
108
+ language=Literal(language),
109
+ logger=logger,
110
+ project_root=path,
111
+ )
108
112
 
109
113
  try:
110
114
  renderable, stack = iolanta.render(
@@ -131,6 +131,10 @@ def _extract_from_mapping( # noqa: WPS213
131
131
  yield from extract_mentioned_urls(algebra['p2'])
132
132
  yield from extract_mentioned_urls(algebra['expr'])
133
133
 
134
+ case 'Join':
135
+ yield from extract_mentioned_urls(algebra['p1'])
136
+ yield from extract_mentioned_urls(algebra['p2'])
137
+
134
138
  case 'ConditionalOrExpression' | 'ConditionalAndExpression':
135
139
  yield from extract_mentioned_urls(algebra['expr'])
136
140
  yield from extract_mentioned_urls(algebra['other'])
@@ -149,10 +153,8 @@ def _extract_from_mapping( # noqa: WPS213
149
153
  case unknown_name:
150
154
  formatted_keys = ', '.join(algebra.keys())
151
155
  loguru.logger.error(
152
- 'Unknown SPARQL expression %s(%s): %s',
153
- unknown_name,
154
- formatted_keys,
155
- algebra,
156
+ 'Unknown SPARQL expression '
157
+ f'{unknown_name}({formatted_keys}): {algebra}',
156
158
  )
157
159
  return
158
160
 
@@ -422,7 +424,7 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
422
424
  and isinstance(self.load(maybe_iri), Loaded)
423
425
  ):
424
426
  is_anything_loaded = True # noqa: WPS220
425
- self.logger.warning( # noqa: WPS220
427
+ self.logger.info( # noqa: WPS220
426
428
  'Newly loaded: {uri}',
427
429
  uri=maybe_iri,
428
430
  )
@@ -430,6 +432,25 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
430
432
  query_result['bindings'] = bindings
431
433
  return query_result
432
434
 
435
+ def _is_loaded(self, uri: URIRef) -> bool:
436
+ """Find out if this URI in the graph already."""
437
+ return funcy.first(
438
+ self.graph.quads((
439
+ uri,
440
+ IOLANTA['last-loaded-time'],
441
+ None,
442
+ URIRef('iolanta://_meta'),
443
+ )),
444
+ ) is not None
445
+
446
+ def _mark_as_loaded(self, uri: URIRef):
447
+ self.graph.add((
448
+ uri,
449
+ IOLANTA['last-loaded-time'],
450
+ Literal(datetime.datetime.now()),
451
+ URIRef('iolanta://_meta'),
452
+ ))
453
+
433
454
  def load( # noqa: C901, WPS210, WPS212, WPS213, WPS231
434
455
  self,
435
456
  source: URIRef,
@@ -446,25 +467,23 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
446
467
  # fails.
447
468
  return Skipped()
448
469
 
470
+ if url.fragment:
471
+ # Fragment on an HTML page resolves to that same page. Let us remove
472
+ # this ambiguity, then.
473
+ # TODO: It works differently for JSON-LD documents AFAIK. Need to
474
+ # double check that.
475
+ url = url.with_fragment(None)
476
+ source = URIRef(str(url))
477
+
449
478
  new_source = self._apply_redirect(source)
450
479
  if new_source != source:
451
480
  return self.load(new_source)
452
481
 
453
482
  source_uri = normalize_term(source)
454
- existing_triple = funcy.first(
455
- self.graph.quads(
456
- (
457
- None,
458
- None,
459
- None,
460
- source_uri,
461
- ),
462
- ),
463
- )
464
- if existing_triple is not None:
483
+ if self._is_loaded(source_uri):
465
484
  return Skipped()
466
485
  else:
467
- self.logger.warning(f'Existing triples not found for {source_uri}')
486
+ self.logger.info(f'{source_uri} is not loaded yet')
468
487
 
469
488
  # FIXME This is definitely inefficient. However, python-yaml-ld caches
470
489
  # the document, so the performance overhead is not super high.
@@ -508,6 +527,8 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
508
527
  RDFS.Class,
509
528
  ))
510
529
 
530
+ self._mark_as_loaded(source_uri)
531
+
511
532
  return Loaded()
512
533
 
513
534
  except Exception as err:
@@ -531,6 +552,9 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
531
552
  RDF.type,
532
553
  RDFS.Class,
533
554
  ))
555
+
556
+ self._mark_as_loaded(source_uri)
557
+
534
558
  return Loaded()
535
559
 
536
560
  if _resolved_source:
@@ -555,6 +579,8 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
555
579
  RDFS.Class,
556
580
  ))
557
581
 
582
+ self._mark_as_loaded(source_uri)
583
+
558
584
  try: # noqa: WPS225
559
585
  ld_rdf = yaml_ld.to_rdf(source)
560
586
  except ConnectionError as name_resolution_error:
@@ -565,10 +591,10 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
565
591
  )
566
592
  return Loaded()
567
593
  except ParserNotFound as parser_not_found:
568
- self.logger.info('%s | %s', source, str(parser_not_found))
594
+ self.logger.info(f'{source} | {parser_not_found}')
569
595
  return Loaded()
570
596
  except YAMLLDError as yaml_ld_error:
571
- self.logger.error('%s | %s', source, str(yaml_ld_error))
597
+ self.logger.error(f'{source} | {yaml_ld_error}')
572
598
  return Loaded()
573
599
 
574
600
  try:
@@ -22,8 +22,7 @@ class PageTitle(IolantaWidgetMixin, Static):
22
22
  """Initialize."""
23
23
  self.iri = iri
24
24
  self.extra = extra
25
- qname = self.iolanta.node_as_qname(iri)
26
- super().__init__(qname)
25
+ super().__init__(iri)
27
26
 
28
27
  def construct_title(self):
29
28
  """Render the title via Iolanta in a thread."""
@@ -4,6 +4,7 @@ from concurrent.futures import ThreadPoolExecutor
4
4
  from rdflib.term import Node
5
5
  from rich.console import RenderableType
6
6
  from textual.app import App, ComposeResult
7
+ from textual.css.query import NoMatches
7
8
  from textual.widgets import Footer, Header
8
9
 
9
10
  from iolanta.facets.textual_browser.page_switcher import (
@@ -13,6 +14,8 @@ from iolanta.facets.textual_browser.page_switcher import (
13
14
  )
14
15
  from iolanta.iolanta import Iolanta
15
16
 
17
+ POPUP_TIMEOUT = 30 # seconds
18
+
16
19
 
17
20
  class DevConsoleHandler(logging.Handler):
18
21
  """Pipe log output → dev console."""
@@ -28,6 +31,17 @@ class DevConsoleHandler(logging.Handler):
28
31
  self.console.write(message)
29
32
 
30
33
 
34
+ def _log_message_to_dev_console(app: App):
35
+ """Log a message to the dev console."""
36
+ def log_message_to_dev_console(message: str): # noqa: WPS430
37
+ try:
38
+ app.query_one(DevConsole).write(message)
39
+ except NoMatches:
40
+ return
41
+
42
+ return log_message_to_dev_console
43
+
44
+
31
45
  class IolantaBrowser(App): # noqa: WPS214, WPS230
32
46
  """Browse Linked Data."""
33
47
 
@@ -69,11 +83,21 @@ class IolantaBrowser(App): # noqa: WPS214, WPS230
69
83
 
70
84
  # Log to the dev console.
71
85
  self.iolanta.logger.add(
72
- lambda msg: self.query_one(DevConsole).write(msg),
86
+ _log_message_to_dev_console(self),
73
87
  level='INFO',
74
88
  format='{time} {level} {message}',
75
89
  )
76
90
 
91
+ self.iolanta.logger.add(
92
+ lambda msg: self.notify(
93
+ msg,
94
+ severity='warning',
95
+ timeout=POPUP_TIMEOUT,
96
+ ),
97
+ level='WARNING',
98
+ format='{message}',
99
+ )
100
+
77
101
  def action_toggle_dark(self) -> None:
78
102
  """Toggle dark mode."""
79
103
  self.dark = not self.dark
@@ -1,5 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
 
3
+ from rdflib import URIRef
4
+
3
5
 
4
6
  @dataclass
5
7
  class Location:
@@ -7,3 +9,4 @@ class Location:
7
9
 
8
10
  page_id: str
9
11
  url: str
12
+ facet_iri: URIRef | None = None
@@ -1,6 +1,10 @@
1
1
  import functools
2
+ import threading
2
3
  import uuid
4
+ from dataclasses import dataclass
5
+ from typing import Any
3
6
 
7
+ import watchfiles
4
8
  from rdflib import BNode, URIRef
5
9
  from textual.widgets import ContentSwitcher, RichLog
6
10
  from textual.worker import Worker, WorkerState
@@ -18,6 +22,21 @@ from iolanta.namespaces import DATATYPES
18
22
  from iolanta.widgets.mixin import IolantaWidgetMixin
19
23
 
20
24
 
25
+ @dataclass
26
+ class RenderResult:
27
+ """
28
+ We asked a thread to render something for us.
29
+
30
+ This is what did we get back.
31
+ """
32
+
33
+ iri: NotLiteralNode
34
+ renderable: Any
35
+ flip_options: list[FlipOption]
36
+ facet_iri: URIRef
37
+ is_reload: bool
38
+
39
+
21
40
  class PageSwitcher(IolantaWidgetMixin, ContentSwitcher): # noqa: WPS214
22
41
  """
23
42
  Container for open pages.
@@ -28,12 +47,15 @@ class PageSwitcher(IolantaWidgetMixin, ContentSwitcher): # noqa: WPS214
28
47
  BINDINGS = [ # noqa: WPS115
29
48
  ('alt+left', 'back', 'Back'),
30
49
  ('alt+right', 'forward', 'Fwd'),
50
+ ('f5', 'reload', '🔄 Reload'),
51
+ ('escape', 'abort', '🛑 Abort'),
31
52
  ('f12', 'console', 'Console'),
32
53
  ]
33
54
 
34
55
  def __init__(self):
35
56
  """Set Home as first tab."""
36
57
  super().__init__(id='page_switcher', initial='home')
58
+ self.stop_file_watcher_event = threading.Event()
37
59
 
38
60
  def action_console(self):
39
61
  """Open dev console."""
@@ -53,22 +75,39 @@ class PageSwitcher(IolantaWidgetMixin, ContentSwitcher): # noqa: WPS214
53
75
  def on_mount(self):
54
76
  """Navigate to the initial page."""
55
77
  self.action_goto(self.app.iri)
78
+ if self.iolanta.project_root:
79
+ self.run_worker(
80
+ self._watch_files,
81
+ thread=True,
82
+ )
83
+
84
+ def on_unmount(self) -> None:
85
+ """Stop watching files."""
86
+ self.stop_file_watcher_event.set()
87
+
88
+ def _watch_files(self):
89
+ for _ in watchfiles.watch( # noqa: WPS352
90
+ self.iolanta.project_root,
91
+ stop_event=self.stop_file_watcher_event,
92
+ ):
93
+ self.app.call_from_thread(self.action_reload)
56
94
 
57
95
  def render_iri( # noqa: WPS210
58
- self, destination: NotLiteralNode, facet_iri: URIRef | None,
59
- ):
96
+ self,
97
+ destination: NotLiteralNode,
98
+ facet_iri: URIRef | None,
99
+ is_reload: bool,
100
+ ) -> RenderResult:
60
101
  """Render an IRI in a thread."""
61
102
  self.iri = destination
62
103
  iolanta: Iolanta = self.iolanta
63
104
 
64
105
  as_datatype = URIRef('https://iolanta.tech/cli/textual')
65
- choices = self.app.call_from_thread(
66
- FacetFinder(
67
- iolanta=self.iolanta,
68
- node=destination,
69
- as_datatype=as_datatype,
70
- ).choices,
71
- )
106
+ choices = FacetFinder(
107
+ iolanta=self.iolanta,
108
+ node=destination,
109
+ as_datatype=as_datatype,
110
+ ).choices()
72
111
 
73
112
  if not choices:
74
113
  raise FacetNotFound(
@@ -106,11 +145,7 @@ class PageSwitcher(IolantaWidgetMixin, ContentSwitcher): # noqa: WPS214
106
145
  )
107
146
 
108
147
  try:
109
- return (
110
- destination,
111
- self.app.call_from_thread(facet.show),
112
- flip_options,
113
- )
148
+ renderable = facet.show()
114
149
 
115
150
  except Exception as err:
116
151
  raise FacetError(
@@ -119,6 +154,14 @@ class PageSwitcher(IolantaWidgetMixin, ContentSwitcher): # noqa: WPS214
119
154
  error=err,
120
155
  ) from err
121
156
 
157
+ return RenderResult(
158
+ iri=destination,
159
+ renderable=renderable,
160
+ flip_options=flip_options,
161
+ facet_iri=facet_iri,
162
+ is_reload=is_reload,
163
+ )
164
+
122
165
  def on_worker_state_changed( # noqa: WPS210
123
166
  self,
124
167
  event: Worker.StateChanged,
@@ -126,24 +169,98 @@ class PageSwitcher(IolantaWidgetMixin, ContentSwitcher): # noqa: WPS214
126
169
  """Render a page as soon as it is ready."""
127
170
  match event.state:
128
171
  case WorkerState.SUCCESS:
129
- iri, renderable, flip_options = event.worker.result
130
- page_uid = uuid.uuid4().hex
131
- page_id = f'page_{page_uid}'
132
- page = Page(
133
- renderable,
134
- iri=iri,
135
- page_id=page_id,
136
- flip_options=flip_options,
137
- )
138
- self.mount(page)
139
- self.current = page_id
140
- page.focus()
141
- self.history.goto(Location(page_id, iri))
142
- self.app.sub_title = iri
172
+ render_result: RenderResult = event.worker.result
173
+
174
+ if render_result.is_reload:
175
+ # We are reloading the current page.
176
+ current_page = self.query_one(f'#{self.current}', Page)
177
+ current_page.remove_children()
178
+ current_page.mount(render_result.renderable)
179
+
180
+ # FIXME: This does not actually change the flip options,
181
+ # but maybe that's okay
182
+ current_page.flip_options = render_result.flip_options
183
+
184
+ else:
185
+ # We are navigating to a new page.
186
+ page_uid = uuid.uuid4().hex
187
+ page_id = f'page_{page_uid}'
188
+ page = Page(
189
+ render_result.renderable,
190
+ iri=render_result.iri,
191
+ page_id=page_id,
192
+ flip_options=render_result.flip_options,
193
+ )
194
+ self.mount(page)
195
+ self.current = page_id
196
+ page.focus()
197
+ self.history.goto(
198
+ Location(
199
+ page_id,
200
+ url=render_result.iri,
201
+ facet_iri=render_result.facet_iri,
202
+ ),
203
+ )
204
+ self.app.sub_title = render_result.iri
143
205
 
144
206
  case WorkerState.ERROR:
145
207
  raise ValueError(event)
146
208
 
209
+ @property
210
+ def is_loading(self) -> bool:
211
+ """Determine if the app is presently loading something."""
212
+ for worker in self.workers:
213
+ if worker.name == 'render_iri':
214
+ return True
215
+
216
+ return False
217
+
218
+ def action_reload(self):
219
+ """Reset Iolanta graph and re-render current view."""
220
+ self.iolanta.reset()
221
+
222
+ self.run_worker(
223
+ functools.partial(
224
+ self.render_iri,
225
+ destination=self.history.current.url,
226
+ facet_iri=self.history.current.facet_iri,
227
+ is_reload=True,
228
+ ),
229
+ thread=True,
230
+ exclusive=True,
231
+ name='render_iri',
232
+ )
233
+ self.refresh_bindings()
234
+
235
+ def action_abort(self):
236
+ """Abort loading."""
237
+ self.notify(
238
+ 'Aborted.',
239
+ severity='warning',
240
+ )
241
+
242
+ for worker in self.workers:
243
+ if worker.name == 'render_iri':
244
+ worker.cancel()
245
+ break
246
+
247
+ self.refresh_bindings()
248
+
249
+ def check_action(
250
+ self,
251
+ action: str,
252
+ parameters: tuple[object, ...], # noqa: WPS110
253
+ ) -> bool | None:
254
+ """Check if action is available."""
255
+ is_loading = self.is_loading
256
+ match action:
257
+ case 'reload':
258
+ return not is_loading
259
+ case 'abort':
260
+ return is_loading
261
+
262
+ return True
263
+
147
264
  def action_goto(
148
265
  self,
149
266
  destination: str,
@@ -158,19 +275,25 @@ class PageSwitcher(IolantaWidgetMixin, ContentSwitcher): # noqa: WPS214
158
275
  self.run_worker(
159
276
  functools.partial(
160
277
  self.render_iri,
161
- iri,
162
- facet_iri and URIRef(facet_iri),
278
+ destination=iri,
279
+ facet_iri=facet_iri and URIRef(facet_iri),
280
+ is_reload=False,
163
281
  ),
164
282
  thread=True,
283
+ exclusive=True,
284
+ name='render_iri',
165
285
  )
286
+ self.refresh_bindings()
166
287
 
167
288
  def action_back(self):
168
289
  """Go backward."""
169
290
  self.current = self.history.back().page_id
291
+ self.focus()
170
292
 
171
293
  def action_forward(self):
172
294
  """Go forward."""
173
295
  self.current = self.history.forward().page_id
296
+ self.focus()
174
297
 
175
298
 
176
299
  class ConsoleSwitcher(ContentSwitcher):
@@ -58,6 +58,7 @@ class TextualDefaultFacet(Facet[Widget]): # noqa: WPS214
58
58
  for property_iri, property_values in self.grouped_properties.items():
59
59
  property_name = PropertyName(
60
60
  iri=property_iri,
61
+ qname=self.iolanta.node_as_qname(property_iri),
61
62
  )
62
63
 
63
64
  property_values = [
@@ -69,6 +70,7 @@ class TextualDefaultFacet(Facet[Widget]): # noqa: WPS214
69
70
  property_value=property_value,
70
71
  subject=self.iri,
71
72
  property_iri=property_iri,
73
+ property_qname=self.iolanta.node_as_qname(property_iri),
72
74
  )
73
75
  for property_value in property_values
74
76
  ]
@@ -48,11 +48,11 @@ class PropertyName(Widget, can_focus=True, inherit_bindings=False):
48
48
  def __init__(
49
49
  self,
50
50
  iri: NotLiteralNode,
51
+ qname: str,
51
52
  ):
52
53
  """Set the IRI."""
53
54
  self.iri = iri
54
55
  super().__init__()
55
- qname = self.app.iolanta.node_as_qname(iri)
56
56
  self.renderable = Text( # noqa: WPS601
57
57
  f'⏳ {qname}',
58
58
  style='#696969',
@@ -130,17 +130,15 @@ class PropertyValue(Widget, can_focus=True, inherit_bindings=False):
130
130
  property_value: Node,
131
131
  subject: NotLiteralNode,
132
132
  property_iri: NotLiteralNode,
133
+ property_qname: str,
133
134
  ):
134
135
  """Initialize parameters for rendering, navigation, & provenance."""
135
136
  self.property_value = property_value
136
137
  self.subject = subject
137
138
  self.property_iri = property_iri
138
139
  super().__init__()
139
- qname = self.app.iolanta.node_as_qname( # noqa: WPS601
140
- property_value,
141
- )
142
140
  self.renderable = Text( # noqa: WPS601
143
- f'⏳ {qname}',
141
+ f'⏳ {property_qname}',
144
142
  style='#696969',
145
143
  )
146
144
 
@@ -80,18 +80,19 @@ class ProvenanceView(Vertical):
80
80
  """Build page structure."""
81
81
  yield Title('Provenan©e for a triple')
82
82
 
83
+ # TODO: Calculate QNames somehow.
83
84
  yield PropertyRow(
84
- PropertyName(RDF.subject),
85
+ PropertyName(RDF.subject, qname=str(RDF.subject)),
85
86
  RDFTermView(self.triple.subject),
86
87
  )
87
88
 
88
89
  yield PropertyRow(
89
- PropertyName(RDF.predicate),
90
+ PropertyName(RDF.predicate, qname=str(RDF.predicate)),
90
91
  RDFTermView(self.triple.predicate),
91
92
  )
92
93
 
93
94
  yield PropertyRow(
94
- PropertyName(RDF.object),
95
+ PropertyName(RDF.object, qname=str(RDF.object)),
95
96
  RDFTermView(self.triple.object),
96
97
  )
97
98
 
iolanta/iolanta.py CHANGED
@@ -2,6 +2,7 @@ import functools
2
2
  from dataclasses import dataclass, field, replace
3
3
  from pathlib import Path
4
4
  from typing import ( # noqa: WPS235
5
+ Annotated,
5
6
  Any,
6
7
  Dict,
7
8
  Iterable,
@@ -30,7 +31,6 @@ from iolanta.errors import UnresolvedIRI
30
31
  from iolanta.facets.errors import FacetError
31
32
  from iolanta.facets.facet import Facet
32
33
  from iolanta.facets.locator import FacetFinder
33
- from iolanta.loaders import Loader
34
34
  from iolanta.loaders.base import SourceType
35
35
  from iolanta.loaders.local_directory import merge_contexts
36
36
  from iolanta.models import (
@@ -45,7 +45,6 @@ from iolanta.parse_quads import parse_quads
45
45
  from iolanta.parsers.yaml import YAML
46
46
  from iolanta.plugin import Plugin
47
47
  from iolanta.resolvers.python_import import PythonImportResolver
48
- from iolanta.shortcuts import construct_root_loader
49
48
  from iolanta.stack import Stack
50
49
  from ldflex import LDFlex
51
50
 
@@ -72,19 +71,24 @@ class LoggerProtocol(Protocol):
72
71
  """Log a WARNING message."""
73
72
 
74
73
 
74
+ def _create_default_graph():
75
+ return ConjunctiveGraph(identifier=namespaces.LOCAL.term('_inference'))
76
+
77
+
75
78
  @dataclass
76
79
  class Iolanta: # noqa: WPS214
77
80
  """Iolanta is a Semantic web browser."""
78
81
 
79
82
  language: Literal = Literal('en')
80
83
 
81
- retrieval_directory: Optional[Path] = None
82
- graph: ConjunctiveGraph = field(
83
- default_factory=functools.partial(
84
- ConjunctiveGraph,
85
- identifier=namespaces.LOCAL.term('_inference'),
84
+ project_root: Annotated[
85
+ Path | None,
86
+ (
87
+ 'File or directory the contents of which '
88
+ 'Iolanta will automatically load into the graph.'
86
89
  ),
87
- )
90
+ ] = None
91
+ graph: ConjunctiveGraph = field(default_factory=_create_default_graph)
88
92
  force_plugins: List[Type[Plugin]] = field(default_factory=list)
89
93
 
90
94
  facet_resolver: Mapping[URIRef, Type[Facet]] = field(
@@ -121,7 +125,7 @@ class Iolanta: # noqa: WPS214
121
125
  for plugin_class in self.plugin_classes
122
126
  ]
123
127
 
124
- @functools.cached_property
128
+ @property
125
129
  def ldflex(self) -> LDFlex:
126
130
  """
127
131
  Create ldflex instance.
@@ -147,7 +151,12 @@ class Iolanta: # noqa: WPS214
147
151
  )
148
152
  }
149
153
 
150
- def add( # noqa: C901, WPS231, WPS210
154
+ def reset(self):
155
+ """Reset Iolanta graph."""
156
+ self.graph = _create_default_graph() # noqa: WPS601
157
+ self.__post_init__()
158
+
159
+ def add( # noqa: C901, WPS231, WPS210, WPS213
151
160
  self,
152
161
  source: Path,
153
162
  context: Optional[LDContext] = None,
@@ -167,20 +176,20 @@ class Iolanta: # noqa: WPS214
167
176
  try: # noqa: WPS225
168
177
  ld_rdf = yaml_ld.to_rdf(source_file)
169
178
  except ConnectionError as name_resolution_error:
170
- self.logger.info(
179
+ self.logger.warning(
171
180
  '%s | name resolution error: %s',
172
181
  source_file,
173
182
  str(name_resolution_error),
174
183
  )
175
184
  continue
176
185
  except ParserNotFound as parser_not_found:
177
- self.logger.info(f'{source} | {parser_not_found}')
186
+ self.logger.error(f'{source} | {parser_not_found}')
178
187
  continue
179
188
  except YAMLLDError as yaml_ld_error:
180
- self.logger.info(f'{source} | {yaml_ld_error}')
189
+ self.logger.error(f'{source} | {yaml_ld_error}')
181
190
  continue
182
191
  except ValueError as value_error:
183
- self.logger.info(f'{source} | {value_error}')
192
+ self.logger.error(f'{source} | {value_error}')
184
193
  continue
185
194
 
186
195
  self.logger.info(f'{source_file} is loaded.')
@@ -304,6 +313,8 @@ class Iolanta: # noqa: WPS214
304
313
  """
305
314
  self.bind_namespaces()
306
315
  self.add_files_from_plugins()
316
+ if self.project_root:
317
+ self.add(self.project_root)
307
318
 
308
319
  def render(
309
320
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iolanta
3
- Version: 1.2.7
3
+ Version: 1.2.9
4
4
  Summary: Semantic Web browser
5
5
  License: MIT
6
6
  Author: Anatoly Scherbakov
@@ -32,12 +32,39 @@ Requires-Dist: requests (>=2.25.1)
32
32
  Requires-Dist: rich (>=13.3.1)
33
33
  Requires-Dist: textual (>=0.83.0)
34
34
  Requires-Dist: typer (>=0.9.0)
35
- Requires-Dist: yaml-ld (>=1.1.3)
35
+ Requires-Dist: watchfiles (>=1.0.4,<2.0.0)
36
+ Requires-Dist: yaml-ld (>=1.1.4)
36
37
  Requires-Dist: yarl (>=1.9.4)
37
38
  Description-Content-Type: text/markdown
38
39
 
39
- # iolanta
40
+ # iolanta | Linked Data browser
40
41
 
41
- Stub repo for the iolanta browser.
42
+ ---
42
43
 
44
+ [![Build Status](https://github.com/iolanta-tech/iolanta/workflows/test/badge.svg?branch=master&event=push)](https://github.com/iolanta-tech/iolanta/actions?query=workflow%3Atest)
45
+ [![codecov](https://codecov.io/gh/iolanta-tech/iolanta/branch/master/graph/badge.svg)](https://codecov.io/gh/iolanta-tech/iolanta)
46
+ [![Python Version](https://img.shields.io/pypi/pyversions/iolanta.svg)](https://pypi.org/project/iolanta/)
47
+ [![wemake-python-styleguide](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/wemake-services/wemake-python-styleguide)
48
+
49
+ ![Iolanta cover image](docs/assets/cover.webp)
50
+
51
+ ## 📦 Install with `pip`
52
+
53
+ ```shell
54
+ pip install iolanta
55
+ ```
56
+
57
+ ## Try it out!
58
+
59
+ Explore a Nanopublication:
60
+
61
+ ```shell
62
+ iolanta https://w3id.org/np/RA7OYmnx-3ln_AY233lElN01wSDJWDOXPz061Ah93EQ2I
63
+ ```
64
+
65
+
66
+
67
+ ![](docs/screenshots/w3id.org.np.ra7oymnx-3ln_ay233leln01wsdjwdoxpz061ah93eq2i.svg)
68
+
69
+ See more [in the docs](https://iolanta.tech).
43
70
 
@@ -6,7 +6,7 @@ iolanta/cli/formatters/choose.py,sha256=Ac4hNoptvnhuJB77K9KOn5oMF7j2RxmNuaLko28W
6
6
  iolanta/cli/formatters/csv.py,sha256=OVucZxhcMjihUli0wkbSOKo500-i7DNV0Zdf6oIougU,753
7
7
  iolanta/cli/formatters/json.py,sha256=jkNldFApSWw0kcMkeIPvI2Vt4JTE-Rvx5mWqKJb3Sj4,741
8
8
  iolanta/cli/formatters/pretty.py,sha256=Ik75CR5GMBDJCvES9eF0bQPj64ZJD40iFDSSZH0s9aA,2920
9
- iolanta/cli/main.py,sha256=e-2VfSEG0rw34Qe3UTBOW0A1en77PErZOUdGtH29PzI,2992
9
+ iolanta/cli/main.py,sha256=-VS9B8kEBgJI6dl4W1cT4mZzWkANnMG1M6JnlXpdSKk,3126
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
@@ -14,13 +14,11 @@ iolanta/conversions.py,sha256=hbLwRF1bAbOxy17eMWLHhYksbdCWN-v4-0y0wn3XSSg,1185
14
14
  iolanta/cyberspace/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  iolanta/cyberspace/inference/wikibase-claim.sparql,sha256=JSawj3VTc9ZPoU9mvh1w1AMYb3cWyZ3x1rEYWUsKZ6A,209
16
16
  iolanta/cyberspace/inference/wikibase-statement-property.sparql,sha256=SkSHZZlxWVDwBM3aLo0Q7hLuOj9BsIQnXtcuAuwaxqU,240
17
- iolanta/cyberspace/processor.py,sha256=uy4dMVjCdQbYJUTwYJcxontkhiWoEEwoQkItHOXhqr0,20065
17
+ iolanta/cyberspace/processor.py,sha256=04_aMktZfThOKq6FmbSsruzcaAp5TUcRKXu_JXv4ZKo,20927
18
18
  iolanta/data/cli.yaml,sha256=TsnldYXoY5GIzoNuPDvwBKGw8eAEForZW1FCKqKI0Kg,1029
19
19
  iolanta/data/context.yaml,sha256=OULEeDkSqshabaXF_gMujgwFLDJvt9eQn_9FftUlSUw,1424
20
- iolanta/data/foaf-meta.yaml,sha256=BhRTFY_uue5FsJEDJnSgnA8hCr927RHnET9ZrQnFpdU,2647
21
20
  iolanta/data/html.yaml,sha256=hVFdLWLy8FMY8xpOrJMYc-tE3S0Nq83xuxVkjRW_7rI,517
22
21
  iolanta/data/iolanta.yaml,sha256=xubIFBNU02lmFXhgOSuyQwUcZD3xCqVfeVAZMvOxKbI,1433
23
- iolanta/data/ontologies-meta.yaml,sha256=nM6ROkZbk_CiMVOBcbUjXvA1noVsyIuhtXFgU4fQBbk,1374
24
22
  iolanta/data/textual-browser.yaml,sha256=ViMQJLiqa3CHdXluv9Fk3bEtyYC2ezNoHO7kQEp_N7w,3799
25
23
  iolanta/ensure_is_context.py,sha256=9aok8asyEx7KPesOR28VBDb3Ch9kfc3eoCpQSJwj07U,717
26
24
  iolanta/entry_points.py,sha256=DZbf-udlEwELFGqeWENj0M2BOUPOWlmGJdqyaEtnot0,504
@@ -48,22 +46,22 @@ iolanta/facets/html/code_literal.py,sha256=qCddzBrg6Y5XMIKohFQ52Tf9GPOcU7bj83tfA
48
46
  iolanta/facets/html/default.py,sha256=Dmf_kYiL2M954iigyYsiWrMstwZ1nvxKetuR6aW3xYY,647
49
47
  iolanta/facets/icon.py,sha256=ddZcolx1Q5_wo3w0jqiCzcc5Lsru6-jA7s7oAd1T8Og,494
50
48
  iolanta/facets/locator.py,sha256=tFwxGT4ujwEjwkgTevK6gwWfj3_1lU9Q305IU7a-I3Y,7697
51
- iolanta/facets/page_title.py,sha256=UQYgvKxS2OCfL49QRrO8WU4EIDAjbJ1-7vdkw3aaxyk,1496
49
+ iolanta/facets/page_title.py,sha256=s70A9w39O7J9Di3ni2ufRGsNctAW-eTzPTuP_wzWAtI,1446
52
50
  iolanta/facets/qname.py,sha256=ztyBbjjcW8dNZzuiNMqhcWfAUxk-gSjbseVGrQE7kVY,531
53
51
  iolanta/facets/textual_browser/__init__.py,sha256=sKgDvXOwib9n9d63kdtKCEv26-FoL0VN6zxDmfcheZ8,104
54
- iolanta/facets/textual_browser/app.py,sha256=c_3YdEbHsJfTLl72c-W51uJnZ5DsIIjRXptsuZIvWA0,2587
52
+ iolanta/facets/textual_browser/app.py,sha256=6-cmEkwRqj9UptVvXKVNIxT3PcDDOZP1OLpv8gPmB_Q,3202
55
53
  iolanta/facets/textual_browser/facet.py,sha256=zFynpMWr0iYaem-imbft5-2tjbkrDQEndYfHDlqqHUQ,740
56
54
  iolanta/facets/textual_browser/history.py,sha256=b3jTwVkVe0ZBcYkHGJ_zKIV4MSMScDdabmLQIjOZfes,1087
57
55
  iolanta/facets/textual_browser/home.py,sha256=GfDD1G2HiwdLkPkNdcYRqVIxDl5tWH9fewh_FJb8G-I,384
58
- iolanta/facets/textual_browser/location.py,sha256=pPnRjgG4yWvo5xOxuqHEM6O8QKL5pkCCioSkwi3K7XA,142
56
+ iolanta/facets/textual_browser/location.py,sha256=w0La8bVTpJiVo1_hFTDQeIUdDdqfhYnoihuZW-f130c,205
59
57
  iolanta/facets/textual_browser/models.py,sha256=DqTBjhkkTt5mNwqr4DzNbPSqzV-QtNqfKj7wpn6T3ao,173
60
58
  iolanta/facets/textual_browser/page.py,sha256=NkcQ5rSKZRbp63C8ozgsR_iVhcKHGv_SytUCQyGa7ss,786
61
- iolanta/facets/textual_browser/page_switcher.py,sha256=wbpZpqnPaJ0AmsehhFZADrP2Ptqa9JlxHLkvItRUr9I,6096
59
+ iolanta/facets/textual_browser/page_switcher.py,sha256=Rbfa7HjGMw-OFOUfq8J8A3pIlrY0iP3ZJQrWl_US5tQ,9693
62
60
  iolanta/facets/textual_class/__init__.py,sha256=tiL0p-3JspGcBRj4qa3rmoBFAuadk71l2ja2lJN6CEs,75
63
61
  iolanta/facets/textual_class/facets.py,sha256=W3-N7bekk-bKwrb2rTPwmySGF42-uV7aqLPAiHwFSU4,6496
64
62
  iolanta/facets/textual_class/sparql/instances.sparql,sha256=tmG4tuYrROKo3A7idjOEblAeNPXiiExHxc6k3fhalSM,113
65
63
  iolanta/facets/textual_default/__init__.py,sha256=snxA0FEY9qfAxNv3MlZLrJsXugD4dvs5hLStZWV7soM,158
66
- iolanta/facets/textual_default/facets.py,sha256=tQasa4p7oIKUqYjXKcRe886_ZiA8fHAaA8y0UMezeMs,4597
64
+ iolanta/facets/textual_default/facets.py,sha256=Pf5GttQ7EG0ZodjwwVXzYMffHus3MAZdJUIcbddcYtw,4738
67
65
  iolanta/facets/textual_default/sparql/inverse-properties.sparql,sha256=daHNdhmh92Q77CSf7ULbhxg57CuYsRFfMnXQz4VYEug,64
68
66
  iolanta/facets/textual_default/sparql/label.sparql,sha256=IWAkkgMKtIZ7Zpg8LUJ5fDZ9tiI8fiRYZo-zT61Fong,121
69
67
  iolanta/facets/textual_default/sparql/nodes-for-property.sparql,sha256=J9vg0Pz2HXDlPCeZ6IS2C0wODrpYDuNeD6DYT6UdNsU,68
@@ -71,7 +69,7 @@ iolanta/facets/textual_default/sparql/properties.sparql,sha256=stDbvFP4g6YKYphpN
71
69
  iolanta/facets/textual_default/tcss/default.tcss,sha256=v6k6LvZMndRW4t9Iq-7QF59U_LJTdohRsyavwTY5ruI,69
72
70
  iolanta/facets/textual_default/templates/default.md,sha256=CuD5lISsE2eAVnm2z6kfNff-vEgrNG95Wi5LTgkieWY,21
73
71
  iolanta/facets/textual_default/triple_uri_ref.py,sha256=XfuNPaAe-YxH8IyrdrHQ641aWh5zVMVs0L0WC3D6A4M,1279
74
- iolanta/facets/textual_default/widgets.py,sha256=JEES6ogGP8JvUa0Zx-cf20zJrrDbNx6e9r-5xo5nmn4,9285
72
+ iolanta/facets/textual_default/widgets.py,sha256=yOw3qJlwXYNqz3HuuRiVM0IZ-YSto_IeI2Hgv6n1BxU,9189
75
73
  iolanta/facets/textual_graph/__init__.py,sha256=DWd2gljzL8SiyYKQdBH78HouF1EMqgCH-w0K5OEmL2I,59
76
74
  iolanta/facets/textual_graph/facets.py,sha256=ZjWEEjzYGHRODvTUPiTFjKEjFQx59Bq-1v_RXU54M9k,876
77
75
  iolanta/facets/textual_graph/sparql/triples.sparql,sha256=5rFVGcvZzEHZj2opQQp0YlxJLpEdl-r1RjkPwo8j7t0,145
@@ -89,7 +87,7 @@ iolanta/facets/textual_ontology/facets.py,sha256=60g8ANmePb9_i_-d4ui-FdtNwg9aktr
89
87
  iolanta/facets/textual_ontology/sparql/terms.sparql,sha256=oVLxN452nqog_95qRaTWnvar6rxPNxPrRonSo7oFFTg,324
90
88
  iolanta/facets/textual_ontology/sparql/visualization-vocab.sparql,sha256=UNbU2Qt5C1Vq-CvdVhok7wecnu6p_IDtw7A8n_g6lBs,66
91
89
  iolanta/facets/textual_provenance/__init__.py,sha256=k5-_iK8Lrdwr5ZEJaDxq-UhGYe4G_adXVqGfOA5DAP8,114
92
- iolanta/facets/textual_provenance/facets.py,sha256=wKv_MFRuVDDlhar81wBCA5yOYpqydfK-ujwWQrkWs0U,3652
90
+ iolanta/facets/textual_provenance/facets.py,sha256=vv3UQsI2duB36DW5Zkw3sqgAXBPmK_xAo7cU0O7jF8g,3767
93
91
  iolanta/facets/textual_provenance/sparql/graphs.sparql,sha256=B45uKFd-1vrBuMDSbTURjUUEjHt51vAbqdL4tUcgMvk,103
94
92
  iolanta/facets/textual_provenance/sparql/triples.sparql,sha256=V-EdVuWbGHY3MspbJIMpwxPQautLDqJJV-AmihDjSHc,53
95
93
  iolanta/facets/title/__init__.py,sha256=fxpkG-YvHDp6eiVL3o7BbwhPMZZe-1R2Qi6S36QCTf8,77
@@ -98,7 +96,7 @@ iolanta/facets/title/sparql/title.sparql,sha256=4rz47tjwX2OJavWMzftaYKil1-ZHB76Z
98
96
  iolanta/facets/wikibase_statement_title/__init__.py,sha256=_yk1akxgSJOiUBJIc8QGrD2vovvmx_iw_vJDuv1rD7M,91
99
97
  iolanta/facets/wikibase_statement_title/facets.py,sha256=mUH7twlAgoeX7DgLQuRBQv4ORT6GWbN-0eJ1aliSfiQ,724
100
98
  iolanta/facets/wikibase_statement_title/sparql/statement-title.sparql,sha256=n07DQWxKqB5c3CA4kacq2HSN0R0dLgnMnLP1AxMo5YA,320
101
- iolanta/iolanta.py,sha256=eUFmtme7ZuFYPc1_KLO5ZLFWRjcVsPpYnGf2WzVkJzA,12662
99
+ iolanta/iolanta.py,sha256=kSMYvDmyjpSDgqLppeI835fHRl1YOVfuwiodIrVlUwo,12954
102
100
  iolanta/loaders/__init__.py,sha256=QTiKCsQc1BTS-IlY2CQsN9iVpEIPqYFvI9ERMYVZCbU,99
103
101
  iolanta/loaders/base.py,sha256=-DxYwqG1bfDXB2p_S-mKpkc_3Sh14OHhePbe65Iq3-s,3381
104
102
  iolanta/loaders/data_type_choice.py,sha256=zRUXBIzjvuW28P_dhMDVevE9C8EFEIx2_X39WydWrtM,1982
@@ -130,7 +128,7 @@ iolanta/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
130
128
  iolanta/widgets/mixin.py,sha256=nDRCOc-gizCf1a5DAcYs4hW8eZEd6pHBPFsfm0ncv7E,251
131
129
  ldflex/__init__.py,sha256=8IELqR55CQXuI0BafjobXSK7_kOc-qKVTrEtEwXnZRA,33
132
130
  ldflex/ldflex.py,sha256=omKmOo5PUyn8Evw4q_lqKCJOq6yqVOcLAYxnYkdwOUs,3332
133
- iolanta-1.2.7.dist-info/METADATA,sha256=1yhC21w4KtNTH5COJ9cpUWoHioHzBV3mzWZMbg6IfyU,1318
134
- iolanta-1.2.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
135
- iolanta-1.2.7.dist-info/entry_points.txt,sha256=fIp9g4kzjSNcTsTSjUCk4BIG3laHd3b3hUZlkjgFAGU,179
136
- iolanta-1.2.7.dist-info/RECORD,,
131
+ iolanta-1.2.9.dist-info/METADATA,sha256=5hu-_eus5cPDpyWqMlB0_SzvWrtEvl4gYrqeI9Kfkmo,2294
132
+ iolanta-1.2.9.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
133
+ iolanta-1.2.9.dist-info/entry_points.txt,sha256=fIp9g4kzjSNcTsTSjUCk4BIG3laHd3b3hUZlkjgFAGU,179
134
+ iolanta-1.2.9.dist-info/RECORD,,
@@ -1,113 +0,0 @@
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
-
10
- $id: "foaf:"
11
- $type: owl:Ontology
12
- vann:termGroup:
13
- - rdfs:label: Core Classes
14
- $reverse:
15
- rdf:type:
16
- - foaf:Person
17
- - foaf:Organization
18
- - foaf:Group
19
- - foaf:Agent
20
-
21
- - rdfs:label: Docs & Images
22
- $reverse:
23
- rdf:type:
24
- - foaf:Document
25
- - foaf:Image
26
- - foaf:img
27
- - foaf:depiction
28
- - foaf:page
29
- - foaf:topic
30
- - foaf:PersonalProfileDocument
31
- - foaf:primaryTopic
32
- - foaf:isPrimaryTopicOf
33
- - foaf:sha1
34
-
35
- - rdfs:label: Personal
36
- $reverse:
37
- rdf:type:
38
- - foaf:name
39
- - foaf:lastName
40
- - foaf:title
41
- - foaf:surname
42
- - foaf:familyName
43
- - foaf:givenName
44
- - foaf:family_name
45
- - foaf:firstName
46
- - foaf:gender
47
- - foaf:birthday
48
- - foaf:age
49
- - foaf:status
50
- - foaf:myersBriggs
51
- - foaf:dnaChecksum
52
- - foaf:nick
53
- - foaf:topic_interest
54
- - foaf:publications
55
- - foaf:workInfoHomepage
56
-
57
- - rdfs:label: Online Presence
58
- $reverse:
59
- rdf:type:
60
- - foaf:mbox
61
- - foaf:phone
62
- - foaf:OnlineAccount
63
- - foaf:OnlineEcommerceAccount
64
- - foaf:OnlineGamingAccount
65
- - foaf:OnlineChatAccount
66
- - foaf:account
67
- - foaf:accountServiceHomepage
68
- - foaf:accountName
69
- - foaf:aimChatID
70
- - foaf:jabberID
71
- - foaf:skypeID
72
- - foaf:msnChatID
73
- - foaf:yahooChatID
74
- - foaf:openid
75
- - foaf:homepage
76
- - foaf:weblog
77
- - foaf:geekcode
78
- - foaf:workplaceHomepage
79
- - foaf:schoolHomepage
80
- - foaf:icqChatID
81
- - foaf:mbox_sha1sum
82
-
83
- - rdfs:label: Social
84
- $reverse:
85
- rdf:type:
86
- - foaf:knows
87
- - foaf:friendOf
88
- - foaf:member
89
- - foaf:membershipClass
90
- - foaf:depiction
91
- - foaf:depicts
92
- - foaf:thumbnail
93
- - foaf:based_near
94
-
95
- - rdfs:label: Activities & Projects
96
- $reverse:
97
- rdf:type:
98
- - foaf:Project
99
- - foaf:fundedBy
100
- - foaf:interest
101
- - foaf:currentProject
102
- - foaf:pastProject
103
- - foaf:logo
104
- - foaf:maker
105
- - foaf:made
106
- - foaf:plan
107
- - foaf:tipjar
108
-
109
- - rdfs:label: Meta
110
- $reverse:
111
- rdf:type:
112
- - foaf:LabelProperty
113
- - foaf:focus
@@ -1,57 +0,0 @@
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
-
10
- $included:
11
- - $id: "owl:"
12
- $type: owl:Ontology
13
-
14
- - $id: "https://purl.org/dc/terms/"
15
- $type: owl:Ontology
16
-
17
- - $id: "rdf:"
18
- vann:termGroup:
19
- - rdfs:label: Properties
20
- $reverse:
21
- rdf:type:
22
- - rdf:Property
23
- - rdf:type
24
-
25
- - rdfs:label: Triples
26
- $reverse:
27
- rdf:type:
28
- - rdf:subject
29
- - rdf:predicate
30
- - rdf:object
31
- - rdf:Statement
32
- - rdfs:label: Containers
33
- $reverse:
34
- rdf:type:
35
- - rdf:first
36
- - rdf:rest
37
- - rdf:List
38
- - rdf:nil
39
- - rdf:Bag
40
- - rdf:Seq
41
- - rdf:Alt
42
- - rdfs:label: Language
43
- $reverse:
44
- rdf:type:
45
- - rdf:langString
46
- - rdf:language
47
-
48
- - $id: rdfs:Datatype
49
- - rdfs:label: Compound Literal
50
- $reverse:
51
- rdf:type:
52
- - rdf:CompoundLiteral
53
- - rdf:direction
54
- - rdfs:label: Value
55
- $reverse:
56
- rdf:type:
57
- - rdf:value