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.
Files changed (46) hide show
  1. deterministic_scenario_engine-1.0.0.dist-info/METADATA +146 -0
  2. deterministic_scenario_engine-1.0.0.dist-info/RECORD +46 -0
  3. deterministic_scenario_engine-1.0.0.dist-info/WHEEL +5 -0
  4. deterministic_scenario_engine-1.0.0.dist-info/entry_points.txt +2 -0
  5. deterministic_scenario_engine-1.0.0.dist-info/licenses/LICENSE +201 -0
  6. deterministic_scenario_engine-1.0.0.dist-info/top_level.txt +1 -0
  7. scenario_engine/__init__.py +41 -0
  8. scenario_engine/_version.py +3 -0
  9. scenario_engine/adapters/__init__.py +5 -0
  10. scenario_engine/adapters/json_file.py +58 -0
  11. scenario_engine/adapters/sqlalchemy.py +207 -0
  12. scenario_engine/address.py +37 -0
  13. scenario_engine/artifacts.py +16 -0
  14. scenario_engine/canonical.py +109 -0
  15. scenario_engine/clock.py +22 -0
  16. scenario_engine/context.py +25 -0
  17. scenario_engine/control_flow.py +63 -0
  18. scenario_engine/dsl/__init__.py +31 -0
  19. scenario_engine/dsl/compiler.py +232 -0
  20. scenario_engine/dsl/errors.py +23 -0
  21. scenario_engine/dsl/models.py +74 -0
  22. scenario_engine/dsl/parser.py +582 -0
  23. scenario_engine/dsl/runtime.py +211 -0
  24. scenario_engine/errors.py +13 -0
  25. scenario_engine/expressions.py +330 -0
  26. scenario_engine/faults.py +36 -0
  27. scenario_engine/history.py +38 -0
  28. scenario_engine/ids.py +36 -0
  29. scenario_engine/integrations/__init__.py +7 -0
  30. scenario_engine/integrations/hypothesis.py +103 -0
  31. scenario_engine/integrations/schemathesis.py +142 -0
  32. scenario_engine/invariants.py +21 -0
  33. scenario_engine/manifest.py +102 -0
  34. scenario_engine/oracle.py +29 -0
  35. scenario_engine/plugins.py +175 -0
  36. scenario_engine/provenance.py +27 -0
  37. scenario_engine/pytest_plugin.py +95 -0
  38. scenario_engine/reference_packs/__init__.py +1 -0
  39. scenario_engine/reference_packs/ecommerce.py +57 -0
  40. scenario_engine/resources.py +163 -0
  41. scenario_engine/result.py +99 -0
  42. scenario_engine/rng.py +36 -0
  43. scenario_engine/runner.py +103 -0
  44. scenario_engine/state.py +39 -0
  45. scenario_engine/validation.py +95 -0
  46. 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()