Mercurial > hgrepos > Python > libs > data-schema
comparison data_schema/__init__.py @ 5:84dfd1a94926
Add the existing implementation.
All tests work.
The documentation as text file is included also.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Thu, 06 Jul 2023 23:41:41 +0200 |
| parents | |
| children | f4e1b6d6fe63 |
comparison
equal
deleted
inserted
replaced
| 4:d715f0c13c60 | 5:84dfd1a94926 |
|---|---|
| 1 # -*- coding: utf-8 -*- | |
| 2 r""" | |
| 3 Object schema validation support. | |
| 4 | |
| 5 Somewhat modelled after JSON schema. | |
| 6 | |
| 7 .. seealso:: https://json-schema.org/understanding-json-schema/index.html | |
| 8 | |
| 9 :Author: Franz Glasner <fzglas.hg@dom66.de> | |
| 10 :Copyright: \(c) 2023 Franz Glasner | |
| 11 :License: BSD 3-Clause "New" or "Revised" License. | |
| 12 See :ref:`LICENSE.txt <license>` for details. | |
| 13 :ID: @(#) $Header$ | |
| 14 | |
| 15 """ | |
| 16 | |
| 17 __version__ = "0.1.dev1" | |
| 18 | |
| 19 __revision__ = "|VCSRevision|" | |
| 20 | |
| 21 __date__ = "|VCSJustDate|" | |
| 22 | |
| 23 __all__ = ["ERROR", "WARNING", "INFO", "ERRORS", "WARNINGS", | |
| 24 "level_name", "problem_message", | |
| 25 "ValidationProblem", "SchemaError", | |
| 26 "validate", | |
| 27 "log_problem_cause"] | |
| 28 | |
| 29 | |
| 30 import ast | |
| 31 import collections | |
| 32 import copy | |
| 33 import datetime | |
| 34 import re | |
| 35 import urllib.parse | |
| 36 | |
| 37 import rfc3986 | |
| 38 | |
| 39 import configmix.yaml | |
| 40 | |
| 41 from .util import get_data_stream | |
| 42 | |
| 43 | |
| 44 def NC_(ctx, msg): | |
| 45 """Mimimum dummy translation support""" | |
| 46 return msg | |
| 47 | |
| 48 | |
| 49 ERROR = 40 | |
| 50 WARNING = 30 | |
| 51 INFO = 20 | |
| 52 | |
| 53 _level_to_name = { | |
| 54 ERROR: "ERROR", | |
| 55 WARNING: "WARNING", | |
| 56 INFO: "INFO", | |
| 57 } | |
| 58 _name_to_level = {name: level for (level, name) in _level_to_name.items()} | |
| 59 | |
| 60 ERRORS = { | |
| 61 10000: NC_("schema-msg", "dict expected"), | |
| 62 10001: NC_("schema-msg", "list expected"), | |
| 63 10002: NC_("schema-msg", "string expected"), | |
| 64 10003: NC_("schema-msg", "dict key must be a string"), | |
| 65 10004: NC_("schema-msg", "additional key encountered"), | |
| 66 10005: NC_("schema-msg", "required key(s) missing"), | |
| 67 10006: NC_("schema-msg", "min string length encountered"), | |
| 68 10007: NC_("schema-msg", "max string length exceeded"), | |
| 69 10008: NC_("schema-msg", "string value does not match the required RE pattern"), | |
| 70 10009: NC_("schema-msg", "string value does not validate"), | |
| 71 10010: NC_("schema-msg", "validation error"), | |
| 72 10011: NC_("schema-msg", "None/Null object expected"), | |
| 73 10012: NC_("schema-msg", "min list length encountered"), | |
| 74 10013: NC_("schema-msg", "max list length exceeded"), | |
| 75 10014: NC_("schema-msg", "tuple expected"), | |
| 76 10015: NC_("schema-msg", "min tuple length encountered"), | |
| 77 10016: NC_("schema-msg", "max tuple length exceeded"), | |
| 78 10017: NC_("schema-msg", "additional items in tuple not allowed"), | |
| 79 10018: NC_("schema-msg", "object is not empty"), | |
| 80 10019: NC_("schema-msg", "more than one match in `one-of' detected"), | |
| 81 10020: NC_("schema-msg", "int expected"), | |
| 82 10021: NC_("schema-msg", "int value lower than minValue"), | |
| 83 10022: NC_("schema-msg", "int value greater than maxValue"), | |
| 84 10023: NC_("schema-msg", "float expected"), | |
| 85 10024: NC_("schema-msg", "float value lower than minValue"), | |
| 86 10025: NC_("schema-msg", "float value greater than maxValue"), | |
| 87 10026: NC_("schema-msg", "boolean value expected"), | |
| 88 10027: NC_("schema-msg", "boolean true expected"), | |
| 89 10028: NC_("schema-msg", "boolean false expected"), | |
| 90 10029: NC_("schema-msg", "`not' expected problems but got none"), | |
| 91 10030: NC_("schema-msg", "numeric type (int or float) expected"), | |
| 92 10031: NC_("schema-msg", "numeric value lower than minValue"), | |
| 93 10032: NC_("schema-msg", "numeric value greater than maxValue"), | |
| 94 10033: NC_("schema-msg", "a plain scalar value expected"), | |
| 95 10034: NC_("schema-msg", "dict key does not match required schema"), | |
| 96 10035: NC_("schema-msg", "binary data expected"), | |
| 97 10036: NC_("schema-msg", "length of binary data lower than minValue"), | |
| 98 10037: NC_("schema-msg", "length of binary data exceeds maxValue"), | |
| 99 10038: NC_("schema-msg", "a set is expected"), | |
| 100 10039: NC_("schema-msg", "length of set lower than minLength"), | |
| 101 10040: NC_("schema-msg", "length of set greater than maxLength"), | |
| 102 10041: NC_("schema-msg", "timestamp expected"), | |
| 103 10042: NC_("schema-msg", "value of timestamp does not validate"), | |
| 104 10043: NC_("schema-msg", "enumerated string value expected but not found"), | |
| 105 10044: NC_("schema-msg", "referenced object doest not exist"), | |
| 106 10045: NC_("schema-msg", "key is not contained in referenced object"), | |
| 107 10046: NC_("schema-msg", "referenced object is not a container"), | |
| 108 10047: NC_("schema-msg", "binary data does not match the required RE pattern"), | |
| 109 10048: NC_("schema-msg", "enumerated integer value expected but not found"), | |
| 110 10049: NC_("schema-msg", "enumerated number value expected but not found"), | |
| 111 10050: NC_("schema-msg", "min dict length encountered"), | |
| 112 10051: NC_("schema-msg", "max dict length exceeded"), | |
| 113 10052: NC_("schema-msg", "index constraint violated"), | |
| 114 10053: NC_("schema-msg", "`one-of' failed"), | |
| 115 10054: NC_("schema-msg", "failing `one-of' item"), | |
| 116 10055: NC_("schema-msg", "`any-of' failed"), | |
| 117 10056: NC_("schema-msg", "failing `any-of' item"), | |
| 118 10057: NC_("schema-msg", "`all-of' failed"), | |
| 119 10058: NC_("schema-msg", "failing `all-of' item"), | |
| 120 } | |
| 121 | |
| 122 WARNINGS = { | |
| 123 80000: NC_("schema-msg", "duplicate dict key"), | |
| 124 } | |
| 125 | |
| 126 if not set(ERRORS.keys()).isdisjoint(set(WARNINGS.keys())): | |
| 127 raise ValueError("ERRORS and WARNINGS must be disjoint") | |
| 128 | |
| 129 | |
| 130 TYPE_RE = type(re.compile(r"\A.+\Z")) | |
| 131 | |
| 132 _SENTINEL = object() | |
| 133 | |
| 134 SCHEMA_REF_KEY = "$ref" | |
| 135 """Key name for schema references (like a symlink within a schema)""" | |
| 136 | |
| 137 SCHEMA_PATH_ROOT = "$root" | |
| 138 """URI path to the root schema""" | |
| 139 | |
| 140 SCHEMA_PATH_SELF = "$self" | |
| 141 """URI path to the current schema""" | |
| 142 | |
| 143 | |
| 144 def level_name(level): | |
| 145 name = _level_to_name.get(level) | |
| 146 if name is None: | |
| 147 name = "Level {}".format(level) | |
| 148 return name | |
| 149 | |
| 150 | |
| 151 def problem_message(pr): | |
| 152 if isinstance(pr, ValidationProblem): | |
| 153 code = getattr(pr, "code", None) | |
| 154 else: | |
| 155 code = pr | |
| 156 msg = ERRORS.get(code, None) | |
| 157 if msg is None: | |
| 158 msg = WARNINGS[code] | |
| 159 return msg | |
| 160 | |
| 161 | |
| 162 class ValidationProblem(object): | |
| 163 | |
| 164 __slots__ = ("code", "severity", "hint", "context", "cause", "index") | |
| 165 | |
| 166 def __init__(self, | |
| 167 code=None, | |
| 168 severity=None, | |
| 169 hint=None, | |
| 170 context=None, | |
| 171 cause=None, | |
| 172 index=None): | |
| 173 if code is not None: | |
| 174 # check validity | |
| 175 if code not in ERRORS and code not in WARNINGS: | |
| 176 raise ValueError( | |
| 177 "unknown validation error code: {}".format(code)) | |
| 178 self.code = code | |
| 179 if severity is None: | |
| 180 # autodetermine | |
| 181 if code in ERRORS: | |
| 182 self.severity = ERROR | |
| 183 elif code in WARNINGS: | |
| 184 self.severity = WARNING | |
| 185 else: | |
| 186 assert False | |
| 187 else: | |
| 188 self.severity = severity | |
| 189 else: | |
| 190 raise TypeError("`code' must be given") | |
| 191 self.hint = hint | |
| 192 self.context = context | |
| 193 if cause: | |
| 194 if not isinstance(cause, (list, tuple, set, frozenset)): | |
| 195 cause = (cause, ) | |
| 196 for c in cause: | |
| 197 if not isinstance(c, ValidationProblem): | |
| 198 raise SchemaError( | |
| 199 "can only nest other `ValidationProblem' instances") | |
| 200 self.cause = cause | |
| 201 self.index = index | |
| 202 | |
| 203 def __repr__(self): | |
| 204 try: | |
| 205 msg = " (" + problem_message(self) + ")" | |
| 206 except LookupError: | |
| 207 msg = "" | |
| 208 if self.index is None: | |
| 209 return "ValidationProblem(code={!r}{}, severity={!r}, hint={}, context=[depth={}]{})".format( | |
| 210 self.code, msg, self.severity, self.hint, self.context.depth, self.context) | |
| 211 else: | |
| 212 return "ValidationProblem(code={!r}{}, severity={!r}, hint={}, context=[depth={}]{}, index={})".format( | |
| 213 self.code, msg, self.severity, self.hint, self.context.depth, self.context, self.index) | |
| 214 | |
| 215 | |
| 216 class SchemaError(Exception): | |
| 217 """An error within the schema itself""" | |
| 218 pass | |
| 219 | |
| 220 | |
| 221 ValidationSettings = collections.namedtuple( | |
| 222 "ValidationSettings", | |
| 223 ["skip_keys", "break_on_keynames_problems"]) | |
| 224 | |
| 225 | |
| 226 class _Schema(dict): | |
| 227 | |
| 228 __slots__ = ("parent", "is_sub_root", "_schema_cache") | |
| 229 | |
| 230 def __init__(self, parent, is_sub_root, *args, **kwds): | |
| 231 super().__init__(*args, **kwds) | |
| 232 if parent is None or isinstance(parent, _Schema): | |
| 233 self.parent = parent | |
| 234 else: | |
| 235 raise TypeError("`_Schema' or `None' expected") | |
| 236 if parent is None: | |
| 237 self._schema_cache = {} | |
| 238 if not is_sub_root: | |
| 239 raise ValueError( | |
| 240 "the root schmema must be a sub-root (aka `$self') also") | |
| 241 self.is_sub_root = True | |
| 242 else: | |
| 243 self.is_sub_root = is_sub_root | |
| 244 | |
| 245 @property | |
| 246 def ROOT(self): | |
| 247 """Get the root schema""" | |
| 248 r = self | |
| 249 while r.parent is not None: | |
| 250 r = r.parent | |
| 251 return r | |
| 252 | |
| 253 @property | |
| 254 def SELF(self): | |
| 255 r = self | |
| 256 while not r.is_sub_root: | |
| 257 r = r.parent | |
| 258 return r | |
| 259 | |
| 260 def copy(self): | |
| 261 return _Schema(self.parent, self.is_sub_root, self) | |
| 262 | |
| 263 def get_child(self, name, default=None): | |
| 264 return self.ensure_child_schema(self.get(name, default)) | |
| 265 | |
| 266 def ensure_child_schema(self, v): | |
| 267 if v is None: | |
| 268 return None | |
| 269 elif isinstance(v, _Schema): | |
| 270 return v | |
| 271 elif isinstance(v, dict): | |
| 272 return _Schema(self, False, v) | |
| 273 else: | |
| 274 return v | |
| 275 | |
| 276 def ensure_list_of_child_schemata(self, v): | |
| 277 if isinstance(v, (list, tuple)): | |
| 278 return [_Schema(self, False, i) for i in v] | |
| 279 else: | |
| 280 return v | |
| 281 | |
| 282 def __eq__(self, other): | |
| 283 if not isinstance(other, _Schema): | |
| 284 return NotImplemented | |
| 285 return (self.parent is other.parent | |
| 286 and bool(self.is_sub_root) == bool(other.is_sub_root) | |
| 287 and dict(self) == dict(other)) | |
| 288 | |
| 289 def __ne__(self, other): | |
| 290 # | |
| 291 # While the default in Python3 is sensible implementing is recommended | |
| 292 # when a built-in __eq__ is overwritten (Raymond Hettinger). | |
| 293 # | |
| 294 # Do not use not self == other because NotImplemented is not handled | |
| 295 # properly in some early Python versions (including Py2). | |
| 296 # | |
| 297 equal = self.__eq__(other) | |
| 298 return NotImplemented if equal is NotImplemented else not equal | |
| 299 | |
| 300 def __copy__(self): | |
| 301 return _Schema(self.parent, self.is_sub_root, self) | |
| 302 | |
| 303 def __deepcopy__(self, memo): | |
| 304 return _Schema(self.parent, | |
| 305 self.is_sub_root, | |
| 306 copy.deepcopy(dict(self), memo)) | |
| 307 | |
| 308 def __str__(self): | |
| 309 return "<_Schema " + super().__str__() + ">" | |
| 310 | |
| 311 def __repr__(self): | |
| 312 return "<_Schema " + super().__repr__() + ">" | |
| 313 | |
| 314 def get_cached_schema(self, key, load_if_needed=True): | |
| 315 root = self.ROOT | |
| 316 s = root._schema_cache.get(key, None) | |
| 317 if s is None and load_if_needed: | |
| 318 with get_data_stream(key) as schemastream: | |
| 319 # load schema a new `$self' (i.e. sub-root is True) | |
| 320 s = _Schema(self, True, configmix.yaml.load(schemastream)) | |
| 321 root._schema_cache[key] = s | |
| 322 return s | |
| 323 | |
| 324 def add_cached_schema(self, key, schema): | |
| 325 r = self.ROOT | |
| 326 assert isinstance(schema, _Schema) | |
| 327 r._schema_cache[key] = schema | |
| 328 | |
| 329 | |
| 330 class Context(object): | |
| 331 | |
| 332 __slots__ = ("_parent", "_key", "_key_index", | |
| 333 "_index", | |
| 334 "root_object", "root_schema", | |
| 335 "_current_object", | |
| 336 "_settings") | |
| 337 | |
| 338 def __init__(self, parent, *, key=_SENTINEL, index=_SENTINEL, | |
| 339 root_object=_SENTINEL, root_schema=_SENTINEL, | |
| 340 current_object=_SENTINEL, | |
| 341 settings=_SENTINEL, | |
| 342 key_index=_SENTINEL): | |
| 343 if parent is None: | |
| 344 if key is not _SENTINEL: | |
| 345 raise TypeError("the root context may not have a key") | |
| 346 if index is not _SENTINEL: | |
| 347 raise TypeError("the root context may not have an index") | |
| 348 if settings is _SENTINEL: | |
| 349 raise TypeError("the root context must have settings") | |
| 350 self.root_object = root_object | |
| 351 if current_object is _SENTINEL: | |
| 352 current_object = root_object | |
| 353 self.root_schema = root_schema | |
| 354 else: | |
| 355 if key is _SENTINEL and index is _SENTINEL: | |
| 356 raise TypeError("one of `key` and `index` must be given in a non-root context") | |
| 357 if root_object is not _SENTINEL: | |
| 358 raise TypeError("non-root context may not have a root object") | |
| 359 if root_schema is not _SENTINEL: | |
| 360 raise TypeError("non-root context may not have a root schema") | |
| 361 if key is not _SENTINEL and index is not _SENTINEL: | |
| 362 raise ValueError("only one of `key` and `index` may be given in a context") | |
| 363 if key_index is not _SENTINEL and key is _SENTINEL: | |
| 364 raise ValueError("when having a `key_index` a `key` also must be given") | |
| 365 self._parent = parent | |
| 366 self._key = key | |
| 367 self._key_index = key_index | |
| 368 self._index = index | |
| 369 self._current_object = current_object | |
| 370 self._settings = settings | |
| 371 | |
| 372 @property | |
| 373 def parent(self): | |
| 374 return self._parent | |
| 375 | |
| 376 @property | |
| 377 def safe_parent(self): | |
| 378 if self.is_root: | |
| 379 raise TypeError("the root context has no parent") | |
| 380 return self.parent | |
| 381 | |
| 382 @property | |
| 383 def root(self): | |
| 384 """Get the root context""" | |
| 385 ctx = self | |
| 386 while not ctx.is_root: | |
| 387 ctx = ctx.parent | |
| 388 return ctx | |
| 389 | |
| 390 @property | |
| 391 def is_root(self): | |
| 392 return not bool(self.parent) | |
| 393 | |
| 394 @property | |
| 395 def key(self): | |
| 396 if self._key is _SENTINEL: | |
| 397 raise AttributeError("no `key' in Context") | |
| 398 return self._key | |
| 399 | |
| 400 @property | |
| 401 def index(self): | |
| 402 if self._index is _SENTINEL: | |
| 403 raise AttributeError("no `index' in Context") | |
| 404 return self._index | |
| 405 | |
| 406 @property | |
| 407 def key_index(self): | |
| 408 if self._key_index is _SENTINEL: | |
| 409 raise AttributeError("no `key_index' in Context") | |
| 410 return self._key_index | |
| 411 | |
| 412 @property | |
| 413 def current_object(self): | |
| 414 if self._current_object is _SENTINEL: | |
| 415 raise AttributeError("no `current_object' in Context") | |
| 416 return self._current_object | |
| 417 | |
| 418 @property | |
| 419 def settings(self): | |
| 420 s = self._settings | |
| 421 return s if s is not _SENTINEL else self.parent.settings | |
| 422 | |
| 423 @property | |
| 424 def depth(self): | |
| 425 if self._key is _SENTINEL and self._index is _SENTINEL and self.is_root: | |
| 426 return 0 | |
| 427 n = 0 | |
| 428 ctx = self | |
| 429 while not ctx.is_root: | |
| 430 n += 1 | |
| 431 ctx = ctx.parent | |
| 432 return n | |
| 433 | |
| 434 def __str__(self): | |
| 435 if self._key is _SENTINEL and self._index is _SENTINEL and self.is_root: | |
| 436 return "<ROOT>" | |
| 437 chain = [] | |
| 438 ctx = self | |
| 439 while not ctx.is_root: | |
| 440 if ctx._key is not _SENTINEL: | |
| 441 chain.append(str(ctx.key)) | |
| 442 elif ctx._index is not _SENTINEL: | |
| 443 chain.append("INDEX:{}".format(ctx.index)) | |
| 444 else: | |
| 445 chain.append("") | |
| 446 ctx = ctx.parent | |
| 447 chain.reverse() | |
| 448 return " / ".join(chain) | |
| 449 | |
| 450 def __repr__(self): | |
| 451 return "<Context path=`{}'>".format(str(self)) | |
| 452 | |
| 453 | |
| 454 def _get_one_of(d, *keys, default=None, strict=True): | |
| 455 """Get the first found key and its value of `keys` from dict `d`. | |
| 456 | |
| 457 """ | |
| 458 for k in keys: | |
| 459 v = d.get(k, _SENTINEL) | |
| 460 if v is not _SENTINEL: | |
| 461 if strict: | |
| 462 # | |
| 463 # check that all no other key of `keys` besides of `k` is | |
| 464 # in `d` | |
| 465 # | |
| 466 other_keys = set(keys) | |
| 467 other_keys.remove(k) | |
| 468 for k2 in other_keys: | |
| 469 if k2 in d: | |
| 470 raise SchemaError("ambiguous key from: {}".format( | |
| 471 ", ".join(keys))) | |
| 472 return k, v | |
| 473 return None, default | |
| 474 | |
| 475 | |
| 476 def validate(obj, schema, **kwds): | |
| 477 """Validate object `obj` against the *specific* schema `schema`. | |
| 478 | |
| 479 Yields errors and warnings | |
| 480 | |
| 481 """ | |
| 482 settings = { | |
| 483 "skip_keys": None, | |
| 484 "break_on_keynames_problems": True, | |
| 485 } | |
| 486 settings.update(kwds) | |
| 487 if not isinstance(schema, _Schema): | |
| 488 if not isinstance(schema, dict): | |
| 489 raise SchemaError("Schema must be a dict-alike." | |
| 490 " Got: {!r}".format(schema)) | |
| 491 schema = _Schema(None, True, schema) | |
| 492 context = Context(None, root_object=obj, root_schema=schema, | |
| 493 settings=ValidationSettings(**settings)) | |
| 494 yield from _validate(obj, schema, context, is_root=True) | |
| 495 | |
| 496 | |
| 497 def _validate(obj, schema, context, is_root=False): | |
| 498 """Validate object `obj` against the *specific* schema `schema`. | |
| 499 | |
| 500 Yields errors and warnings | |
| 501 | |
| 502 """ | |
| 503 if not isinstance(schema, _Schema): | |
| 504 raise SchemaError("Schema must be a `_Schema'." | |
| 505 " Got: {!r}. Context: {!s}".format(schema, context)) | |
| 506 # 1. Process "cond" or "match" | |
| 507 schema = process_schema_conditionals(schema, context) | |
| 508 # 2. Process "$ref" schema references | |
| 509 schema = process_schema_references( | |
| 510 schema, context, check_single_ref_key=not is_root) | |
| 511 | |
| 512 # 3. Real validation | |
| 513 | |
| 514 # check combinator shortcuts without "type" indirection | |
| 515 combinator, combinator_schema = _get_one_of( | |
| 516 schema, "not", "all-of", "any-of", "one-of") | |
| 517 if combinator is None: | |
| 518 try: | |
| 519 t = schema["type"] | |
| 520 except KeyError: | |
| 521 raise SchemaError("Schema has no `type' key: {!r}." | |
| 522 " Context: {!s}".format(schema, context)) | |
| 523 else: | |
| 524 # | |
| 525 # Construct a temporary schema with the proper indirection for | |
| 526 # the check below | |
| 527 # | |
| 528 t = {"type": {combinator: combinator_schema}} | |
| 529 if combinator_schema is None: | |
| 530 raise SchemaError("a combinator requires a child") | |
| 531 if callable(t): | |
| 532 yield from t(obj, schema, context) | |
| 533 elif t is None: | |
| 534 yield from validate_null(obj, schema, context) | |
| 535 elif isinstance(t, dict): | |
| 536 if len(t) != 1: | |
| 537 raise SchemaError("type dict must be of length 1") | |
| 538 # Check whether a shortcut is already seen above | |
| 539 if combinator is None: | |
| 540 combinator = list(t.keys())[0] | |
| 541 combinator_schema = t[combinator] | |
| 542 if combinator == "not": | |
| 543 yield from validate_not( | |
| 544 obj, schema.ensure_child_schema(combinator_schema), context) | |
| 545 elif combinator == "all-of": | |
| 546 yield from validate_allOf( | |
| 547 obj, | |
| 548 schema.ensure_list_of_child_schemata(combinator_schema), | |
| 549 context) | |
| 550 elif combinator == "any-of": | |
| 551 yield from validate_anyOf( | |
| 552 obj, | |
| 553 schema.ensure_list_of_child_schemata(combinator_schema), | |
| 554 context) | |
| 555 elif combinator == "one-of": | |
| 556 yield from validate_oneOf( | |
| 557 obj, | |
| 558 schema.ensure_list_of_child_schemata(combinator_schema), | |
| 559 context) | |
| 560 else: | |
| 561 raise SchemaError("unknown combinator: {}".format(combinator)) | |
| 562 elif isinstance(t, (list, tuple)): | |
| 563 # a simple list is "any-of" | |
| 564 yield from validate_anyOf( | |
| 565 obj, schema.ensure_list_of_child_schemata(t), context) | |
| 566 elif t in ("dict", "map", "object"): | |
| 567 yield from validate_dict(obj, schema, context) | |
| 568 elif t in ("list", "array",): | |
| 569 yield from validate_list(obj, schema, context) | |
| 570 elif t in ("tuple", "record"): | |
| 571 yield from validate_tuple(obj, schema, context) | |
| 572 elif t in ("set", "frozenset"): | |
| 573 yield from validate_set(obj, schema, context) | |
| 574 elif t in ("string", "str"): | |
| 575 yield from validate_str(obj, schema, context) | |
| 576 elif t in ("deny", ): | |
| 577 yield from validate_deny(obj, schema, context) | |
| 578 elif t in ("accept", ): | |
| 579 yield from validate_accept(obj, schema, context) | |
| 580 elif t in ("none", "null", "nil"): | |
| 581 yield from validate_null(obj, schema, context) | |
| 582 elif t in ("empty", ): | |
| 583 yield from validate_empty(obj, schema, context) | |
| 584 elif t in ("integer", "int"): | |
| 585 yield from validate_integer(obj, schema, context) | |
| 586 elif t in ("float", "real", "double"): | |
| 587 yield from validate_float(obj, schema, context) | |
| 588 elif t in ("number", "num"): | |
| 589 yield from validate_number(obj, schema, context) | |
| 590 elif t in ("bool", "boolean"): | |
| 591 yield from validate_bool(obj, schema, context) | |
| 592 elif t in ("scalar", ): | |
| 593 yield from validate_scalar(obj, schema, context) | |
| 594 elif t in ("binary", ): | |
| 595 yield from validate_binary(obj, schema, context) | |
| 596 elif t in ("timestamp", "datetime"): | |
| 597 yield from validate_timestamp(obj, schema, context) | |
| 598 else: | |
| 599 raise SchemaError("unknown type in schema: {}".format(t)) | |
| 600 | |
| 601 | |
| 602 def _is_in_skip_keys(key, skip_keys): | |
| 603 if not skip_keys: | |
| 604 return False | |
| 605 for sk in skip_keys: | |
| 606 if isinstance(sk, str): | |
| 607 if key == sk: | |
| 608 return True | |
| 609 else: | |
| 610 if sk.search(key): | |
| 611 return True | |
| 612 return False | |
| 613 | |
| 614 | |
| 615 def _is_null_allowed_for_object(obj, schema, context): | |
| 616 if obj is None and schema.get("nullable", False): | |
| 617 return True | |
| 618 return False | |
| 619 | |
| 620 | |
| 621 def _validate_index_constraint(obj, schema, context): | |
| 622 # No evaluation of index constraints for the root context | |
| 623 if context.is_root: | |
| 624 return | |
| 625 try: | |
| 626 index_constraints = schema["index-constraint"] | |
| 627 except KeyError: | |
| 628 return # no constraints | |
| 629 else: | |
| 630 if not isinstance(index_constraints, (list, tuple, set, frozenset)): | |
| 631 index_constraints = [index_constraints] | |
| 632 if not index_constraints: | |
| 633 return | |
| 634 parent = context.safe_parent | |
| 635 try: | |
| 636 effective_index = context.index | |
| 637 except AttributeError: | |
| 638 try: | |
| 639 effective_index = context.key_index | |
| 640 except AttributeError: | |
| 641 raise SchemaError("parent container has no usable index") | |
| 642 for idx in index_constraints: | |
| 643 if idx < 0: | |
| 644 idx = len(parent.current_object) + idx | |
| 645 if idx == effective_index: | |
| 646 break | |
| 647 else: | |
| 648 yield ValidationProblem(code=10052, context=context) | |
| 649 | |
| 650 | |
| 651 def validate_dict(obj, schema, context): | |
| 652 if _is_null_allowed_for_object(obj, schema, context): | |
| 653 return | |
| 654 if not isinstance(obj, dict): | |
| 655 yield ValidationProblem(code=10000, hint="got: {}".format(type(obj).__name__), context=context) | |
| 656 return | |
| 657 yield from _validate_index_constraint(obj, schema, context) | |
| 658 minlen = schema.get("minLength", None) | |
| 659 if minlen: | |
| 660 if len(obj) < minlen: | |
| 661 yield ValidationProblem(code=10050, hint=obj, context=context) | |
| 662 maxlen = schema.get("maxLength", None) | |
| 663 if maxlen is not None: | |
| 664 if len(obj) > maxlen: | |
| 665 yield ValidationProblem(code=10051, hint=obj, context=context) | |
| 666 schema_keys = schema.get("keys", {}) if schema else {} | |
| 667 seen_keys = set() | |
| 668 schema_keynames = schema.get_child("keyNames", None) | |
| 669 idx = -1 | |
| 670 for key, item in obj.items(): | |
| 671 idx += 1 | |
| 672 if schema_keynames is None: | |
| 673 if not isinstance(key, str): | |
| 674 yield ValidationProblem(code=10003, hint=repr(key), context=context) | |
| 675 else: | |
| 676 # validate the key against given schema | |
| 677 new_context = Context(context, key=key, key_index=idx, current_object=key) | |
| 678 key_probs = list(_validate(key, schema_keynames, new_context)) | |
| 679 if key_probs: | |
| 680 yield ValidationProblem( | |
| 681 code=10034, hint=key, context=context, cause=key_probs) | |
| 682 if context.settings.break_on_keynames_problems: | |
| 683 return | |
| 684 if key in seen_keys: | |
| 685 yield ValidationProblem(code=80000, hint=key, context=context) | |
| 686 else: | |
| 687 seen_keys.add(key) | |
| 688 # XXX FIXME: context: new leaf context with new key for recursion | |
| 689 if key in schema_keys: | |
| 690 new_context = Context(context, key=key, key_index=idx, current_object=item) | |
| 691 yield from _validate(item, schema.ensure_child_schema(schema_keys[key]), new_context) | |
| 692 else: | |
| 693 # check whether additional keys are allowed | |
| 694 additional_keys = schema.get_child("additionalKeys", False) | |
| 695 if isinstance(additional_keys, bool): | |
| 696 if not additional_keys: | |
| 697 if not _is_in_skip_keys(key, context.settings.skip_keys): | |
| 698 yield ValidationProblem(code=10004, hint=str(key), context=context) | |
| 699 else: | |
| 700 if not _is_in_skip_keys(key, context.settings.skip_keys): | |
| 701 # try this as the common schema for all the additional keys | |
| 702 new_context = Context(context, key=key, key_index=idx, current_object=item) | |
| 703 yield from _validate(item, additional_keys, new_context) | |
| 704 # check whether all required keys are seen | |
| 705 try: | |
| 706 required_keys = set(schema.get("required", set())) | |
| 707 except (TypeError, ValueError): | |
| 708 raise SchemaError("`required` must be an iterable") | |
| 709 if not required_keys <= seen_keys: | |
| 710 hs = [str(i) for i in required_keys - seen_keys] | |
| 711 yield ValidationProblem(code=10005, hint=sorted(hs), context=context) | |
| 712 | |
| 713 | |
| 714 def validate_list(obj, schema, context): | |
| 715 if _is_null_allowed_for_object(obj, schema, context): | |
| 716 return | |
| 717 if not isinstance(obj, (list, tuple)): | |
| 718 yield ValidationProblem(code=10001, hint="got: {}".format(type(obj).__name__), context=context) | |
| 719 return | |
| 720 yield from _validate_index_constraint(obj, schema, context) | |
| 721 minlen = schema.get("minLength", None) | |
| 722 if minlen: | |
| 723 if len(obj) < minlen: | |
| 724 yield ValidationProblem(code=10012, hint=obj, context=context) | |
| 725 maxlen = schema.get("maxLength", None) | |
| 726 if maxlen is not None: | |
| 727 if len(obj) > maxlen: | |
| 728 yield ValidationProblem(code=10013, hint=obj, context=context) | |
| 729 try: | |
| 730 schema_items = schema.ensure_child_schema(schema["items"]) | |
| 731 except KeyError: | |
| 732 schema_items = _Schema(schema, False, {"type": validate_deny}) | |
| 733 for idx, o in enumerate(obj): | |
| 734 new_context = Context(parent=context, index=idx, current_object=o) | |
| 735 yield from _validate(o, schema_items, new_context) | |
| 736 | |
| 737 | |
| 738 def validate_set(obj, schema, context): | |
| 739 if _is_null_allowed_for_object(obj, schema, context): | |
| 740 return | |
| 741 if not isinstance(obj, (set, frozenset)): | |
| 742 yield ValidationProblem(code=10038, hint="got: {}".format(type(obj).__name__), context=context) | |
| 743 return | |
| 744 yield from _validate_index_constraint(obj, schema, context) | |
| 745 minlen = schema.get("minLength", None) | |
| 746 if minlen: | |
| 747 if len(obj) < minlen: | |
| 748 yield ValidationProblem(code=10039, hint=obj, context=context) | |
| 749 maxlen = schema.get("maxLength", None) | |
| 750 if maxlen is not None: | |
| 751 if len(obj) > maxlen: | |
| 752 yield ValidationProblem(code=10040, hint=obj, context=context) | |
| 753 try: | |
| 754 schema_items = schema.ensure_child_schema(schema["items"]) | |
| 755 except KeyError: | |
| 756 schema_items = _Schema(schema, False, {"type": validate_deny}) | |
| 757 for o in obj: | |
| 758 new_context = Context(parent=context, key=o, current_object=o) | |
| 759 yield from _validate(o, schema_items, new_context) | |
| 760 | |
| 761 | |
| 762 def validate_tuple(obj, schema, context): | |
| 763 if _is_null_allowed_for_object(obj, schema, context): | |
| 764 return | |
| 765 if not isinstance(obj, (list, tuple)): | |
| 766 yield ValidationProblem(code=10014, hint="got: {}".format(type(obj).__name__), context=context) | |
| 767 return | |
| 768 yield from _validate_index_constraint(obj, schema, context) | |
| 769 minlen = schema.get("minLength", None) | |
| 770 if minlen: | |
| 771 if len(obj) < minlen: | |
| 772 yield ValidationProblem(code=10015, hint=obj, context=context) | |
| 773 maxlen = schema.get("maxLength", None) | |
| 774 if maxlen is not None: | |
| 775 if len(obj) > maxlen: | |
| 776 yield ValidationProblem(code=10016, hint=obj, context=context) | |
| 777 schema_items = schema.get("items", []) | |
| 778 if not isinstance(schema_items, (list, tuple)): | |
| 779 raise SchemaError("tuple items require a list of schemata in items") | |
| 780 for idx, o in enumerate(obj): | |
| 781 # early exit at maxlen | |
| 782 if maxlen is not None and idx >= maxlen: | |
| 783 break | |
| 784 new_context = Context(parent=context, index=idx, current_object=o) | |
| 785 try: | |
| 786 schema_index = schema.ensure_child_schema(schema_items[idx]) | |
| 787 except IndexError: | |
| 788 additional_items = schema.get_child("additionalItems", False) | |
| 789 if isinstance(additional_items, bool): | |
| 790 if not additional_items: | |
| 791 yield ValidationProblem(code=10017, context=new_context) | |
| 792 else: | |
| 793 yield from _validate(o, additional_items, new_context) | |
| 794 else: | |
| 795 yield from _validate(o, schema_index, new_context) | |
| 796 | |
| 797 | |
| 798 def validate_str(obj, schema, context): | |
| 799 if _is_null_allowed_for_object(obj, schema, context): | |
| 800 return | |
| 801 if not isinstance(obj, str): | |
| 802 yield ValidationProblem(code=10002, hint=obj, context=context) | |
| 803 else: | |
| 804 yield from _validate_index_constraint(obj, schema, context) | |
| 805 enumvalues = schema.get("enum", None) | |
| 806 if enumvalues is not None: | |
| 807 for ev in enumvalues: | |
| 808 if ev == obj: | |
| 809 break | |
| 810 else: | |
| 811 yield ValidationProblem(code=10043, hint=obj, context=context) | |
| 812 minlen = schema.get("minLength", None) | |
| 813 if minlen: | |
| 814 if len(obj) < minlen: | |
| 815 yield ValidationProblem(code=10006, hint=obj, context=context) | |
| 816 maxlen = schema.get("maxLength", None) | |
| 817 if maxlen is not None: | |
| 818 if len(obj) > maxlen: | |
| 819 yield ValidationProblem(code=10007, hint=obj, context=context) | |
| 820 pattern = schema.get("pattern", None) | |
| 821 if pattern is not None: | |
| 822 if isinstance(pattern, str): | |
| 823 mo = re.search(pattern, obj) | |
| 824 if not mo: | |
| 825 yield ValidationProblem(code=10008, context=context) | |
| 826 elif isinstance(pattern, TYPE_RE): | |
| 827 mo = pattern.search(obj) | |
| 828 if not mo: | |
| 829 yield ValidationProblem(code=10008, context=context) | |
| 830 elif callable(pattern): | |
| 831 yield from pattern(obj, schema, context) | |
| 832 else: | |
| 833 raise SchemaError("unknown pattern type") | |
| 834 is_contained = schema.get("is-contained-in-ref", None) | |
| 835 if is_contained is not None: | |
| 836 refobj = try_get_reference(is_contained, | |
| 837 context, | |
| 838 schema, | |
| 839 default=_SENTINEL) | |
| 840 if refobj is _SENTINEL: | |
| 841 yield ValidationProblem(code=10044, context=context) | |
| 842 else: | |
| 843 try: | |
| 844 if obj not in refobj: | |
| 845 yield ValidationProblem(code=10045, context=context) | |
| 846 except TypeError: | |
| 847 yield ValidationProblem(code=10046, context=context) | |
| 848 | |
| 849 | |
| 850 def validate_binary(obj, schema, context): | |
| 851 if not isinstance(obj, (bytes, bytearray)): | |
| 852 yield ValidationProblem(code=10035, hint=obj, context=context) | |
| 853 else: | |
| 854 yield from _validate_index_constraint(obj, schema, context) | |
| 855 minlen = schema.get("minLength", None) | |
| 856 if minlen: | |
| 857 if len(obj) < minlen: | |
| 858 yield ValidationProblem(code=10036, hint=obj, context=context) | |
| 859 maxlen = schema.get("maxLength", None) | |
| 860 if maxlen is not None: | |
| 861 if len(obj) > maxlen: | |
| 862 yield ValidationProblem(code=10037, hint=obj, context=context) | |
| 863 pattern = schema.get("pattern", None) | |
| 864 if pattern is not None: | |
| 865 if isinstance(pattern, (str, bytes, bytearray)): | |
| 866 if isinstance(pattern, str): | |
| 867 if "'''" not in pattern: | |
| 868 bytes_pattern = ast.literal_eval( | |
| 869 "b'''" + pattern + "'''") | |
| 870 elif '"""' not in pattern: | |
| 871 bytes_pattern = ast.literal_eval( | |
| 872 'b"""' + pattern + '"""') | |
| 873 else: | |
| 874 raise SchemaError("incompatible bytes pattern") | |
| 875 else: | |
| 876 bytes_pattern = pattern | |
| 877 mo = re.search(bytes_pattern, obj) | |
| 878 if not mo: | |
| 879 yield ValidationProblem(code=10047, context=context) | |
| 880 elif isinstance(pattern, TYPE_RE): | |
| 881 mo = pattern.search(obj) | |
| 882 if not mo: | |
| 883 yield ValidationProblem(code=10047, context=context) | |
| 884 elif callable(pattern): | |
| 885 yield from pattern(obj, schema, context) | |
| 886 else: | |
| 887 raise SchemaError("unknown pattern type") | |
| 888 | |
| 889 | |
| 890 def validate_timestamp(obj, schema, context): | |
| 891 if not isinstance(obj, datetime.datetime): | |
| 892 yield ValidationProblem(code=10041, hint=obj, context=context) | |
| 893 else: | |
| 894 yield from _validate_index_constraint(obj, schema, context) | |
| 895 value = schema.get("value", None) | |
| 896 if value is not None: | |
| 897 if callable(value): | |
| 898 yield from value(obj, schema, context) | |
| 899 else: | |
| 900 raise SchemaError("unknown value validator (only a callable allowed)") | |
| 901 | |
| 902 | |
| 903 def validate_integer(obj, schema, context): | |
| 904 if _is_null_allowed_for_object(obj, schema, context): | |
| 905 return | |
| 906 if not isinstance(obj, int): | |
| 907 yield ValidationProblem(code=10020, hint=obj, context=context) | |
| 908 else: | |
| 909 yield from _validate_index_constraint(obj, schema, context) | |
| 910 minValue = schema.get("minValue", None) | |
| 911 if minValue is not None and obj < minValue: | |
| 912 yield ValidationProblem(code=10021, hint=obj, context=context) | |
| 913 maxValue = schema.get("maxValue", None) | |
| 914 if maxValue is not None and obj > maxValue: | |
| 915 yield ValidationProblem(code=10022, hint=obj, context=context) | |
| 916 enumvalues = schema.get("enum", None) | |
| 917 if enumvalues is not None: | |
| 918 for ev in enumvalues: | |
| 919 if ev == obj: | |
| 920 break | |
| 921 else: | |
| 922 yield ValidationProblem(code=10048, hint=obj, context=context) | |
| 923 value = schema.get("value", None) | |
| 924 if value is not None: | |
| 925 if callable(value): | |
| 926 yield from value(obj, schema, context) | |
| 927 else: | |
| 928 raise SchemaError("unknown value validator (only a callable allowed)") | |
| 929 | |
| 930 | |
| 931 def validate_float(obj, schema, context): | |
| 932 if _is_null_allowed_for_object(obj, schema, context): | |
| 933 return | |
| 934 if not isinstance(obj, float): | |
| 935 yield ValidationProblem(code=10023, hint=obj, context=context) | |
| 936 else: | |
| 937 yield from _validate_index_constraint(obj, schema, context) | |
| 938 minValue = schema.get("minValue", None) | |
| 939 if minValue is not None and obj < minValue: | |
| 940 yield ValidationProblem(code=10024, hint=obj, context=context) | |
| 941 maxValue = schema.get("maxValue", None) | |
| 942 if maxValue is not None and obj > maxValue: | |
| 943 yield ValidationProblem(code=10025, hint=obj, context=context) | |
| 944 value = schema.get("value", None) | |
| 945 if value is not None: | |
| 946 if callable(value): | |
| 947 yield from value(obj, schema, context) | |
| 948 else: | |
| 949 raise SchemaError("unknown value validator (only a callable allowed)") | |
| 950 | |
| 951 | |
| 952 def validate_number(obj, schema, context): | |
| 953 if _is_null_allowed_for_object(obj, schema, context): | |
| 954 return | |
| 955 if not isinstance(obj, (int, float)): | |
| 956 yield ValidationProblem(code=10030, hint=obj, context=context) | |
| 957 else: | |
| 958 yield from _validate_index_constraint(obj, schema, context) | |
| 959 minValue = schema.get("minValue", None) | |
| 960 if minValue is not None and isinstance(obj, float): | |
| 961 minValue *= 1.0 | |
| 962 if minValue is not None and obj < minValue: | |
| 963 yield ValidationProblem(code=10031, hint=obj, context=context) | |
| 964 maxValue = schema.get("maxValue", None) | |
| 965 if maxValue is not None and isinstance(obj, float): | |
| 966 maxValue *= 1.0 | |
| 967 if maxValue is not None and obj > maxValue: | |
| 968 yield ValidationProblem(code=10032, hint=obj, context=context) | |
| 969 enumvalues = schema.get("enum", None) | |
| 970 if enumvalues is not None: | |
| 971 for ev in enumvalues: | |
| 972 if ev == obj: | |
| 973 break | |
| 974 else: | |
| 975 yield ValidationProblem(code=10049, hint=obj, context=context) | |
| 976 value = schema.get("value", None) | |
| 977 if value is not None: | |
| 978 if callable(value): | |
| 979 yield from value(obj, schema, context) | |
| 980 else: | |
| 981 raise SchemaError("unknown value validator (only a callable allowed)") | |
| 982 | |
| 983 | |
| 984 def validate_scalar(obj, schema, context): | |
| 985 if _is_null_allowed_for_object(obj, schema, context): | |
| 986 return | |
| 987 yield from _validate_index_constraint(obj, schema, context) | |
| 988 if obj is None: | |
| 989 yield ValidationProblem(code=10033, hint=obj, context=context) | |
| 990 if isinstance(obj, (dict, list, tuple, set, frozenset)): | |
| 991 yield ValidationProblem(code=10033, hint=obj, context=context) | |
| 992 | |
| 993 | |
| 994 def validate_deny(obj, schema, context): | |
| 995 yield from _validate_index_constraint(obj, schema, context) | |
| 996 yield ValidationProblem(code=10010, context=context) | |
| 997 | |
| 998 | |
| 999 def validate_accept(obj, schema, context): | |
| 1000 yield from _validate_index_constraint(obj, schema, context) | |
| 1001 | |
| 1002 | |
| 1003 def validate_null(obj, schema, context): | |
| 1004 yield from _validate_index_constraint(obj, schema, context) | |
| 1005 if obj is not None: | |
| 1006 yield ValidationProblem(code=10011, context=context) | |
| 1007 | |
| 1008 | |
| 1009 def validate_empty(obj, schema, context): | |
| 1010 yield from _validate_index_constraint(obj, schema, context) | |
| 1011 if obj is None: | |
| 1012 return | |
| 1013 if isinstance(obj, (dict, list, tuple, set, frozenset)) and not obj: | |
| 1014 return | |
| 1015 yield ValidationProblem(10018, context=context) | |
| 1016 | |
| 1017 | |
| 1018 def validate_bool(obj, schema, context): | |
| 1019 if _is_null_allowed_for_object(obj, schema, context): | |
| 1020 return | |
| 1021 if not isinstance(obj, bool): | |
| 1022 yield ValidationProblem(code=10026, hint=obj, context=context) | |
| 1023 else: | |
| 1024 yield from _validate_index_constraint(obj, schema, context) | |
| 1025 value = schema.get("value", None) | |
| 1026 if value is not None: | |
| 1027 if callable(value): | |
| 1028 yield from value(obj, schema, context) | |
| 1029 elif value and not obj: | |
| 1030 yield ValidationProblem(code=10027, hint=obj, context=context) | |
| 1031 elif not value and obj: | |
| 1032 yield ValidationProblem(code=10028, hint=obj, context=context) | |
| 1033 | |
| 1034 | |
| 1035 def validate_allOf(obj, schema, context): | |
| 1036 if not isinstance(schema, (list, tuple)): | |
| 1037 raise SchemaError("require a list of schematas for `all-of'") | |
| 1038 res = [] | |
| 1039 for idx, s in enumerate(schema): | |
| 1040 assert isinstance(s, _Schema) | |
| 1041 tr = list(_validate(obj, s, context)) | |
| 1042 if tr: | |
| 1043 res.append((idx, tr, )) | |
| 1044 if res: | |
| 1045 yield ValidationProblem( | |
| 1046 code=10057, | |
| 1047 context=context, | |
| 1048 cause=[ | |
| 1049 ValidationProblem( | |
| 1050 code=10058, | |
| 1051 context=context, | |
| 1052 cause=tr, | |
| 1053 index=idx) for (idx, tr) in res]) | |
| 1054 | |
| 1055 | |
| 1056 def validate_anyOf(obj, schema, context): | |
| 1057 if not isinstance(schema, (list, tuple)): | |
| 1058 raise SchemaError("require a list of schematas for `any-of'") | |
| 1059 res = [] | |
| 1060 for s in schema: | |
| 1061 assert isinstance(s, _Schema) | |
| 1062 tr = list(_validate(obj, s, context)) | |
| 1063 if tr: | |
| 1064 res.append(tr) | |
| 1065 else: | |
| 1066 # Erfolg: gleich positiv zurueck ohne Meldungen | |
| 1067 return | |
| 1068 # Ansonsten: alle Fehlschlaege protokollieren | |
| 1069 if res: | |
| 1070 yield ValidationProblem( | |
| 1071 code=10055, | |
| 1072 context=context, | |
| 1073 cause=[ | |
| 1074 ValidationProblem( | |
| 1075 code=10056, | |
| 1076 context=context, | |
| 1077 cause=tr) for tr in res]) | |
| 1078 | |
| 1079 | |
| 1080 def validate_oneOf(obj, schema, context): | |
| 1081 if not isinstance(schema, (list, tuple)): | |
| 1082 raise SchemaError("require a list of schematas for `one-of'") | |
| 1083 success_res = [] | |
| 1084 failed_res = [] | |
| 1085 for idx, s in enumerate(schema): | |
| 1086 assert isinstance(s, _Schema) | |
| 1087 tr = list(_validate(obj, s, context)) | |
| 1088 if tr: | |
| 1089 failed_res.append((idx, tr, )) | |
| 1090 else: | |
| 1091 success_res.append(idx) | |
| 1092 if len(success_res) == 1: | |
| 1093 return | |
| 1094 elif len(success_res) == 0: | |
| 1095 # Ansonsten: alle Fehlschlaege protokollieren | |
| 1096 if failed_res: | |
| 1097 yield ValidationProblem( | |
| 1098 code=10053, | |
| 1099 context=context, | |
| 1100 cause=[ | |
| 1101 ValidationProblem( | |
| 1102 code=10054, | |
| 1103 context=context, | |
| 1104 cause=tr, | |
| 1105 index=idx) for (idx, tr) in failed_res]) | |
| 1106 else: | |
| 1107 # Die Indizes der "zuvielen" in "hint" anzeigen | |
| 1108 yield ValidationProblem(code=10019, hint=",".join([str(k) for k in success_res])) | |
| 1109 | |
| 1110 | |
| 1111 def validate_not(obj, schema, context): | |
| 1112 assert isinstance(schema, _Schema) | |
| 1113 res = list(_validate(obj, schema, context)) | |
| 1114 if not res: | |
| 1115 yield ValidationProblem(code=10029, hint=obj, context=context, | |
| 1116 cause=res) | |
| 1117 | |
| 1118 | |
| 1119 def process_schema_references(schema, context, check_single_ref_key=True): | |
| 1120 try: | |
| 1121 ref = schema[SCHEMA_REF_KEY] | |
| 1122 except (KeyError, TypeError): | |
| 1123 return schema | |
| 1124 # if `$ref' is found it MUST be the only key | |
| 1125 if check_single_ref_key and len(schema) != 1: | |
| 1126 raise SchemaError("`{}' must be the single key if it exists") | |
| 1127 schema = try_get_reference(ref, context, schema) | |
| 1128 if not isinstance(schema, _Schema): | |
| 1129 raise SchemaError( | |
| 1130 "dereferenced schema is not a `_Schema': {}".format(ref)) | |
| 1131 schema = copy.deepcopy(schema) | |
| 1132 return process_schema_references(schema, context, check_single_ref_key=True) | |
| 1133 | |
| 1134 | |
| 1135 def process_schema_conditionals(schema, context): | |
| 1136 """Lisp-like `cond` to provide schema modifications | |
| 1137 | |
| 1138 :param schema: the input schema | |
| 1139 :param context: the validation context with a valid | |
| 1140 `context.root.root_object` | |
| 1141 :returns: the processed schema: the schema itself if it is unchanged and | |
| 1142 a copy of the schema if has been changed | |
| 1143 | |
| 1144 """ | |
| 1145 what, conds = _get_one_of(schema, "cond", "match", default=None) | |
| 1146 if what is None or conds is None: | |
| 1147 return schema | |
| 1148 if not isinstance(conds, (list, tuple)): | |
| 1149 raise SchemaError("the conditions of a cond must be a sequence") | |
| 1150 if what == "cond": | |
| 1151 return _process_schema_conditionals_cond(schema, conds, context) | |
| 1152 elif what == "match": | |
| 1153 return _process_schema_conditionals_match(schema, conds, context) | |
| 1154 else: | |
| 1155 assert False, "unreachable" | |
| 1156 | |
| 1157 | |
| 1158 def _process_schema_conditionals_cond(schema, conds, context): | |
| 1159 for cond in conds: | |
| 1160 if not isinstance(cond, dict): | |
| 1161 raise SchemaError("a single condition must be a dict") | |
| 1162 if eval_condition(cond, context, schema): | |
| 1163 rep_type, rep_schema = _get_one_of( | |
| 1164 cond, "then", "then-replace", "then-merge") | |
| 1165 rep_schema = schema.ensure_child_schema(rep_schema) | |
| 1166 if rep_type in ("then", "then-replace"): | |
| 1167 do_merge = False | |
| 1168 elif rep_type == "then-merge": | |
| 1169 do_merge = True | |
| 1170 else: | |
| 1171 raise SchemaError("unknown then type: {}".format(rep_type)) | |
| 1172 break | |
| 1173 else: | |
| 1174 # | |
| 1175 # No condition was true: just remove the "cond" to get the | |
| 1176 # effective schema. | |
| 1177 # | |
| 1178 rep_schema = None | |
| 1179 do_merge = False | |
| 1180 | |
| 1181 new_schema = schema.copy() | |
| 1182 del new_schema["cond"] | |
| 1183 if rep_schema: | |
| 1184 rep_schema = process_schema_references(rep_schema, context) | |
| 1185 # this could insert a new nested "cond" or "match" again | |
| 1186 if do_merge: | |
| 1187 rep_schema = copy.deepcopy(rep_schema) | |
| 1188 new_schema = _merge(rep_schema, new_schema) | |
| 1189 else: | |
| 1190 new_schema.update(rep_schema) | |
| 1191 # Recursively apply "cond/match" evaluation to the resulting schema | |
| 1192 return process_schema_conditionals(new_schema, context) | |
| 1193 | |
| 1194 | |
| 1195 def _process_schema_conditionals_match(schema, conds, context): | |
| 1196 rep_schemata = [] | |
| 1197 for cond in conds: | |
| 1198 if not isinstance(cond, dict): | |
| 1199 raise SchemaError("a single condition must be a dict") | |
| 1200 if eval_condition(cond, context, schema): | |
| 1201 rep_type, rep_schema = _get_one_of( | |
| 1202 cond, "then", "then-replace", "then-merge") | |
| 1203 rep_schema = schema.ensure_child_schema(rep_schema) | |
| 1204 if rep_type in ("then", "then-replace"): | |
| 1205 rep_schemata.append((False, rep_schema)) | |
| 1206 elif rep_type == "then-merge": | |
| 1207 rep_schemata.append((True, rep_schema)) | |
| 1208 else: | |
| 1209 raise SchemaError("unknown then type: {}".format(rep_type)) | |
| 1210 | |
| 1211 new_schema = schema.copy() | |
| 1212 del new_schema["match"] | |
| 1213 for do_merge, rep_schema in rep_schemata: | |
| 1214 rep_schema = process_schema_references(rep_schema, context) | |
| 1215 # this could insert a new nested "cond" or "match" again | |
| 1216 if do_merge: | |
| 1217 rep_schema = copy.deepcopy(rep_schema) | |
| 1218 new_schema = _merge(rep_schema, new_schema) | |
| 1219 else: | |
| 1220 new_schema.update(rep_schema) | |
| 1221 # Recursively apply "cond/match" evaluation to the resulting schema | |
| 1222 return process_schema_conditionals(new_schema, context) | |
| 1223 | |
| 1224 | |
| 1225 def eval_condition(cond, context, schema): | |
| 1226 """Eval the condition in `cond` and return a tuple `(hit, predval)` | |
| 1227 | |
| 1228 """ | |
| 1229 pred, predval = _get_one_of( | |
| 1230 cond, | |
| 1231 "when-ref-true", "when-ref-exists", "when", | |
| 1232 default=_SENTINEL) | |
| 1233 | |
| 1234 if pred == "when": | |
| 1235 # rekursive evaluation of `predval` as the real predicate | |
| 1236 return eval_pred(predval, context, schema) | |
| 1237 elif pred == "when-ref-true": | |
| 1238 refobj = try_get_reference(predval, context, schema, default=None) | |
| 1239 return bool(refobj) | |
| 1240 elif pred == "when-ref-exists": | |
| 1241 refobj = try_get_reference(predval, context, schema, default=_SENTINEL) | |
| 1242 return refobj is not _SENTINEL | |
| 1243 else: | |
| 1244 raise SchemaError("unknown condition type: {}".format(pred)) | |
| 1245 | |
| 1246 | |
| 1247 def eval_pred(pred, context, schema): | |
| 1248 if isinstance(pred, dict): | |
| 1249 combinator, combinator_val = _get_one_of( | |
| 1250 pred, | |
| 1251 "not", "all-of", "any-of", "one-of", | |
| 1252 default=None) | |
| 1253 if combinator: | |
| 1254 if combinator == "not": | |
| 1255 return not eval_pred(combinator_val, context, schema) | |
| 1256 elif combinator == "all-of": | |
| 1257 if not isinstance(combinator_val, (list, tuple)): | |
| 1258 raise SchemaError("`all-of' requires a list of childs") | |
| 1259 for cv in combinator_val: | |
| 1260 if not eval_pred(cv, context, schema): | |
| 1261 return False | |
| 1262 return True | |
| 1263 elif combinator == "any-of": | |
| 1264 if not isinstance(combinator_val, (list, tuple)): | |
| 1265 raise SchemaError("`any-of' requires a list of childs") | |
| 1266 for cv in combinator_val: | |
| 1267 if eval_pred(cv, context, schema): | |
| 1268 return True | |
| 1269 return False | |
| 1270 elif combinator == "one-of": | |
| 1271 if not isinstance(combinator_val, (list, tuple)): | |
| 1272 raise SchemaError("`one-of' requires a list of childs") | |
| 1273 num_true = 0 | |
| 1274 for cv in combinator_val: | |
| 1275 if eval_pred(cv, context, schema): | |
| 1276 num_true += 1 | |
| 1277 # shortcut | |
| 1278 if num_true > 1: | |
| 1279 return False | |
| 1280 if num_true == 1: | |
| 1281 return True | |
| 1282 else: | |
| 1283 return False | |
| 1284 else: | |
| 1285 raise SchemaError( | |
| 1286 "unknown logical operator: {}".format(combinator)) | |
| 1287 else: | |
| 1288 pred_key, pred_val = _get_one_of( | |
| 1289 pred, | |
| 1290 "ref-true", "ref-exists", "equals", | |
| 1291 default=None) | |
| 1292 if pred_key == "ref-true": | |
| 1293 refobj = try_get_reference( | |
| 1294 pred_val, context, schema, default=None) | |
| 1295 return bool(refobj) | |
| 1296 elif pred_key == "ref-exists": | |
| 1297 refobj = try_get_reference( | |
| 1298 pred_val, context, schema, default=_SENTINEL) | |
| 1299 return refobj is not _SENTINEL | |
| 1300 elif pred_key == "equals": | |
| 1301 if not isinstance(pred_val, (list, tuple)): | |
| 1302 raise SchemaError("`equals' requires a list as childs") | |
| 1303 if len(pred_val) != 2: | |
| 1304 raise SchemaError("`equals' requires a list of len 2") | |
| 1305 op1 = eval_comparison_operator_operand( | |
| 1306 pred_val[0], context, schema) | |
| 1307 op2 = eval_comparison_operator_operand( | |
| 1308 pred_val[1], context, schema) | |
| 1309 return op1 == op2 | |
| 1310 else: | |
| 1311 raise SchemaError("unknown predicate: {}".format(pred)) | |
| 1312 elif isinstance(pred, list): | |
| 1313 # implicit all-of (aka AND) | |
| 1314 for cv in pred: | |
| 1315 if not eval_pred(cv, context, schema): | |
| 1316 return False | |
| 1317 return True | |
| 1318 else: | |
| 1319 return pred | |
| 1320 | |
| 1321 | |
| 1322 def eval_comparison_operator_operand(op, context, schema): | |
| 1323 if not isinstance(op, dict): | |
| 1324 raise SchemaError("an operand must be a dict") | |
| 1325 opkey, opval = _get_one_of(op, "ref", "val", "value") | |
| 1326 if opkey is None: | |
| 1327 raise SchemaError("no operant given in {!r}".format(op)) | |
| 1328 if opkey == "ref": | |
| 1329 return try_get_reference(opval, context, schema) | |
| 1330 elif opkey in ("val", "value"): | |
| 1331 return opval | |
| 1332 else: | |
| 1333 assert False | |
| 1334 | |
| 1335 | |
| 1336 def try_get_reference(ref, context, schema, default=None): | |
| 1337 """Get the object referenced in `ref` | |
| 1338 | |
| 1339 Use `context` as data/object context and `schema` as the current schema | |
| 1340 context. | |
| 1341 | |
| 1342 """ | |
| 1343 uri = rfc3986.URIReference.from_string(ref).normalize() | |
| 1344 if not uri.scheme: | |
| 1345 uri = uri.copy_with(scheme="object") | |
| 1346 if uri.scheme == "object": | |
| 1347 if ref.startswith("object#"): | |
| 1348 for attr in ("authority", "path", "query"): | |
| 1349 if getattr(uri, attr, None) is not None: | |
| 1350 raise SchemaError( | |
| 1351 "bogus {} in URI reference `{}'".format(attr, ref)) | |
| 1352 if uri.fragment is None: | |
| 1353 raise SchemaError("fragment required in reference") | |
| 1354 if not uri.fragment: | |
| 1355 return context.root.root_object | |
| 1356 elif uri.fragment == '.': | |
| 1357 return context.current_object | |
| 1358 parts = uri.fragment.split('.') # use '.' separator as in configmix | |
| 1359 if parts[0]: | |
| 1360 # absolute | |
| 1361 d = context.root.root_object | |
| 1362 else: | |
| 1363 # relative | |
| 1364 d = context.current_object | |
| 1365 parts = parts[1:] | |
| 1366 c = context # needed to determine relative object references | |
| 1367 relative_refs_allowed = True | |
| 1368 for part in [urllib.parse.unquote(p) for p in parts]: | |
| 1369 if part: | |
| 1370 relative_refs_allowed = False | |
| 1371 try: | |
| 1372 d = d[part] | |
| 1373 except (KeyError, IndexError, TypeError): | |
| 1374 return default | |
| 1375 else: | |
| 1376 if not relative_refs_allowed: | |
| 1377 raise SchemaError( | |
| 1378 "empty part in path to object reference not allowed") | |
| 1379 c = c.safe_parent | |
| 1380 d = c.current_object | |
| 1381 return d | |
| 1382 elif uri.scheme == "schema": | |
| 1383 if not uri.path or (uri.path == SCHEMA_PATH_SELF): | |
| 1384 s = schema.SELF | |
| 1385 elif uri.path == SCHEMA_PATH_ROOT: | |
| 1386 s = schema.ROOT | |
| 1387 else: | |
| 1388 s = schema.get_cached_schema(uri.path, load_if_needed=True) | |
| 1389 if uri.fragment is None: | |
| 1390 raise SchemaError("fragment required in reference") | |
| 1391 | |
| 1392 if not uri.fragment.startswith('/'): | |
| 1393 raise SchemaError("references to parts of a schema must be absolute (begin with `/')") | |
| 1394 if uri.fragment == '/': | |
| 1395 return s | |
| 1396 parts = uri.fragment.split('/') | |
| 1397 parent_for_subschema = s | |
| 1398 for part in [urllib.parse.unquote(p) for p in parts[1:]]: | |
| 1399 try: | |
| 1400 v = s[part] | |
| 1401 except (KeyError, IndexError, TypeError): | |
| 1402 return default | |
| 1403 else: | |
| 1404 if isinstance(v, _Schema): | |
| 1405 pass | |
| 1406 elif isinstance(v, dict): | |
| 1407 s = _Schema(parent_for_subschema, False, v) | |
| 1408 else: | |
| 1409 # need not try further | |
| 1410 return default | |
| 1411 return s | |
| 1412 else: | |
| 1413 raise SchemaError("Unknown schema reference scheme: {}".format(uri.scheme)) | |
| 1414 | |
| 1415 | |
| 1416 _DEL_VALUE = '{{::DEL::}}' | |
| 1417 """Sigil to mark keys to be deleted in the target when merging""" | |
| 1418 | |
| 1419 | |
| 1420 def _merge(user, default): | |
| 1421 """Logically merge the configuration in `user` into `default`. | |
| 1422 | |
| 1423 :param dict user: | |
| 1424 the new configuration that will be logically merged | |
| 1425 into `default` | |
| 1426 :param dict default: | |
| 1427 the base configuration where `user` is logically merged into | |
| 1428 :returns: `user` with the necessary amendments from `default`. | |
| 1429 If `user` is ``None`` then `default` is returned. | |
| 1430 | |
| 1431 .. note:: Implementation: The configuration in `user` is | |
| 1432 augmented/changed **inplace**. | |
| 1433 | |
| 1434 If a value in `user` is equal to :data:`._DEL_VALUE` | |
| 1435 (``{{::DEL::}}``) the corresponding key will be deleted from the | |
| 1436 merged output. | |
| 1437 | |
| 1438 From http://stackoverflow.com/questions/823196/yaml-merge-in-python | |
| 1439 | |
| 1440 """ | |
| 1441 if user is None: | |
| 1442 _filter_deletions(default) | |
| 1443 return default | |
| 1444 if isinstance(user, dict) and isinstance(default, dict): | |
| 1445 for k, v in default.items(): | |
| 1446 if k in user: | |
| 1447 if user[k] == _DEL_VALUE: | |
| 1448 # do not copy and delete the marker | |
| 1449 del user[k] | |
| 1450 else: | |
| 1451 user[k] = _merge_item(user[k], v) | |
| 1452 else: | |
| 1453 user[k] = v | |
| 1454 else: | |
| 1455 raise SchemaError("can only merge two dicts on top-level") | |
| 1456 _filter_deletions(user) | |
| 1457 return user | |
| 1458 | |
| 1459 | |
| 1460 def _merge_item(user, default): | |
| 1461 """Recursion helper for :func:`._merge` | |
| 1462 | |
| 1463 """ | |
| 1464 if isinstance(user, dict) and isinstance(default, dict): | |
| 1465 for k, v in default.items(): | |
| 1466 if k in user: | |
| 1467 if user[k] == _DEL_VALUE: | |
| 1468 # do not copy and delete the marker | |
| 1469 del user[k] | |
| 1470 else: | |
| 1471 user[k] = _merge_item(user[k], v) | |
| 1472 else: | |
| 1473 user[k] = v | |
| 1474 elif isinstance(user, (list, tuple)) and isinstance(default, (list, tuple)): | |
| 1475 for idx, v in enumerate(default): | |
| 1476 user.insert(idx, v) | |
| 1477 return user | |
| 1478 | |
| 1479 | |
| 1480 def _filter_deletions(d): | |
| 1481 """Recursively filter deletions in the dict `d`. | |
| 1482 | |
| 1483 Deletions have values that equal :data:`._DEL_VALUE`. | |
| 1484 | |
| 1485 """ | |
| 1486 if not isinstance(d, dict): | |
| 1487 return | |
| 1488 # use a copy of the items because we change `d` while iterating | |
| 1489 for k, v in list(d.items()): | |
| 1490 if v == _DEL_VALUE: | |
| 1491 del d[k] | |
| 1492 else: | |
| 1493 if isinstance(d[k], dict): | |
| 1494 _filter_deletions(d[k]) | |
| 1495 | |
| 1496 | |
| 1497 def _log_problem_cause_all(logger, loglevel, level, problems): | |
| 1498 if not problems: | |
| 1499 return | |
| 1500 for pr in problems: | |
| 1501 logger.log(loglevel, "%s> %r", "-"*((level*2)+2), pr) | |
| 1502 _log_problem_cause_all(logger, loglevel, level+1, pr.cause) | |
| 1503 | |
| 1504 | |
| 1505 def _build_problems_by_level_and_depth(by_level, by_depth, level, problems): | |
| 1506 for pr in problems: | |
| 1507 if not pr.cause: | |
| 1508 continue | |
| 1509 try: | |
| 1510 prl = by_level[level] | |
| 1511 except LookupError: | |
| 1512 prl= [] | |
| 1513 by_level[level] = prl | |
| 1514 prl.append(pr) | |
| 1515 | |
| 1516 depth = pr.context.depth | |
| 1517 try: | |
| 1518 prd = by_depth[depth] | |
| 1519 except LookupError: | |
| 1520 prd= [] | |
| 1521 by_depth[depth] = prd | |
| 1522 prd.append(pr) | |
| 1523 _build_problems_by_level_and_depth( | |
| 1524 by_level, by_depth, level+1, pr.cause) | |
| 1525 | |
| 1526 | |
| 1527 def _log_problem_cause(logger, loglevel, max_level, max_depth, level, problems): | |
| 1528 for pr in problems: | |
| 1529 # | |
| 1530 # Check whether we will start logging from this level downwards | |
| 1531 # all problems | |
| 1532 # | |
| 1533 if max_level is None or level == max_level: | |
| 1534 new_max_level = None # trigger logging | |
| 1535 else: | |
| 1536 new_max_level = max_level | |
| 1537 if max_depth is None or max_depth == pr.context.depth: | |
| 1538 new_max_depth = None # trigger logging | |
| 1539 else: | |
| 1540 new_max_depth = max_depth | |
| 1541 if new_max_level is None or new_max_depth is None: | |
| 1542 logger.log(loglevel, "%s> %r", "-"*((level*2)+2), pr) | |
| 1543 if pr.cause: | |
| 1544 _log_problem_cause( | |
| 1545 logger, loglevel, | |
| 1546 new_max_level, new_max_depth, | |
| 1547 level+1, pr.cause) | |
| 1548 | |
| 1549 | |
| 1550 def log_problem_cause(logger, loglevel, debug, level, problems): | |
| 1551 if not problems: | |
| 1552 return | |
| 1553 if debug: | |
| 1554 _log_problem_cause_all(logger, loglevel, level, problems) | |
| 1555 else: | |
| 1556 by_level = {} # to determine maximum problem nesting level | |
| 1557 by_depth = {} # to determine maximum context nexting level | |
| 1558 _build_problems_by_level_and_depth(by_level, by_depth, level, problems) | |
| 1559 | |
| 1560 max_level = max(by_level.keys()) | |
| 1561 max_depth = max(by_depth.keys()) | |
| 1562 | |
| 1563 _log_problem_cause( | |
| 1564 logger, loglevel, max_level, max_depth, level, problems) |
