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 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
@@ -0,0 +1,5 @@
1
+ # onya.serial
2
+
3
+ '''
4
+ Onya serializers and deserializers
5
+ '''
@@ -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,2 @@
1
+ # http://legacy.python.org/dev/peps/pep-0440/
2
+ version_info = ('0', '0', '1')
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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)"