iolanta 1.2.6__tar.gz → 1.2.8__tar.gz

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.
Files changed (135) hide show
  1. {iolanta-1.2.6 → iolanta-1.2.8}/PKG-INFO +2 -1
  2. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cli/main.py +37 -8
  3. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cyberspace/processor.py +16 -31
  4. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/page_title.py +1 -2
  5. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_browser/app.py +25 -1
  6. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_browser/location.py +3 -0
  7. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_browser/page_switcher.py +153 -30
  8. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/facets.py +2 -0
  9. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/widgets.py +3 -5
  10. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_provenance/facets.py +4 -3
  11. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/iolanta.py +27 -55
  12. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/parse_quads.py +15 -0
  13. {iolanta-1.2.6 → iolanta-1.2.8}/pyproject.toml +2 -1
  14. iolanta-1.2.6/iolanta/data/foaf-meta.yaml +0 -113
  15. iolanta-1.2.6/iolanta/data/ontologies-meta.yaml +0 -57
  16. {iolanta-1.2.6 → iolanta-1.2.8}/README.md +0 -0
  17. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/__init__.py +0 -0
  18. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/base_plugin.py +0 -0
  19. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cli/__init__.py +0 -0
  20. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cli/formatters/__init__.py +0 -0
  21. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cli/formatters/choose.py +0 -0
  22. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cli/formatters/csv.py +0 -0
  23. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cli/formatters/json.py +0 -0
  24. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cli/formatters/pretty.py +0 -0
  25. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cli/models.py +0 -0
  26. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cli/pretty_print.py +0 -0
  27. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/context.py +0 -0
  28. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/conversions.py +0 -0
  29. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cyberspace/__init__.py +0 -0
  30. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cyberspace/inference/wikibase-claim.sparql +0 -0
  31. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/cyberspace/inference/wikibase-statement-property.sparql +0 -0
  32. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/data/cli.yaml +0 -0
  33. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/data/context.yaml +0 -0
  34. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/data/html.yaml +0 -0
  35. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/data/iolanta.yaml +0 -0
  36. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/data/textual-browser.yaml +0 -0
  37. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/ensure_is_context.py +0 -0
  38. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/entry_points.py +0 -0
  39. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/errors.py +0 -0
  40. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/__init__.py +0 -0
  41. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/cli/__init__.py +0 -0
  42. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/cli/base.py +0 -0
  43. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/cli/default.py +0 -0
  44. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/cli/record.py +0 -0
  45. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/cli/sparql/link.sparql +0 -0
  46. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/cli/sparql/record.sparql +0 -0
  47. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/errors.py +0 -0
  48. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/facet.py +0 -0
  49. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/foaf_person_title/__init__.py +0 -0
  50. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/foaf_person_title/facet.py +0 -0
  51. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/foaf_person_title/sparql/names.sparql +0 -0
  52. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/generic/__init__.py +0 -0
  53. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/generic/bool_literal.py +0 -0
  54. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/generic/date_literal.py +0 -0
  55. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/generic/default.py +0 -0
  56. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/generic/sparql/default.sparql +0 -0
  57. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/html/__init__.py +0 -0
  58. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/html/base.py +0 -0
  59. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/html/code_literal.py +0 -0
  60. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/html/default.py +0 -0
  61. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/icon.py +0 -0
  62. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/locator.py +0 -0
  63. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/qname.py +0 -0
  64. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_browser/__init__.py +0 -0
  65. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_browser/facet.py +0 -0
  66. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_browser/history.py +0 -0
  67. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_browser/home.py +0 -0
  68. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_browser/models.py +0 -0
  69. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_browser/page.py +0 -0
  70. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_class/__init__.py +0 -0
  71. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_class/facets.py +0 -0
  72. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_class/sparql/instances.sparql +0 -0
  73. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/__init__.py +0 -0
  74. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/sparql/inverse-properties.sparql +0 -0
  75. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/sparql/label.sparql +0 -0
  76. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/sparql/nodes-for-property.sparql +0 -0
  77. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/sparql/properties.sparql +0 -0
  78. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/tcss/default.tcss +0 -0
  79. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/templates/default.md +0 -0
  80. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_default/triple_uri_ref.py +0 -0
  81. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_graph/__init__.py +0 -0
  82. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_graph/facets.py +0 -0
  83. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_graph/sparql/triples.sparql +0 -0
  84. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_graph_triples.py +0 -0
  85. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_link/__init__.py +0 -0
  86. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_link/facet.py +0 -0
  87. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_nanopublication/__init__.py +0 -0
  88. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_nanopublication/facet.py +0 -0
  89. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_nanopublication/models.py +0 -0
  90. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_nanopublication/nanopublication_widget.py +0 -0
  91. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_nanopublication/term_list_widget.py +0 -0
  92. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_nanopublication/term_widget.py +0 -0
  93. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_ontology/__init__.py +0 -0
  94. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_ontology/facets.py +0 -0
  95. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_ontology/sparql/terms.sparql +0 -0
  96. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_ontology/sparql/visualization-vocab.sparql +0 -0
  97. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_provenance/__init__.py +0 -0
  98. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_provenance/sparql/graphs.sparql +0 -0
  99. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/textual_provenance/sparql/triples.sparql +0 -0
  100. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/title/__init__.py +0 -0
  101. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/title/facets.py +0 -0
  102. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/title/sparql/title.sparql +0 -0
  103. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/wikibase_statement_title/__init__.py +0 -0
  104. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/wikibase_statement_title/facets.py +0 -0
  105. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/facets/wikibase_statement_title/sparql/statement-title.sparql +0 -0
  106. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/loaders/__init__.py +0 -0
  107. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/loaders/base.py +0 -0
  108. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/loaders/data_type_choice.py +0 -0
  109. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/loaders/dict_loader.py +0 -0
  110. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/loaders/errors.py +0 -0
  111. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/loaders/http.py +0 -0
  112. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/loaders/local_directory.py +0 -0
  113. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/loaders/local_file.py +0 -0
  114. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/loaders/scheme_choice.py +0 -0
  115. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/models.py +0 -0
  116. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/namespaces.py +0 -0
  117. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/node_to_qname.py +0 -0
  118. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/parsers/__init__.py +0 -0
  119. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/parsers/base.py +0 -0
  120. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/parsers/dict_parser.py +0 -0
  121. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/parsers/errors.py +0 -0
  122. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/parsers/json.py +0 -0
  123. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/parsers/markdown.py +0 -0
  124. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/parsers/yaml.py +0 -0
  125. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/plugin.py +0 -0
  126. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/reformat_blank_nodes.py +0 -0
  127. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/resolvers/__init__.py +0 -0
  128. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/resolvers/base.py +0 -0
  129. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/resolvers/python_import.py +0 -0
  130. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/shortcuts.py +0 -0
  131. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/stack.py +0 -0
  132. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/widgets/__init__.py +0 -0
  133. {iolanta-1.2.6 → iolanta-1.2.8}/iolanta/widgets/mixin.py +0 -0
  134. {iolanta-1.2.6 → iolanta-1.2.8}/ldflex/__init__.py +0 -0
  135. {iolanta-1.2.6 → iolanta-1.2.8}/ldflex/ldflex.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iolanta
3
- Version: 1.2.6
3
+ Version: 1.2.8
4
4
  Summary: Semantic Web browser
5
5
  License: MIT
6
6
  Author: Anatoly Scherbakov
@@ -32,6 +32,7 @@ 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: watchfiles (>=1.0.4,<2.0.0)
35
36
  Requires-Dist: yaml-ld (>=1.1.3)
36
37
  Requires-Dist: yarl (>=1.9.4)
37
38
  Description-Content-Type: text/markdown
@@ -1,17 +1,20 @@
1
1
  import locale
2
2
  import logging
3
+ from pathlib import Path
3
4
  from typing import Annotated
4
5
 
5
6
  import loguru
6
7
  import platformdirs
7
8
  from documented import DocumentedError
8
- from rdflib import Literal
9
+ from rdflib import Literal, URIRef
9
10
  from rich.console import Console
10
11
  from rich.markdown import Markdown
11
12
  from typer import Argument, Exit, Option, Typer
13
+ from yarl import URL
12
14
 
13
15
  from iolanta.cli.models import LogLevel
14
16
  from iolanta.iolanta import Iolanta
17
+ from iolanta.models import NotLiteralNode
15
18
 
16
19
  DEFAULT_LANGUAGE = locale.getlocale()[0].split('_')[0]
17
20
 
@@ -38,8 +41,24 @@ def construct_app() -> Typer:
38
41
  app = construct_app()
39
42
 
40
43
 
44
+ def string_to_node(name: str) -> NotLiteralNode:
45
+ """
46
+ Parse a string into a node identifier.
47
+
48
+ String might be:
49
+ * a URL,
50
+ * or a local disk path.
51
+ """
52
+ url = URL(name)
53
+ if url.scheme:
54
+ return URIRef(name)
55
+
56
+ path = Path(name).absolute()
57
+ return URIRef(f'file://{path}')
58
+
59
+
41
60
  @app.command(name='browse')
42
- def render_command( # noqa: WPS231, WPS238
61
+ def render_command( # noqa: WPS231, WPS238, WPS210, C901
43
62
  url: Annotated[str, Argument()],
44
63
  as_datatype: Annotated[
45
64
  str, Option(
@@ -74,17 +93,27 @@ def render_command( # noqa: WPS231, WPS238
74
93
  enqueue=True,
75
94
  )
76
95
 
77
- iolanta: Iolanta = Iolanta(
78
- language=Literal(language),
79
- logger=logger,
80
- )
96
+ node_url = URL(url)
97
+ if node_url.scheme:
98
+ node = URIRef(url)
81
99
 
82
- node = iolanta.string_to_node(url)
100
+ iolanta: Iolanta = Iolanta(
101
+ language=Literal(language),
102
+ logger=logger,
103
+ )
104
+ else:
105
+ path = Path(url).absolute()
106
+ node = URIRef(f'file://{path}')
107
+ iolanta: Iolanta = Iolanta(
108
+ language=Literal(language),
109
+ logger=logger,
110
+ project_root=path,
111
+ )
83
112
 
84
113
  try:
85
114
  renderable, stack = iolanta.render(
86
115
  node=node,
87
- as_datatype=iolanta.string_to_node(as_datatype),
116
+ as_datatype=URIRef(as_datatype),
88
117
  )
89
118
 
90
119
  except DocumentedError as documented_error:
@@ -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
  )
@@ -463,6 +465,8 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
463
465
  )
464
466
  if existing_triple is not None:
465
467
  return Skipped()
468
+ else:
469
+ self.logger.info(f'Existing triples not found for {source_uri}')
466
470
 
467
471
  # FIXME This is definitely inefficient. However, python-yaml-ld caches
468
472
  # the document, so the performance overhead is not super high.
@@ -563,10 +567,10 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
563
567
  )
564
568
  return Loaded()
565
569
  except ParserNotFound as parser_not_found:
566
- self.logger.info('%s | %s', source, str(parser_not_found))
570
+ self.logger.info(f'{source} | {parser_not_found}')
567
571
  return Loaded()
568
572
  except YAMLLDError as yaml_ld_error:
569
- self.logger.error('%s | %s', source, str(yaml_ld_error))
573
+ self.logger.error(f'{source} | {yaml_ld_error}')
570
574
  return Loaded()
571
575
 
572
576
  try:
@@ -604,32 +608,13 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
604
608
  self.graph.addN(quad_tuples)
605
609
  self.graph.last_not_inferred_source = source
606
610
 
607
- created_graphs = {
608
- normalize_term(quad.graph)
611
+ into_graphs = ', '.join({
612
+ quad.graph
609
613
  for quad in quads
610
- if quad.graph != source_uri
611
- }
612
- self.graph.addN(
613
- itertools.chain.from_iterable(
614
- [
615
- (
616
- source_uri,
617
- IOLANTA['has-sub-graph'],
618
- created_graph,
619
- source_uri,
620
- ),
621
- (
622
- created_graph,
623
- RDF.type,
624
- IOLANTA.Graph,
625
- source_uri,
626
- ),
627
- ]
628
- for created_graph in created_graphs
629
- ),
614
+ })
615
+ self.logger.info(
616
+ f'{source} | loaded successfully into graphs: {into_graphs}',
630
617
  )
631
-
632
- self.logger.info(f'{source} | loaded successfully.')
633
618
  return Loaded()
634
619
 
635
620
  def resolve_term(self, term: Node, bindings: dict[str, Node]):
@@ -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