Onya 0.1.1__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.
- onya/__about__.py +1 -0
- onya/__init__.py +29 -0
- onya/contrib/mkdcomments.py +54 -0
- onya/graph.py +182 -0
- onya/serial/__init__.py +5 -0
- onya/serial/literate.py +116 -0
- onya/serial/literate_lex.py +523 -0
- onya/serial/litparse_util.py +54 -0
- onya/terms.py +18 -0
- onya/version.py +2 -0
- onya-0.1.1.dist-info/METADATA +201 -0
- onya-0.1.1.dist-info/RECORD +14 -0
- onya-0.1.1.dist-info/WHEEL +4 -0
- onya-0.1.1.dist-info/licenses/LICENSE-spec +47 -0
onya/__about__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
onya/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# onya
|
|
2
|
+
|
|
3
|
+
'''
|
|
4
|
+
Onya - Property graph model for Web resources
|
|
5
|
+
'''
|
|
6
|
+
|
|
7
|
+
from .__about__ import __version__ as __version__ # noqa: F401
|
|
8
|
+
from amara.iri import I
|
|
9
|
+
|
|
10
|
+
# Base IRI for Onya vocabulary
|
|
11
|
+
ONYA_BASEIRI = I('http://purl.org/onya/vocab/')
|
|
12
|
+
|
|
13
|
+
# Special null value for Onya
|
|
14
|
+
ONYA_NULL = I('http://purl.org/onya/vocab/null')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LITERAL:
|
|
18
|
+
'''
|
|
19
|
+
Wrapper for literal (text) values in Onya
|
|
20
|
+
'''
|
|
21
|
+
def __init__(self, s):
|
|
22
|
+
self._s = s
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return self._s
|
|
26
|
+
|
|
27
|
+
def __repr__(self):
|
|
28
|
+
return f'LITERAL({repr(self._s)})'
|
|
29
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A Python-Markdown preprocessor extension to ignore html comments opened by
|
|
3
|
+
three dashes (<!---comment-->) and any whitespace prior to them. I believe
|
|
4
|
+
pandoc has similar functionality.
|
|
5
|
+
|
|
6
|
+
Note: This extension does not work with the markdownfromFile function or
|
|
7
|
+
the convertFile method. They raise a UnicodeDecodeError.
|
|
8
|
+
|
|
9
|
+
Note: If using multiple extensions, mkdcomments probably should be last in
|
|
10
|
+
the list. Markdown extensions are loaded into the OrderedDict from which they
|
|
11
|
+
are executed in the order of the extension list. If a different extension is
|
|
12
|
+
loaded after mkdcomments, it may insert itself before mkdcomments in the
|
|
13
|
+
OrderedDict. Undesirable results may ensue. If, for instance, the 'meta'
|
|
14
|
+
extension is executed before mkdcomments, any comments in the meta-data will be
|
|
15
|
+
included in meta's dictionary.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from markdown.preprocessors import Preprocessor
|
|
20
|
+
from markdown.extensions import Extension
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CommentsExtension(Extension):
|
|
24
|
+
def extendMarkdown(self, md, md_globals):
|
|
25
|
+
md.registerExtension(self)
|
|
26
|
+
md.preprocessors.add("comments", CommentsProcessor(md), "_begin")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommentsProcessor(Preprocessor):
|
|
30
|
+
def run(self, lines):
|
|
31
|
+
new_lines = []
|
|
32
|
+
multi = False
|
|
33
|
+
for line in lines:
|
|
34
|
+
if not multi:
|
|
35
|
+
multi = self._uncommenter(line)[1]
|
|
36
|
+
new_lines.append(self._uncommenter(line)[0])
|
|
37
|
+
else:
|
|
38
|
+
multi = self._unmultiliner(line)[1]
|
|
39
|
+
new_lines.append(self._unmultiliner(line)[0])
|
|
40
|
+
return new_lines
|
|
41
|
+
|
|
42
|
+
def _uncommenter(self, line):
|
|
43
|
+
if re.match(r'.*<!---.*-->', line): # inline(could start multiline)
|
|
44
|
+
return self._uncommenter(re.sub(r'\s*<!---.*?-->', '', line))
|
|
45
|
+
elif re.match(r'.*<!---', line): # start multiline
|
|
46
|
+
return [re.sub(r'\s*<!---.*', '', line), True]
|
|
47
|
+
else: # no comment
|
|
48
|
+
return [line, False]
|
|
49
|
+
|
|
50
|
+
def _unmultiliner(self, line):
|
|
51
|
+
if re.match(r'.*-->', line): # end multiline (could start comment)
|
|
52
|
+
return self._uncommenter(re.sub(r'.*?-->', '', line, count=1))
|
|
53
|
+
else:
|
|
54
|
+
return ['', True] # continue multiline
|
onya/graph.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Graph and other fundamental classes for Onya
|
|
3
|
+
|
|
4
|
+
The Onya graph model:
|
|
5
|
+
- A graph is a collection of nodes
|
|
6
|
+
- Each node has:
|
|
7
|
+
- An identifier (IRI)
|
|
8
|
+
- A set of types (IRIs)
|
|
9
|
+
- A set of assertions (edges and properties)
|
|
10
|
+
- Edges connect nodes to nodes via IRI labels
|
|
11
|
+
- Properties connect nodes to string values via IRI labels
|
|
12
|
+
- Both edges and properties are collectively called "assertions"
|
|
13
|
+
- Each assertion is in effect an anonymous node defined by (origin IRI, assertion IRI)
|
|
14
|
+
- Assertions can themselves be origins for further assertions (natural recursiveness)
|
|
15
|
+
'''
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
from collections.abc import MutableMapping, Iterator
|
|
19
|
+
from abc import ABC
|
|
20
|
+
|
|
21
|
+
from amara.iri import I
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class assertions_mixin:
|
|
25
|
+
'''
|
|
26
|
+
Mixin for objects that can have assertions (edges and properties)
|
|
27
|
+
'''
|
|
28
|
+
def add_property(self, label: I | str, value: str):
|
|
29
|
+
p = property_(self, label, value)
|
|
30
|
+
self.properties.add(p)
|
|
31
|
+
return p
|
|
32
|
+
|
|
33
|
+
def add_edge(self, label: I | str, target: 'node'):
|
|
34
|
+
e = edge(self, label, target)
|
|
35
|
+
self.edges.add(e)
|
|
36
|
+
return e
|
|
37
|
+
|
|
38
|
+
def remove_property(self, prop: 'property_'):
|
|
39
|
+
self.properties.remove(prop)
|
|
40
|
+
|
|
41
|
+
def remove_edge(self, edge_: 'edge'):
|
|
42
|
+
self.edges.remove(edge_)
|
|
43
|
+
|
|
44
|
+
def getprop(self, label: I | str):
|
|
45
|
+
'''Get properties with a given label'''
|
|
46
|
+
for prop in self.properties:
|
|
47
|
+
if prop.label == label:
|
|
48
|
+
yield prop
|
|
49
|
+
|
|
50
|
+
def getedge(self, label: I | str):
|
|
51
|
+
'''Get edges with a given label'''
|
|
52
|
+
for edge_ in self.edges:
|
|
53
|
+
if edge_.label == label:
|
|
54
|
+
yield edge_
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class node(assertions_mixin):
|
|
58
|
+
'''
|
|
59
|
+
A node in the Onya graph.
|
|
60
|
+
|
|
61
|
+
A node has an identifier (IRI), optional types (IRIs), and assertions
|
|
62
|
+
(edges and properties). Both edges and properties are sets, not sequences,
|
|
63
|
+
because pervasive ordering is not a core requirement of the model.
|
|
64
|
+
'''
|
|
65
|
+
__slots__ = ['id', 'types', 'properties', 'edges']
|
|
66
|
+
|
|
67
|
+
def __init__(self, id_: I | str, types: I | str | set[I | str] | None = None):
|
|
68
|
+
self.id = id_
|
|
69
|
+
if isinstance(types, str):
|
|
70
|
+
types = I(types)
|
|
71
|
+
if isinstance(types, I):
|
|
72
|
+
types = {types}
|
|
73
|
+
self.types: set[I | str] = set(types) if types else set()
|
|
74
|
+
self.properties: set['property_'] = set()
|
|
75
|
+
self.edges: set['edge'] = set()
|
|
76
|
+
|
|
77
|
+
def traverse(self, label: I | str) -> Iterator['edge']:
|
|
78
|
+
'''Find edges with a given label'''
|
|
79
|
+
for e in self.edges:
|
|
80
|
+
if e.label == label:
|
|
81
|
+
yield e
|
|
82
|
+
|
|
83
|
+
def reverse(self, label: I | str, graph: 'graph') -> Iterator['edge']:
|
|
84
|
+
'''Find edges targeting this node with a given label (requires graph access)'''
|
|
85
|
+
for nid, nobj in graph.nodes.items():
|
|
86
|
+
for e in nobj.traverse(label):
|
|
87
|
+
if e.target == self:
|
|
88
|
+
yield e
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class assertion(assertions_mixin, ABC):
|
|
92
|
+
'''
|
|
93
|
+
Abstract base class for assertions (edges and properties)
|
|
94
|
+
|
|
95
|
+
Each assertion is an anonymous node defined by the combination of
|
|
96
|
+
its origin and its label IRI.
|
|
97
|
+
'''
|
|
98
|
+
__slots__ = ['origin', 'label', 'properties', 'edges']
|
|
99
|
+
|
|
100
|
+
def __init__(self, origin: 'node | assertion', label: I | str):
|
|
101
|
+
self.origin = origin
|
|
102
|
+
self.label = label
|
|
103
|
+
self.properties: set['property_'] = set()
|
|
104
|
+
self.edges: set['edge'] = set()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class property_(assertion):
|
|
108
|
+
'''
|
|
109
|
+
A property assertion connects an origin to a string value via an IRI label.
|
|
110
|
+
|
|
111
|
+
Properties in Onya are simple: they always have string values.
|
|
112
|
+
No numbers, dates, or other types at the core layer - those can be
|
|
113
|
+
handled by annotation systems built on top.
|
|
114
|
+
'''
|
|
115
|
+
__slots__ = ['value']
|
|
116
|
+
|
|
117
|
+
def __init__(self, origin: 'node | assertion', label: I | str, value: str):
|
|
118
|
+
super().__init__(origin, label)
|
|
119
|
+
self.value: str = value
|
|
120
|
+
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
return f'property_({self.label}={self.value!r})'
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class edge(assertion):
|
|
126
|
+
'''
|
|
127
|
+
An edge assertion connects an origin node to a target node via an IRI label.
|
|
128
|
+
'''
|
|
129
|
+
__slots__ = ['target']
|
|
130
|
+
|
|
131
|
+
def __init__(self, origin: 'node | assertion', label: I | str, target: 'node'):
|
|
132
|
+
super().__init__(origin, label)
|
|
133
|
+
self.target: 'node' = target
|
|
134
|
+
|
|
135
|
+
def __repr__(self):
|
|
136
|
+
return f'edge({self.label} -> {self.target.id})'
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class graph(MutableMapping):
|
|
140
|
+
'''
|
|
141
|
+
A collection of nodes managed and queried together.
|
|
142
|
+
|
|
143
|
+
This is the top-level container for an Onya graph.
|
|
144
|
+
'''
|
|
145
|
+
def __init__(self, nodes: list[node] = ()):
|
|
146
|
+
self.nodes: dict[I | str, node] = {}
|
|
147
|
+
self.nodes.update({n.id: n for n in nodes})
|
|
148
|
+
|
|
149
|
+
def __getitem__(self, key: I | str) -> node:
|
|
150
|
+
return self.nodes[key]
|
|
151
|
+
|
|
152
|
+
def __delitem__(self, nid: I | str) -> None:
|
|
153
|
+
del self.nodes[nid]
|
|
154
|
+
|
|
155
|
+
def __setitem__(self, nid: I | str, nobj: node) -> None:
|
|
156
|
+
self.nodes[nid] = nobj
|
|
157
|
+
|
|
158
|
+
def __iter__(self) -> Iterator[I | str]:
|
|
159
|
+
return iter(self.nodes)
|
|
160
|
+
|
|
161
|
+
def __len__(self) -> int:
|
|
162
|
+
return len(self.nodes)
|
|
163
|
+
|
|
164
|
+
def __repr__(self) -> str:
|
|
165
|
+
return f'{type(self).__name__} with {len(self.nodes)} nodes'
|
|
166
|
+
|
|
167
|
+
def node(self, nid: I | str, types: I | str | set[I | str] | None = None) -> node:
|
|
168
|
+
'''
|
|
169
|
+
Convenience for constructing, then adding a new node to the graph
|
|
170
|
+
'''
|
|
171
|
+
n = node(nid, types)
|
|
172
|
+
self[nid] = n
|
|
173
|
+
return n
|
|
174
|
+
|
|
175
|
+
def typematch(self, types: I | str | set[I | str]) -> Iterator[node]:
|
|
176
|
+
'''Find nodes with matching types'''
|
|
177
|
+
if isinstance(types, (str, I)):
|
|
178
|
+
types = {types}
|
|
179
|
+
types_set = set(types)
|
|
180
|
+
for n in self.nodes.values():
|
|
181
|
+
if n.types & types_set:
|
|
182
|
+
yield n
|
onya/serial/__init__.py
ADDED
onya/serial/literate.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# onya.serial.literate
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Serialize and deserialize between an Onya model and Onya Literate (Markdown)
|
|
5
|
+
|
|
6
|
+
see: doc/literate_format.md
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from amara import iri
|
|
13
|
+
|
|
14
|
+
from onya import I, ONYA_BASEIRI
|
|
15
|
+
|
|
16
|
+
# from onya.serial.litparse_util import parse
|
|
17
|
+
|
|
18
|
+
TYPE_REL = I(iri.absolutize('type', ONYA_BASEIRI))
|
|
19
|
+
|
|
20
|
+
__all__ = ['read', 'write',
|
|
21
|
+
# Non-standard
|
|
22
|
+
'longtext',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def longtext(t):
|
|
27
|
+
'''
|
|
28
|
+
Prepare long text to be e.g. included as an Onya literate property value,
|
|
29
|
+
according to markdown rules
|
|
30
|
+
|
|
31
|
+
Only use this function if you're Ok with possible whitespace-specific changes
|
|
32
|
+
|
|
33
|
+
>>> from onya.serial.literate import longtext
|
|
34
|
+
>>> longtext()
|
|
35
|
+
'''
|
|
36
|
+
# >>> markdown.markdown('* abc\ndef\nghi')
|
|
37
|
+
# '<ul>\n<li>abc\ndef\nghi</li>\n</ul>'
|
|
38
|
+
# >>> markdown.markdown('* abc\n\ndef\n\nghi')
|
|
39
|
+
# '<ul>\n<li>abc</li>\n</ul>\n<p>def</p>\n<p>ghi</p>'
|
|
40
|
+
# >>> markdown.markdown('* abc\n\n def\n\n ghi')
|
|
41
|
+
# '<ul>\n<li>\n<p>abc</p>\n<p>def</p>\n<p>ghi</p>\n</li>\n</ul>'
|
|
42
|
+
|
|
43
|
+
# Insert blank line after list item & before start of secondary paragraph.
|
|
44
|
+
# Indent the line with at least one space to ensure it is indented as part of the list.
|
|
45
|
+
endswith_cr = t[-1] == '\n'
|
|
46
|
+
new_t = t.replace('\n', '\n ')
|
|
47
|
+
if endswith_cr:
|
|
48
|
+
new_t = new_t[:-5]
|
|
49
|
+
return new_t
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def abbreviate(rel, bases):
|
|
53
|
+
for base in bases:
|
|
54
|
+
abbr = iri.relativize(rel, base, subPathOnly=True)
|
|
55
|
+
if abbr:
|
|
56
|
+
if base is ONYA_BASEIRI:
|
|
57
|
+
abbr = '@' + abbr
|
|
58
|
+
return abbr
|
|
59
|
+
return I(rel)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def value_format(val):
|
|
63
|
+
if isinstance(val, I):
|
|
64
|
+
return f'<{val}>'
|
|
65
|
+
else:
|
|
66
|
+
return f'"{val}"'
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def write(model, out=sys.stdout, base=None, propertybase=None, shorteners=None):
|
|
70
|
+
'''
|
|
71
|
+
models - input Onya model from which output is generated
|
|
72
|
+
'''
|
|
73
|
+
shorteners = shorteners or {}
|
|
74
|
+
|
|
75
|
+
all_propertybase = [propertybase] if propertybase else []
|
|
76
|
+
all_propertybase.append(ONYA_BASEIRI)
|
|
77
|
+
|
|
78
|
+
if any((base, propertybase, shorteners)):
|
|
79
|
+
out.write('# @docheader\n\n* @iri:\n')
|
|
80
|
+
if base:
|
|
81
|
+
out.write(' * @base: {0}'.format(base))
|
|
82
|
+
#for k, v in shorteners:
|
|
83
|
+
# out.write(' * @base: {0}'.format(base))
|
|
84
|
+
|
|
85
|
+
out.write('\n\n')
|
|
86
|
+
|
|
87
|
+
# Get all origin nodes from the model (all node IDs in the graph)
|
|
88
|
+
origin_space = set(model.keys()) if hasattr(model, 'keys') else set()
|
|
89
|
+
|
|
90
|
+
for o in origin_space:
|
|
91
|
+
out.write('# {0}\n\n'.format(o))
|
|
92
|
+
for o_, r, t, a in model.match(o):
|
|
93
|
+
rendered_r = abbreviate(r, all_propertybase)
|
|
94
|
+
if isinstance(rendered_r, I):
|
|
95
|
+
rendered_r = f'<{rendered_r}>'
|
|
96
|
+
value_format(t)
|
|
97
|
+
out.write(f'* {rendered_r}: {value_format(t)}\n')
|
|
98
|
+
for k, v in a.items():
|
|
99
|
+
rendered_k = abbreviate(k, all_propertybase)
|
|
100
|
+
if isinstance(rendered_k, I):
|
|
101
|
+
rendered_r = f'<{rendered_k}>'
|
|
102
|
+
out.write(f' * {rendered_k}: {value_format(t)}\n')
|
|
103
|
+
|
|
104
|
+
out.write('\n')
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def read(fp, g):
|
|
109
|
+
'''
|
|
110
|
+
Read Onya Literate format from file pointer into graph
|
|
111
|
+
|
|
112
|
+
fp -- file pointer to read from
|
|
113
|
+
g -- graph to populate
|
|
114
|
+
'''
|
|
115
|
+
# TODO: Implement this using literate_lex.parse
|
|
116
|
+
pass
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# onya.serial.literate_lex.py
|
|
3
|
+
'''
|
|
4
|
+
Main body of the Onya Literate parser
|
|
5
|
+
|
|
6
|
+
Onya Literate, or Onya Lit, is a Markdown-based format
|
|
7
|
+
|
|
8
|
+
Proper entry point of use is onya.serial.literate
|
|
9
|
+
|
|
10
|
+
see: doc/literate_format.md
|
|
11
|
+
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum
|
|
17
|
+
|
|
18
|
+
from amara import iri # for absolutize & matches_uri_syntax
|
|
19
|
+
|
|
20
|
+
from onya import I, ONYA_BASEIRI, ONYA_NULL, LITERAL
|
|
21
|
+
|
|
22
|
+
from pyparsing import (
|
|
23
|
+
ParserElement, Literal, htmlComment, Optional, Word, alphas, alphanums,
|
|
24
|
+
Combine, MatchFirst, QuotedString, Regex, ZeroOrMore, White, Suppress,
|
|
25
|
+
Group, delimited_list, Forward, OneOrMore, rest_of_line
|
|
26
|
+
) # pip install pyparsing
|
|
27
|
+
ParserElement.setDefaultWhitespaceChars(' \t')
|
|
28
|
+
|
|
29
|
+
URI_ABBR_PAT = re.compile('@([\\-_\\w]+)([#/@])(.+)', re.DOTALL)
|
|
30
|
+
URI_EXPLICIT_PAT = re.compile('<(.+)>', re.DOTALL)
|
|
31
|
+
|
|
32
|
+
TYPE_REL = ONYA_BASEIRI('type')
|
|
33
|
+
|
|
34
|
+
class value_type(Enum):
|
|
35
|
+
'''
|
|
36
|
+
Basic typing info for values (really just text vs node reference)
|
|
37
|
+
'''
|
|
38
|
+
TEXT_VAL = 1
|
|
39
|
+
RES_VAL = 2
|
|
40
|
+
UNKNOWN_VAL = 3
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# For parse trees
|
|
44
|
+
@dataclass
|
|
45
|
+
class prop_info:
|
|
46
|
+
indent: int = None #
|
|
47
|
+
key: str = None #
|
|
48
|
+
value: list = None #
|
|
49
|
+
children: list = None #
|
|
50
|
+
is_text_ref: bool = False # True if this is a text reference (uses ::)
|
|
51
|
+
multiline_text: str = None # For storing multiline text content
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class doc_info:
|
|
56
|
+
iri: str = None # iri of the doc being parsed, itself
|
|
57
|
+
nodebase: str = None # used to resolve relative node IRIs
|
|
58
|
+
schemabase: str = None # used to resolve relative schema IRIs
|
|
59
|
+
typebase: str = None # used to resolve relative type IRIs
|
|
60
|
+
lang: str = None # other IRI abbreviations
|
|
61
|
+
iris: dict = None # iterpretations of untyped values (e.g. string vs list of strs vs IRI)
|
|
62
|
+
text_refs: dict = None # text references defined with :name = """content"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class value_info:
|
|
67
|
+
verbatim: int = None # Literal value input text
|
|
68
|
+
typeindic: int = None # Value type indicator (from value_type enum)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _make_tree(string, location, tokens):
|
|
72
|
+
'''
|
|
73
|
+
Parse action to return a parsed tree node from tokens
|
|
74
|
+
'''
|
|
75
|
+
return prop_info(indent=len(tokens[0]), key=tokens[1],
|
|
76
|
+
value=tokens[2], children=None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _make_value(string, location, tokens):
|
|
80
|
+
'''
|
|
81
|
+
Parse action to make sure the right type of value is created during parse
|
|
82
|
+
'''
|
|
83
|
+
val = tokens[0]
|
|
84
|
+
# Must check IRI first, since it is a subclass of str
|
|
85
|
+
if isinstance(val, I):
|
|
86
|
+
typeindic = value_type.RES_VAL
|
|
87
|
+
elif isinstance(val, LITERAL):
|
|
88
|
+
typeindic = value_type.TEXT_VAL
|
|
89
|
+
elif isinstance(val, str):
|
|
90
|
+
val = val.strip()
|
|
91
|
+
typeindic = value_type.UNKNOWN_VAL
|
|
92
|
+
|
|
93
|
+
return value_info(verbatim=val, typeindic=typeindic)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def literal_parse_action(toks):
|
|
97
|
+
'''
|
|
98
|
+
Parse action to coerce to literal value
|
|
99
|
+
'''
|
|
100
|
+
return LITERAL(toks[0])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def iriref_parse_action(toks):
|
|
104
|
+
'''
|
|
105
|
+
Parse action to coerce to IRI reference value
|
|
106
|
+
'''
|
|
107
|
+
return I(toks[0])
|
|
108
|
+
|
|
109
|
+
RIGHT_ARROW = Literal('->') | Literal('→') # U+2192
|
|
110
|
+
DOUBLE_COLON = Literal('::') # For text references
|
|
111
|
+
|
|
112
|
+
COMMENT = htmlComment # Using HTML-style comments for cleaner markdown compatibility
|
|
113
|
+
OPCOMMENT = Optional(COMMENT)
|
|
114
|
+
IDENT = Word(alphas, alphanums + '_' + '-')
|
|
115
|
+
IDENT_KEY = Combine(Optional('@') + IDENT).leaveWhitespace()
|
|
116
|
+
# EXPLICIT_IRI = QuotedString('<', end_quote_char='>')
|
|
117
|
+
QUOTED_STRING = MatchFirst((QuotedString('"', escChar='\\'), QuotedString("'", escChar='\\'))) \
|
|
118
|
+
.setParseAction(literal_parse_action)
|
|
119
|
+
# Triple-quoted strings for text references - handle multiline properly
|
|
120
|
+
TRIPLE_QUOTED_STRING = Regex(r'"""([^"]*(?:"[^"]*)*?)"""', re.DOTALL) \
|
|
121
|
+
.setParseAction(lambda tokens: LITERAL(tokens[0]))
|
|
122
|
+
# See: https://rdflib.readthedocs.io/en/stable/_modules/rdflib/plugins/sparql/parser.html
|
|
123
|
+
IRIREF = Regex(r'[^<>"{}|^`\\\[\]%s]*' % "".join(
|
|
124
|
+
"\\x%02X" % i for i in range(33)
|
|
125
|
+
)) \
|
|
126
|
+
.setParseAction(iriref_parse_action)
|
|
127
|
+
#REST_OF_LINE = rest_of_line.leave_whitespace()
|
|
128
|
+
|
|
129
|
+
blank_to_eol = ZeroOrMore(COMMENT) + White('\n')
|
|
130
|
+
explicit_iriref = Combine(Suppress("<") + IRIREF + Suppress(">")) \
|
|
131
|
+
.setParseAction(iriref_parse_action)
|
|
132
|
+
|
|
133
|
+
# Text reference definition: :name = """content"""
|
|
134
|
+
text_ref_def = Suppress(':') + IDENT + Suppress('=') + TRIPLE_QUOTED_STRING
|
|
135
|
+
|
|
136
|
+
value_expr = ( explicit_iriref + Suppress(ZeroOrMore(COMMENT)) ) | ( QUOTED_STRING + Suppress(ZeroOrMore(COMMENT)) ) | rest_of_line # noqa: E501
|
|
137
|
+
prop = Optional(White(' \t').leaveWhitespace(), '') + Suppress('*' + White()) + \
|
|
138
|
+
( explicit_iriref | IDENT_KEY | IRIREF ) + Suppress(':') + Optional(value_expr, None)
|
|
139
|
+
# Text reference property: label:: reference_name
|
|
140
|
+
prop_text_ref = Optional(White(' \t').leaveWhitespace(), '') + Suppress('*' + White()) + \
|
|
141
|
+
( explicit_iriref | IDENT_KEY | IRIREF ) + Suppress(DOUBLE_COLON) + Optional(IRIREF, None)
|
|
142
|
+
edge = Optional(White(' \t').leaveWhitespace(), '') + Suppress('*' + White()) + \
|
|
143
|
+
( explicit_iriref | IDENT_KEY | IRIREF ) + Suppress(RIGHT_ARROW) + Optional(value_expr, None)
|
|
144
|
+
propset = Group(delimited_list(prop_text_ref | prop | edge | COMMENT, delim='\n'))
|
|
145
|
+
node_header = Word('#') + Optional(IRIREF, None) + Optional(QuotedString('[', end_quote_char=']'), None)
|
|
146
|
+
node_block = Forward()
|
|
147
|
+
node_block << Group(node_header + White('\n').suppress() + Suppress(ZeroOrMore(blank_to_eol)) + propset)
|
|
148
|
+
|
|
149
|
+
# Start symbol - allow text reference definitions anywhere
|
|
150
|
+
node_seq = OneOrMore(
|
|
151
|
+
Suppress(ZeroOrMore(blank_to_eol)) + \
|
|
152
|
+
(node_block | text_ref_def) + Optional(White('\n')).suppress() + \
|
|
153
|
+
Suppress(ZeroOrMore(blank_to_eol))
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _make_text_ref_tree(string, location, tokens):
|
|
157
|
+
'''
|
|
158
|
+
Parse action to return a parsed tree node for text references
|
|
159
|
+
'''
|
|
160
|
+
return prop_info(indent=len(tokens[0]), key=tokens[1],
|
|
161
|
+
value=tokens[2] if len(tokens) > 2 else None, children=None,
|
|
162
|
+
is_text_ref=True)
|
|
163
|
+
|
|
164
|
+
def parse_multiline_text(lines, start_idx, current_indent):
|
|
165
|
+
"""
|
|
166
|
+
Parse multiline text that continues after a property definition.
|
|
167
|
+
Returns (text_content, next_line_idx)
|
|
168
|
+
"""
|
|
169
|
+
if start_idx >= len(lines):
|
|
170
|
+
return "", start_idx
|
|
171
|
+
|
|
172
|
+
text_lines = []
|
|
173
|
+
i = start_idx
|
|
174
|
+
|
|
175
|
+
while i < len(lines):
|
|
176
|
+
line = lines[i]
|
|
177
|
+
|
|
178
|
+
# Skip empty lines
|
|
179
|
+
if not line.strip():
|
|
180
|
+
i += 1
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
# Check if this line is indented enough to be part of the multiline text
|
|
184
|
+
# Must be indented more than the current property level
|
|
185
|
+
line_indent = len(line) - len(line.lstrip())
|
|
186
|
+
if line_indent > current_indent:
|
|
187
|
+
# This is a continuation line
|
|
188
|
+
text_lines.append(line[current_indent:]) # Remove the base indentation
|
|
189
|
+
i += 1
|
|
190
|
+
else:
|
|
191
|
+
# This line is not indented enough, stop parsing multiline text
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
return '\n'.join(text_lines), i
|
|
195
|
+
|
|
196
|
+
def _make_text_ref_def(string, location, tokens):
|
|
197
|
+
'''
|
|
198
|
+
Parse action for text reference definitions
|
|
199
|
+
'''
|
|
200
|
+
return ('text_ref_def', tokens[0], tokens[1])
|
|
201
|
+
|
|
202
|
+
prop.setParseAction(_make_tree)
|
|
203
|
+
prop_text_ref.setParseAction(_make_text_ref_tree)
|
|
204
|
+
edge.setParseAction(_make_tree)
|
|
205
|
+
text_ref_def.setParseAction(_make_text_ref_def)
|
|
206
|
+
value_expr.setParseAction(_make_value)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def parse(lit_text, graph_obj, encoding='utf-8'):
|
|
210
|
+
"""
|
|
211
|
+
Translate Onya Literate text into Onya graph
|
|
212
|
+
|
|
213
|
+
lit_text -- Onya Literate source text
|
|
214
|
+
graph_obj -- Onya graph to populate
|
|
215
|
+
encoding -- character encoding (defaults to UTF-8)
|
|
216
|
+
|
|
217
|
+
Returns: The document IRI from @document header, or None
|
|
218
|
+
|
|
219
|
+
>>> from onya.driver.memory import newmodel
|
|
220
|
+
>>> from onya.serial.literate import parse # Delegates to literate_lex.parse
|
|
221
|
+
>>> m = newmodel()
|
|
222
|
+
>>> parse(open('test/resource/poetry.onya').read(), m)
|
|
223
|
+
'http://uche.ogbuji.net/poems/'
|
|
224
|
+
>>> m.size()
|
|
225
|
+
40
|
|
226
|
+
>>> next(m.match(None, 'http://uche.ogbuji.net/poems/updated', '2013-10-15'))
|
|
227
|
+
(I(http://uche.ogbuji.net/poems/1), I(http://uche.ogbuji.net/poems/updated), '2013-10-15', {})
|
|
228
|
+
"""
|
|
229
|
+
# Set up document parameters
|
|
230
|
+
doc = doc_info()
|
|
231
|
+
doc.iris = {} # Initialize the iris dictionary
|
|
232
|
+
doc.text_refs = {} # Initialize the text references dictionary
|
|
233
|
+
|
|
234
|
+
parsed = node_seq.parseString(lit_text, parseAll=True)
|
|
235
|
+
|
|
236
|
+
# First pass: collect all text reference definitions
|
|
237
|
+
for item in parsed:
|
|
238
|
+
if isinstance(item, tuple) and item[0] == 'text_ref_def':
|
|
239
|
+
ref_name, ref_content = item[1], item[2]
|
|
240
|
+
doc.text_refs[ref_name] = str(ref_content)
|
|
241
|
+
|
|
242
|
+
# Second pass: process node blocks
|
|
243
|
+
for item in parsed:
|
|
244
|
+
if not (isinstance(item, tuple) and item[0] == 'text_ref_def'):
|
|
245
|
+
# Handle node blocks
|
|
246
|
+
process_nodeblock(item, graph_obj, doc)
|
|
247
|
+
|
|
248
|
+
return doc.iri
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def expand_iri(iri_in, base, nodecontext=None, doc=None):
|
|
252
|
+
if iri_in is None:
|
|
253
|
+
return ONYA_NULL
|
|
254
|
+
# Abreviation for special, Onya-specific properties
|
|
255
|
+
if iri_in.startswith('@'):
|
|
256
|
+
return I(iri.absolutize(iri_in[1:], ONYA_BASEIRI))
|
|
257
|
+
|
|
258
|
+
# Is it an explicit IRI (i.e. with <…>)?
|
|
259
|
+
if iri_match := URI_EXPLICIT_PAT.match(iri_in):
|
|
260
|
+
return iri_match.group(1) if base is None else I(iri.absolutize(iri_match.group(1), base))
|
|
261
|
+
|
|
262
|
+
# XXX Clarify this bit?
|
|
263
|
+
if iri_match := URI_ABBR_PAT.match(iri_in):
|
|
264
|
+
if doc is None or doc.iris is None:
|
|
265
|
+
raise ValueError(f'IRI abbreviation "{iri_match.group(1)}" used but no doc context provided')
|
|
266
|
+
uri = doc.iris[iri_match.group(1)]
|
|
267
|
+
fulliri = URI_ABBR_PAT.sub(uri + '\\2\\3', iri_in)
|
|
268
|
+
else:
|
|
269
|
+
# Replace upstream ValueError with our own
|
|
270
|
+
if nodecontext and not(iri.matches_uri_ref_syntax(iri_in)):
|
|
271
|
+
# FIXME: Replace with a Onya-specific error
|
|
272
|
+
raise ValueError(f'Invalid IRI reference provided for node context {nodecontext}: "{iri_in}"')
|
|
273
|
+
fulliri = iri_in if base is None else I(iri.absolutize(iri_in, base))
|
|
274
|
+
return I(fulliri)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def process_nodeblock(nodeblock, graph_obj, doc):
|
|
278
|
+
headermarks, nid, ntype, props = nodeblock
|
|
279
|
+
|
|
280
|
+
if nid == '@docheader':
|
|
281
|
+
process_docheader(props, graph_obj, doc)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
nid = expand_iri(nid, doc.nodebase, doc=doc)
|
|
285
|
+
|
|
286
|
+
# Get or create the node
|
|
287
|
+
if nid not in graph_obj:
|
|
288
|
+
n = graph_obj.node(nid)
|
|
289
|
+
else:
|
|
290
|
+
n = graph_obj[nid]
|
|
291
|
+
|
|
292
|
+
# Add type if specified
|
|
293
|
+
if ntype:
|
|
294
|
+
type_iri = expand_iri(ntype, doc.typebase, doc=doc)
|
|
295
|
+
n.types.add(type_iri)
|
|
296
|
+
|
|
297
|
+
# Track current assertion for nested assertions
|
|
298
|
+
outer_indent = -1
|
|
299
|
+
current_assertion = None
|
|
300
|
+
|
|
301
|
+
for prop_info in props:
|
|
302
|
+
if isinstance(prop_info, str):
|
|
303
|
+
# Just a comment. Skip.
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# Handle text reference definitions
|
|
307
|
+
if isinstance(prop_info, tuple) and prop_info[0] == 'text_ref_def':
|
|
308
|
+
ref_name, ref_content = prop_info[1], prop_info[2]
|
|
309
|
+
doc.text_refs[ref_name] = str(ref_content)
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
# First assertion encountered determines outer indent
|
|
313
|
+
if outer_indent == -1:
|
|
314
|
+
outer_indent = prop_info.indent
|
|
315
|
+
|
|
316
|
+
# Expand the assertion label IRI
|
|
317
|
+
assertion_label = expand_iri(prop_info.key, doc.schemabase, doc=doc)
|
|
318
|
+
|
|
319
|
+
if prop_info.indent == outer_indent:
|
|
320
|
+
# This is a top-level assertion
|
|
321
|
+
if prop_info.is_text_ref:
|
|
322
|
+
# Handle text reference
|
|
323
|
+
ref_name = str(prop_info.value) if prop_info.value else None
|
|
324
|
+
if ref_name and ref_name in doc.text_refs:
|
|
325
|
+
str_val = doc.text_refs[ref_name]
|
|
326
|
+
current_assertion = n.add_property(assertion_label, str_val)
|
|
327
|
+
else:
|
|
328
|
+
# Text reference not found, skip or use empty string
|
|
329
|
+
current_assertion = n.add_property(assertion_label, "")
|
|
330
|
+
elif prop_info.value:
|
|
331
|
+
val, typeindic = prop_info.value.verbatim, prop_info.value.typeindic
|
|
332
|
+
|
|
333
|
+
if typeindic == value_type.RES_VAL:
|
|
334
|
+
# This is an edge (node to node)
|
|
335
|
+
target_id = expand_iri(val, doc.typebase, doc=doc)
|
|
336
|
+
# Get or create Janelaet node
|
|
337
|
+
if target_id not in graph_obj:
|
|
338
|
+
target_node = graph_obj.node(target_id)
|
|
339
|
+
else:
|
|
340
|
+
target_node = graph_obj[target_id]
|
|
341
|
+
# Add edge and track it for nested assertions
|
|
342
|
+
current_assertion = n.add_edge(assertion_label, target_node)
|
|
343
|
+
elif typeindic == value_type.TEXT_VAL or typeindic == value_type.UNKNOWN_VAL:
|
|
344
|
+
# This is a property (node to string value)
|
|
345
|
+
str_val = str(val)
|
|
346
|
+
current_assertion = n.add_property(assertion_label, str_val)
|
|
347
|
+
|
|
348
|
+
else:
|
|
349
|
+
# This is a nested assertion on current_assertion
|
|
350
|
+
if current_assertion is None:
|
|
351
|
+
continue # Skip nested without a parent
|
|
352
|
+
|
|
353
|
+
if prop_info.is_text_ref:
|
|
354
|
+
# Handle nested text reference
|
|
355
|
+
ref_name = str(prop_info.value) if prop_info.value else None
|
|
356
|
+
if ref_name and ref_name in doc.text_refs:
|
|
357
|
+
str_val = doc.text_refs[ref_name]
|
|
358
|
+
current_assertion.add_property(assertion_label, str_val)
|
|
359
|
+
elif prop_info.value:
|
|
360
|
+
val, typeindic = prop_info.value.verbatim, prop_info.value.typeindic
|
|
361
|
+
if typeindic == value_type.RES_VAL:
|
|
362
|
+
# Nested edge
|
|
363
|
+
target_id = expand_iri(val, doc.typebase, doc=doc)
|
|
364
|
+
if target_id not in graph_obj:
|
|
365
|
+
target_node = graph_obj.node(target_id)
|
|
366
|
+
else:
|
|
367
|
+
target_node = graph_obj[target_id]
|
|
368
|
+
current_assertion.add_edge(assertion_label, target_node)
|
|
369
|
+
elif typeindic == value_type.TEXT_VAL or typeindic == value_type.UNKNOWN_VAL:
|
|
370
|
+
# Nested property
|
|
371
|
+
str_val = str(val)
|
|
372
|
+
current_assertion.add_property(assertion_label, str_val)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def process_docheader(props, graph_obj, doc):
|
|
376
|
+
outer_indent = -1
|
|
377
|
+
current_outer_prop = None
|
|
378
|
+
for prop in props:
|
|
379
|
+
# Skip comments
|
|
380
|
+
if isinstance(prop, str):
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
# @iri section is where key IRI prefixes can be set
|
|
384
|
+
# First property encountered determines outer indent
|
|
385
|
+
if outer_indent == -1:
|
|
386
|
+
outer_indent = prop.indent
|
|
387
|
+
if prop.indent == outer_indent:
|
|
388
|
+
current_outer_prop = prop
|
|
389
|
+
#Setting an IRI for this very document being parsed
|
|
390
|
+
if prop.key == '@document':
|
|
391
|
+
doc.iri = prop.value.verbatim if prop.value else None
|
|
392
|
+
elif prop.key == '@language':
|
|
393
|
+
doc.lang = prop.value.verbatim if prop.value else None
|
|
394
|
+
elif prop.key == '@base':
|
|
395
|
+
doc.nodebase = doc.typebase = prop.value.verbatim if prop.value else None
|
|
396
|
+
elif prop.key == '@schema':
|
|
397
|
+
doc.schemabase = prop.value.verbatim if prop.value else None
|
|
398
|
+
elif prop.key == '@resource-type' or prop.key == '@type-base':
|
|
399
|
+
doc.typebase = prop.value.verbatim if prop.value else None
|
|
400
|
+
#If we have a document node to which to attach them, just attach all other properties
|
|
401
|
+
elif doc.iri:
|
|
402
|
+
# Create document node if needed
|
|
403
|
+
if doc.iri not in graph_obj:
|
|
404
|
+
doc_node = graph_obj.node(doc.iri)
|
|
405
|
+
else:
|
|
406
|
+
doc_node = graph_obj[doc.iri]
|
|
407
|
+
|
|
408
|
+
fullprop = I(iri.absolutize(prop.key, doc.schemabase))
|
|
409
|
+
if prop.value:
|
|
410
|
+
doc_node.add_property(fullprop, prop.value.verbatim)
|
|
411
|
+
else:
|
|
412
|
+
# Handle nested properties (attributes)
|
|
413
|
+
if current_outer_prop and current_outer_prop.key == '@iri':
|
|
414
|
+
k, uri = prop.key, prop.value.verbatim if prop.value else None
|
|
415
|
+
if k == '@base':
|
|
416
|
+
doc.nodebase = doc.typebase = uri
|
|
417
|
+
elif k == '@schema':
|
|
418
|
+
doc.schemabase = uri
|
|
419
|
+
elif k == '@resource-type' or k == '@type-base':
|
|
420
|
+
doc.typebase = uri
|
|
421
|
+
else:
|
|
422
|
+
doc.iris[k] = uri
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
'''
|
|
427
|
+
def handle_resourceset(ltext, **kwargs):
|
|
428
|
+
'Helper that converts sets of resources from a textual format such as Markdown, including absolutizing relative IRIs'
|
|
429
|
+
fullprop=kwargs.get('fullprop')
|
|
430
|
+
rid=kwargs.get('rid')
|
|
431
|
+
base=kwargs.get('base', ONYA_BASEIRI)
|
|
432
|
+
model=kwargs.get('model')
|
|
433
|
+
iris = ltext.strip().split()
|
|
434
|
+
for i in iris:
|
|
435
|
+
model.add(rid, fullprop, I(iri.absolutize(i, base)))
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
PREP_METHODS = {
|
|
440
|
+
ONYA_BASEIRI + 'text': lambda x, **kwargs: x,
|
|
441
|
+
# '@text': lambda x, **kwargs: x,
|
|
442
|
+
ONYA_BASEIRI + 'resource': lambda x, base=ONYA_BASEIRI, **kwargs: I(iri.absolutize(x, base)),
|
|
443
|
+
ONYA_BASEIRI + 'resourceset': handle_resourceset,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
from onya.driver.memory import newmodel
|
|
447
|
+
m = newmodel()
|
|
448
|
+
parse(open('/tmp/poetry.md').read(), m)
|
|
449
|
+
print(m.size())
|
|
450
|
+
import pprint; pprint.pprint(list(m.match()))
|
|
451
|
+
# next(m.match(None, 'http://uche.ogbuji.net/poems/updated', '2013-10-15'))
|
|
452
|
+
''' # noqa: E501
|
|
453
|
+
|
|
454
|
+
'''
|
|
455
|
+
|
|
456
|
+
for s in [ ' "quick-brown-fox"',
|
|
457
|
+
' "quick-brown-fox"\n',
|
|
458
|
+
' <quick-brown-fox>',
|
|
459
|
+
' <quick-brown-fox>\n',
|
|
460
|
+
' <quick-brown-fox> <!-- COMMENT -->',
|
|
461
|
+
' "quick-brown-fox" <!-- COMMENT -->',
|
|
462
|
+
'"\"1\""',
|
|
463
|
+
]:
|
|
464
|
+
parsed = value_expr.parseString(s, parseAll=True)
|
|
465
|
+
print(s, '∴', parsed)
|
|
466
|
+
|
|
467
|
+
for s in [ '# resX\n<!-- COMMENT -->\n\n * a-b-c: <quick-brown-fox>',
|
|
468
|
+
]:
|
|
469
|
+
print(s, end='')
|
|
470
|
+
parsed = resource_block.parseString(s, parseAll=True)
|
|
471
|
+
print('∴', parsed)
|
|
472
|
+
|
|
473
|
+
for s in [ ' * a-b-c: <quick-brown-fox>',
|
|
474
|
+
' * a-b-c: quick brown fox',
|
|
475
|
+
' * a-b-c: " quick brown fox"',
|
|
476
|
+
]:
|
|
477
|
+
parsed = prop.parseString(s, parseAll=True)
|
|
478
|
+
print(s, '∴', parsed)
|
|
479
|
+
|
|
480
|
+
for s in [ '# resX\n * a-b-c: <quick-brown-fox>',
|
|
481
|
+
'# resX [Person]\n * a-b-c: <quick-brown-fox>',
|
|
482
|
+
'# resX [Person]\n * a-b-c: <quick-brown-fox>\n * d-e-f: "lazy dog"',
|
|
483
|
+
]:
|
|
484
|
+
parsed = resource_block.parseString(s, parseAll=True)
|
|
485
|
+
print(s, '∴', parsed)
|
|
486
|
+
|
|
487
|
+
for s in [ '# resX\n * a-b-c: <quick-brown-fox>\n lang: en',
|
|
488
|
+
]:
|
|
489
|
+
parsed = resource_block.parseString(s, parseAll=True)
|
|
490
|
+
print(s, '∴', parsed)
|
|
491
|
+
|
|
492
|
+
for s in [ '# res1\n<!-- COMMENT -->\n\n * a-b-c: <quick-brown-fox>\n\n\n# res2\n\n * d-e-f: <jumps-over>\n\n\n',
|
|
493
|
+
]:
|
|
494
|
+
print(s, end='')
|
|
495
|
+
parsed = resource_block.parseString(s, parseAll=True)
|
|
496
|
+
print('∴', parsed)
|
|
497
|
+
|
|
498
|
+
for s in [ '# res1\n<!-- COMMENT -->\n\n * a-b-c: <quick-brown-fox>\n\n\n\n\n# res2\n\n * d-e-f: <jumps-over>\n\n\n',
|
|
499
|
+
]:
|
|
500
|
+
print(s, end='')
|
|
501
|
+
parsed = resource_seq.parseString(s, parseAll=True)
|
|
502
|
+
print('∴', parsed)
|
|
503
|
+
|
|
504
|
+
''' # noqa: E501, E502
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
'''
|
|
508
|
+
|
|
509
|
+
a-b-c: <quick-brown-fox> ∴ [prop_info(key='a-b-c', value=ParseResults([I(quick-brown-fox)], {}), children=[ParseResults([], {})])]
|
|
510
|
+
a-b-c: quick brown fox ∴ [prop_info(key='a-b-c', value=ParseResults(['quick brown fox'], {}), children=[ParseResults([], {})])]
|
|
511
|
+
a-b-c: " quick brown fox" ∴ [prop_info(key='a-b-c', value=ParseResults([LITERAL(' quick brown fox')], {}), children=[ParseResults([], {})])]
|
|
512
|
+
# resX
|
|
513
|
+
a-b-c: <quick-brown-fox> ∴ [I(resX), None, prop_info(key='a-b-c', value=ParseResults([I(quick-brown-fox)], {}), children=[ParseResults([], {})])]
|
|
514
|
+
# resX [Person]
|
|
515
|
+
a-b-c: <quick-brown-fox> ∴ [I(resX), 'Person', prop_info(key='a-b-c', value=ParseResults([I(quick-brown-fox)], {}), children=[ParseResults([], {})])]
|
|
516
|
+
# resX [Person]
|
|
517
|
+
a-b-c: <quick-brown-fox>
|
|
518
|
+
d-e-f: "lazy dog" ∴ [I(resX), 'Person', prop_info(key='a-b-c', value=ParseResults([I(quick-brown-fox)], {}), children=[ParseResults([prop_info(key='d-e-f', value=LITERAL('lazy dog'), children=[])], {})])]
|
|
519
|
+
# resX
|
|
520
|
+
a-b-c: <quick-brown-fox>
|
|
521
|
+
lang: en ∴ [I(resX), None, prop_info(key='a-b-c', value=ParseResults([I(quick-brown-fox)], {}), children=[ParseResults([prop_info(key='lang', value='en', children=[])], {})])]
|
|
522
|
+
|
|
523
|
+
''' # noqa: E501, E502
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# onya.serial.litparse_util.py
|
|
3
|
+
'''
|
|
4
|
+
Utility wrapper for Onya Literate parser
|
|
5
|
+
|
|
6
|
+
Provides a simple interface to the pyparsing-based parser in literate_lex
|
|
7
|
+
'''
|
|
8
|
+
|
|
9
|
+
from onya.serial import literate_lex
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class parser:
|
|
13
|
+
'''
|
|
14
|
+
Reusable object for parsing Onya Literate
|
|
15
|
+
|
|
16
|
+
This is a simple wrapper around the pyparsing-based parser
|
|
17
|
+
implementation in literate_lex.
|
|
18
|
+
'''
|
|
19
|
+
def __init__(self, config=None, encoding='utf-8'):
|
|
20
|
+
'''
|
|
21
|
+
Initialize the parser
|
|
22
|
+
|
|
23
|
+
config -- optional configuration dict
|
|
24
|
+
encoding -- character encoding (defaults to UTF-8)
|
|
25
|
+
'''
|
|
26
|
+
self.config = config or {}
|
|
27
|
+
self.encoding = encoding
|
|
28
|
+
|
|
29
|
+
def run(self, lit_text, g):
|
|
30
|
+
'''
|
|
31
|
+
Parse Onya Literate text into an Onya graph
|
|
32
|
+
|
|
33
|
+
lit_text -- Onya Literate source text
|
|
34
|
+
g -- Onya graph to populate with parsed relationships
|
|
35
|
+
encoding -- character encoding (defaults to UTF-8)
|
|
36
|
+
|
|
37
|
+
Returns: tuple of (docheader, nodes) where:
|
|
38
|
+
- docheader: document header info (may be None)
|
|
39
|
+
- nodes: set of nodes parsed from the document
|
|
40
|
+
'''
|
|
41
|
+
# Track nodes before parsing
|
|
42
|
+
nodes_before = set(g.nodes.keys())
|
|
43
|
+
|
|
44
|
+
# Parse the literate text using the pyparsing implementation
|
|
45
|
+
doc_iri = literate_lex.parse(lit_text, g, encoding=self.encoding)
|
|
46
|
+
|
|
47
|
+
# Get nodes that were added during parsing
|
|
48
|
+
nodes_after = set(g.nodes.keys())
|
|
49
|
+
nodes = nodes_after - nodes_before
|
|
50
|
+
|
|
51
|
+
# Return doc_iri as docheader (for compatibility)
|
|
52
|
+
docheader = doc_iri
|
|
53
|
+
|
|
54
|
+
return docheader, nodes
|
onya/terms.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Commonly used IRIs as vocabulary terms
|
|
3
|
+
'''
|
|
4
|
+
|
|
5
|
+
from amara.iri import I
|
|
6
|
+
|
|
7
|
+
# Onya
|
|
8
|
+
|
|
9
|
+
ONYA = I('http://purl.org/onya/vocab')
|
|
10
|
+
ONYA_TYPE = ONYA('type')
|
|
11
|
+
|
|
12
|
+
# RDF
|
|
13
|
+
|
|
14
|
+
RDF_NS = I('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
|
|
15
|
+
RDFS_NS = I('http://www.w3.org/2000/01/rdf-schema#')
|
|
16
|
+
|
|
17
|
+
RDF_TYPE = RDF_TYPE = RDF_NS('type')
|
|
18
|
+
|
onya/version.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Onya
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Knowledge graph (property graph-style) model and serialization for web resources
|
|
5
|
+
Project-URL: Homepage, https://github.com/OoriData/Onya
|
|
6
|
+
Project-URL: Repository, https://github.com/OoriData/Onya
|
|
7
|
+
Project-URL: Issues, https://github.com/OoriData/Onya/issues
|
|
8
|
+
Author-email: Uche Ogbuji <uche@ogbuji.net>
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
License-File: LICENSE-spec
|
|
11
|
+
Keywords: data,graph,linked-data,rdf,web
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: amara>=4.0.1
|
|
24
|
+
Requires-Dist: markdown
|
|
25
|
+
Requires-Dist: pyparsing
|
|
26
|
+
Provides-Extra: demo
|
|
27
|
+
Requires-Dist: fire; extra == 'demo'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build; extra == 'dev'
|
|
30
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
31
|
+
Requires-Dist: pipdeptree; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-mock; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
36
|
+
Requires-Dist: twine; extra == 'dev'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
**Onya** is a knowledge graph expression and implementation. This repository combines a [data model and format spec](SPEC.md) with a Python parser and API implementation.
|
|
40
|
+
|
|
41
|
+
# Python quick start
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
<!--
|
|
46
|
+
```bash
|
|
47
|
+
pip install amara
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or with uv (recommended):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv pip install amara
|
|
54
|
+
```
|
|
55
|
+
-->
|
|
56
|
+
|
|
57
|
+
Requires Python 3.12 or later. The package is still in early development, so install directly from source:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/OoriData/Onya.git
|
|
61
|
+
cd Onya
|
|
62
|
+
pip install -U .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
<!--
|
|
66
|
+
pip install git+https://github.com/OoriData/Onya.git
|
|
67
|
+
-->
|
|
68
|
+
|
|
69
|
+
## Basic Usage
|
|
70
|
+
|
|
71
|
+
Here's a simple example demonstrating the core Onya API. First, let's define a small friendship graph in Onya Literate format:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
# @docheader
|
|
75
|
+
|
|
76
|
+
* @document: http://example.org/friendship-graph
|
|
77
|
+
* @base: http://example.org/people/
|
|
78
|
+
* @schema: https://schema.org/
|
|
79
|
+
* @type-base: https://schema.org/
|
|
80
|
+
|
|
81
|
+
# Chuks [Person]
|
|
82
|
+
|
|
83
|
+
* name: Chukwuemeka Okafor
|
|
84
|
+
* nickname: Chuks
|
|
85
|
+
* age: 28
|
|
86
|
+
|
|
87
|
+
# Ify [Person]
|
|
88
|
+
|
|
89
|
+
* name: Ifeoma Adebayo
|
|
90
|
+
* nickname: Ify
|
|
91
|
+
* age: 27
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Now let's parse this graph and interact with it using the Python API:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from onya.graph import graph
|
|
98
|
+
from onya.serial import literate_lex
|
|
99
|
+
|
|
100
|
+
# Parse the Onya Literate text into a graph
|
|
101
|
+
onya_text = '''
|
|
102
|
+
# @docheader
|
|
103
|
+
|
|
104
|
+
* @document: http://example.org/friendship-graph
|
|
105
|
+
* @base: http://example.org/people/
|
|
106
|
+
* @schema: https://schema.org/
|
|
107
|
+
* @type-base: https://schema.org/
|
|
108
|
+
|
|
109
|
+
# Chuks [Person]
|
|
110
|
+
|
|
111
|
+
* name: Chukwuemeka Okafor
|
|
112
|
+
* nickname: Chuks
|
|
113
|
+
* age: 28
|
|
114
|
+
|
|
115
|
+
# Ify [Person]
|
|
116
|
+
|
|
117
|
+
* name: Ifeoma Adebayo
|
|
118
|
+
* nickname: Ify
|
|
119
|
+
* age: 27
|
|
120
|
+
'''
|
|
121
|
+
|
|
122
|
+
g = graph()
|
|
123
|
+
doc_iri = literate_lex.parse(onya_text, g)
|
|
124
|
+
print(f"Parsed document: {doc_iri}")
|
|
125
|
+
print(f"Graph has {len(g)} nodes")
|
|
126
|
+
|
|
127
|
+
# Access nodes and their properties
|
|
128
|
+
chuks = g['http://example.org/people/Chuks']
|
|
129
|
+
ify = g['http://example.org/people/Ify']
|
|
130
|
+
|
|
131
|
+
# Get a specific property value
|
|
132
|
+
for prop in chuks.getprop('https://schema.org/name'):
|
|
133
|
+
print(f"Name: {prop.value}")
|
|
134
|
+
|
|
135
|
+
# Add a friendship edge between Chuks and Ify
|
|
136
|
+
friendship = chuks.add_edge('https://schema.org/knows', ify)
|
|
137
|
+
print(f"Added edge: {friendship}")
|
|
138
|
+
|
|
139
|
+
# Add nested properties to the friendship (metadata about the relationship)
|
|
140
|
+
friendship.add_property('https://schema.org/startDate', '2018-03-15')
|
|
141
|
+
friendship.add_property('https://schema.org/description', 'Met at university')
|
|
142
|
+
|
|
143
|
+
# Add a new property to Ify
|
|
144
|
+
ify.add_property('https://schema.org/jobTitle', 'Software Engineer')
|
|
145
|
+
|
|
146
|
+
# Modify a property by removing the old one and adding a new one
|
|
147
|
+
age_props = list(chuks.getprop('https://schema.org/age'))
|
|
148
|
+
for prop in age_props:
|
|
149
|
+
chuks.remove_property(prop)
|
|
150
|
+
chuks.add_property('https://schema.org/age', '29')
|
|
151
|
+
|
|
152
|
+
# Traverse edges
|
|
153
|
+
for edge in chuks.traverse('https://schema.org/knows'):
|
|
154
|
+
friend = edge.target
|
|
155
|
+
for name_prop in friend.getprop('https://schema.org/name'):
|
|
156
|
+
print(f"Chuks knows: {name_prop.value}")
|
|
157
|
+
# Access nested properties on the edge
|
|
158
|
+
for date_prop in edge.getprop('https://schema.org/startDate'):
|
|
159
|
+
print(f" Friends since: {date_prop.value}")
|
|
160
|
+
|
|
161
|
+
# Find all nodes of a certain type
|
|
162
|
+
for person in g.typematch('https://schema.org/Person'):
|
|
163
|
+
for name_prop in person.getprop('https://schema.org/name'):
|
|
164
|
+
print(f"Person in graph: {name_prop.value}")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
This example demonstrates:
|
|
168
|
+
- Parsing Onya Literate format
|
|
169
|
+
- Accessing nodes and properties
|
|
170
|
+
- Adding edges with nested properties (reified relationships)
|
|
171
|
+
- Modifying properties
|
|
172
|
+
- Traversing the graph
|
|
173
|
+
- Querying by type
|
|
174
|
+
|
|
175
|
+
# Background
|
|
176
|
+
|
|
177
|
+
Onya is based on experience from developing [Versa](https://github.com/uogbuji/versa) and also working on [the MicroXML spec](https://dvcs.w3.org/hg/microxml/raw-file/tip/spec/microxml.html) and implementations thereof.
|
|
178
|
+
|
|
179
|
+
The URL used for metavocabulary is [managed via purl.org](https://purl.archive.org/purl/onya/vocab).
|
|
180
|
+
|
|
181
|
+
The name is from Igbo "ọ́nyà", web, snare, trap, and by extension, network. The expanded sense is ọ́nyà úchè, web of knowledge.
|
|
182
|
+
|
|
183
|
+
# Contributing
|
|
184
|
+
|
|
185
|
+
Contributions welcome! We're interested in feedback from the community about what works and what doesn't in real-world usage. To get help with the code implementation, read [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
186
|
+
|
|
187
|
+
# License
|
|
188
|
+
|
|
189
|
+
- **Code** (Python library): Apache 2.0 - See [LICENSE](LICENSE)
|
|
190
|
+
- **Specification** (wordloom_spec.md): [Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) - See [LICENSE-spec](LICENSE-spec)
|
|
191
|
+
|
|
192
|
+
The specification is under CC BY 4.0 to encourage broad adoption and derivative work while ensuring attribution. We want the format itself to be as open and reusable as possible, allowing anyone to create implementations in any language or adapt the format for their specific needs.
|
|
193
|
+
|
|
194
|
+
# Acknowledgments
|
|
195
|
+
|
|
196
|
+
Created by [Oori Data](https://oori.dev). Word Loom emerged from our work building multilingual LLM applications and finding gaps in existing prompt management approaches.
|
|
197
|
+
|
|
198
|
+
# Related Work
|
|
199
|
+
|
|
200
|
+
- [networkx](https://github.com/networkx/networkx): Network Analysis in Python
|
|
201
|
+
- [Apache AGE](https://github.com/apache/incubator-age): PostgreSQL Extension that for graphs. ANSI SQL & openCypher over the same DB.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
onya/__about__.py,sha256=rnObPjuBcEStqSO0S6gsdS_ot8ITOQjVj_-P1LUUYpg,22
|
|
2
|
+
onya/__init__.py,sha256=9uH16tXsn_OEcGbp1pL8uNa5U3Se9AYTxBxNE8WTlFI,553
|
|
3
|
+
onya/graph.py,sha256=qNxVpauro20YRzjiCt5sBf5at47EtOP5VSDRUeCDYeI,5755
|
|
4
|
+
onya/terms.py,sha256=4s-P6qhjMjnCuueJJTsJCX4M3HZxiJxmJ0_MFHDt8i8,303
|
|
5
|
+
onya/version.py,sha256=hQn5JmgQ287WzONNi7gGqb9gJAIwtqbjxNw3UI4od6M,77
|
|
6
|
+
onya/contrib/mkdcomments.py,sha256=nthBD18knhgzlg882qjpWgw9KiiMBOPmtiGLLLRXHDA,2219
|
|
7
|
+
onya/serial/__init__.py,sha256=laeecAq7WK9LHkgqu9Yp3QaW275ts-L36-bX1u6IpI8,58
|
|
8
|
+
onya/serial/literate.py,sha256=CakOQBmNV0MHS4haYppvKJjbKmWS8MTrSyGmTfzCtrk,3209
|
|
9
|
+
onya/serial/literate_lex.py,sha256=FjZIfcF_EJmRGtRKDWJvLzkjsfVeeITAjh4xShcf-T8,20626
|
|
10
|
+
onya/serial/litparse_util.py,sha256=vFondeLB8ZTwcMArrdrZLB9Gy7QkVhDSlB6IJuPVHS8,1608
|
|
11
|
+
onya-0.1.1.dist-info/METADATA,sha256=h0XXT40hclcojhsRAOSMXV41_Kl4Ar3PprG4i6SlRjM,6588
|
|
12
|
+
onya-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
13
|
+
onya-0.1.1.dist-info/licenses/LICENSE-spec,sha256=k56tqgDsUX64EIJo2WC7GbdDIEnnzA_0_gKDfYPHPTw,1386
|
|
14
|
+
onya-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Word Loom Specification License
|
|
2
|
+
|
|
3
|
+
The Word Loom specification document (wordloom_spec.md) is licensed under the
|
|
4
|
+
Creative Commons Attribution 4.0 International License (CC BY 4.0).
|
|
5
|
+
|
|
6
|
+
To view a copy of this license, visit:
|
|
7
|
+
https://creativecommons.org/licenses/by/4.0/
|
|
8
|
+
|
|
9
|
+
or send a letter to:
|
|
10
|
+
Creative Commons
|
|
11
|
+
PO Box 1866
|
|
12
|
+
Mountain View, CA 94042
|
|
13
|
+
USA
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
You are free to:
|
|
18
|
+
|
|
19
|
+
* Share — copy and redistribute the specification in any medium or format
|
|
20
|
+
* Adapt — remix, transform, and build upon the specification for any purpose,
|
|
21
|
+
even commercially
|
|
22
|
+
|
|
23
|
+
Under the following terms:
|
|
24
|
+
|
|
25
|
+
* Attribution — You must give appropriate credit, provide a link to the
|
|
26
|
+
license, and indicate if changes were made. You may do so in any reasonable
|
|
27
|
+
manner, but not in any way that suggests the licensor endorses you or your
|
|
28
|
+
use.
|
|
29
|
+
|
|
30
|
+
* No additional restrictions — You may not apply legal terms or technological
|
|
31
|
+
measures that legally restrict others from doing anything the license
|
|
32
|
+
permits.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
This is a human-readable summary of (and not a substitute for) the full license.
|
|
37
|
+
|
|
38
|
+
The full legal text of the Creative Commons Attribution 4.0 International
|
|
39
|
+
License is available at:
|
|
40
|
+
https://creativecommons.org/licenses/by/4.0/legalcode
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
Copyright (c) 2023-2025 Oori Data
|
|
45
|
+
|
|
46
|
+
Attribution example:
|
|
47
|
+
"Based on the Word Loom specification by Oori Data (https://github.com/OoriData/WordLoom)"
|