triplate 0.3.0__tar.gz
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.
- triplate-0.3.0/.gitignore +12 -0
- triplate-0.3.0/PKG-INFO +55 -0
- triplate-0.3.0/README.md +33 -0
- triplate-0.3.0/pyproject.toml +31 -0
- triplate-0.3.0/src/triplate/__init__.py +77 -0
- triplate-0.3.0/src/triplate/_ast.py +181 -0
- triplate-0.3.0/src/triplate/_examples.py +60 -0
- triplate-0.3.0/src/triplate/_lexer.py +682 -0
- triplate-0.3.0/src/triplate/_parser.py +138 -0
- triplate-0.3.0/src/triplate/_registry.py +21 -0
- triplate-0.3.0/src/triplate/_renderer.py +303 -0
- triplate-0.3.0/src/triplate/_serializers.py +252 -0
- triplate-0.3.0/src/triplate/errors.py +26 -0
- triplate-0.3.0/tests/test_api.py +116 -0
- triplate-0.3.0/tests/test_conformance.py +26 -0
triplate-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: triplate
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A templating engine for SPARQL queries with typed, injection-safe placeholders and loops
|
|
5
|
+
Project-URL: Homepage, https://triplate.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/triplate/triplate
|
|
7
|
+
Project-URL: Issues, https://github.com/triplate/triplate/issues
|
|
8
|
+
Author: Triplate contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: injection-safe,query,rdf,sparql,template
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: rdflib>=6.0; extra == 'dev'
|
|
19
|
+
Provides-Extra: rdflib
|
|
20
|
+
Requires-Dist: rdflib>=6.0; extra == 'rdflib'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Triplate (Python)
|
|
24
|
+
|
|
25
|
+
A templating engine for RDF query & data languages (SPARQL, Turtle, …) with a
|
|
26
|
+
typed `---` frontmatter header, injection-safe values, loops and conditionals.
|
|
27
|
+
Python reference implementation of the [Triplate language](https://triplate.dev).
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from triplate import compile
|
|
31
|
+
|
|
32
|
+
template = """\
|
|
33
|
+
---
|
|
34
|
+
params {
|
|
35
|
+
classes: iri[] min 1
|
|
36
|
+
limit: int optional
|
|
37
|
+
}
|
|
38
|
+
---
|
|
39
|
+
SELECT ?s WHERE {
|
|
40
|
+
{% for c in classes join "UNION" %}
|
|
41
|
+
{ ?s a ${c} }
|
|
42
|
+
{% endfor %}
|
|
43
|
+
}
|
|
44
|
+
{% if limit %}LIMIT ${limit}{% endif %}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
tmpl = compile(template)
|
|
48
|
+
sparql = tmpl.render(classes=["http://example.org/Person"], limit=10)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Every input is declared in the mandatory `---` frontmatter header with its RDF
|
|
52
|
+
type; the context is validated and each value escaped accordingly, so rendered
|
|
53
|
+
output is injection-safe and an unprocessed template fails fast.
|
|
54
|
+
|
|
55
|
+
See [triplate.dev](https://triplate.dev) for the full guide and specification.
|
triplate-0.3.0/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Triplate (Python)
|
|
2
|
+
|
|
3
|
+
A templating engine for RDF query & data languages (SPARQL, Turtle, …) with a
|
|
4
|
+
typed `---` frontmatter header, injection-safe values, loops and conditionals.
|
|
5
|
+
Python reference implementation of the [Triplate language](https://triplate.dev).
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from triplate import compile
|
|
9
|
+
|
|
10
|
+
template = """\
|
|
11
|
+
---
|
|
12
|
+
params {
|
|
13
|
+
classes: iri[] min 1
|
|
14
|
+
limit: int optional
|
|
15
|
+
}
|
|
16
|
+
---
|
|
17
|
+
SELECT ?s WHERE {
|
|
18
|
+
{% for c in classes join "UNION" %}
|
|
19
|
+
{ ?s a ${c} }
|
|
20
|
+
{% endfor %}
|
|
21
|
+
}
|
|
22
|
+
{% if limit %}LIMIT ${limit}{% endif %}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
tmpl = compile(template)
|
|
26
|
+
sparql = tmpl.render(classes=["http://example.org/Person"], limit=10)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Every input is declared in the mandatory `---` frontmatter header with its RDF
|
|
30
|
+
type; the context is validated and each value escaped accordingly, so rendered
|
|
31
|
+
output is injection-safe and an unprocessed template fails fast.
|
|
32
|
+
|
|
33
|
+
See [triplate.dev](https://triplate.dev) for the full guide and specification.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "triplate"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "A templating engine for SPARQL queries with typed, injection-safe placeholders and loops"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Triplate contributors" }]
|
|
13
|
+
keywords = ["sparql", "template", "rdf", "query", "injection-safe"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development :: Libraries",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://triplate.dev"
|
|
23
|
+
Repository = "https://github.com/triplate/triplate"
|
|
24
|
+
Issues = "https://github.com/triplate/triplate/issues"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
rdflib = ["rdflib>=6.0"]
|
|
28
|
+
dev = ["pytest>=8.0", "rdflib>=6.0"]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/triplate"]
|
|
@@ -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)
|
|
@@ -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]
|
|
@@ -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')
|