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,207 @@
1
+ """Narrow, insert-only SQLAlchemy Core materialization adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timedelta
8
+ from decimal import Decimal
9
+ from types import MappingProxyType
10
+ from typing import Any, Mapping
11
+
12
+ from scenario_engine.errors import ScenarioEngineError
13
+
14
+ try:
15
+ from sqlalchemy import Table
16
+ from sqlalchemy.engine import Engine
17
+ except ImportError as exc: # pragma: no cover - exercised in an isolated process
18
+ raise ImportError(
19
+ "scenario_engine.adapters.sqlalchemy requires the optional "
20
+ "'SQLAlchemy>=2.0,<3' dependency"
21
+ ) from exc
22
+
23
+ from scenario_engine.ids import LogicalID
24
+ from scenario_engine.result import ScenarioResult
25
+ from scenario_engine.values import MISSING, fingerprint
26
+
27
+
28
+ class SqlAlchemyMaterializerError(ScenarioEngineError, ValueError):
29
+ """Base class for deterministic adapter errors."""
30
+
31
+
32
+ class InvalidRowArtifactError(SqlAlchemyMaterializerError):
33
+ pass
34
+
35
+
36
+ class UnknownTableBindingError(SqlAlchemyMaterializerError):
37
+ pass
38
+
39
+
40
+ class InvalidTableBindingError(SqlAlchemyMaterializerError):
41
+ pass
42
+
43
+
44
+ class InvalidColumnBindingError(SqlAlchemyMaterializerError):
45
+ pass
46
+
47
+
48
+ class MissingPrimaryKeyError(SqlAlchemyMaterializerError):
49
+ pass
50
+
51
+
52
+ class UnsupportedMaterializedValueError(SqlAlchemyMaterializerError):
53
+ pass
54
+
55
+
56
+ class MaterializationExecutionError(SqlAlchemyMaterializerError):
57
+ pass
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class SqlAlchemyRowCommand:
62
+ artifact_index: int
63
+ table_name: str
64
+ values: Mapping[str, Any]
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class MaterializationReport:
69
+ commands_attempted: int
70
+ rows_inserted: int
71
+ per_table_counts: Mapping[str, int]
72
+ command_fingerprint: str
73
+
74
+
75
+ def _freeze_values(values: Mapping[str, Any]) -> Mapping[str, Any]:
76
+ return MappingProxyType(dict(values))
77
+
78
+
79
+ def extract_row_commands(result: ScenarioResult) -> tuple[SqlAlchemyRowCommand, ...]:
80
+ """Extract validated-shape row commands in committed artifact order."""
81
+ if not isinstance(result, ScenarioResult):
82
+ raise TypeError("result must be a ScenarioResult")
83
+ commands = []
84
+ for artifact_index, artifact in enumerate(result.artifacts):
85
+ if artifact.artifact_type != "sqlalchemy_row":
86
+ continue
87
+ payload = artifact.value
88
+ if not isinstance(payload, Mapping) or set(payload) != {"table", "values"}:
89
+ raise InvalidRowArtifactError(
90
+ f"sqlalchemy_row artifact {artifact_index} payload must contain exactly table and values"
91
+ )
92
+ table_name, values = payload["table"], payload["values"]
93
+ if not isinstance(table_name, str) or not table_name:
94
+ raise InvalidRowArtifactError(
95
+ f"sqlalchemy_row artifact {artifact_index} table must be a non-empty string"
96
+ )
97
+ if not isinstance(values, Mapping) or not values or not all(
98
+ isinstance(name, str) and name for name in values
99
+ ):
100
+ raise InvalidRowArtifactError(
101
+ f"sqlalchemy_row artifact {artifact_index} values must be a non-empty string-keyed mapping"
102
+ )
103
+ commands.append(SqlAlchemyRowCommand(artifact_index, table_name, _freeze_values(values)))
104
+ return tuple(commands)
105
+
106
+
107
+ def _prepare_value(value: Any, command_index: int, column_name: str) -> Any:
108
+ if value is MISSING:
109
+ raise UnsupportedMaterializedValueError(
110
+ f"command {command_index} column {column_name}: MISSING is not SQL NULL"
111
+ )
112
+ if isinstance(value, LogicalID):
113
+ return value.value
114
+ if value is None or isinstance(value, (bool, int, Decimal, str, timedelta)):
115
+ return value
116
+ if isinstance(value, datetime):
117
+ if value.tzinfo is None or value.utcoffset() is None:
118
+ raise UnsupportedMaterializedValueError(
119
+ f"command {command_index} column {column_name}: datetime must be timezone-aware"
120
+ )
121
+ return value
122
+ raise UnsupportedMaterializedValueError(
123
+ f"command {command_index} column {column_name}: unsupported {type(value).__name__} value"
124
+ )
125
+
126
+
127
+ def prepare_row_commands(
128
+ commands: tuple[SqlAlchemyRowCommand, ...], tables: Mapping[str, Table]
129
+ ) -> tuple[tuple[SqlAlchemyRowCommand, Table, Mapping[str, Any]], ...]:
130
+ """Purely prevalidate and prepare every command before transaction entry."""
131
+ prepared = []
132
+ for command_index, command in enumerate(commands):
133
+ if command.table_name not in tables:
134
+ raise UnknownTableBindingError(
135
+ f"command {command_index}: unknown table binding {command.table_name}"
136
+ )
137
+ table = tables[command.table_name]
138
+ if not isinstance(table, Table):
139
+ raise InvalidTableBindingError(
140
+ f"command {command_index}: binding {command.table_name} is not a SQLAlchemy Table"
141
+ )
142
+ unknown = sorted(set(command.values) - set(table.c.keys()))
143
+ if unknown:
144
+ raise InvalidColumnBindingError(
145
+ f"command {command_index} table {command.table_name}: unknown columns {','.join(unknown)}"
146
+ )
147
+ primary_keys = tuple(column.name for column in table.primary_key.columns)
148
+ if not primary_keys:
149
+ raise MissingPrimaryKeyError(
150
+ f"command {command_index} table {command.table_name}: target table has no primary key"
151
+ )
152
+ missing = tuple(name for name in primary_keys if name not in command.values)
153
+ if missing:
154
+ raise MissingPrimaryKeyError(
155
+ f"command {command_index} table {command.table_name}: missing primary key columns {','.join(missing)}"
156
+ )
157
+ values = {name: _prepare_value(command.values[name], command_index, name)
158
+ for name in sorted(command.values)}
159
+ prepared.append((command, table, MappingProxyType(values)))
160
+ return tuple(prepared)
161
+
162
+
163
+ def command_fingerprint(commands: tuple[SqlAlchemyRowCommand, ...]) -> str:
164
+ intent = [{
165
+ "artifact_index": command.artifact_index,
166
+ "table": command.table_name,
167
+ "values": dict(command.values),
168
+ } for command in commands]
169
+ return fingerprint(intent)
170
+
171
+
172
+ def materialize_result(
173
+ engine: Engine, result: ScenarioResult, tables: Mapping[str, Table]
174
+ ) -> MaterializationReport:
175
+ """Prevalidate all commands, then insert all rows in one transaction."""
176
+ if not isinstance(engine, Engine):
177
+ raise TypeError("engine must be a SQLAlchemy Engine")
178
+ commands = extract_row_commands(result)
179
+ digest = command_fingerprint(commands)
180
+ if not commands:
181
+ return MaterializationReport(0, 0, MappingProxyType({}), digest)
182
+ prepared = prepare_row_commands(commands, tables)
183
+ try:
184
+ with engine.begin() as connection:
185
+ for command_index, (command, table, values) in enumerate(prepared):
186
+ try:
187
+ connection.execute(table.insert().values(**dict(values)))
188
+ except Exception as exc:
189
+ raise MaterializationExecutionError(
190
+ f"command {command_index} table {command.table_name}: {type(exc).__name__}"
191
+ ) from exc
192
+ except MaterializationExecutionError:
193
+ raise
194
+ counts = Counter(command.table_name for command in commands)
195
+ return MaterializationReport(
196
+ len(commands), len(commands),
197
+ MappingProxyType({name: counts[name] for name in sorted(counts)}), digest,
198
+ )
199
+
200
+
201
+ __all__ = [
202
+ "InvalidColumnBindingError", "InvalidRowArtifactError", "InvalidTableBindingError",
203
+ "MaterializationExecutionError", "MaterializationReport", "MissingPrimaryKeyError",
204
+ "SqlAlchemyMaterializerError", "SqlAlchemyRowCommand", "UnknownTableBindingError",
205
+ "UnsupportedMaterializedValueError", "command_fingerprint", "extract_row_commands",
206
+ "materialize_result", "prepare_row_commands",
207
+ ]
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, replace
4
+ import json
5
+
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class ExecutionAddress:
9
+ scenario_id: str
10
+ run_index: int = 0
11
+ subflow_invocations: tuple[int, ...] = ()
12
+ repetition_indexes: tuple[int, ...] = ()
13
+ step_id: str | None = None
14
+ semantic_path: tuple[str, ...] = ()
15
+
16
+ def for_step(self, step_id: str) -> ExecutionAddress:
17
+ return replace(self, step_id=step_id, semantic_path=())
18
+
19
+ def child(self, *path: str) -> ExecutionAddress:
20
+ return replace(self, semantic_path=self.semantic_path + tuple(path))
21
+
22
+ def with_subflow_invocation(self, index: int) -> ExecutionAddress:
23
+ return replace(self, subflow_invocations=self.subflow_invocations + (index,))
24
+
25
+ def with_repetition(self, index: int) -> ExecutionAddress:
26
+ return replace(self, repetition_indexes=self.repetition_indexes + (index,))
27
+
28
+ def canonical(self) -> str:
29
+ value = {
30
+ "path": list(self.semantic_path),
31
+ "repetitions": list(self.repetition_indexes),
32
+ "run": self.run_index,
33
+ "scenario": self.scenario_id,
34
+ "step": self.step_id,
35
+ "subflows": list(self.subflow_invocations),
36
+ }
37
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from .address import ExecutionAddress
7
+ from .ids import LogicalID
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class GeneratedArtifact:
12
+ artifact_type: str
13
+ name: str
14
+ value: Any
15
+ logical_id: LogicalID
16
+ address: ExecutionAddress
@@ -0,0 +1,109 @@
1
+ """Canonical semantic representation for validated Phase 0.1 DSL documents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from typing import TYPE_CHECKING, Any, Mapping
8
+
9
+ from .values import normalize
10
+
11
+ if TYPE_CHECKING:
12
+ from .dsl.models import CompiledScenario, ScenarioDocument, StepDocument
13
+
14
+
15
+ _SEMANTIC_WRAPPERS = {"$decimal", "$datetime", "$duration", "$missing"}
16
+
17
+
18
+ def _canonical_node(value: Any) -> Any:
19
+ if isinstance(value, Mapping):
20
+ if len(value) == 1 and next(iter(value)) in _SEMANTIC_WRAPPERS:
21
+ from .dsl.parser import decode_semantic_value
22
+
23
+ return decode_semantic_value(value)
24
+ if len(value) == 1 and "$literal" in value:
25
+ from .dsl.parser import decode_semantic_value
26
+
27
+ return {"$literal": decode_semantic_value(value["$literal"])}
28
+ return {key: _canonical_node(value[key]) for key in sorted(value)}
29
+ if isinstance(value, (list, tuple)):
30
+ return [_canonical_node(item) for item in value]
31
+ return value
32
+
33
+
34
+ def _step_payload(step: StepDocument) -> Mapping[str, Any]:
35
+ payload = {
36
+ "advance": step.advance,
37
+ "derive": _canonical_node(step.derive),
38
+ "emit": _canonical_node(step.emit),
39
+ "generate": _canonical_node(step.generate),
40
+ "id": step.step_id,
41
+ "transition": step.transition,
42
+ "write": _canonical_node(step.write),
43
+ }
44
+ if step.call is not None: payload["call"] = _canonical_node(step.call)
45
+ if step.branch is not None: payload["branch"] = _canonical_node(step.branch)
46
+ if step.repeat is not None: payload["repeat"] = _canonical_node(step.repeat)
47
+ return payload
48
+
49
+
50
+ def _document(value: str | ScenarioDocument | CompiledScenario) -> ScenarioDocument:
51
+ from .dsl.models import CompiledScenario, ScenarioDocument
52
+ from .dsl.parser import parse_yaml
53
+
54
+ if isinstance(value, str):
55
+ return parse_yaml(value)
56
+ if isinstance(value, CompiledScenario):
57
+ return value.document
58
+ if isinstance(value, ScenarioDocument):
59
+ return value
60
+ raise TypeError("scenario must be YAML text, ScenarioDocument, or CompiledScenario")
61
+
62
+
63
+ def canonical_scenario_payload(
64
+ scenario: str | ScenarioDocument | CompiledScenario,
65
+ ) -> Mapping[str, Any]:
66
+ """Return the JSON-compatible semantic payload of a validated scenario."""
67
+ document = _document(scenario)
68
+ payload = {
69
+ "clock": {"start": document.reference_clock_start},
70
+ "dsl_version": document.dsl_version,
71
+ "initial_state": document.initial_state,
72
+ "scenario": document.scenario_id,
73
+ "steps": [_step_payload(step) for step in document.steps],
74
+ }
75
+ if document.resources:
76
+ payload["resources"] = _canonical_node(document.resources)
77
+ if document.validators:
78
+ payload["validators"] = _canonical_node(document.validators)
79
+ if document.constraints:
80
+ payload["constraints"] = _canonical_node(document.constraints)
81
+ if document.subflows:
82
+ payload["subflows"] = {
83
+ name: {"steps": [_step_payload(step) for step in document.subflows[name]]}
84
+ for name in sorted(document.subflows)
85
+ }
86
+ if document.invariants:
87
+ payload["invariants"] = _canonical_node(document.invariants)
88
+ if document.faults:
89
+ payload["faults"] = _canonical_node(document.faults)
90
+ if document.oracle is not None:
91
+ payload["oracle"] = _canonical_node(document.oracle)
92
+ return normalize(payload)
93
+
94
+
95
+ def canonical_scenario_bytes(
96
+ scenario: str | ScenarioDocument | CompiledScenario,
97
+ ) -> bytes:
98
+ return json.dumps(
99
+ canonical_scenario_payload(scenario),
100
+ ensure_ascii=False,
101
+ separators=(",", ":"),
102
+ sort_keys=True,
103
+ ).encode("utf-8")
104
+
105
+
106
+ def canonical_scenario_hash(
107
+ scenario: str | ScenarioDocument | CompiledScenario,
108
+ ) -> str:
109
+ return hashlib.sha256(canonical_scenario_bytes(scenario)).hexdigest()
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+
6
+ class LogicalClock:
7
+ def __init__(self, reference: datetime):
8
+ if reference.tzinfo is None or reference.utcoffset() is None:
9
+ raise ValueError("logical clock reference must be timezone-aware")
10
+ self._current = reference.astimezone(timezone.utc)
11
+
12
+ @property
13
+ def current(self) -> datetime:
14
+ return self._current
15
+
16
+ def prospective(self, advance: timedelta) -> datetime:
17
+ return self._current + advance
18
+
19
+ def commit(self, timestamp: datetime) -> None:
20
+ if timestamp.tzinfo is None or timestamp.utcoffset() is None:
21
+ raise ValueError("committed timestamp must be timezone-aware")
22
+ self._current = timestamp.astimezone(timezone.utc)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .address import ExecutionAddress
6
+ from .clock import LogicalClock
7
+ from .ids import DeterministicIDProvider, LogicalID
8
+ from .rng import DeterministicRNG
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class GenerationContext:
13
+ root_seed: str | int
14
+ address: ExecutionAddress
15
+ clock: LogicalClock
16
+ ids: DeterministicIDProvider
17
+
18
+ def rng(self) -> DeterministicRNG:
19
+ return DeterministicRNG(self.root_seed, self.address)
20
+
21
+ def logical_id(self, slot: str) -> LogicalID:
22
+ return self.ids.derive(self.address, slot)
23
+
24
+ def at(self, address: ExecutionAddress) -> GenerationContext:
25
+ return GenerationContext(self.root_seed, address, self.clock, self.ids)
@@ -0,0 +1,63 @@
1
+ """Deterministic, bounded structured-control primitives for Phase 0.4."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from copy import deepcopy
7
+ from types import MappingProxyType
8
+ from typing import Any, Mapping
9
+
10
+ from .errors import ScenarioEngineError
11
+ from .expressions import EvaluationEnvironment
12
+
13
+ MAX_REPEAT_COUNT = 100
14
+
15
+
16
+ class ControlFlowError(ScenarioEngineError, ValueError):
17
+ pass
18
+
19
+
20
+ class UnknownSubflowError(ControlFlowError):
21
+ pass
22
+
23
+
24
+ class SubflowCycleError(ControlFlowError):
25
+ pass
26
+
27
+
28
+ class BranchConditionError(ControlFlowError):
29
+ pass
30
+
31
+
32
+ class RepeatCountError(ControlFlowError):
33
+ pass
34
+
35
+
36
+ class RepeatLimitError(ControlFlowError):
37
+ pass
38
+
39
+
40
+ def invocation_component(control_id: str, subflow: str) -> int:
41
+ """Return stable call-site identity in the reserved integer address dimension."""
42
+ payload = f"phase0.4\0{control_id}\0{subflow}".encode("utf-8")
43
+ return int.from_bytes(hashlib.sha256(payload).digest()[:8], "big")
44
+
45
+
46
+ def immutable_scope(values: Mapping[str, Any]) -> Mapping[str, Any]:
47
+ return MappingProxyType(deepcopy(dict(values)))
48
+
49
+
50
+ def evaluate_bindings(bindings, compile_expression, state, resources, scope):
51
+ env = EvaluationEnvironment(state, MappingProxyType({}), MappingProxyType({}), scope)
52
+ return immutable_scope({
53
+ name: compile_expression(bindings[name], resources).evaluate(env)
54
+ for name in sorted(bindings)
55
+ })
56
+
57
+
58
+ def validate_repeat_count(value: Any, maximum: int) -> int:
59
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
60
+ raise RepeatCountError("repeat count must be a nonnegative integer, not boolean")
61
+ if value > maximum:
62
+ raise RepeatLimitError(f"repeat count {value} exceeds declared max {maximum}")
63
+ return value
@@ -0,0 +1,31 @@
1
+ """Phase 0.1B minimal, strictly validated linear declarative DSL."""
2
+
3
+ from .compiler import compile_document
4
+ from .errors import (
5
+ DSLCompilationError, DSLError, DSLParseError, DSLSchemaError,
6
+ UnsupportedDSLVersionError,
7
+ )
8
+ from .models import CompiledScenario, ScenarioDocument
9
+ from .parser import decode_semantic_value, parse_yaml, parse_yaml_file
10
+ from .runtime import ScenarioResult, evaluate_scenario, replay_scenario, run_scenario
11
+ from scenario_engine.invariants import InvariantDefinitionError, InvariantViolation
12
+ from scenario_engine.oracle import OracleEvaluation, OracleMismatchError, OracleReport
13
+ from scenario_engine.resources import ResourceCycleError, ResourceResolutionError, ResolvedResources, resolve_resources
14
+ from scenario_engine.validation import ConstraintDefinitionError, ConstraintViolation, ResourceValidationError
15
+ from scenario_engine.control_flow import (BranchConditionError, ControlFlowError,
16
+ RepeatCountError, RepeatLimitError, SubflowCycleError, UnknownSubflowError)
17
+ from scenario_engine.expressions import ScopeResolutionError
18
+ from scenario_engine.plugins import PluginCompatibilityError
19
+
20
+ __all__ = [
21
+ "CompiledScenario", "DSLCompilationError", "DSLError", "DSLParseError",
22
+ "DSLSchemaError", "ScenarioDocument", "ScenarioResult",
23
+ "UnsupportedDSLVersionError", "compile_document", "decode_semantic_value",
24
+ "parse_yaml", "parse_yaml_file", "replay_scenario", "run_scenario", "evaluate_scenario",
25
+ "ResourceCycleError", "ResourceResolutionError", "ResolvedResources", "resolve_resources",
26
+ "ConstraintDefinitionError", "ConstraintViolation", "ResourceValidationError",
27
+ "BranchConditionError", "ControlFlowError", "RepeatCountError", "RepeatLimitError",
28
+ "ScopeResolutionError", "SubflowCycleError", "UnknownSubflowError",
29
+ "InvariantDefinitionError", "InvariantViolation", "OracleEvaluation", "OracleMismatchError", "OracleReport",
30
+ "PluginCompatibilityError",
31
+ ]