shacl2code 0.0.11__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.
- shacl2code/__init__.py +4 -0
- shacl2code/__main__.py +12 -0
- shacl2code/context.py +167 -0
- shacl2code/lang/__init__.py +11 -0
- shacl2code/lang/common.py +144 -0
- shacl2code/lang/jinja.py +29 -0
- shacl2code/lang/jsonschema.py +51 -0
- shacl2code/lang/lang.py +19 -0
- shacl2code/lang/python.py +54 -0
- shacl2code/lang/templates/jsonschema.j2 +277 -0
- shacl2code/lang/templates/python.j2 +2054 -0
- shacl2code/main.py +139 -0
- shacl2code/model.py +293 -0
- shacl2code/urlcontext.py +23 -0
- shacl2code/version.py +1 -0
- shacl2code-0.0.11.dist-info/METADATA +205 -0
- shacl2code-0.0.11.dist-info/RECORD +20 -0
- shacl2code-0.0.11.dist-info/WHEEL +4 -0
- shacl2code-0.0.11.dist-info/entry_points.txt +2 -0
- shacl2code-0.0.11.dist-info/licenses/LICENSE +21 -0
shacl2code/__init__.py
ADDED
shacl2code/__main__.py
ADDED
shacl2code/context.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Copyright (c) 2024 Joshua Watt
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Context(object):
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
|
|
9
|
+
def __init__(self, contexts=[]):
|
|
10
|
+
self.contexts = [c for c in contexts if c]
|
|
11
|
+
self.__vocabs = []
|
|
12
|
+
self.__expanded = {}
|
|
13
|
+
self.__compacted = {}
|
|
14
|
+
|
|
15
|
+
@contextmanager
|
|
16
|
+
def vocab_push(self, vocab):
|
|
17
|
+
if not vocab:
|
|
18
|
+
yield self
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
self.__vocabs.append(vocab)
|
|
22
|
+
try:
|
|
23
|
+
yield self
|
|
24
|
+
finally:
|
|
25
|
+
self.__vocabs.pop()
|
|
26
|
+
|
|
27
|
+
def __get_vocab_contexts(self):
|
|
28
|
+
contexts = []
|
|
29
|
+
|
|
30
|
+
for v in self.__vocabs:
|
|
31
|
+
for ctx in self.contexts:
|
|
32
|
+
# Check for vocabulary contexts
|
|
33
|
+
for name, value in ctx.items():
|
|
34
|
+
if (
|
|
35
|
+
isinstance(value, dict)
|
|
36
|
+
and value["@type"] == "@vocab"
|
|
37
|
+
and v == self.__expand(value["@id"], self.contexts)
|
|
38
|
+
):
|
|
39
|
+
contexts.insert(0, value["@context"])
|
|
40
|
+
|
|
41
|
+
return contexts
|
|
42
|
+
|
|
43
|
+
def compact(self, _id):
|
|
44
|
+
return self.__compact_contexts(_id)
|
|
45
|
+
|
|
46
|
+
def compact_vocab(self, _id, vocab=None):
|
|
47
|
+
with self.vocab_push(vocab):
|
|
48
|
+
if not self.__vocabs:
|
|
49
|
+
v = ""
|
|
50
|
+
else:
|
|
51
|
+
v = self.__vocabs[-1]
|
|
52
|
+
|
|
53
|
+
return self.__compact_contexts(_id, v, self.__get_vocab_contexts())
|
|
54
|
+
|
|
55
|
+
def __compact_contexts(self, _id, v="", apply_vocabs=False):
|
|
56
|
+
if v not in self.__compacted or _id not in self.__compacted[v]:
|
|
57
|
+
if apply_vocabs:
|
|
58
|
+
contexts = self.__get_vocab_contexts() + self.contexts
|
|
59
|
+
else:
|
|
60
|
+
contexts = self.contexts
|
|
61
|
+
|
|
62
|
+
self.__compacted.setdefault(v, {})[_id] = self.__compact(
|
|
63
|
+
_id,
|
|
64
|
+
contexts,
|
|
65
|
+
apply_vocabs,
|
|
66
|
+
)
|
|
67
|
+
return self.__compacted[v][_id]
|
|
68
|
+
|
|
69
|
+
def __compact(self, _id, contexts, apply_vocabs):
|
|
70
|
+
def remove_prefix(_id, value):
|
|
71
|
+
possible = set()
|
|
72
|
+
if _id.startswith(value):
|
|
73
|
+
tmp_id = _id[len(value) :]
|
|
74
|
+
possible.add(tmp_id)
|
|
75
|
+
possible |= collect_possible(tmp_id)
|
|
76
|
+
return possible
|
|
77
|
+
|
|
78
|
+
def collect_possible(_id):
|
|
79
|
+
possible = set()
|
|
80
|
+
for ctx in contexts:
|
|
81
|
+
for name, value in ctx.items():
|
|
82
|
+
if name == "@vocab":
|
|
83
|
+
if apply_vocabs:
|
|
84
|
+
possible |= remove_prefix(_id, value)
|
|
85
|
+
elif name == "@base":
|
|
86
|
+
possible |= remove_prefix(_id, value)
|
|
87
|
+
else:
|
|
88
|
+
if isinstance(value, dict):
|
|
89
|
+
value = value["@id"]
|
|
90
|
+
|
|
91
|
+
if _id == value:
|
|
92
|
+
possible.add(name)
|
|
93
|
+
possible |= collect_possible(name)
|
|
94
|
+
elif _id.startswith(value):
|
|
95
|
+
tmp_id = name + ":" + _id[len(value) :].lstrip("/")
|
|
96
|
+
possible.add(tmp_id)
|
|
97
|
+
possible |= collect_possible(tmp_id)
|
|
98
|
+
|
|
99
|
+
return possible
|
|
100
|
+
|
|
101
|
+
possible = collect_possible(_id)
|
|
102
|
+
if not possible:
|
|
103
|
+
return _id
|
|
104
|
+
|
|
105
|
+
# To select from the possible identifiers, choose the one that has the
|
|
106
|
+
# least context (fewest ":"), then the shortest, and finally
|
|
107
|
+
# alphabetically
|
|
108
|
+
possible = list(possible)
|
|
109
|
+
possible.sort(key=lambda p: (p.count(":"), len(p), p))
|
|
110
|
+
|
|
111
|
+
return possible[0]
|
|
112
|
+
|
|
113
|
+
def is_relative(self, _id):
|
|
114
|
+
import re
|
|
115
|
+
|
|
116
|
+
return not re.match(r"[^:]+:", _id)
|
|
117
|
+
|
|
118
|
+
def __expand_contexts(self, _id, v="", apply_vocabs=False):
|
|
119
|
+
if v not in self.__expanded or _id not in self.__expanded[v]:
|
|
120
|
+
if apply_vocabs:
|
|
121
|
+
contexts = self.__get_vocab_contexts() + self.contexts
|
|
122
|
+
|
|
123
|
+
# Apply contexts
|
|
124
|
+
for ctx in contexts:
|
|
125
|
+
for name, value in ctx.items():
|
|
126
|
+
if name == "@vocab":
|
|
127
|
+
_id = value + _id
|
|
128
|
+
else:
|
|
129
|
+
contexts = self.contexts
|
|
130
|
+
|
|
131
|
+
for ctx in contexts:
|
|
132
|
+
for name, value in ctx.items():
|
|
133
|
+
if name == "@base" and self.is_relative(_id):
|
|
134
|
+
_id = value + _id
|
|
135
|
+
|
|
136
|
+
self.__expanded.setdefault(v, {})[_id] = self.__expand(_id, contexts)
|
|
137
|
+
|
|
138
|
+
return self.__expanded[v][_id]
|
|
139
|
+
|
|
140
|
+
def expand(self, _id):
|
|
141
|
+
return self.__expand_contexts(_id)
|
|
142
|
+
|
|
143
|
+
def expand_vocab(self, _id, vocab=""):
|
|
144
|
+
with self.vocab_push(vocab):
|
|
145
|
+
if not self.__vocabs:
|
|
146
|
+
v = ""
|
|
147
|
+
else:
|
|
148
|
+
v = self.__vocabs[-1]
|
|
149
|
+
|
|
150
|
+
return self.__expand_contexts(_id, v, True)
|
|
151
|
+
|
|
152
|
+
def __expand(self, _id, contexts):
|
|
153
|
+
for ctx in contexts:
|
|
154
|
+
if ":" not in _id:
|
|
155
|
+
if _id in ctx:
|
|
156
|
+
if isinstance(ctx[_id], dict):
|
|
157
|
+
return self.__expand(ctx[_id]["@id"], contexts)
|
|
158
|
+
return self.__expand(ctx[_id], contexts)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
prefix, suffix = _id.split(":", 1)
|
|
162
|
+
if prefix not in ctx:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
return self.__expand(prefix, contexts) + suffix
|
|
166
|
+
|
|
167
|
+
return _id
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024 Joshua Watt
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
from .lang import LANGUAGES # noqa: F401
|
|
7
|
+
|
|
8
|
+
# All renderers must be imported here to be registered
|
|
9
|
+
from .jinja import JinjaRender # noqa: F401
|
|
10
|
+
from .python import PythonRender # noqa: F401
|
|
11
|
+
from .jsonschema import JsonSchemaRender # noqa: F401
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024 Joshua Watt
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from jinja2 import Environment, FileSystemLoader, TemplateRuntimeError
|
|
11
|
+
from rdflib.namespace import SH
|
|
12
|
+
from ..model import SHACL2CODE
|
|
13
|
+
|
|
14
|
+
THIS_DIR = Path(__file__).parent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OutputFile(object):
|
|
18
|
+
def __init__(self, path):
|
|
19
|
+
self.path = path
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def open(self):
|
|
23
|
+
if self.path == "-":
|
|
24
|
+
yield sys.stdout
|
|
25
|
+
else:
|
|
26
|
+
with open(self.path, "w") as f:
|
|
27
|
+
yield f
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BasicJinjaRender(object):
|
|
31
|
+
"""
|
|
32
|
+
Common Jinja Template Renderer
|
|
33
|
+
|
|
34
|
+
Renderers that only use a single Jinja file can derive from this class. For
|
|
35
|
+
example:
|
|
36
|
+
|
|
37
|
+
@language("my-lang")
|
|
38
|
+
class MyRendered(BasicJinjaRenderer):
|
|
39
|
+
HELP = "Generates my-lang bindings"
|
|
40
|
+
|
|
41
|
+
def __init__(self, args):
|
|
42
|
+
super().__init__(args, PATH / TO / TEMPLATE)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, args, template):
|
|
46
|
+
self.__output = args.output
|
|
47
|
+
self.__template = template
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def get_arguments(cls, parser):
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--output",
|
|
53
|
+
"-o",
|
|
54
|
+
type=OutputFile,
|
|
55
|
+
help="Output file or '-' for stdout",
|
|
56
|
+
required=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def get_additional_render_args(self):
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
def get_extra_env(self):
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
def render(self, template, output, *, extra_env={}, render_args={}):
|
|
66
|
+
def abort_helper(msg):
|
|
67
|
+
raise TemplateRuntimeError(msg)
|
|
68
|
+
|
|
69
|
+
env = Environment(loader=FileSystemLoader([template.parent, THIS_DIR.parent]))
|
|
70
|
+
for k, v in extra_env.items():
|
|
71
|
+
env.globals[k] = v
|
|
72
|
+
env.globals["abort"] = abort_helper
|
|
73
|
+
env.globals["SHACL2CODE"] = SHACL2CODE
|
|
74
|
+
env.globals["SH"] = SH
|
|
75
|
+
template = env.get_template(template.name)
|
|
76
|
+
|
|
77
|
+
render = template.render(
|
|
78
|
+
disclaimer=f"This file was automatically generated by {os.path.basename(sys.argv[0])}. DO NOT MANUALLY MODIFY IT",
|
|
79
|
+
**render_args,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
output.write(render)
|
|
83
|
+
if not render[-1] == "\n":
|
|
84
|
+
output.write("\n")
|
|
85
|
+
|
|
86
|
+
def output(self, model):
|
|
87
|
+
"""
|
|
88
|
+
Render the provided model
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
class ObjectList(object):
|
|
92
|
+
def __init__(self, objs):
|
|
93
|
+
self.__objs = objs
|
|
94
|
+
|
|
95
|
+
def __iter__(self):
|
|
96
|
+
return iter(self.__objs)
|
|
97
|
+
|
|
98
|
+
def get(self, _id):
|
|
99
|
+
for o in self.__objs:
|
|
100
|
+
if o._id == _id:
|
|
101
|
+
return o
|
|
102
|
+
raise KeyError(f"Object with ID {_id} not found")
|
|
103
|
+
|
|
104
|
+
def get_all_derived(cls):
|
|
105
|
+
nonlocal classes
|
|
106
|
+
|
|
107
|
+
def _recurse(cls):
|
|
108
|
+
result = set(cls.derived_ids)
|
|
109
|
+
for r in cls.derived_ids:
|
|
110
|
+
result |= _recurse(classes.get(r))
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
d = list(_recurse(cls))
|
|
114
|
+
d.sort()
|
|
115
|
+
return d
|
|
116
|
+
|
|
117
|
+
classes = ObjectList(model.classes)
|
|
118
|
+
concrete_classes = ObjectList(
|
|
119
|
+
list(c for c in model.classes if not c.is_abstract)
|
|
120
|
+
)
|
|
121
|
+
abstract_classes = ObjectList(list(c for c in model.classes if c.is_abstract))
|
|
122
|
+
enums = ObjectList(model.enums)
|
|
123
|
+
|
|
124
|
+
render_args = {
|
|
125
|
+
"classes": classes,
|
|
126
|
+
"concrete_classes": concrete_classes,
|
|
127
|
+
"abstract_classes": abstract_classes,
|
|
128
|
+
"enums": enums,
|
|
129
|
+
"context": model.context,
|
|
130
|
+
**self.get_additional_render_args(),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
env = {
|
|
134
|
+
"get_all_derived": get_all_derived,
|
|
135
|
+
**self.get_extra_env(),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
with self.__output.open() as f:
|
|
139
|
+
self.render(
|
|
140
|
+
self.__template,
|
|
141
|
+
f,
|
|
142
|
+
extra_env=env,
|
|
143
|
+
render_args=render_args,
|
|
144
|
+
)
|
shacl2code/lang/jinja.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024 Joshua Watt
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .common import BasicJinjaRender
|
|
9
|
+
from .lang import language
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@language("jinja")
|
|
13
|
+
class JinjaRender(BasicJinjaRender):
|
|
14
|
+
HELP = "Render Jinja Output (for testing)"
|
|
15
|
+
|
|
16
|
+
def __init__(self, args):
|
|
17
|
+
super().__init__(args, args.template)
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def get_arguments(cls, parser):
|
|
21
|
+
super().get_arguments(parser)
|
|
22
|
+
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--template",
|
|
25
|
+
"-t",
|
|
26
|
+
type=Path,
|
|
27
|
+
help="Jinja Template file",
|
|
28
|
+
required=True,
|
|
29
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024 Joshua Watt
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
from .common import BasicJinjaRender
|
|
7
|
+
from .lang import language, TEMPLATE_DIR
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import keyword
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def varname(*name):
|
|
14
|
+
name = str("_".join(name)).replace("@", "_")
|
|
15
|
+
name = re.sub(r"[^a-zA-Z0-9_]", "", name)
|
|
16
|
+
while keyword.iskeyword(name):
|
|
17
|
+
name = name + "_"
|
|
18
|
+
return name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@language("jsonschema")
|
|
22
|
+
class JsonSchemaRender(BasicJinjaRender):
|
|
23
|
+
HELP = "JSON Schema"
|
|
24
|
+
|
|
25
|
+
def __init__(self, args):
|
|
26
|
+
super().__init__(args, TEMPLATE_DIR / "jsonschema.j2")
|
|
27
|
+
self.__render_args = {
|
|
28
|
+
"schema_title": args.title,
|
|
29
|
+
"schema_id": args.id,
|
|
30
|
+
"allow_elided_lists": args.allow_elided_lists,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_arguments(cls, parser):
|
|
35
|
+
super().get_arguments(parser)
|
|
36
|
+
|
|
37
|
+
parser.add_argument("--title", help="Schema title")
|
|
38
|
+
parser.add_argument("--id", help="Schema ID")
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--allow-elided-lists",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Allow lists to be elided if they only contain a single element",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def get_extra_env(self):
|
|
46
|
+
return {
|
|
47
|
+
"varname": varname,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def get_additional_render_args(self):
|
|
51
|
+
return self.__render_args
|
shacl2code/lang/lang.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024 Joshua Watt
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
LANGUAGES = {}
|
|
9
|
+
|
|
10
|
+
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def language(name):
|
|
14
|
+
def inner(cls):
|
|
15
|
+
global LANGUAGES
|
|
16
|
+
LANGUAGES[name] = cls
|
|
17
|
+
return cls
|
|
18
|
+
|
|
19
|
+
return inner
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024 Joshua Watt
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
from .common import BasicJinjaRender
|
|
7
|
+
from .lang import language, TEMPLATE_DIR
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import keyword
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def varname(*name):
|
|
14
|
+
name = "_".join(name)
|
|
15
|
+
# Any invalid characters at the beginning of the name are removed (except
|
|
16
|
+
# "@")
|
|
17
|
+
name = re.sub(r"^[^a-zA-Z0-9_@]*", "", name)
|
|
18
|
+
# Any other invalid characters are replaced with "_" (including "@")
|
|
19
|
+
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
20
|
+
# Consolidate runs of "_" to a single one
|
|
21
|
+
name = re.sub(r"__+", "_", name)
|
|
22
|
+
# Add a _ to anything that is a python keyword
|
|
23
|
+
while keyword.iskeyword(name):
|
|
24
|
+
name = name + "_"
|
|
25
|
+
return name
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@language("python")
|
|
29
|
+
class PythonRender(BasicJinjaRender):
|
|
30
|
+
HELP = "Python Language Bindings"
|
|
31
|
+
|
|
32
|
+
def __init__(self, args):
|
|
33
|
+
super().__init__(args, TEMPLATE_DIR / "python.j2")
|
|
34
|
+
self.__render_args = {
|
|
35
|
+
"elide_lists": args.elide_lists,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_arguments(cls, parser):
|
|
40
|
+
super().get_arguments(parser)
|
|
41
|
+
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--elide-lists",
|
|
44
|
+
action="store_true",
|
|
45
|
+
help="Elide lists when writing documents if they only contain a single item",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get_extra_env(self):
|
|
49
|
+
return {
|
|
50
|
+
"varname": varname,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def get_additional_render_args(self):
|
|
54
|
+
return self.__render_args
|