iolanta 1.2.5__py3-none-any.whl → 1.2.7__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
@@ -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(
@@ -79,12 +98,18 @@ def render_command( # noqa: WPS231, WPS238
79
98
  logger=logger,
80
99
  )
81
100
 
82
- node = iolanta.string_to_node(url)
101
+ node_url = URL(url)
102
+ if node_url.scheme:
103
+ node = URIRef(url)
104
+ else:
105
+ path = Path(url).absolute()
106
+ node = URIRef(f'file://{path}')
107
+ iolanta.add(path)
83
108
 
84
109
  try:
85
110
  renderable, stack = iolanta.render(
86
111
  node=node,
87
- as_datatype=iolanta.string_to_node(as_datatype),
112
+ as_datatype=URIRef(as_datatype),
88
113
  )
89
114
 
90
115
  except DocumentedError as documented_error:
@@ -8,14 +8,18 @@ from threading import Lock
8
8
  from types import MappingProxyType
9
9
  from typing import Any, Iterable, Mapping
10
10
 
11
+ import diskcache
11
12
  import funcy
12
13
  import loguru
14
+ import platformdirs
13
15
  import reasonable
14
16
  import yaml_ld
17
+ from nanopub import NanopubClient
15
18
  from rdflib import ConjunctiveGraph, Namespace, URIRef, Variable
16
19
  from rdflib.plugins.sparql.algebra import translateQuery
17
20
  from rdflib.plugins.sparql.evaluate import evalQuery
18
21
  from rdflib.plugins.sparql.parser import parseQuery
22
+ from rdflib.plugins.sparql.parserutils import CompValue
19
23
  from rdflib.plugins.sparql.sparql import Query
20
24
  from rdflib.query import Processor
21
25
  from rdflib.term import BNode, Literal, Node
@@ -53,7 +57,7 @@ REDIRECTS = MappingProxyType({
53
57
  # - either find a way to resolve these URLs automatically,
54
58
  # - or create a repository of those redirects online.
55
59
  'https://purl.org/vocab/vann/': URIRef(
56
- 'https://vocab.org/vann/vann-vocab-20100607.rdf'
60
+ 'https://vocab.org/vann/vann-vocab-20100607.rdf',
57
61
  ),
58
62
  URIRef(DC): URIRef(DCTERMS),
59
63
  URIRef(RDF): URIRef(RDF),
@@ -69,6 +73,34 @@ REDIRECTS = MappingProxyType({
69
73
  })
70
74
 
71
75
 
76
+ @diskcache.Cache(
77
+ directory=str(
78
+ platformdirs.user_cache_path(
79
+ appname='iolanta',
80
+ ) / 'find_retractions_for',
81
+ ),
82
+ ).memoize(expire=datetime.timedelta(days=8).total_seconds())
83
+ def find_retractions_for(nanopublication: URIRef) -> set[URIRef]:
84
+ """Find nanopublications that retract the given one."""
85
+ # See https://github.com/fair-workflows/nanopub/issues/168 for
86
+ # context of this dirty hack.
87
+ use_server = 'http://grlc.nanopubs.lod.labs.vu.nl/api/local/local/'
88
+
89
+ client = NanopubClient(
90
+ use_server=use_server,
91
+ )
92
+ client.grlc_urls = [use_server]
93
+
94
+ retractions = client.find_retractions_of(
95
+ str(nanopublication).replace(
96
+ 'https://',
97
+ 'http://',
98
+ ),
99
+ )
100
+
101
+ return {URIRef(retraction) for retraction in retractions}
102
+
103
+
72
104
  def _extract_from_mapping( # noqa: WPS213
73
105
  algebra: Mapping[str, Any],
74
106
  ) -> Iterable[URIRef | Variable]:
@@ -188,15 +220,12 @@ def resolve_variables(
188
220
 
189
221
 
190
222
  def extract_mentioned_urls_from_query(
191
- query: str,
223
+ query: Query,
192
224
  bindings: dict[str, Node],
193
225
  base: str | None,
194
226
  namespaces: dict[str, Namespace],
195
227
  ) -> tuple[Query, set[URIRef]]:
196
228
  """Extract URLs that a SPARQL query somehow mentions."""
197
- parse_tree = parseQuery(query)
198
- query = translateQuery(parse_tree, base, namespaces)
199
-
200
229
  return query, set(
201
230
  resolve_variables(
202
231
  extract_mentioned_urls(query.algebra),
@@ -218,6 +247,35 @@ class Skipped:
218
247
  LoadResult = Loaded | Skipped
219
248
 
220
249
 
250
+ def _extract_nanopublication_uris(
251
+ algebra: CompValue,
252
+ ) -> Iterable[URIRef]:
253
+ """Extract nanopublications to get retracting information for."""
254
+ match algebra.name:
255
+ case 'SelectQuery' | 'Project' | 'Distinct' | 'Graph':
256
+ yield from _extract_nanopublication_uris(algebra['p'])
257
+
258
+ case 'BGP':
259
+ for retractor, retracts, retractee in algebra['triples']:
260
+ if retracts == URIRef(
261
+ 'https://purl.org/nanopub/x/retracts',
262
+ ) and isinstance(retractor, Variable):
263
+ yield retractee
264
+
265
+ case 'LeftJoin' | 'Join':
266
+ yield from _extract_nanopublication_uris(algebra['p1'])
267
+ yield from _extract_nanopublication_uris(algebra['p2'])
268
+
269
+ case 'Filter' | 'OrderBy':
270
+ return
271
+
272
+ case unknown_name:
273
+ raise ValueError(
274
+ f'Unknown algebra name: {unknown_name}, '
275
+ f'content: {algebra}',
276
+ )
277
+
278
+
221
279
  @dataclasses.dataclass(frozen=True)
222
280
  class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
223
281
  """
@@ -325,8 +383,18 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
325
383
  query = strOrQuery
326
384
 
327
385
  else:
386
+ parse_tree = parseQuery(strOrQuery)
387
+ query = translateQuery(parse_tree, base, initNs)
388
+
389
+ self.load_retracting_nanopublications_by_query(
390
+ query=query,
391
+ bindings=initBindings,
392
+ base=base,
393
+ namespaces=initNs,
394
+ )
395
+
328
396
  query, urls = extract_mentioned_urls_from_query(
329
- query=strOrQuery,
397
+ query=query,
330
398
  bindings=initBindings,
331
399
  base=base,
332
400
  namespaces=initNs,
@@ -395,6 +463,8 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
395
463
  )
396
464
  if existing_triple is not None:
397
465
  return Skipped()
466
+ else:
467
+ self.logger.warning(f'Existing triples not found for {source_uri}')
398
468
 
399
469
  # FIXME This is definitely inefficient. However, python-yaml-ld caches
400
470
  # the document, so the performance overhead is not super high.
@@ -441,7 +511,7 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
441
511
  return Loaded()
442
512
 
443
513
  except Exception as err:
444
- self.logger.info('%s | Failed: %s', source, err)
514
+ self.logger.info(f'{source} | Failed: {err}')
445
515
 
446
516
  self.graph.add((
447
517
  source_uri,
@@ -536,32 +606,13 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
536
606
  self.graph.addN(quad_tuples)
537
607
  self.graph.last_not_inferred_source = source
538
608
 
539
- created_graphs = {
540
- normalize_term(quad.graph)
609
+ into_graphs = ', '.join({
610
+ quad.graph
541
611
  for quad in quads
542
- if quad.graph != source_uri
543
- }
544
- self.graph.addN(
545
- itertools.chain.from_iterable(
546
- [
547
- (
548
- source_uri,
549
- IOLANTA['has-sub-graph'],
550
- created_graph,
551
- source_uri,
552
- ),
553
- (
554
- created_graph,
555
- RDF.type,
556
- IOLANTA.Graph,
557
- source_uri,
558
- ),
559
- ]
560
- for created_graph in created_graphs
561
- ),
612
+ })
613
+ self.logger.info(
614
+ f'{source} | loaded successfully into graphs: {into_graphs}',
562
615
  )
563
-
564
- self.logger.info(f'{source} | loaded successfully.')
565
616
  return Loaded()
566
617
 
567
618
  def resolve_term(self, term: Node, bindings: dict[str, Node]):
@@ -573,3 +624,32 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
573
624
  )
574
625
 
575
626
  return term
627
+
628
+ def load_retracting_nanopublications_by_query( # noqa: WPS231
629
+ self,
630
+ query: Query,
631
+ bindings: dict[str, Node],
632
+ base: str | None,
633
+ namespaces: dict[str, Namespace],
634
+ ):
635
+ """
636
+ If the query requires information about retracting nanopubs, load them.
637
+
638
+ FIXME: This function presently does nothing because `nanopub` library
639
+ has problems: https://github.com/fair-workflows/nanopub/issues/168
640
+
641
+ TODO: Generalize this mechanism to allow for plugins which analyze
642
+ SPARQL queries and load information into the graph based on their
643
+ content.
644
+ """
645
+ nanopublications = list(
646
+ resolve_variables(
647
+ terms=_extract_nanopublication_uris(query.algebra),
648
+ bindings=bindings,
649
+ ),
650
+ )
651
+
652
+ for nanopublication in nanopublications:
653
+ retractions = find_retractions_for(nanopublication)
654
+ for retraction in retractions:
655
+ self.load(retraction)
iolanta/iolanta.py CHANGED
@@ -104,15 +104,6 @@ class Iolanta: # noqa: WPS214
104
104
  init=False,
105
105
  )
106
106
 
107
- @functools.cached_property
108
- def loader(self) -> Loader[SourceType]:
109
- """
110
- Construct loader.
111
-
112
- FIXME: Delete this.
113
- """
114
- return construct_root_loader(logger=self.logger)
115
-
116
107
  @property
117
108
  def plugin_classes(self) -> List[Type[Plugin]]:
118
109
  """Installed Iolanta plugins."""
@@ -162,19 +153,14 @@ class Iolanta: # noqa: WPS214
162
153
  context: Optional[LDContext] = None,
163
154
  graph_iri: Optional[URIRef] = None,
164
155
  ) -> 'Iolanta':
165
- """
166
- Parse & load information from given URL into the graph.
167
-
168
- FIXME:
169
- * Maybe implement context.json/context.yaml files support.
170
- """
156
+ """Parse & load information from given URL into the graph."""
171
157
  self.logger.info(f'Adding to graph: {source}')
172
158
  self.sources_added_not_yet_inferred.append(source)
173
159
 
174
160
  if not isinstance(source, Path):
175
161
  source = Path(source)
176
162
 
177
- for source_file in source.rglob('*'):
163
+ for source_file in list(source.rglob('*')) or [source]:
178
164
  if source_file.is_dir():
179
165
  continue
180
166
 
@@ -319,31 +305,6 @@ class Iolanta: # noqa: WPS214
319
305
  self.bind_namespaces()
320
306
  self.add_files_from_plugins()
321
307
 
322
- def string_to_node(self, name: str | Node) -> NotLiteralNode:
323
- """
324
- Parse a string into a node identifier.
325
-
326
- String might be:
327
- * a full IRI,
328
- * a qname,
329
- * a qname with implied `local:` part,
330
- * or a blank node identifier.
331
- """
332
- if isinstance(name, Node):
333
- return name
334
-
335
- if ':' in name:
336
- # This is either a full IRI, a qname, or a blank node identifier.
337
- try:
338
- # Trying to interpret this as QName.
339
- return self.graph.namespace_manager.expand_curie(name)
340
- except ValueError:
341
- # If it is not a QName then it is an IRI, let's return it.
342
- return URIRef(name)
343
-
344
- # This string does not include an ":", so we imply `local:`.
345
- return URIRef(f'local:{name}')
346
-
347
308
  def render(
348
309
  self,
349
310
  node: Node,
iolanta/parse_quads.py CHANGED
@@ -7,6 +7,7 @@ from rdflib.term import Node
7
7
 
8
8
  from iolanta.errors import UnresolvedIRI
9
9
  from iolanta.models import Quad
10
+ from iolanta.namespaces import IOLANTA, RDF
10
11
  from iolanta.parsers.errors import SpaceInProperty
11
12
 
12
13
 
@@ -66,6 +67,20 @@ def parse_quads(
66
67
  else:
67
68
  graph_name = URIRef(graph_name)
68
69
 
70
+ yield Quad(
71
+ graph,
72
+ IOLANTA['has-sub-graph'],
73
+ graph_name,
74
+ graph,
75
+ )
76
+
77
+ yield Quad(
78
+ graph_name,
79
+ RDF.type,
80
+ IOLANTA.Graph,
81
+ IOLANTA.meta,
82
+ )
83
+
69
84
  for quad in quads:
70
85
  try:
71
86
  yield Quad(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iolanta
3
- Version: 1.2.5
3
+ Version: 1.2.7
4
4
  Summary: Semantic Web browser
5
5
  License: MIT
6
6
  Author: Anatoly Scherbakov
@@ -15,12 +15,14 @@ Provides-Extra: all
15
15
  Requires-Dist: boltons (>=24.0.0)
16
16
  Requires-Dist: classes (>=0.4.0)
17
17
  Requires-Dist: deepmerge (>=0.1.1)
18
+ Requires-Dist: diskcache (>=5.6.3)
18
19
  Requires-Dist: documented (>=0.1.1)
19
20
  Requires-Dist: dominate (>=2.6.0)
20
21
  Requires-Dist: funcy (>=2.0)
21
22
  Requires-Dist: iolanta-tables (>=0.1.7,<0.2.0) ; extra == "all"
22
23
  Requires-Dist: loguru (>=0.7.3)
23
24
  Requires-Dist: more-itertools (>=9.0.0)
25
+ Requires-Dist: nanopub (>=2.0.1)
24
26
  Requires-Dist: owlrl (>=6.0.2)
25
27
  Requires-Dist: oxrdflib (>=0.4.0)
26
28
  Requires-Dist: python-frontmatter (>=0.5.0)
@@ -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=xuVm5kVYEvx_BbfRbIODoikHdum-g6BK_c0dvlV3HZ4,2436
9
+ iolanta/cli/main.py,sha256=e-2VfSEG0rw34Qe3UTBOW0A1en77PErZOUdGtH29PzI,2992
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,7 +14,7 @@ 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=RX96E9OLQL_fJMxsgmVn0XCwQfb8phnxYEiQgLY9akg,17365
17
+ iolanta/cyberspace/processor.py,sha256=uy4dMVjCdQbYJUTwYJcxontkhiWoEEwoQkItHOXhqr0,20065
18
18
  iolanta/data/cli.yaml,sha256=TsnldYXoY5GIzoNuPDvwBKGw8eAEForZW1FCKqKI0Kg,1029
19
19
  iolanta/data/context.yaml,sha256=OULEeDkSqshabaXF_gMujgwFLDJvt9eQn_9FftUlSUw,1424
20
20
  iolanta/data/foaf-meta.yaml,sha256=BhRTFY_uue5FsJEDJnSgnA8hCr927RHnET9ZrQnFpdU,2647
@@ -98,7 +98,7 @@ iolanta/facets/title/sparql/title.sparql,sha256=4rz47tjwX2OJavWMzftaYKil1-ZHB76Z
98
98
  iolanta/facets/wikibase_statement_title/__init__.py,sha256=_yk1akxgSJOiUBJIc8QGrD2vovvmx_iw_vJDuv1rD7M,91
99
99
  iolanta/facets/wikibase_statement_title/facets.py,sha256=mUH7twlAgoeX7DgLQuRBQv4ORT6GWbN-0eJ1aliSfiQ,724
100
100
  iolanta/facets/wikibase_statement_title/sparql/statement-title.sparql,sha256=n07DQWxKqB5c3CA4kacq2HSN0R0dLgnMnLP1AxMo5YA,320
101
- iolanta/iolanta.py,sha256=nCDhbolzqYVCvi9ld8uaVOSD3NfvNnwpxyIsXOX34q0,13810
101
+ iolanta/iolanta.py,sha256=eUFmtme7ZuFYPc1_KLO5ZLFWRjcVsPpYnGf2WzVkJzA,12662
102
102
  iolanta/loaders/__init__.py,sha256=QTiKCsQc1BTS-IlY2CQsN9iVpEIPqYFvI9ERMYVZCbU,99
103
103
  iolanta/loaders/base.py,sha256=-DxYwqG1bfDXB2p_S-mKpkc_3Sh14OHhePbe65Iq3-s,3381
104
104
  iolanta/loaders/data_type_choice.py,sha256=zRUXBIzjvuW28P_dhMDVevE9C8EFEIx2_X39WydWrtM,1982
@@ -111,7 +111,7 @@ iolanta/loaders/scheme_choice.py,sha256=GHA4JAD-Qq3uNdej4vs_bCrN1WMRshVRvOMxaYyP
111
111
  iolanta/models.py,sha256=L0iFaga4-PCaWP18WmT03NLK_oT7q49V0WMTQskoffI,2824
112
112
  iolanta/namespaces.py,sha256=fyWDCGPwU8Cs8d-Vmci4H0-fuIe7BMSevvEKFvG7Gf4,1153
113
113
  iolanta/node_to_qname.py,sha256=a82_qpgT87cbekY_76tTkl4Z-6Rz6am4UGIQChUf9Y0,794
114
- iolanta/parse_quads.py,sha256=rIzpy6zt-ad63pvT5nX0CG6v89HgUzPFDl31jklHkOw,2504
114
+ iolanta/parse_quads.py,sha256=yOI154hGRCNv4UjEuhicP0cQWNWid_52mE86VWYfH8w,2833
115
115
  iolanta/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
116
  iolanta/parsers/base.py,sha256=ZFjj0BLzW4985BdC6PwbngenhMuSYW5mNLpprZRWjhA,1048
117
117
  iolanta/parsers/dict_parser.py,sha256=t_OK2meUh49DqSaOYkSgEwxMKUNNgjJY8rZAyL4NQKI,4546
@@ -130,7 +130,7 @@ iolanta/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
130
130
  iolanta/widgets/mixin.py,sha256=nDRCOc-gizCf1a5DAcYs4hW8eZEd6pHBPFsfm0ncv7E,251
131
131
  ldflex/__init__.py,sha256=8IELqR55CQXuI0BafjobXSK7_kOc-qKVTrEtEwXnZRA,33
132
132
  ldflex/ldflex.py,sha256=omKmOo5PUyn8Evw4q_lqKCJOq6yqVOcLAYxnYkdwOUs,3332
133
- iolanta-1.2.5.dist-info/METADATA,sha256=bVTLK79ikFHEPuag6C6wDKfQSLOlTB2U6MzRDt73GDA,1250
134
- iolanta-1.2.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
135
- iolanta-1.2.5.dist-info/entry_points.txt,sha256=fIp9g4kzjSNcTsTSjUCk4BIG3laHd3b3hUZlkjgFAGU,179
136
- iolanta-1.2.5.dist-info/RECORD,,
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,,