triplate 0.3.0__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.
Potentially problematic release.
This version of triplate might be problematic. Click here for more details.
- triplate/__init__.py +77 -0
- triplate/_ast.py +181 -0
- triplate/_examples.py +60 -0
- triplate/_lexer.py +682 -0
- triplate/_parser.py +138 -0
- triplate/_registry.py +21 -0
- triplate/_renderer.py +303 -0
- triplate/_serializers.py +252 -0
- triplate/errors.py +26 -0
- triplate-0.3.0.dist-info/METADATA +55 -0
- triplate-0.3.0.dist-info/RECORD +12 -0
- triplate-0.3.0.dist-info/WHEEL +4 -0
triplate/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Triplate — a templating engine for RDF query & data languages.
|
|
2
|
+
|
|
3
|
+
Templates declare their inputs in a ``---`` frontmatter header and use ``${ }``
|
|
4
|
+
substitutions, ``$"…"`` / ``$<…>`` constructs, and ``{% for %}`` / ``{% if %}``
|
|
5
|
+
directives. Values are validated and escaped per their declared RDF type, so
|
|
6
|
+
rendered queries are injection-safe by construction. See https://triplate.dev.
|
|
7
|
+
|
|
8
|
+
from triplate import compile, render
|
|
9
|
+
|
|
10
|
+
tmpl = compile(template_string) # parse once
|
|
11
|
+
sparql = tmpl.render(classes=[...]) # render many
|
|
12
|
+
render(template_string, classes=[...]) # one-shot convenience
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ._examples import example_set_to_context, extract_prefixes
|
|
16
|
+
from ._parser import parse as _parse
|
|
17
|
+
from ._registry import register_type
|
|
18
|
+
from ._renderer import render as _render
|
|
19
|
+
from .errors import (
|
|
20
|
+
TriplateBindingError,
|
|
21
|
+
TriplateCardinalityError,
|
|
22
|
+
TriplateError,
|
|
23
|
+
TriplateSyntaxError,
|
|
24
|
+
TriplateTypeError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"CompiledTemplate",
|
|
29
|
+
"compile",
|
|
30
|
+
"render",
|
|
31
|
+
"register_type",
|
|
32
|
+
"TriplateError",
|
|
33
|
+
"TriplateSyntaxError",
|
|
34
|
+
"TriplateBindingError",
|
|
35
|
+
"TriplateTypeError",
|
|
36
|
+
"TriplateCardinalityError",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
__version__ = "0.3.0"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CompiledTemplate:
|
|
43
|
+
"""A parsed template that can be rendered many times."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, data, source):
|
|
46
|
+
self._data = data
|
|
47
|
+
self._source = source
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def schema(self):
|
|
51
|
+
return self._data.schema
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def examples(self):
|
|
55
|
+
return self._data.examples
|
|
56
|
+
|
|
57
|
+
def render(self, context=None, **kwargs):
|
|
58
|
+
ctx = dict(context) if context else {}
|
|
59
|
+
ctx.update(kwargs)
|
|
60
|
+
return _render(self._data, ctx)
|
|
61
|
+
|
|
62
|
+
def preview_example(self, example_id):
|
|
63
|
+
for e in self._data.examples:
|
|
64
|
+
if e.id == example_id:
|
|
65
|
+
ctx = example_set_to_context(e, self._data.schema, extract_prefixes(self._source))
|
|
66
|
+
return _render(self._data, ctx)
|
|
67
|
+
raise TriplateError(f"no example set with id: {example_id}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def compile(template):
|
|
71
|
+
"""Parses a template once; the result can be rendered many times."""
|
|
72
|
+
return CompiledTemplate(_parse(template), template)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def render(template, context=None, **kwargs):
|
|
76
|
+
"""One-shot convenience: compile and render in a single call."""
|
|
77
|
+
return compile(template).render(context, **kwargs)
|
triplate/_ast.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""AST and schema types. Mirrors the TypeScript implementation's ast.ts."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
5
|
+
|
|
6
|
+
RefPath = Tuple[str, ...]
|
|
7
|
+
|
|
8
|
+
# A scalar/record "base" is a dict with a 'kind' key:
|
|
9
|
+
# {'kind': 'iri' | 'pname' | 'string' | ... | 'term' | 'raw'}
|
|
10
|
+
# {'kind': 'literal', 'datatype': str}
|
|
11
|
+
# {'kind': 'custom', 'name': str}
|
|
12
|
+
# {'kind': 'record', 'fields': Dict[str, TypeExpr]}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class TypeExpr:
|
|
17
|
+
base: dict
|
|
18
|
+
array: bool = False
|
|
19
|
+
optional: bool = False
|
|
20
|
+
min: Optional[int] = None
|
|
21
|
+
max: Optional[int] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ParamDecl:
|
|
26
|
+
name: str
|
|
27
|
+
type: TypeExpr
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Schema:
|
|
32
|
+
params: List[ParamDecl]
|
|
33
|
+
by_name: Dict[str, TypeExpr]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class LangStatic:
|
|
38
|
+
static: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class LangPath:
|
|
43
|
+
path: RefPath
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
LangSpec = Union[LangStatic, LangPath]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class PartText:
|
|
51
|
+
text: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class PartHole:
|
|
56
|
+
path: RefPath
|
|
57
|
+
line: int
|
|
58
|
+
column: int
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
Part = Union[PartText, PartHole]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class TextNode:
|
|
66
|
+
value: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ValueNode:
|
|
71
|
+
path: RefPath
|
|
72
|
+
line: int
|
|
73
|
+
column: int
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class InterpNode:
|
|
78
|
+
parts: Tuple[Part, ...]
|
|
79
|
+
lang: Optional[LangSpec]
|
|
80
|
+
datatype: Optional[str]
|
|
81
|
+
line: int
|
|
82
|
+
column: int
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class IriNode:
|
|
87
|
+
parts: Tuple[Part, ...]
|
|
88
|
+
line: int
|
|
89
|
+
column: int
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class Cond:
|
|
94
|
+
negated: bool
|
|
95
|
+
path: RefPath
|
|
96
|
+
line: int
|
|
97
|
+
column: int
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class ForNode:
|
|
102
|
+
item: str
|
|
103
|
+
source: RefPath
|
|
104
|
+
join: Optional[str]
|
|
105
|
+
join_exact: bool
|
|
106
|
+
body: List["Node"] = field(default_factory=list)
|
|
107
|
+
line: int = 0
|
|
108
|
+
column: int = 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class Branch:
|
|
113
|
+
cond: Cond
|
|
114
|
+
body: List["Node"] = field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class IfNode:
|
|
119
|
+
branches: List[Branch]
|
|
120
|
+
else_body: Optional[List["Node"]]
|
|
121
|
+
line: int
|
|
122
|
+
column: int
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
Node = Union[TextNode, ValueNode, InterpNode, IriNode, ForNode, IfNode]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Example values (RDF term literals)
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class ExIri:
|
|
131
|
+
value: str
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class ExPname:
|
|
136
|
+
prefix: str
|
|
137
|
+
local: str
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class ExString:
|
|
142
|
+
value: str
|
|
143
|
+
lang: Optional[str] = None
|
|
144
|
+
datatype: Optional[str] = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class ExNumber:
|
|
149
|
+
value: float
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class ExBoolean:
|
|
154
|
+
value: bool
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class ExList:
|
|
159
|
+
items: tuple
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class ExRecord:
|
|
164
|
+
fields: dict
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
ExampleValue = Union[ExIri, ExPname, ExString, ExNumber, ExBoolean, ExList, ExRecord]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class ExampleSet:
|
|
172
|
+
id: str
|
|
173
|
+
description: Optional[str]
|
|
174
|
+
bindings: Dict[str, ExampleValue]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class CompiledTemplateData:
|
|
179
|
+
schema: Schema
|
|
180
|
+
examples: List[ExampleSet]
|
|
181
|
+
body: List[Node]
|
triplate/_examples.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Example-set preview support. Mirrors examples.ts."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ._ast import ExBoolean, ExIri, ExList, ExNumber, ExPname, ExRecord, ExString
|
|
6
|
+
from .errors import TriplateError
|
|
7
|
+
|
|
8
|
+
_PREFIX_RE = re.compile(r"(?:PREFIX|@prefix)\s+([A-Za-z_][\w.-]*)?\s*:\s*<([^>]*)>", re.IGNORECASE)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def extract_prefixes(template):
|
|
12
|
+
out = {}
|
|
13
|
+
for m in _PREFIX_RE.finditer(template):
|
|
14
|
+
out[m.group(1) or ""] = m.group(2)
|
|
15
|
+
return out
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def example_set_to_context(example_set, schema, prefixes):
|
|
19
|
+
ctx = {}
|
|
20
|
+
for name, ev in example_set.bindings.items():
|
|
21
|
+
type_ = schema.by_name.get(name)
|
|
22
|
+
if type_ is None:
|
|
23
|
+
raise TriplateError(f'example "{example_set.id}" binds unknown parameter: {name}')
|
|
24
|
+
ctx[name] = _convert(ev, type_, prefixes, example_set.id)
|
|
25
|
+
return ctx
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _convert(ev, type_, prefixes, eid):
|
|
29
|
+
from ._ast import TypeExpr
|
|
30
|
+
|
|
31
|
+
if type_.array:
|
|
32
|
+
if not isinstance(ev, ExList):
|
|
33
|
+
raise TriplateError(f'example "{eid}": expected a list')
|
|
34
|
+
elem = TypeExpr(type_.base, False, False, None, None)
|
|
35
|
+
return [_convert(it, elem, prefixes, eid) for it in ev.items]
|
|
36
|
+
if type_.base["kind"] == "record":
|
|
37
|
+
if not isinstance(ev, ExRecord):
|
|
38
|
+
raise TriplateError(f'example "{eid}": expected a record')
|
|
39
|
+
out = {}
|
|
40
|
+
for f, ft in type_.base["fields"].items():
|
|
41
|
+
if f in ev.fields:
|
|
42
|
+
out[f] = _convert(ev.fields[f], ft, prefixes, eid)
|
|
43
|
+
return out
|
|
44
|
+
scalar = type_.base["kind"]
|
|
45
|
+
if isinstance(ev, ExIri):
|
|
46
|
+
return ev.value
|
|
47
|
+
if isinstance(ev, ExPname):
|
|
48
|
+
if scalar == "pname":
|
|
49
|
+
return f"{ev.prefix}:{ev.local}"
|
|
50
|
+
ns = prefixes.get(ev.prefix)
|
|
51
|
+
if ns is None:
|
|
52
|
+
raise TriplateError(f'example "{eid}": unknown prefix \'{ev.prefix}:\'')
|
|
53
|
+
return ns + ev.local
|
|
54
|
+
if isinstance(ev, ExString):
|
|
55
|
+
return ev.value
|
|
56
|
+
if isinstance(ev, ExNumber):
|
|
57
|
+
return ev.value
|
|
58
|
+
if isinstance(ev, ExBoolean):
|
|
59
|
+
return ev.value
|
|
60
|
+
raise TriplateError(f'example "{eid}": value does not match declared type')
|