deterministic-scenario-engine 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.
- deterministic_scenario_engine-1.0.0.dist-info/METADATA +146 -0
- deterministic_scenario_engine-1.0.0.dist-info/RECORD +46 -0
- deterministic_scenario_engine-1.0.0.dist-info/WHEEL +5 -0
- deterministic_scenario_engine-1.0.0.dist-info/entry_points.txt +2 -0
- deterministic_scenario_engine-1.0.0.dist-info/licenses/LICENSE +201 -0
- deterministic_scenario_engine-1.0.0.dist-info/top_level.txt +1 -0
- scenario_engine/__init__.py +41 -0
- scenario_engine/_version.py +3 -0
- scenario_engine/adapters/__init__.py +5 -0
- scenario_engine/adapters/json_file.py +58 -0
- scenario_engine/adapters/sqlalchemy.py +207 -0
- scenario_engine/address.py +37 -0
- scenario_engine/artifacts.py +16 -0
- scenario_engine/canonical.py +109 -0
- scenario_engine/clock.py +22 -0
- scenario_engine/context.py +25 -0
- scenario_engine/control_flow.py +63 -0
- scenario_engine/dsl/__init__.py +31 -0
- scenario_engine/dsl/compiler.py +232 -0
- scenario_engine/dsl/errors.py +23 -0
- scenario_engine/dsl/models.py +74 -0
- scenario_engine/dsl/parser.py +582 -0
- scenario_engine/dsl/runtime.py +211 -0
- scenario_engine/errors.py +13 -0
- scenario_engine/expressions.py +330 -0
- scenario_engine/faults.py +36 -0
- scenario_engine/history.py +38 -0
- scenario_engine/ids.py +36 -0
- scenario_engine/integrations/__init__.py +7 -0
- scenario_engine/integrations/hypothesis.py +103 -0
- scenario_engine/integrations/schemathesis.py +142 -0
- scenario_engine/invariants.py +21 -0
- scenario_engine/manifest.py +102 -0
- scenario_engine/oracle.py +29 -0
- scenario_engine/plugins.py +175 -0
- scenario_engine/provenance.py +27 -0
- scenario_engine/pytest_plugin.py +95 -0
- scenario_engine/reference_packs/__init__.py +1 -0
- scenario_engine/reference_packs/ecommerce.py +57 -0
- scenario_engine/resources.py +163 -0
- scenario_engine/result.py +99 -0
- scenario_engine/rng.py +36 -0
- scenario_engine/runner.py +103 -0
- scenario_engine/state.py +39 -0
- scenario_engine/validation.py +95 -0
- scenario_engine/values.py +66 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Pure deterministic resource validators and pre-execution constraints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
from .errors import ScenarioEngineError
|
|
10
|
+
from .expressions import EvaluationEnvironment, Expression
|
|
11
|
+
from .ids import LogicalID
|
|
12
|
+
from .resources import ResolvedResources, ResourceResolutionError
|
|
13
|
+
from .values import MISSING, canonical_bytes
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ResourceValidationError(ScenarioEngineError, ValueError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConstraintError(ScenarioEngineError, ValueError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConstraintDefinitionError(ConstraintError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConstraintViolation(ConstraintError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def semantic_equal(left: Any, right: Any) -> bool:
|
|
33
|
+
return canonical_bytes(left) == canonical_bytes(right)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _semantic_type(value: Any) -> str:
|
|
37
|
+
if value is MISSING: return "missing"
|
|
38
|
+
if value is None: return "null"
|
|
39
|
+
if type(value) is bool: return "boolean"
|
|
40
|
+
if type(value) is int: return "integer"
|
|
41
|
+
if isinstance(value, Decimal): return "decimal"
|
|
42
|
+
if isinstance(value, str): return "string"
|
|
43
|
+
if isinstance(value, datetime): return "datetime"
|
|
44
|
+
if isinstance(value, timedelta): return "duration"
|
|
45
|
+
if isinstance(value, LogicalID): return "logical_id"
|
|
46
|
+
if isinstance(value, (list, tuple)): return "list"
|
|
47
|
+
if isinstance(value, Mapping): return "map"
|
|
48
|
+
return "unsupported"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_resources(resources: ResolvedResources, declarations: tuple[Mapping[str, Any], ...]) -> None:
|
|
52
|
+
for declaration in declarations:
|
|
53
|
+
validator_id = declaration["id"]
|
|
54
|
+
path = declaration["resource"]
|
|
55
|
+
kind = declaration["kind"]
|
|
56
|
+
try:
|
|
57
|
+
value = resources.lookup(path)
|
|
58
|
+
except ResourceResolutionError as error:
|
|
59
|
+
raise ResourceValidationError(
|
|
60
|
+
f"validator {validator_id} ({kind}) resource {path}: {error}"
|
|
61
|
+
) from None
|
|
62
|
+
reason: str | None = None
|
|
63
|
+
if kind == "required":
|
|
64
|
+
if value is MISSING: reason = "value is MISSING"
|
|
65
|
+
elif kind == "type":
|
|
66
|
+
actual = _semantic_type(value)
|
|
67
|
+
if actual != declaration["type"]: reason = f"expected {declaration['type']}, got {actual}"
|
|
68
|
+
elif kind == "range":
|
|
69
|
+
if type(value) not in (int, Decimal): reason = "value is not integer or decimal"
|
|
70
|
+
else:
|
|
71
|
+
if "min" in declaration and Decimal(value) < Decimal(declaration["min"]): reason = "below inclusive minimum"
|
|
72
|
+
if "max" in declaration and Decimal(value) > Decimal(declaration["max"]): reason = "above inclusive maximum"
|
|
73
|
+
elif kind == "length":
|
|
74
|
+
if not isinstance(value, (str, list, tuple, Mapping)): reason = "value has no supported length"
|
|
75
|
+
else:
|
|
76
|
+
if "min" in declaration and len(value) < declaration["min"]: reason = "below inclusive minimum length"
|
|
77
|
+
if "max" in declaration and len(value) > declaration["max"]: reason = "above inclusive maximum length"
|
|
78
|
+
elif kind == "one_of":
|
|
79
|
+
if not any(semantic_equal(value, candidate) for candidate in declaration["values"]):
|
|
80
|
+
reason = "value is not one of allowed semantic values"
|
|
81
|
+
if reason:
|
|
82
|
+
raise ResourceValidationError(
|
|
83
|
+
f"validator {validator_id} ({kind}) resource {path}: {reason}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def evaluate_constraints(constraints: tuple[tuple[str, Expression, str | None], ...]) -> None:
|
|
88
|
+
environment = EvaluationEnvironment({}, {}, {})
|
|
89
|
+
for constraint_id, expression, message in constraints:
|
|
90
|
+
result = expression.evaluate(environment)
|
|
91
|
+
if type(result) is not bool:
|
|
92
|
+
raise ConstraintDefinitionError(f"constraint {constraint_id}: check must return boolean")
|
|
93
|
+
if not result:
|
|
94
|
+
suffix = f": {message}" if message else ""
|
|
95
|
+
raise ConstraintViolation(f"constraint {constraint_id} violated{suffix}")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
from .ids import LogicalID
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Missing:
|
|
13
|
+
__slots__ = ()
|
|
14
|
+
|
|
15
|
+
def __copy__(self) -> _Missing:
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> _Missing:
|
|
19
|
+
memo[id(self)] = self
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def __repr__(self) -> str:
|
|
23
|
+
return "MISSING"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
MISSING = _Missing()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def normalize(value: Any) -> Any:
|
|
30
|
+
if value is MISSING:
|
|
31
|
+
return {"$type": "missing"}
|
|
32
|
+
if value is None:
|
|
33
|
+
return None
|
|
34
|
+
if isinstance(value, bool):
|
|
35
|
+
return value
|
|
36
|
+
if isinstance(value, int):
|
|
37
|
+
return value
|
|
38
|
+
if isinstance(value, Decimal):
|
|
39
|
+
return {"$type": "decimal", "value": str(value)}
|
|
40
|
+
if isinstance(value, str):
|
|
41
|
+
return value
|
|
42
|
+
if isinstance(value, LogicalID):
|
|
43
|
+
return {"$type": "logical-id", "value": value.value}
|
|
44
|
+
if isinstance(value, datetime):
|
|
45
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
46
|
+
raise ValueError("datetime values must be timezone-aware")
|
|
47
|
+
utc = value.astimezone(timezone.utc)
|
|
48
|
+
return {"$type": "datetime", "value": utc.isoformat(timespec="microseconds")}
|
|
49
|
+
if isinstance(value, timedelta):
|
|
50
|
+
total_micros = ((value.days * 86400 + value.seconds) * 1_000_000 + value.microseconds)
|
|
51
|
+
return {"$type": "duration-microseconds", "value": total_micros}
|
|
52
|
+
if isinstance(value, (list, tuple)):
|
|
53
|
+
return [normalize(item) for item in value]
|
|
54
|
+
if isinstance(value, Mapping):
|
|
55
|
+
if not all(isinstance(key, str) for key in value):
|
|
56
|
+
raise TypeError("semantic mappings require string keys")
|
|
57
|
+
return {key: normalize(value[key]) for key in sorted(value)}
|
|
58
|
+
raise TypeError(f"unsupported semantic value: {type(value).__name__}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def canonical_bytes(value: Any) -> bytes:
|
|
62
|
+
return json.dumps(normalize(value), sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def fingerprint(value: Any) -> str:
|
|
66
|
+
return hashlib.sha256(canonical_bytes(value)).hexdigest()
|