strictyamlx 0.1.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.
- strictyamlx-0.1.0/LICENSE +21 -0
- strictyamlx-0.1.0/PKG-INFO +20 -0
- strictyamlx-0.1.0/README.md +2 -0
- strictyamlx-0.1.0/pyproject.toml +25 -0
- strictyamlx-0.1.0/src/strictyamlx/__init__.py +7 -0
- strictyamlx-0.1.0/src/strictyamlx/blocks.py +44 -0
- strictyamlx-0.1.0/src/strictyamlx/builder.py +78 -0
- strictyamlx-0.1.0/src/strictyamlx/control.py +65 -0
- strictyamlx-0.1.0/src/strictyamlx/dmap.py +97 -0
- strictyamlx-0.1.0/src/strictyamlx/forwardref.py +37 -0
- strictyamlx-0.1.0/src/strictyamlx/utils.py +17 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muneeb ur Rahman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strictyamlx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An extension of StrictYAML that adds more expressive schema tools for validation.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Muneeb ur Rahman
|
|
8
|
+
Author-email: muneebdev1@gmail.com
|
|
9
|
+
Requires-Python: >=3.12,<=3.14
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Dist: strictyaml (==1.7.3)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# strictyamlx
|
|
19
|
+
An extension of StrictYAML that adds more expressive schema tools for validation.
|
|
20
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "strictyamlx"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "An extension of StrictYAML that adds more expressive schema tools for validation."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Muneeb ur Rahman",email = "muneebdev1@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = {text = "MIT"}
|
|
10
|
+
requires-python = ">=3.12,<=3.14"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"strictyaml==1.7.3"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.poetry]
|
|
16
|
+
packages = [{include = "strictyamlx", from = "src"}]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
20
|
+
build-backend = "poetry.core.masonry.api"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest (>=9.0.2,<10.0.0)"
|
|
25
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from strictyaml import Validator
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Block:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
when: Callable[..., bool],
|
|
9
|
+
schema: Validator,
|
|
10
|
+
constraints: list[Callable[..., bool]] | None = None,
|
|
11
|
+
):
|
|
12
|
+
assert isinstance(schema, Validator), "schema must be of type Validator"
|
|
13
|
+
self.when = when
|
|
14
|
+
self._validator = schema
|
|
15
|
+
self.constraints = constraints
|
|
16
|
+
|
|
17
|
+
def __repr__(self):
|
|
18
|
+
return "{0}(when={1}, schema={2}{3})".format(
|
|
19
|
+
self.__class__.__name__,
|
|
20
|
+
repr(self.when),
|
|
21
|
+
repr(self._validator),
|
|
22
|
+
", constraints={0}".format(repr(self.constraints)) if self.constraints else "",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Case(Block):
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
when: Callable[..., bool],
|
|
30
|
+
schema: Validator,
|
|
31
|
+
constraints: list[Callable[..., bool]] | None = None,
|
|
32
|
+
):
|
|
33
|
+
super().__init__(when, schema, constraints)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# TODO: implement in DMap
|
|
37
|
+
class Overlay(Block):
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
when: Callable[..., bool],
|
|
41
|
+
schema: Validator,
|
|
42
|
+
constraints: list[Callable[..., bool]] | None = None,
|
|
43
|
+
):
|
|
44
|
+
super().__init__(when, schema, constraints)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from strictyaml import Validator
|
|
2
|
+
from strictyaml.validators import MapValidator
|
|
3
|
+
from strictyaml import Map, MapCombined
|
|
4
|
+
import copy
|
|
5
|
+
from .utils import unpack, ensure_validator_dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ValidatorBuilder:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
control_validator: Validator,
|
|
12
|
+
case_validator: Validator,
|
|
13
|
+
control_source: tuple[str] | str | None = None,
|
|
14
|
+
):
|
|
15
|
+
self.control_source = control_source
|
|
16
|
+
self.control_validator = control_validator
|
|
17
|
+
self.case_validator = case_validator
|
|
18
|
+
self.validator = self._build()
|
|
19
|
+
|
|
20
|
+
def merge_recursive(self, control_validator, case_validator):
|
|
21
|
+
control_validator = unpack(control_validator)
|
|
22
|
+
if not hasattr(control_validator, '_validator') or not isinstance(control_validator._validator, dict):
|
|
23
|
+
return
|
|
24
|
+
if not hasattr(case_validator, '_validator') or not isinstance(case_validator._validator, dict):
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
for key, val in control_validator._validator.items():
|
|
28
|
+
val_unpacked = unpack(val)
|
|
29
|
+
if isinstance(val_unpacked, MapValidator):
|
|
30
|
+
if key not in case_validator._validator:
|
|
31
|
+
case_validator._validator[key] = Map({})
|
|
32
|
+
|
|
33
|
+
target = ensure_validator_dict(case_validator._validator[key])
|
|
34
|
+
case_validator._validator[key] = target
|
|
35
|
+
self.merge_recursive(val, target)
|
|
36
|
+
else:
|
|
37
|
+
if key not in case_validator._validator:
|
|
38
|
+
case_validator._validator[key] = val
|
|
39
|
+
|
|
40
|
+
def rebuild_validator_recursive(self, validator):
|
|
41
|
+
validator = ensure_validator_dict(validator)
|
|
42
|
+
if not hasattr(validator, '_validator') or not isinstance(validator._validator, dict):
|
|
43
|
+
return validator
|
|
44
|
+
|
|
45
|
+
new_dict = {}
|
|
46
|
+
for key, val in validator._validator.items():
|
|
47
|
+
val_unpacked = ensure_validator_dict(val)
|
|
48
|
+
if hasattr(val_unpacked, '_validator') and isinstance(val_unpacked._validator, dict):
|
|
49
|
+
new_dict[key] = self.rebuild_validator_recursive(val_unpacked)
|
|
50
|
+
else:
|
|
51
|
+
new_dict[key] = val_unpacked
|
|
52
|
+
|
|
53
|
+
if isinstance(validator, MapCombined):
|
|
54
|
+
return MapCombined(new_dict, validator.key_validator, getattr(validator, '_value_validator', None))
|
|
55
|
+
return Map(new_dict)
|
|
56
|
+
|
|
57
|
+
def _build(self):
|
|
58
|
+
control_validator = copy.deepcopy(unpack(self.control_validator))
|
|
59
|
+
if self.control_source:
|
|
60
|
+
if isinstance(self.control_source, str):
|
|
61
|
+
self.control_source = [self.control_source]
|
|
62
|
+
for key in reversed(self.control_source):
|
|
63
|
+
map_layer = Map({})
|
|
64
|
+
map_layer._validator[key] = control_validator
|
|
65
|
+
control_validator = map_layer
|
|
66
|
+
|
|
67
|
+
case_validator = copy.deepcopy(ensure_validator_dict(self.case_validator))
|
|
68
|
+
|
|
69
|
+
if hasattr(case_validator, 'control') and hasattr(case_validator.control, '_validator'):
|
|
70
|
+
case_validator.control._validator = ValidatorBuilder(
|
|
71
|
+
control_validator, case_validator.control._validator
|
|
72
|
+
).validator
|
|
73
|
+
return case_validator
|
|
74
|
+
|
|
75
|
+
self.merge_recursive(control_validator, case_validator)
|
|
76
|
+
final_validator = self.rebuild_validator_recursive(case_validator)
|
|
77
|
+
|
|
78
|
+
return final_validator
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from strictyaml import Validator
|
|
2
|
+
from strictyaml.validators import MapValidator
|
|
3
|
+
from strictyaml.yamllocation import YAMLChunk
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from strictyaml.ruamel.comments import CommentedMap
|
|
6
|
+
from strictyaml.exceptions import YAMLSerializationError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Control:
|
|
10
|
+
def __init__(self, validator: Validator, source: tuple[str] | str | None = None):
|
|
11
|
+
self._validator = validator
|
|
12
|
+
self.source = source
|
|
13
|
+
self.validated = None
|
|
14
|
+
|
|
15
|
+
assert isinstance(
|
|
16
|
+
self._validator, Validator
|
|
17
|
+
), "validator must be of type Validator"
|
|
18
|
+
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
return "Control({0}{1})".format(
|
|
21
|
+
repr(self._validator),
|
|
22
|
+
", source={0}".format(repr(self.source)) if self.source else "",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def projection(self, chunk, validator):
|
|
26
|
+
from .utils import unpack
|
|
27
|
+
validator = unpack(validator)
|
|
28
|
+
projected_chunk = {}
|
|
29
|
+
|
|
30
|
+
if hasattr(validator, '_validator_dict'):
|
|
31
|
+
keys = validator._validator_dict.items()
|
|
32
|
+
elif hasattr(validator, '_validator') and isinstance(validator._validator, dict):
|
|
33
|
+
# for when _validator_dict isn't explicitly built but _validator is a dict
|
|
34
|
+
keys = [
|
|
35
|
+
(k.key if hasattr(k, 'key') else k, v)
|
|
36
|
+
for k, v in validator._validator.items()
|
|
37
|
+
]
|
|
38
|
+
else:
|
|
39
|
+
from strictyaml.exceptions import InvalidValidatorError
|
|
40
|
+
raise InvalidValidatorError("Control validator must be a Map with specific keys")
|
|
41
|
+
|
|
42
|
+
for key, val in keys:
|
|
43
|
+
val = unpack(val)
|
|
44
|
+
if key in chunk:
|
|
45
|
+
if isinstance(val, MapValidator):
|
|
46
|
+
projected_chunk[key] = self.projection(
|
|
47
|
+
chunk[key], val
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
projected_chunk[key] = chunk[key]
|
|
51
|
+
|
|
52
|
+
return CommentedMap(projected_chunk)
|
|
53
|
+
|
|
54
|
+
def validate(self, chunk):
|
|
55
|
+
if self.source and self.source != "":
|
|
56
|
+
if isinstance(self.source, str):
|
|
57
|
+
chunk_pointer = chunk.contents[self.source]
|
|
58
|
+
elif isinstance(self.source, tuple):
|
|
59
|
+
chunk_pointer = reduce(
|
|
60
|
+
lambda d, key: d[key], self.source, chunk.contents
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
chunk_pointer = chunk.contents
|
|
64
|
+
source_chunk = YAMLChunk(self.projection(chunk_pointer, self._validator))
|
|
65
|
+
self.validated = self._validator(source_chunk)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from strictyaml import Validator
|
|
2
|
+
from strictyaml.validators import MapValidator
|
|
3
|
+
from strictyaml.exceptions import YAMLSerializationError, InvalidValidatorError
|
|
4
|
+
from .control import Control
|
|
5
|
+
from .blocks import Block
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from .builder import ValidatorBuilder
|
|
8
|
+
from strictyaml.representation import YAML
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DMap(MapValidator):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
control: Control,
|
|
15
|
+
blocks: list[Block],
|
|
16
|
+
constraints: list[Callable[..., bool]] | None = None,
|
|
17
|
+
):
|
|
18
|
+
assert isinstance(control, Control), "control must be of type Control"
|
|
19
|
+
assert isinstance(blocks, list), "blocks must be a list of Block"
|
|
20
|
+
for block in blocks:
|
|
21
|
+
assert isinstance(block, Block), "all blocks must be of type Block"
|
|
22
|
+
|
|
23
|
+
if constraints is not None:
|
|
24
|
+
assert isinstance(constraints, list), "constraints must be a list of Callable"
|
|
25
|
+
for constraint in constraints:
|
|
26
|
+
assert callable(constraint), "every constraint must be callable"
|
|
27
|
+
|
|
28
|
+
self.control = control
|
|
29
|
+
self.blocks = blocks
|
|
30
|
+
self.constraints = constraints
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def compile_when(when):
|
|
34
|
+
if callable(when):
|
|
35
|
+
return when
|
|
36
|
+
return lambda raw, ctrl: bool(when)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def compile_constraint(when):
|
|
40
|
+
if callable(when):
|
|
41
|
+
return when
|
|
42
|
+
return lambda raw, ctrl, val: bool(when)
|
|
43
|
+
|
|
44
|
+
def validate(self, chunk):
|
|
45
|
+
chunk.expect_mapping()
|
|
46
|
+
self.control.validate(chunk)
|
|
47
|
+
ctrl = self.control.validated.data
|
|
48
|
+
raw = chunk.whole_document
|
|
49
|
+
control_validator = self.control._validator
|
|
50
|
+
true_case_block = None
|
|
51
|
+
# TODO: what if the user doesn't really want a control validator and only selects based on raw
|
|
52
|
+
for block in self.blocks:
|
|
53
|
+
if DMap.compile_when(block.when)(raw, ctrl):
|
|
54
|
+
if true_case_block is None:
|
|
55
|
+
true_case_block = block
|
|
56
|
+
else:
|
|
57
|
+
chunk.expecting_but_found("when evaluating DMap blocks", "multiple cases were true")
|
|
58
|
+
|
|
59
|
+
if true_case_block is None:
|
|
60
|
+
chunk.expecting_but_found("when evaluating DMap blocks", "none of the cases were true")
|
|
61
|
+
|
|
62
|
+
final_validator = ValidatorBuilder(
|
|
63
|
+
control_validator, true_case_block._validator, self.control.source
|
|
64
|
+
).validator
|
|
65
|
+
|
|
66
|
+
self.validated = final_validator(chunk)
|
|
67
|
+
val = self.validated.data
|
|
68
|
+
|
|
69
|
+
if self.constraints:
|
|
70
|
+
for constraint in self.constraints:
|
|
71
|
+
if not DMap.compile_constraint(constraint)(raw, ctrl, val):
|
|
72
|
+
chunk.expecting_but_found("when evaluating DMap constraints", "constraints not fulfilled")
|
|
73
|
+
if true_case_block.constraints:
|
|
74
|
+
for constraint in true_case_block.constraints:
|
|
75
|
+
if not DMap.compile_constraint(constraint)(raw, ctrl, val):
|
|
76
|
+
chunk.expecting_but_found("when evaluating DMap case constraints", "constraints not fulfilled")
|
|
77
|
+
|
|
78
|
+
def to_yaml(self, data):
|
|
79
|
+
self._should_be_mapping(data)
|
|
80
|
+
|
|
81
|
+
for block in self.blocks:
|
|
82
|
+
try:
|
|
83
|
+
final_validator = ValidatorBuilder(
|
|
84
|
+
self.control._validator, block._validator, self.control.source
|
|
85
|
+
).validator
|
|
86
|
+
return final_validator.to_yaml(data)
|
|
87
|
+
except (YAMLSerializationError, KeyError):
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
raise YAMLSerializationError("None of the DMap cases successfully serialized the data")
|
|
91
|
+
|
|
92
|
+
def __repr__(self):
|
|
93
|
+
return "DMap({0}, {1}{2})".format(
|
|
94
|
+
repr(self.control),
|
|
95
|
+
repr(self.blocks),
|
|
96
|
+
", constraints={0}".format(repr(self.constraints)) if self.constraints else "",
|
|
97
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from strictyaml import Validator
|
|
2
|
+
from strictyaml.exceptions import YAMLSerializationError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ForwardRefValidator(Validator):
|
|
6
|
+
def _should_be_validator(self, validator):
|
|
7
|
+
if not isinstance(validator, Validator):
|
|
8
|
+
raise YAMLSerializationError(
|
|
9
|
+
"Expected a Validator, found '{}'".format(validator)
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ForwardRef(ForwardRefValidator):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self._validator = None
|
|
16
|
+
self.has_expanded = False
|
|
17
|
+
|
|
18
|
+
def set(self, validator: Validator):
|
|
19
|
+
self._should_be_validator(validator)
|
|
20
|
+
self._validator = validator
|
|
21
|
+
|
|
22
|
+
def __call__(self, chunk):
|
|
23
|
+
if self._validator is None:
|
|
24
|
+
raise YAMLSerializationError("ForwardRef was used before it was set")
|
|
25
|
+
return self._validator(chunk)
|
|
26
|
+
|
|
27
|
+
def __repr__(self):
|
|
28
|
+
if self._validator is None:
|
|
29
|
+
return "{0}()".format(self.__class__.__name__)
|
|
30
|
+
if not self.has_expanded:
|
|
31
|
+
self.has_expanded = True
|
|
32
|
+
return "{0}".format(repr(self._validator))
|
|
33
|
+
else:
|
|
34
|
+
return "{0}()".format(self.__class__.__name__)
|
|
35
|
+
|
|
36
|
+
def to_yaml(self, data):
|
|
37
|
+
return self._validator.to_yaml(data)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .forwardref import ForwardRef
|
|
2
|
+
from strictyaml import Map, MapCombined, MapPattern
|
|
3
|
+
from strictyaml.validators import Validator
|
|
4
|
+
from strictyaml.exceptions import YAMLSerializationError
|
|
5
|
+
|
|
6
|
+
def unpack(validator):
|
|
7
|
+
while isinstance(validator, ForwardRef):
|
|
8
|
+
if validator._validator is None:
|
|
9
|
+
raise YAMLSerializationError("ForwardRef was used before it was set")
|
|
10
|
+
validator = validator._validator
|
|
11
|
+
return validator
|
|
12
|
+
|
|
13
|
+
def ensure_validator_dict(validator):
|
|
14
|
+
validator = unpack(validator)
|
|
15
|
+
if isinstance(validator, MapPattern):
|
|
16
|
+
return MapCombined({}, validator._key_validator, validator._value_validator)
|
|
17
|
+
return validator
|