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 +20 -0
- schemapack/__main__.py +16 -0
- schemapack/exceptions.py +202 -0
- schemapack/integrate.py +131 -0
- schemapack/isolate.py +185 -0
- schemapack/load.py +49 -0
- schemapack/main.py +45 -0
- schemapack/spec/__init__.py +17 -0
- schemapack/spec/datapack.py +135 -0
- schemapack/spec/schemapack.py +400 -0
- schemapack/utils.py +146 -0
- schemapack/validation/__init__.py +20 -0
- schemapack/validation/base.py +128 -0
- schemapack/validation/default.py +54 -0
- schemapack/validation/main.py +239 -0
- schemapack/validation/plugins/__init__.py +52 -0
- schemapack/validation/plugins/cardinality_plural.py +79 -0
- schemapack/validation/plugins/content_schema.py +78 -0
- schemapack/validation/plugins/duplicate_foreign_id.py +88 -0
- schemapack/validation/plugins/foreign_id.py +79 -0
- schemapack/validation/plugins/id_from_content.py +65 -0
- schemapack/validation/plugins/missing_class.py +60 -0
- schemapack/validation/plugins/missing_relations.py +70 -0
- schemapack/validation/plugins/one_to_many_overlap.py +101 -0
- schemapack/validation/plugins/one_to_one_overlap.py +101 -0
- schemapack/validation/plugins/unknown_class.py +63 -0
- schemapack/validation/plugins/unknown_relations.py +70 -0
- schemapack-1.0.0.dist-info/LICENSE +202 -0
- schemapack-1.0.0.dist-info/METADATA +66 -0
- schemapack-1.0.0.dist-info/RECORD +32 -0
- schemapack-1.0.0.dist-info/WHEEL +5 -0
- schemapack-1.0.0.dist-info/top_level.txt +1 -0
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"""
|
schemapack/exceptions.py
ADDED
|
@@ -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
|
+
"""
|
schemapack/integrate.py
ADDED
|
@@ -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."""
|