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.
@@ -0,0 +1,12 @@
1
+ node_modules/
2
+ dist/
3
+ .astro/
4
+ __pycache__/
5
+ *.pyc
6
+ .venv/
7
+ *.egg-info/
8
+ build/
9
+ .pytest_cache/
10
+ coverage/
11
+ target/
12
+ .DS_Store
@@ -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.
@@ -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')