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 ADDED
@@ -0,0 +1,4 @@
1
+ from .model import Model, ModelException # noqa: F401
2
+ from .urlcontext import UrlContext, ContextData # noqa: F401
3
+ from .main import main # noqa: F401
4
+ from .version import VERSION # noqa: F401
shacl2code/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ #! /usr/bin/env python3
2
+ #
3
+ # Copyright (c) 2024 Joshua Watt
4
+ #
5
+ # SPDX-License-Identifier: MIT
6
+
7
+ import sys
8
+
9
+ from . import main
10
+
11
+ if __name__ == "__main__":
12
+ sys.exit(main())
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
+ )
@@ -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
@@ -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