iolanta 1.2.7__tar.gz → 1.2.9__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 (136) hide show
  1. {iolanta-1.2.7 → iolanta-1.2.9}/PKG-INFO +31 -4
  2. iolanta-1.2.9/README.md +30 -0
  3. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cli/main.py +10 -6
  4. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cyberspace/processor.py +45 -19
  5. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/page_title.py +1 -2
  6. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_browser/app.py +25 -1
  7. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_browser/location.py +3 -0
  8. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_browser/page_switcher.py +153 -30
  9. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/facets.py +2 -0
  10. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/widgets.py +3 -5
  11. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_provenance/facets.py +4 -3
  12. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/iolanta.py +25 -14
  13. {iolanta-1.2.7 → iolanta-1.2.9}/pyproject.toml +3 -2
  14. iolanta-1.2.7/README.md +0 -4
  15. iolanta-1.2.7/iolanta/data/foaf-meta.yaml +0 -113
  16. iolanta-1.2.7/iolanta/data/ontologies-meta.yaml +0 -57
  17. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/__init__.py +0 -0
  18. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/base_plugin.py +0 -0
  19. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cli/__init__.py +0 -0
  20. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cli/formatters/__init__.py +0 -0
  21. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cli/formatters/choose.py +0 -0
  22. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cli/formatters/csv.py +0 -0
  23. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cli/formatters/json.py +0 -0
  24. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cli/formatters/pretty.py +0 -0
  25. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cli/models.py +0 -0
  26. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cli/pretty_print.py +0 -0
  27. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/context.py +0 -0
  28. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/conversions.py +0 -0
  29. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cyberspace/__init__.py +0 -0
  30. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cyberspace/inference/wikibase-claim.sparql +0 -0
  31. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/cyberspace/inference/wikibase-statement-property.sparql +0 -0
  32. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/data/cli.yaml +0 -0
  33. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/data/context.yaml +0 -0
  34. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/data/html.yaml +0 -0
  35. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/data/iolanta.yaml +0 -0
  36. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/data/textual-browser.yaml +0 -0
  37. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/ensure_is_context.py +0 -0
  38. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/entry_points.py +0 -0
  39. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/errors.py +0 -0
  40. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/__init__.py +0 -0
  41. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/cli/__init__.py +0 -0
  42. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/cli/base.py +0 -0
  43. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/cli/default.py +0 -0
  44. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/cli/record.py +0 -0
  45. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/cli/sparql/link.sparql +0 -0
  46. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/cli/sparql/record.sparql +0 -0
  47. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/errors.py +0 -0
  48. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/facet.py +0 -0
  49. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/foaf_person_title/__init__.py +0 -0
  50. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/foaf_person_title/facet.py +0 -0
  51. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/foaf_person_title/sparql/names.sparql +0 -0
  52. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/generic/__init__.py +0 -0
  53. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/generic/bool_literal.py +0 -0
  54. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/generic/date_literal.py +0 -0
  55. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/generic/default.py +0 -0
  56. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/generic/sparql/default.sparql +0 -0
  57. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/html/__init__.py +0 -0
  58. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/html/base.py +0 -0
  59. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/html/code_literal.py +0 -0
  60. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/html/default.py +0 -0
  61. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/icon.py +0 -0
  62. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/locator.py +0 -0
  63. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/qname.py +0 -0
  64. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_browser/__init__.py +0 -0
  65. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_browser/facet.py +0 -0
  66. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_browser/history.py +0 -0
  67. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_browser/home.py +0 -0
  68. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_browser/models.py +0 -0
  69. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_browser/page.py +0 -0
  70. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_class/__init__.py +0 -0
  71. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_class/facets.py +0 -0
  72. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_class/sparql/instances.sparql +0 -0
  73. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/__init__.py +0 -0
  74. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/sparql/inverse-properties.sparql +0 -0
  75. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/sparql/label.sparql +0 -0
  76. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/sparql/nodes-for-property.sparql +0 -0
  77. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/sparql/properties.sparql +0 -0
  78. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/tcss/default.tcss +0 -0
  79. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/templates/default.md +0 -0
  80. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_default/triple_uri_ref.py +0 -0
  81. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_graph/__init__.py +0 -0
  82. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_graph/facets.py +0 -0
  83. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_graph/sparql/triples.sparql +0 -0
  84. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_graph_triples.py +0 -0
  85. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_link/__init__.py +0 -0
  86. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_link/facet.py +0 -0
  87. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_nanopublication/__init__.py +0 -0
  88. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_nanopublication/facet.py +0 -0
  89. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_nanopublication/models.py +0 -0
  90. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_nanopublication/nanopublication_widget.py +0 -0
  91. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_nanopublication/term_list_widget.py +0 -0
  92. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_nanopublication/term_widget.py +0 -0
  93. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_ontology/__init__.py +0 -0
  94. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_ontology/facets.py +0 -0
  95. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_ontology/sparql/terms.sparql +0 -0
  96. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_ontology/sparql/visualization-vocab.sparql +0 -0
  97. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_provenance/__init__.py +0 -0
  98. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_provenance/sparql/graphs.sparql +0 -0
  99. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/textual_provenance/sparql/triples.sparql +0 -0
  100. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/title/__init__.py +0 -0
  101. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/title/facets.py +0 -0
  102. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/title/sparql/title.sparql +0 -0
  103. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/wikibase_statement_title/__init__.py +0 -0
  104. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/wikibase_statement_title/facets.py +0 -0
  105. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/facets/wikibase_statement_title/sparql/statement-title.sparql +0 -0
  106. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/loaders/__init__.py +0 -0
  107. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/loaders/base.py +0 -0
  108. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/loaders/data_type_choice.py +0 -0
  109. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/loaders/dict_loader.py +0 -0
  110. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/loaders/errors.py +0 -0
  111. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/loaders/http.py +0 -0
  112. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/loaders/local_directory.py +0 -0
  113. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/loaders/local_file.py +0 -0
  114. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/loaders/scheme_choice.py +0 -0
  115. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/models.py +0 -0
  116. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/namespaces.py +0 -0
  117. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/node_to_qname.py +0 -0
  118. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/parse_quads.py +0 -0
  119. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/parsers/__init__.py +0 -0
  120. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/parsers/base.py +0 -0
  121. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/parsers/dict_parser.py +0 -0
  122. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/parsers/errors.py +0 -0
  123. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/parsers/json.py +0 -0
  124. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/parsers/markdown.py +0 -0
  125. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/parsers/yaml.py +0 -0
  126. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/plugin.py +0 -0
  127. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/reformat_blank_nodes.py +0 -0
  128. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/resolvers/__init__.py +0 -0
  129. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/resolvers/base.py +0 -0
  130. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/resolvers/python_import.py +0 -0
  131. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/shortcuts.py +0 -0
  132. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/stack.py +0 -0
  133. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/widgets/__init__.py +0 -0
  134. {iolanta-1.2.7 → iolanta-1.2.9}/iolanta/widgets/mixin.py +0 -0
  135. {iolanta-1.2.7 → iolanta-1.2.9}/ldflex/__init__.py +0 -0
  136. {iolanta-1.2.7 → iolanta-1.2.9}/ldflex/ldflex.py +0 -0
@@ -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
 
@@ -0,0 +1,30 @@
1
+ # iolanta | Linked Data browser
2
+
3
+ ---
4
+
5
+ [![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)
6
+ [![codecov](https://codecov.io/gh/iolanta-tech/iolanta/branch/master/graph/badge.svg)](https://codecov.io/gh/iolanta-tech/iolanta)
7
+ [![Python Version](https://img.shields.io/pypi/pyversions/iolanta.svg)](https://pypi.org/project/iolanta/)
8
+ [![wemake-python-styleguide](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/wemake-services/wemake-python-styleguide)
9
+
10
+ ![Iolanta cover image](docs/assets/cover.webp)
11
+
12
+ ## 📦 Install with `pip`
13
+
14
+ ```shell
15
+ pip install iolanta
16
+ ```
17
+
18
+ ## Try it out!
19
+
20
+ Explore a Nanopublication:
21
+
22
+ ```shell
23
+ iolanta https://w3id.org/np/RA7OYmnx-3ln_AY233lElN01wSDJWDOXPz061Ah93EQ2I
24
+ ```
25
+
26
+
27
+
28
+ ![](docs/screenshots/w3id.org.np.ra7oymnx-3ln_ay233leln01wsdjwdoxpz061ah93eq2i.svg)
29
+
30
+ See more [in the docs](https://iolanta.tech).
@@ -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