nadap 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.
- nadap/__init__.py +14 -0
- nadap/base.py +222 -0
- nadap/errors.py +28 -0
- nadap/mixin/accuracy.py +88 -0
- nadap/mixin/allowed_value.py +79 -0
- nadap/mixin/ip.py +177 -0
- nadap/mixin/min_max.py +205 -0
- nadap/mixin/not_allowed_value.py +79 -0
- nadap/mixin/regex_mode.py +41 -0
- nadap/mixin/replace_empty_to.py +39 -0
- nadap/nadap.py +85 -0
- nadap/references.py +538 -0
- nadap/results.py +48 -0
- nadap/schema.py +558 -0
- nadap/types/any.py +43 -0
- nadap/types/base.py +530 -0
- nadap/types/bool.py +42 -0
- nadap/types/bool_false.py +41 -0
- nadap/types/bool_true.py +41 -0
- nadap/types/dict.py +536 -0
- nadap/types/enum.py +50 -0
- nadap/types/float.py +60 -0
- nadap/types/hostname.py +373 -0
- nadap/types/idlist.py +183 -0
- nadap/types/int.py +60 -0
- nadap/types/ip4_address.py +50 -0
- nadap/types/ip4_interface.py +57 -0
- nadap/types/ip4_network.py +54 -0
- nadap/types/ip6_address.py +50 -0
- nadap/types/ip6_interface.py +57 -0
- nadap/types/ip6_network.py +54 -0
- nadap/types/ip_address.py +51 -0
- nadap/types/ip_interface.py +58 -0
- nadap/types/ip_network.py +55 -0
- nadap/types/list.py +197 -0
- nadap/types/multitype.py +184 -0
- nadap/types/none.py +35 -0
- nadap/types/number.py +60 -0
- nadap/types/str.py +252 -0
- nadap-1.0.0.dist-info/METADATA +59 -0
- nadap-1.0.0.dist-info/RECORD +43 -0
- nadap-1.0.0.dist-info/WHEEL +4 -0
- nadap-1.0.0.dist-info/licenses/LICENSE +674 -0
nadap/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Central import for types
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from nadap.nadap import Nadap
|
|
6
|
+
from nadap.errors import SchemaDefinitionError, DataValidationError
|
|
7
|
+
from nadap.schema import Schema
|
|
8
|
+
from nadap.references import ReferenceFinding
|
|
9
|
+
from nadap.results import ValidationFinding
|
|
10
|
+
from nadap.base import SET_DEFAULTS, CONVERT_DATA, NOFLAG
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__version__ = "1.0.0"
|
|
14
|
+
version_info = (1, 0, 0)
|
nadap/base.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic constants and functions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# pylint: disable=too-few-public-methods
|
|
6
|
+
|
|
7
|
+
import enum
|
|
8
|
+
import copy
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
if hasattr(re, "NOFLAG"):
|
|
12
|
+
NOFLAG = re.NOFLAG
|
|
13
|
+
else:
|
|
14
|
+
NOFLAG = 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OPT(enum.IntFlag): # pylint: disable=no-member
|
|
18
|
+
"""
|
|
19
|
+
Flags used to define a reference
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
NONE = 1
|
|
23
|
+
UNIQUE = 2
|
|
24
|
+
UNIQUE_GLOBAL = 4
|
|
25
|
+
PRODUCER = 8
|
|
26
|
+
PRODUCER_GLOBAL = 16
|
|
27
|
+
CONSUMER = 32
|
|
28
|
+
CONSUMER_GLOBAL = 64
|
|
29
|
+
ALLOW_ORPHAN_PRODUCER = 128
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
INIT_OPTIONS = OPT.UNIQUE | OPT.UNIQUE_GLOBAL | OPT.PRODUCER | OPT.PRODUCER_GLOBAL
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RegexObject:
|
|
36
|
+
"""
|
|
37
|
+
Represents a regex allowed or not allowed value
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, pattern: str, multiline: bool, fullmatch: bool):
|
|
41
|
+
self.multiline = multiline
|
|
42
|
+
self.fullmatch = fullmatch
|
|
43
|
+
self.pattern = re.compile(pattern, self.flags)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def flags(self) -> list:
|
|
47
|
+
"""
|
|
48
|
+
Create a list of re args to pass to match
|
|
49
|
+
"""
|
|
50
|
+
if self.multiline:
|
|
51
|
+
return re.MULTILINE
|
|
52
|
+
return NOFLAG
|
|
53
|
+
|
|
54
|
+
def match(self, data: any) -> re.Match:
|
|
55
|
+
"""
|
|
56
|
+
Test data if it matches
|
|
57
|
+
Returns:
|
|
58
|
+
re.Match object or None
|
|
59
|
+
"""
|
|
60
|
+
if self.fullmatch:
|
|
61
|
+
return self.pattern.fullmatch(data)
|
|
62
|
+
return self.pattern.search(data)
|
|
63
|
+
|
|
64
|
+
def __hash__(self):
|
|
65
|
+
return self.pattern.pattern.__hash__()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PreProcessingFlag(enum.IntFlag): # pylint: disable=no-member
|
|
69
|
+
"""
|
|
70
|
+
Flags used as pre-processing options
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
NOFLAG = 0
|
|
74
|
+
SET_DEFAULTS = 1
|
|
75
|
+
CONVERT_DATA = 2
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ValEnv:
|
|
79
|
+
"""
|
|
80
|
+
Contains information and references required during
|
|
81
|
+
validation and pre-processing
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
references,
|
|
87
|
+
findings,
|
|
88
|
+
flags: PreProcessingFlag = PreProcessingFlag.NOFLAG,
|
|
89
|
+
):
|
|
90
|
+
self.references = references
|
|
91
|
+
self.findings = findings
|
|
92
|
+
self.flags = flags
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
NOFLAG = PreProcessingFlag.NOFLAG
|
|
96
|
+
SET_DEFAULTS = PreProcessingFlag.SET_DEFAULTS
|
|
97
|
+
CONVERT_DATA = PreProcessingFlag.CONVERT_DATA
|
|
98
|
+
UNDEFINED = object()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def number_to_str_number(x: any) -> any:
|
|
102
|
+
"""
|
|
103
|
+
Test if x is a str and represents an integer.
|
|
104
|
+
If yes return a string 'x', else return x
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(x, str):
|
|
107
|
+
try:
|
|
108
|
+
int(x)
|
|
109
|
+
x = f"'{x}'"
|
|
110
|
+
except ValueError:
|
|
111
|
+
try:
|
|
112
|
+
float(x)
|
|
113
|
+
x = f"'{x}'"
|
|
114
|
+
except ValueError:
|
|
115
|
+
pass
|
|
116
|
+
return x
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def merge_lists(left: list, right: list, list_merge: str = "append_rp") -> list:
|
|
120
|
+
"""
|
|
121
|
+
Merge elements of list 'right' into list 'left'
|
|
122
|
+
"""
|
|
123
|
+
if not isinstance(left, list):
|
|
124
|
+
raise ValueError("'left' is not a list")
|
|
125
|
+
if not isinstance(right, list):
|
|
126
|
+
raise ValueError("'right' is not a list")
|
|
127
|
+
_left = copy.deepcopy(left)
|
|
128
|
+
if list_merge == "replace":
|
|
129
|
+
return copy.deepcopy(right)
|
|
130
|
+
if list_merge == "append":
|
|
131
|
+
return _left + copy.deepcopy(right)
|
|
132
|
+
if list_merge == "prepend":
|
|
133
|
+
return copy.deepcopy(right) + _left
|
|
134
|
+
if list_merge in ["append_rp", "prepend_rp"]:
|
|
135
|
+
append = list_merge == "append_rp"
|
|
136
|
+
for element in right:
|
|
137
|
+
if element not in _left:
|
|
138
|
+
if append:
|
|
139
|
+
_left.append(copy.deepcopy(element))
|
|
140
|
+
else:
|
|
141
|
+
_left.insert(0, copy.deepcopy(element))
|
|
142
|
+
return _left
|
|
143
|
+
raise ValueError(f"'list_merge' option {list_merge} is not known")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def merge_dictionaries(
|
|
147
|
+
left: dict, right: dict, recursive: bool = True, list_merge: str = "append_rp"
|
|
148
|
+
) -> dict:
|
|
149
|
+
"""
|
|
150
|
+
Merge keys (recursively) from dictionary 'right' into dictionary 'left'.
|
|
151
|
+
"""
|
|
152
|
+
if not isinstance(left, dict):
|
|
153
|
+
raise ValueError("'left' is not a dictionary")
|
|
154
|
+
if not isinstance(right, dict):
|
|
155
|
+
raise ValueError("'right' is not a dictionary")
|
|
156
|
+
_left = copy.deepcopy(left)
|
|
157
|
+
for key, r_value in right.items():
|
|
158
|
+
if key not in _left or not isinstance(r_value, type(_left[key])):
|
|
159
|
+
# right element not in left or mismatching types => Just copy right to left
|
|
160
|
+
_left[key] = copy.deepcopy(r_value)
|
|
161
|
+
else:
|
|
162
|
+
# left value is same type as right value
|
|
163
|
+
if isinstance(r_value, list):
|
|
164
|
+
# Merge left list with right list
|
|
165
|
+
_left[key] = merge_lists(
|
|
166
|
+
left=_left[key], right=right[key], list_merge=list_merge
|
|
167
|
+
)
|
|
168
|
+
elif isinstance(r_value, dict):
|
|
169
|
+
if recursive:
|
|
170
|
+
# Merge left dict with right dict
|
|
171
|
+
_left[key] = merge_dictionaries(
|
|
172
|
+
left=_left[key],
|
|
173
|
+
right=right[key],
|
|
174
|
+
recursive=recursive,
|
|
175
|
+
list_merge=list_merge,
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
# Overwrite left with right value
|
|
179
|
+
_left[key] = copy.deepcopy(r_value)
|
|
180
|
+
else:
|
|
181
|
+
# Something like integer or string and just copy right to left
|
|
182
|
+
_left[key] = copy.deepcopy(r_value)
|
|
183
|
+
return _left
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def merge_template_data(
|
|
187
|
+
template_data: dict,
|
|
188
|
+
additional_data: dict,
|
|
189
|
+
recursive: bool = True,
|
|
190
|
+
list_merge: str = "append_rp",
|
|
191
|
+
) -> dict:
|
|
192
|
+
"""
|
|
193
|
+
Merge definition from 'additional' template into template.
|
|
194
|
+
"""
|
|
195
|
+
template_data = template_data.copy()
|
|
196
|
+
template_data_keys = set(list(template_data.keys()))
|
|
197
|
+
additional_data_keys = set(list(additional_data.keys()))
|
|
198
|
+
return_dict = template_data
|
|
199
|
+
for key in template_data_keys.intersection(additional_data_keys):
|
|
200
|
+
if isinstance(additional_data[key], list):
|
|
201
|
+
if isinstance(template_data[key], list):
|
|
202
|
+
return_dict[key] = merge_lists(
|
|
203
|
+
left=template_data[key],
|
|
204
|
+
right=additional_data[key],
|
|
205
|
+
list_merge=list_merge,
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
return_dict[key] = additional_data[key]
|
|
209
|
+
elif not isinstance(additional_data[key], dict) or not isinstance(
|
|
210
|
+
template_data[key], dict
|
|
211
|
+
):
|
|
212
|
+
return_dict[key] = additional_data[key]
|
|
213
|
+
else:
|
|
214
|
+
return_dict[key] = merge_dictionaries(
|
|
215
|
+
left=template_data[key],
|
|
216
|
+
right=additional_data[key],
|
|
217
|
+
recursive=recursive,
|
|
218
|
+
list_merge=list_merge,
|
|
219
|
+
)
|
|
220
|
+
for key in additional_data_keys - template_data_keys:
|
|
221
|
+
return_dict[key] = additional_data[key]
|
|
222
|
+
return return_dict
|
nadap/errors.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Execption classes
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# pylint: disable=too-few-public-methods
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SchemaDefinitionError(Exception):
|
|
9
|
+
"""
|
|
10
|
+
Error while parsing schema definition.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, msg: str, path: str = None):
|
|
14
|
+
if path is not None:
|
|
15
|
+
msg = f"{path}: {msg}"
|
|
16
|
+
super().__init__(msg)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DataValidationError(Exception):
|
|
20
|
+
"""
|
|
21
|
+
Exception indicating that data validation failed.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NadapReferenceError(Exception):
|
|
26
|
+
"""
|
|
27
|
+
Class to identify an error during reference processing
|
|
28
|
+
"""
|
nadap/mixin/accuracy.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Accuracy Mixin
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# pylint: disable=too-few-public-methods
|
|
6
|
+
|
|
7
|
+
import nadap.schema
|
|
8
|
+
from nadap.base import ValEnv
|
|
9
|
+
from nadap.errors import SchemaDefinitionError, DataValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AccuracyMixin:
|
|
13
|
+
"""
|
|
14
|
+
Add options for maximum_decimals
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, **kwargs):
|
|
18
|
+
self._maximum_decimals = None
|
|
19
|
+
self._round_to = None
|
|
20
|
+
super().__init__(**kwargs)
|
|
21
|
+
|
|
22
|
+
def _pop_options(self, definition: dict, schema_path: str):
|
|
23
|
+
self._maximum_decimals = definition.pop("maximum_decimals", None)
|
|
24
|
+
self._round_to = definition.pop("round_to_decimals", None)
|
|
25
|
+
super()._pop_options(definition=definition, schema_path=schema_path)
|
|
26
|
+
|
|
27
|
+
def _validate_options(self, schema_path: str):
|
|
28
|
+
super()._validate_options(schema_path=schema_path)
|
|
29
|
+
if self._maximum_decimals is not None:
|
|
30
|
+
_path = f"{schema_path}.maximum_decimals"
|
|
31
|
+
nadap.schema.is_int(self._maximum_decimals, _path)
|
|
32
|
+
if not 0 <= self._maximum_decimals <= 17:
|
|
33
|
+
raise SchemaDefinitionError("Must be >= 0 and <= 17", _path)
|
|
34
|
+
if self._round_to is not None:
|
|
35
|
+
_path = f"{schema_path}.round_to_decimals"
|
|
36
|
+
nadap.schema.is_int(self._round_to, _path)
|
|
37
|
+
if not 0 <= self._round_to <= 17:
|
|
38
|
+
raise SchemaDefinitionError("Must be >= 0 and <= 17", _path)
|
|
39
|
+
|
|
40
|
+
def _validate_data(
|
|
41
|
+
self,
|
|
42
|
+
data: any,
|
|
43
|
+
path: str,
|
|
44
|
+
env: "ValEnv" = None,
|
|
45
|
+
) -> "any":
|
|
46
|
+
data = super()._validate_data(data=data, path=path, env=env)
|
|
47
|
+
if self._maximum_decimals is not None and data != round(
|
|
48
|
+
data, self._maximum_decimals
|
|
49
|
+
):
|
|
50
|
+
env.findings.append(
|
|
51
|
+
nadap.results.ValidationFinding(
|
|
52
|
+
"Float value has too many decimals",
|
|
53
|
+
path,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
raise DataValidationError()
|
|
57
|
+
return data
|
|
58
|
+
|
|
59
|
+
def _convert_data(self, data: any) -> any:
|
|
60
|
+
if self._round_to is not None:
|
|
61
|
+
data = round(data, self._round_to)
|
|
62
|
+
return super()._convert_data(data)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def restrictions(self) -> "list[str]":
|
|
66
|
+
"""
|
|
67
|
+
Get all restrictions for valid data
|
|
68
|
+
"""
|
|
69
|
+
ret_list = super().restrictions
|
|
70
|
+
if self._maximum_decimals is not None:
|
|
71
|
+
ret_list.append(f"max allowed decimals: {self._maximum_decimals}")
|
|
72
|
+
return ret_list
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _doc_options_md_upper_part(cls) -> list[str]:
|
|
76
|
+
return super()._doc_options_md_upper_part() + [
|
|
77
|
+
"| **maximum_decimals** | `int` | | Must be >= 0 and <= 17 | "
|
|
78
|
+
+ "Value's accuracy must be lower or equal |",
|
|
79
|
+
"| **round_to_decimals** | `int` | | Must be >= 0 and <= 17 | "
|
|
80
|
+
+ "Convert value to this accuracy |",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def _doc_options_yaml_upper_part(cls) -> list[str]:
|
|
85
|
+
return super()._doc_options_yaml_upper_part() + [
|
|
86
|
+
"maximum_decimals: <int>",
|
|
87
|
+
"round_to_decimals: <int>",
|
|
88
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AllowedValueMixin
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# pylint: disable=too-few-public-methods
|
|
6
|
+
|
|
7
|
+
import nadap.schema
|
|
8
|
+
from nadap.base import ValEnv, number_to_str_number
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AllowedValueMixin:
|
|
12
|
+
"""
|
|
13
|
+
Add allowed value tests for data
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, **kwargs):
|
|
17
|
+
self.allowed_values = None
|
|
18
|
+
super().__init__(**kwargs)
|
|
19
|
+
|
|
20
|
+
def _validate_data(
|
|
21
|
+
self,
|
|
22
|
+
data: any,
|
|
23
|
+
path: str,
|
|
24
|
+
env: "ValEnv" = None,
|
|
25
|
+
) -> "any":
|
|
26
|
+
"""
|
|
27
|
+
Check if data matches allowed values
|
|
28
|
+
"""
|
|
29
|
+
data = super()._validate_data(data=data, path=path, env=env)
|
|
30
|
+
if self.allowed_values and data not in self.allowed_values:
|
|
31
|
+
self._create_finding_with_error(
|
|
32
|
+
msg="Data not within allowed values",
|
|
33
|
+
path=path,
|
|
34
|
+
env=env,
|
|
35
|
+
)
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
def _validate_options(self, schema_path: str):
|
|
39
|
+
super()._validate_options(schema_path=schema_path)
|
|
40
|
+
if self.allowed_values is not None:
|
|
41
|
+
nadap.schema.is_non_empty_list(
|
|
42
|
+
self.allowed_values, f"{schema_path}.allowed_values"
|
|
43
|
+
)
|
|
44
|
+
for index, v in enumerate(self.allowed_values):
|
|
45
|
+
self._test_data_type(
|
|
46
|
+
data=v, path=f"{schema_path}.allowed_values[{index}]"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _pop_options(self, definition: dict, schema_path: str):
|
|
50
|
+
self.allowed_values = definition.pop("allowed_values", None)
|
|
51
|
+
super()._pop_options(definition=definition, schema_path=schema_path)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def restrictions(self) -> "list[str]":
|
|
55
|
+
"""
|
|
56
|
+
Get all restrictions for valid data
|
|
57
|
+
"""
|
|
58
|
+
ret_list = super().restrictions
|
|
59
|
+
if self.allowed_values:
|
|
60
|
+
ret_list.append("allowed values:")
|
|
61
|
+
ret_list.extend(
|
|
62
|
+
[f" - {str(number_to_str_number(x))}" for x in self.allowed_values]
|
|
63
|
+
)
|
|
64
|
+
return ret_list
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def _doc_options_md_upper_part(cls) -> list[str]:
|
|
68
|
+
return super()._doc_options_md_upper_part() + [
|
|
69
|
+
"| **allowed_values** | `list` | | | "
|
|
70
|
+
+ " Data must match one of these values |",
|
|
71
|
+
"| - <value> | `any` | | Must match data type's type(s) | |",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _doc_options_yaml_upper_part(cls) -> list[str]:
|
|
76
|
+
return super()._doc_options_yaml_upper_part() + [
|
|
77
|
+
"allowed_values:",
|
|
78
|
+
" - <any>",
|
|
79
|
+
]
|
nadap/mixin/ip.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mixin for IP data types
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# pylint: disable=too-few-public-methods
|
|
6
|
+
|
|
7
|
+
import ipaddress
|
|
8
|
+
|
|
9
|
+
from nadap.base import ValEnv
|
|
10
|
+
from nadap.errors import SchemaDefinitionError
|
|
11
|
+
import nadap.mixin.allowed_value
|
|
12
|
+
import nadap.mixin.not_allowed_value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IpMixin(
|
|
16
|
+
nadap.mixin.not_allowed_value.NotAllowedValueMixin,
|
|
17
|
+
nadap.mixin.allowed_value.AllowedValueMixin,
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Add handling of strings representing IPv4 or IPv6 string
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_cls_python_classes = [str]
|
|
24
|
+
_doc_data_type = "str"
|
|
25
|
+
_ip_data_type_name = "IP"
|
|
26
|
+
|
|
27
|
+
def __init__(self, **kwargs):
|
|
28
|
+
self._test_loopback = None
|
|
29
|
+
self._test_link_local = None
|
|
30
|
+
self._test_multicast = None
|
|
31
|
+
self._test_private = None
|
|
32
|
+
self._test_public = None
|
|
33
|
+
super().__init__(**kwargs)
|
|
34
|
+
|
|
35
|
+
def _get_ip_obj(self, data):
|
|
36
|
+
return ipaddress.ip_address(data)
|
|
37
|
+
|
|
38
|
+
def _test_data_type(self, data: any, path: str, env: "ValEnv" = None) -> "any":
|
|
39
|
+
# pylint: disable=raise-missing-from
|
|
40
|
+
data = super()._test_data_type(data=data, path=path, env=env)
|
|
41
|
+
try:
|
|
42
|
+
self._get_ip_obj(data)
|
|
43
|
+
except ValueError:
|
|
44
|
+
msg = f"String does not represent an {self._ip_data_type_name}"
|
|
45
|
+
if env is None:
|
|
46
|
+
raise SchemaDefinitionError(
|
|
47
|
+
msg=msg,
|
|
48
|
+
path=path,
|
|
49
|
+
)
|
|
50
|
+
self._create_finding_with_error(msg=msg, path=path, env=env)
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
def _pop_options(self, definition: dict, schema_path: str):
|
|
54
|
+
self._test_loopback = definition.pop("loopback", None)
|
|
55
|
+
self._test_link_local = definition.pop("link_local", None)
|
|
56
|
+
self._test_private = definition.pop("private", None)
|
|
57
|
+
self._test_public = definition.pop("public", None)
|
|
58
|
+
self._test_multicast = definition.pop("multicast", None)
|
|
59
|
+
super()._pop_options(definition=definition, schema_path=schema_path)
|
|
60
|
+
|
|
61
|
+
def _validate_options(self, schema_path: str):
|
|
62
|
+
super()._validate_options(schema_path=schema_path)
|
|
63
|
+
if (
|
|
64
|
+
self._test_link_local is not None
|
|
65
|
+
and self._test_loopback is not None
|
|
66
|
+
and self._test_link_local == self._test_loopback
|
|
67
|
+
):
|
|
68
|
+
raise SchemaDefinitionError(
|
|
69
|
+
"Mismatching test for link_local and loopback", schema_path
|
|
70
|
+
)
|
|
71
|
+
if (
|
|
72
|
+
self._test_link_local is not None
|
|
73
|
+
and self._test_multicast is not None
|
|
74
|
+
and self._test_link_local == self._test_multicast
|
|
75
|
+
):
|
|
76
|
+
raise SchemaDefinitionError(
|
|
77
|
+
"Mismatching test for link_local and multicast", schema_path
|
|
78
|
+
)
|
|
79
|
+
if (
|
|
80
|
+
self._test_loopback is not None
|
|
81
|
+
and self._test_multicast is not None
|
|
82
|
+
and self._test_loopback == self._test_multicast
|
|
83
|
+
):
|
|
84
|
+
raise SchemaDefinitionError(
|
|
85
|
+
"Mismatching test for loopback and multicast", schema_path
|
|
86
|
+
)
|
|
87
|
+
if (
|
|
88
|
+
self._test_private is not None
|
|
89
|
+
and self._test_public is not None
|
|
90
|
+
and self._test_private == self._test_public
|
|
91
|
+
):
|
|
92
|
+
raise SchemaDefinitionError(
|
|
93
|
+
"Mismatching test for private and public", schema_path
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _validate_data(
|
|
97
|
+
self,
|
|
98
|
+
data: any,
|
|
99
|
+
path: str,
|
|
100
|
+
env: "ValEnv" = None,
|
|
101
|
+
) -> "any":
|
|
102
|
+
"""
|
|
103
|
+
Check if data matches IP options
|
|
104
|
+
"""
|
|
105
|
+
data = super()._validate_data(data=data, path=path, env=env)
|
|
106
|
+
ip_obj = self._get_ip_obj(data)
|
|
107
|
+
msg = None
|
|
108
|
+
if (
|
|
109
|
+
self._test_link_local is not None
|
|
110
|
+
and self._test_link_local != ip_obj.is_link_local
|
|
111
|
+
):
|
|
112
|
+
msg = "Is link-local" if ip_obj.is_link_local else "Is not link-local"
|
|
113
|
+
elif (
|
|
114
|
+
self._test_loopback is not None
|
|
115
|
+
and self._test_loopback != ip_obj.is_loopback
|
|
116
|
+
):
|
|
117
|
+
msg = "Is loopback" if ip_obj.is_loopback else "Is not loopback"
|
|
118
|
+
elif (
|
|
119
|
+
self._test_multicast is not None
|
|
120
|
+
and self._test_multicast != ip_obj.is_multicast
|
|
121
|
+
):
|
|
122
|
+
msg = "Is multicast" if ip_obj.is_multicast else "Is not multicast"
|
|
123
|
+
elif self._test_private is not None and self._test_private != ip_obj.is_private:
|
|
124
|
+
msg = "Is private" if ip_obj.is_private else "Is not private"
|
|
125
|
+
elif self._test_public is not None and self._test_public != ip_obj.is_global:
|
|
126
|
+
msg = "Is public" if ip_obj.is_global else "Is not public"
|
|
127
|
+
if msg:
|
|
128
|
+
self._create_finding_with_error(msg=msg, path=path, env=env)
|
|
129
|
+
|
|
130
|
+
return data
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def restrictions(self) -> "list[str]":
|
|
134
|
+
"""
|
|
135
|
+
Get all restrictions for valid data
|
|
136
|
+
"""
|
|
137
|
+
ret_list = super().restrictions
|
|
138
|
+
opt_list = []
|
|
139
|
+
if self._test_link_local is not None:
|
|
140
|
+
opt_list.append(
|
|
141
|
+
f"{self._markdown_indent}- link-local: {self._test_link_local}"
|
|
142
|
+
)
|
|
143
|
+
if self._test_loopback is not None:
|
|
144
|
+
opt_list.append(f"{self._markdown_indent}- loopback: {self._test_loopback}")
|
|
145
|
+
if self._test_multicast is not None:
|
|
146
|
+
opt_list.append(
|
|
147
|
+
f"{self._markdown_indent}- multicast: {self._test_multicast}"
|
|
148
|
+
)
|
|
149
|
+
if self._test_private is not None:
|
|
150
|
+
opt_list.append(f"{self._markdown_indent}- private: {self._test_private}")
|
|
151
|
+
if self._test_public is not None:
|
|
152
|
+
opt_list.append(f"{self._markdown_indent}- public: {self._test_public}")
|
|
153
|
+
if opt_list:
|
|
154
|
+
ret_list.append("IP restrictions:")
|
|
155
|
+
ret_list.extend(opt_list)
|
|
156
|
+
return ret_list
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def _doc_options_md_upper_part(cls) -> list[str]:
|
|
160
|
+
return super()._doc_options_md_upper_part() + [
|
|
161
|
+
"| **link_local** | `bool` | | | IP must be link-local |",
|
|
162
|
+
"| **loopback** | `bool` | | | IP must be loopback |",
|
|
163
|
+
"| **multicast** | `bool` | | | IP must be multicast |",
|
|
164
|
+
"| **private** | `bool` | | | IP must be private |",
|
|
165
|
+
"| **public** | `bool` | | | IP must be public/global |",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def _doc_options_yaml_upper_part(cls) -> list[str]:
|
|
170
|
+
return super()._doc_options_yaml_upper_part() + [
|
|
171
|
+
"",
|
|
172
|
+
"link_local: <true|false",
|
|
173
|
+
"loopback: <true|false",
|
|
174
|
+
"multicast: <true|false",
|
|
175
|
+
"private: <true|false",
|
|
176
|
+
"public: <true|false",
|
|
177
|
+
]
|