schemapack 1.0.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.
schemapack/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ # Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """A specification (plus tooling) for describing linked data models based on JSON schema."""
17
+
18
+ from importlib.metadata import version
19
+
20
+ __version__ = version(__package__)
schemapack/__main__.py ADDED
@@ -0,0 +1,16 @@
1
+ # Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """Entrypoint of the package"""
@@ -0,0 +1,202 @@
1
+ # Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Collection of package-specific exceptions"""
18
+
19
+ from abc import ABC
20
+ from dataclasses import dataclass
21
+ from typing import Optional
22
+
23
+ import pydantic_core
24
+
25
+
26
+ class BaseError(ABC, Exception):
27
+ """Base class for all schemapack errors."""
28
+
29
+
30
+ class SpecError(BaseError, ValueError):
31
+ """An error indicating that an instance was not valid against the specification."""
32
+
33
+ def __init__(self, message: str, *, details: list[pydantic_core.ErrorDetails]):
34
+ """Initiate a SpecError.
35
+
36
+ Args:
37
+ message: A human-readable message describing the error.
38
+ details: Details on which fields of the instance are invalid.
39
+ """
40
+ super().__init__(message)
41
+ self.message = message
42
+ self.details = details
43
+
44
+
45
+ class SchemaPackSpecError(SpecError):
46
+ """An error indicating that the provided data is not valid against the schemapack
47
+ specification.
48
+ """
49
+
50
+ def __init__(self, message: str, *, details: list[pydantic_core.ErrorDetails]):
51
+ """Initiate a SchemaPackSpecError.
52
+
53
+ Args:
54
+ message: A human-readable message describing the error.
55
+ details: Details on which fields of the instance are invalid.
56
+ """
57
+ message = (
58
+ "The provided object was not compatible with the schemapack"
59
+ + f" specification: {message}"
60
+ )
61
+ super().__init__(message, details=details)
62
+
63
+
64
+ class DataPackSpecError(SpecError):
65
+ """An error indicating that the provided data is not valid against the datapack
66
+ specification (this is independent of validation done using a schemapack).
67
+ """
68
+
69
+ def __init__(self, message: str, *, details: list[pydantic_core.ErrorDetails]):
70
+ """Initiate a SchemaPackSpecError.
71
+
72
+ Args:
73
+ message: A human-readable message describing the error.
74
+ details: Details on which fields of the instance are invalid.
75
+ """
76
+ message = (
77
+ "The provided object was not compatible with the datapack"
78
+ + f" specification: {message}"
79
+ )
80
+ super().__init__(message, details=details)
81
+
82
+
83
+ class ValidationPluginError(BaseError, ValueError):
84
+ """Raised by a ValidationPlugin."""
85
+
86
+ def __init__(
87
+ self, *, type_: str, message: str, details: Optional[dict[str, object]] = None
88
+ ):
89
+ """Initiate a ValidationPluginError.
90
+
91
+ Args:
92
+ type: A preferably short type label.
93
+ message: A human-readable message describing the error.
94
+ details: A dictionary for transporting additional machine-readable details.
95
+ """
96
+ super().__init__(message)
97
+ self.type_ = type_
98
+ self.message = message
99
+ self.details = details if details else {}
100
+
101
+
102
+ @dataclass
103
+ class ValidationErrorRecord:
104
+ """A record of an Error occuring during validation on a specific context
105
+ (e.g. a specific resource) regarding a specific validation aspect.
106
+
107
+ Attributes:
108
+ subject_class:
109
+ If relevant, the name of the subject class as per the schemapack.
110
+ subject_resource:
111
+ If relevant, the ID or the subject resource of the subject class.
112
+ type:
113
+ A preferably short type label.
114
+ message:
115
+ A human-readable message describing the error.
116
+ details:
117
+ A dictionary for transporting additional machine-readable details.
118
+ """
119
+
120
+ subject_class: Optional[str]
121
+ subject_resource: Optional[str]
122
+ type: str
123
+ message: str
124
+ details: dict[str, object]
125
+
126
+
127
+ def _val_record_to_str(record: ValidationErrorRecord) -> str:
128
+ """Translate a ValidationErrorRecords into a human-readable message
129
+ to be displayed to the user on the terminal.
130
+ """
131
+ if record.subject_class:
132
+ if record.subject_resource:
133
+ context = f"resource '{record.subject_class}.{record.subject_resource}'"
134
+ else:
135
+ context = "class '{record.subject_class}'"
136
+ else:
137
+ context = "global datapack"
138
+
139
+ return (
140
+ f"Error in {context}:"
141
+ + f"\n\tType: {record.type}"
142
+ + f"\n\tMessage: {record.message}"
143
+ )
144
+
145
+
146
+ def _sorted_val_records(
147
+ records: list[ValidationErrorRecord],
148
+ ) -> list[ValidationErrorRecord]:
149
+ """Sort a set of ValidationErrorRecords."""
150
+ return sorted(records, key=lambda r: (r.subject_class, r.subject_resource, r.type))
151
+
152
+
153
+ class ValidationError(BaseError, ValueError):
154
+ """A collection of ValidationErrorRecords raised when a datapack fails validation
155
+ against a schemapack.
156
+ """
157
+
158
+ def __init__(self, records: list[ValidationErrorRecord]):
159
+ """Initiate a ValidationError.
160
+
161
+ Args:
162
+ records: A list of ValidationErrorRecords.
163
+ """
164
+ if not records:
165
+ raise ValueError("ValidationError must be raised with at least one record.")
166
+
167
+ self.records = _sorted_val_records(records)
168
+ n_records = len(self.records)
169
+ record_messages = "\n".join(_val_record_to_str(r) for r in self.records)
170
+ message = f"Validation failed with {n_records} issue(s):\n{record_messages}"
171
+
172
+ super().__init__(message)
173
+
174
+
175
+ class ValidationAssumptionError(BaseError, RuntimeError):
176
+ """This is raised when a unit of code assumes to work with a datapack that has
177
+ already been validated against a specific schemapack, however, it became apparent
178
+ that this assumption was not correct. The error should not contain the details on
179
+ which aspect is invalid. These details can be retrieved by performing a validation
180
+ and receiving a ValidationError.
181
+ """
182
+
183
+ def __init__(self, *, context: str):
184
+ """Initiate a ValidationAssumptionError.
185
+
186
+ Args:
187
+ context: Some context to be included in the error message.
188
+ """
189
+ message = (
190
+ "It was assumed that the provided datapack has already been validated"
191
+ + " against the provided schemapack, however, this assumption failed in"
192
+ + f" the context of {context}. Please validate the datapack against the"
193
+ + " schemapack to get further details."
194
+ )
195
+ super().__init__(message)
196
+ self.message = message
197
+
198
+
199
+ class CircularRelationError(BaseError, ValueError):
200
+ """Raised when a circular relation between resources is detected, but the requested
201
+ operation cannot be performed on datapacks with circular relations.
202
+ """
@@ -0,0 +1,131 @@
1
+ # Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Integrate rooted datapacks into nested json objects."""
18
+
19
+ from collections import defaultdict
20
+ from collections.abc import Mapping
21
+ from typing import Optional
22
+
23
+ from typing_extensions import TypeAlias
24
+
25
+ from schemapack.exceptions import CircularRelationError, ValidationAssumptionError
26
+ from schemapack.spec.datapack import ClassName, DataPack, ResourceId
27
+ from schemapack.spec.schemapack import SchemaPack
28
+
29
+ JsonObjectCompatible: TypeAlias = dict[str, object]
30
+
31
+
32
+ def integrate( # noqa: PLR0912,C901
33
+ *,
34
+ datapack: DataPack,
35
+ schemapack: SchemaPack,
36
+ _resource_blacklist: Optional[Mapping[ClassName, set[ResourceId]]] = None,
37
+ _alt_root_class_name: Optional[ClassName] = None,
38
+ _alt_root_resource_id: Optional[ResourceId] = None,
39
+ ) -> JsonObjectCompatible:
40
+ """Integrate a rooted datapack into a nested json object-compatible data structure.
41
+ It is assumed that the provided datapack has already been validated against the
42
+ provided schemapack.
43
+
44
+ Args:
45
+ datapack:
46
+ The datapack to be integrated. Must be rooted.
47
+ schemapack:
48
+ The schemapack to be used for looking up the classes of relations.
49
+ _resource_blacklist:
50
+ An optional blacklist of resources (a mapping with resource ids as values
51
+ and class names as keys) that cause an error to be raised if they are
52
+ encountered when assembling the json compatible object. This is only used
53
+ internally to prevent circular relations.
54
+ _alt_root_class_name:
55
+ An optional alternative root class name to be used instead of the class
56
+ name specified as root in the datapack. This is only used internally for
57
+ recursive calls.
58
+ _alt_root_resource_id:
59
+ An optional alternative root resource id to be used instead of the resource
60
+ id specified as root in the datapack. This is only used internally for
61
+ recursive calls.
62
+
63
+ Raises:
64
+ ValueError:
65
+ If the datapack is not rooted.
66
+ schemapack.exceptions.ValidationAssumptionError:
67
+ If the datapack is not valid against the schemapack.
68
+ schemapack.exceptions.CircularRelationError:
69
+ If a circular relation is detected.
70
+ """
71
+ if not datapack.root:
72
+ raise ValueError("Datapack must be rooted.")
73
+
74
+ root_class_name = _alt_root_class_name or datapack.root.class_name
75
+ root_resource_id = _alt_root_resource_id or datapack.root.resource_id
76
+
77
+ resource_blacklist: dict[ClassName, set[ResourceId]] = defaultdict(set)
78
+ resource_blacklist[root_class_name].add(root_resource_id)
79
+ if _resource_blacklist:
80
+ for class_name, resource_ids in _resource_blacklist.items():
81
+ resource_blacklist[class_name].update(resource_ids)
82
+
83
+ root_resource = datapack.resources[root_class_name][root_resource_id]
84
+ try:
85
+ root_class_definition = schemapack.classes[root_class_name]
86
+ except KeyError as error:
87
+ raise ValidationAssumptionError(context="class lookup") from error
88
+
89
+ integrated_object: JsonObjectCompatible = {}
90
+ integrated_object.update(root_resource.content)
91
+
92
+ for relation_name, foreign_ids in root_resource.relations.items():
93
+ if isinstance(foreign_ids, str):
94
+ foreign_ids = [foreign_ids]
95
+
96
+ try:
97
+ relation_definition = root_class_definition.relations[relation_name]
98
+ except KeyError as error:
99
+ raise ValidationAssumptionError(context="relation resolution") from error
100
+
101
+ foreign_class_name = relation_definition.to
102
+
103
+ is_plural = relation_definition.cardinality.endswith("to_many")
104
+ if is_plural:
105
+ integrated_object[relation_name] = []
106
+
107
+ for foreign_id in foreign_ids:
108
+ if (
109
+ foreign_class_name in resource_blacklist
110
+ and foreign_id in resource_blacklist[foreign_class_name]
111
+ ):
112
+ raise CircularRelationError(
113
+ "Cannot perform integration of datapack with circular relations."
114
+ + " The circular relation involved the resource with id"
115
+ + f" {foreign_id} of class {foreign_class_name}."
116
+ )
117
+
118
+ foreign_resource = integrate(
119
+ datapack=datapack,
120
+ schemapack=schemapack,
121
+ _resource_blacklist=resource_blacklist,
122
+ _alt_root_class_name=foreign_class_name,
123
+ _alt_root_resource_id=foreign_id,
124
+ )
125
+
126
+ if is_plural:
127
+ integrated_object[relation_name].append(foreign_resource) # type: ignore
128
+ else:
129
+ integrated_object[relation_name] = foreign_resource
130
+
131
+ return integrated_object
schemapack/isolate.py ADDED
@@ -0,0 +1,185 @@
1
+ # Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Logic to isolate a resource from a non-rooted datapack to create a rooted datapack."""
18
+
19
+ from collections import defaultdict
20
+ from collections.abc import Mapping
21
+ from typing import Optional
22
+
23
+ from schemapack.exceptions import ValidationAssumptionError
24
+ from schemapack.spec.datapack import (
25
+ ClassName,
26
+ DataPack,
27
+ Resource,
28
+ ResourceId,
29
+ RootResource,
30
+ )
31
+ from schemapack.spec.schemapack import SchemaPack
32
+
33
+
34
+ def identify_dependencies(
35
+ *,
36
+ datapack: DataPack,
37
+ class_name: ClassName,
38
+ resource_id: ResourceId,
39
+ schemapack: SchemaPack,
40
+ include_target: bool = False,
41
+ _resource_blacklist: Optional[Mapping[ClassName, set[ResourceId]]] = None,
42
+ ) -> Mapping[ClassName, set[ResourceId]]:
43
+ """Identify all dependencies (recursively) for a given resource
44
+ of the given class in the given datapack. Please note that it is assumed that
45
+ the datapack has already been validated against the schemapack.
46
+
47
+ Args:
48
+ datapack:
49
+ Containing the target resource and all dependencies.
50
+ class_name:
51
+ The class of the target resource.
52
+ resource_id:
53
+ The id of the target resource.
54
+ schemapack:
55
+ The schemapack used for looking up the classes of relations.
56
+ include_target:
57
+ Set to true if the target dependency shall be included in the result.
58
+ _resource_blacklist:
59
+ A mapping containing resources (resource ids as values and class names as
60
+ keys) that shall be ignored during the dependency resolution and thus will
61
+ not appear in the result. This is only used internally for recursion.
62
+
63
+ Returns:
64
+ A mapping containing resource ids (values) of dependencies by class names
65
+ (keys).
66
+
67
+ Raises:
68
+ KeyError:
69
+ If the resource_class or the resource_id does not exist in the schemapack
70
+ or datapack.
71
+ schemapack.Exceptions.ValidationAssumptionError:
72
+ If it became apparent that the datapack was not already validated against
73
+ the schemapack.
74
+ """
75
+ target_resource = datapack.resources[class_name][resource_id]
76
+ target_class_definition = schemapack.classes[class_name]
77
+
78
+ # Define a blacklist of resources to avoid getting lost in infinity loop for
79
+ # circular dependencies:
80
+ resource_blacklist: dict[ClassName, set[ResourceId]] = defaultdict(set)
81
+ resource_blacklist[class_name].add(resource_id)
82
+ if _resource_blacklist:
83
+ for class_name, resource_ids in _resource_blacklist.items():
84
+ resource_blacklist[class_name].update(resource_ids)
85
+
86
+ dependencies_by_class: dict[ClassName, set[ResourceId]] = defaultdict(set)
87
+
88
+ for relation_name, foreign_ids in target_resource.relations.items():
89
+ if isinstance(foreign_ids, str):
90
+ foreign_ids = [foreign_ids]
91
+
92
+ try:
93
+ foreign_class_name = target_class_definition.relations[relation_name].to
94
+ except KeyError as error:
95
+ raise ValidationAssumptionError(context="relation resolution") from error
96
+
97
+ for foreign_id in foreign_ids:
98
+ if (
99
+ foreign_class_name in resource_blacklist
100
+ and foreign_id in resource_blacklist[foreign_class_name]
101
+ ):
102
+ continue
103
+
104
+ dependencies_by_class[foreign_class_name].add(foreign_id)
105
+
106
+ # Recursively add dependencies of this foreign resource:
107
+ nested_dependencies = identify_dependencies(
108
+ datapack=datapack,
109
+ class_name=foreign_class_name,
110
+ resource_id=foreign_id,
111
+ schemapack=schemapack,
112
+ _resource_blacklist=resource_blacklist,
113
+ )
114
+ for nested_class_name, nested_ids in nested_dependencies.items():
115
+ dependencies_by_class[nested_class_name].update(nested_ids)
116
+
117
+ if include_target:
118
+ dependencies_by_class[class_name].add(resource_id)
119
+
120
+ return dependencies_by_class
121
+
122
+
123
+ def downscope_datapack(
124
+ datapack: DataPack,
125
+ resource_map: Mapping[ClassName, set[ResourceId]],
126
+ ignore_non_existing: bool = False,
127
+ ) -> DataPack:
128
+ """Downscope a datapack to only contain the given resources.
129
+
130
+ Args:
131
+ ignore_non_existing:
132
+ Controls how to handle resources from the resource_map that are not in the
133
+ datapack. If set to `True`, these resources will be ignored. If set to
134
+ `False` (default), a KeyError will be raised.
135
+
136
+ Raises:
137
+ KeyError:
138
+ If a resource from the resource_map is not in the datapack and
139
+ ignore_non_existing is set to `False`.
140
+
141
+ """
142
+ resources: dict[ClassName, dict[ResourceId, Resource]] = defaultdict(dict)
143
+
144
+ for class_name, resource_ids in resource_map.items():
145
+ try:
146
+ existing_resources = datapack.resources[class_name]
147
+ except KeyError as error:
148
+ if ignore_non_existing:
149
+ continue
150
+ raise error
151
+
152
+ for resource_id in resource_ids:
153
+ try:
154
+ existing_resource = existing_resources[resource_id]
155
+ except KeyError as error:
156
+ if ignore_non_existing:
157
+ continue
158
+ raise error
159
+
160
+ resources[class_name][resource_id] = existing_resource
161
+
162
+ return datapack.model_copy(update={"resources": resources})
163
+
164
+
165
+ def isolate(
166
+ *,
167
+ datapack: DataPack,
168
+ class_name: ClassName,
169
+ resource_id: ResourceId,
170
+ schemapack: SchemaPack,
171
+ ) -> DataPack:
172
+ """Isolate a resource from a non-rooted datapack to created a rooted datapack. I.e.
173
+ the resulting datapack will only contain resources referenced by the root resource
174
+ as well as the root resource itself.
175
+ """
176
+ dependency_map = identify_dependencies(
177
+ datapack=datapack,
178
+ class_name=class_name,
179
+ resource_id=resource_id,
180
+ schemapack=schemapack,
181
+ include_target=True,
182
+ )
183
+ rooted_datapack = downscope_datapack(datapack=datapack, resource_map=dependency_map)
184
+ rooted_datapack.root = RootResource(class_name=class_name, resource_id=resource_id)
185
+ return rooted_datapack
schemapack/load.py ADDED
@@ -0,0 +1,49 @@
1
+ # Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Loading schemapack and datapack definitions."""
18
+
19
+ from pathlib import Path
20
+
21
+ import pydantic
22
+
23
+ from schemapack.exceptions import DataPackSpecError, SchemaPackSpecError
24
+ from schemapack.spec.datapack import DataPack
25
+ from schemapack.spec.schemapack import SchemaPack
26
+ from schemapack.utils import read_json_or_yaml, transient_directory_change
27
+
28
+
29
+ def load_schemapack(path: Path):
30
+ """Load a schemapack definition from a file."""
31
+ schemapack_dict = read_json_or_yaml(path)
32
+
33
+ with transient_directory_change(path.parent):
34
+ try:
35
+ return SchemaPack.model_validate(schemapack_dict)
36
+ except pydantic.ValidationError as error:
37
+ raise SchemaPackSpecError(
38
+ message=str(error), details=error.errors()
39
+ ) from error
40
+
41
+
42
+ def load_datapack(path: Path):
43
+ """Load a datapack definition from a file."""
44
+ datapack_dict = read_json_or_yaml(path)
45
+
46
+ try:
47
+ return DataPack.model_validate(datapack_dict)
48
+ except pydantic.ValidationError as error:
49
+ raise DataPackSpecError(message=str(error), details=error.errors()) from error
schemapack/main.py ADDED
@@ -0,0 +1,45 @@
1
+ # Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """High-level convenience functions."""
18
+
19
+ from pathlib import Path
20
+
21
+ from schemapack.load import load_datapack, load_schemapack
22
+ from schemapack.spec.datapack import DataPack
23
+ from schemapack.spec.schemapack import SchemaPack
24
+ from schemapack.validation import SchemaPackValidator
25
+
26
+
27
+ def load_and_validate(
28
+ *, schemapack_path: Path, datapack_path: Path
29
+ ) -> tuple[SchemaPack, DataPack]:
30
+ """Load and validate a datapack with respect to a schemapack using the default
31
+ validation plugins.
32
+
33
+ Raises:
34
+ schemapack.exceptions.SchemaPackFormatError:
35
+ If the schemapack has an invalid format.
36
+ schemapack.exceptions.DataPackFormatError:
37
+ If the datapack has an invalid format.
38
+ schemapack.exceptions.ValidationError:
39
+ If the datapack is not valid against the schemapack.
40
+ """
41
+ schemapack = load_schemapack(schemapack_path)
42
+ datapack = load_datapack(datapack_path)
43
+ validator = SchemaPackValidator(schemapack=schemapack)
44
+ validator.validate(datapack=datapack)
45
+ return schemapack, datapack
@@ -0,0 +1,17 @@
1
+ # Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Models for SchemaPack and DataPack definitions."""