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.
@@ -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,2 @@
1
+ # strictyamlx
2
+ An extension of StrictYAML that adds more expressive schema tools for validation.
@@ -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,7 @@
1
+ from strictyaml import *
2
+
3
+
4
+ from .forwardref import ForwardRef
5
+ from .dmap import DMap
6
+ from .control import Control
7
+ from .blocks import Block, Case
@@ -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