iolanta 2.1.11__py3-none-any.whl → 2.1.13__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.
Files changed (30) hide show
  1. iolanta/cli/main.py +200 -80
  2. iolanta/facets/facet.py +15 -9
  3. iolanta/facets/mermaid_roadmap/__init__.py +0 -0
  4. iolanta/facets/mermaid_roadmap/facet.py +133 -0
  5. iolanta/facets/mermaid_roadmap/inference/blocks.sparql +13 -0
  6. iolanta/facets/mermaid_roadmap/inference/has-task-default-type.sparql +16 -0
  7. iolanta/facets/mermaid_roadmap/inference/task.sparql +26 -0
  8. iolanta/facets/mermaid_roadmap/inference/unblocked.sparql +21 -0
  9. iolanta/facets/mermaid_roadmap/mermaid_roadmap.yamlld +59 -0
  10. iolanta/facets/mermaid_roadmap/sparql/edges.sparql +25 -0
  11. iolanta/facets/mermaid_roadmap/sparql/nodes.sparql +17 -0
  12. iolanta/facets/query/__init__.py +0 -0
  13. iolanta/facets/query/ask_result_csv.py +23 -0
  14. iolanta/facets/query/ask_result_json.py +24 -0
  15. iolanta/facets/query/ask_result_table.py +23 -0
  16. iolanta/facets/query/construct_result_csv.py +34 -0
  17. iolanta/facets/query/construct_result_json.py +32 -0
  18. iolanta/facets/query/construct_result_table.py +55 -0
  19. iolanta/facets/query/data/query_result.yamlld +102 -0
  20. iolanta/facets/query/select_result_csv.py +36 -0
  21. iolanta/facets/query/select_result_json.py +24 -0
  22. iolanta/facets/query/select_result_table.py +48 -0
  23. iolanta/iolanta.py +146 -55
  24. iolanta/mcp/cli.py +16 -3
  25. iolanta/mermaid/models.py +74 -40
  26. iolanta/sparqlspace/processor.py +232 -179
  27. {iolanta-2.1.11.dist-info → iolanta-2.1.13.dist-info}/METADATA +2 -2
  28. {iolanta-2.1.11.dist-info → iolanta-2.1.13.dist-info}/RECORD +30 -10
  29. {iolanta-2.1.11.dist-info → iolanta-2.1.13.dist-info}/WHEEL +1 -1
  30. {iolanta-2.1.11.dist-info → iolanta-2.1.13.dist-info}/entry_points.txt +10 -0
iolanta/cli/main.py CHANGED
@@ -7,18 +7,25 @@ from typing import Annotated
7
7
  import loguru
8
8
  import platformdirs
9
9
  from documented import DocumentedError
10
- from rdflib import Literal, URIRef
10
+ from rdflib import Graph, Literal, URIRef
11
11
  from rich.console import Console
12
12
  from rich.markdown import Markdown
13
13
  from rich.table import Table
14
- from typer import Argument, Context, Exit, Option, Typer
14
+ from typer import Argument, Exit, Option, Typer
15
15
  from yarl import URL
16
16
 
17
17
  from iolanta.cli.models import LogLevel
18
+ from iolanta.facets.errors import FacetNotFound
18
19
  from iolanta.iolanta import Iolanta
19
20
  from iolanta.models import NotLiteralNode
21
+ from iolanta.namespaces import DATATYPES
22
+ from iolanta.query_result import (
23
+ QueryResult,
24
+ SPARQLParseException,
25
+ SelectResult,
26
+ )
20
27
 
21
- DEFAULT_LANGUAGE = locale.getlocale()[0].split('_')[0]
28
+ DEFAULT_LANGUAGE = locale.getlocale()[0].split("_")[0]
22
29
 
23
30
 
24
31
  console = Console()
@@ -40,23 +47,18 @@ def string_to_node(name: str) -> NotLiteralNode:
40
47
  return URIRef(name)
41
48
 
42
49
  path = Path(name).absolute()
43
- return URIRef(f'file://{path}')
50
+ return URIRef(f"file://{path}")
44
51
 
45
52
 
46
53
  def decode_datatype(datatype: str) -> URIRef:
47
- if datatype.startswith('http'):
54
+ if datatype.startswith("http"):
48
55
  return URIRef(datatype)
49
56
 
50
- return URIRef(f'https://iolanta.tech/datatypes/{datatype}')
57
+ return URIRef(f"https://iolanta.tech/datatypes/{datatype}")
51
58
 
52
59
 
53
- def render_and_return(
54
- url: str,
55
- as_datatype: str,
56
- language: str = DEFAULT_LANGUAGE,
57
- log_level: LogLevel = LogLevel.ERROR,
58
- ):
59
- """Render a given URL."""
60
+ def setup_logging(log_level: LogLevel):
61
+ """Configure and return logger."""
60
62
  level = {
61
63
  LogLevel.DEBUG: logging.DEBUG,
62
64
  LogLevel.INFO: logging.INFO,
@@ -64,111 +66,229 @@ def render_and_return(
64
66
  LogLevel.ERROR: logging.ERROR,
65
67
  }[log_level]
66
68
 
67
- log_file_path = platformdirs.user_log_path(
68
- 'iolanta',
69
- ensure_exists=True,
70
- ) / 'iolanta.log'
69
+ log_file_path = (
70
+ platformdirs.user_log_path(
71
+ "iolanta",
72
+ ensure_exists=True,
73
+ )
74
+ / "iolanta.log"
75
+ )
71
76
 
72
- # Get the level name first
73
77
  level_name = {
74
- logging.DEBUG: 'DEBUG',
75
- logging.INFO: 'INFO',
76
- logging.WARNING: 'WARNING',
77
- logging.ERROR: 'ERROR',
78
+ logging.DEBUG: "DEBUG",
79
+ logging.INFO: "INFO",
80
+ logging.WARNING: "WARNING",
81
+ logging.ERROR: "ERROR",
78
82
  }[level]
79
-
80
- # Configure global loguru logger BEFORE creating Iolanta instance
83
+
81
84
  loguru.logger.remove()
82
85
  loguru.logger.add(
83
86
  log_file_path,
84
87
  level=level_name,
85
- format='{time} {level} {message}',
88
+ format="{time} {level} {message}",
86
89
  enqueue=True,
87
90
  )
88
91
  loguru.logger.add(
89
92
  sys.stderr,
90
93
  level=level_name,
91
- format='{time} | {level:<8} | {name}:{function}:{line} - {message}',
94
+ format="{time} | {level:<8} | {name}:{function}:{line} - {message}",
92
95
  )
93
96
  loguru.logger.level(level_name)
94
-
95
- # Use the global logger
96
- logger = loguru.logger
97
-
98
- node_url = URL(url)
99
- if node_url.scheme and node_url.scheme != 'file':
100
- node = URIRef(url)
101
97
 
102
- iolanta: Iolanta = Iolanta(
103
- language=Literal(language),
104
- logger=logger,
98
+ return loguru.logger
99
+
100
+
101
+ def handle_error(
102
+ error: Exception,
103
+ log_level: LogLevel,
104
+ use_markdown: bool = True,
105
+ ) -> None:
106
+ """
107
+ Handle an error by checking log level and printing appropriately.
108
+
109
+ If log level is DEBUG or INFO, re-raise the error.
110
+ Otherwise, print it and exit with code 1.
111
+ """
112
+ level = {
113
+ LogLevel.DEBUG: logging.DEBUG,
114
+ LogLevel.INFO: logging.INFO,
115
+ LogLevel.WARNING: logging.WARNING,
116
+ LogLevel.ERROR: logging.ERROR,
117
+ }[log_level]
118
+
119
+ if level in {logging.DEBUG, logging.INFO}:
120
+ raise error
121
+
122
+ if use_markdown:
123
+ console.print(
124
+ Markdown(
125
+ str(error),
126
+ justify="left",
127
+ ),
105
128
  )
106
-
107
129
  else:
108
- path = Path(node_url.path).absolute()
109
- node = URIRef(f'file://{path}')
130
+ console.print(str(error))
131
+
132
+ raise Exit(1)
133
+
134
+
135
+ def create_query_node(query_result: QueryResult) -> Literal:
136
+ """
137
+ Create a Literal node from a query result.
138
+
139
+ Converts QueryResult (SelectResult, Graph, or bool) into a Literal
140
+ with the appropriate datatype for facet rendering.
141
+ """
142
+ match query_result:
143
+ case SelectResult():
144
+ return Literal(
145
+ query_result,
146
+ datatype=DATATYPES["sparql-select-result"],
147
+ )
148
+ case Graph():
149
+ return Literal(
150
+ query_result,
151
+ datatype=DATATYPES["sparql-construct-result"],
152
+ )
153
+ case bool():
154
+ return Literal(
155
+ query_result,
156
+ datatype=DATATYPES["sparql-ask-result"],
157
+ )
158
+
159
+
160
+ def render_and_return(
161
+ node: Literal | URIRef,
162
+ as_datatype: str,
163
+ language: str = DEFAULT_LANGUAGE,
164
+ log_level: LogLevel = LogLevel.ERROR,
165
+ ):
166
+ """
167
+ Render a node.
168
+
169
+ The node must be either a URIRef (for URLs) or a Literal (for query results).
170
+ """
171
+ logger = setup_logging(log_level)
172
+
173
+ # Determine Iolanta instance based on node type
174
+ if isinstance(node, Literal):
175
+ # Literal nodes (e.g., from query results) are used directly
176
+ # Use current directory as project_root for query results
110
177
  iolanta: Iolanta = Iolanta(
111
178
  language=Literal(language),
112
179
  logger=logger,
113
- project_root=path,
180
+ project_root=Path.cwd(),
114
181
  )
182
+ elif isinstance(node, URIRef):
183
+ # URIRef - determine project_root if it's a file:// URI
184
+ if str(node).startswith("file://"):
185
+ path = Path(str(node).replace("file://", ""))
186
+ iolanta: Iolanta = Iolanta(
187
+ language=Literal(language),
188
+ logger=logger,
189
+ project_root=path,
190
+ )
191
+ else:
192
+ iolanta: Iolanta = Iolanta(
193
+ language=Literal(language),
194
+ logger=logger,
195
+ )
196
+ else:
197
+ # This should never happen due to type checking, but kept for safety
198
+ raise TypeError(f"Expected Literal or URIRef, got {type(node)}")
115
199
 
116
200
  return iolanta.render(
117
- node=URIRef(node),
201
+ node=node,
118
202
  as_datatype=decode_datatype(as_datatype),
119
203
  )
120
204
 
121
205
 
122
- @app.command(name='render')
123
- def render_command( # noqa: WPS231, WPS238, WPS210, C901
124
- url: Annotated[str, Argument()],
206
+ @app.command(name="render")
207
+ def render_command( # noqa: WPS231, WPS238, WPS210, C901
208
+ url: Annotated[str | None, Argument()] = None,
209
+ query: Annotated[
210
+ str | None,
211
+ Option(
212
+ "--query",
213
+ help="SPARQL query to execute.",
214
+ ),
215
+ ] = None,
125
216
  as_datatype: Annotated[
126
- str, Option(
127
- '--as',
217
+ str | None,
218
+ Option(
219
+ "--as",
128
220
  ),
129
- ] = 'https://iolanta.tech/cli/interactive',
221
+ ] = None,
130
222
  language: Annotated[
131
- str, Option(
132
- help='Data language to prefer.',
223
+ str,
224
+ Option(
225
+ help="Data language to prefer.",
133
226
  ),
134
227
  ] = DEFAULT_LANGUAGE,
135
228
  log_level: LogLevel = LogLevel.ERROR,
136
229
  ):
137
230
  """Render a given URL."""
138
- try:
139
- renderable = render_and_return(url, as_datatype, language, log_level)
140
- except DocumentedError as documented_error:
141
- level = {
142
- LogLevel.DEBUG: logging.DEBUG,
143
- LogLevel.INFO: logging.INFO,
144
- LogLevel.WARNING: logging.WARNING,
145
- LogLevel.ERROR: logging.ERROR,
146
- }[log_level]
147
-
148
- if level in {logging.DEBUG, logging.INFO}:
149
- raise
231
+ if query is not None:
232
+ # For queries, default to 'table' format
233
+ if as_datatype is None:
234
+ as_datatype = "table"
150
235
 
151
- console.print(
152
- Markdown(
153
- str(documented_error),
154
- justify='left',
155
- ),
236
+ # Setup logging and create Iolanta instance (unlikely to raise)
237
+ logger = setup_logging(log_level)
238
+ iolanta: Iolanta = Iolanta(
239
+ language=Literal(language),
240
+ logger=logger,
241
+ project_root=Path.cwd(),
156
242
  )
157
- raise Exit(1)
158
243
 
159
- except Exception as err:
160
- level = {
161
- LogLevel.DEBUG: logging.DEBUG,
162
- LogLevel.INFO: logging.INFO,
163
- LogLevel.WARNING: logging.WARNING,
164
- LogLevel.ERROR: logging.ERROR,
165
- }[log_level]
166
-
167
- if level in {logging.DEBUG, logging.INFO}:
168
- raise
169
-
170
- console.print(str(err))
244
+ try:
245
+ renderable = render_and_return(
246
+ node=create_query_node(iolanta.query(query)),
247
+ as_datatype=as_datatype,
248
+ language=language,
249
+ log_level=log_level,
250
+ )
251
+ except (SPARQLParseException, DocumentedError, FacetNotFound) as error:
252
+ handle_error(error, log_level, use_markdown=True)
253
+ except Exception as error:
254
+ handle_error(error, log_level, use_markdown=False)
255
+ else:
256
+ # FIXME: An intermediary Literal can be used to dispatch rendering.
257
+ match renderable:
258
+ case Table() as table:
259
+ console.print(table)
260
+
261
+ case unknown:
262
+ console.print(unknown)
263
+ return
264
+
265
+ if url is None:
266
+ console.print("Error: Either URL or --query must be provided")
171
267
  raise Exit(1)
268
+
269
+ # For URLs, default to interactive mode
270
+ if as_datatype is None:
271
+ as_datatype = "https://iolanta.tech/cli/interactive"
272
+
273
+ # Parse string URL to URIRef (URL() is permissive and won't raise)
274
+ node_url = URL(url)
275
+ if node_url.scheme and node_url.scheme != "file":
276
+ node = URIRef(url)
277
+ else:
278
+ path = Path(node_url.path).absolute()
279
+ node = URIRef(f"file://{path}")
280
+
281
+ try:
282
+ renderable = render_and_return(
283
+ node=node,
284
+ as_datatype=as_datatype,
285
+ language=language,
286
+ log_level=log_level,
287
+ )
288
+ except DocumentedError as error:
289
+ handle_error(error, log_level, use_markdown=True)
290
+ except Exception as error:
291
+ handle_error(error, log_level, use_markdown=False)
172
292
  else:
173
293
  # FIXME: An intermediary Literal can be used to dispatch rendering.
174
294
  match renderable:
@@ -176,4 +296,4 @@ def render_command( # noqa: WPS231, WPS238, WPS210, C901
176
296
  console.print(table)
177
297
 
178
298
  case unknown:
179
- print(unknown)
299
+ console.print(unknown)
iolanta/facets/facet.py CHANGED
@@ -2,32 +2,38 @@ import inspect
2
2
  from dataclasses import dataclass, field
3
3
  from functools import cached_property
4
4
  from pathlib import Path
5
- from typing import Any, Generic, Iterable, Optional, TypeVar, Union
5
+ from typing import Any, Generic, Optional, TypeVar, Union
6
6
 
7
- from rdflib.term import BNode, Literal, Node, URIRef
7
+ from rdflib.term import Literal, Node
8
8
 
9
- from iolanta.models import NotLiteralNode, Triple, TripleTemplate
9
+ from iolanta.models import NotLiteralNode
10
10
  from iolanta.query_result import QueryResult, SPARQLQueryArgument
11
11
 
12
- FacetOutput = TypeVar('FacetOutput')
12
+ FacetOutput = TypeVar("FacetOutput")
13
13
 
14
14
 
15
15
  @dataclass
16
- class Facet(Generic[FacetOutput]):
16
+ class Facet(Generic[FacetOutput]): # noqa: WPS214
17
17
  """Base facet class."""
18
18
 
19
19
  this: Node
20
- iolanta: 'iolanta.Iolanta' = field(repr=False)
20
+ iolanta: "iolanta.Iolanta" = field(repr=False)
21
21
  as_datatype: Optional[NotLiteralNode] = None
22
22
 
23
23
  def __post_init__(self):
24
- if type(self.this) == str:
25
- raise ValueError(f'Facet {self.__class__.__name__} received a string as this: {self.this}')
24
+ if not isinstance(self.this, Node):
25
+ facet_name = self.__class__.__name__
26
+ this_type = type(self.this).__name__
27
+ raise ValueError(
28
+ f"Facet {facet_name} received a non-Node as this: {self.this} (type: {this_type})"
29
+ )
26
30
 
27
31
  @property
28
32
  def stored_queries_path(self) -> Path:
29
33
  """Construct directory for stored queries for this facet."""
30
- return Path(inspect.getfile(self.__class__)).parent / 'sparql'
34
+ return Path(inspect.getfile(self.__class__)).parent / "sparql"
35
+
36
+ inference_path: Optional[Path] = None
31
37
 
32
38
  def query(
33
39
  self,
File without changes
@@ -0,0 +1,133 @@
1
+ from pathlib import Path
2
+ from typing import Iterable
3
+
4
+ from rdflib import BNode, Literal, Node, URIRef
5
+
6
+ from iolanta import Facet
7
+ from iolanta.mermaid.models import (
8
+ Diagram,
9
+ MermaidBlankNode,
10
+ MermaidEdge,
11
+ MermaidLiteral,
12
+ MermaidScalar,
13
+ MermaidSubgraph,
14
+ MermaidURINode,
15
+ )
16
+ from iolanta.namespaces import DATATYPES
17
+ from pydantic import AnyUrl
18
+ from rdflib import URIRef as RDFURIRef
19
+
20
+
21
+ class TaskNode(MermaidScalar):
22
+ """A Mermaid node with attached CSS classes."""
23
+
24
+ node: MermaidURINode | MermaidBlankNode | MermaidLiteral
25
+ classes: list[str] = []
26
+
27
+ def __str__(self) -> str:
28
+ """Render the wrapped node."""
29
+ return str(self.node)
30
+
31
+ @property
32
+ def id(self) -> str:
33
+ """Get the ID of the wrapped node."""
34
+ return self.node.id
35
+
36
+
37
+ class BlocksEdge(MermaidEdge):
38
+ """
39
+ {self.source.id} --> {self.target.id}
40
+ """
41
+
42
+ def __init__(self, source, target):
43
+ # Initialize with empty title and a dummy predicate
44
+ super().__init__(
45
+ source=source,
46
+ target=target,
47
+ predicate=RDFURIRef('https://iolanta.tech/roadmap/blocks'),
48
+ title='',
49
+ )
50
+
51
+ def __str__(self) -> str:
52
+ # Override to remove intermediate node - just direct arrow
53
+ return f'{self.source.id} --> {self.target.id}'
54
+
55
+
56
+ # Rebuild Pydantic models to resolve forward references
57
+ # Need to rebuild MermaidEdge first so MermaidSubgraph is available
58
+ MermaidEdge.model_rebuild()
59
+ BlocksEdge.model_rebuild()
60
+
61
+
62
+ class MermaidRoadmap(Facet[str]):
63
+ """Mermaid roadmap diagram."""
64
+
65
+ META = Path(__file__).parent / 'mermaid_roadmap.yamlld'
66
+
67
+ inference_path = Path(__file__).parent / 'inference'
68
+
69
+ def show(self) -> str:
70
+ """Render mermaid roadmap diagram."""
71
+ children = list(self.construct_mermaid_for_graph(self.this))
72
+
73
+ # Extract class assignments from TaskNode instances
74
+ tail_parts = ['classDef unblocked fill:#0a5,stroke:#063,stroke-width:2px,color:#fff;']
75
+ for child in children:
76
+ if isinstance(child, TaskNode) and child.classes:
77
+ for class_name in child.classes:
78
+ tail_parts.append(f'class {child.id} {class_name}')
79
+
80
+ tail = '\n'.join(tail_parts)
81
+
82
+ return str(Diagram(
83
+ children=children,
84
+ tail=tail,
85
+ ))
86
+
87
+ def as_mermaid(self, node: Node):
88
+ """Convert RDF node to Mermaid node."""
89
+ match node:
90
+ case URIRef() as uri:
91
+ return MermaidURINode(
92
+ uri=uri,
93
+ url=AnyUrl(uri),
94
+ title=self.render(uri, as_datatype=DATATYPES.title),
95
+ )
96
+ case Literal() as literal:
97
+ return MermaidLiteral(literal=literal)
98
+ case BNode() as bnode:
99
+ return MermaidBlankNode(
100
+ node=bnode,
101
+ title=self.render(bnode, as_datatype=DATATYPES.title),
102
+ )
103
+ case unknown:
104
+ unknown_type = type(unknown)
105
+ raise ValueError(f'Unknown node type: {unknown} ({unknown_type})')
106
+
107
+ def construct_mermaid_for_graph(self, graph: URIRef) -> Iterable[MermaidScalar]:
108
+ """Render graph as mermaid."""
109
+ # Get nodes
110
+ node_rows = self.stored_query('nodes.sparql')
111
+ node_rows_list = list(node_rows)
112
+
113
+ nodes = [
114
+ TaskNode(
115
+ node=self.as_mermaid(row['node']),
116
+ classes=['unblocked'] if row.get('is_unblocked', False) else [],
117
+ )
118
+ for row in node_rows_list
119
+ ]
120
+
121
+ # Get edges for roadmap:blocks relationships
122
+ edge_rows = self.stored_query('edges.sparql')
123
+ edge_rows_list = list(edge_rows)
124
+
125
+ edges = [
126
+ BlocksEdge(
127
+ source=self.as_mermaid(row['source']),
128
+ target=self.as_mermaid(row['target']),
129
+ )
130
+ for row in edge_rows_list
131
+ ]
132
+
133
+ return [*nodes, *edges]
@@ -0,0 +1,13 @@
1
+ PREFIX roadmap: <https://iolanta.tech/roadmap/>
2
+
3
+ CONSTRUCT {
4
+ ?a roadmap:blocks ?b .
5
+ }
6
+ WHERE {
7
+ ?b roadmap:is-blocked-by ?a .
8
+
9
+ # Only infer if the relationship doesn't already exist
10
+ FILTER NOT EXISTS {
11
+ ?a roadmap:blocks ?b .
12
+ }
13
+ }
@@ -0,0 +1,16 @@
1
+ PREFIX roadmap: <https://iolanta.tech/roadmap/>
2
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
3
+
4
+ CONSTRUCT {
5
+ ?item a roadmap:Task .
6
+ }
7
+ WHERE {
8
+ # Find items that are values of roadmap:has-task property
9
+ ?container roadmap:has-task ?item .
10
+
11
+ # Only infer if the item isn't already typed as a roadmap type
12
+ FILTER NOT EXISTS {
13
+ ?item rdf:type ?type .
14
+ FILTER(?type IN (roadmap:Task, roadmap:Event, roadmap:Bug))
15
+ }
16
+ }
@@ -0,0 +1,26 @@
1
+ # It is imperative that this query executes after `blocks.sparql` because it relies upon proper `roadmap:blocks` relations.
2
+
3
+ PREFIX roadmap: <https://iolanta.tech/roadmap/>
4
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
5
+
6
+ CONSTRUCT {
7
+ ?other a roadmap:Task .
8
+ }
9
+ WHERE {
10
+ # Match either Task or Event
11
+ {
12
+ ?task rdf:type roadmap:Task .
13
+ } UNION {
14
+ ?task rdf:type roadmap:Event .
15
+ }
16
+
17
+ # Find nodes connected via roadmap:blocks chain (in either direction)
18
+ # ^roadmap:blocks is the inverse (equivalent to roadmap:is-blocked-by)
19
+ ?task (roadmap:blocks | ^roadmap:blocks)+ ?other .
20
+
21
+ # Only infer if the node isn't already a Task
22
+ # (to avoid redundant triples, but allow inference even if node has other types)
23
+ FILTER NOT EXISTS {
24
+ ?other rdf:type roadmap:Task .
25
+ }
26
+ }
@@ -0,0 +1,21 @@
1
+ PREFIX roadmap: <https://iolanta.tech/roadmap/>
2
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
3
+
4
+ CONSTRUCT {
5
+ ?task a roadmap:Unblocked .
6
+ }
7
+ WHERE {
8
+ # Match Task, Event, or Bug instances
9
+ {
10
+ ?task a roadmap:Task .
11
+ } UNION {
12
+ ?task a roadmap:Event .
13
+ } UNION {
14
+ ?task a roadmap:Bug .
15
+ }
16
+
17
+ # Only infer if there are no incoming blocks links
18
+ FILTER NOT EXISTS {
19
+ ?other roadmap:blocks ?task .
20
+ }
21
+ }
@@ -0,0 +1,59 @@
1
+ "@context":
2
+ "@import": https://json-ld.org/contexts/dollar-convenience.jsonld
3
+ roadmap: https://roadmap.iolanta.tech/
4
+ iolanta: https://iolanta.tech/
5
+ rdfs: http://www.w3.org/2000/01/rdf-schema#
6
+ prov: http://www.w3.org/ns/prov#
7
+ owl: http://www.w3.org/2002/07/owl#
8
+
9
+ $: rdfs:label
10
+
11
+ rdfs:subClassOf:
12
+ "@type": "@id"
13
+
14
+ rdfs:subPropertyOf:
15
+ "@type": "@id"
16
+
17
+ owl:inverseOf:
18
+ "@type": "@id"
19
+
20
+ →:
21
+ "@type": "@id"
22
+ "@id": iolanta:outputs
23
+
24
+ ↦:
25
+ "@id": iolanta:matches
26
+ "@type": iolanta:SPARQLText
27
+
28
+ $id: pkg:pypi/iolanta#mermaid-roadmap
29
+ $: Mermaid Roadmap
30
+
31
+ →:
32
+ $id: https://iolanta.tech/roadmap/datatypes/mermaid
33
+ $: Mermaid
34
+ $type: iolanta:OutputDatatype
35
+ rdfs:subClassOf: https://iolanta.tech/datatypes/mermaid
36
+
37
+ ↦:
38
+ - ASK WHERE { GRAPH $this { ?s ?p ?o } }
39
+
40
+ $included:
41
+ - $id: roadmap:Task
42
+ rdfs:subClassOf: prov:Activity
43
+ $: Task
44
+
45
+ - $id: roadmap:Bug
46
+ rdfs:subClassOf: roadmap:Task
47
+ $: Bug
48
+
49
+ - $id: roadmap:Event
50
+ rdfs:subClassOf: prov:Activity
51
+ $: Event
52
+
53
+ - $id: roadmap:is-blocked-by
54
+ rdfs:subPropertyOf: prov:wasInformedBy
55
+ $: Is blocked by
56
+
57
+ - $id: roadmap:blocks
58
+ owl:inverseOf: roadmap:is-blocked-by
59
+ $: Blocks